@fieldwangai/agentflow 0.1.29 → 0.1.31
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/agents/agentflow-node-executor-code.md +3 -2
- package/agents/agentflow-node-executor-planning.md +3 -2
- package/agents/agentflow-node-executor-requirement.md +3 -2
- package/agents/agentflow-node-executor-test.md +3 -2
- package/agents/agentflow-node-executor-ui.md +3 -2
- package/agents/agentflow-node-executor.md +3 -2
- package/agents/en/agentflow-node-executor.md +3 -2
- package/agents/zh/agentflow-node-executor.md +3 -2
- package/bin/lib/agent-runners.mjs +63 -14
- package/bin/lib/api-runner.mjs +30 -4
- package/bin/lib/apply.mjs +6 -5
- package/bin/lib/auth.mjs +240 -0
- package/bin/lib/catalog-agents.mjs +2 -2
- package/bin/lib/catalog-flows.mjs +196 -17
- package/bin/lib/composer-agent.mjs +22 -1
- package/bin/lib/composer-skill-router.mjs +10 -78
- package/bin/lib/flow-import.mjs +2 -2
- package/bin/lib/flow-write.mjs +20 -20
- package/bin/lib/help.mjs +2 -2
- package/bin/lib/locales/en.json +29 -1
- package/bin/lib/locales/zh.json +31 -3
- package/bin/lib/main.mjs +6 -1
- package/bin/lib/node-exec-context.mjs +5 -5
- package/bin/lib/node-execute.mjs +15 -10
- package/bin/lib/paths.mjs +69 -13
- package/bin/lib/recent-runs.mjs +2 -2
- package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
- package/bin/lib/runtime-context.mjs +225 -0
- package/bin/lib/scheduler.mjs +42 -38
- package/bin/lib/skill-registry.mjs +145 -0
- package/bin/lib/ui-server.mjs +1517 -57
- package/bin/lib/user-env.mjs +83 -0
- package/bin/lib/workspace-tree.mjs +4 -3
- package/bin/lib/workspace.mjs +9 -11
- package/bin/pipeline/build-node-prompt.mjs +29 -4
- package/bin/pipeline/get-env.mjs +5 -29
- package/bin/pipeline/get-exec-id.mjs +2 -2
- package/bin/pipeline/get-resolved-values.mjs +1 -0
- package/bin/pipeline/pre-process-node.mjs +328 -6
- package/bin/pipeline/run-tool-nodejs.mjs +7 -0
- package/bin/pipeline/validate-flow.mjs +2 -0
- package/builtin/nodes/agent_subAgent.md +12 -3
- package/builtin/nodes/control_cd_workspace.md +45 -0
- package/builtin/nodes/control_load_skills.md +50 -0
- package/builtin/nodes/control_user_workspace.md +20 -0
- package/builtin/nodes/display_ascii.md +22 -0
- package/builtin/nodes/display_markdown.md +22 -0
- package/builtin/nodes/display_mermaid.md +22 -0
- package/builtin/nodes/tool_git_checkout.md +57 -0
- package/builtin/nodes/tool_nodejs.md +8 -1
- package/builtin/nodes/tool_print.md +4 -1
- package/builtin/web-ui/dist/assets/index-BVWwQpvg.css +1 -0
- package/builtin/web-ui/dist/assets/index-CvNy1n3f.js +197 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-flow-recipes/SKILL.md +24 -0
- package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
- package/skills/agentflow-node-reference/SKILL.md +25 -0
- package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
- package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
- package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
- package/skills/agentflow-runtime-reference/SKILL.md +25 -0
- package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
- package/skills/agentflow-workspace-ascii/SKILL.md +42 -0
- package/skills/agentflow-workspace-graph/SKILL.md +67 -0
- package/skills/agentflow-workspace-markdown/SKILL.md +44 -0
- package/skills/agentflow-workspace-mermaid/SKILL.md +43 -0
- package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
- package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -7,11 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import http from "http";
|
|
10
|
+
import os from "os";
|
|
10
11
|
import path from "path";
|
|
11
|
-
import { spawn } from "child_process";
|
|
12
|
+
import { execFile, spawn } from "child_process";
|
|
12
13
|
import busboy from "busboy";
|
|
13
14
|
import { log } from "./log.mjs";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getFlowYamlAbs,
|
|
17
|
+
listFlowsJson,
|
|
18
|
+
listNodesJson,
|
|
19
|
+
readFlowJson,
|
|
20
|
+
readNodeDetailJson,
|
|
21
|
+
readNodeFilePreview,
|
|
22
|
+
} from "./catalog-flows.mjs";
|
|
15
23
|
import {
|
|
16
24
|
FLOW_YAML_FILENAME,
|
|
17
25
|
archiveFlowPipeline,
|
|
@@ -33,6 +41,7 @@ import { t } from "./i18n.mjs";
|
|
|
33
41
|
import {
|
|
34
42
|
PACKAGE_ROOT,
|
|
35
43
|
getAgentflowUserConfigAbs,
|
|
44
|
+
getAgentflowUserDataRoot,
|
|
36
45
|
getModelListsAbs,
|
|
37
46
|
getRunDir,
|
|
38
47
|
} from "./paths.mjs";
|
|
@@ -42,6 +51,7 @@ import {
|
|
|
42
51
|
loadResourcesForIntents,
|
|
43
52
|
loadResourcesForSkillKeys,
|
|
44
53
|
listComposerSkills,
|
|
54
|
+
readComposerSkillDetail,
|
|
45
55
|
buildSkillInjectionBlock,
|
|
46
56
|
buildSkillCompactInjectionBlock,
|
|
47
57
|
} from "./composer-skill-router.mjs";
|
|
@@ -65,6 +75,15 @@ import { runNodeScript } from "./pipeline-scripts.mjs";
|
|
|
65
75
|
import { readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
|
|
66
76
|
import { listScheduleStatuses } from "./scheduler.mjs";
|
|
67
77
|
import { installFlowDependency, listMarketplacePackages, publishNodeFromInstance } from "./marketplace.mjs";
|
|
78
|
+
import {
|
|
79
|
+
authSetupRequired,
|
|
80
|
+
buildClearSessionCookie,
|
|
81
|
+
buildSessionCookie,
|
|
82
|
+
getAuthUserFromRequest,
|
|
83
|
+
loginOrCreateUser,
|
|
84
|
+
logoutRequest,
|
|
85
|
+
} from "./auth.mjs";
|
|
86
|
+
import { readUserEnvObject, readUserEnvRows, writeUserEnvRows } from "./user-env.mjs";
|
|
68
87
|
|
|
69
88
|
const MIME = {
|
|
70
89
|
".html": "text/html; charset=utf-8",
|
|
@@ -76,6 +95,49 @@ const MIME = {
|
|
|
76
95
|
};
|
|
77
96
|
|
|
78
97
|
const RUN_CONFIG_FILENAME = "run-config.json";
|
|
98
|
+
const SKILL_COLLECTIONS_FILENAME = "skill-collections.json";
|
|
99
|
+
const BUILTIN_SKILL_COLLECTIONS = [
|
|
100
|
+
{
|
|
101
|
+
id: "pipeline",
|
|
102
|
+
name: "Pipeline",
|
|
103
|
+
defaultKeys: [
|
|
104
|
+
"agentflow-flow-add-instances",
|
|
105
|
+
"agentflow-flow-edit-node-fields",
|
|
106
|
+
"agentflow-flow-recipes",
|
|
107
|
+
"agentflow-flow-sync-ui",
|
|
108
|
+
"agentflow-node-reference",
|
|
109
|
+
"agentflow-placeholder-reference",
|
|
110
|
+
"agentflow-runtime-reference",
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "workspace",
|
|
115
|
+
name: "Workspace",
|
|
116
|
+
defaultKeys: [
|
|
117
|
+
"agentflow-workspace-graph",
|
|
118
|
+
"agentflow-workspace-markdown",
|
|
119
|
+
"agentflow-workspace-mermaid",
|
|
120
|
+
"agentflow-workspace-ascii",
|
|
121
|
+
"agentflow-node-reference",
|
|
122
|
+
"agentflow-placeholder-reference",
|
|
123
|
+
"agentflow-runtime-reference",
|
|
124
|
+
],
|
|
125
|
+
legacyDefaultKeys: [
|
|
126
|
+
[
|
|
127
|
+
"agentflow-flow-add-instances",
|
|
128
|
+
"agentflow-flow-edit-node-fields",
|
|
129
|
+
"agentflow-node-reference",
|
|
130
|
+
"agentflow-placeholder-reference",
|
|
131
|
+
"agentflow-runtime-reference",
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
"agentflow-node-reference",
|
|
135
|
+
"agentflow-placeholder-reference",
|
|
136
|
+
"agentflow-runtime-reference",
|
|
137
|
+
],
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
];
|
|
79
141
|
|
|
80
142
|
function json(res, status, obj) {
|
|
81
143
|
const body = JSON.stringify(obj);
|
|
@@ -86,6 +148,155 @@ function json(res, status, obj) {
|
|
|
86
148
|
res.end(body);
|
|
87
149
|
}
|
|
88
150
|
|
|
151
|
+
function skillCollectionsAbs(userCtx = {}) {
|
|
152
|
+
return path.join(getAgentflowUserDataRoot(userCtx.userId), SKILL_COLLECTIONS_FILENAME);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function slugifySkillCollectionId(name, fallback = "collection") {
|
|
156
|
+
const raw = String(name || "").trim().toLowerCase();
|
|
157
|
+
const id = raw
|
|
158
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
159
|
+
.replace(/^-+|-+$/g, "")
|
|
160
|
+
.slice(0, 48);
|
|
161
|
+
return id || fallback;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isBuiltinSkillCollectionId(id) {
|
|
165
|
+
return BUILTIN_SKILL_COLLECTIONS.some((collection) => collection.id === id);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveSkillKeys(skillNamesOrKeys = [], availableSkills = []) {
|
|
169
|
+
const byToken = buildSkillKeyLookup(availableSkills);
|
|
170
|
+
return skillNamesOrKeys.map((key) => byToken.get(key)).filter(Boolean);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildSkillKeyLookup(availableSkills = []) {
|
|
174
|
+
const byToken = new Map();
|
|
175
|
+
for (const skill of availableSkills) {
|
|
176
|
+
const key = String(skill?.key || "").trim();
|
|
177
|
+
if (!key) continue;
|
|
178
|
+
for (const token of [skill.key, skill.name, skill.id]) {
|
|
179
|
+
const normalized = String(token || "").trim();
|
|
180
|
+
if (normalized && !byToken.has(normalized)) byToken.set(normalized, key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return byToken;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function defaultSkillKeysForCollection(def, availableSkills = []) {
|
|
187
|
+
const exact = resolveSkillKeys(def.defaultKeys, availableSkills);
|
|
188
|
+
if (exact.length > 0) return exact;
|
|
189
|
+
return availableSkills
|
|
190
|
+
.filter((skill) => String(skill?.name || skill?.id || skill?.key || "").includes("agentflow-"))
|
|
191
|
+
.map((skill) => String(skill.key || "").trim())
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sameSkillKeySet(a = [], b = []) {
|
|
196
|
+
if (a.length !== b.length) return false;
|
|
197
|
+
const set = new Set(a);
|
|
198
|
+
return b.every((key) => set.has(key));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeSkillCollectionConfig(value) {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const seenIds = new Set();
|
|
204
|
+
const collections = [];
|
|
205
|
+
const input = Array.isArray(value?.collections) ? value.collections : [];
|
|
206
|
+
for (const item of input) {
|
|
207
|
+
if (!item || typeof item !== "object") continue;
|
|
208
|
+
const name = String(item.name || item.id || "").trim().slice(0, 80);
|
|
209
|
+
if (!name) continue;
|
|
210
|
+
let id = slugifySkillCollectionId(item.id || name);
|
|
211
|
+
let suffix = 2;
|
|
212
|
+
while (seenIds.has(id)) {
|
|
213
|
+
id = `${slugifySkillCollectionId(item.id || name)}-${suffix++}`;
|
|
214
|
+
}
|
|
215
|
+
seenIds.add(id);
|
|
216
|
+
const skillSeen = new Set();
|
|
217
|
+
const skillKeys = [];
|
|
218
|
+
for (const key of Array.isArray(item.skillKeys) ? item.skillKeys : []) {
|
|
219
|
+
const normalized = String(key || "").trim();
|
|
220
|
+
if (!normalized || skillSeen.has(normalized)) continue;
|
|
221
|
+
skillSeen.add(normalized);
|
|
222
|
+
skillKeys.push(normalized);
|
|
223
|
+
}
|
|
224
|
+
collections.push({
|
|
225
|
+
id,
|
|
226
|
+
name,
|
|
227
|
+
skillKeys,
|
|
228
|
+
builtin: Boolean(item.builtin) || isBuiltinSkillCollectionId(id),
|
|
229
|
+
createdAt: Number.isFinite(item.createdAt) ? Number(item.createdAt) : now,
|
|
230
|
+
updatedAt: Number.isFinite(item.updatedAt) ? Number(item.updatedAt) : now,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return { version: 1, collections };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function withBuiltinSkillCollections(config, availableSkills = []) {
|
|
237
|
+
const normalized = normalizeSkillCollectionConfig(config);
|
|
238
|
+
const byId = new Map(normalized.collections.map((collection) => [collection.id, collection]));
|
|
239
|
+
const out = [];
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
for (const def of BUILTIN_SKILL_COLLECTIONS) {
|
|
242
|
+
const existing = byId.get(def.id);
|
|
243
|
+
if (existing) {
|
|
244
|
+
const nextDefaultKeys = defaultSkillKeysForCollection(def, availableSkills);
|
|
245
|
+
const legacyDefaultSets = (Array.isArray(def.legacyDefaultKeys) ? def.legacyDefaultKeys : [])
|
|
246
|
+
.map((keys) => Array.isArray(keys) ? resolveSkillKeys(keys, availableSkills) : [])
|
|
247
|
+
.filter((keys) => keys.length > 0);
|
|
248
|
+
const shouldMigrateLegacyDefault =
|
|
249
|
+
existing.skillKeys.length > 0 &&
|
|
250
|
+
legacyDefaultSets.some((keys) => sameSkillKeySet(existing.skillKeys, keys));
|
|
251
|
+
out.push({
|
|
252
|
+
...existing,
|
|
253
|
+
name: def.name,
|
|
254
|
+
builtin: true,
|
|
255
|
+
skillKeys: existing.skillKeys.length > 0 && !shouldMigrateLegacyDefault ? existing.skillKeys : nextDefaultKeys,
|
|
256
|
+
});
|
|
257
|
+
byId.delete(def.id);
|
|
258
|
+
} else {
|
|
259
|
+
out.push({
|
|
260
|
+
id: def.id,
|
|
261
|
+
name: def.name,
|
|
262
|
+
builtin: true,
|
|
263
|
+
skillKeys: defaultSkillKeysForCollection(def, availableSkills),
|
|
264
|
+
createdAt: now,
|
|
265
|
+
updatedAt: now,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
out.push(...Array.from(byId.values()).map((collection) => ({ ...collection, builtin: false })));
|
|
270
|
+
return { version: 1, collections: out };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readSkillCollectionConfig(userCtx = {}, availableSkills = []) {
|
|
274
|
+
const p = skillCollectionsAbs(userCtx);
|
|
275
|
+
try {
|
|
276
|
+
if (!fs.existsSync(p)) return withBuiltinSkillCollections({}, availableSkills);
|
|
277
|
+
return withBuiltinSkillCollections(JSON.parse(fs.readFileSync(p, "utf-8")), availableSkills);
|
|
278
|
+
} catch {
|
|
279
|
+
return withBuiltinSkillCollections({}, availableSkills);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function writeSkillCollectionConfig(userCtx = {}, payload = {}, availableSkills = []) {
|
|
284
|
+
const p = skillCollectionsAbs(userCtx);
|
|
285
|
+
const config = withBuiltinSkillCollections(payload, availableSkills);
|
|
286
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
287
|
+
fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
288
|
+
return config;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function runtimeEnvForUser(userCtx = {}, extra = {}) {
|
|
292
|
+
return {
|
|
293
|
+
...process.env,
|
|
294
|
+
...readUserEnvObject(userCtx.userId),
|
|
295
|
+
...extra,
|
|
296
|
+
AGENTFLOW_USER_ID: userCtx.userId || "",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
89
300
|
function readAgentflowUserConfigObject() {
|
|
90
301
|
const p = getAgentflowUserConfigAbs();
|
|
91
302
|
try {
|
|
@@ -123,6 +334,99 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
123
334
|
}
|
|
124
335
|
}
|
|
125
336
|
|
|
337
|
+
const SKILLHUB_TIMEOUT_MS = 60_000;
|
|
338
|
+
|
|
339
|
+
function runSkillhub(args, opts = {}) {
|
|
340
|
+
return new Promise((resolve) => {
|
|
341
|
+
execFile("skillhub", args, {
|
|
342
|
+
cwd: opts.cwd || process.cwd(),
|
|
343
|
+
timeout: opts.timeoutMs || SKILLHUB_TIMEOUT_MS,
|
|
344
|
+
maxBuffer: opts.maxBuffer || 2 * 1024 * 1024,
|
|
345
|
+
env: {
|
|
346
|
+
...process.env,
|
|
347
|
+
FORCE_COLOR: "0",
|
|
348
|
+
},
|
|
349
|
+
}, (error, stdout, stderr) => {
|
|
350
|
+
const out = String(stdout || "");
|
|
351
|
+
const err = String(stderr || "");
|
|
352
|
+
resolve({
|
|
353
|
+
ok: !error,
|
|
354
|
+
code: error && typeof error.code === "number" ? error.code : 0,
|
|
355
|
+
error: error ? (err.trim() || error.message || "skillhub failed") : "",
|
|
356
|
+
stdout: out,
|
|
357
|
+
stderr: err,
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function parseJsonText(text, fallback = null) {
|
|
364
|
+
const s = String(text || "").trim();
|
|
365
|
+
if (!s) return fallback;
|
|
366
|
+
try {
|
|
367
|
+
return JSON.parse(s);
|
|
368
|
+
} catch {
|
|
369
|
+
const match = s.match(/(\{[\s\S]*\}|\[[\s\S]*\])\s*$/);
|
|
370
|
+
if (!match) return fallback;
|
|
371
|
+
try { return JSON.parse(match[1]); } catch { return fallback; }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function normalizeSkillhubSearchPayload(raw) {
|
|
376
|
+
const data = raw && typeof raw === "object" ? raw : {};
|
|
377
|
+
const items = Array.isArray(data.items) ? data.items : Array.isArray(data.results) ? data.results : [];
|
|
378
|
+
return {
|
|
379
|
+
total: Number(data.total) || items.length,
|
|
380
|
+
mode: typeof data.mode === "string" ? data.mode : "",
|
|
381
|
+
degraded: Boolean(data.degraded),
|
|
382
|
+
items: items.map((item) => {
|
|
383
|
+
const x = item && typeof item === "object" ? item : {};
|
|
384
|
+
const id = x.id ?? x.skillId ?? x.skill_id ?? "";
|
|
385
|
+
const slug = String(x.slug ?? x.name ?? x.displayName ?? x.display_name ?? id ?? "").trim();
|
|
386
|
+
return {
|
|
387
|
+
id: String(id || slug),
|
|
388
|
+
slug,
|
|
389
|
+
name: String(x.displayName ?? x.display_name ?? x.name ?? slug),
|
|
390
|
+
summary: String(x.summary ?? x.description ?? ""),
|
|
391
|
+
version: String(x.version ?? x.latestVersion ?? x.latest_version ?? ""),
|
|
392
|
+
tags: Array.isArray(x.tags) ? x.tags.map(String) : [],
|
|
393
|
+
};
|
|
394
|
+
}).filter((x) => x.slug || x.name),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizeSkillhubListPayload(raw) {
|
|
399
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
400
|
+
return arr.map((x) => ({
|
|
401
|
+
name: String(x?.name ?? ""),
|
|
402
|
+
baseDir: String(x?.baseDir ?? ""),
|
|
403
|
+
path: String(x?.path ?? ""),
|
|
404
|
+
kind: String(x?.kind ?? ""),
|
|
405
|
+
agent: String(x?.agent ?? ""),
|
|
406
|
+
})).filter((x) => x.name);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function skillhubInstallArgs(payload, { uninstall = false } = {}) {
|
|
410
|
+
const slug = String(payload?.slug || payload?.name || "").trim();
|
|
411
|
+
if (!slug && !payload?.collection) return null;
|
|
412
|
+
const args = [uninstall ? "uninstall" : "install"];
|
|
413
|
+
if (payload?.collection) {
|
|
414
|
+
args.push("--collection", String(payload.collection).trim());
|
|
415
|
+
} else {
|
|
416
|
+
args.push(slug);
|
|
417
|
+
}
|
|
418
|
+
if (payload?.skillId) args.push("--skill-id", String(payload.skillId).trim());
|
|
419
|
+
const target = String(payload?.target || "project").trim();
|
|
420
|
+
const agent = String(payload?.agent || "codex").trim();
|
|
421
|
+
if (target === "global") {
|
|
422
|
+
args.push("--global", "--agent", agent);
|
|
423
|
+
} else if (payload?.dir) {
|
|
424
|
+
args.push("--dir", String(payload.dir).trim());
|
|
425
|
+
}
|
|
426
|
+
if (payload?.force) args.push("--force");
|
|
427
|
+
return args;
|
|
428
|
+
}
|
|
429
|
+
|
|
126
430
|
function readBody(req) {
|
|
127
431
|
return new Promise((resolve, reject) => {
|
|
128
432
|
const chunks = [];
|
|
@@ -132,6 +436,550 @@ function readBody(req) {
|
|
|
132
436
|
});
|
|
133
437
|
}
|
|
134
438
|
|
|
439
|
+
const WORKSPACE_FILE_SKIP_DIRS = new Set([
|
|
440
|
+
".git",
|
|
441
|
+
"node_modules",
|
|
442
|
+
".next",
|
|
443
|
+
".nuxt",
|
|
444
|
+
".turbo",
|
|
445
|
+
"dist",
|
|
446
|
+
"build",
|
|
447
|
+
"coverage",
|
|
448
|
+
]);
|
|
449
|
+
|
|
450
|
+
const WORKSPACE_TEXT_EXTS = new Set([
|
|
451
|
+
".md",
|
|
452
|
+
".markdown",
|
|
453
|
+
".txt",
|
|
454
|
+
".json",
|
|
455
|
+
".yaml",
|
|
456
|
+
".yml",
|
|
457
|
+
".js",
|
|
458
|
+
".jsx",
|
|
459
|
+
".ts",
|
|
460
|
+
".tsx",
|
|
461
|
+
".css",
|
|
462
|
+
".html",
|
|
463
|
+
".mjs",
|
|
464
|
+
".cjs",
|
|
465
|
+
]);
|
|
466
|
+
|
|
467
|
+
function resolveWorkspaceFilePath(workspaceRoot, relPath) {
|
|
468
|
+
const root = path.resolve(workspaceRoot);
|
|
469
|
+
const rel = String(relPath || "").replace(/^[/\\]+/, "");
|
|
470
|
+
const abs = path.resolve(root, rel);
|
|
471
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
472
|
+
throw new Error("Path traversal not allowed");
|
|
473
|
+
}
|
|
474
|
+
return { root, rel: path.relative(root, abs).replace(/\\/g, "/"), abs };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function workspaceFileIcon(fileName, isDir = false) {
|
|
478
|
+
if (isDir) return "folder";
|
|
479
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
480
|
+
if (ext === ".md" || ext === ".markdown") return "article";
|
|
481
|
+
if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) return "code";
|
|
482
|
+
if ([".yaml", ".yml", ".json"].includes(ext)) return "data_object";
|
|
483
|
+
if (ext === ".css") return "palette";
|
|
484
|
+
if (ext === ".html") return "web";
|
|
485
|
+
return "draft";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
|
|
489
|
+
if (depth > maxDepth || budget.count > 500) return [];
|
|
490
|
+
let entries;
|
|
491
|
+
try {
|
|
492
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
493
|
+
} catch {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
const out = [];
|
|
497
|
+
for (const entry of entries) {
|
|
498
|
+
if (budget.count > 500) break;
|
|
499
|
+
if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
|
|
500
|
+
const abs = path.join(dir, entry.name);
|
|
501
|
+
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
502
|
+
if (entry.isDirectory()) {
|
|
503
|
+
if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
|
|
504
|
+
budget.count++;
|
|
505
|
+
out.push({
|
|
506
|
+
type: "directory",
|
|
507
|
+
name: entry.name,
|
|
508
|
+
path: rel,
|
|
509
|
+
icon: workspaceFileIcon(entry.name, true),
|
|
510
|
+
children: readWorkspaceFilesRecursive(abs, root, depth + 1, maxDepth, budget),
|
|
511
|
+
});
|
|
512
|
+
} else if (entry.isFile()) {
|
|
513
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
514
|
+
if (!WORKSPACE_TEXT_EXTS.has(ext)) continue;
|
|
515
|
+
let size = 0;
|
|
516
|
+
try { size = fs.statSync(abs).size; } catch {}
|
|
517
|
+
budget.count++;
|
|
518
|
+
out.push({ type: "file", name: entry.name, path: rel, icon: workspaceFileIcon(entry.name), size });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
out.sort((a, b) => {
|
|
522
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
523
|
+
return a.name.localeCompare(b.name);
|
|
524
|
+
});
|
|
525
|
+
return out;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function readWorkspaceFiles(workspaceRoot) {
|
|
529
|
+
const root = path.resolve(workspaceRoot);
|
|
530
|
+
return { root, files: readWorkspaceFilesRecursive(root, root) };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const WORKSPACE_GRAPH_FILENAME = "workspace.graph.json";
|
|
534
|
+
|
|
535
|
+
function workspaceGraphPath(workspaceRoot) {
|
|
536
|
+
return path.join(path.resolve(workspaceRoot), WORKSPACE_GRAPH_FILENAME);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function emptyWorkspaceGraph() {
|
|
540
|
+
return { version: 1, instances: {}, edges: [], ui: { nodePositions: {} } };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function readWorkspaceGraph(workspaceRoot) {
|
|
544
|
+
const graphPath = workspaceGraphPath(workspaceRoot);
|
|
545
|
+
if (!fs.existsSync(graphPath)) return { path: graphPath, graph: emptyWorkspaceGraph() };
|
|
546
|
+
const raw = fs.readFileSync(graphPath, "utf-8");
|
|
547
|
+
if (!raw.trim()) return { path: graphPath, graph: emptyWorkspaceGraph() };
|
|
548
|
+
const parsed = JSON.parse(raw);
|
|
549
|
+
const graph = parsed && typeof parsed === "object" ? parsed : {};
|
|
550
|
+
return {
|
|
551
|
+
path: graphPath,
|
|
552
|
+
graph: {
|
|
553
|
+
version: Number(graph.version) || 1,
|
|
554
|
+
instances: graph.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances) ? graph.instances : {},
|
|
555
|
+
edges: Array.isArray(graph.edges) ? graph.edges : [],
|
|
556
|
+
ui: graph.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function normalizeWorkspaceGraphPayload(payload) {
|
|
562
|
+
const graph = payload?.graph && typeof payload.graph === "object" ? payload.graph : payload;
|
|
563
|
+
return {
|
|
564
|
+
version: 1,
|
|
565
|
+
instances: graph?.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances) ? graph.instances : {},
|
|
566
|
+
edges: Array.isArray(graph?.edges) ? graph.edges : [],
|
|
567
|
+
ui: graph?.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
|
|
568
|
+
updatedAt: new Date().toISOString(),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
573
|
+
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
574
|
+
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
575
|
+
const flowSource = params.flowSource != null && String(params.flowSource).trim()
|
|
576
|
+
? String(params.flowSource).trim()
|
|
577
|
+
: "user";
|
|
578
|
+
const archived = params.archived === true || params.archived === "1" || params.flowArchived === true;
|
|
579
|
+
if (!isValidFlowSourceRead(flowSource)) {
|
|
580
|
+
return { root: "", error: "Invalid flowSource" };
|
|
581
|
+
}
|
|
582
|
+
const result = getPipelineFiles(workspaceRoot, flowId, flowSource, archived, opts);
|
|
583
|
+
if (result.error || !result.path) {
|
|
584
|
+
return { root: "", error: result.error || "Pipeline workspace not found" };
|
|
585
|
+
}
|
|
586
|
+
return { root: path.resolve(result.path), flowId, flowSource, archived };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function buildWorkspaceGeneratePrompt(payload) {
|
|
590
|
+
const userPrompt = String(payload?.prompt || "").trim();
|
|
591
|
+
const outputKind = String(payload?.outputKind || payload?.kind || "markdown").trim().toLowerCase();
|
|
592
|
+
const allowFlowYaml = payload?.allowFlowYaml === true || payload?.allowFlowYaml === "1";
|
|
593
|
+
const workspaceGraph = payload?.workspaceGraph && typeof payload.workspaceGraph === "object" ? payload.workspaceGraph : null;
|
|
594
|
+
const selectedNodeIds = Array.isArray(payload?.selectedNodeIds)
|
|
595
|
+
? payload.selectedNodeIds.map((id) => String(id || "").trim()).filter(Boolean)
|
|
596
|
+
: [];
|
|
597
|
+
const skillsBlock = typeof payload?.skillsBlock === "string" ? payload.skillsBlock.trim() : "";
|
|
598
|
+
const contexts = Array.isArray(payload?.contexts) ? payload.contexts : [];
|
|
599
|
+
const contextBlocks = contexts
|
|
600
|
+
.map((ctx, idx) => {
|
|
601
|
+
const title = String(ctx?.title || ctx?.path || `context-${idx + 1}`).trim();
|
|
602
|
+
const kind = String(ctx?.kind || "text").trim();
|
|
603
|
+
const content = String(ctx?.content || "").trim();
|
|
604
|
+
if (!content) return "";
|
|
605
|
+
return `### ${title} (${kind})\n\n${content}`;
|
|
606
|
+
})
|
|
607
|
+
.filter(Boolean)
|
|
608
|
+
.join("\n\n---\n\n");
|
|
609
|
+
const kindInstruction =
|
|
610
|
+
outputKind === "mermaid"
|
|
611
|
+
? [
|
|
612
|
+
"你是 workspace Mermaid 图节点的内容生成器。",
|
|
613
|
+
"请根据用户 prompt 和上游节点/文件上下文生成 Mermaid flowchart 源码。",
|
|
614
|
+
"只输出 Mermaid 源码,不要解释,不要包裹 Markdown 代码围栏。",
|
|
615
|
+
"优先使用 `flowchart TD` 或 `graph TD`,节点 ID 使用简单英文/数字/下划线,节点 label 使用清晰短文本。",
|
|
616
|
+
].join("\n")
|
|
617
|
+
: outputKind === "ascii"
|
|
618
|
+
? [
|
|
619
|
+
"你是 workspace ASCII 图节点的内容生成器。",
|
|
620
|
+
"请根据用户 prompt 和上游节点/文件上下文生成等宽字体下可读的 ASCII 图。",
|
|
621
|
+
"只输出 ASCII 图正文,不要解释,不要包裹 Markdown 代码围栏。",
|
|
622
|
+
"使用 +-|/\\<> 等字符表达结构,尽量保持对齐。",
|
|
623
|
+
].join("\n")
|
|
624
|
+
: [
|
|
625
|
+
"你是 AgentFlow Workspace Composer。",
|
|
626
|
+
"优先根据已选择的 Skills 操作 workspace.graph.json,创建或修改 workspace 画布节点、连线与展示节点。",
|
|
627
|
+
"如果用户请求需要项目分析、加载代码、整理流程或生成展示结果,不要只给泛泛回答;应先让 Skills 驱动画布建模,例如创建 Git/工作目录/Load Skills/Agent/Markdown Display 等合适节点。",
|
|
628
|
+
"只有当用户明确只是询问概念或无需画布变更时,才直接输出 Markdown 回复。",
|
|
629
|
+
].join("\n");
|
|
630
|
+
return [
|
|
631
|
+
"你正在 AgentFlow 的 Workspace 工作画布中执行任务。",
|
|
632
|
+
"Workspace 是当前 pipeline 的临时工作区,用于分析、试验、生成中间文件和展示结果。",
|
|
633
|
+
"Workspace 与 Pipeline 各自有独立的 Skill collection;此处只使用当前 Workspace Composer 选择的 collections / skills 作为本次行为规则与编辑依据。",
|
|
634
|
+
"当 Skills 提到修改 flow.yaml / instances / edges / ui 时,在 Workspace 视图下应映射为修改当前工作区的 workspace.graph.json,除非用户显式勾选并要求修改正式 flow.yaml。",
|
|
635
|
+
"workspace.graph.json 使用 JSON:{ version, instances, edges, ui: { nodePositions, nodeSizes } }。instances 的结构与 flow.yaml instances 一致;edges 使用 source/target/sourceHandle/targetHandle;ui.nodePositions 记录节点坐标。",
|
|
636
|
+
allowFlowYaml
|
|
637
|
+
? "用户已允许你考虑正式 flow.yaml;如需修改仍必须明确说明影响。"
|
|
638
|
+
: "默认不要修改正式 flow.yaml;优先在 workspace 文件、workspace.graph.json 或回复内容中完成任务。",
|
|
639
|
+
workspaceGraph ? `\n## 当前 workspace graph\n\n${JSON.stringify(workspaceGraph, null, 2)}` : "",
|
|
640
|
+
selectedNodeIds.length > 0 ? `\n## 当前用户选中的 workspace 节点\n\n${selectedNodeIds.map((id) => `- ${id}`).join("\n")}` : "",
|
|
641
|
+
skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
|
|
642
|
+
kindInstruction,
|
|
643
|
+
contextBlocks ? `\n## 上下文\n\n${contextBlocks}` : "",
|
|
644
|
+
`\n## 用户 prompt\n\n${userPrompt}`,
|
|
645
|
+
].filter(Boolean).join("\n");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function workspaceSlotValue(slot) {
|
|
649
|
+
if (!slot || typeof slot !== "object") return "";
|
|
650
|
+
for (const key of ["value", "default"]) {
|
|
651
|
+
if (slot[key] != null && String(slot[key]).trim()) return String(slot[key]);
|
|
652
|
+
}
|
|
653
|
+
return "";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function workspaceInstanceText(instance) {
|
|
657
|
+
const body = String(instance?.body || "").trim();
|
|
658
|
+
if (body) return body;
|
|
659
|
+
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
660
|
+
const textSlot = slots.find((slot) => String(slot?.type || "") === "text" && workspaceSlotValue(slot).trim());
|
|
661
|
+
return textSlot ? workspaceSlotValue(textSlot) : "";
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function workspaceDisplayKind(definitionId) {
|
|
665
|
+
const id = String(definitionId || "");
|
|
666
|
+
if (id === "display_markdown") return "markdown";
|
|
667
|
+
if (id === "display_mermaid") return "mermaid";
|
|
668
|
+
if (id === "display_ascii") return "ascii";
|
|
669
|
+
return "";
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function workspaceRunOrder(graph, runNodeId) {
|
|
673
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
674
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
675
|
+
const target = String(runNodeId || "").trim();
|
|
676
|
+
if (!target || !instances[target]) throw new Error("Missing workspace run node");
|
|
677
|
+
const upstream = new Map();
|
|
678
|
+
const downstream = new Map();
|
|
679
|
+
for (const edge of edges) {
|
|
680
|
+
const source = String(edge?.source || "");
|
|
681
|
+
const dest = String(edge?.target || "");
|
|
682
|
+
if (!source || !dest || !instances[source] || !instances[dest]) continue;
|
|
683
|
+
if (!upstream.has(dest)) upstream.set(dest, []);
|
|
684
|
+
upstream.get(dest).push(source);
|
|
685
|
+
if (!downstream.has(source)) downstream.set(source, []);
|
|
686
|
+
downstream.get(source).push(dest);
|
|
687
|
+
}
|
|
688
|
+
const reachable = new Set();
|
|
689
|
+
const visit = (id) => {
|
|
690
|
+
if (!id || reachable.has(id)) return;
|
|
691
|
+
reachable.add(id);
|
|
692
|
+
for (const next of downstream.get(id) || []) visit(next);
|
|
693
|
+
};
|
|
694
|
+
visit(target);
|
|
695
|
+
reachable.delete(target);
|
|
696
|
+
const needed = reachable;
|
|
697
|
+
const indegree = new Map(Array.from(needed).map((id) => [id, 0]));
|
|
698
|
+
for (const id of needed) {
|
|
699
|
+
for (const prev of upstream.get(id) || []) {
|
|
700
|
+
if (needed.has(prev)) indegree.set(id, (indegree.get(id) || 0) + 1);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const ready = Array.from(needed).filter((id) => (indegree.get(id) || 0) === 0);
|
|
704
|
+
const ordered = [];
|
|
705
|
+
while (ready.length) {
|
|
706
|
+
const id = ready.shift();
|
|
707
|
+
ordered.push(id);
|
|
708
|
+
for (const next of downstream.get(id) || []) {
|
|
709
|
+
if (!needed.has(next)) continue;
|
|
710
|
+
const n = (indegree.get(next) || 0) - 1;
|
|
711
|
+
indegree.set(next, n);
|
|
712
|
+
if (n === 0) ready.push(next);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (ordered.length !== needed.size) {
|
|
716
|
+
throw new Error("Workspace run graph contains a cycle");
|
|
717
|
+
}
|
|
718
|
+
return ordered;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function workspaceUpstreamText(graph, nodeId, outputs) {
|
|
722
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
723
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
724
|
+
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
725
|
+
const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
|
|
726
|
+
if (!contentEdge) return "";
|
|
727
|
+
const sourceId = String(contentEdge.source || "");
|
|
728
|
+
const out = outputs.get(sourceId);
|
|
729
|
+
if (out != null && String(out).trim()) return String(out);
|
|
730
|
+
return workspaceInstanceText(instances[sourceId]);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function parseWorkspaceSkillKeys(raw) {
|
|
734
|
+
const text = String(raw || "").trim();
|
|
735
|
+
if (!text) return [];
|
|
736
|
+
try {
|
|
737
|
+
const parsed = JSON.parse(text);
|
|
738
|
+
if (Array.isArray(parsed)) return parsed.map((item) => String(item || "").trim()).filter(Boolean);
|
|
739
|
+
} catch {
|
|
740
|
+
/* plain list fallback */
|
|
741
|
+
}
|
|
742
|
+
return text.split(/[\n,]+/).map((item) => item.trim()).filter(Boolean);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function selectedSkillKeysFromInstance(instance) {
|
|
746
|
+
const bodyKeys = parseWorkspaceSkillKeys(instance?.body || "");
|
|
747
|
+
if (bodyKeys.length > 0) return bodyKeys;
|
|
748
|
+
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
749
|
+
const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
|
|
750
|
+
return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function workspaceUpstreamSkillBlocks(graph, nodeId, outputs) {
|
|
754
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
755
|
+
return edges
|
|
756
|
+
.filter((edge) => String(edge?.target || "") === String(nodeId))
|
|
757
|
+
.map((edge) => String(outputs.get(String(edge.source || "")) || ""))
|
|
758
|
+
.filter((text) => text.includes("##") || text.includes("Skill"))
|
|
759
|
+
.join("\n\n---\n\n");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function workspaceWriteDisplayContent(instance, content) {
|
|
763
|
+
const next = { ...(instance || {}) };
|
|
764
|
+
const text = String(content || "");
|
|
765
|
+
next.body = text;
|
|
766
|
+
next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
|
|
767
|
+
String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
|
|
768
|
+
? { ...slot, default: text, value: text }
|
|
769
|
+
: slot
|
|
770
|
+
));
|
|
771
|
+
next.output = (Array.isArray(next.output) ? next.output : []).map((slot) => (
|
|
772
|
+
String(slot?.name || "") === "content" || String(slot?.type || "") === "text"
|
|
773
|
+
? { ...slot, default: text, value: text }
|
|
774
|
+
: slot
|
|
775
|
+
));
|
|
776
|
+
return next;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function workspaceUpdateDirectDisplays(graph, sourceId, content) {
|
|
780
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
781
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
782
|
+
const updated = [];
|
|
783
|
+
for (const edge of edges) {
|
|
784
|
+
if (String(edge?.source || "") !== String(sourceId)) continue;
|
|
785
|
+
const targetId = String(edge?.target || "");
|
|
786
|
+
const target = instances[targetId];
|
|
787
|
+
if (!target || !workspaceDisplayKind(target.definitionId)) continue;
|
|
788
|
+
instances[targetId] = workspaceWriteDisplayContent(target, content);
|
|
789
|
+
updated.push(targetId);
|
|
790
|
+
}
|
|
791
|
+
return updated;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock) {
|
|
795
|
+
const instance = graph.instances[nodeId] || {};
|
|
796
|
+
const body = String(instance.body || "").trim();
|
|
797
|
+
const label = String(instance.label || nodeId).trim();
|
|
798
|
+
return [
|
|
799
|
+
"你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
|
|
800
|
+
"只输出该节点要传给下游展示/后续节点的正文,不要解释运行过程。",
|
|
801
|
+
skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
|
|
802
|
+
upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
|
|
803
|
+
`\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
|
|
804
|
+
`\n## 节点任务\n\n${body || upstreamText}`,
|
|
805
|
+
].filter(Boolean).join("\n");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
809
|
+
const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
|
|
810
|
+
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
811
|
+
const order = workspaceRunOrder(graph, runNodeId);
|
|
812
|
+
const fallbackSelectedSkillKeys = Array.isArray(payload?.selectedSkills)
|
|
813
|
+
? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
|
|
814
|
+
: [];
|
|
815
|
+
const skillsBlockCache = new Map();
|
|
816
|
+
const loadSkillsBlockForKeys = (keys) => {
|
|
817
|
+
const normalized = Array.from(new Set((keys || []).map((x) => String(x || "").trim()).filter(Boolean)));
|
|
818
|
+
const cacheKey = normalized.join("\n");
|
|
819
|
+
if (skillsBlockCache.has(cacheKey)) return skillsBlockCache.get(cacheKey);
|
|
820
|
+
const selectedSkillResources = normalized.length > 0
|
|
821
|
+
? loadResourcesForSkillKeys(normalized, PACKAGE_ROOT, scopedRoot)
|
|
822
|
+
: { skills: [], references: [] };
|
|
823
|
+
const block = normalized.length > 0
|
|
824
|
+
? buildSkillCompactInjectionBlock(selectedSkillResources.skills, selectedSkillResources.references)
|
|
825
|
+
: "";
|
|
826
|
+
skillsBlockCache.set(cacheKey, block);
|
|
827
|
+
return block;
|
|
828
|
+
};
|
|
829
|
+
const outputs = new Map();
|
|
830
|
+
const events = [];
|
|
831
|
+
const emit = (event) => {
|
|
832
|
+
events.push(event);
|
|
833
|
+
if (typeof opts.onEvent === "function") opts.onEvent(event);
|
|
834
|
+
};
|
|
835
|
+
let cwd = scopedRoot;
|
|
836
|
+
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
837
|
+
|
|
838
|
+
for (const nodeId of order) {
|
|
839
|
+
const instance = graph.instances[nodeId];
|
|
840
|
+
if (!instance) continue;
|
|
841
|
+
const defId = String(instance.definitionId || "");
|
|
842
|
+
emit({ type: "node-start", nodeId, definitionId: defId });
|
|
843
|
+
|
|
844
|
+
if (defId === "workspace_run") {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (defId === "control_load_skills") {
|
|
849
|
+
const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
|
|
850
|
+
const activeSkillKeys = nodeSkillKeys.length > 0 ? nodeSkillKeys : fallbackSelectedSkillKeys;
|
|
851
|
+
const skillsBlock = loadSkillsBlockForKeys(activeSkillKeys);
|
|
852
|
+
graph.instances[nodeId] = {
|
|
853
|
+
...instance,
|
|
854
|
+
output: (Array.isArray(instance.output) ? instance.output : []).map((slot) => (
|
|
855
|
+
String(slot?.name || "") === "skillsContext" || String(slot?.type || "") === "text"
|
|
856
|
+
? { ...slot, default: skillsBlock, value: skillsBlock }
|
|
857
|
+
: slot
|
|
858
|
+
)),
|
|
859
|
+
};
|
|
860
|
+
outputs.set(nodeId, skillsBlock);
|
|
861
|
+
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock);
|
|
862
|
+
emit({ type: "graph", nodeId, graph });
|
|
863
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (workspaceDisplayKind(defId)) {
|
|
868
|
+
const content = workspaceUpstreamText(graph, nodeId, outputs);
|
|
869
|
+
graph.instances[nodeId] = workspaceWriteDisplayContent(instance, content);
|
|
870
|
+
outputs.set(nodeId, content);
|
|
871
|
+
emit({ type: "graph", nodeId, graph });
|
|
872
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (defId === "provide_str") {
|
|
877
|
+
const content = workspaceInstanceText(instance);
|
|
878
|
+
outputs.set(nodeId, content);
|
|
879
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (defId === "provide_file") {
|
|
884
|
+
const fileValue = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
|
|
885
|
+
const abs = path.resolve(scopedRoot, fileValue);
|
|
886
|
+
if (!abs.startsWith(path.resolve(scopedRoot) + path.sep) && abs !== path.resolve(scopedRoot)) {
|
|
887
|
+
throw new Error(`Workspace file is outside root: ${fileValue}`);
|
|
888
|
+
}
|
|
889
|
+
const content = fs.existsSync(abs) && fs.statSync(abs).isFile() ? fs.readFileSync(abs, "utf-8") : fileValue;
|
|
890
|
+
outputs.set(nodeId, content);
|
|
891
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (defId === "control_cd_workspace") {
|
|
896
|
+
const inputText = workspaceUpstreamText(graph, nodeId, outputs);
|
|
897
|
+
const inputSlots = Array.isArray(instance.input) ? instance.input : [];
|
|
898
|
+
const pathSlot = inputSlots.find((slot) => String(slot?.name || "") === "path") ||
|
|
899
|
+
inputSlots.find((slot) => String(slot?.name || "") === "target");
|
|
900
|
+
const candidate = workspaceSlotValue(pathSlot) || workspaceInstanceText(instance) || inputText;
|
|
901
|
+
const abs = candidate ? path.resolve(scopedRoot, candidate) : scopedRoot;
|
|
902
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) cwd = abs;
|
|
903
|
+
outputs.set(nodeId, cwd);
|
|
904
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (defId === "control_user_workspace") {
|
|
909
|
+
cwd = path.resolve(os.homedir());
|
|
910
|
+
outputs.set(nodeId, cwd);
|
|
911
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const upstreamText = workspaceUpstreamText(graph, nodeId, outputs);
|
|
916
|
+
const body = String(instance.body || "").trim();
|
|
917
|
+
if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
|
|
918
|
+
throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
|
|
919
|
+
}
|
|
920
|
+
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
921
|
+
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, upstreamSkillBlocks || loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
|
|
922
|
+
let content = "";
|
|
923
|
+
const maxAttempts = 3;
|
|
924
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
925
|
+
let attemptContent = "";
|
|
926
|
+
try {
|
|
927
|
+
const handle = startComposerAgent({
|
|
928
|
+
uiWorkspaceRoot: scopedRoot,
|
|
929
|
+
cliWorkspace: cwd,
|
|
930
|
+
prompt,
|
|
931
|
+
modelKey,
|
|
932
|
+
agentflowUserId: userCtx.userId || "",
|
|
933
|
+
onStreamEvent: (ev) => {
|
|
934
|
+
emit({ ...ev, nodeId });
|
|
935
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
936
|
+
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
937
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, attemptContent);
|
|
938
|
+
if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
await handle.finished;
|
|
943
|
+
content = attemptContent.trim();
|
|
944
|
+
break;
|
|
945
|
+
} catch (e) {
|
|
946
|
+
if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
|
|
947
|
+
emit({ type: "status", nodeId, line: `Workspace node retry ${attempt + 1}/${maxAttempts} after network error` });
|
|
948
|
+
await sleepMs(Math.min(1500 * attempt, 5000));
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
throw e;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
outputs.set(nodeId, content);
|
|
955
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, content);
|
|
956
|
+
if (updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
957
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
958
|
+
}
|
|
959
|
+
graph.updatedAt = new Date().toISOString();
|
|
960
|
+
return { graph, events, order };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function isTransientAgentNetworkError(err) {
|
|
964
|
+
const text = [
|
|
965
|
+
err?.message,
|
|
966
|
+
err?.cursorStderrTail,
|
|
967
|
+
err?.stderr,
|
|
968
|
+
err?.stack,
|
|
969
|
+
].filter(Boolean).join("\n");
|
|
970
|
+
return /Client network socket disconnected before secure TLS connection was established/i.test(text) ||
|
|
971
|
+
/secure TLS connection was established/i.test(text) ||
|
|
972
|
+
/\bECONNRESET\b/i.test(text) ||
|
|
973
|
+
/\bETIMEDOUT\b/i.test(text) ||
|
|
974
|
+
/\bEAI_AGAIN\b/i.test(text) ||
|
|
975
|
+
/network socket disconnected/i.test(text) ||
|
|
976
|
+
/socket hang up/i.test(text);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function sleepMs(ms) {
|
|
980
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
981
|
+
}
|
|
982
|
+
|
|
135
983
|
/** ZIP 本地头:PK\x03\x04 / \x05\x06 / \x07\x08 */
|
|
136
984
|
function bufferLooksLikeZip(buf) {
|
|
137
985
|
return (
|
|
@@ -211,12 +1059,12 @@ const flowEditorSyncSubscribers = new Map();
|
|
|
211
1059
|
/** 每次 broadcastFlowEditorSync 时递增,供轮询端点 /api/flow-editor-sync-poll 使用 */
|
|
212
1060
|
const flowEditorSyncVersions = new Map();
|
|
213
1061
|
|
|
214
|
-
function flowEditorSyncKey(flowId, flowSource, flowArchived) {
|
|
215
|
-
return `${String(flowId)}\t${String(flowSource)}\t${flowArchived ? "1" : "0"}`;
|
|
1062
|
+
function flowEditorSyncKey(flowId, flowSource, flowArchived, userId = "") {
|
|
1063
|
+
return `${String(userId || "")}\t${String(flowId)}\t${String(flowSource)}\t${flowArchived ? "1" : "0"}`;
|
|
216
1064
|
}
|
|
217
1065
|
|
|
218
|
-
function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false) {
|
|
219
|
-
const key = flowEditorSyncKey(flowId, flowSource, flowArchived);
|
|
1066
|
+
function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userId = "") {
|
|
1067
|
+
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userId);
|
|
220
1068
|
|
|
221
1069
|
/* 递增轮询版本号 */
|
|
222
1070
|
flowEditorSyncVersions.set(key, (flowEditorSyncVersions.get(key) ?? 0) + 1);
|
|
@@ -340,6 +1188,7 @@ function normalizeContextInstanceIds(raw) {
|
|
|
340
1188
|
* @param {object} opts
|
|
341
1189
|
* @param {string} opts.workspaceRoot
|
|
342
1190
|
* @param {number} opts.port
|
|
1191
|
+
* @param {boolean} [opts.hideCommunityLinks]
|
|
343
1192
|
* @param {string} [opts.staticDir] 默认 PACKAGE_ROOT/builtin/web-ui/dist(npm run build 产出)
|
|
344
1193
|
* @returns {Promise<import('http').Server>}
|
|
345
1194
|
*/
|
|
@@ -347,10 +1196,12 @@ export function startUiServer({
|
|
|
347
1196
|
workspaceRoot,
|
|
348
1197
|
port,
|
|
349
1198
|
host = "127.0.0.1",
|
|
1199
|
+
hideCommunityLinks = false,
|
|
350
1200
|
staticDir = path.join(PACKAGE_ROOT, "builtin", "web-ui", "dist"),
|
|
351
1201
|
}) {
|
|
352
1202
|
const root = path.resolve(workspaceRoot);
|
|
353
1203
|
const uiPort = port;
|
|
1204
|
+
const uiConfig = { hideCommunityLinks: Boolean(hideCommunityLinks) };
|
|
354
1205
|
|
|
355
1206
|
const server = http.createServer(async (req, res) => {
|
|
356
1207
|
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
@@ -363,10 +1214,58 @@ export function startUiServer({
|
|
|
363
1214
|
return origEnd(...args);
|
|
364
1215
|
};
|
|
365
1216
|
|
|
1217
|
+
if (url.pathname === "/api/auth/me" && req.method === "GET") {
|
|
1218
|
+
const user = getAuthUserFromRequest(req);
|
|
1219
|
+
json(res, 200, { authenticated: Boolean(user), user: user || null, setupRequired: authSetupRequired() });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (url.pathname === "/api/auth/login" && req.method === "POST") {
|
|
1224
|
+
let payload;
|
|
1225
|
+
try {
|
|
1226
|
+
payload = JSON.parse(await readBody(req));
|
|
1227
|
+
} catch {
|
|
1228
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const result = loginOrCreateUser(payload?.username, payload?.password);
|
|
1232
|
+
if (!result.ok) {
|
|
1233
|
+
json(res, 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const body = JSON.stringify({ authenticated: true, user: result.user, setupRequired: false, migration: result.migration || null });
|
|
1237
|
+
res.writeHead(200, {
|
|
1238
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1239
|
+
"Content-Length": Buffer.byteLength(body),
|
|
1240
|
+
"Set-Cookie": buildSessionCookie(result.token),
|
|
1241
|
+
});
|
|
1242
|
+
res.end(body);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (url.pathname === "/api/auth/logout" && req.method === "POST") {
|
|
1247
|
+
logoutRequest(req);
|
|
1248
|
+
const body = JSON.stringify({ ok: true });
|
|
1249
|
+
res.writeHead(200, {
|
|
1250
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1251
|
+
"Content-Length": Buffer.byteLength(body),
|
|
1252
|
+
"Set-Cookie": buildClearSessionCookie(),
|
|
1253
|
+
});
|
|
1254
|
+
res.end(body);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const authUser = getAuthUserFromRequest(req);
|
|
1259
|
+
const userCtx = authUser ? { userId: authUser.userId } : {};
|
|
1260
|
+
if (url.pathname.startsWith("/api/") && !authUser) {
|
|
1261
|
+
json(res, 401, { error: "Authentication required", setupRequired: authSetupRequired() });
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
366
1265
|
if (url.pathname === "/api/flows") {
|
|
367
1266
|
if (req.method === "GET") {
|
|
368
1267
|
try {
|
|
369
|
-
json(res, 200, listFlowsJson(root));
|
|
1268
|
+
json(res, 200, listFlowsJson(root, userCtx));
|
|
370
1269
|
} catch (e) {
|
|
371
1270
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
372
1271
|
}
|
|
@@ -400,7 +1299,7 @@ export function startUiServer({
|
|
|
400
1299
|
if (ts === "workspace" || ts === "user") {
|
|
401
1300
|
targetSpace = ts;
|
|
402
1301
|
}
|
|
403
|
-
const existing = listFlowsJson(root);
|
|
1302
|
+
const existing = listFlowsJson(root, userCtx);
|
|
404
1303
|
if (
|
|
405
1304
|
existing.some(
|
|
406
1305
|
(f) => f.id === flowId && (f.source ?? "user") === targetSpace && !f.archived,
|
|
@@ -410,7 +1309,7 @@ export function startUiServer({
|
|
|
410
1309
|
return;
|
|
411
1310
|
}
|
|
412
1311
|
const flowYaml = buildEmptyUserFlowYaml({ description: desc });
|
|
413
|
-
const result = writeFlowYaml(root, flowId, targetSpace, flowYaml);
|
|
1312
|
+
const result = writeFlowYaml(root, flowId, targetSpace, flowYaml, userCtx);
|
|
414
1313
|
if (!result.success) {
|
|
415
1314
|
json(res, 400, result);
|
|
416
1315
|
return;
|
|
@@ -456,7 +1355,7 @@ export function startUiServer({
|
|
|
456
1355
|
}
|
|
457
1356
|
const flowId = idCheck.flowId;
|
|
458
1357
|
const targetSpace = parsed.targetSpace === "workspace" ? "workspace" : "user";
|
|
459
|
-
const existing = listFlowsJson(root);
|
|
1358
|
+
const existing = listFlowsJson(root, userCtx);
|
|
460
1359
|
if (
|
|
461
1360
|
existing.some(
|
|
462
1361
|
(f) => f.id === flowId && (f.source ?? "user") === targetSpace && !f.archived,
|
|
@@ -487,7 +1386,7 @@ export function startUiServer({
|
|
|
487
1386
|
filesMap = new Map([["flow.yaml", Buffer.from(text, "utf8")]]);
|
|
488
1387
|
}
|
|
489
1388
|
|
|
490
|
-
const w = writePipelineTree(root, flowId, targetSpace, filesMap);
|
|
1389
|
+
const w = writePipelineTree(root, flowId, targetSpace, filesMap, userCtx);
|
|
491
1390
|
if (!w.success) {
|
|
492
1391
|
json(res, 400, { error: w.error });
|
|
493
1392
|
return;
|
|
@@ -507,7 +1406,7 @@ export function startUiServer({
|
|
|
507
1406
|
return;
|
|
508
1407
|
}
|
|
509
1408
|
const { getNodeExecContext } = await import("./node-exec-context.mjs");
|
|
510
|
-
json(res, 200, getNodeExecContext(root, flowId, instanceId, runId));
|
|
1409
|
+
json(res, 200, getNodeExecContext(root, flowId, instanceId, runId, userCtx));
|
|
511
1410
|
} catch (e) {
|
|
512
1411
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
513
1412
|
}
|
|
@@ -516,7 +1415,7 @@ export function startUiServer({
|
|
|
516
1415
|
|
|
517
1416
|
if (req.method === "GET" && url.pathname === "/api/pipeline-recent-runs") {
|
|
518
1417
|
try {
|
|
519
|
-
json(res, 200, { runs: listRecentRunsFromDisk(root) });
|
|
1418
|
+
json(res, 200, { runs: listRecentRunsFromDisk(root, userCtx) });
|
|
520
1419
|
} catch (e) {
|
|
521
1420
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
522
1421
|
}
|
|
@@ -532,7 +1431,7 @@ export function startUiServer({
|
|
|
532
1431
|
return;
|
|
533
1432
|
}
|
|
534
1433
|
const { getRunNodeStatusesFromDisk } = await import("./run-node-statuses-from-disk.mjs");
|
|
535
|
-
json(res, 200, { statuses: getRunNodeStatusesFromDisk(root, flowId, runId) });
|
|
1434
|
+
json(res, 200, { statuses: getRunNodeStatusesFromDisk(root, flowId, runId, userCtx) });
|
|
536
1435
|
} catch (e) {
|
|
537
1436
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
538
1437
|
}
|
|
@@ -554,7 +1453,7 @@ export function startUiServer({
|
|
|
554
1453
|
const { getRunDir } = await import("./workspace.mjs");
|
|
555
1454
|
const { RUN_LOG_REL } = await import("./paths.mjs");
|
|
556
1455
|
const { default: fsMod } = await import("node:fs");
|
|
557
|
-
const logPath = path.join(getRunDir(root, flowId, runId), RUN_LOG_REL);
|
|
1456
|
+
const logPath = path.join(getRunDir(root, flowId, runId, userCtx), RUN_LOG_REL);
|
|
558
1457
|
if (!fsMod.existsSync(logPath)) {
|
|
559
1458
|
json(res, 200, { bytes: 0, text: "" });
|
|
560
1459
|
return;
|
|
@@ -596,6 +1495,357 @@ export function startUiServer({
|
|
|
596
1495
|
return;
|
|
597
1496
|
}
|
|
598
1497
|
|
|
1498
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/files") {
|
|
1499
|
+
try {
|
|
1500
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1501
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
1502
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
1503
|
+
archived: url.searchParams.get("archived") === "1",
|
|
1504
|
+
}, userCtx);
|
|
1505
|
+
if (scoped.error) {
|
|
1506
|
+
json(res, 400, { error: scoped.error });
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
json(res, 200, { ...readWorkspaceFiles(scoped.root), flowId: scoped.flowId, flowSource: scoped.flowSource, archived: scoped.archived });
|
|
1510
|
+
} catch (e) {
|
|
1511
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1512
|
+
}
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/graph") {
|
|
1517
|
+
try {
|
|
1518
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1519
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
1520
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
1521
|
+
archived: url.searchParams.get("archived") === "1",
|
|
1522
|
+
}, userCtx);
|
|
1523
|
+
if (scoped.error) {
|
|
1524
|
+
json(res, 400, { error: scoped.error });
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const { path: graphPath, graph } = readWorkspaceGraph(scoped.root);
|
|
1528
|
+
json(res, 200, {
|
|
1529
|
+
ok: true,
|
|
1530
|
+
graph,
|
|
1531
|
+
path: graphPath,
|
|
1532
|
+
root: scoped.root,
|
|
1533
|
+
flowId: scoped.flowId,
|
|
1534
|
+
flowSource: scoped.flowSource,
|
|
1535
|
+
archived: scoped.archived,
|
|
1536
|
+
writable: !(scoped.archived || scoped.flowSource === "builtin"),
|
|
1537
|
+
});
|
|
1538
|
+
} catch (e) {
|
|
1539
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1540
|
+
}
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/graph") {
|
|
1545
|
+
let payload;
|
|
1546
|
+
try {
|
|
1547
|
+
payload = JSON.parse(await readBody(req));
|
|
1548
|
+
} catch {
|
|
1549
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
try {
|
|
1553
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1554
|
+
flowId: payload.flowId || "",
|
|
1555
|
+
flowSource: payload.flowSource || "user",
|
|
1556
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1557
|
+
}, userCtx);
|
|
1558
|
+
if (scoped.error) {
|
|
1559
|
+
json(res, 400, { error: scoped.error });
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1563
|
+
json(res, 400, { error: "Cannot write workspace graph for builtin or archived pipeline" });
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const graph = normalizeWorkspaceGraphPayload(payload.graph || payload);
|
|
1567
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
1568
|
+
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf-8");
|
|
1569
|
+
json(res, 200, { ok: true, path: graphPath, graph });
|
|
1570
|
+
} catch (e) {
|
|
1571
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1572
|
+
}
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/run") {
|
|
1577
|
+
let payload;
|
|
1578
|
+
try {
|
|
1579
|
+
payload = JSON.parse(await readBody(req));
|
|
1580
|
+
} catch {
|
|
1581
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1586
|
+
flowId: payload.flowId || "",
|
|
1587
|
+
flowSource: payload.flowSource || "user",
|
|
1588
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1589
|
+
}, userCtx);
|
|
1590
|
+
if (scoped.error) {
|
|
1591
|
+
json(res, 400, { error: scoped.error });
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1595
|
+
json(res, 400, { error: "Cannot run workspace graph for builtin or archived pipeline" });
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const wantsStream = /\bapplication\/x-ndjson\b/i.test(req.headers.accept || "") || payload.stream === true;
|
|
1599
|
+
if (wantsStream) {
|
|
1600
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
1601
|
+
res.writeHead(200, {
|
|
1602
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
1603
|
+
"Cache-Control": "no-cache",
|
|
1604
|
+
"X-Accel-Buffering": "no",
|
|
1605
|
+
});
|
|
1606
|
+
const writeEvent = (event) => {
|
|
1607
|
+
res.write(JSON.stringify(event) + "\n");
|
|
1608
|
+
};
|
|
1609
|
+
try {
|
|
1610
|
+
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx, { onEvent: writeEvent });
|
|
1611
|
+
fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
|
|
1612
|
+
writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order });
|
|
1613
|
+
res.end();
|
|
1614
|
+
} catch (e) {
|
|
1615
|
+
writeEvent({ type: "error", error: (e && e.message) || String(e) });
|
|
1616
|
+
res.end();
|
|
1617
|
+
}
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
const result = await runWorkspaceGraph(root, scoped.root, payload, userCtx);
|
|
1621
|
+
const graphPath = workspaceGraphPath(scoped.root);
|
|
1622
|
+
fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
|
|
1623
|
+
json(res, 200, { ok: true, path: graphPath, ...result });
|
|
1624
|
+
} catch (e) {
|
|
1625
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1626
|
+
}
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/file") {
|
|
1631
|
+
try {
|
|
1632
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1633
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
1634
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
1635
|
+
archived: url.searchParams.get("archived") === "1",
|
|
1636
|
+
}, userCtx);
|
|
1637
|
+
if (scoped.error) {
|
|
1638
|
+
json(res, 400, { error: scoped.error });
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
|
|
1642
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
1643
|
+
json(res, 404, { error: "File not found" });
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const stat = fs.statSync(abs);
|
|
1647
|
+
if (stat.size > 2 * 1024 * 1024) {
|
|
1648
|
+
json(res, 413, { error: "File too large" });
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
json(res, 200, { path: rel, content: fs.readFileSync(abs, "utf-8"), size: stat.size });
|
|
1652
|
+
} catch (e) {
|
|
1653
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
1654
|
+
}
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/file") {
|
|
1659
|
+
let payload;
|
|
1660
|
+
try {
|
|
1661
|
+
payload = JSON.parse(await readBody(req));
|
|
1662
|
+
} catch {
|
|
1663
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
try {
|
|
1667
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1668
|
+
flowId: payload.flowId || "",
|
|
1669
|
+
flowSource: payload.flowSource || "user",
|
|
1670
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1671
|
+
}, userCtx);
|
|
1672
|
+
if (scoped.error) {
|
|
1673
|
+
json(res, 400, { error: scoped.error });
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1677
|
+
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, payload.path || "");
|
|
1681
|
+
if (!rel) {
|
|
1682
|
+
json(res, 400, { error: "Missing path" });
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
1686
|
+
fs.writeFileSync(abs, String(payload.content ?? ""), "utf-8");
|
|
1687
|
+
json(res, 200, { ok: true, path: rel });
|
|
1688
|
+
} catch (e) {
|
|
1689
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
1690
|
+
}
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/folder") {
|
|
1695
|
+
let payload;
|
|
1696
|
+
try {
|
|
1697
|
+
payload = JSON.parse(await readBody(req));
|
|
1698
|
+
} catch {
|
|
1699
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
try {
|
|
1703
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1704
|
+
flowId: payload.flowId || "",
|
|
1705
|
+
flowSource: payload.flowSource || "user",
|
|
1706
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1707
|
+
}, userCtx);
|
|
1708
|
+
if (scoped.error) {
|
|
1709
|
+
json(res, 400, { error: scoped.error });
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1713
|
+
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, payload.path || "");
|
|
1717
|
+
if (!rel) {
|
|
1718
|
+
json(res, 400, { error: "Missing path" });
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
1722
|
+
json(res, 200, { ok: true, path: rel });
|
|
1723
|
+
} catch (e) {
|
|
1724
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
1725
|
+
}
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/delete") {
|
|
1730
|
+
let payload;
|
|
1731
|
+
try {
|
|
1732
|
+
payload = JSON.parse(await readBody(req));
|
|
1733
|
+
} catch {
|
|
1734
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
try {
|
|
1738
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1739
|
+
flowId: payload.flowId || "",
|
|
1740
|
+
flowSource: payload.flowSource || "user",
|
|
1741
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1742
|
+
}, userCtx);
|
|
1743
|
+
if (scoped.error) {
|
|
1744
|
+
json(res, 400, { error: scoped.error });
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (scoped.archived || scoped.flowSource === "builtin") {
|
|
1748
|
+
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, payload.path || "");
|
|
1752
|
+
if (!rel) {
|
|
1753
|
+
json(res, 400, { error: "Missing path" });
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (!fs.existsSync(abs)) {
|
|
1757
|
+
json(res, 404, { error: "Path not found" });
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
fs.rmSync(abs, { recursive: true, force: true });
|
|
1761
|
+
json(res, 200, { ok: true, path: rel });
|
|
1762
|
+
} catch (e) {
|
|
1763
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
1764
|
+
}
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/generate") {
|
|
1769
|
+
let payload;
|
|
1770
|
+
try {
|
|
1771
|
+
payload = JSON.parse(await readBody(req));
|
|
1772
|
+
} catch {
|
|
1773
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
const prompt = String(payload?.prompt || "").trim();
|
|
1777
|
+
if (!prompt) {
|
|
1778
|
+
json(res, 400, { error: "Missing prompt" });
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
try {
|
|
1782
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
1783
|
+
flowId: payload.flowId || "",
|
|
1784
|
+
flowSource: payload.flowSource || "user",
|
|
1785
|
+
archived: payload.archived === true || payload.flowArchived === true,
|
|
1786
|
+
}, userCtx);
|
|
1787
|
+
if (scoped.error) {
|
|
1788
|
+
json(res, 400, { error: scoped.error });
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
const selectedSkillKeys = Array.isArray(payload?.selectedSkills)
|
|
1792
|
+
? payload.selectedSkills.map((x) => String(x || "").trim()).filter(Boolean)
|
|
1793
|
+
: [];
|
|
1794
|
+
const selectedSkillResources = selectedSkillKeys.length > 0
|
|
1795
|
+
? loadResourcesForSkillKeys(selectedSkillKeys, PACKAGE_ROOT, scoped.root)
|
|
1796
|
+
: { skills: [], references: [] };
|
|
1797
|
+
const skillsBlock = selectedSkillKeys.length > 0
|
|
1798
|
+
? buildSkillCompactInjectionBlock(selectedSkillResources.skills, selectedSkillResources.references)
|
|
1799
|
+
: "";
|
|
1800
|
+
let content = "";
|
|
1801
|
+
const events = [];
|
|
1802
|
+
const maxAttempts = 3;
|
|
1803
|
+
const promptText = buildWorkspaceGeneratePrompt({ ...payload, skillsBlock });
|
|
1804
|
+
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
1805
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1806
|
+
let attemptContent = "";
|
|
1807
|
+
try {
|
|
1808
|
+
if (attempt > 1) {
|
|
1809
|
+
events.push({
|
|
1810
|
+
type: "status",
|
|
1811
|
+
line: `Workspace agent retry ${attempt}/${maxAttempts} after transient network failure...`,
|
|
1812
|
+
});
|
|
1813
|
+
await sleepMs(Math.min(1500 * attempt, 5000));
|
|
1814
|
+
}
|
|
1815
|
+
const handle = startComposerAgent({
|
|
1816
|
+
uiWorkspaceRoot: scoped.root,
|
|
1817
|
+
cliWorkspace: scoped.root,
|
|
1818
|
+
prompt: promptText,
|
|
1819
|
+
modelKey,
|
|
1820
|
+
agentflowUserId: userCtx.userId || "",
|
|
1821
|
+
onStreamEvent: (ev) => {
|
|
1822
|
+
events.push(ev);
|
|
1823
|
+
if (ev?.type === "natural" && ev.kind === "assistant" && typeof ev.text === "string") {
|
|
1824
|
+
attemptContent += (attemptContent ? "\n" : "") + ev.text;
|
|
1825
|
+
}
|
|
1826
|
+
},
|
|
1827
|
+
});
|
|
1828
|
+
await handle.finished;
|
|
1829
|
+
content = attemptContent;
|
|
1830
|
+
break;
|
|
1831
|
+
} catch (e) {
|
|
1832
|
+
if (attempt < maxAttempts && isTransientAgentNetworkError(e)) {
|
|
1833
|
+
events.push({
|
|
1834
|
+
type: "status",
|
|
1835
|
+
line: `Workspace agent transient network error: ${String(e.message || e).slice(0, 220)}`,
|
|
1836
|
+
});
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
throw e;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
json(res, 200, { ok: true, content: content.trim(), events });
|
|
1843
|
+
} catch (e) {
|
|
1844
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
1845
|
+
}
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
599
1849
|
if (req.method === "GET" && url.pathname === "/api/pipeline-files") {
|
|
600
1850
|
const flowId = url.searchParams.get("flowId");
|
|
601
1851
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
@@ -605,7 +1855,7 @@ export function startUiServer({
|
|
|
605
1855
|
return;
|
|
606
1856
|
}
|
|
607
1857
|
try {
|
|
608
|
-
const result = getPipelineFiles(root, flowId, flowSource, archived);
|
|
1858
|
+
const result = getPipelineFiles(root, flowId, flowSource, archived, userCtx);
|
|
609
1859
|
json(res, 200, result);
|
|
610
1860
|
} catch (e) {
|
|
611
1861
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
@@ -623,7 +1873,7 @@ export function startUiServer({
|
|
|
623
1873
|
return;
|
|
624
1874
|
}
|
|
625
1875
|
try {
|
|
626
|
-
const result = getPipelineFiles(root, flowId, flowSource, archived);
|
|
1876
|
+
const result = getPipelineFiles(root, flowId, flowSource, archived, userCtx);
|
|
627
1877
|
if (result.error) {
|
|
628
1878
|
json(res, 404, { error: result.error });
|
|
629
1879
|
return;
|
|
@@ -669,7 +1919,7 @@ export function startUiServer({
|
|
|
669
1919
|
content = String(body);
|
|
670
1920
|
}
|
|
671
1921
|
try {
|
|
672
|
-
const result = getPipelineFiles(root, flowId, flowSource, archived);
|
|
1922
|
+
const result = getPipelineFiles(root, flowId, flowSource, archived, userCtx);
|
|
673
1923
|
if (result.error) {
|
|
674
1924
|
json(res, 404, { error: result.error });
|
|
675
1925
|
return;
|
|
@@ -702,7 +1952,7 @@ export function startUiServer({
|
|
|
702
1952
|
|
|
703
1953
|
if (req.method === "GET" && url.pathname === "/api/ui-context") {
|
|
704
1954
|
try {
|
|
705
|
-
json(res, 200, { workspaceRoot: root });
|
|
1955
|
+
json(res, 200, { workspaceRoot: root, ...uiConfig });
|
|
706
1956
|
} catch (e) {
|
|
707
1957
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
708
1958
|
}
|
|
@@ -812,6 +2062,32 @@ export function startUiServer({
|
|
|
812
2062
|
return;
|
|
813
2063
|
}
|
|
814
2064
|
|
|
2065
|
+
if (req.method === "GET" && url.pathname === "/api/user-env") {
|
|
2066
|
+
try {
|
|
2067
|
+
json(res, 200, { env: readUserEnvRows(userCtx.userId) });
|
|
2068
|
+
} catch (e) {
|
|
2069
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2070
|
+
}
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
if (req.method === "POST" && url.pathname === "/api/user-env") {
|
|
2075
|
+
let payload;
|
|
2076
|
+
try {
|
|
2077
|
+
payload = JSON.parse(await readBody(req));
|
|
2078
|
+
} catch {
|
|
2079
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
const envRows = writeUserEnvRows(userCtx.userId, payload?.env || []);
|
|
2084
|
+
json(res, 200, { success: true, env: envRows });
|
|
2085
|
+
} catch (e) {
|
|
2086
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2087
|
+
}
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
815
2091
|
if (req.method === "POST" && url.pathname === "/api/update-model-lists") {
|
|
816
2092
|
try {
|
|
817
2093
|
let opencodeProviderOverride = "";
|
|
@@ -833,15 +2109,108 @@ export function startUiServer({
|
|
|
833
2109
|
return;
|
|
834
2110
|
}
|
|
835
2111
|
|
|
2112
|
+
if (req.method === "GET" && url.pathname === "/api/skillhub/status") {
|
|
2113
|
+
const versionRes = await runSkillhub(["version"], { cwd: root, timeoutMs: 15_000 });
|
|
2114
|
+
const whoRes = await runSkillhub(["whoami"], { cwd: root, timeoutMs: 15_000 });
|
|
2115
|
+
json(res, 200, {
|
|
2116
|
+
available: versionRes.ok,
|
|
2117
|
+
version: versionRes.ok ? versionRes.stdout.trim() : "",
|
|
2118
|
+
loggedIn: whoRes.ok,
|
|
2119
|
+
user: whoRes.ok ? whoRes.stdout.trim() : "",
|
|
2120
|
+
error: versionRes.ok ? "" : versionRes.error,
|
|
2121
|
+
});
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
if (req.method === "GET" && url.pathname === "/api/skillhub/list") {
|
|
2126
|
+
const target = url.searchParams.get("target") || "global";
|
|
2127
|
+
const agent = url.searchParams.get("agent") || "codex";
|
|
2128
|
+
const args = ["list", "--json"];
|
|
2129
|
+
if (target === "all") args.push("--all");
|
|
2130
|
+
else if (target === "global") args.push("--global", "--agent", agent);
|
|
2131
|
+
const result = await runSkillhub(args, { cwd: root });
|
|
2132
|
+
if (!result.ok) {
|
|
2133
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
json(res, 200, { skills: normalizeSkillhubListPayload(parseJsonText(result.stdout, [])) });
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
if (req.method === "GET" && url.pathname === "/api/skillhub/search") {
|
|
2141
|
+
const q = (url.searchParams.get("q") || "").trim();
|
|
2142
|
+
if (!q) {
|
|
2143
|
+
json(res, 200, { total: 0, items: [] });
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
const result = await runSkillhub(["search", "-q", q], { cwd: root });
|
|
2147
|
+
if (!result.ok) {
|
|
2148
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
json(res, 200, normalizeSkillhubSearchPayload(parseJsonText(result.stdout, {})));
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
if (req.method === "POST" && url.pathname === "/api/skillhub/install") {
|
|
2156
|
+
let payload;
|
|
2157
|
+
try {
|
|
2158
|
+
payload = JSON.parse(await readBody(req));
|
|
2159
|
+
} catch {
|
|
2160
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const args = skillhubInstallArgs(payload);
|
|
2164
|
+
if (!args) {
|
|
2165
|
+
json(res, 400, { error: "Missing skill slug or collection" });
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
const result = await runSkillhub(args, { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
2169
|
+
if (!result.ok) {
|
|
2170
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
json(res, 200, { ok: true, stdout: result.stdout });
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
if (req.method === "POST" && url.pathname === "/api/skillhub/uninstall") {
|
|
2178
|
+
let payload;
|
|
2179
|
+
try {
|
|
2180
|
+
payload = JSON.parse(await readBody(req));
|
|
2181
|
+
} catch {
|
|
2182
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const args = skillhubInstallArgs(payload, { uninstall: true });
|
|
2186
|
+
if (!args) {
|
|
2187
|
+
json(res, 400, { error: "Missing skill slug or collection" });
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const result = await runSkillhub(args, { cwd: root, timeoutMs: 120_000, maxBuffer: 4 * 1024 * 1024 });
|
|
2191
|
+
if (!result.ok) {
|
|
2192
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
json(res, 200, { ok: true, stdout: result.stdout });
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
if (req.method === "POST" && url.pathname === "/api/skillhub/update") {
|
|
2200
|
+
const result = await runSkillhub(["update"], { cwd: root, timeoutMs: 180_000, maxBuffer: 4 * 1024 * 1024 });
|
|
2201
|
+
if (!result.ok) {
|
|
2202
|
+
json(res, 500, { error: result.error, stdout: result.stdout });
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
json(res, 200, { ok: true, stdout: result.stdout });
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
836
2209
|
if (req.method === "GET" && url.pathname === "/api/nodes") {
|
|
837
2210
|
const flowId = url.searchParams.get("flowId");
|
|
838
2211
|
const flowSource = url.searchParams.get("flowSource") || "user";
|
|
839
2212
|
const lang = url.searchParams.get("lang") || "en";
|
|
840
|
-
if (!
|
|
841
|
-
json(res, 400, { error: "Missing flowId" });
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
if (!isValidFlowSourceRead(flowSource)) {
|
|
2213
|
+
if (flowId && !isValidFlowSourceRead(flowSource)) {
|
|
845
2214
|
json(res, 400, { error: "Invalid flowSource" });
|
|
846
2215
|
return;
|
|
847
2216
|
}
|
|
@@ -849,7 +2218,60 @@ export function startUiServer({
|
|
|
849
2218
|
try {
|
|
850
2219
|
const { setLanguage } = await import("./i18n.mjs");
|
|
851
2220
|
setLanguage(lang);
|
|
852
|
-
json(res, 200, listNodesJson(root, flowId, flowSource, { archived: nodesArchived }));
|
|
2221
|
+
json(res, 200, listNodesJson(root, flowId || "", flowId ? flowSource : "", { archived: nodesArchived, ...userCtx }));
|
|
2222
|
+
} catch (e) {
|
|
2223
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2224
|
+
}
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
if (req.method === "GET" && url.pathname === "/api/nodes/detail") {
|
|
2229
|
+
const nodeId = url.searchParams.get("id") || "";
|
|
2230
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
2231
|
+
const flowSource = url.searchParams.get("flowSource") || "";
|
|
2232
|
+
if (!nodeId) {
|
|
2233
|
+
json(res, 400, { error: "Missing node id" });
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
if (flowId && !isValidFlowSourceRead(flowSource || "user")) {
|
|
2237
|
+
json(res, 400, { error: "Invalid flowSource" });
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const archived = url.searchParams.get("archived") === "1";
|
|
2241
|
+
try {
|
|
2242
|
+
const detail = readNodeDetailJson(root, nodeId, flowId, flowId ? (flowSource || "user") : "", { archived, ...userCtx });
|
|
2243
|
+
if (detail.error) {
|
|
2244
|
+
json(res, 404, { error: detail.error });
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
json(res, 200, detail);
|
|
2248
|
+
} catch (e) {
|
|
2249
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
2250
|
+
}
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
if (req.method === "GET" && url.pathname === "/api/nodes/file") {
|
|
2255
|
+
const nodeId = url.searchParams.get("id") || "";
|
|
2256
|
+
const relPath = url.searchParams.get("path") || "";
|
|
2257
|
+
const flowId = url.searchParams.get("flowId") || "";
|
|
2258
|
+
const flowSource = url.searchParams.get("flowSource") || "";
|
|
2259
|
+
if (!nodeId || !relPath) {
|
|
2260
|
+
json(res, 400, { error: "Missing node id or path" });
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
if (flowId && !isValidFlowSourceRead(flowSource || "user")) {
|
|
2264
|
+
json(res, 400, { error: "Invalid flowSource" });
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
const archived = url.searchParams.get("archived") === "1";
|
|
2268
|
+
try {
|
|
2269
|
+
const file = readNodeFilePreview(root, nodeId, relPath, flowId, flowId ? (flowSource || "user") : "", { archived, ...userCtx });
|
|
2270
|
+
if (file.error) {
|
|
2271
|
+
json(res, 404, { error: file.error });
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
json(res, 200, file);
|
|
853
2275
|
} catch (e) {
|
|
854
2276
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
855
2277
|
}
|
|
@@ -890,7 +2312,7 @@ export function startUiServer({
|
|
|
890
2312
|
return;
|
|
891
2313
|
}
|
|
892
2314
|
try {
|
|
893
|
-
const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
|
|
2315
|
+
const resolved = resolveFlowDirForWrite(root, flowId, flowSource, userCtx);
|
|
894
2316
|
if (resolved.error || !resolved.flowDir) {
|
|
895
2317
|
json(res, 400, { error: resolved.error || "Could not resolve flow directory" });
|
|
896
2318
|
return;
|
|
@@ -916,7 +2338,7 @@ export function startUiServer({
|
|
|
916
2338
|
const flowSource = payload?.flowSource || "user";
|
|
917
2339
|
let flowDir = "";
|
|
918
2340
|
if (flowId && isValidFlowSourceWrite(flowSource)) {
|
|
919
|
-
const resolved = resolveFlowDirForWrite(root, flowId, flowSource);
|
|
2341
|
+
const resolved = resolveFlowDirForWrite(root, flowId, flowSource, userCtx);
|
|
920
2342
|
if (!resolved.error && resolved.flowDir) flowDir = resolved.flowDir;
|
|
921
2343
|
}
|
|
922
2344
|
const result = publishNodeFromInstance(root, payload || {}, { flowDir });
|
|
@@ -939,7 +2361,7 @@ export function startUiServer({
|
|
|
939
2361
|
return;
|
|
940
2362
|
}
|
|
941
2363
|
const flowArchived = url.searchParams.get("archived") === "1";
|
|
942
|
-
const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived });
|
|
2364
|
+
const result = readFlowJson(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
943
2365
|
if (result.error) {
|
|
944
2366
|
json(res, 404, result);
|
|
945
2367
|
return;
|
|
@@ -965,7 +2387,7 @@ export function startUiServer({
|
|
|
965
2387
|
json(res, 400, { error: "Missing runUuid, instanceId, or content" });
|
|
966
2388
|
return;
|
|
967
2389
|
}
|
|
968
|
-
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid));
|
|
2390
|
+
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid, userCtx));
|
|
969
2391
|
const outputPath = path.join(runDir, `output/${instanceId}/node_${instanceId}_content.md`);
|
|
970
2392
|
try {
|
|
971
2393
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
@@ -1000,7 +2422,7 @@ ${content}
|
|
|
1000
2422
|
|
|
1001
2423
|
const opencodeCmd = process.env.OPENCODE_CMD || "opencode";
|
|
1002
2424
|
const tmpPromptFile = path.join(
|
|
1003
|
-
getRunDir(root, payload.flowId || "unknown", runUuid),
|
|
2425
|
+
getRunDir(root, payload.flowId || "unknown", runUuid, userCtx),
|
|
1004
2426
|
"intermediate",
|
|
1005
2427
|
`${instanceId}_ai_edit_prompt.txt`,
|
|
1006
2428
|
);
|
|
@@ -1045,7 +2467,7 @@ ${content}
|
|
|
1045
2467
|
json(res, 400, { error: "Missing runUuid or instanceId" });
|
|
1046
2468
|
return;
|
|
1047
2469
|
}
|
|
1048
|
-
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid));
|
|
2470
|
+
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid, userCtx));
|
|
1049
2471
|
const resultPath = path.join(runDir, `intermediate/${instanceId}/${instanceId}.result.md`);
|
|
1050
2472
|
try {
|
|
1051
2473
|
fs.mkdirSync(path.dirname(resultPath), { recursive: true });
|
|
@@ -1075,7 +2497,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1075
2497
|
json(res, 400, { error: "Missing runUuid, instanceId, or branch" });
|
|
1076
2498
|
return;
|
|
1077
2499
|
}
|
|
1078
|
-
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid));
|
|
2500
|
+
const runDir = path.join(getRunDir(root, payload.flowId || "unknown", runUuid, userCtx));
|
|
1079
2501
|
const resultPath = path.join(runDir, `intermediate/${instanceId}/${instanceId}.result.md`);
|
|
1080
2502
|
try {
|
|
1081
2503
|
fs.mkdirSync(path.dirname(resultPath), { recursive: true });
|
|
@@ -1123,7 +2545,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1123
2545
|
return;
|
|
1124
2546
|
}
|
|
1125
2547
|
const flowArchived = Boolean(payload.flowArchived);
|
|
1126
|
-
const result = writeFlowYaml(root, flowId, flowSource, flowYaml, { archived: flowArchived });
|
|
2548
|
+
const result = writeFlowYaml(root, flowId, flowSource, flowYaml, { archived: flowArchived, ...userCtx });
|
|
1127
2549
|
if (!result.success) {
|
|
1128
2550
|
json(res, 400, result);
|
|
1129
2551
|
return;
|
|
@@ -1151,7 +2573,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1151
2573
|
return;
|
|
1152
2574
|
}
|
|
1153
2575
|
const flowArchived = Boolean(payload.flowArchived);
|
|
1154
|
-
broadcastFlowEditorSync(flowId, flowSource, flowArchived);
|
|
2576
|
+
broadcastFlowEditorSync(flowId, flowSource, flowArchived, userCtx.userId);
|
|
1155
2577
|
json(res, 200, { ok: true });
|
|
1156
2578
|
return;
|
|
1157
2579
|
}
|
|
@@ -1168,7 +2590,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1168
2590
|
return;
|
|
1169
2591
|
}
|
|
1170
2592
|
const flowArchived = url.searchParams.get("archived") === "1";
|
|
1171
|
-
const key = flowEditorSyncKey(flowId, flowSource, flowArchived);
|
|
2593
|
+
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
|
|
1172
2594
|
let set = flowEditorSyncSubscribers.get(key);
|
|
1173
2595
|
if (!set) {
|
|
1174
2596
|
set = new Set();
|
|
@@ -1202,7 +2624,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1202
2624
|
return;
|
|
1203
2625
|
}
|
|
1204
2626
|
const flowArchived = url.searchParams.get("archived") === "1";
|
|
1205
|
-
const key = flowEditorSyncKey(flowId, flowSource, flowArchived);
|
|
2627
|
+
const key = flowEditorSyncKey(flowId, flowSource, flowArchived, userCtx.userId);
|
|
1206
2628
|
const serverVer = flowEditorSyncVersions.get(key) ?? 0;
|
|
1207
2629
|
const clientVer = parseInt(url.searchParams.get("v") ?? "0", 10) || 0;
|
|
1208
2630
|
json(res, 200, { version: serverVer, changed: serverVer > clientVer });
|
|
@@ -1232,7 +2654,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1232
2654
|
json(res, 400, { error: "Invalid toSource" });
|
|
1233
2655
|
return;
|
|
1234
2656
|
}
|
|
1235
|
-
const result = moveFlowDirectory(root, flowId.trim(), fromSource, toSource);
|
|
2657
|
+
const result = moveFlowDirectory(root, flowId.trim(), fromSource, toSource, userCtx);
|
|
1236
2658
|
if (!result.success) {
|
|
1237
2659
|
json(res, 400, { error: result.error || "Move failed" });
|
|
1238
2660
|
return;
|
|
@@ -1269,7 +2691,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1269
2691
|
json(res, 200, { success: true, flowId, flowSource });
|
|
1270
2692
|
return;
|
|
1271
2693
|
}
|
|
1272
|
-
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: false });
|
|
2694
|
+
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: false, ...userCtx });
|
|
1273
2695
|
if (yamlRes.error || !yamlRes.path) {
|
|
1274
2696
|
json(res, 404, { error: yamlRes.error || "找不到流水线" });
|
|
1275
2697
|
return;
|
|
@@ -1312,7 +2734,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1312
2734
|
json(res, 400, { error: "仅支持归档用户目录或工作区流水线" });
|
|
1313
2735
|
return;
|
|
1314
2736
|
}
|
|
1315
|
-
const result = archiveFlowPipeline(root, flowId, flowSource);
|
|
2737
|
+
const result = archiveFlowPipeline(root, flowId, flowSource, userCtx);
|
|
1316
2738
|
if (!result.success) {
|
|
1317
2739
|
json(res, 400, { error: result.error || "归档失败" });
|
|
1318
2740
|
return;
|
|
@@ -1345,7 +2767,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1345
2767
|
json(res, 400, { error: "仅支持删除用户目录或工作区流水线" });
|
|
1346
2768
|
return;
|
|
1347
2769
|
}
|
|
1348
|
-
const result = deleteFlowPipeline(root, flowId, flowSource, { archived: flowArchived });
|
|
2770
|
+
const result = deleteFlowPipeline(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1349
2771
|
if (!result.success) {
|
|
1350
2772
|
json(res, 400, { error: result.error || "删除失败" });
|
|
1351
2773
|
return;
|
|
@@ -1366,7 +2788,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1366
2788
|
json(res, 400, { error: "Invalid flowSource" });
|
|
1367
2789
|
return;
|
|
1368
2790
|
}
|
|
1369
|
-
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived });
|
|
2791
|
+
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1370
2792
|
if (yamlRes.error) {
|
|
1371
2793
|
json(res, 404, { error: yamlRes.error });
|
|
1372
2794
|
return;
|
|
@@ -1407,7 +2829,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1407
2829
|
json(res, 400, { error: "Cannot save config to builtin or archived flow" });
|
|
1408
2830
|
return;
|
|
1409
2831
|
}
|
|
1410
|
-
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived });
|
|
2832
|
+
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1411
2833
|
if (yamlRes.error) {
|
|
1412
2834
|
json(res, 404, { error: yamlRes.error });
|
|
1413
2835
|
return;
|
|
@@ -1437,12 +2859,12 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1437
2859
|
json(res, 400, { error: "Invalid flowSource" });
|
|
1438
2860
|
return;
|
|
1439
2861
|
}
|
|
1440
|
-
const result = readFlowSchedule(root, flowId, flowSource, { archived: flowArchived });
|
|
2862
|
+
const result = readFlowSchedule(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1441
2863
|
if (!result.success) {
|
|
1442
2864
|
json(res, 400, { error: result.error || "Could not read schedule" });
|
|
1443
2865
|
return;
|
|
1444
2866
|
}
|
|
1445
|
-
const status = listScheduleStatuses(root).find(
|
|
2867
|
+
const status = listScheduleStatuses(root, userCtx).find(
|
|
1446
2868
|
(s) => s.flowId === flowId && (s.flowSource || "user") === (flowSource || "user"),
|
|
1447
2869
|
);
|
|
1448
2870
|
json(res, 200, { schedule: result.schedule, state: result.state || {}, status: status || null });
|
|
@@ -1468,7 +2890,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1468
2890
|
json(res, 400, { error: "Cannot save schedule to builtin or archived flow" });
|
|
1469
2891
|
return;
|
|
1470
2892
|
}
|
|
1471
|
-
const result = writeFlowSchedule(root, flowId, flowSource, payload.schedule || {});
|
|
2893
|
+
const result = writeFlowSchedule(root, flowId, flowSource, payload.schedule || {}, userCtx);
|
|
1472
2894
|
if (!result.success) {
|
|
1473
2895
|
json(res, 400, { error: result.error || "Could not save schedule" });
|
|
1474
2896
|
return;
|
|
@@ -1491,7 +2913,8 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1491
2913
|
return;
|
|
1492
2914
|
}
|
|
1493
2915
|
const runUuid = typeof payload.uuid === "string" ? payload.uuid.trim() : "";
|
|
1494
|
-
|
|
2916
|
+
const runKey = `${userCtx.userId || ""}:${payload.flowSource || "user"}:${flowId}`;
|
|
2917
|
+
if (activeFlowRuns.has(runKey)) {
|
|
1495
2918
|
json(res, 409, { error: "该流水线已在运行中" });
|
|
1496
2919
|
return;
|
|
1497
2920
|
}
|
|
@@ -1500,7 +2923,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1500
2923
|
// UI 轮询会把 runMode 翻回 stopped,即便 CLI 正在运行也显示 PAUSED。
|
|
1501
2924
|
if (runUuid) {
|
|
1502
2925
|
try {
|
|
1503
|
-
const runDir = getRunDir(root, flowId, runUuid);
|
|
2926
|
+
const runDir = getRunDir(root, flowId, runUuid, userCtx);
|
|
1504
2927
|
const interruptedPath = path.join(runDir, RUN_INTERRUPTED_FILENAME);
|
|
1505
2928
|
if (fs.existsSync(interruptedPath)) fs.unlinkSync(interruptedPath);
|
|
1506
2929
|
} catch (e) {
|
|
@@ -1542,7 +2965,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1542
2965
|
const endSafe = () => {
|
|
1543
2966
|
if (responseEnded) return;
|
|
1544
2967
|
responseEnded = true;
|
|
1545
|
-
activeFlowRuns.delete(
|
|
2968
|
+
activeFlowRuns.delete(runKey);
|
|
1546
2969
|
try {
|
|
1547
2970
|
res.end();
|
|
1548
2971
|
} catch (_) {}
|
|
@@ -1557,7 +2980,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1557
2980
|
child = spawn(process.execPath, args, {
|
|
1558
2981
|
cwd: root,
|
|
1559
2982
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1560
|
-
env: {
|
|
2983
|
+
env: runtimeEnvForUser(userCtx, { FORCE_COLOR: "0" }),
|
|
1561
2984
|
// detached: true 使 child 成为新进程组 leader,/api/flow/run/stop 时
|
|
1562
2985
|
// 用 process.kill(-pid) 可以一次性 SIGTERM 整棵进程树(含 cursor-agent 等孙进程)
|
|
1563
2986
|
detached: true,
|
|
@@ -1570,7 +2993,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1570
2993
|
|
|
1571
2994
|
/** @type {{ child: import("child_process").ChildProcess, runUuid: string | null }} */
|
|
1572
2995
|
const runEntry = { child, runUuid: runUuid || null };
|
|
1573
|
-
activeFlowRuns.set(
|
|
2996
|
+
activeFlowRuns.set(runKey, runEntry);
|
|
1574
2997
|
log.debug(`[ui] flow/run: spawned pid=${child.pid} flowId=${flowId}${runUuid ? ` uuid=${runUuid}` : ""}`);
|
|
1575
2998
|
|
|
1576
2999
|
let stdoutBuf = "";
|
|
@@ -1643,7 +3066,8 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1643
3066
|
json(res, 400, { error: "Missing flowId" });
|
|
1644
3067
|
return;
|
|
1645
3068
|
}
|
|
1646
|
-
const
|
|
3069
|
+
const runKey = `${userCtx.userId || ""}:${payload.flowSource || "user"}:${flowId}`;
|
|
3070
|
+
const entry = activeFlowRuns.get(runKey);
|
|
1647
3071
|
if (!entry || !entry.child) {
|
|
1648
3072
|
json(res, 404, { error: "该流水线未在运行" });
|
|
1649
3073
|
return;
|
|
@@ -1661,10 +3085,10 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1661
3085
|
try { entry.child.kill("SIGTERM"); } catch (_) {}
|
|
1662
3086
|
}
|
|
1663
3087
|
const uuid = entry.runUuid;
|
|
1664
|
-
activeFlowRuns.delete(
|
|
3088
|
+
activeFlowRuns.delete(runKey);
|
|
1665
3089
|
if (uuid) {
|
|
1666
3090
|
try {
|
|
1667
|
-
const runDir = getRunDir(root, flowId, uuid);
|
|
3091
|
+
const runDir = getRunDir(root, flowId, uuid, userCtx);
|
|
1668
3092
|
fs.mkdirSync(runDir, { recursive: true });
|
|
1669
3093
|
fs.writeFileSync(
|
|
1670
3094
|
path.join(runDir, RUN_INTERRUPTED_FILENAME),
|
|
@@ -1684,6 +3108,38 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1684
3108
|
return;
|
|
1685
3109
|
}
|
|
1686
3110
|
|
|
3111
|
+
if (req.method === "GET" && url.pathname === "/api/skill-collections") {
|
|
3112
|
+
json(res, 200, readSkillCollectionConfig(userCtx, listComposerSkills(PACKAGE_ROOT, root)));
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
if (req.method === "POST" && url.pathname === "/api/skill-collections") {
|
|
3117
|
+
let payload;
|
|
3118
|
+
try {
|
|
3119
|
+
payload = JSON.parse(await readBody(req));
|
|
3120
|
+
} catch {
|
|
3121
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
try {
|
|
3125
|
+
json(res, 200, writeSkillCollectionConfig(userCtx, payload, listComposerSkills(PACKAGE_ROOT, root)));
|
|
3126
|
+
} catch (e) {
|
|
3127
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
3128
|
+
}
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
if (req.method === "GET" && url.pathname === "/api/skills/detail") {
|
|
3133
|
+
const key = url.searchParams.get("key") || url.searchParams.get("name") || "";
|
|
3134
|
+
const detail = readComposerSkillDetail(PACKAGE_ROOT, root, key);
|
|
3135
|
+
if (!detail) {
|
|
3136
|
+
json(res, 404, { error: "Skill not found" });
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
json(res, 200, { skill: detail });
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
1687
3143
|
if (req.method === "POST" && url.pathname === "/api/composer-agent") {
|
|
1688
3144
|
let payload;
|
|
1689
3145
|
try {
|
|
@@ -1739,7 +3195,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1739
3195
|
return;
|
|
1740
3196
|
}
|
|
1741
3197
|
const flowArchived = Boolean(payload.flowArchived);
|
|
1742
|
-
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived });
|
|
3198
|
+
const yamlRes = getFlowYamlAbs(root, flowId, flowSource, { archived: flowArchived, ...userCtx });
|
|
1743
3199
|
if (yamlRes.error || !yamlRes.path) {
|
|
1744
3200
|
json(res, 400, { error: yamlRes.error || "Could not resolve flow.yaml" });
|
|
1745
3201
|
return;
|
|
@@ -1750,7 +3206,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1750
3206
|
let editorSyncFlowSource = flowSource;
|
|
1751
3207
|
let flowDirForCli = path.dirname(flowYamlAbs);
|
|
1752
3208
|
if (flowSource === "builtin") {
|
|
1753
|
-
const w = resolveFlowDirForWrite(root, flowId, "workspace");
|
|
3209
|
+
const w = resolveFlowDirForWrite(root, flowId, "workspace", userCtx);
|
|
1754
3210
|
if (w.error || !w.flowDir) {
|
|
1755
3211
|
json(res, 400, { error: w.error || "Could not resolve workspace flow directory" });
|
|
1756
3212
|
return;
|
|
@@ -1783,6 +3239,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1783
3239
|
flowYamlAbs,
|
|
1784
3240
|
flowId,
|
|
1785
3241
|
flowSource,
|
|
3242
|
+
userId: userCtx.userId || "",
|
|
1786
3243
|
intents: multiStepIntents,
|
|
1787
3244
|
canvasInstanceIds: instanceIds,
|
|
1788
3245
|
skillsHint: multiStepResources.skillsHint,
|
|
@@ -1939,6 +3396,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1939
3396
|
thread,
|
|
1940
3397
|
phaseContext,
|
|
1941
3398
|
phaseRole: phaseRole || undefined,
|
|
3399
|
+
agentflowUserId: userCtx.userId || "",
|
|
1942
3400
|
force: true,
|
|
1943
3401
|
onStreamEvent,
|
|
1944
3402
|
});
|
|
@@ -1952,7 +3410,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1952
3410
|
flowSource: flowSource || null,
|
|
1953
3411
|
});
|
|
1954
3412
|
if (flowId && flowSource) {
|
|
1955
|
-
broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived));
|
|
3413
|
+
broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived), userCtx.userId);
|
|
1956
3414
|
}
|
|
1957
3415
|
try { res.write(JSON.stringify({ type: "done" }) + "\n"); } catch (_) {}
|
|
1958
3416
|
}
|
|
@@ -1989,6 +3447,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
1989
3447
|
cliWorkspace,
|
|
1990
3448
|
prompt: finalPrompt,
|
|
1991
3449
|
modelKey: typeof model === "string" ? model.trim() : "",
|
|
3450
|
+
agentflowUserId: userCtx.userId || "",
|
|
1992
3451
|
onStreamEvent,
|
|
1993
3452
|
});
|
|
1994
3453
|
child = handle.child;
|
|
@@ -2007,6 +3466,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
2007
3466
|
flowYamlAbs,
|
|
2008
3467
|
flowContext: flowContextForMultiStep,
|
|
2009
3468
|
modelKey: typeof model === "string" ? model.trim() : "",
|
|
3469
|
+
agentflowUserId: userCtx.userId || "",
|
|
2010
3470
|
force: true,
|
|
2011
3471
|
onStreamEvent,
|
|
2012
3472
|
getAborted: () => clientDisconnected || responseEnded,
|
|
@@ -2029,7 +3489,7 @@ finishedAt: "${new Date().toISOString()}"
|
|
|
2029
3489
|
flowSource: flowSource || null,
|
|
2030
3490
|
});
|
|
2031
3491
|
if (flowYamlChanged && flowId && flowSource) {
|
|
2032
|
-
broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived));
|
|
3492
|
+
broadcastFlowEditorSync(flowId, flowSource, Boolean(payload.flowArchived), userCtx.userId);
|
|
2033
3493
|
}
|
|
2034
3494
|
try { res.write(JSON.stringify({ type: "done" }) + "\n"); } catch (_) {}
|
|
2035
3495
|
}
|