@fieldwangai/agentflow 0.1.86 → 0.1.88
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 +125 -4
- package/bin/lib/cursor-api-key-pool.mjs +70 -0
- package/bin/lib/ui-server.mjs +161 -4
- package/builtin/web-ui/dist/assets/index-BqOb-W89.css +1 -0
- package/builtin/web-ui/dist/assets/index-DlyrI-8L.js +340 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CSr7NGO4.js +0 -322
- package/builtin/web-ui/dist/assets/index-DIO0cfyb.css +0 -1
|
@@ -10,6 +10,14 @@ import { appendRunLogLine } from "./run-events.mjs";
|
|
|
10
10
|
import { writeWithPrefix } from "./terminal.mjs";
|
|
11
11
|
import { t } from "./i18n.mjs";
|
|
12
12
|
import { readMergedEnvObject } from "./user-env.mjs";
|
|
13
|
+
import {
|
|
14
|
+
createCursorApiKeyAttempts,
|
|
15
|
+
cursorApiKeyCooldownMinutes,
|
|
16
|
+
cursorApiKeyEnv,
|
|
17
|
+
cursorApiKeyLabel,
|
|
18
|
+
isCursorQuotaError,
|
|
19
|
+
markCursorApiKeyQuotaBlocked,
|
|
20
|
+
} from "./cursor-api-key-pool.mjs";
|
|
13
21
|
import { outputNodeBasename } from "../pipeline/get-exec-id.mjs";
|
|
14
22
|
|
|
15
23
|
function shouldPassCursorModelArg(model) {
|
|
@@ -23,6 +31,45 @@ function childEnv(options = {}, extra = {}) {
|
|
|
23
31
|
return { ...process.env, ...readMergedEnvObject(userId), ...optEnv, ...extra };
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
function cursorAttemptOptions(options = {}) {
|
|
35
|
+
const baseEnv = childEnv(options);
|
|
36
|
+
const attempts = Array.isArray(options._agentflowCursorApiKeyAttempts)
|
|
37
|
+
? options._agentflowCursorApiKeyAttempts
|
|
38
|
+
: createCursorApiKeyAttempts(baseEnv);
|
|
39
|
+
const attemptIndex = Math.max(0, Number(options._agentflowCursorApiKeyAttemptIndex || 0) || 0);
|
|
40
|
+
const selection = attempts[attemptIndex];
|
|
41
|
+
return { baseEnv, attempts, attemptIndex, selection };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nextCursorAttemptOptions(options = {}, attempts = [], attemptIndex = 0) {
|
|
45
|
+
return {
|
|
46
|
+
...options,
|
|
47
|
+
_agentflowCursorApiKeyAttempts: attempts,
|
|
48
|
+
_agentflowCursorApiKeyAttemptIndex: attemptIndex + 1,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cursorResultErrorText(event) {
|
|
53
|
+
if (!event || typeof event !== "object") return "";
|
|
54
|
+
const candidates = [
|
|
55
|
+
event.result,
|
|
56
|
+
event.message,
|
|
57
|
+
event.error,
|
|
58
|
+
event.error?.message,
|
|
59
|
+
event.error?.error,
|
|
60
|
+
event.data?.error,
|
|
61
|
+
event.data?.message,
|
|
62
|
+
];
|
|
63
|
+
for (const value of candidates) {
|
|
64
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
65
|
+
if (value && typeof value === "object") {
|
|
66
|
+
const nested = value.message || value.error || value.result;
|
|
67
|
+
if (typeof nested === "string" && nested.trim()) return nested;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
|
|
26
73
|
function writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, text) {
|
|
27
74
|
const body = String(text ?? "").trim();
|
|
28
75
|
if (!body) return;
|
|
@@ -298,6 +345,12 @@ export function runCursorAgentForNode(
|
|
|
298
345
|
const rawPrefix = options.outputPrefix != null ? `[${options.outputPrefix}] ` : "";
|
|
299
346
|
const coloredPrefix = rawPrefix && options.prefixColor ? options.prefixColor(rawPrefix) : rawPrefix;
|
|
300
347
|
const agentContentColor = options.contentColor ?? ((line) => chalk.gray(line));
|
|
348
|
+
const {
|
|
349
|
+
baseEnv: cursorBaseEnv,
|
|
350
|
+
attempts: cursorAttempts,
|
|
351
|
+
attemptIndex: cursorAttemptIndex,
|
|
352
|
+
selection: cursorSelection,
|
|
353
|
+
} = cursorAttemptOptions(options);
|
|
301
354
|
|
|
302
355
|
return new Promise((resolve, reject) => {
|
|
303
356
|
const agentCmd = process.env.CURSOR_AGENT_CMD || "agent";
|
|
@@ -310,6 +363,15 @@ export function runCursorAgentForNode(
|
|
|
310
363
|
if (options.flowName && options.uuid) {
|
|
311
364
|
const argvLog = args.slice(0, -1).concat([`(prompt ${args[args.length - 1].length} chars)`]);
|
|
312
365
|
appendRunLogLine(workspaceRoot, options.flowName, options.uuid, "cli-raw", `Cursor CLI 完整参数: ${agentCmd} ${JSON.stringify(argvLog)}`);
|
|
366
|
+
if (cursorAttempts.length > 1) {
|
|
367
|
+
appendRunLogLine(
|
|
368
|
+
workspaceRoot,
|
|
369
|
+
options.flowName,
|
|
370
|
+
options.uuid,
|
|
371
|
+
"cli-raw",
|
|
372
|
+
`Cursor API Key 尝试: ${cursorApiKeyLabel(cursorSelection)} / ${cursorAttempts.length}`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
313
375
|
appendRunLogLine(
|
|
314
376
|
workspaceRoot,
|
|
315
377
|
options.flowName,
|
|
@@ -324,11 +386,12 @@ export function runCursorAgentForNode(
|
|
|
324
386
|
cwd: execWorkspaceRoot,
|
|
325
387
|
stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
|
|
326
388
|
shell: false,
|
|
327
|
-
env: childEnv(options),
|
|
389
|
+
env: childEnv(options, cursorApiKeyEnv(cursorSelection)),
|
|
328
390
|
});
|
|
329
391
|
|
|
330
392
|
let lastResult = null;
|
|
331
393
|
let hadError = false;
|
|
394
|
+
let hadToolActivity = false;
|
|
332
395
|
const assistantTextChunks = [];
|
|
333
396
|
const STDERR_CAP_BYTES = 1024 * 1024;
|
|
334
397
|
const stderrChunks = [];
|
|
@@ -419,6 +482,7 @@ export function runCursorAgentForNode(
|
|
|
419
482
|
if (out) writeStdout(out);
|
|
420
483
|
}
|
|
421
484
|
} else if (event.type === "tool_call") {
|
|
485
|
+
hadToolActivity = true;
|
|
422
486
|
const toolName =
|
|
423
487
|
event.tool_call && typeof event.tool_call === "object" ? Object.keys(event.tool_call)[0] ?? "?" : "?";
|
|
424
488
|
const subtype = event.subtype ?? "";
|
|
@@ -438,6 +502,7 @@ export function runCursorAgentForNode(
|
|
|
438
502
|
} catch (_) {
|
|
439
503
|
let out;
|
|
440
504
|
if (line.includes('"type":"tool_call"') || line.includes('"type": "tool_call"')) {
|
|
505
|
+
hadToolActivity = true;
|
|
441
506
|
let subtype = "?";
|
|
442
507
|
try {
|
|
443
508
|
const ev = JSON.parse(line);
|
|
@@ -480,9 +545,27 @@ export function runCursorAgentForNode(
|
|
|
480
545
|
if (coloredPrefix && stderrLineBuffer) {
|
|
481
546
|
writeWithPrefix(process.stderr, stderrLineBuffer.endsWith("\n") ? stderrLineBuffer : stderrLineBuffer + "\n", coloredPrefix);
|
|
482
547
|
}
|
|
548
|
+
const retryCursorQuota = (errorText) => {
|
|
549
|
+
if (!cursorSelection) return false;
|
|
550
|
+
if (cursorAttemptIndex >= cursorAttempts.length - 1) return false;
|
|
551
|
+
if (hadToolActivity) return false;
|
|
552
|
+
if (!isCursorQuotaError(errorText)) return false;
|
|
553
|
+
markCursorApiKeyQuotaBlocked(cursorSelection, cursorApiKeyCooldownMinutes(cursorBaseEnv));
|
|
554
|
+
const nextOptions = nextCursorAttemptOptions(options, cursorAttempts, cursorAttemptIndex);
|
|
555
|
+
const line = `[agentflow] Cursor API Key ${cursorApiKeyLabel(cursorSelection)} reached quota, retrying ${cursorAttemptIndex + 2}/${cursorAttempts.length}...\n`;
|
|
556
|
+
writeStdout(line);
|
|
557
|
+
if (flowName && uuid) appendRunLogLine(workspaceRoot, flowName, uuid, "cli-raw", line.trim());
|
|
558
|
+
runCursorAgentForNode(
|
|
559
|
+
workspaceRoot,
|
|
560
|
+
{ promptPath, nodeContext, taskBody, intermediatePath, resultPathRel, subagent, instanceId },
|
|
561
|
+
nextOptions,
|
|
562
|
+
).then(resolve).catch(reject);
|
|
563
|
+
return true;
|
|
564
|
+
};
|
|
483
565
|
if (code !== 0 && lastResult == null) {
|
|
484
566
|
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
485
567
|
const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
|
|
568
|
+
if (retryCursorQuota(stderrTail)) return;
|
|
486
569
|
const autoOnly =
|
|
487
570
|
/named models unavailable/i.test(stderrTail) ||
|
|
488
571
|
(/free plans?/i.test(stderrTail) && /only use auto/i.test(stderrTail)) ||
|
|
@@ -506,7 +589,9 @@ export function runCursorAgentForNode(
|
|
|
506
589
|
return;
|
|
507
590
|
}
|
|
508
591
|
if (hadError || (lastResult && lastResult.is_error)) {
|
|
509
|
-
|
|
592
|
+
const errorText = cursorResultErrorText(lastResult) || "Agent reported error.";
|
|
593
|
+
if (retryCursorQuota(errorText)) return;
|
|
594
|
+
reject(new Error(errorText));
|
|
510
595
|
return;
|
|
511
596
|
}
|
|
512
597
|
writeAgentTextArtifacts(absResultPath, absRunDir, instanceId, assistantTextChunks.join("") || lastResult?.result || "");
|
|
@@ -1225,6 +1310,12 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1225
1310
|
const ws = path.resolve(cliWorkspace);
|
|
1226
1311
|
const model = normalizeCursorModelForCli(options.model ?? process.env.CURSOR_AGENT_MODEL ?? null);
|
|
1227
1312
|
const agentCmd = process.env.CURSOR_AGENT_CMD || "agent";
|
|
1313
|
+
const {
|
|
1314
|
+
baseEnv: cursorBaseEnv,
|
|
1315
|
+
attempts: cursorAttempts,
|
|
1316
|
+
attemptIndex: cursorAttemptIndex,
|
|
1317
|
+
selection: cursorSelection,
|
|
1318
|
+
} = cursorAttemptOptions(options);
|
|
1228
1319
|
// Web UI Composer 需要能无交互执行本机 curl 等命令来刷新画布。
|
|
1229
1320
|
const args = ["--print", "--output-format", "stream-json", "--trust", "--sandbox", "disabled", "--workspace", ws];
|
|
1230
1321
|
const approveMcps = process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "0" && process.env.AGENTFLOW_CURSOR_APPROVE_MCPS !== "false";
|
|
@@ -1238,11 +1329,12 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1238
1329
|
cwd: ws,
|
|
1239
1330
|
stdio: ["ignore", "pipe", useStderrInherit ? "inherit" : "pipe"],
|
|
1240
1331
|
shell: false,
|
|
1241
|
-
env: childEnv(options),
|
|
1332
|
+
env: childEnv(options, cursorApiKeyEnv(cursorSelection)),
|
|
1242
1333
|
});
|
|
1243
1334
|
|
|
1244
1335
|
let lastResult = null;
|
|
1245
1336
|
let hadError = false;
|
|
1337
|
+
let hadToolActivity = false;
|
|
1246
1338
|
const STDERR_CAP_BYTES = 1024 * 1024;
|
|
1247
1339
|
const stderrChunks = [];
|
|
1248
1340
|
let stderrTotalBytes = 0;
|
|
@@ -1254,6 +1346,13 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1254
1346
|
} catch (_) {}
|
|
1255
1347
|
};
|
|
1256
1348
|
|
|
1349
|
+
if (cursorAttempts.length > 1) {
|
|
1350
|
+
emit({
|
|
1351
|
+
type: "status",
|
|
1352
|
+
line: `Cursor API Key ${cursorApiKeyLabel(cursorSelection)} / ${cursorAttempts.length}`,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1257
1356
|
if (!useStderrInherit) {
|
|
1258
1357
|
child.stderr.on("data", (chunk) => {
|
|
1259
1358
|
const s = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
@@ -1314,6 +1413,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1314
1413
|
emit({ type: "status", line: t("runner.generating_reply") });
|
|
1315
1414
|
}
|
|
1316
1415
|
} else if (event.type === "tool_call") {
|
|
1416
|
+
hadToolActivity = true;
|
|
1317
1417
|
const toolName =
|
|
1318
1418
|
event.tool_call && typeof event.tool_call === "object" ? Object.keys(event.tool_call)[0] ?? "?" : "?";
|
|
1319
1419
|
const subtype = event.subtype ?? "";
|
|
@@ -1347,6 +1447,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1347
1447
|
} catch (_) {
|
|
1348
1448
|
emit({ type: "raw", source: "cursor", stream: "stdout", eventType: "line", text: rawTraceText(line) });
|
|
1349
1449
|
if (line.includes('"type":"tool_call"') || line.includes('"type": "tool_call"')) {
|
|
1450
|
+
hadToolActivity = true;
|
|
1350
1451
|
let subtype = "?";
|
|
1351
1452
|
try {
|
|
1352
1453
|
const ev = JSON.parse(line);
|
|
@@ -1389,9 +1490,28 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1389
1490
|
const rest = stderrComposerBuffer.trim();
|
|
1390
1491
|
emit({ type: "status", line: `[stderr] ${truncateComposerLine(rest)}` });
|
|
1391
1492
|
}
|
|
1493
|
+
const retryCursorQuota = (errorText) => {
|
|
1494
|
+
if (!cursorSelection) return false;
|
|
1495
|
+
if (cursorAttemptIndex >= cursorAttempts.length - 1) return false;
|
|
1496
|
+
if (hadToolActivity) return false;
|
|
1497
|
+
if (!isCursorQuotaError(errorText)) return false;
|
|
1498
|
+
markCursorApiKeyQuotaBlocked(cursorSelection, cursorApiKeyCooldownMinutes(cursorBaseEnv));
|
|
1499
|
+
emit({
|
|
1500
|
+
type: "status",
|
|
1501
|
+
line: `Cursor API Key ${cursorApiKeyLabel(cursorSelection)} reached quota, retrying ${cursorAttemptIndex + 2}/${cursorAttempts.length}`,
|
|
1502
|
+
});
|
|
1503
|
+
const next = runCursorAgentWithPrompt(
|
|
1504
|
+
cliWorkspace,
|
|
1505
|
+
promptText,
|
|
1506
|
+
nextCursorAttemptOptions(options, cursorAttempts, cursorAttemptIndex),
|
|
1507
|
+
);
|
|
1508
|
+
next.finished.then(resolve).catch(reject);
|
|
1509
|
+
return true;
|
|
1510
|
+
};
|
|
1392
1511
|
if (code !== 0 && lastResult == null) {
|
|
1393
1512
|
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
1394
1513
|
const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
|
|
1514
|
+
if (retryCursorQuota(stderrTail)) return;
|
|
1395
1515
|
const err = new Error(`Cursor CLI exited ${code}. ${stderrTail || "No result event received."}`);
|
|
1396
1516
|
err.cursorStderrTail = stderrTail;
|
|
1397
1517
|
emit({ type: "status", line: truncateComposerLine(err.message) });
|
|
@@ -1399,7 +1519,8 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
|
|
|
1399
1519
|
return;
|
|
1400
1520
|
}
|
|
1401
1521
|
if (hadError || (lastResult && lastResult.is_error)) {
|
|
1402
|
-
const msg = lastResult
|
|
1522
|
+
const msg = cursorResultErrorText(lastResult) || "Agent reported error.";
|
|
1523
|
+
if (retryCursorQuota(msg)) return;
|
|
1403
1524
|
emit({ type: "status", line: truncateComposerLine(msg) });
|
|
1404
1525
|
reject(new Error(msg));
|
|
1405
1526
|
return;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
let nextCursorApiKeyCursor = 0;
|
|
2
|
+
const cursorApiKeyBlockedUntil = new Map();
|
|
3
|
+
|
|
4
|
+
export function parseCursorApiKeyPool(value = "") {
|
|
5
|
+
const raw = String(value || "").trim();
|
|
6
|
+
if (!raw) return [];
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(raw);
|
|
9
|
+
if (Array.isArray(parsed)) {
|
|
10
|
+
return parsed
|
|
11
|
+
.map((item) => {
|
|
12
|
+
if (typeof item === "string") return item.trim();
|
|
13
|
+
if (item && typeof item === "object") return String(item.key || "").trim();
|
|
14
|
+
return "";
|
|
15
|
+
})
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// Legacy comma-separated format.
|
|
20
|
+
}
|
|
21
|
+
return raw.split(",").map((key) => key.trim()).filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createCursorApiKeyAttempts(env = {}) {
|
|
25
|
+
const keys = parseCursorApiKeyPool(env.CURSOR_API_KEYS);
|
|
26
|
+
if (keys.length === 0) return [undefined];
|
|
27
|
+
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const candidates = keys
|
|
30
|
+
.map((key, index) => ({ key, index, total: keys.length }))
|
|
31
|
+
.filter((selection) => (cursorApiKeyBlockedUntil.get(selection.index) || 0) <= now);
|
|
32
|
+
const effective = candidates.length > 0 ? candidates : keys.map((key, index) => ({ key, index, total: keys.length }));
|
|
33
|
+
const start = nextCursorApiKeyCursor % effective.length;
|
|
34
|
+
nextCursorApiKeyCursor += 1;
|
|
35
|
+
return [...effective.slice(start), ...effective.slice(0, start)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function cursorApiKeyEnv(selection) {
|
|
39
|
+
return selection && selection.key ? { CURSOR_API_KEY: selection.key } : {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function markCursorApiKeyQuotaBlocked(selection, cooldownMinutes = 30) {
|
|
43
|
+
if (!selection || !Number.isFinite(selection.index)) return;
|
|
44
|
+
const minutes = Math.max(1, Number(cooldownMinutes) || 30);
|
|
45
|
+
cursorApiKeyBlockedUntil.set(selection.index, Date.now() + minutes * 60 * 1000);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function cursorApiKeyLabel(selection) {
|
|
49
|
+
if (!selection) return "default";
|
|
50
|
+
return `${selection.index + 1}/${selection.total}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isCursorQuotaError(error = "") {
|
|
54
|
+
const text = String(error || "");
|
|
55
|
+
if (!text) return false;
|
|
56
|
+
return [
|
|
57
|
+
/\b429\b/i,
|
|
58
|
+
/rate[_\s-]*limit/i,
|
|
59
|
+
/too many requests/i,
|
|
60
|
+
/quota/i,
|
|
61
|
+
/usage\s+limit/i,
|
|
62
|
+
/limit\s+(?:exceeded|reached)/i,
|
|
63
|
+
/exceeded\s+(?:your\s+)?limit/i,
|
|
64
|
+
/ActionRequiredError/i,
|
|
65
|
+
].some((pattern) => pattern.test(text));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function cursorApiKeyCooldownMinutes(env = {}) {
|
|
69
|
+
return Math.max(1, Number(env.AGENTFLOW_CURSOR_API_KEY_COOLDOWN_MINUTES || env.CURSOR_API_KEY_COOLDOWN_MINUTES || 30) || 30);
|
|
70
|
+
}
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -1682,6 +1682,103 @@ function writeUserWorkspaces(userCtx = {}, entries = []) {
|
|
|
1682
1682
|
return workspaces;
|
|
1683
1683
|
}
|
|
1684
1684
|
|
|
1685
|
+
function redactWorkspaceSecret(text = "", secret = "") {
|
|
1686
|
+
let out = String(text || "");
|
|
1687
|
+
const raw = String(secret || "");
|
|
1688
|
+
if (!raw) return out;
|
|
1689
|
+
out = out.split(raw).join("<redacted>");
|
|
1690
|
+
try {
|
|
1691
|
+
out = out.split(encodeURIComponent(raw)).join("<redacted>");
|
|
1692
|
+
} catch {
|
|
1693
|
+
// ignore invalid encoding edge cases
|
|
1694
|
+
}
|
|
1695
|
+
return out;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function workspaceRepoUrlWithCredential(repoUrl = "", credential = "") {
|
|
1699
|
+
const token = String(credential || "").trim();
|
|
1700
|
+
if (!token) return String(repoUrl || "").trim();
|
|
1701
|
+
try {
|
|
1702
|
+
const u = new URL(String(repoUrl || "").trim());
|
|
1703
|
+
if (!/^https?:$/.test(u.protocol)) return String(repoUrl || "").trim();
|
|
1704
|
+
if (!u.username) u.username = "oauth2";
|
|
1705
|
+
u.password = token;
|
|
1706
|
+
return u.toString();
|
|
1707
|
+
} catch {
|
|
1708
|
+
return String(repoUrl || "").trim();
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function gitWorkspaceCommandOrThrow(args, cwd, label, secret = "") {
|
|
1713
|
+
const result = runGit(args, cwd);
|
|
1714
|
+
if (result.status !== 0) {
|
|
1715
|
+
const message = redactWorkspaceSecret(result.stderr || result.stdout || result.error?.message || "unknown error", secret);
|
|
1716
|
+
throw new Error(`${label} failed: ${message}`);
|
|
1717
|
+
}
|
|
1718
|
+
return {
|
|
1719
|
+
stdout: redactWorkspaceSecret(result.stdout || "", secret),
|
|
1720
|
+
stderr: redactWorkspaceSecret(result.stderr || "", secret),
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function syncGitWorkspace(entry = {}, userCtx = {}) {
|
|
1725
|
+
const workspace = normalizeWorkspaceEntry(entry, 0, userCtx);
|
|
1726
|
+
if (!workspace || workspace.kind !== "git") throw new Error("只能拉取 Git 工作区");
|
|
1727
|
+
if (!workspace.repoUrl) throw new Error("Git 工作区缺少 repoUrl");
|
|
1728
|
+
const env = readMergedEnvObject(userCtx.userId || "");
|
|
1729
|
+
const token = workspace.credentialRef ? String(env[workspace.credentialRef] || "").trim() : "";
|
|
1730
|
+
const repoUrl = workspaceRepoUrlWithCredential(workspace.repoUrl, token);
|
|
1731
|
+
const targetDir = path.resolve(workspace.path);
|
|
1732
|
+
const parentDir = path.dirname(targetDir);
|
|
1733
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
1734
|
+
|
|
1735
|
+
const lines = [];
|
|
1736
|
+
let changed = false;
|
|
1737
|
+
if (fs.existsSync(path.join(targetDir, ".git"))) {
|
|
1738
|
+
const originalRemote = runGit(["remote", "get-url", "origin"], targetDir).stdout.trim();
|
|
1739
|
+
try {
|
|
1740
|
+
if (token) gitWorkspaceCommandOrThrow(["remote", "set-url", "origin", repoUrl], targetDir, "git remote set-url", token);
|
|
1741
|
+
const before = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
1742
|
+
gitWorkspaceCommandOrThrow(["fetch", "origin", "--prune"], targetDir, "git fetch", token);
|
|
1743
|
+
if (workspace.branch) {
|
|
1744
|
+
const checkout = runGit(["checkout", workspace.branch], targetDir);
|
|
1745
|
+
if (checkout.status !== 0) {
|
|
1746
|
+
gitWorkspaceCommandOrThrow(["checkout", "-b", workspace.branch, `origin/${workspace.branch}`], targetDir, "git checkout", token);
|
|
1747
|
+
}
|
|
1748
|
+
gitWorkspaceCommandOrThrow(["pull", "--ff-only", "origin", workspace.branch], targetDir, "git pull", token);
|
|
1749
|
+
} else {
|
|
1750
|
+
gitWorkspaceCommandOrThrow(["pull", "--ff-only"], targetDir, "git pull", token);
|
|
1751
|
+
}
|
|
1752
|
+
const after = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
1753
|
+
changed = before !== after;
|
|
1754
|
+
lines.push(changed ? `updated ${before.slice(0, 8)} -> ${after.slice(0, 8)}` : `already up to date ${after.slice(0, 8)}`);
|
|
1755
|
+
} finally {
|
|
1756
|
+
if (token && originalRemote) runGit(["remote", "set-url", "origin", originalRemote], targetDir);
|
|
1757
|
+
}
|
|
1758
|
+
} else {
|
|
1759
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
1760
|
+
throw new Error(`目标路径已存在但不是 Git 仓库:${targetDir}`);
|
|
1761
|
+
}
|
|
1762
|
+
const args = ["clone"];
|
|
1763
|
+
if (workspace.branch) args.push("--branch", workspace.branch);
|
|
1764
|
+
args.push(repoUrl, targetDir);
|
|
1765
|
+
gitWorkspaceCommandOrThrow(args, parentDir, "git clone", token);
|
|
1766
|
+
const commit = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
1767
|
+
changed = true;
|
|
1768
|
+
lines.push(`cloned ${commit.slice(0, 8)}`);
|
|
1769
|
+
if (token) runGit(["remote", "set-url", "origin", workspace.repoUrl], targetDir);
|
|
1770
|
+
}
|
|
1771
|
+
const branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], targetDir).stdout.trim();
|
|
1772
|
+
const commit = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
1773
|
+
return {
|
|
1774
|
+
workspace: normalizeWorkspaceEntry({ ...workspace, path: targetDir }, 0, userCtx),
|
|
1775
|
+
changed,
|
|
1776
|
+
branch,
|
|
1777
|
+
commit,
|
|
1778
|
+
message: lines.join("\n"),
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1685
1782
|
function listConfiguredWorkspaces(root, scopedRoot, userCtx = {}) {
|
|
1686
1783
|
const currentRoot = path.resolve(scopedRoot || root);
|
|
1687
1784
|
const homeRoot = path.resolve(os.homedir());
|
|
@@ -2300,13 +2397,27 @@ function normalizeDisplayShareNodeIds(ids, graph) {
|
|
|
2300
2397
|
const id = String(rawId || "").trim();
|
|
2301
2398
|
if (!id || seen.has(id)) continue;
|
|
2302
2399
|
const instance = instances[id];
|
|
2303
|
-
if (!
|
|
2400
|
+
if (!workspaceDisplayKindFromInstance(instance)) continue;
|
|
2304
2401
|
seen.add(id);
|
|
2305
2402
|
out.push(id);
|
|
2306
2403
|
}
|
|
2307
2404
|
return out;
|
|
2308
2405
|
}
|
|
2309
2406
|
|
|
2407
|
+
function workspaceDisplayContentFromInstance(instance, kind = "") {
|
|
2408
|
+
const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
|
|
2409
|
+
const primaryName = kind === "image" ? "src" : "content";
|
|
2410
|
+
const slotText = (slot) => workspaceSlotValue(slot);
|
|
2411
|
+
const hasSlotText = (slot) => slotText(slot).trim();
|
|
2412
|
+
const contentSlot =
|
|
2413
|
+
slots.find((slot) => String(slot?.name || "") === primaryName && hasSlotText(slot)) ||
|
|
2414
|
+
slots.find((slot) => String(slot?.name || "") === "result" && hasSlotText(slot)) ||
|
|
2415
|
+
slots.find((slot) => String(slot?.name || "") === "filePath" && hasSlotText(slot)) ||
|
|
2416
|
+
slots.find((slot) => String(slot?.type || "") === "text" && hasSlotText(slot));
|
|
2417
|
+
const slotContent = contentSlot ? slotText(contentSlot) : "";
|
|
2418
|
+
return String(isWorkspaceOneClickTaskDefinitionId(instance?.definitionId) ? slotContent : (instance?.body || slotContent));
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2310
2421
|
function normalizeDisplayShareLayout(layout, fallback = "canvas") {
|
|
2311
2422
|
const text = String(layout || "").trim();
|
|
2312
2423
|
return ["canvas", "gallery", "slides", "document", "single"].includes(text) ? text : fallback;
|
|
@@ -2518,8 +2629,8 @@ function publicDisplayPayloadFromShare(root, share) {
|
|
|
2518
2629
|
const nodes = nodeIds.map((id) => {
|
|
2519
2630
|
const instance = instances[id] || {};
|
|
2520
2631
|
const definitionId = String(instance.definitionId || "");
|
|
2521
|
-
const kind =
|
|
2522
|
-
const rawBody =
|
|
2632
|
+
const kind = workspaceDisplayKindFromInstance(instance);
|
|
2633
|
+
const rawBody = workspaceDisplayContentFromInstance(instance, kind);
|
|
2523
2634
|
const filePath = workspaceDisplayTextFilePath(rawBody, kind);
|
|
2524
2635
|
let body = rawBody;
|
|
2525
2636
|
if (filePath) {
|
|
@@ -2811,6 +2922,20 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
2811
2922
|
? payload.selectedNodeIds.map((id) => String(id || "").trim()).filter(Boolean)
|
|
2812
2923
|
: [];
|
|
2813
2924
|
const skillsBlock = typeof payload?.skillsBlock === "string" ? payload.skillsBlock.trim() : "";
|
|
2925
|
+
const history = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
2926
|
+
const historyBlock = history
|
|
2927
|
+
.slice(-16)
|
|
2928
|
+
.map((msg) => {
|
|
2929
|
+
const text = String(msg?.text || "").trim();
|
|
2930
|
+
if (!text) return "";
|
|
2931
|
+
const kind = String(msg?.kind || "").trim();
|
|
2932
|
+
if (kind === "raw" || kind === "prompt" || kind === "thinking") return "";
|
|
2933
|
+
const role = msg?.role === "user" ? "user" : (msg?.error ? "error" : "assistant");
|
|
2934
|
+
if (kind === "run-summary" || kind === "activity") return `context: ${text}`;
|
|
2935
|
+
return `${role}: ${text}`;
|
|
2936
|
+
})
|
|
2937
|
+
.filter(Boolean)
|
|
2938
|
+
.join("\n\n");
|
|
2814
2939
|
const contexts = Array.isArray(payload?.contexts) ? payload.contexts : [];
|
|
2815
2940
|
const contextBlocks = contexts
|
|
2816
2941
|
.map((ctx, idx) => {
|
|
@@ -2867,6 +2992,7 @@ function buildWorkspaceGeneratePrompt(payload) {
|
|
|
2867
2992
|
skillsBlock ? `\n## Selected Skills\n\n${skillsBlock}` : "",
|
|
2868
2993
|
kindInstruction,
|
|
2869
2994
|
contextBlocks ? `\n## 上下文\n\n${contextBlocks}` : "",
|
|
2995
|
+
historyBlock ? `\n## 对话历史\n\n${historyBlock}` : "",
|
|
2870
2996
|
`\n## 用户 prompt\n\n${userPrompt}`,
|
|
2871
2997
|
].filter(Boolean).join("\n");
|
|
2872
2998
|
}
|
|
@@ -3462,6 +3588,14 @@ function workspaceDisplayKind(definitionId) {
|
|
|
3462
3588
|
return "";
|
|
3463
3589
|
}
|
|
3464
3590
|
|
|
3591
|
+
function workspaceDisplayKindFromInstance(instance) {
|
|
3592
|
+
const direct = workspaceDisplayKind(instance?.definitionId);
|
|
3593
|
+
if (direct) return direct;
|
|
3594
|
+
if (!isWorkspaceOneClickTaskDefinitionId(instance?.definitionId)) return "";
|
|
3595
|
+
if (!workspaceDisplayContentFromInstance(instance, workspaceContextRunDisplayKind(instance)).trim()) return "";
|
|
3596
|
+
return workspaceContextRunDisplayKind(instance);
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3465
3599
|
function workspaceDisplayTextFilePath(value, kind = "") {
|
|
3466
3600
|
const text = String(value || "").trim();
|
|
3467
3601
|
if (!text || text.length > 260) return "";
|
|
@@ -7630,7 +7764,7 @@ export function startUiServer({
|
|
|
7630
7764
|
|
|
7631
7765
|
if (req.method === "POST" && url.pathname === "/api/workspaces") {
|
|
7632
7766
|
try {
|
|
7633
|
-
const payload = await
|
|
7767
|
+
const payload = JSON.parse(await readBody(req));
|
|
7634
7768
|
const customWorkspaces = writeUserWorkspaces(userCtx, payload?.workspaces || payload?.customWorkspaces || []);
|
|
7635
7769
|
json(res, 200, {
|
|
7636
7770
|
path: userWorkspacesPath(userCtx),
|
|
@@ -7643,6 +7777,29 @@ export function startUiServer({
|
|
|
7643
7777
|
return;
|
|
7644
7778
|
}
|
|
7645
7779
|
|
|
7780
|
+
if (req.method === "POST" && url.pathname === "/api/workspaces/sync") {
|
|
7781
|
+
try {
|
|
7782
|
+
const payload = JSON.parse(await readBody(req));
|
|
7783
|
+
const id = String(payload?.id || "").trim();
|
|
7784
|
+
const workspaces = readUserWorkspaces(userCtx);
|
|
7785
|
+
const workspace = workspaces.find((entry) => String(entry.id || "") === id);
|
|
7786
|
+
if (!workspace) {
|
|
7787
|
+
json(res, 404, { error: "工作区不存在" });
|
|
7788
|
+
return;
|
|
7789
|
+
}
|
|
7790
|
+
const result = syncGitWorkspace(workspace, userCtx);
|
|
7791
|
+
json(res, 200, {
|
|
7792
|
+
ok: true,
|
|
7793
|
+
...result,
|
|
7794
|
+
workspaces: listConfiguredWorkspaces(root, root, userCtx),
|
|
7795
|
+
customWorkspaces: readUserWorkspaces(userCtx),
|
|
7796
|
+
});
|
|
7797
|
+
} catch (e) {
|
|
7798
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
7799
|
+
}
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
|
|
7646
7803
|
if (req.method === "GET" && url.pathname === "/api/workspace/graph") {
|
|
7647
7804
|
try {
|
|
7648
7805
|
const scoped = resolveWorkspaceScopeRoot(root, {
|