@fieldwangai/agentflow 0.1.39 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/agent-runners.mjs +2 -2
- package/bin/lib/apply.mjs +2 -2
- package/bin/lib/composer-agent.mjs +63 -3
- package/bin/lib/scheduler.mjs +3 -3
- package/bin/lib/ui-server.mjs +323 -54
- package/bin/lib/user-env.mjs +32 -2
- package/bin/pipeline/run-tool-nodejs.mjs +2 -2
- package/builtin/web-ui/dist/assets/index-DyBxNnlo.css +1 -0
- package/builtin/web-ui/dist/assets/index-TXmzGUhf.js +218 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-B7-hwwU7.css +0 -1
- package/builtin/web-ui/dist/assets/index-Cchb13X1.js +0 -218
|
@@ -9,7 +9,7 @@ import { normalizeCursorModelForCli } from "./model-config.mjs";
|
|
|
9
9
|
import { appendRunLogLine } from "./run-events.mjs";
|
|
10
10
|
import { writeWithPrefix } from "./terminal.mjs";
|
|
11
11
|
import { t } from "./i18n.mjs";
|
|
12
|
-
import {
|
|
12
|
+
import { readMergedEnvObject } from "./user-env.mjs";
|
|
13
13
|
import { outputNodeBasename } from "../pipeline/get-exec-id.mjs";
|
|
14
14
|
|
|
15
15
|
function shouldPassCursorModelArg(model) {
|
|
@@ -20,7 +20,7 @@ function shouldPassCursorModelArg(model) {
|
|
|
20
20
|
function childEnv(options = {}, extra = {}) {
|
|
21
21
|
const optEnv = options && options.env && typeof options.env === "object" ? options.env : {};
|
|
22
22
|
const userId = optEnv.AGENTFLOW_USER_ID || process.env.AGENTFLOW_USER_ID || "";
|
|
23
|
-
return { ...process.env, ...
|
|
23
|
+
return { ...process.env, ...readMergedEnvObject(userId), ...optEnv, ...extra };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, text) {
|
package/bin/lib/apply.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { formatDuration } from "./terminal.mjs";
|
|
|
23
23
|
import { printEntryAndFlowFiles, printNodeStatusTable, runValidateFlowAndExitIfInvalid } from "./ui-print.mjs";
|
|
24
24
|
import { clearApplyActiveLock, writeApplyActiveLock } from "./run-apply-active-lock.mjs";
|
|
25
25
|
import { ensureReference, findFlowNameByUuid, getFlowDir, getRunDir } from "./workspace.mjs";
|
|
26
|
-
import {
|
|
26
|
+
import { readMergedEnvObject } from "./user-env.mjs";
|
|
27
27
|
|
|
28
28
|
const PARALLEL_PREFIX_COLORS = [
|
|
29
29
|
(s) => chalk.cyan(s),
|
|
@@ -347,7 +347,7 @@ ${currentContent}
|
|
|
347
347
|
|
|
348
348
|
const result = spawnSync(opencodeCmd, ["--prompt-file", tmpPromptFile, "--print"], {
|
|
349
349
|
cwd: workspaceRoot,
|
|
350
|
-
env: { ...process.env, ...
|
|
350
|
+
env: { ...process.env, ...readMergedEnvObject(process.env.AGENTFLOW_USER_ID || ""), OPENCODE_NON_INTERACTIVE: "1" },
|
|
351
351
|
stdio: ["ignore", "pipe", "pipe"],
|
|
352
352
|
});
|
|
353
353
|
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* 用户 prompt → planner 分解 → [script 直执 | agent 子调用(按复杂度选模型)] → sync UI
|
|
6
6
|
*/
|
|
7
7
|
import fs from "fs";
|
|
8
|
+
import os from "os";
|
|
8
9
|
import path from "path";
|
|
9
|
-
import { getAgentflowDataRoot, sanitizeAgentflowUserId } from "./paths.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { getAgentflowDataRoot, getAgentflowUserDataRoot, sanitizeAgentflowUserId } from "./paths.mjs";
|
|
11
|
+
import { readMergedEnvObject } from "./user-env.mjs";
|
|
11
12
|
import { resolveCliAndModel } from "./model-config.mjs";
|
|
12
13
|
import { runClaudeCodeAgentWithPrompt, runCursorAgentWithPrompt, runOpenCodeAgentWithPrompt } from "./agent-runners.mjs";
|
|
13
14
|
import { planComposerTasks, hasPlannerApiAvailable, shouldUsePhased, classifyComplexity, classifyTaskComplexity, PHASED_DEFINITIONS } from "./composer-planner.mjs";
|
|
@@ -24,9 +25,68 @@ const MAX_PROMPT_CHARS = 500_000;
|
|
|
24
25
|
const MAX_COMPOSER_VALIDATION_REPAIR = 5;
|
|
25
26
|
const MAX_SCRIPT_INJECT_BYTES = 30_000;
|
|
26
27
|
|
|
28
|
+
function readJsonObject(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
if (!fs.existsSync(filePath)) return {};
|
|
31
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
32
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readUserMcpPrivateEnvObject(userId) {
|
|
39
|
+
const safe = sanitizeAgentflowUserId(userId);
|
|
40
|
+
const data = readJsonObject(path.join(getAgentflowUserDataRoot(safe), "mcp-private.json"));
|
|
41
|
+
const servers = data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
|
|
42
|
+
const env = {};
|
|
43
|
+
for (const server of Object.values(servers)) {
|
|
44
|
+
const serverEnv = server?.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
|
|
45
|
+
for (const [key, value] of Object.entries(serverEnv)) {
|
|
46
|
+
const envKey = String(key || "").trim();
|
|
47
|
+
if (envKey) env[envKey] = String(value ?? "");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return env;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pruneCursorMcpPrivateEnvPlaceholders() {
|
|
54
|
+
const filePath = path.join(os.homedir(), ".cursor", "mcp.json");
|
|
55
|
+
const config = readJsonObject(filePath);
|
|
56
|
+
const servers = config?.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
57
|
+
? config.mcpServers
|
|
58
|
+
: null;
|
|
59
|
+
if (!servers) return;
|
|
60
|
+
let changed = false;
|
|
61
|
+
const nextServers = {};
|
|
62
|
+
for (const [name, raw] of Object.entries(servers)) {
|
|
63
|
+
const server = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...raw } : raw;
|
|
64
|
+
const privateEnvKeys = Array.isArray(server?.__agentflowPrivateKeys?.env)
|
|
65
|
+
? server.__agentflowPrivateKeys.env.map((key) => String(key || "").trim()).filter(Boolean)
|
|
66
|
+
: [];
|
|
67
|
+
if (!privateEnvKeys.length || !server?.env || typeof server.env !== "object" || Array.isArray(server.env)) {
|
|
68
|
+
nextServers[name] = server;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const nextEnv = { ...server.env };
|
|
72
|
+
for (const key of privateEnvKeys) {
|
|
73
|
+
if (Object.prototype.hasOwnProperty.call(nextEnv, key) && String(nextEnv[key] ?? "") === "") {
|
|
74
|
+
delete nextEnv[key];
|
|
75
|
+
changed = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
nextServers[name] = { ...server, env: nextEnv };
|
|
79
|
+
if (Object.keys(nextEnv).length === 0) delete nextServers[name].env;
|
|
80
|
+
}
|
|
81
|
+
if (!changed) return;
|
|
82
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
83
|
+
fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
|
|
84
|
+
}
|
|
85
|
+
|
|
27
86
|
function agentflowUserEnv(userId) {
|
|
28
87
|
const safe = sanitizeAgentflowUserId(userId);
|
|
29
|
-
|
|
88
|
+
pruneCursorMcpPrivateEnvPlaceholders();
|
|
89
|
+
return { ...readMergedEnvObject(safe), ...(safe ? readUserMcpPrivateEnvObject(safe) : {}), AGENTFLOW_USER_ID: safe };
|
|
30
90
|
}
|
|
31
91
|
|
|
32
92
|
// ─── script 内容注入辅助 ─────────────────────────────────────────────────
|
package/bin/lib/scheduler.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { getAgentflowUserContexts, getRunDir, PACKAGE_ROOT } from "./paths.mjs";
|
|
13
13
|
import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
|
|
14
14
|
import { log } from "./log.mjs";
|
|
15
|
-
import {
|
|
15
|
+
import { readMergedEnvObject } from "./user-env.mjs";
|
|
16
16
|
import { writeResult } from "../pipeline/write-result.mjs";
|
|
17
17
|
|
|
18
18
|
const DEFAULT_POLL_MS = 30_000;
|
|
@@ -258,7 +258,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state, opts = {}) {
|
|
|
258
258
|
const child = spawn(process.execPath, args, {
|
|
259
259
|
cwd: path.resolve(workspaceRoot),
|
|
260
260
|
stdio: ["ignore", "pipe", "pipe"],
|
|
261
|
-
env: { ...process.env, ...
|
|
261
|
+
env: { ...process.env, ...readMergedEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
|
|
262
262
|
detached: true,
|
|
263
263
|
});
|
|
264
264
|
|
|
@@ -331,7 +331,7 @@ function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
|
|
|
331
331
|
const child = spawn(process.execPath, args, {
|
|
332
332
|
cwd: path.resolve(workspaceRoot),
|
|
333
333
|
stdio: ["ignore", "pipe", "pipe"],
|
|
334
|
-
env: { ...process.env, ...
|
|
334
|
+
env: { ...process.env, ...readMergedEnvObject(opts.userId), FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
|
|
335
335
|
detached: true,
|
|
336
336
|
});
|
|
337
337
|
child.stdout.on("data", () => {});
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -97,7 +97,7 @@ import {
|
|
|
97
97
|
logoutRequest,
|
|
98
98
|
readUserAllowlist,
|
|
99
99
|
} from "./auth.mjs";
|
|
100
|
-
import {
|
|
100
|
+
import { readGlobalEnvRows, readMergedEnvObject, readUserEnvRows, writeGlobalEnvRows, writeUserEnvRows } from "./user-env.mjs";
|
|
101
101
|
import {
|
|
102
102
|
readAdminBuiltinPipelineConfig,
|
|
103
103
|
updateAdminBuiltinPipelineConfig,
|
|
@@ -190,7 +190,6 @@ function writeFeedbackItems(items) {
|
|
|
190
190
|
function createFeedbackItem(payload, user) {
|
|
191
191
|
const title = String(payload?.title || "").trim().slice(0, 120);
|
|
192
192
|
const content = String(payload?.content || "").trim().slice(0, 5000);
|
|
193
|
-
const contact = String(payload?.contact || "").trim().slice(0, 160);
|
|
194
193
|
const pageUrl = String(payload?.pageUrl || "").trim().slice(0, 500);
|
|
195
194
|
if (!title) return { error: "Missing feedback title" };
|
|
196
195
|
if (!content) return { error: "Missing feedback content" };
|
|
@@ -199,7 +198,6 @@ function createFeedbackItem(payload, user) {
|
|
|
199
198
|
id: `fb_${Date.now().toString(36)}_${crypto.randomBytes(5).toString("hex")}`,
|
|
200
199
|
title,
|
|
201
200
|
content,
|
|
202
|
-
contact,
|
|
203
201
|
pageUrl,
|
|
204
202
|
userId: String(user?.userId || ""),
|
|
205
203
|
username: String(user?.username || user?.userId || ""),
|
|
@@ -390,7 +388,7 @@ function removeSkillhubCollectionGroup(userCtx = {}, collectionId = "", root = p
|
|
|
390
388
|
function runtimeEnvForUser(userCtx = {}, extra = {}) {
|
|
391
389
|
return {
|
|
392
390
|
...process.env,
|
|
393
|
-
...
|
|
391
|
+
...readMergedEnvObject(userCtx.userId),
|
|
394
392
|
...extra,
|
|
395
393
|
AGENTFLOW_USER_ID: userCtx.userId || "",
|
|
396
394
|
};
|
|
@@ -565,8 +563,6 @@ function writeCursorMcpServer(payload = {}, userCtx = {}) {
|
|
|
565
563
|
env: omitObjectKeys(server.env || {}, privateEnvKeys),
|
|
566
564
|
headers: omitObjectKeys(server.headers || {}, privateHeaderKeys),
|
|
567
565
|
};
|
|
568
|
-
publicServer.env = withPrivatePlaceholders(publicServer.env, privateEnvKeys);
|
|
569
|
-
publicServer.headers = withPrivatePlaceholders(publicServer.headers, privateHeaderKeys);
|
|
570
566
|
if (privateEnvKeys.size || privateHeaderKeys.size) {
|
|
571
567
|
publicServer.__agentflowPrivateKeys = {
|
|
572
568
|
...(privateEnvKeys.size ? { env: Array.from(privateEnvKeys) } : {}),
|
|
@@ -1369,6 +1365,188 @@ function workspaceSetOutputSlot(instance, name, value) {
|
|
|
1369
1365
|
};
|
|
1370
1366
|
}
|
|
1371
1367
|
|
|
1368
|
+
function workspaceSourceSlotForEdge(graph, edge) {
|
|
1369
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1370
|
+
const source = instances[String(edge?.source || "")];
|
|
1371
|
+
const output = Array.isArray(source?.output) ? source.output : [];
|
|
1372
|
+
return output[workspaceHandleIndex(edge?.sourceHandle, "output")] || null;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function workspaceOutputSlotValueForEdge(graph, outputs, edge) {
|
|
1376
|
+
const sourceId = String(edge?.source || "");
|
|
1377
|
+
const slot = workspaceSourceSlotForEdge(graph, edge);
|
|
1378
|
+
if (slot && String(slot?.type || "") !== "node") {
|
|
1379
|
+
const value = workspaceSlotValue(slot);
|
|
1380
|
+
if (value.trim()) return value;
|
|
1381
|
+
}
|
|
1382
|
+
const out = outputs.get(sourceId);
|
|
1383
|
+
if (out != null && String(out).trim()) return String(out);
|
|
1384
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1385
|
+
return workspaceInstanceText(instances[sourceId]);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function workspaceParseJsonObjectFromText(text) {
|
|
1389
|
+
const raw = String(text || "").trim();
|
|
1390
|
+
if (!raw) return null;
|
|
1391
|
+
const candidates = [raw];
|
|
1392
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1393
|
+
if (fenced?.[1]) candidates.unshift(fenced[1].trim());
|
|
1394
|
+
const first = raw.indexOf("{");
|
|
1395
|
+
const last = raw.lastIndexOf("}");
|
|
1396
|
+
if (first >= 0 && last > first) candidates.unshift(raw.slice(first, last + 1));
|
|
1397
|
+
for (const candidate of candidates) {
|
|
1398
|
+
try {
|
|
1399
|
+
const parsed = JSON.parse(candidate);
|
|
1400
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
1401
|
+
} catch {
|
|
1402
|
+
/* try next */
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function workspaceStringifyOutputValue(value) {
|
|
1409
|
+
if (value == null) return "";
|
|
1410
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function workspaceUnescapeLooseJsonString(value) {
|
|
1414
|
+
return String(value ?? "")
|
|
1415
|
+
.replace(/\\n/g, "\n")
|
|
1416
|
+
.replace(/\\r/g, "\r")
|
|
1417
|
+
.replace(/\\t/g, "\t")
|
|
1418
|
+
.replace(/\\"/g, '"')
|
|
1419
|
+
.replace(/\\\\/g, "\\")
|
|
1420
|
+
.trim();
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function workspaceExtractLooseOutParams(raw) {
|
|
1424
|
+
const text = String(raw || "");
|
|
1425
|
+
const out = {};
|
|
1426
|
+
const startMatch = /["']outParams["']\s*:\s*\{/i.exec(text);
|
|
1427
|
+
if (!startMatch) return out;
|
|
1428
|
+
const start = startMatch.index + startMatch[0].length;
|
|
1429
|
+
const end = text.indexOf("}", start);
|
|
1430
|
+
const block = end >= start ? text.slice(start, end) : text.slice(start);
|
|
1431
|
+
const pairRe = /["']?([A-Za-z_][A-Za-z0-9_-]*)["']?\s*:\s*(?:"([^"]*)"|'([^']*)'|([^,\n\r}]+))/g;
|
|
1432
|
+
let match;
|
|
1433
|
+
while ((match = pairRe.exec(block))) {
|
|
1434
|
+
const key = String(match[1] || "").trim();
|
|
1435
|
+
const value = match[2] ?? match[3] ?? match[4] ?? "";
|
|
1436
|
+
if (key) out[key] = workspaceUnescapeLooseJsonString(value).replace(/^["'`]|["'`]$/g, "").trim();
|
|
1437
|
+
}
|
|
1438
|
+
return out;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function workspaceExtractLooseResult(raw) {
|
|
1442
|
+
const text = String(raw || "").trim();
|
|
1443
|
+
const resultMatch = /["']result["']\s*:\s*(["'])/i.exec(text);
|
|
1444
|
+
if (!resultMatch) return "";
|
|
1445
|
+
const quote = resultMatch[1];
|
|
1446
|
+
const start = resultMatch.index + resultMatch[0].length;
|
|
1447
|
+
const outParamsMatch = /,\s*["']outParams["']\s*:/i.exec(text.slice(start));
|
|
1448
|
+
if (outParamsMatch) {
|
|
1449
|
+
const end = start + outParamsMatch.index;
|
|
1450
|
+
let value = text.slice(start, end).trim();
|
|
1451
|
+
if (value.endsWith(quote)) value = value.slice(0, -1);
|
|
1452
|
+
return workspaceUnescapeLooseJsonString(value);
|
|
1453
|
+
}
|
|
1454
|
+
const end = text.lastIndexOf(quote);
|
|
1455
|
+
if (end > start) return workspaceUnescapeLooseJsonString(text.slice(start, end));
|
|
1456
|
+
return "";
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function workspaceStructuredAgentOutput(content) {
|
|
1460
|
+
const raw = String(content || "").trim();
|
|
1461
|
+
const parsed = workspaceParseJsonObjectFromText(raw);
|
|
1462
|
+
if (!parsed) {
|
|
1463
|
+
const looseResult = workspaceExtractLooseResult(raw);
|
|
1464
|
+
const looseOutParams = workspaceExtractLooseOutParams(raw);
|
|
1465
|
+
if (looseResult || Object.keys(looseOutParams).length) {
|
|
1466
|
+
return {
|
|
1467
|
+
result: looseResult || raw,
|
|
1468
|
+
outParams: looseOutParams,
|
|
1469
|
+
structured: true,
|
|
1470
|
+
parsed: null,
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
return { result: raw, outParams: {}, structured: false, parsed: null };
|
|
1474
|
+
}
|
|
1475
|
+
const hasEnvelope = Object.prototype.hasOwnProperty.call(parsed, "result") ||
|
|
1476
|
+
Object.prototype.hasOwnProperty.call(parsed, "outParams");
|
|
1477
|
+
if (!hasEnvelope) return { result: raw, outParams: {}, structured: false, parsed };
|
|
1478
|
+
const outParamsRaw = parsed.outParams && typeof parsed.outParams === "object" && !Array.isArray(parsed.outParams)
|
|
1479
|
+
? parsed.outParams
|
|
1480
|
+
: {};
|
|
1481
|
+
const outParams = {};
|
|
1482
|
+
for (const [key, value] of Object.entries(outParamsRaw)) {
|
|
1483
|
+
const name = String(key || "").trim();
|
|
1484
|
+
if (name) outParams[name] = workspaceStringifyOutputValue(value);
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
result: workspaceStringifyOutputValue(parsed.result ?? ""),
|
|
1488
|
+
outParams,
|
|
1489
|
+
structured: true,
|
|
1490
|
+
parsed,
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function workspaceExtractNamedOutputValue(content, slotName) {
|
|
1495
|
+
const name = String(slotName || "").trim();
|
|
1496
|
+
if (!name) return "";
|
|
1497
|
+
const structured = workspaceStructuredAgentOutput(content);
|
|
1498
|
+
if (Object.prototype.hasOwnProperty.call(structured.outParams, name)) {
|
|
1499
|
+
return String(structured.outParams[name] ?? "");
|
|
1500
|
+
}
|
|
1501
|
+
const parsed = structured.parsed || workspaceParseJsonObjectFromText(content);
|
|
1502
|
+
if (parsed && Object.prototype.hasOwnProperty.call(parsed, name)) {
|
|
1503
|
+
const value = parsed[name];
|
|
1504
|
+
return workspaceStringifyOutputValue(value);
|
|
1505
|
+
}
|
|
1506
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1507
|
+
const looseOutParams = workspaceExtractLooseOutParams(content);
|
|
1508
|
+
if (Object.prototype.hasOwnProperty.call(looseOutParams, name)) {
|
|
1509
|
+
return String(looseOutParams[name] ?? "").trim();
|
|
1510
|
+
}
|
|
1511
|
+
const patterns = [
|
|
1512
|
+
new RegExp(`["']?${escaped}["']?\\s*:\\s*["']?([^"',}\\n\\r]+)`, "i"),
|
|
1513
|
+
new RegExp(`(?:\\$\\{${escaped}\\}|\\$${escaped})\\s*[=::]\\s*([^\\n\\r]+)`, "i"),
|
|
1514
|
+
new RegExp(`(?:^|[\\n\\r])\\s*${escaped}\\s*[=::]\\s*([^\\n\\r]+)`, "i"),
|
|
1515
|
+
];
|
|
1516
|
+
for (const pattern of patterns) {
|
|
1517
|
+
const match = pattern.exec(String(content || ""));
|
|
1518
|
+
if (!match?.[1]) continue;
|
|
1519
|
+
return match[1].replace(/^["'`]|["'`]$/g, "").trim();
|
|
1520
|
+
}
|
|
1521
|
+
return "";
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function workspaceApplyAgentOutputSlots(instance, content) {
|
|
1525
|
+
const structured = workspaceStructuredAgentOutput(content);
|
|
1526
|
+
const text = String(structured.result || "").trim();
|
|
1527
|
+
let changed = false;
|
|
1528
|
+
const next = {
|
|
1529
|
+
...(instance || {}),
|
|
1530
|
+
output: (Array.isArray(instance?.output) ? instance.output : []).map((slot, index) => {
|
|
1531
|
+
const name = String(slot?.name || "").trim();
|
|
1532
|
+
const type = String(slot?.type || "");
|
|
1533
|
+
if (type === "node" || name === "next" || !name) return slot;
|
|
1534
|
+
let value = "";
|
|
1535
|
+
if (name === "result" || name === "content" || index === 0) {
|
|
1536
|
+
value = text;
|
|
1537
|
+
} else if (Object.prototype.hasOwnProperty.call(structured.outParams, name)) {
|
|
1538
|
+
value = structured.outParams[name];
|
|
1539
|
+
} else {
|
|
1540
|
+
value = workspaceExtractNamedOutputValue(text, name);
|
|
1541
|
+
}
|
|
1542
|
+
if (!value) return slot;
|
|
1543
|
+
changed = true;
|
|
1544
|
+
return { ...slot, default: value, value };
|
|
1545
|
+
}),
|
|
1546
|
+
};
|
|
1547
|
+
return { instance: changed ? next : instance, changed };
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1372
1550
|
function workspaceResolvePath(baseCwd, raw) {
|
|
1373
1551
|
const text = String(raw || "").trim();
|
|
1374
1552
|
if (!text) return "";
|
|
@@ -1407,6 +1585,60 @@ function workspaceDisplayKind(definitionId) {
|
|
|
1407
1585
|
return "";
|
|
1408
1586
|
}
|
|
1409
1587
|
|
|
1588
|
+
function workspaceOutputFieldForSlot(slot, index = 0) {
|
|
1589
|
+
const name = String(slot?.name || "").trim();
|
|
1590
|
+
if (!name || name === "result" || name === "content" || index === 0) return "result";
|
|
1591
|
+
return `outParams.${name}`;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function workspaceDisplayKindExample(kind, field) {
|
|
1595
|
+
if (kind === "table") return { columns: ["列名1", "列名2"], rows: [["值1", "值2"]] };
|
|
1596
|
+
if (kind === "chart") return { type: "chart", version: "1.0", renderer: "echarts", option: { xAxis: { type: "category", data: [] }, yAxis: { type: "value" }, series: [{ type: "bar", data: [] }] } };
|
|
1597
|
+
if (kind === "html") return "<可直接渲染的 HTML>";
|
|
1598
|
+
if (kind === "mermaid") return "flowchart TD\n A[开始] --> B[结束]";
|
|
1599
|
+
if (kind === "ascii") return "+---+\n| |\n+---+";
|
|
1600
|
+
if (kind === "image") return "<图片 URL 或 data URL>";
|
|
1601
|
+
if (kind === "markdown") return field === "result" ? "<给用户看的完整 Markdown 正文>" : "<Markdown 正文>";
|
|
1602
|
+
return `<${field} 的值>`;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function workspaceDisplayFieldRule(kind, field, slotName = "") {
|
|
1606
|
+
const slotText = field === "result" ? "`result` 字段" : `\`${field}\``;
|
|
1607
|
+
const prefix = slotName ? `- 输出引脚 \`${slotName}\` 连接了 ${kind} 展示节点:` : `- 下游连接了 ${kind} 展示节点:`;
|
|
1608
|
+
if (kind === "html") return `${prefix}将可直接放入 iframe 渲染的 HTML 放在 ${slotText} 中。可以是完整 HTML 文档或 HTML fragment;不要使用 Markdown 代码围栏。`;
|
|
1609
|
+
if (kind === "markdown") return `${prefix}将 Markdown 正文放在 ${slotText} 中;除非正文确实需要代码块,否则不要额外包裹代码围栏。`;
|
|
1610
|
+
if (kind === "mermaid") return `${prefix}将 Mermaid 图表代码放在 ${slotText} 中,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。`;
|
|
1611
|
+
if (kind === "ascii") return `${prefix}将纯文本/ASCII 图或表格放在 ${slotText} 中;不要输出 HTML 或 Markdown 装饰。`;
|
|
1612
|
+
if (kind === "image") return `${prefix}将可作为 img src 使用的图片地址、data URL 或 base64 data URL 放在 ${slotText} 中;不要输出 Markdown 图片语法。`;
|
|
1613
|
+
if (kind === "chart") return `${prefix}将 ChartSpec JSON 对象放在 ${slotText} 中。ChartSpec 必须包含 "type":"chart"、"version":"1.0"、"renderer":"echarts"、"option";option.series[].type 只使用 line/bar/pie/scatter/radar/heatmap/tree/treemap/sunburst/sankey/graph/gauge/funnel;不要输出 HTML、script、iframe 或 JS 函数。`;
|
|
1614
|
+
if (kind === "table") return `${prefix}将表格数据放在 ${slotText} 中。推荐格式:{"columns":["列名1","列名2"],"rows":[["值1","值2"]]};也可使用对象数组、Markdown 表格、CSV 或 TSV。不要输出 HTML。`;
|
|
1615
|
+
return `${prefix}将展示内容放在 ${slotText} 中。`;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function workspaceDownstreamOutputDisplayBindings(graph, nodeId) {
|
|
1619
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1620
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
1621
|
+
const source = instances[String(nodeId || "")] || {};
|
|
1622
|
+
const output = Array.isArray(source.output) ? source.output : [];
|
|
1623
|
+
const bindings = [];
|
|
1624
|
+
for (const edge of edges) {
|
|
1625
|
+
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
1626
|
+
const target = instances[String(edge?.target || "")];
|
|
1627
|
+
const kind = workspaceDisplayKind(target?.definitionId);
|
|
1628
|
+
if (!kind) continue;
|
|
1629
|
+
const index = workspaceHandleIndex(edge?.sourceHandle, "output");
|
|
1630
|
+
const slot = output[index] || null;
|
|
1631
|
+
const name = String(slot?.name || "").trim() || (index === 0 ? "result" : `output-${index}`);
|
|
1632
|
+
bindings.push({
|
|
1633
|
+
kind,
|
|
1634
|
+
index,
|
|
1635
|
+
name,
|
|
1636
|
+
field: workspaceOutputFieldForSlot(slot, index),
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
return bindings;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1410
1642
|
function normalizeHtmlDisplayContent(content) {
|
|
1411
1643
|
let text = String(content || "").trim();
|
|
1412
1644
|
if (!text) return "";
|
|
@@ -1441,47 +1673,67 @@ function normalizeHtmlDisplayContent(content) {
|
|
|
1441
1673
|
}
|
|
1442
1674
|
|
|
1443
1675
|
function workspaceDownstreamDisplayRequirements(graph, nodeId) {
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
const
|
|
1447
|
-
for (const edge of edges) {
|
|
1448
|
-
if (String(edge?.source || "") !== String(nodeId)) continue;
|
|
1449
|
-
const target = instances[String(edge?.target || "")];
|
|
1450
|
-
const kind = workspaceDisplayKind(target?.definitionId);
|
|
1451
|
-
if (kind) kinds.add(kind);
|
|
1452
|
-
}
|
|
1453
|
-
if (kinds.size === 0) return "";
|
|
1676
|
+
const bindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
|
|
1677
|
+
if (bindings.length === 0) return "";
|
|
1678
|
+
const seen = new Set();
|
|
1454
1679
|
const rules = [];
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
rules.push(
|
|
1460
|
-
}
|
|
1461
|
-
if (kinds.has("mermaid")) {
|
|
1462
|
-
rules.push("- 下游连接了 Mermaid 展示节点:只输出 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏;不要附加解释。");
|
|
1463
|
-
}
|
|
1464
|
-
if (kinds.has("ascii")) {
|
|
1465
|
-
rules.push("- 下游连接了 ASCII 展示节点:输出纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。");
|
|
1466
|
-
}
|
|
1467
|
-
if (kinds.has("image")) {
|
|
1468
|
-
rules.push("- 下游连接了图片展示节点:输出可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法或解释文字。");
|
|
1469
|
-
}
|
|
1470
|
-
if (kinds.has("chart")) {
|
|
1471
|
-
rules.push('- 下游连接了 Chart 展示节点:只输出 ChartSpec JSON 对象,不要 Markdown 代码围栏,不要解释文字。格式必须包含 `"type":"chart"`、`"version":"1.0"`、`"renderer":"echarts"`、`"option"`;`option.series[].type` 只使用 line/bar/pie/scatter/radar/heatmap/tree/treemap/sunburst/sankey/graph/gauge/funnel;不要输出 HTML、script、iframe 或 JS 函数。');
|
|
1472
|
-
}
|
|
1473
|
-
if (kinds.has("table")) {
|
|
1474
|
-
rules.push('- 下游连接了表格展示节点:优先只输出表格 JSON,不要解释文字。推荐格式:`{"columns":["列名1","列名2"],"rows":[["值1","值2"]]}`;也可输出对象数组、Markdown 表格、CSV 或 TSV。不要输出 HTML。');
|
|
1680
|
+
for (const binding of bindings) {
|
|
1681
|
+
const key = `${binding.field}:${binding.kind}`;
|
|
1682
|
+
if (seen.has(key)) continue;
|
|
1683
|
+
seen.add(key);
|
|
1684
|
+
rules.push(workspaceDisplayFieldRule(binding.kind, binding.field, binding.name));
|
|
1475
1685
|
}
|
|
1476
1686
|
return [
|
|
1477
1687
|
"## 下游输出要求",
|
|
1478
1688
|
"",
|
|
1479
1689
|
...rules,
|
|
1480
1690
|
"",
|
|
1691
|
+
"这些要求按输出引脚分别生效:不要把非 `result` 引脚连接的展示内容误写到 `result`;具名引脚应写入 `outParams.<引脚名>`。",
|
|
1481
1692
|
"如果用户任务与下游展示格式没有冲突,优先满足上述格式要求;如果用户明确指定了其他格式,以用户任务为准。",
|
|
1482
1693
|
].join("\n");
|
|
1483
1694
|
}
|
|
1484
1695
|
|
|
1696
|
+
function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
1697
|
+
const instance = graph?.instances?.[nodeId] || {};
|
|
1698
|
+
const outputSlots = Array.isArray(instance.output) ? instance.output : [];
|
|
1699
|
+
const displayBindings = workspaceDownstreamOutputDisplayBindings(graph, nodeId);
|
|
1700
|
+
const displayByField = new Map();
|
|
1701
|
+
for (const binding of displayBindings) {
|
|
1702
|
+
if (!displayByField.has(binding.field)) displayByField.set(binding.field, binding.kind);
|
|
1703
|
+
}
|
|
1704
|
+
const slots = outputSlots
|
|
1705
|
+
.filter((slot) => {
|
|
1706
|
+
const name = String(slot?.name || "").trim();
|
|
1707
|
+
const type = String(slot?.type || "");
|
|
1708
|
+
return name && type !== "node" && name !== "next" && name !== "result" && name !== "content";
|
|
1709
|
+
})
|
|
1710
|
+
.map((slot) => String(slot.name).trim());
|
|
1711
|
+
const outParamsExample = slots.length
|
|
1712
|
+
? Object.fromEntries(slots.map((name) => [name, workspaceDisplayKindExample(displayByField.get(`outParams.${name}`) || "", name)]))
|
|
1713
|
+
: {};
|
|
1714
|
+
const resultExample = workspaceDisplayKindExample(displayByField.get("result") || "markdown", "result");
|
|
1715
|
+
const slotDisplayRules = displayBindings
|
|
1716
|
+
.map((binding) => `- 输出引脚 \`${binding.name}\` -> ${binding.kind} 展示节点:写入 \`${binding.field}\`。`)
|
|
1717
|
+
.filter((line, index, arr) => arr.indexOf(line) === index);
|
|
1718
|
+
return [
|
|
1719
|
+
"## Workspace 输出协议",
|
|
1720
|
+
"",
|
|
1721
|
+
"最终回复必须是一个 JSON 对象,必须直接以 `{` 开头并以 `}` 结尾;不要使用 Markdown 代码围栏,不要在 JSON 外追加解释文字、进度说明或自然语言前后缀。",
|
|
1722
|
+
"JSON 必须可被 `JSON.parse` 解析;如果 `result` 是多行 Markdown,必须在 JSON 字符串里使用 `\\n` 转义换行,不能把裸 Markdown 直接塞进未转义的字符串。",
|
|
1723
|
+
"固定格式:",
|
|
1724
|
+
"",
|
|
1725
|
+
JSON.stringify({ result: resultExample, outParams: outParamsExample }, null, 2),
|
|
1726
|
+
"",
|
|
1727
|
+
"- `result`:完整正文,写入 `result` / `content` 输出口,直连默认展示节点时展示它。",
|
|
1728
|
+
"- `outParams`:具名输出参数,只写入同名输出引脚。",
|
|
1729
|
+
...(slotDisplayRules.length ? ["- 当前输出引脚与展示节点映射:", ...slotDisplayRules] : []),
|
|
1730
|
+
...(slots.length
|
|
1731
|
+
? [`- 当前节点具名输出槽:${slots.map((name) => `\`${name}\``).join("、")}。例如任务要求写入 \`${slots[0]}\` 时,放到 \`outParams.${slots[0]}\`。`]
|
|
1732
|
+
: ["- 当前节点没有额外具名输出槽,`outParams` 返回空对象即可。"]),
|
|
1733
|
+
"- 如果 `result` 需要承载表格、ChartSpec、HTML 等结构化内容,可把对象或字符串放入 `result`;系统会把它转换给下游展示节点。",
|
|
1734
|
+
].join("\n");
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1485
1737
|
function workspaceRunPlan(graph, runNodeId) {
|
|
1486
1738
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1487
1739
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
@@ -1542,10 +1794,7 @@ function workspaceUpstreamText(graph, nodeId, outputs) {
|
|
|
1542
1794
|
const incoming = edges.filter((edge) => String(edge?.target || "") === String(nodeId));
|
|
1543
1795
|
const contentEdge = incoming.find((edge) => String(edge?.targetHandle || "") === "input-1") || incoming[0];
|
|
1544
1796
|
if (!contentEdge) return "";
|
|
1545
|
-
|
|
1546
|
-
const out = outputs.get(sourceId);
|
|
1547
|
-
if (out != null && String(out).trim()) return String(out);
|
|
1548
|
-
return workspaceInstanceText(instances[sourceId]);
|
|
1797
|
+
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
1549
1798
|
}
|
|
1550
1799
|
|
|
1551
1800
|
function workspaceHandleIndex(handle, prefix) {
|
|
@@ -1573,10 +1822,7 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs) {
|
|
|
1573
1822
|
const contentEdges = incoming.filter((edge) => !isWorkspaceSemanticInputSlot(workspaceTargetSlotForEdge(graph, edge)));
|
|
1574
1823
|
const contentEdge = contentEdges.find((edge) => String(edge?.targetHandle || "") === "input-1") || contentEdges[0];
|
|
1575
1824
|
if (!contentEdge) return "";
|
|
1576
|
-
|
|
1577
|
-
const out = outputs.get(sourceId);
|
|
1578
|
-
if (out != null && String(out).trim()) return String(out);
|
|
1579
|
-
return workspaceInstanceText(instances[sourceId]);
|
|
1825
|
+
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
1580
1826
|
}
|
|
1581
1827
|
|
|
1582
1828
|
function parseWorkspaceSkillKeys(raw) {
|
|
@@ -1727,7 +1973,7 @@ function workspaceWriteDisplayContent(instance, content) {
|
|
|
1727
1973
|
return next;
|
|
1728
1974
|
}
|
|
1729
1975
|
|
|
1730
|
-
function workspaceUpdateDirectDisplays(graph, sourceId, content) {
|
|
1976
|
+
function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null) {
|
|
1731
1977
|
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
1732
1978
|
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
1733
1979
|
const updated = [];
|
|
@@ -1736,7 +1982,8 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content) {
|
|
|
1736
1982
|
const targetId = String(edge?.target || "");
|
|
1737
1983
|
const target = instances[targetId];
|
|
1738
1984
|
if (!target || !workspaceDisplayKind(target.definitionId)) continue;
|
|
1739
|
-
|
|
1985
|
+
const value = outputs ? workspaceOutputSlotValueForEdge(graph, outputs, edge) : String(content || "");
|
|
1986
|
+
instances[targetId] = workspaceWriteDisplayContent(target, value || content);
|
|
1740
1987
|
updated.push(targetId);
|
|
1741
1988
|
}
|
|
1742
1989
|
return updated;
|
|
@@ -1747,14 +1994,16 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
1747
1994
|
const body = String(instance.body || "").trim();
|
|
1748
1995
|
const label = String(instance.label || nodeId).trim();
|
|
1749
1996
|
const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
|
|
1997
|
+
const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
|
|
1750
1998
|
return [
|
|
1751
1999
|
"你正在执行 AgentFlow Workspace 画布中的一个临时节点。",
|
|
1752
|
-
"
|
|
2000
|
+
"按 Workspace 输出协议返回该节点要传给下游展示/后续节点的数据。",
|
|
1753
2001
|
workspaceSearchGuardrailsBlock(),
|
|
1754
2002
|
skillsBlock ? `\n## Available Skills\n\n${skillsBlock}` : "",
|
|
1755
2003
|
mcpBlock ? `\n## Available MCP\n\n${mcpBlock}` : "",
|
|
1756
2004
|
upstreamText ? `\n## 上游上下文\n\n${upstreamText}` : "",
|
|
1757
2005
|
downstreamRequirements ? `\n${downstreamRequirements}` : "",
|
|
2006
|
+
outputProtocolRequirements ? `\n${outputProtocolRequirements}` : "",
|
|
1758
2007
|
`\n## 当前节点\n\n- id: ${nodeId}\n- label: ${label}\n- definitionId: ${instance.definitionId || ""}`,
|
|
1759
2008
|
`\n## 节点任务\n\n${body || upstreamText}`,
|
|
1760
2009
|
].filter(Boolean).join("\n");
|
|
@@ -1844,7 +2093,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1844
2093
|
)),
|
|
1845
2094
|
};
|
|
1846
2095
|
outputs.set(nodeId, skillsBlock);
|
|
1847
|
-
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock);
|
|
2096
|
+
workspaceUpdateDirectDisplays(graph, nodeId, skillsBlock, outputs);
|
|
1848
2097
|
emit({ type: "graph", nodeId, graph });
|
|
1849
2098
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
1850
2099
|
continue;
|
|
@@ -1864,7 +2113,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
1864
2113
|
)),
|
|
1865
2114
|
};
|
|
1866
2115
|
outputs.set(nodeId, mcpBlock);
|
|
1867
|
-
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock);
|
|
2116
|
+
workspaceUpdateDirectDisplays(graph, nodeId, mcpBlock, outputs);
|
|
1868
2117
|
emit({ type: "graph", nodeId, graph });
|
|
1869
2118
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
1870
2119
|
continue;
|
|
@@ -2166,9 +2415,13 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2166
2415
|
throw e;
|
|
2167
2416
|
}
|
|
2168
2417
|
}
|
|
2169
|
-
|
|
2170
|
-
const
|
|
2171
|
-
|
|
2418
|
+
const normalizedAgentOutput = workspaceStructuredAgentOutput(content);
|
|
2419
|
+
const resultContent = normalizedAgentOutput.result || content;
|
|
2420
|
+
outputs.set(nodeId, resultContent);
|
|
2421
|
+
const slotUpdate = workspaceApplyAgentOutputSlots(instance, content);
|
|
2422
|
+
if (slotUpdate.changed) graph.instances[nodeId] = slotUpdate.instance;
|
|
2423
|
+
const updatedDisplays = workspaceUpdateDirectDisplays(graph, nodeId, resultContent, outputs);
|
|
2424
|
+
if (slotUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
2172
2425
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
2173
2426
|
}
|
|
2174
2427
|
if (pauseNodeIds.length > 0) {
|
|
@@ -3569,7 +3822,11 @@ export function startUiServer({
|
|
|
3569
3822
|
|
|
3570
3823
|
if (req.method === "GET" && url.pathname === "/api/user-env") {
|
|
3571
3824
|
try {
|
|
3572
|
-
json(res, 200, {
|
|
3825
|
+
json(res, 200, {
|
|
3826
|
+
env: readUserEnvRows(userCtx.userId),
|
|
3827
|
+
globalEnv: authUser?.isAdmin ? readGlobalEnvRows() : [],
|
|
3828
|
+
canEditGlobalEnv: Boolean(authUser?.isAdmin),
|
|
3829
|
+
});
|
|
3573
3830
|
} catch (e) {
|
|
3574
3831
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
3575
3832
|
}
|
|
@@ -3585,8 +3842,20 @@ export function startUiServer({
|
|
|
3585
3842
|
return;
|
|
3586
3843
|
}
|
|
3587
3844
|
try {
|
|
3845
|
+
if (Object.prototype.hasOwnProperty.call(payload || {}, "globalEnv") && !authUser?.isAdmin) {
|
|
3846
|
+
json(res, 403, { error: "Admin permission required" });
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3588
3849
|
const envRows = writeUserEnvRows(userCtx.userId, payload?.env || []);
|
|
3589
|
-
|
|
3850
|
+
const globalEnvRows = authUser?.isAdmin && Object.prototype.hasOwnProperty.call(payload || {}, "globalEnv")
|
|
3851
|
+
? writeGlobalEnvRows(payload?.globalEnv || [])
|
|
3852
|
+
: readGlobalEnvRows();
|
|
3853
|
+
json(res, 200, {
|
|
3854
|
+
success: true,
|
|
3855
|
+
env: envRows,
|
|
3856
|
+
globalEnv: authUser?.isAdmin ? globalEnvRows : [],
|
|
3857
|
+
canEditGlobalEnv: Boolean(authUser?.isAdmin),
|
|
3858
|
+
});
|
|
3590
3859
|
} catch (e) {
|
|
3591
3860
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
3592
3861
|
}
|