@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.
@@ -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
- reject(new Error(lastResult?.result || "Agent reported error."));
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?.result || "Agent reported error.";
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
+ }
@@ -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 (!workspaceDisplayKind(instance?.definitionId)) continue;
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 = workspaceDisplayKind(definitionId);
2522
- const rawBody = String(instance.body || "");
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 readJson(req);
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, {