@fieldwangai/agentflow 0.1.87 → 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.
|
@@ -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());
|
|
@@ -7667,7 +7764,7 @@ export function startUiServer({
|
|
|
7667
7764
|
|
|
7668
7765
|
if (req.method === "POST" && url.pathname === "/api/workspaces") {
|
|
7669
7766
|
try {
|
|
7670
|
-
const payload = await
|
|
7767
|
+
const payload = JSON.parse(await readBody(req));
|
|
7671
7768
|
const customWorkspaces = writeUserWorkspaces(userCtx, payload?.workspaces || payload?.customWorkspaces || []);
|
|
7672
7769
|
json(res, 200, {
|
|
7673
7770
|
path: userWorkspacesPath(userCtx),
|
|
@@ -7680,6 +7777,29 @@ export function startUiServer({
|
|
|
7680
7777
|
return;
|
|
7681
7778
|
}
|
|
7682
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
|
+
|
|
7683
7803
|
if (req.method === "GET" && url.pathname === "/api/workspace/graph") {
|
|
7684
7804
|
try {
|
|
7685
7805
|
const scoped = resolveWorkspaceScopeRoot(root, {
|