@fieldwangai/agentflow 0.1.99 → 0.1.101

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.
@@ -128,6 +128,7 @@ import {
128
128
  listWorkspaceRunLogs,
129
129
  readWorkspaceRunLogEvents,
130
130
  } from "./workspace-run-logs.mjs";
131
+ import { createWorkspaceRunController } from "./workspace-run-controller.mjs";
131
132
 
132
133
  const MIME = {
133
134
  ".html": "text/html; charset=utf-8",
@@ -4947,6 +4948,8 @@ async function workspaceGeneratePlannedImplementationMarkdown({
4947
4948
  prompt,
4948
4949
  modelKey,
4949
4950
  agentflowUserId: userCtx?.userId || "",
4951
+ detached: process.platform !== "win32",
4952
+ onChild: onActiveChild,
4950
4953
  extraEnv: runtimeEnvForUser(userCtx, {
4951
4954
  AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
4952
4955
  AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
@@ -4967,7 +4970,6 @@ async function workspaceGeneratePlannedImplementationMarkdown({
4967
4970
  emit?.({ type: "status", nodeId, line: `优化工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
4968
4971
  },
4969
4972
  });
4970
- if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
4971
4973
  try {
4972
4974
  await handle.finished;
4973
4975
  } finally {
@@ -5009,6 +5011,8 @@ async function workspaceGenerateImplementationMarkdown({
5009
5011
  prompt,
5010
5012
  modelKey,
5011
5013
  agentflowUserId: userCtx?.userId || "",
5014
+ detached: process.platform !== "win32",
5015
+ onChild: onActiveChild,
5012
5016
  extraEnv: runtimeEnvForUser(userCtx, {
5013
5017
  AGENTFLOW_IMPLEMENTATION_REF: implementationPath || "",
5014
5018
  AGENTFLOW_NODE_RUN_DIR: runPackage?.nodeRunDir || "",
@@ -5029,7 +5033,6 @@ async function workspaceGenerateImplementationMarkdown({
5029
5033
  emit?.({ type: "status", line: `总结方案工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
5030
5034
  },
5031
5035
  });
5032
- if (typeof onActiveChild === "function") onActiveChild(handle.child || null);
5033
5036
  try {
5034
5037
  await handle.finished;
5035
5038
  } finally {
@@ -5994,6 +5997,8 @@ async function workspaceRunToolNodejsScript({
5994
5997
  userCtx,
5995
5998
  envOverlay = {},
5996
5999
  emit,
6000
+ signal,
6001
+ onActiveChild,
5997
6002
  }) {
5998
6003
  const scriptRef = String(instance?.scriptRef || "").trim();
5999
6004
  const scriptAbs = scriptRef ? workspaceResolveFlowFile(scopedRoot, scriptRef, "scriptRef") : "";
@@ -6041,14 +6046,24 @@ async function workspaceRunToolNodejsScript({
6041
6046
 
6042
6047
  const started = Date.now();
6043
6048
  return await new Promise((resolve, reject) => {
6049
+ const processGroup = process.platform !== "win32";
6044
6050
  const child = spawn(command, [], {
6045
6051
  cwd: runPackage.nodeRunDir,
6046
6052
  shell: true,
6047
6053
  stdio: ["ignore", "pipe", "pipe"],
6048
6054
  env,
6055
+ detached: processGroup,
6049
6056
  });
6057
+ if (typeof onActiveChild === "function") onActiveChild(child, { processGroup });
6050
6058
  let stdout = "";
6051
6059
  let stderr = "";
6060
+ let settled = false;
6061
+ const finish = (callback) => {
6062
+ if (settled) return;
6063
+ settled = true;
6064
+ if (typeof onActiveChild === "function") onActiveChild(null);
6065
+ callback();
6066
+ };
6052
6067
  child.stdout.setEncoding("utf-8");
6053
6068
  child.stderr.setEncoding("utf-8");
6054
6069
  child.stdout.on("data", (chunk) => {
@@ -6057,19 +6072,27 @@ async function workspaceRunToolNodejsScript({
6057
6072
  child.stderr.on("data", (chunk) => {
6058
6073
  stderr += String(chunk);
6059
6074
  });
6060
- child.on("error", reject);
6075
+ child.on("error", (error) => finish(() => reject(error)));
6061
6076
  child.on("close", (code) => {
6077
+ if (signal?.aborted) {
6078
+ finish(() => {
6079
+ const error = new Error("Workspace run stopped");
6080
+ error.code = "WORKSPACE_RUN_ABORTED";
6081
+ reject(error);
6082
+ });
6083
+ return;
6084
+ }
6062
6085
  if (stderr.trim()) {
6063
6086
  emit?.({ type: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
6064
6087
  }
6065
6088
  if (code !== 0) {
6066
- reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`));
6089
+ finish(() => reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`)));
6067
6090
  return;
6068
6091
  }
6069
6092
  const elapsedMs = Math.max(0, Date.now() - started);
6070
6093
  emit?.({ type: "status", line: `Timing script: ${elapsedMs}ms`, timing: { label: "script", elapsedMs } });
6071
6094
  const content = stdout.trim() || workspaceEnvelopeFromOutputFiles(outputRefs, runPackage.nodeRunDir);
6072
- resolve(content);
6095
+ finish(() => resolve(content));
6073
6096
  });
6074
6097
  });
6075
6098
  }
@@ -6691,6 +6714,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6691
6714
  userCtx,
6692
6715
  envOverlay: runEnv,
6693
6716
  emit: (event) => emit({ ...event, nodeId }),
6717
+ signal,
6718
+ onActiveChild: opts.onActiveChild,
6694
6719
  });
6695
6720
  const normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
6696
6721
  const resultContent = normalizedAgentOutput.result || content;
@@ -6787,6 +6812,8 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6787
6812
  prompt,
6788
6813
  modelKey: nodeModelKey,
6789
6814
  agentflowUserId: userCtx.userId || "",
6815
+ detached: process.platform !== "win32",
6816
+ onChild: opts.onActiveChild,
6790
6817
  extraEnv: runtimeEnv({
6791
6818
  AGENTFLOW_WORKSPACE_TMP_ROOT: runTmpRoot,
6792
6819
  AGENTFLOW_NODE_RUN_DIR: runPackage.nodeRunDir,
@@ -6823,7 +6850,6 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
6823
6850
  emit({ type: "status", nodeId, line: `工具 ${tool || "thinking"}${sub ? ` (${sub})` : ""}` });
6824
6851
  },
6825
6852
  });
6826
- if (typeof opts.onActiveChild === "function") opts.onActiveChild(handle.child || null);
6827
6853
  emitTiming(nodeId, "spawn-agent", spawnStartedAt, { attempt });
6828
6854
  try {
6829
6855
  await handle.finished;
@@ -7395,9 +7421,7 @@ function prdWorkflowReviewRenderTable(lines) {
7395
7421
  ].join("");
7396
7422
  }
7397
7423
 
7398
- function prdWorkflowReviewMarkdownToHtml(markdown) {
7399
- const { frontmatter, body } = prdWorkflowReviewSplitFrontmatter(markdown);
7400
- const lines = String(body || "").replace(/\r\n/g, "\n").split("\n");
7424
+ function prdWorkflowReviewMarkdownLinesToHtml(lines) {
7401
7425
  const html = [];
7402
7426
  let paragraph = [];
7403
7427
  let list = [];
@@ -7474,11 +7498,147 @@ function prdWorkflowReviewMarkdownToHtml(markdown) {
7474
7498
  }
7475
7499
  if (code) html.push(`<pre><code>${htmlEscapeAttribute(prdWorkflowReviewNormalizeText(code.lines.join("\n")))}</code></pre>`);
7476
7500
  flushBlocks();
7477
- return [prdWorkflowReviewRenderFrontmatter(frontmatter), ...html].filter(Boolean).join("\n");
7501
+ return html.join("\n");
7502
+ }
7503
+
7504
+ function prdWorkflowReviewActionLine(line) {
7505
+ const match = String(line || "").match(/^\s*[-*]\s+(?:\[( |x|X)\]\s+)?(A\d+|Action\s*\d+)(?=\s|[((::.-]|$)(.*)$/i);
7506
+ if (!match) return null;
7507
+ return {
7508
+ checked: match[1] ? match[1].toLowerCase() === "x" : null,
7509
+ label: match[2].replace(/\s+/g, " ").toUpperCase(),
7510
+ title: String(match[3] || "").replace(/^\s*[-::]\s*/, "").trim(),
7511
+ };
7512
+ }
7513
+
7514
+ function prdWorkflowReviewIsActionsHeading(line) {
7515
+ const heading = String(line || "").trim().match(/^(#{1,6})\s+(.+)$/);
7516
+ if (!heading) return null;
7517
+ const title = heading[2].replace(/[*_`]/g, "").trim();
7518
+ if (!/(?:\bTODO\s+Actions?\b|\bActions?\b|待办(?:事项|行动)?|行动项)/i.test(title)) return null;
7519
+ return { level: heading[1].length };
7520
+ }
7521
+
7522
+ function prdWorkflowReviewRenderActionSection(lines, sectionIndex) {
7523
+ const actionStarts = [];
7524
+ lines.forEach((line, index) => {
7525
+ const action = prdWorkflowReviewActionLine(line);
7526
+ if (action) actionStarts.push({ index, action });
7527
+ });
7528
+ if (!actionStarts.length) return prdWorkflowReviewMarkdownLinesToHtml(lines);
7529
+
7530
+ const ids = new Map();
7531
+ const actions = actionStarts.map(({ index, action }, actionIndex) => {
7532
+ const nextStart = actionStarts[actionIndex + 1]?.index ?? lines.length;
7533
+ let bodyStart = index + 1;
7534
+ const titleParts = [action.title].filter(Boolean);
7535
+ while (bodyStart < nextStart) {
7536
+ const continuation = String(lines[bodyStart] || "");
7537
+ if (!/^\s{2,}\S/.test(continuation) || /^\s*[-*]\s+/.test(continuation) || /^\s*```/.test(continuation)) break;
7538
+ titleParts.push(continuation.trim());
7539
+ bodyStart += 1;
7540
+ }
7541
+ const labelId = action.label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || actionIndex + 1;
7542
+ const baseId = `action-${labelId}${sectionIndex ? `-${sectionIndex + 1}` : ""}`;
7543
+ const duplicate = ids.get(baseId) || 0;
7544
+ ids.set(baseId, duplicate + 1);
7545
+ return {
7546
+ ...action,
7547
+ id: duplicate ? `${baseId}-${sectionIndex + 1}-${duplicate + 1}` : baseId,
7548
+ title: titleParts.join(" ") || action.label,
7549
+ body: lines.slice(bodyStart, nextStart),
7550
+ };
7551
+ });
7552
+
7553
+ const intro = prdWorkflowReviewMarkdownLinesToHtml(lines.slice(0, actionStarts[0].index));
7554
+ const navigation = actions.length > 1
7555
+ ? `<nav class="action-index" aria-label="Action 快速跳转"><span class="action-index__label">快速跳转</span>${actions.map((action) => `<a href="#${htmlEscapeAttribute(action.id)}">${htmlEscapeAttribute(action.label)}</a>`).join("")}</nav>`
7556
+ : "";
7557
+ const cards = actions.map((action) => {
7558
+ const status = action.checked === null
7559
+ ? ""
7560
+ : `<span class="action-card__status${action.checked ? " is-complete" : ""}">${action.checked ? "已完成" : "待完成"}</span>`;
7561
+ const body = prdWorkflowReviewMarkdownLinesToHtml(action.body);
7562
+ return `<section class="action-card${action.checked ? " is-complete" : ""}" id="${htmlEscapeAttribute(action.id)}">
7563
+ <div class="action-card__header">
7564
+ <span class="action-card__index">${htmlEscapeAttribute(action.label)}</span>
7565
+ <h3 class="action-card__title">${prdWorkflowReviewInlineMarkdown(action.title)}</h3>
7566
+ ${status}
7567
+ </div>
7568
+ <div class="action-card__body">${body}</div>
7569
+ </section>`;
7570
+ }).join("\n");
7571
+ return [intro, navigation, cards].filter(Boolean).join("\n");
7572
+ }
7573
+
7574
+ function prdWorkflowReviewBodyToHtml(body) {
7575
+ const lines = String(body || "").replace(/\r\n/g, "\n").split("\n");
7576
+ const html = [];
7577
+ let cursor = 0;
7578
+ let sectionIndex = 0;
7579
+ while (cursor < lines.length) {
7580
+ const actionsHeading = prdWorkflowReviewIsActionsHeading(lines[cursor]);
7581
+ if (!actionsHeading) {
7582
+ const nextHeading = lines.findIndex((line, index) => index > cursor && prdWorkflowReviewIsActionsHeading(line));
7583
+ const end = nextHeading >= 0 ? nextHeading : lines.length;
7584
+ html.push(prdWorkflowReviewMarkdownLinesToHtml(lines.slice(cursor, end)));
7585
+ cursor = end;
7586
+ continue;
7587
+ }
7588
+ let end = cursor + 1;
7589
+ while (end < lines.length) {
7590
+ const heading = String(lines[end] || "").trim().match(/^(#{1,6})\s+/);
7591
+ if (heading && heading[1].length <= actionsHeading.level) break;
7592
+ end += 1;
7593
+ }
7594
+ html.push(prdWorkflowReviewMarkdownLinesToHtml([lines[cursor]]));
7595
+ html.push(prdWorkflowReviewRenderActionSection(lines.slice(cursor + 1, end), sectionIndex));
7596
+ sectionIndex += 1;
7597
+ cursor = end;
7598
+ }
7599
+ return html.filter(Boolean).join("\n");
7600
+ }
7601
+
7602
+ export function prdWorkflowReviewMarkdownToHtml(markdown) {
7603
+ const { frontmatter, body } = prdWorkflowReviewSplitFrontmatter(markdown);
7604
+ return [
7605
+ prdWorkflowReviewRenderFrontmatter(frontmatter),
7606
+ prdWorkflowReviewBodyToHtml(body),
7607
+ ].filter(Boolean).join("\n");
7608
+ }
7609
+
7610
+ function prdWorkflowReviewExtractPageTitle(markdown, fallbackTitle) {
7611
+ const { frontmatter, body } = prdWorkflowReviewSplitFrontmatter(markdown);
7612
+ const lines = String(body || "").replace(/\r\n/g, "\n").split("\n");
7613
+ const firstContentIndex = lines.findIndex((line) => String(line || "").trim());
7614
+ const heading = firstContentIndex >= 0
7615
+ ? String(lines[firstContentIndex] || "").trim().match(/^#\s+(.+)$/)
7616
+ : null;
7617
+ if (!heading) {
7618
+ return {
7619
+ markdown: String(markdown || ""),
7620
+ title: String(fallbackTitle || "PRD Workflow Review"),
7621
+ };
7622
+ }
7623
+
7624
+ lines.splice(firstContentIndex, 1);
7625
+ const markdownTitle = prdWorkflowReviewNormalizeText(heading[1])
7626
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
7627
+ .replace(/[`*_~]/g, "")
7628
+ .trim();
7629
+ const bodyWithoutTitle = lines.join("\n").replace(/^\n+/, "");
7630
+ return {
7631
+ markdown: [
7632
+ frontmatter ? `---\n${frontmatter}\n---` : "",
7633
+ bodyWithoutTitle,
7634
+ ].filter(Boolean).join("\n\n"),
7635
+ title: markdownTitle || String(fallbackTitle || "PRD Workflow Review"),
7636
+ };
7478
7637
  }
7479
7638
 
7480
- function prdWorkflowReviewHtml(title, markdown, meta = {}) {
7481
- const escapedTitle = htmlEscapeAttribute(title || "PRD Workflow Review");
7639
+ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
7640
+ const page = prdWorkflowReviewExtractPageTitle(markdown, title);
7641
+ const escapedTitle = htmlEscapeAttribute(page.title);
7482
7642
  const escapedMeta = htmlEscapeAttribute([
7483
7643
  meta.tapdId ? `TAPD ${meta.tapdId}` : "",
7484
7644
  meta.stage ? `stage ${meta.stage}` : "",
@@ -7492,7 +7652,7 @@ function prdWorkflowReviewHtml(title, markdown, meta = {}) {
7492
7652
  meta.persistence ? `persistence ${meta.persistence}` : "",
7493
7653
  ].filter(Boolean).join(" · ");
7494
7654
  const escapedLifecycle = htmlEscapeAttribute(lifecycle);
7495
- const renderedMarkdown = prdWorkflowReviewMarkdownToHtml(markdown || "");
7655
+ const renderedMarkdown = prdWorkflowReviewMarkdownToHtml(page.markdown);
7496
7656
  const rawHref = htmlEscapeAttribute(meta.rawHref || "?raw=1");
7497
7657
  return `<!doctype html>
7498
7658
  <html lang="zh-CN" data-theme="dark">
@@ -7504,57 +7664,80 @@ function prdWorkflowReviewHtml(title, markdown, meta = {}) {
7504
7664
  :root {
7505
7665
  color-scheme: dark;
7506
7666
  font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
7507
- --bg: #0b1020;
7508
- --bg-radial: rgba(124,58,237,.18);
7509
- --panel: rgba(15,23,42,.68);
7510
- --panel-strong: rgba(15,23,42,.88);
7511
- --panel-soft: rgba(30,41,59,.74);
7512
- --border: rgba(148,163,184,.20);
7513
- --border-soft: rgba(148,163,184,.14);
7667
+ --bg: #1a1b26;
7668
+ --panel: #1f2335;
7669
+ --panel-strong: #24283b;
7670
+ --panel-soft: #292e42;
7671
+ --border: #3b4261;
7672
+ --border-soft: #30364f;
7514
7673
  --text: #e5e7eb;
7515
- --heading: #f8fafc;
7516
- --muted: #94a3b8;
7517
- --body: #dbe4f0;
7518
- --link: #c4b5fd;
7519
- --button: rgba(124,58,237,.16);
7520
- --button-text: #ddd6fe;
7521
- --code-bg: rgba(2,6,23,.58);
7522
- --code-block: rgba(2,6,23,.72);
7523
- --shadow: rgba(0,0,0,.28);
7674
+ --heading: #f4f4f5;
7675
+ --muted: #8b90a0;
7676
+ --body: #d4d4d8;
7677
+ --link: #7dcfff;
7678
+ --interactive: #7aa2f7;
7679
+ --purple: #bb9af7;
7680
+ --button: #24283b;
7681
+ --button-text: #cbd0da;
7682
+ --code-bg: #292e42;
7683
+ --code-inline: #7dcfff;
7684
+ --code-block: #16161e;
7685
+ --code-block-text: #e5e7eb;
7686
+ --action-bg: #1f2335;
7687
+ --action-header: #24283b;
7688
+ --pending-bg: rgba(224,175,104,.10);
7689
+ --pending-border: rgba(224,175,104,.34);
7690
+ --pending-text: #e0af68;
7691
+ --complete-bg: rgba(158,206,106,.10);
7692
+ --complete-border: rgba(158,206,106,.34);
7693
+ --complete-text: #9ece6a;
7694
+ --shadow: rgba(9,10,15,.28);
7524
7695
  background: var(--bg);
7525
7696
  }
7526
7697
  :root[data-theme="light"] {
7527
7698
  color-scheme: light;
7528
- --bg: #f8fafc;
7529
- --bg-radial: rgba(124,58,237,.12);
7530
- --panel: rgba(255,255,255,.92);
7531
- --panel-strong: rgba(255,255,255,.98);
7532
- --panel-soft: rgba(241,245,249,.96);
7533
- --border: rgba(100,116,139,.24);
7534
- --border-soft: rgba(100,116,139,.18);
7535
- --text: #0f172a;
7536
- --heading: #020617;
7537
- --muted: #64748b;
7538
- --body: #1e293b;
7539
- --link: #6d28d9;
7540
- --button: rgba(124,58,237,.10);
7541
- --button-text: #4c1d95;
7542
- --code-bg: rgba(226,232,240,.76);
7543
- --code-block: rgba(241,245,249,.92);
7544
- --shadow: rgba(15,23,42,.10);
7699
+ --bg: #e1e2e7;
7700
+ --panel: #f3f3f5;
7701
+ --panel-strong: #e9e9ed;
7702
+ --panel-soft: #dcdfe7;
7703
+ --border: #c8cad4;
7704
+ --border-soft: #d5d7df;
7705
+ --text: #4c505e;
7706
+ --heading: #343b58;
7707
+ --muted: #7f849c;
7708
+ --body: #4c505e;
7709
+ --link: #007197;
7710
+ --interactive: #2e7de9;
7711
+ --purple: #7847bd;
7712
+ --button: #e7e8ed;
7713
+ --button-text: #4c505e;
7714
+ --code-bg: #dcdfe7;
7715
+ --code-inline: #007197;
7716
+ --code-block: #d5d8e1;
7717
+ --code-block-text: #343b58;
7718
+ --action-bg: #f3f3f5;
7719
+ --action-header: #e9e9ed;
7720
+ --pending-bg: rgba(177,92,0,.08);
7721
+ --pending-border: rgba(177,92,0,.28);
7722
+ --pending-text: #9a5200;
7723
+ --complete-bg: rgba(88,117,57,.10);
7724
+ --complete-border: rgba(88,117,57,.28);
7725
+ --complete-text: #587539;
7726
+ --shadow: rgba(52,59,88,.10);
7545
7727
  }
7546
7728
  *, *::before, *::after { box-sizing: border-box; }
7547
7729
  html, body { overflow-x: hidden; }
7548
- body { margin: 0; min-height: 100vh; background: radial-gradient(circle at top left, var(--bg-radial), transparent 34rem), var(--bg); color: var(--text); }
7549
- main { width: min(100%, 1120px); margin: 0 auto; padding: 40px 24px 72px; min-width: 0; }
7730
+ body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--text); }
7731
+ main { width: min(100%, 1180px); margin: 0 auto; padding: 40px 24px 72px; min-width: 0; }
7550
7732
  header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 22px; }
7551
- h1 { margin: 0 0 10px; font-size: clamp(28px, 5vw, 56px); line-height: 1.05; letter-spacing: 0; }
7733
+ h1 { margin: 0 0 10px; font-size: clamp(28px, 4vw, 44px); line-height: 1.12; letter-spacing: -.015em; }
7552
7734
  .meta { margin: 0; color: var(--muted); font-size: 14px; }
7553
7735
  .toolbar { flex: 0 0 auto; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
7554
- .raw, .theme-toggle { border: 1px solid rgba(124,58,237,.34); border-radius: 999px; color: var(--button-text); background: var(--button); padding: 9px 14px; text-decoration: none; font-size: 13px; font-weight: 800; line-height: 1.2; }
7736
+ .raw, .theme-toggle { border: 1px solid var(--border); border-radius: 999px; color: var(--button-text); background: var(--button); padding: 9px 14px; text-decoration: none; font-size: 13px; font-weight: 800; line-height: 1.2; }
7737
+ .raw:hover, .theme-toggle:hover { border-color: var(--interactive); color: var(--link); }
7555
7738
  .theme-toggle { cursor: pointer; font-family: inherit; }
7556
- .lifecycle { margin-top: 10px; display: inline-flex; max-width: 100%; border: 1px solid rgba(124,58,237,.30); border-radius: 999px; background: rgba(124,58,237,.12); color: var(--button-text); padding: 6px 10px; font-size: 12px; font-weight: 800; line-height: 1.35; overflow-wrap: anywhere; }
7557
- article { min-width: 0; border: 1px solid var(--border); border-radius: 14px; background: var(--panel); box-shadow: 0 28px 80px var(--shadow); padding: clamp(20px, 4vw, 34px); }
7739
+ .lifecycle { margin-top: 10px; display: inline-flex; max-width: 100%; border: 1px solid var(--border); border-radius: 999px; background: var(--button); color: var(--muted); padding: 6px 10px; font-size: 12px; font-weight: 800; line-height: 1.35; overflow-wrap: anywhere; }
7740
+ article { min-width: 0; border: 1px solid var(--border); border-radius: 14px; background: var(--panel); box-shadow: 0 18px 50px var(--shadow); padding: clamp(20px, 4vw, 34px); }
7558
7741
  article > *:first-child { margin-top: 0; }
7559
7742
  article > *:last-child { margin-bottom: 0; }
7560
7743
  h2, h3, h4, h5, h6 { margin: 1.7em 0 .65em; line-height: 1.25; letter-spacing: 0; color: var(--heading); overflow-wrap: anywhere; }
@@ -7565,10 +7748,10 @@ function prdWorkflowReviewHtml(title, markdown, meta = {}) {
7565
7748
  ul { margin: .65rem 0 1rem; padding-left: 1.35rem; }
7566
7749
  li { margin: .28rem 0; color: var(--body); }
7567
7750
  li input { margin-right: .38rem; transform: translateY(1px); }
7568
- code { display: inline; max-width: 100%; border: 1px solid var(--border-soft); border-radius: 6px; background: var(--code-bg); color: var(--heading); padding: .1rem .34rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: .92em; white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
7751
+ code { display: inline; max-width: 100%; border: 1px solid var(--border-soft); border-radius: 6px; background: var(--code-bg); color: var(--code-inline); padding: .1rem .34rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: .92em; white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
7569
7752
  pre { max-width: 100%; overflow: auto; border: 1px solid var(--border); border-radius: 10px; background: var(--code-block); padding: 16px; line-height: 1.65; }
7570
- pre code { border: 0; background: transparent; padding: 0; white-space: pre; overflow-wrap: normal; word-break: normal; }
7571
- blockquote { margin: 1rem 0; border-left: 3px solid rgba(124,58,237,.72); background: rgba(124,58,237,.10); padding: .75rem 1rem; color: var(--body); }
7753
+ pre code { border: 0; background: transparent; color: var(--code-block-text); padding: 0; white-space: pre; overflow-wrap: normal; word-break: normal; }
7754
+ blockquote { margin: 1rem 0; border-left: 3px solid var(--purple); background: var(--panel-soft); padding: .75rem 1rem; color: var(--body); }
7572
7755
  a { color: var(--link); text-decoration-thickness: .08em; text-underline-offset: .16em; overflow-wrap: anywhere; }
7573
7756
  .table-wrap { max-width: 100%; overflow-x: auto; margin: 1rem 0 1.25rem; border: 1px solid var(--border-soft); border-radius: 10px; background: var(--panel-strong); }
7574
7757
  table { width: 100%; max-width: 100%; border-collapse: collapse; table-layout: fixed; }
@@ -7580,7 +7763,34 @@ function prdWorkflowReviewHtml(title, markdown, meta = {}) {
7580
7763
  .frontmatter table { min-width: 0; margin-top: .7rem; }
7581
7764
  .frontmatter th { width: min(34%, 12rem); background: var(--panel-soft); color: var(--body); }
7582
7765
  .frontmatter-list { margin: 0; padding-left: 1.1rem; }
7583
- @media (max-width: 720px) { main { padding: 28px 14px 48px; } header { display: block; } .toolbar { justify-content: flex-start; margin-top: 14px; } article { padding: 18px; } th, td { padding: .58rem .65rem; } }
7766
+ .action-index { position: sticky; top: 10px; z-index: 4; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 1rem 0 1.25rem; border: 1px solid var(--border); border-radius: 12px; background: var(--panel-strong); box-shadow: 0 8px 22px var(--shadow); padding: 10px 12px; }
7767
+ .action-index__label { margin-right: 2px; color: var(--muted); font-size: 12px; font-weight: 800; }
7768
+ .action-index a { min-width: 38px; border: 1px solid color-mix(in srgb, var(--interactive) 38%, var(--border)); border-radius: 999px; background: color-mix(in srgb, var(--interactive) 10%, var(--button)); color: var(--interactive); padding: 5px 10px; text-align: center; text-decoration: none; font-size: 12px; font-weight: 900; }
7769
+ .action-index a:hover { border-color: var(--link); background: color-mix(in srgb, var(--interactive) 18%, var(--button)); color: var(--link); }
7770
+ .action-card { scroll-margin-top: 78px; margin: 0 0 20px; overflow: hidden; border: 1px solid var(--border); border-radius: 12px; background: var(--action-bg); box-shadow: 0 8px 24px var(--shadow); }
7771
+ .action-card:target { border-color: var(--interactive); box-shadow: 0 0 0 2px color-mix(in srgb, var(--interactive) 18%, transparent), 0 8px 24px var(--shadow); }
7772
+ .action-card__header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: 12px; border-bottom: 1px solid var(--border-soft); background: var(--action-header); padding: 16px 18px; }
7773
+ .action-card__index { display: inline-grid; place-items: center; min-width: 42px; min-height: 30px; border: 1px solid color-mix(in srgb, var(--interactive) 34%, var(--border)); border-radius: 8px; background: color-mix(in srgb, var(--interactive) 10%, var(--panel-soft)); color: var(--interactive); font: 900 13px/1 "SFMono-Regular", Consolas, monospace; }
7774
+ .action-card__title { margin: 3px 0 0; font-size: 16px; line-height: 1.55; letter-spacing: 0; color: var(--heading); }
7775
+ .action-card__status { margin-top: 2px; border: 1px solid var(--pending-border); border-radius: 999px; background: var(--pending-bg); color: var(--pending-text); padding: 5px 9px; font-size: 11px; font-weight: 900; white-space: nowrap; }
7776
+ .action-card__status.is-complete { border-color: var(--complete-border); background: var(--complete-bg); color: var(--complete-text); }
7777
+ .action-card__body { padding: 15px 20px 20px; }
7778
+ .action-card__body > *:first-child { margin-top: 0; }
7779
+ .action-card__body > *:last-child { margin-bottom: 0; }
7780
+ .action-card__body > ul { margin: 0 0 1rem; padding-left: 1.3rem; }
7781
+ .action-card__body > ul > li { margin: .55rem 0; padding-left: .15rem; }
7782
+ .action-card__body pre { margin: .9rem 0 1.1rem; }
7783
+ @media (max-width: 720px) {
7784
+ main { padding: 28px 14px 48px; }
7785
+ header { display: block; }
7786
+ .toolbar { justify-content: flex-start; margin-top: 14px; }
7787
+ article { padding: 18px; }
7788
+ th, td { padding: .58rem .65rem; }
7789
+ .action-index { top: 6px; }
7790
+ .action-card__header { grid-template-columns: auto minmax(0, 1fr); padding: 14px; }
7791
+ .action-card__status { grid-column: 2; justify-self: start; }
7792
+ .action-card__body { padding: 14px 16px 18px; }
7793
+ }
7584
7794
  </style>
7585
7795
  </head>
7586
7796
  <body>
@@ -9026,6 +9236,14 @@ function workspaceRunEntryKey(scopeKey, runId) {
9026
9236
  return `${scopeKey}:${String(runId || "").trim() || runLedgerId("workspace")}`;
9027
9237
  }
9028
9238
 
9239
+ function workspaceRunControl(abortController) {
9240
+ return createWorkspaceRunController({
9241
+ abortController,
9242
+ gracefulTimeoutMs: 3_000,
9243
+ forceTimeoutMs: 1_500,
9244
+ });
9245
+ }
9246
+
9029
9247
  function workspaceRuntimeNodeLabel(graph, nodeId, fallback = "Workspace Run") {
9030
9248
  const id = String(nodeId || "").trim();
9031
9249
  const instance = graph?.instances && typeof graph.instances === "object" ? graph.instances[id] : null;
@@ -9463,11 +9681,12 @@ async function runWorkspaceScheduledEntry(root, entry) {
9463
9681
  }
9464
9682
 
9465
9683
  const controller = new AbortController();
9684
+ const runControl = workspaceRunControl(controller);
9466
9685
  const runKey = workspaceRunEntryKey(scopeKey, runId);
9467
9686
  const runEntry = {
9468
9687
  scopeKey,
9469
9688
  controller,
9470
- child: null,
9689
+ runControl,
9471
9690
  runId,
9472
9691
  userId: userCtx.userId,
9473
9692
  username: String(authUser.username || entry.username || userCtx.userId),
@@ -9478,11 +9697,6 @@ async function runWorkspaceScheduledEntry(root, entry) {
9478
9697
  plannedNodeIds,
9479
9698
  startedAt: Date.now(),
9480
9699
  scheduled: true,
9481
- stopChild() {
9482
- if (this.child && !this.child.killed) {
9483
- try { this.child.kill("SIGTERM"); } catch (_) {}
9484
- }
9485
- },
9486
9700
  };
9487
9701
  activeWorkspaceRuns.set(runKey, runEntry);
9488
9702
  appendWorkspaceRunStarted(runEntry);
@@ -9496,9 +9710,8 @@ async function runWorkspaceScheduledEntry(root, entry) {
9496
9710
  timezone: config.timezone,
9497
9711
  lastError: "",
9498
9712
  });
9499
- const setActiveChild = (child) => {
9500
- runEntry.child = child || null;
9501
- if (controller.signal.aborted) runEntry.stopChild();
9713
+ const setActiveChild = (child, childOptions = {}) => {
9714
+ runControl.setChild(child, childOptions);
9502
9715
  };
9503
9716
  try {
9504
9717
  const result = await runWorkspaceGraph(root, scoped.root, {
@@ -9531,23 +9744,28 @@ async function runWorkspaceScheduledEntry(root, entry) {
9531
9744
  } catch (e) {
9532
9745
  const endedAt = Date.now();
9533
9746
  const error = (e && e.message) || String(e);
9534
- appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "failed");
9535
- appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error, ts: endedAt });
9536
- finishWorkspaceRunLogSession(runLog.runId, "failed", {
9747
+ const stopped = isWorkspaceRunAbortError(e) || controller.signal.aborted;
9748
+ const finalStatus = stopped ? "stopped" : "failed";
9749
+ appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, finalStatus);
9750
+ appendWorkspaceRunLogEvent(runLog.runId, stopped
9751
+ ? { type: "stopped", message: "Workspace run stopped", ts: endedAt }
9752
+ : { type: "error", error, ts: endedAt });
9753
+ finishWorkspaceRunLogSession(runLog.runId, finalStatus, {
9537
9754
  endedAt,
9538
9755
  durationMs: endedAt - runEntry.startedAt,
9539
9756
  runNodeId: targetRunNodeId,
9540
- error,
9757
+ error: stopped ? "" : error,
9541
9758
  });
9542
9759
  updateWorkspaceScheduleEntry(entry.key, {
9543
9760
  nextRunAt: computeNext(config),
9544
9761
  lastFinishedAt: endedAt,
9545
- lastStatus: "failed",
9546
- lastError: error,
9547
- lastErrorAt: endedAt,
9762
+ lastStatus: finalStatus,
9763
+ lastError: stopped ? "" : error,
9764
+ ...(stopped ? {} : { lastErrorAt: endedAt }),
9548
9765
  });
9549
- log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${error}`);
9766
+ if (!stopped) log.info(`[workspace-scheduler] failed ${entry.flowId}/${targetRunNodeId}: ${error}`);
9550
9767
  } finally {
9768
+ runControl.finish(controller.signal.aborted ? "stopped" : "finished");
9551
9769
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
9552
9770
  }
9553
9771
  }
@@ -11516,13 +11734,14 @@ export function startUiServer({
11516
11734
  return;
11517
11735
  }
11518
11736
  const controller = new AbortController();
11737
+ const runControl = workspaceRunControl(controller);
11519
11738
  const runId = String(payload.runSessionId || payload.runId || "").trim() || runLedgerId("workspace");
11520
11739
  const runKey = workspaceRunEntryKey(scopeKey, runId);
11521
11740
  const runAlias = String(payload.runAlias || "").trim() || workspaceRuntimeNodeLabel(runtimeGraph, runNodeId, "Workspace Run");
11522
11741
  const runEntry = {
11523
11742
  scopeKey,
11524
11743
  controller,
11525
- child: null,
11744
+ runControl,
11526
11745
  runId,
11527
11746
  userId: String(userCtx.userId || ""),
11528
11747
  username: String(authUser?.username || userCtx.userId || ""),
@@ -11532,11 +11751,6 @@ export function startUiServer({
11532
11751
  flowSource: scoped.flowSource || payload.flowSource || "user",
11533
11752
  plannedNodeIds,
11534
11753
  startedAt: Date.now(),
11535
- stopChild() {
11536
- if (this.child && !this.child.killed) {
11537
- try { this.child.kill("SIGTERM"); } catch (_) {}
11538
- }
11539
- },
11540
11754
  };
11541
11755
  const runLog = createWorkspaceRunLogSession({
11542
11756
  runId,
@@ -11553,11 +11767,11 @@ export function startUiServer({
11553
11767
  });
11554
11768
  activeWorkspaceRuns.set(runKey, runEntry);
11555
11769
  appendWorkspaceRunStarted(runEntry);
11556
- const setActiveChild = (child) => {
11557
- runEntry.child = child || null;
11558
- if (controller.signal.aborted) runEntry.stopChild();
11770
+ const setActiveChild = (child, childOptions = {}) => {
11771
+ runControl.setChild(child, childOptions);
11559
11772
  };
11560
- const clearActiveRun = () => {
11773
+ const clearActiveRun = (status = "finished") => {
11774
+ runControl.finish(status);
11561
11775
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
11562
11776
  };
11563
11777
  if (wantsStream) {
@@ -11626,7 +11840,7 @@ export function startUiServer({
11626
11840
  }
11627
11841
  res.end();
11628
11842
  } finally {
11629
- clearActiveRun();
11843
+ clearActiveRun(controller.signal.aborted ? "stopped" : "finished");
11630
11844
  }
11631
11845
  return;
11632
11846
  }
@@ -11684,7 +11898,7 @@ export function startUiServer({
11684
11898
  throw e;
11685
11899
  }
11686
11900
  } finally {
11687
- clearActiveRun();
11901
+ clearActiveRun(controller.signal.aborted ? "stopped" : "finished");
11688
11902
  }
11689
11903
  } catch (e) {
11690
11904
  json(res, 500, { error: (e && e.message) || String(e) });
@@ -11750,6 +11964,7 @@ export function startUiServer({
11750
11964
  const entry = entries[0] || null;
11751
11965
  json(res, 200, {
11752
11966
  running: entries.length > 0,
11967
+ state: entry?.runControl?.state || (entries.length > 0 ? "running" : "idle"),
11753
11968
  flowId,
11754
11969
  flowSource,
11755
11970
  runNodeId: entry?.runNodeId || "",
@@ -11762,6 +11977,7 @@ export function startUiServer({
11762
11977
  startedAt: item?.startedAt || null,
11763
11978
  plannedNodeIds: Array.isArray(item?.plannedNodeIds) ? item.plannedNodeIds : [],
11764
11979
  scheduled: item?.scheduled === true,
11980
+ state: item?.runControl?.state || "running",
11765
11981
  })),
11766
11982
  });
11767
11983
  return;
@@ -11792,9 +12008,38 @@ export function startUiServer({
11792
12008
  json(res, 404, { error: "该 Workspace 未在运行" });
11793
12009
  return;
11794
12010
  }
11795
- try { entry.controller?.abort(); } catch (_) {}
11796
- try { entry.stopChild?.(); } catch (_) {}
11797
- json(res, 200, { ok: true, stopped: true });
12011
+ appendWorkspaceRunLogEvent(entry.runId, {
12012
+ type: "stop-requested",
12013
+ runNodeId: entry.runNodeId || "",
12014
+ ts: Date.now(),
12015
+ });
12016
+ const result = await entry.runControl.stop();
12017
+ if (!result.stopped) {
12018
+ appendWorkspaceRunLogEvent(entry.runId, {
12019
+ type: "stop-failed",
12020
+ runNodeId: entry.runNodeId || "",
12021
+ reason: result.timedOut ? "timeout" : "unknown",
12022
+ ts: Date.now(),
12023
+ });
12024
+ json(res, 409, {
12025
+ error: "停止请求已发送,但运行进程未能退出",
12026
+ ok: false,
12027
+ stopped: false,
12028
+ state: entry.runControl.state,
12029
+ });
12030
+ return;
12031
+ }
12032
+ appendWorkspaceRunLogEvent(entry.runId, {
12033
+ type: "stop-completed",
12034
+ runNodeId: entry.runNodeId || "",
12035
+ forced: result.forced === true,
12036
+ ts: Date.now(),
12037
+ });
12038
+ json(res, 200, {
12039
+ ok: true,
12040
+ stopped: true,
12041
+ forced: result.forced === true,
12042
+ });
11798
12043
  return;
11799
12044
  }
11800
12045