@fieldwangai/agentflow 0.1.92 → 0.1.94
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/ui-server.mjs +3002 -186
- package/builtin/web-ui/dist/assets/index-CYwUDlvT.js +346 -0
- package/builtin/web-ui/dist/assets/index-DV-QkHHi.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-cli/SKILL.md +133 -0
- package/skills/agentflow-cli/agents/openai.yaml +4 -0
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +341 -0
- package/builtin/web-ui/dist/assets/index-CSBbCP8S.css +0 -1
- package/builtin/web-ui/dist/assets/index-Dk3al84f.js +0 -341
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -134,6 +134,8 @@ const MIME = {
|
|
|
134
134
|
".js": "text/javascript; charset=utf-8",
|
|
135
135
|
".css": "text/css; charset=utf-8",
|
|
136
136
|
".json": "application/json; charset=utf-8",
|
|
137
|
+
".md": "text/markdown; charset=utf-8",
|
|
138
|
+
".markdown": "text/markdown; charset=utf-8",
|
|
137
139
|
".png": "image/png",
|
|
138
140
|
".jpg": "image/jpeg",
|
|
139
141
|
".jpeg": "image/jpeg",
|
|
@@ -1072,9 +1074,10 @@ async function checkCursorMcpServers(name = "", userCtx = {}) {
|
|
|
1072
1074
|
return { results };
|
|
1073
1075
|
}
|
|
1074
1076
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1077
|
+
const MODEL_LIST_KEYS = ["cursor", "opencode", "claudeCode", "codex"];
|
|
1078
|
+
|
|
1079
|
+
function emptyModelLists() {
|
|
1080
|
+
return {
|
|
1078
1081
|
cursor: [],
|
|
1079
1082
|
opencode: [],
|
|
1080
1083
|
claudeCode: [],
|
|
@@ -1084,6 +1087,62 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
1084
1087
|
claudeCodeFetchedAt: null,
|
|
1085
1088
|
codexFetchedAt: null,
|
|
1086
1089
|
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function modelListEntryId(entry) {
|
|
1093
|
+
const text = String(entry || "").trim();
|
|
1094
|
+
const idx = text.indexOf(" - ");
|
|
1095
|
+
return idx >= 0 ? text.slice(0, idx).trim() : text;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function normalizeHiddenModelConfig(raw) {
|
|
1099
|
+
const src = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
1100
|
+
const out = {};
|
|
1101
|
+
for (const key of MODEL_LIST_KEYS) {
|
|
1102
|
+
out[key] = Array.isArray(src[key])
|
|
1103
|
+
? [...new Set(src[key].map(modelListEntryId).filter(Boolean))]
|
|
1104
|
+
: [];
|
|
1105
|
+
}
|
|
1106
|
+
return out;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function readHiddenModelConfig() {
|
|
1110
|
+
const cfg = readAgentflowUserConfigObject();
|
|
1111
|
+
const visibility = cfg.modelVisibility && typeof cfg.modelVisibility === "object" && !Array.isArray(cfg.modelVisibility)
|
|
1112
|
+
? cfg.modelVisibility
|
|
1113
|
+
: {};
|
|
1114
|
+
return normalizeHiddenModelConfig(visibility.hiddenModels);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function writeHiddenModelConfig(hiddenModels) {
|
|
1118
|
+
const cfgPath = getAgentflowUserConfigAbs();
|
|
1119
|
+
const prev = readAgentflowUserConfigObject();
|
|
1120
|
+
const next = {
|
|
1121
|
+
...prev,
|
|
1122
|
+
modelVisibility: {
|
|
1123
|
+
...(prev.modelVisibility && typeof prev.modelVisibility === "object" && !Array.isArray(prev.modelVisibility) ? prev.modelVisibility : {}),
|
|
1124
|
+
hiddenModels: normalizeHiddenModelConfig(hiddenModels),
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
1128
|
+
fs.writeFileSync(cfgPath, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
1129
|
+
return next.modelVisibility.hiddenModels;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function applyModelVisibility(modelLists, hiddenModels) {
|
|
1133
|
+
const hidden = normalizeHiddenModelConfig(hiddenModels);
|
|
1134
|
+
const out = { ...modelLists };
|
|
1135
|
+
for (const key of MODEL_LIST_KEYS) {
|
|
1136
|
+
const hiddenIds = new Set(hidden[key]);
|
|
1137
|
+
out[key] = (Array.isArray(modelLists[key]) ? modelLists[key] : [])
|
|
1138
|
+
.filter((entry) => !hiddenIds.has(modelListEntryId(entry)));
|
|
1139
|
+
}
|
|
1140
|
+
return out;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function readRawModelListsFromDisk() {
|
|
1144
|
+
const p = getModelListsAbs();
|
|
1145
|
+
const empty = emptyModelLists();
|
|
1087
1146
|
try {
|
|
1088
1147
|
if (!fs.existsSync(p)) return empty;
|
|
1089
1148
|
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
@@ -1102,6 +1161,12 @@ function readModelListsFromDisk(workspaceRoot) {
|
|
|
1102
1161
|
}
|
|
1103
1162
|
}
|
|
1104
1163
|
|
|
1164
|
+
function readModelListsFromDisk(_workspaceRoot, opts = {}) {
|
|
1165
|
+
const raw = readRawModelListsFromDisk();
|
|
1166
|
+
if (opts.raw) return raw;
|
|
1167
|
+
return applyModelVisibility(raw, readHiddenModelConfig());
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1105
1170
|
const SKILLHUB_TIMEOUT_MS = 60_000;
|
|
1106
1171
|
const SKILLHUB_API_BASE = String(process.env.SKILLHUB_API_BASE || "https://skillhub.bigo.sg/api/v1").replace(/\/+$/, "");
|
|
1107
1172
|
const skillhubCollectionInfoCache = new Map();
|
|
@@ -1414,6 +1479,19 @@ function htmlEscapeAttribute(value) {
|
|
|
1414
1479
|
.replace(/>/g, ">");
|
|
1415
1480
|
}
|
|
1416
1481
|
|
|
1482
|
+
function prdWorkflowReviewNormalizeText(value) {
|
|
1483
|
+
return String(value || "")
|
|
1484
|
+
.replace(/"/g, '"')
|
|
1485
|
+
.replace(/"/g, '"')
|
|
1486
|
+
.replace(/"/gi, '"')
|
|
1487
|
+
.replace(/'/g, "'")
|
|
1488
|
+
.replace(/'/gi, "'")
|
|
1489
|
+
.replace(/'/g, "'")
|
|
1490
|
+
.replace(/</g, "<")
|
|
1491
|
+
.replace(/>/g, ">")
|
|
1492
|
+
.replace(/&/g, "&");
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1417
1495
|
function fileUrlFromPath(absPath) {
|
|
1418
1496
|
return pathToFileURL(path.resolve(absPath)).href;
|
|
1419
1497
|
}
|
|
@@ -2635,15 +2713,42 @@ function displayShareOutputUrl(shareId, baseUrl = "") {
|
|
|
2635
2713
|
}
|
|
2636
2714
|
}
|
|
2637
2715
|
|
|
2716
|
+
function normalizePublicBaseUrl(baseUrl = "") {
|
|
2717
|
+
const raw = String(baseUrl || "").trim();
|
|
2718
|
+
if (!raw) return "";
|
|
2719
|
+
try {
|
|
2720
|
+
const url = new URL(raw);
|
|
2721
|
+
if (url.hostname === "0.0.0.0" || url.hostname === "::" || url.hostname === "[::]") {
|
|
2722
|
+
url.hostname = "127.0.0.1";
|
|
2723
|
+
}
|
|
2724
|
+
return url.origin;
|
|
2725
|
+
} catch {
|
|
2726
|
+
return "";
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2638
2730
|
function requestPublicBaseUrl(req) {
|
|
2639
2731
|
const origin = String(req?.headers?.origin || "").trim();
|
|
2640
|
-
if (/^https?:\/\//i.test(origin)) return origin;
|
|
2732
|
+
if (/^https?:\/\//i.test(origin)) return normalizePublicBaseUrl(origin);
|
|
2641
2733
|
const forwardedHost = String(req?.headers?.["x-forwarded-host"] || "").split(",")[0].trim();
|
|
2642
2734
|
const host = forwardedHost || String(req?.headers?.host || "").trim();
|
|
2643
2735
|
if (!host) return "";
|
|
2644
2736
|
const forwardedProto = String(req?.headers?.["x-forwarded-proto"] || "").split(",")[0].trim();
|
|
2645
2737
|
const proto = /^https?$/i.test(forwardedProto) ? forwardedProto.toLowerCase() : "http";
|
|
2646
|
-
return `${proto}://${host}
|
|
2738
|
+
return normalizePublicBaseUrl(`${proto}://${host}`);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function configuredPublicBaseUrl(payload = null) {
|
|
2742
|
+
const envBase = normalizePublicBaseUrl(process.env.AGENTFLOW_PUBLIC_BASE_URL || process.env.AGENTFLOW_SHARE_BASE_URL || "");
|
|
2743
|
+
if (envBase) return envBase;
|
|
2744
|
+
const payloadBase = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
2745
|
+
? normalizePublicBaseUrl(payload.publicBaseUrl || payload.public_base_url || payload.reviewBaseUrl || payload.review_base_url || "")
|
|
2746
|
+
: "";
|
|
2747
|
+
return payloadBase;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
function serverPublicBaseUrl(req, host, port, payload = null) {
|
|
2751
|
+
return configuredPublicBaseUrl(payload) || requestPublicBaseUrl(req) || normalizePublicBaseUrl(`http://${host}:${port}`);
|
|
2647
2752
|
}
|
|
2648
2753
|
|
|
2649
2754
|
function normalizeRunEnvKey(key) {
|
|
@@ -3831,7 +3936,11 @@ function workspacePublishNodeOutputFile(runPackage, relPath) {
|
|
|
3831
3936
|
throw new Error(`Invalid node output path: ${clean}`);
|
|
3832
3937
|
}
|
|
3833
3938
|
if (!fs.existsSync(src) || !fs.statSync(src).isFile()) {
|
|
3834
|
-
throw new Error(
|
|
3939
|
+
throw new Error(
|
|
3940
|
+
`Agent returned resultFile but did not create it under this node's outputs: ${clean}\n` +
|
|
3941
|
+
`Expected file: ${src}\n` +
|
|
3942
|
+
`Write outputs using a relative path from the node cwd, or use AGENTFLOW_OUTPUTS_DIR.`
|
|
3943
|
+
);
|
|
3835
3944
|
}
|
|
3836
3945
|
const nodePart = workspaceSanitizeTmpSegment(runPackage?.nodeId || "node", "node");
|
|
3837
3946
|
const destRel = clean.slice("outputs/".length).replace(/^\/+/, "");
|
|
@@ -4050,6 +4159,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
4050
4159
|
"## 输出",
|
|
4051
4160
|
"",
|
|
4052
4161
|
`请把${resultKindText}结果写入 \`${resultFile}\`。${resultGuidance}`,
|
|
4162
|
+
`必须先在当前执行目录下创建 \`${resultFile}\`,不要写到绝对路径或其它 run 目录;然后再返回下面的 agentflow envelope。`,
|
|
4053
4163
|
downstreamInputRequirements ? `\n${downstreamInputRequirements}` : "",
|
|
4054
4164
|
...(slots.length ? [`额外输出:${slots.map((name) => `\`${name}\``).join("、")}。短值可写在 \`outParams\`,文件值写成 \`outParams.<name>File\`。`] : []),
|
|
4055
4165
|
"最终只输出下面的 agentflow envelope,不要输出解释、进度或其它文字:",
|
|
@@ -4291,6 +4401,7 @@ function workspaceNodeFileBoundaryBlock(runPackage = {}) {
|
|
|
4291
4401
|
nodeTmpDir ? `- 临时文件只能写入:\`${nodeTmpDir}\`,也可通过环境变量 \`AGENTFLOW_NODE_TMP_DIR\` 获取。` : "",
|
|
4292
4402
|
Object.keys(runPackage?.inputMounts || {}).length ? "- 已挂载的输入文件位于本任务 `inputs/`;`inputs/` 只用于读取,正式产物仍写入 `outputs/`。" : "",
|
|
4293
4403
|
`- 正式产物写入本任务 \`${outputsRel}/\`,例如 \`${outputsRel}/result.html\`、\`${outputsRel}/result.md\`;返回时仍使用 \`${outputsRel}/...\` 相对路径。`,
|
|
4404
|
+
`- 写正式产物时不要手写或复制上面的绝对路径;请在当前执行目录下使用相对路径 \`${outputsRel}/...\`,或使用环境变量 \`AGENTFLOW_OUTPUTS_DIR\`。`,
|
|
4294
4405
|
"- 不要在执行目录根部创建 `temp_*`、`_out.json`、`tmp.html` 等临时产物。",
|
|
4295
4406
|
"- 不要自行删除 run package;AgentFlow 会在运行结束后统一清理。",
|
|
4296
4407
|
].filter(Boolean).join("\n");
|
|
@@ -5164,11 +5275,70 @@ function workspaceKnowledgeContextBlockFromSources(sources = []) {
|
|
|
5164
5275
|
return lines.join("\n");
|
|
5165
5276
|
}
|
|
5166
5277
|
|
|
5278
|
+
function workspaceDedupeKnowledgeSources(sources = []) {
|
|
5279
|
+
const seen = new Set();
|
|
5280
|
+
const out = [];
|
|
5281
|
+
for (const source of Array.isArray(sources) ? sources : []) {
|
|
5282
|
+
if (!source || typeof source !== "object") continue;
|
|
5283
|
+
const key = [
|
|
5284
|
+
path.resolve(String(source.path || source.repoPath || "")),
|
|
5285
|
+
String(source.mountPath || ""),
|
|
5286
|
+
String(source.repoUrl || ""),
|
|
5287
|
+
String(source.branch || ""),
|
|
5288
|
+
].join("\n");
|
|
5289
|
+
if (seen.has(key)) continue;
|
|
5290
|
+
seen.add(key);
|
|
5291
|
+
out.push(source);
|
|
5292
|
+
}
|
|
5293
|
+
return out;
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
function workspaceGlobalKnowledgeNodeIds(graph) {
|
|
5297
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
5298
|
+
return Object.entries(instances)
|
|
5299
|
+
.filter(([id, instance]) => {
|
|
5300
|
+
if (String(instance?.definitionId || "") !== "control_cd_workspace") return false;
|
|
5301
|
+
if (String(id || "") === "background_knowledge") return true;
|
|
5302
|
+
if (instance?.globalContext === true || instance?.globalKnowledge === true) return true;
|
|
5303
|
+
const scope = String(instance?.scope || workspaceSlotValue(workspaceSlotByName(instance, "scope")) || "").trim().toLowerCase();
|
|
5304
|
+
return scope === "global" || scope === "workspace";
|
|
5305
|
+
})
|
|
5306
|
+
.map(([id]) => id);
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
function workspaceKnowledgeSourcesFromInstance(instance, baseCwd = "", scopedRoot = "") {
|
|
5310
|
+
const knowledgeText = workspaceSlotValue(workspaceSlotByName(instance, "knowledgeContext"));
|
|
5311
|
+
let sources = workspaceKnowledgeSourcesFromText(knowledgeText, baseCwd || scopedRoot, scopedRoot);
|
|
5312
|
+
if (sources.length) return sources;
|
|
5313
|
+
const pathText = workspaceSlotValue(workspaceSlotByName(instance, "path")) ||
|
|
5314
|
+
workspaceSlotValue(workspaceSlotByName(instance, "target")) ||
|
|
5315
|
+
workspaceInstanceText(instance);
|
|
5316
|
+
sources = workspaceKnowledgeSourcesFromText(pathText, baseCwd || scopedRoot, scopedRoot);
|
|
5317
|
+
const label = workspaceSlotValue(workspaceSlotByName(instance, "label"));
|
|
5318
|
+
if (!label) return sources;
|
|
5319
|
+
return sources.map((source) => ({ ...source, label }));
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5322
|
+
function workspaceGlobalKnowledgeSources(graph, scopedRoot = "", logicalCwd = "", excludeNodeId = "") {
|
|
5323
|
+
const root = scopedRoot ? path.resolve(scopedRoot) : "";
|
|
5324
|
+
const cwd = logicalCwd ? path.resolve(logicalCwd) : root;
|
|
5325
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
5326
|
+
return workspaceDedupeKnowledgeSources(
|
|
5327
|
+
workspaceGlobalKnowledgeNodeIds(graph)
|
|
5328
|
+
.filter((id) => String(id) !== String(excludeNodeId || ""))
|
|
5329
|
+
.flatMap((id) => workspaceKnowledgeSourcesFromInstance(instances[id], cwd || root, root))
|
|
5330
|
+
);
|
|
5331
|
+
}
|
|
5332
|
+
|
|
5167
5333
|
function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot = "", logicalCwd = "") {
|
|
5168
5334
|
const root = scopedRoot ? path.resolve(scopedRoot) : "";
|
|
5169
5335
|
const cwd = logicalCwd ? path.resolve(logicalCwd) : root;
|
|
5170
5336
|
const knowledgeText = workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot);
|
|
5171
5337
|
let knowledgeSources = workspaceKnowledgeSourcesFromText(knowledgeText, cwd || root, scopedRoot);
|
|
5338
|
+
knowledgeSources = workspaceDedupeKnowledgeSources([
|
|
5339
|
+
...workspaceGlobalKnowledgeSources(graph, scopedRoot, logicalCwd, nodeId),
|
|
5340
|
+
...knowledgeSources,
|
|
5341
|
+
]);
|
|
5172
5342
|
const workspaceText = workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot);
|
|
5173
5343
|
let workspaceContext = workspaceContextObjectFromText(workspaceText, cwd || root, scopedRoot);
|
|
5174
5344
|
if (!knowledgeSources.length && workspaceContext?.cwd) {
|
|
@@ -6414,8 +6584,13 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
6414
6584
|
}
|
|
6415
6585
|
const historyBlock = workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage);
|
|
6416
6586
|
let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd);
|
|
6417
|
-
if (!isContextRunNode && workspaceBoolSlot(instance, "includeWorkspaceContext", true)
|
|
6418
|
-
|
|
6587
|
+
if (!isContextRunNode && workspaceBoolSlot(instance, "includeWorkspaceContext", true)) {
|
|
6588
|
+
const defaultWorkspaceBlock = workspaceDefaultWorkspaceContextBlock(scopedRoot, cwd);
|
|
6589
|
+
if (!workspaceContextBlock) {
|
|
6590
|
+
workspaceContextBlock = defaultWorkspaceBlock;
|
|
6591
|
+
} else if (defaultWorkspaceBlock && !workspaceContextBlock.includes("## Workspace 上下文")) {
|
|
6592
|
+
workspaceContextBlock = [workspaceContextBlock, defaultWorkspaceBlock].filter(Boolean).join("\n\n");
|
|
6593
|
+
}
|
|
6419
6594
|
}
|
|
6420
6595
|
const prompt = workspaceNodePrompt(graph, nodeId, promptUpstreamText, promptSkillsBlock, promptMcpBlock, runtimeInputValues, runPackage, historyBlock, workspaceContextBlock);
|
|
6421
6596
|
try {
|
|
@@ -6725,177 +6900,1921 @@ function broadcastFlowEditorSync(flowId, flowSource, flowArchived = false, userI
|
|
|
6725
6900
|
const activeFlowRuns = new Map();
|
|
6726
6901
|
/** 正在执行的 Workspace 临时 run(runId/sessionId → { controller, child, runNodeId, startedAt, plannedNodeIds }) */
|
|
6727
6902
|
const activeWorkspaceRuns = new Map();
|
|
6903
|
+
const prdWorkflowSubscribers = new Map();
|
|
6904
|
+
const prdWorkflowIdempotency = new Map();
|
|
6905
|
+
const prdWorkflowActionLocks = new Map();
|
|
6906
|
+
const PRD_WORKFLOW_IDEMPOTENCY_MAX = 1000;
|
|
6907
|
+
const PRD_WORKFLOW_RUNTIME_EVENTS_MAX = 1000;
|
|
6728
6908
|
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
6729
6909
|
const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
|
|
6730
6910
|
const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = true;
|
|
6731
6911
|
const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
|
|
6732
6912
|
const WORKSPACE_NODE_HISTORY_MAX_CHARS = 80000;
|
|
6733
6913
|
|
|
6734
|
-
function
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
return "0 9 * * *";
|
|
6914
|
+
function prdWorkflowKey(userCtx = {}, flowSource = "user", flowId = "", tapdId = "") {
|
|
6915
|
+
return [
|
|
6916
|
+
String(userCtx?.userId || ""),
|
|
6917
|
+
String(flowSource || "user"),
|
|
6918
|
+
String(flowId || ""),
|
|
6919
|
+
String(tapdId || ""),
|
|
6920
|
+
].join("\t");
|
|
6742
6921
|
}
|
|
6743
6922
|
|
|
6744
|
-
function
|
|
6745
|
-
|
|
6923
|
+
function prdWorkflowBroadcast(key, event = {}) {
|
|
6924
|
+
const set = prdWorkflowSubscribers.get(String(key || ""));
|
|
6925
|
+
if (!set || set.size === 0) return;
|
|
6926
|
+
const chunk = `data: ${JSON.stringify({ ...event, ts: Date.now() })}\n\n`;
|
|
6927
|
+
for (const clientRes of set) {
|
|
6928
|
+
try {
|
|
6929
|
+
clientRes.write(chunk);
|
|
6930
|
+
} catch (_) {}
|
|
6931
|
+
}
|
|
6746
6932
|
}
|
|
6747
6933
|
|
|
6748
|
-
function
|
|
6749
|
-
|
|
6934
|
+
function prdWorkflowCollaborationState(userCtx = {}, flowSource = "user", flowId = "", tapdId = "") {
|
|
6935
|
+
const key = prdWorkflowKey(userCtx, flowSource, flowId, tapdId);
|
|
6936
|
+
const active = prdWorkflowActionLocks.get(key) || null;
|
|
6937
|
+
const subscribers = prdWorkflowSubscribers.get(key);
|
|
6938
|
+
return {
|
|
6939
|
+
subscribers: subscribers ? subscribers.size : 0,
|
|
6940
|
+
activeAction: active ? {
|
|
6941
|
+
action: String(active.action || ""),
|
|
6942
|
+
tapdId: String(active.tapdId || tapdId || ""),
|
|
6943
|
+
title: String(active.title || active.action || ""),
|
|
6944
|
+
stage: String(active.stage || ""),
|
|
6945
|
+
issueKey: String(active.issueKey || ""),
|
|
6946
|
+
startedAt: active.startedAt || 0,
|
|
6947
|
+
startedAtIso: active.startedAt ? new Date(active.startedAt).toISOString() : "",
|
|
6948
|
+
id: String(active.id || ""),
|
|
6949
|
+
userId: String(active.userId || userCtx?.userId || ""),
|
|
6950
|
+
} : null,
|
|
6951
|
+
};
|
|
6750
6952
|
}
|
|
6751
6953
|
|
|
6752
|
-
function
|
|
6753
|
-
const
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6954
|
+
function prdWorkflowCliCandidates(root, scopedRoot) {
|
|
6955
|
+
const fromEnv = String(process.env.PRD_FLOW_CLI || "").trim();
|
|
6956
|
+
return [
|
|
6957
|
+
fromEnv,
|
|
6958
|
+
scopedRoot ? path.join(scopedRoot, ".workspace", "prd-flow", "bin", "prd-flow") : "",
|
|
6959
|
+
root ? path.join(root, ".workspace", "prd-flow", "bin", "prd-flow") : "",
|
|
6960
|
+
scopedRoot ? path.join(scopedRoot, ".agents", "skills", "prd-flow", "bin", "prd-flow") : "",
|
|
6961
|
+
root ? path.join(root, ".agents", "skills", "prd-flow", "bin", "prd-flow") : "",
|
|
6962
|
+
"prd-flow",
|
|
6963
|
+
].filter(Boolean);
|
|
6757
6964
|
}
|
|
6758
6965
|
|
|
6759
|
-
function
|
|
6760
|
-
const
|
|
6761
|
-
|
|
6762
|
-
|
|
6966
|
+
function prdWorkflowResolveCli(root, scopedRoot) {
|
|
6967
|
+
const candidates = prdWorkflowCliCandidates(root, scopedRoot);
|
|
6968
|
+
for (const candidate of candidates) {
|
|
6969
|
+
if (candidate === "prd-flow") return { command: candidate, source: "PATH" };
|
|
6970
|
+
try {
|
|
6971
|
+
if (fs.existsSync(candidate)) return { command: candidate, source: candidate };
|
|
6972
|
+
} catch (_) {}
|
|
6973
|
+
}
|
|
6974
|
+
return { command: "prd-flow", source: "PATH" };
|
|
6763
6975
|
}
|
|
6764
6976
|
|
|
6765
|
-
function
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6977
|
+
function prdWorkflowFallbackSnapshot(tapdId, phase, message, patch = {}) {
|
|
6978
|
+
const now = new Date().toISOString();
|
|
6979
|
+
return {
|
|
6980
|
+
tapdId: String(tapdId || ""),
|
|
6981
|
+
phase: String(phase || "unavailable"),
|
|
6982
|
+
pointer: String(message || "PRD workflow unavailable"),
|
|
6983
|
+
revision: "",
|
|
6984
|
+
nextAction: null,
|
|
6985
|
+
actions: [],
|
|
6986
|
+
milestones: [],
|
|
6987
|
+
issues: [],
|
|
6988
|
+
artifacts: [],
|
|
6989
|
+
optionalGaps: [{ severity: "warn", text: String(message || "PRD workflow unavailable") }],
|
|
6990
|
+
sources: { checkedAt: now },
|
|
6991
|
+
...patch,
|
|
6992
|
+
};
|
|
6771
6993
|
}
|
|
6772
6994
|
|
|
6773
|
-
function
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
const
|
|
6777
|
-
|
|
6778
|
-
|
|
6995
|
+
function prdWorkflowStableValue(value) {
|
|
6996
|
+
if (Array.isArray(value)) return value.map((item) => prdWorkflowStableValue(item));
|
|
6997
|
+
if (value && typeof value === "object") {
|
|
6998
|
+
const out = {};
|
|
6999
|
+
for (const key of Object.keys(value).sort()) {
|
|
7000
|
+
if ([
|
|
7001
|
+
"checkedAt",
|
|
7002
|
+
"updatedAt",
|
|
7003
|
+
"createdAt",
|
|
7004
|
+
"clientReportedAt",
|
|
7005
|
+
"cacheUpdatedAt",
|
|
7006
|
+
"runtimeEventsUpdatedAt",
|
|
7007
|
+
"collaboration",
|
|
7008
|
+
"sources",
|
|
7009
|
+
"rawOutput",
|
|
7010
|
+
].includes(key)) continue;
|
|
7011
|
+
out[key] = prdWorkflowStableValue(value[key]);
|
|
6779
7012
|
}
|
|
6780
|
-
|
|
6781
|
-
.map((id) => String(id || "").trim())
|
|
6782
|
-
.filter((id) => id && planned.has(id));
|
|
6783
|
-
if (conflictNodeIds.length) return { key, entry, conflictNodeIds };
|
|
7013
|
+
return out;
|
|
6784
7014
|
}
|
|
6785
|
-
return
|
|
7015
|
+
return value;
|
|
6786
7016
|
}
|
|
6787
7017
|
|
|
6788
|
-
function
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
} catch {
|
|
6795
|
-
parsed = {};
|
|
6796
|
-
}
|
|
6797
|
-
}
|
|
6798
|
-
const intervalMinutes = Number(parsed.intervalMinutes);
|
|
6799
|
-
const migratedCron = workspaceIntervalMinutesToCron(intervalMinutes);
|
|
6800
|
-
const cron = typeof parsed.cron === "string" && parsed.cron.trim()
|
|
6801
|
-
? parsed.cron.trim()
|
|
6802
|
-
: migratedCron;
|
|
6803
|
-
const timezone = typeof parsed.timezone === "string" && parsed.timezone.trim()
|
|
6804
|
-
? parsed.timezone.trim()
|
|
6805
|
-
: "Asia/Shanghai";
|
|
6806
|
-
const targetRunNodeId = typeof parsed.targetRunNodeId === "string" ? parsed.targetRunNodeId.trim() : "";
|
|
6807
|
-
const overlapPolicy = parsed.overlapPolicy === "skip" ? "skip" : "skip";
|
|
6808
|
-
return {
|
|
6809
|
-
enabled: parsed.enabled === true,
|
|
6810
|
-
cron,
|
|
6811
|
-
timezone,
|
|
6812
|
-
targetRunNodeId,
|
|
6813
|
-
overlapPolicy,
|
|
6814
|
-
};
|
|
7018
|
+
function prdWorkflowRevisionHash(value) {
|
|
7019
|
+
return crypto
|
|
7020
|
+
.createHash("sha256")
|
|
7021
|
+
.update(JSON.stringify(prdWorkflowStableValue(value)))
|
|
7022
|
+
.digest("hex")
|
|
7023
|
+
.slice(0, 24);
|
|
6815
7024
|
}
|
|
6816
7025
|
|
|
6817
|
-
function
|
|
6818
|
-
|
|
6819
|
-
|
|
7026
|
+
function prdWorkflowSnapshotRevision(snapshot = {}) {
|
|
7027
|
+
const explicit = String(snapshot?.revision || snapshot?.prd?.revision || snapshot?.next?.revision || "").trim();
|
|
7028
|
+
if (explicit) return explicit;
|
|
7029
|
+
return `snap:${prdWorkflowRevisionHash({
|
|
7030
|
+
tapdId: snapshot?.tapdId || snapshot?.tapd_id || snapshot?.prd?.tapd_id || "",
|
|
7031
|
+
phase: snapshot?.phase || snapshot?.workflow_stage || snapshot?.next?.code || "",
|
|
7032
|
+
pointer: snapshot?.pointer || snapshot?.current || snapshot?.status || snapshot?.next?.title || "",
|
|
7033
|
+
next: snapshot?.nextAction || snapshot?.next || null,
|
|
7034
|
+
actions: snapshot?.actions || snapshot?.workflowActions || snapshot?.workflow_actions || [],
|
|
7035
|
+
milestones: snapshot?.milestones || [],
|
|
7036
|
+
epics: snapshot?.epics || snapshot?.epicGroups || snapshot?.prd?.epics || [],
|
|
7037
|
+
issues: snapshot?.issues || snapshot?.issueGroups || snapshot?.prd?.issues || [],
|
|
7038
|
+
artifacts: snapshot?.artifacts || snapshot?.outputs || [],
|
|
7039
|
+
prd: snapshot?.prd || null,
|
|
7040
|
+
})}`;
|
|
7041
|
+
}
|
|
7042
|
+
|
|
7043
|
+
function prdWorkflowSafeStateId(value) {
|
|
7044
|
+
return String(value || "")
|
|
7045
|
+
.trim()
|
|
7046
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
7047
|
+
.replace(/^_+|_+$/g, "")
|
|
7048
|
+
.slice(0, 128) || "unknown";
|
|
6820
7049
|
}
|
|
6821
7050
|
|
|
6822
|
-
function
|
|
6823
|
-
|
|
7051
|
+
function prdWorkflowStatePath(scopedRoot, tapdId) {
|
|
7052
|
+
const rootDir = scopedRoot || process.cwd();
|
|
7053
|
+
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.json`);
|
|
6824
7054
|
}
|
|
6825
7055
|
|
|
6826
|
-
function
|
|
6827
|
-
const
|
|
6828
|
-
|
|
7056
|
+
function prdWorkflowCachePath(scopedRoot, tapdId) {
|
|
7057
|
+
const rootDir = scopedRoot || process.cwd();
|
|
7058
|
+
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.cache.json`);
|
|
7059
|
+
}
|
|
7060
|
+
|
|
7061
|
+
function prdWorkflowProjectPath(scopedRoot, tapdId) {
|
|
7062
|
+
const rootDir = scopedRoot || process.cwd();
|
|
7063
|
+
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.project.json`);
|
|
7064
|
+
}
|
|
7065
|
+
|
|
7066
|
+
function prdWorkflowClientsPath(scopedRoot, tapdId) {
|
|
7067
|
+
const rootDir = scopedRoot || process.cwd();
|
|
7068
|
+
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.clients.json`);
|
|
7069
|
+
}
|
|
7070
|
+
|
|
7071
|
+
function prdWorkflowEventsPath(scopedRoot, tapdId) {
|
|
7072
|
+
const rootDir = scopedRoot || process.cwd();
|
|
7073
|
+
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.events.json`);
|
|
7074
|
+
}
|
|
7075
|
+
|
|
7076
|
+
function prdWorkflowReviewDir(scopedRoot, tapdId) {
|
|
7077
|
+
const rootDir = path.resolve(scopedRoot || process.cwd());
|
|
7078
|
+
return path.join(rootDir, ".workspace", "prd-flow", "reviews", prdWorkflowSafeStateId(tapdId));
|
|
7079
|
+
}
|
|
7080
|
+
|
|
7081
|
+
function prdWorkflowReviewPaths(scopedRoot, tapdId, reviewId) {
|
|
7082
|
+
const dir = prdWorkflowReviewDir(scopedRoot, tapdId);
|
|
7083
|
+
const safeId = prdWorkflowSafeStateId(reviewId);
|
|
7084
|
+
return {
|
|
7085
|
+
dir,
|
|
7086
|
+
id: safeId,
|
|
7087
|
+
markdownPath: path.join(dir, `${safeId}.md`),
|
|
7088
|
+
metaPath: path.join(dir, `${safeId}.json`),
|
|
7089
|
+
};
|
|
7090
|
+
}
|
|
7091
|
+
|
|
7092
|
+
function prdWorkflowPruneReviews(scopedRoot, tapdId, maxReviews = 200) {
|
|
6829
7093
|
try {
|
|
6830
|
-
const
|
|
6831
|
-
return
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
7094
|
+
const dir = prdWorkflowReviewDir(scopedRoot, tapdId);
|
|
7095
|
+
if (!fs.existsSync(dir)) return;
|
|
7096
|
+
const now = Date.now();
|
|
7097
|
+
const entries = fs.readdirSync(dir)
|
|
7098
|
+
.filter((name) => name.endsWith(".json"))
|
|
7099
|
+
.map((name) => {
|
|
7100
|
+
const abs = path.join(dir, name);
|
|
7101
|
+
let mtimeMs = 0;
|
|
7102
|
+
try { mtimeMs = fs.statSync(abs).mtimeMs; } catch (_) {}
|
|
7103
|
+
let meta = {};
|
|
7104
|
+
try { meta = JSON.parse(fs.readFileSync(abs, "utf-8")); } catch (_) {}
|
|
7105
|
+
const expiresMs = Date.parse(meta?.expiresAt || "");
|
|
7106
|
+
return { name, abs, id: name.replace(/\.json$/i, ""), mtimeMs, expiresMs };
|
|
7107
|
+
})
|
|
7108
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
7109
|
+
for (const entry of entries.filter((item) => Number.isFinite(item.expiresMs) && item.expiresMs < now)) {
|
|
7110
|
+
try { fs.unlinkSync(entry.abs); } catch (_) {}
|
|
7111
|
+
try { fs.unlinkSync(path.join(dir, `${entry.id}.md`)); } catch (_) {}
|
|
7112
|
+
}
|
|
7113
|
+
for (const entry of entries.filter((item) => !(Number.isFinite(item.expiresMs) && item.expiresMs < now)).slice(maxReviews)) {
|
|
7114
|
+
try { fs.unlinkSync(entry.abs); } catch (_) {}
|
|
7115
|
+
try { fs.unlinkSync(path.join(dir, `${entry.id}.md`)); } catch (_) {}
|
|
7116
|
+
}
|
|
7117
|
+
} catch (_) {}
|
|
6840
7118
|
}
|
|
6841
7119
|
|
|
6842
|
-
function
|
|
6843
|
-
const
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
7120
|
+
function prdWorkflowReviewInlineMarkdown(text) {
|
|
7121
|
+
const codeSpans = [];
|
|
7122
|
+
let escaped = htmlEscapeAttribute(prdWorkflowReviewNormalizeText(text || "")).replace(/`([^`]+)`/g, (_m, code) => {
|
|
7123
|
+
const token = `@@CODE${codeSpans.length}@@`;
|
|
7124
|
+
codeSpans.push(`<code>${htmlEscapeAttribute(prdWorkflowReviewNormalizeText(code))}</code>`);
|
|
7125
|
+
return token;
|
|
7126
|
+
});
|
|
7127
|
+
escaped = escaped
|
|
7128
|
+
.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+|\/[^)\s]+|file:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>')
|
|
7129
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
7130
|
+
.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
|
|
7131
|
+
codeSpans.forEach((html, index) => {
|
|
7132
|
+
escaped = escaped.replaceAll(`@@CODE${index}@@`, html);
|
|
7133
|
+
});
|
|
7134
|
+
return escaped;
|
|
6850
7135
|
}
|
|
6851
7136
|
|
|
6852
|
-
function
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
String(scheduleNodeId || ""),
|
|
6858
|
-
].join(":");
|
|
7137
|
+
function prdWorkflowReviewSplitFrontmatter(markdown) {
|
|
7138
|
+
const text = String(markdown || "");
|
|
7139
|
+
const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
|
|
7140
|
+
if (!match) return { frontmatter: "", body: text };
|
|
7141
|
+
return { frontmatter: match[1].trim(), body: text.slice(match[0].length) };
|
|
6859
7142
|
}
|
|
6860
7143
|
|
|
6861
|
-
function
|
|
6862
|
-
|
|
6863
|
-
const
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
)
|
|
6870
|
-
|
|
7144
|
+
function prdWorkflowReviewRenderFrontmatter(frontmatter) {
|
|
7145
|
+
if (!String(frontmatter || "").trim()) return "";
|
|
7146
|
+
const rows = String(frontmatter || "").split(/\r?\n/)
|
|
7147
|
+
.map((line) => {
|
|
7148
|
+
const match = line.match(/^([^:]+):\s*(.*)$/);
|
|
7149
|
+
if (!match) return `<tr><td colspan="2">${prdWorkflowReviewInlineMarkdown(line)}</td></tr>`;
|
|
7150
|
+
return `<tr><th>${htmlEscapeAttribute(match[1].trim())}</th><td>${prdWorkflowReviewInlineMarkdown(match[2].trim())}</td></tr>`;
|
|
7151
|
+
})
|
|
7152
|
+
.join("");
|
|
7153
|
+
return `<details class="frontmatter" open><summary>文档元数据</summary><table>${rows}</table></details>`;
|
|
6871
7154
|
}
|
|
6872
7155
|
|
|
6873
|
-
function
|
|
6874
|
-
const
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
.
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
7156
|
+
function prdWorkflowReviewRenderTable(lines) {
|
|
7157
|
+
const splitRow = (line) => String(line || "")
|
|
7158
|
+
.trim()
|
|
7159
|
+
.replace(/^\|/, "")
|
|
7160
|
+
.replace(/\|$/, "")
|
|
7161
|
+
.split("|")
|
|
7162
|
+
.map((cell) => cell.trim());
|
|
7163
|
+
const headers = splitRow(lines[0]);
|
|
7164
|
+
const body = lines.slice(2).map(splitRow);
|
|
7165
|
+
return [
|
|
7166
|
+
'<div class="table-wrap"><table>',
|
|
7167
|
+
`<thead><tr>${headers.map((cell) => `<th>${prdWorkflowReviewInlineMarkdown(cell)}</th>`).join("")}</tr></thead>`,
|
|
7168
|
+
`<tbody>${body.map((row) => `<tr>${row.map((cell) => `<td>${prdWorkflowReviewInlineMarkdown(cell)}</td>`).join("")}</tr>`).join("")}</tbody>`,
|
|
7169
|
+
"</table></div>",
|
|
7170
|
+
].join("");
|
|
7171
|
+
}
|
|
7172
|
+
|
|
7173
|
+
function prdWorkflowReviewMarkdownToHtml(markdown) {
|
|
7174
|
+
const { frontmatter, body } = prdWorkflowReviewSplitFrontmatter(markdown);
|
|
7175
|
+
const lines = String(body || "").replace(/\r\n/g, "\n").split("\n");
|
|
7176
|
+
const html = [];
|
|
7177
|
+
let paragraph = [];
|
|
7178
|
+
let list = [];
|
|
7179
|
+
let code = null;
|
|
7180
|
+
const flushParagraph = () => {
|
|
7181
|
+
if (!paragraph.length) return;
|
|
7182
|
+
html.push(`<p>${prdWorkflowReviewInlineMarkdown(paragraph.join(" "))}</p>`);
|
|
7183
|
+
paragraph = [];
|
|
7184
|
+
};
|
|
7185
|
+
const flushList = () => {
|
|
7186
|
+
if (!list.length) return;
|
|
7187
|
+
html.push(`<ul>${list.map((item) => `<li>${item}</li>`).join("")}</ul>`);
|
|
7188
|
+
list = [];
|
|
7189
|
+
};
|
|
7190
|
+
const flushBlocks = () => {
|
|
7191
|
+
flushParagraph();
|
|
7192
|
+
flushList();
|
|
7193
|
+
};
|
|
7194
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
7195
|
+
const line = lines[i];
|
|
7196
|
+
const trimmed = line.trim();
|
|
7197
|
+
const fence = trimmed.match(/^```(\w+)?\s*$/);
|
|
7198
|
+
if (code) {
|
|
7199
|
+
if (fence) {
|
|
7200
|
+
html.push(`<pre><code>${htmlEscapeAttribute(prdWorkflowReviewNormalizeText(code.lines.join("\n")))}</code></pre>`);
|
|
7201
|
+
code = null;
|
|
7202
|
+
} else {
|
|
7203
|
+
code.lines.push(line);
|
|
7204
|
+
}
|
|
6888
7205
|
continue;
|
|
6889
7206
|
}
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
7207
|
+
if (fence) {
|
|
7208
|
+
flushBlocks();
|
|
7209
|
+
code = { lang: fence[1] || "", lines: [] };
|
|
7210
|
+
continue;
|
|
7211
|
+
}
|
|
7212
|
+
if (!trimmed) {
|
|
7213
|
+
flushBlocks();
|
|
7214
|
+
continue;
|
|
7215
|
+
}
|
|
7216
|
+
if (/^\|.+\|\s*$/.test(trimmed) && i + 1 < lines.length && /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[i + 1].trim())) {
|
|
7217
|
+
flushBlocks();
|
|
7218
|
+
const tableLines = [line, lines[i + 1]];
|
|
7219
|
+
i += 2;
|
|
7220
|
+
while (i < lines.length && /^\|.+\|\s*$/.test(lines[i].trim())) {
|
|
7221
|
+
tableLines.push(lines[i]);
|
|
7222
|
+
i += 1;
|
|
7223
|
+
}
|
|
7224
|
+
i -= 1;
|
|
7225
|
+
html.push(prdWorkflowReviewRenderTable(tableLines));
|
|
7226
|
+
continue;
|
|
7227
|
+
}
|
|
7228
|
+
const heading = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
7229
|
+
if (heading) {
|
|
7230
|
+
flushBlocks();
|
|
7231
|
+
const level = Math.min(6, heading[1].length);
|
|
7232
|
+
html.push(`<h${level}>${prdWorkflowReviewInlineMarkdown(heading[2].trim())}</h${level}>`);
|
|
7233
|
+
continue;
|
|
7234
|
+
}
|
|
7235
|
+
const quote = trimmed.match(/^>\s+(.+)$/);
|
|
7236
|
+
if (quote) {
|
|
7237
|
+
flushBlocks();
|
|
7238
|
+
html.push(`<blockquote>${prdWorkflowReviewInlineMarkdown(quote[1])}</blockquote>`);
|
|
7239
|
+
continue;
|
|
7240
|
+
}
|
|
7241
|
+
const bullet = trimmed.match(/^[-*]\s+(?:\[( |x|X)\]\s+)?(.+)$/);
|
|
7242
|
+
if (bullet) {
|
|
7243
|
+
flushParagraph();
|
|
7244
|
+
const checked = bullet[1] ? `<input type="checkbox" disabled${bullet[1].toLowerCase() === "x" ? " checked" : ""}> ` : "";
|
|
7245
|
+
list.push(`${checked}${prdWorkflowReviewInlineMarkdown(bullet[2])}`);
|
|
7246
|
+
continue;
|
|
7247
|
+
}
|
|
7248
|
+
paragraph.push(trimmed);
|
|
7249
|
+
}
|
|
7250
|
+
if (code) html.push(`<pre><code>${htmlEscapeAttribute(prdWorkflowReviewNormalizeText(code.lines.join("\n")))}</code></pre>`);
|
|
7251
|
+
flushBlocks();
|
|
7252
|
+
return [prdWorkflowReviewRenderFrontmatter(frontmatter), ...html].filter(Boolean).join("\n");
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
function prdWorkflowReviewHtml(title, markdown, meta = {}) {
|
|
7256
|
+
const escapedTitle = htmlEscapeAttribute(title || "PRD Workflow Review");
|
|
7257
|
+
const escapedMeta = htmlEscapeAttribute([
|
|
7258
|
+
meta.tapdId ? `TAPD ${meta.tapdId}` : "",
|
|
7259
|
+
meta.stage ? `stage ${meta.stage}` : "",
|
|
7260
|
+
meta.issueKey ? `issue ${meta.issueKey}` : "",
|
|
7261
|
+
meta.createdAt || "",
|
|
7262
|
+
].filter(Boolean).join(" · "));
|
|
7263
|
+
const renderedMarkdown = prdWorkflowReviewMarkdownToHtml(markdown || "");
|
|
7264
|
+
const rawHref = htmlEscapeAttribute(meta.rawHref || "?raw=1");
|
|
7265
|
+
return `<!doctype html>
|
|
7266
|
+
<html lang="zh-CN" data-theme="dark">
|
|
7267
|
+
<head>
|
|
7268
|
+
<meta charset="utf-8" />
|
|
7269
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7270
|
+
<title>${escapedTitle}</title>
|
|
7271
|
+
<style>
|
|
7272
|
+
:root {
|
|
7273
|
+
color-scheme: dark;
|
|
7274
|
+
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
7275
|
+
--bg: #0b1020;
|
|
7276
|
+
--bg-radial: rgba(124,58,237,.18);
|
|
7277
|
+
--panel: rgba(15,23,42,.68);
|
|
7278
|
+
--panel-strong: rgba(15,23,42,.88);
|
|
7279
|
+
--panel-soft: rgba(30,41,59,.74);
|
|
7280
|
+
--border: rgba(148,163,184,.20);
|
|
7281
|
+
--border-soft: rgba(148,163,184,.14);
|
|
7282
|
+
--text: #e5e7eb;
|
|
7283
|
+
--heading: #f8fafc;
|
|
7284
|
+
--muted: #94a3b8;
|
|
7285
|
+
--body: #dbe4f0;
|
|
7286
|
+
--link: #c4b5fd;
|
|
7287
|
+
--button: rgba(124,58,237,.16);
|
|
7288
|
+
--button-text: #ddd6fe;
|
|
7289
|
+
--code-bg: rgba(2,6,23,.58);
|
|
7290
|
+
--code-block: rgba(2,6,23,.72);
|
|
7291
|
+
--shadow: rgba(0,0,0,.28);
|
|
7292
|
+
background: var(--bg);
|
|
7293
|
+
}
|
|
7294
|
+
:root[data-theme="light"] {
|
|
7295
|
+
color-scheme: light;
|
|
7296
|
+
--bg: #f8fafc;
|
|
7297
|
+
--bg-radial: rgba(124,58,237,.12);
|
|
7298
|
+
--panel: rgba(255,255,255,.92);
|
|
7299
|
+
--panel-strong: rgba(255,255,255,.98);
|
|
7300
|
+
--panel-soft: rgba(241,245,249,.96);
|
|
7301
|
+
--border: rgba(100,116,139,.24);
|
|
7302
|
+
--border-soft: rgba(100,116,139,.18);
|
|
7303
|
+
--text: #0f172a;
|
|
7304
|
+
--heading: #020617;
|
|
7305
|
+
--muted: #64748b;
|
|
7306
|
+
--body: #1e293b;
|
|
7307
|
+
--link: #6d28d9;
|
|
7308
|
+
--button: rgba(124,58,237,.10);
|
|
7309
|
+
--button-text: #4c1d95;
|
|
7310
|
+
--code-bg: rgba(226,232,240,.76);
|
|
7311
|
+
--code-block: rgba(241,245,249,.92);
|
|
7312
|
+
--shadow: rgba(15,23,42,.10);
|
|
7313
|
+
}
|
|
7314
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
7315
|
+
html, body { overflow-x: hidden; }
|
|
7316
|
+
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at top left, var(--bg-radial), transparent 34rem), var(--bg); color: var(--text); }
|
|
7317
|
+
main { width: min(100%, 1120px); margin: 0 auto; padding: 40px 24px 72px; min-width: 0; }
|
|
7318
|
+
header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 22px; }
|
|
7319
|
+
h1 { margin: 0 0 10px; font-size: clamp(28px, 5vw, 56px); line-height: 1.05; letter-spacing: 0; }
|
|
7320
|
+
.meta { margin: 0; color: var(--muted); font-size: 14px; }
|
|
7321
|
+
.toolbar { flex: 0 0 auto; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
|
|
7322
|
+
.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; }
|
|
7323
|
+
.theme-toggle { cursor: pointer; font-family: inherit; }
|
|
7324
|
+
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); }
|
|
7325
|
+
article > *:first-child { margin-top: 0; }
|
|
7326
|
+
article > *:last-child { margin-bottom: 0; }
|
|
7327
|
+
h2, h3, h4, h5, h6 { margin: 1.7em 0 .65em; line-height: 1.25; letter-spacing: 0; color: var(--heading); overflow-wrap: anywhere; }
|
|
7328
|
+
h2 { padding-bottom: .4rem; border-bottom: 1px solid var(--border-soft); font-size: 1.5rem; }
|
|
7329
|
+
h3 { font-size: 1.2rem; }
|
|
7330
|
+
p, li, td, th, blockquote { font-size: 15px; line-height: 1.8; overflow-wrap: anywhere; word-break: break-word; }
|
|
7331
|
+
p { margin: .75rem 0; color: var(--body); }
|
|
7332
|
+
ul { margin: .65rem 0 1rem; padding-left: 1.35rem; }
|
|
7333
|
+
li { margin: .28rem 0; color: var(--body); }
|
|
7334
|
+
li input { margin-right: .38rem; transform: translateY(1px); }
|
|
7335
|
+
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; }
|
|
7336
|
+
pre { max-width: 100%; overflow: auto; border: 1px solid var(--border); border-radius: 10px; background: var(--code-block); padding: 16px; line-height: 1.65; }
|
|
7337
|
+
pre code { border: 0; background: transparent; padding: 0; white-space: pre; overflow-wrap: normal; word-break: normal; }
|
|
7338
|
+
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); }
|
|
7339
|
+
a { color: var(--link); text-decoration-thickness: .08em; text-underline-offset: .16em; overflow-wrap: anywhere; }
|
|
7340
|
+
.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); }
|
|
7341
|
+
table { width: 100%; max-width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
7342
|
+
th, td { min-width: 0; border-bottom: 1px solid var(--border-soft); padding: .65rem .8rem; text-align: left; vertical-align: top; }
|
|
7343
|
+
th { background: var(--panel-soft); color: var(--heading); font-weight: 800; }
|
|
7344
|
+
tr:last-child td { border-bottom: 0; }
|
|
7345
|
+
.frontmatter { margin: 0 0 1.35rem; border: 1px solid var(--border-soft); border-radius: 10px; background: var(--panel-strong); padding: .75rem .9rem; }
|
|
7346
|
+
.frontmatter summary { cursor: pointer; color: var(--body); font-weight: 800; }
|
|
7347
|
+
.frontmatter table { min-width: 0; margin-top: .7rem; }
|
|
7348
|
+
.frontmatter th { width: min(34%, 12rem); background: var(--panel-soft); color: var(--body); }
|
|
7349
|
+
@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; } }
|
|
7350
|
+
</style>
|
|
7351
|
+
</head>
|
|
7352
|
+
<body>
|
|
7353
|
+
<main>
|
|
7354
|
+
<header>
|
|
7355
|
+
<div>
|
|
7356
|
+
<h1>${escapedTitle}</h1>
|
|
7357
|
+
${escapedMeta ? `<p class="meta">${escapedMeta}</p>` : ""}
|
|
7358
|
+
</div>
|
|
7359
|
+
<div class="toolbar">
|
|
7360
|
+
<button class="theme-toggle" type="button" data-theme-toggle>明亮模式</button>
|
|
7361
|
+
<a class="raw" href="${rawHref}">Raw Markdown</a>
|
|
7362
|
+
</div>
|
|
7363
|
+
</header>
|
|
7364
|
+
<article>${renderedMarkdown}</article>
|
|
7365
|
+
</main>
|
|
7366
|
+
<script>
|
|
7367
|
+
(() => {
|
|
7368
|
+
const key = "prd-workflow-review-theme";
|
|
7369
|
+
const root = document.documentElement;
|
|
7370
|
+
const button = document.querySelector("[data-theme-toggle]");
|
|
7371
|
+
const apply = (theme) => {
|
|
7372
|
+
root.dataset.theme = theme;
|
|
7373
|
+
if (button) button.textContent = theme === "light" ? "暗黑模式" : "明亮模式";
|
|
7374
|
+
try { window.localStorage.setItem(key, theme); } catch (_) {}
|
|
7375
|
+
};
|
|
7376
|
+
let saved = "dark";
|
|
7377
|
+
try { saved = window.localStorage.getItem(key) || "dark"; } catch (_) {}
|
|
7378
|
+
apply(saved === "light" ? "light" : "dark");
|
|
7379
|
+
if (button) button.addEventListener("click", () => apply(root.dataset.theme === "light" ? "dark" : "light"));
|
|
7380
|
+
})();
|
|
7381
|
+
</script>
|
|
7382
|
+
</body>
|
|
7383
|
+
</html>`;
|
|
7384
|
+
}
|
|
7385
|
+
|
|
7386
|
+
function prdWorkflowCreateReview(scopedRoot, tapdId, payload = {}, urlBase = "") {
|
|
7387
|
+
const content = String(payload.markdown || payload.content || payload.rawOutput || "").slice(0, 500000);
|
|
7388
|
+
if (!content.trim()) throw new Error("Missing review markdown");
|
|
7389
|
+
const requestedReviewId = String(payload.reviewId || payload.review_id || "").trim();
|
|
7390
|
+
const reviewId = requestedReviewId
|
|
7391
|
+
? prdWorkflowSafeStateId(requestedReviewId)
|
|
7392
|
+
: `review_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
|
|
7393
|
+
const paths = prdWorkflowReviewPaths(scopedRoot, tapdId, reviewId);
|
|
7394
|
+
const title = String(payload.title || payload.label || "PRD Workflow Review").trim().slice(0, 160) || "PRD Workflow Review";
|
|
7395
|
+
const durability = String(payload.durability || (payload.durable === true || payload.permanent === true ? "durable" : "temporary")).trim().toLowerCase() || "temporary";
|
|
7396
|
+
const ttlDaysRaw = Number(payload.ttlDays || payload.ttl_days || (durability === "temporary" ? 7 : 0));
|
|
7397
|
+
const ttlDays = Number.isFinite(ttlDaysRaw) && ttlDaysRaw > 0 ? ttlDaysRaw : 0;
|
|
7398
|
+
const createdAt = new Date();
|
|
7399
|
+
const explicitExpiresAt = String(payload.expiresAt || payload.expires_at || "").trim();
|
|
7400
|
+
const expiresAt = durability === "temporary"
|
|
7401
|
+
? (explicitExpiresAt || new Date(createdAt.getTime() + ttlDays * 86400000).toISOString())
|
|
7402
|
+
: "";
|
|
7403
|
+
const meta = {
|
|
7404
|
+
id: paths.id,
|
|
7405
|
+
tapdId: String(tapdId || ""),
|
|
7406
|
+
title,
|
|
7407
|
+
stage: String(payload.stage || payload.stageKey || payload.stage_key || "").trim(),
|
|
7408
|
+
action: String(payload.action || payload.actionId || payload.action_id || "").trim(),
|
|
7409
|
+
issueKey: String(payload.issueKey || payload.issue_key || payload.issue || "").trim(),
|
|
7410
|
+
durability,
|
|
7411
|
+
persistence: "runtime",
|
|
7412
|
+
source: payload.source && typeof payload.source === "object" && !Array.isArray(payload.source)
|
|
7413
|
+
? payload.source
|
|
7414
|
+
: {
|
|
7415
|
+
kind: durability === "durable" ? "ai-doc" : "local-draft",
|
|
7416
|
+
durability,
|
|
7417
|
+
},
|
|
7418
|
+
ttlDays,
|
|
7419
|
+
expiresAt,
|
|
7420
|
+
createdAt: createdAt.toISOString(),
|
|
7421
|
+
};
|
|
7422
|
+
fs.mkdirSync(paths.dir, { recursive: true });
|
|
7423
|
+
fs.writeFileSync(paths.markdownPath, content.trimEnd() + "\n", "utf-8");
|
|
7424
|
+
fs.writeFileSync(paths.metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
7425
|
+
prdWorkflowPruneReviews(scopedRoot, tapdId);
|
|
7426
|
+
const url = `${String(urlBase || "").replace(/\/+$/, "")}/api/prd-workflow/review/${encodeURIComponent(prdWorkflowSafeStateId(tapdId))}/${encodeURIComponent(paths.id)}`;
|
|
7427
|
+
return { ...meta, url, markdownPath: paths.markdownPath };
|
|
7428
|
+
}
|
|
7429
|
+
|
|
7430
|
+
function prdWorkflowReadCachedSnapshotFile(filePath) {
|
|
7431
|
+
try {
|
|
7432
|
+
if (!fs.existsSync(filePath)) return null;
|
|
7433
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
7434
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : null;
|
|
7435
|
+
} catch {
|
|
7436
|
+
return null;
|
|
7437
|
+
}
|
|
7438
|
+
}
|
|
7439
|
+
|
|
7440
|
+
function prdWorkflowReadCachedSnapshot(scopedRoot, tapdId) {
|
|
7441
|
+
const cache = prdWorkflowReadCachedSnapshotFile(prdWorkflowCachePath(scopedRoot, tapdId));
|
|
7442
|
+
if (cache) return { ...cache, cacheKind: "projection-cache" };
|
|
7443
|
+
const legacy = prdWorkflowReadCachedSnapshotFile(prdWorkflowStatePath(scopedRoot, tapdId));
|
|
7444
|
+
return legacy ? { ...legacy, cacheKind: "legacy-projection-cache" } : null;
|
|
7445
|
+
}
|
|
7446
|
+
|
|
7447
|
+
function prdWorkflowReadCachedSnapshotWithFallback(root, scopedRoot, tapdId) {
|
|
7448
|
+
const scoped = prdWorkflowReadCachedSnapshot(scopedRoot, tapdId);
|
|
7449
|
+
if (scoped) return { ...scoped, cacheScope: "scoped", cacheRoot: scopedRoot };
|
|
7450
|
+
if (root && path.resolve(root) !== path.resolve(scopedRoot || root)) {
|
|
7451
|
+
const global = prdWorkflowReadCachedSnapshot(root, tapdId);
|
|
7452
|
+
if (global) return { ...global, cacheScope: "global", cacheRoot: root };
|
|
7453
|
+
}
|
|
7454
|
+
return null;
|
|
7455
|
+
}
|
|
7456
|
+
|
|
7457
|
+
function prdWorkflowReadJsonFile(filePath, fallback = null) {
|
|
7458
|
+
try {
|
|
7459
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
7460
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
7461
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : fallback;
|
|
7462
|
+
} catch {
|
|
7463
|
+
return fallback;
|
|
7464
|
+
}
|
|
7465
|
+
}
|
|
7466
|
+
|
|
7467
|
+
function prdWorkflowWriteJsonFile(filePath, data) {
|
|
7468
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
7469
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
7470
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
7471
|
+
fs.renameSync(tmp, filePath);
|
|
7472
|
+
return data;
|
|
7473
|
+
}
|
|
7474
|
+
|
|
7475
|
+
function prdWorkflowReadProjectState(scopedRoot, tapdId) {
|
|
7476
|
+
return prdWorkflowReadJsonFile(prdWorkflowProjectPath(scopedRoot, tapdId), {
|
|
7477
|
+
version: 1,
|
|
7478
|
+
tapdId: String(tapdId || ""),
|
|
7479
|
+
updatedAt: "",
|
|
7480
|
+
snapshot: null,
|
|
7481
|
+
conflicts: [],
|
|
7482
|
+
});
|
|
7483
|
+
}
|
|
7484
|
+
|
|
7485
|
+
function prdWorkflowReadProjectStateWithFallback(root, scopedRoot, tapdId) {
|
|
7486
|
+
const scoped = prdWorkflowReadProjectState(scopedRoot, tapdId);
|
|
7487
|
+
if (scoped?.snapshot) return { ...scoped, cacheScope: "scoped", cacheRoot: scopedRoot };
|
|
7488
|
+
if (root && path.resolve(root) !== path.resolve(scopedRoot || root)) {
|
|
7489
|
+
const global = prdWorkflowReadProjectState(root, tapdId);
|
|
7490
|
+
if (global?.snapshot) return { ...global, cacheScope: "global", cacheRoot: root };
|
|
7491
|
+
}
|
|
7492
|
+
return scoped?.snapshot ? scoped : null;
|
|
7493
|
+
}
|
|
7494
|
+
|
|
7495
|
+
function prdWorkflowWriteProjectState(scopedRoot, tapdId, snapshot, patch = {}) {
|
|
7496
|
+
const prev = prdWorkflowReadProjectState(scopedRoot, tapdId);
|
|
7497
|
+
return prdWorkflowWriteJsonFile(prdWorkflowProjectPath(scopedRoot, tapdId), {
|
|
7498
|
+
version: 1,
|
|
7499
|
+
tapdId: String(tapdId || ""),
|
|
7500
|
+
updatedAt: new Date().toISOString(),
|
|
7501
|
+
snapshot,
|
|
7502
|
+
conflicts: Array.isArray(patch.conflicts) ? patch.conflicts : Array.isArray(prev.conflicts) ? prev.conflicts : [],
|
|
7503
|
+
sources: patch.sources && typeof patch.sources === "object" ? patch.sources : prev.sources || {},
|
|
7504
|
+
});
|
|
7505
|
+
}
|
|
7506
|
+
|
|
7507
|
+
function prdWorkflowReadClientState(scopedRoot, tapdId) {
|
|
7508
|
+
const data = prdWorkflowReadJsonFile(prdWorkflowClientsPath(scopedRoot, tapdId), null);
|
|
7509
|
+
if (!data) return { version: 1, tapdId: String(tapdId || ""), updatedAt: "", clients: {} };
|
|
7510
|
+
const clients = data.clients && typeof data.clients === "object" && !Array.isArray(data.clients)
|
|
7511
|
+
? data.clients
|
|
7512
|
+
: {};
|
|
7513
|
+
return { ...data, clients };
|
|
7514
|
+
}
|
|
7515
|
+
|
|
7516
|
+
function prdWorkflowReadClientStateWithFallback(root, scopedRoot, tapdId) {
|
|
7517
|
+
const merged = { version: 1, tapdId: String(tapdId || ""), updatedAt: "", clients: {} };
|
|
7518
|
+
const add = (state, cacheScope) => {
|
|
7519
|
+
if (!state?.clients) return;
|
|
7520
|
+
if (state.updatedAt && (!merged.updatedAt || Date.parse(state.updatedAt) > Date.parse(merged.updatedAt))) {
|
|
7521
|
+
merged.updatedAt = state.updatedAt;
|
|
7522
|
+
}
|
|
7523
|
+
for (const [clientId, item] of Object.entries(state.clients)) {
|
|
7524
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
7525
|
+
const prev = merged.clients[clientId];
|
|
7526
|
+
const itemTime = Date.parse(item.reportedAt || item.observedAt || "");
|
|
7527
|
+
const prevTime = Date.parse(prev?.reportedAt || prev?.observedAt || "");
|
|
7528
|
+
if (!prev || !Number.isFinite(prevTime) || (Number.isFinite(itemTime) && itemTime >= prevTime)) {
|
|
7529
|
+
merged.clients[clientId] = { ...item, cacheScope };
|
|
7530
|
+
}
|
|
7531
|
+
}
|
|
7532
|
+
};
|
|
7533
|
+
if (root && path.resolve(root) !== path.resolve(scopedRoot || root)) {
|
|
7534
|
+
add(prdWorkflowReadClientState(root, tapdId), "global");
|
|
7535
|
+
}
|
|
7536
|
+
add(prdWorkflowReadClientState(scopedRoot, tapdId), "scoped");
|
|
7537
|
+
return merged;
|
|
7538
|
+
}
|
|
7539
|
+
|
|
7540
|
+
function prdWorkflowWriteClientObservation(scopedRoot, tapdId, meta, snapshot) {
|
|
7541
|
+
const state = prdWorkflowReadClientState(scopedRoot, tapdId);
|
|
7542
|
+
const clientId = prdWorkflowSafeStateId(meta.clientId || "anonymous");
|
|
7543
|
+
const nextClients = {
|
|
7544
|
+
...state.clients,
|
|
7545
|
+
[clientId]: {
|
|
7546
|
+
clientId: String(meta.clientId || clientId),
|
|
7547
|
+
userId: String(meta.userId || ""),
|
|
7548
|
+
observedAt: String(meta.observedAt || ""),
|
|
7549
|
+
reportedAt: String(meta.reportedAt || new Date().toISOString()),
|
|
7550
|
+
revision: String(snapshot?.revision || ""),
|
|
7551
|
+
phase: String(snapshot?.phase || ""),
|
|
7552
|
+
pointer: String(snapshot?.pointer || ""),
|
|
7553
|
+
nextAction: snapshot?.nextAction || null,
|
|
7554
|
+
issues: Array.isArray(snapshot?.issues) ? snapshot.issues : [],
|
|
7555
|
+
artifacts: Array.isArray(snapshot?.artifacts) ? snapshot.artifacts : [],
|
|
7556
|
+
snapshot,
|
|
7557
|
+
},
|
|
7558
|
+
};
|
|
7559
|
+
return prdWorkflowWriteJsonFile(prdWorkflowClientsPath(scopedRoot, tapdId), {
|
|
7560
|
+
version: 1,
|
|
7561
|
+
tapdId: String(tapdId || ""),
|
|
7562
|
+
updatedAt: new Date().toISOString(),
|
|
7563
|
+
clients: nextClients,
|
|
7564
|
+
});
|
|
7565
|
+
}
|
|
7566
|
+
|
|
7567
|
+
const PRD_WORKFLOW_PROJECTION_SOURCE_KEYS = new Set([
|
|
7568
|
+
"projectionMode",
|
|
7569
|
+
"projectCacheScope",
|
|
7570
|
+
"legacyCacheScope",
|
|
7571
|
+
"clientsUpdatedAt",
|
|
7572
|
+
"runtimeEventsUpdatedAt",
|
|
7573
|
+
]);
|
|
7574
|
+
|
|
7575
|
+
function prdWorkflowStoredObservationSources(...sourcesList) {
|
|
7576
|
+
const out = {};
|
|
7577
|
+
for (const sources of sourcesList) {
|
|
7578
|
+
if (!sources || typeof sources !== "object" || Array.isArray(sources)) continue;
|
|
7579
|
+
for (const [key, value] of Object.entries(sources)) {
|
|
7580
|
+
if (PRD_WORKFLOW_PROJECTION_SOURCE_KEYS.has(key)) continue;
|
|
7581
|
+
out[key] = value;
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
return out;
|
|
7585
|
+
}
|
|
7586
|
+
|
|
7587
|
+
function prdWorkflowStoredObservationSnapshot(snapshot, sourcePatch = {}) {
|
|
7588
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return snapshot;
|
|
7589
|
+
const clean = { ...snapshot };
|
|
7590
|
+
delete clean.events;
|
|
7591
|
+
delete clean.runtimeEvents;
|
|
7592
|
+
delete clean.runtime_events;
|
|
7593
|
+
delete clean.collaboration;
|
|
7594
|
+
delete clean.clientObservations;
|
|
7595
|
+
delete clean.clients;
|
|
7596
|
+
clean.sources = prdWorkflowStoredObservationSources(snapshot.sources, sourcePatch);
|
|
7597
|
+
return clean;
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7600
|
+
function prdWorkflowProjectFactSource(payload = {}, rawSnapshot = {}) {
|
|
7601
|
+
const sources = rawSnapshot?.sources && typeof rawSnapshot.sources === "object" && !Array.isArray(rawSnapshot.sources)
|
|
7602
|
+
? rawSnapshot.sources
|
|
7603
|
+
: {};
|
|
7604
|
+
const truth = String(payload.truth || rawSnapshot.truth || sources.truth || "").trim().toLowerCase();
|
|
7605
|
+
if (!["durable_fact", "project_fact"].includes(truth)) return null;
|
|
7606
|
+
return {
|
|
7607
|
+
truth,
|
|
7608
|
+
authority: String(payload.authority || rawSnapshot.authority || sources.authority || "ai-doc").trim() || "ai-doc",
|
|
7609
|
+
persistence: String(payload.persistence || rawSnapshot.persistence || sources.persistence || "ai-doc").trim() || "ai-doc",
|
|
7610
|
+
};
|
|
7611
|
+
}
|
|
7612
|
+
|
|
7613
|
+
function prdWorkflowSnapshotMetaFromReport(payload = {}, rawSnapshot = {}, req = null, userCtx = {}) {
|
|
7614
|
+
const sources = rawSnapshot.sources && typeof rawSnapshot.sources === "object" && !Array.isArray(rawSnapshot.sources)
|
|
7615
|
+
? rawSnapshot.sources
|
|
7616
|
+
: {};
|
|
7617
|
+
const headerClientId = req?.headers?.["x-agentflow-client-id"];
|
|
7618
|
+
const headerObservedAt = req?.headers?.["x-agentflow-observed-at"];
|
|
7619
|
+
const reportedAt = new Date().toISOString();
|
|
7620
|
+
return {
|
|
7621
|
+
reportedAt,
|
|
7622
|
+
observedAt: String(payload.observedAt || payload.observed_at || rawSnapshot.observedAt || rawSnapshot.observed_at || sources.observedAt || sources.checkedAt || headerObservedAt || reportedAt),
|
|
7623
|
+
clientId: String(payload.clientId || payload.client_id || sources.clientId || headerClientId || userCtx?.userId || "anonymous").slice(0, 160),
|
|
7624
|
+
userId: String(userCtx?.userId || payload.userId || payload.user_id || "").slice(0, 160),
|
|
7625
|
+
baseRevision: String(payload.baseRevision || payload.base_revision || payload.expectedRevision || payload.expected_revision || rawSnapshot.baseRevision || sources.baseRevision || "").trim(),
|
|
7626
|
+
scope: String(payload.scope || rawSnapshot.scope || rawSnapshot.next?.scope || sources.scope || "client").trim().toLowerCase() || "client",
|
|
7627
|
+
platform: String(payload.platform || rawSnapshot.platform || rawSnapshot.next?.platform || sources.platform || "").trim(),
|
|
7628
|
+
issueKey: String(payload.issueKey || payload.issue_key || rawSnapshot.issueKey || rawSnapshot.issue_key || rawSnapshot.next?.issue || rawSnapshot.next?.issueKey || "").trim(),
|
|
7629
|
+
stageKey: String(payload.stageKey || payload.stage_key || rawSnapshot.stageKey || rawSnapshot.stage_key || rawSnapshot.next?.code || "").trim(),
|
|
7630
|
+
force: payload.force === true || payload.force === "1",
|
|
7631
|
+
};
|
|
7632
|
+
}
|
|
7633
|
+
|
|
7634
|
+
function prdWorkflowSnapshotReportConflict(existingRecord, incomingSnapshot, meta) {
|
|
7635
|
+
if (!existingRecord?.snapshot || meta.force) return null;
|
|
7636
|
+
const current = existingRecord.snapshot;
|
|
7637
|
+
const currentRevision = String(current.revision || "").trim();
|
|
7638
|
+
const incomingRevision = String(incomingSnapshot?.revision || "").trim();
|
|
7639
|
+
if (meta.baseRevision && currentRevision && meta.baseRevision !== currentRevision) {
|
|
7640
|
+
return {
|
|
7641
|
+
reason: "base-revision-mismatch",
|
|
7642
|
+
message: `snapshot base revision ${meta.baseRevision} is stale; current revision is ${currentRevision}`,
|
|
7643
|
+
expectedRevision: meta.baseRevision,
|
|
7644
|
+
currentRevision,
|
|
7645
|
+
incomingRevision,
|
|
7646
|
+
};
|
|
7647
|
+
}
|
|
7648
|
+
const incomingObservedAt = Date.parse(meta.observedAt || "");
|
|
7649
|
+
const currentObservedAt = Date.parse(
|
|
7650
|
+
current?.sources?.clientObservedAt ||
|
|
7651
|
+
current?.sources?.clientReportedAt ||
|
|
7652
|
+
existingRecord.updatedAt ||
|
|
7653
|
+
"",
|
|
7654
|
+
);
|
|
7655
|
+
if (
|
|
7656
|
+
Number.isFinite(incomingObservedAt) &&
|
|
7657
|
+
Number.isFinite(currentObservedAt) &&
|
|
7658
|
+
incomingObservedAt + 1000 < currentObservedAt &&
|
|
7659
|
+
incomingRevision !== currentRevision
|
|
7660
|
+
) {
|
|
7661
|
+
return {
|
|
7662
|
+
reason: "older-observation",
|
|
7663
|
+
message: "snapshot was observed before the current cached workflow state",
|
|
7664
|
+
currentRevision,
|
|
7665
|
+
incomingRevision,
|
|
7666
|
+
currentObservedAt: new Date(currentObservedAt).toISOString(),
|
|
7667
|
+
incomingObservedAt: new Date(incomingObservedAt).toISOString(),
|
|
7668
|
+
};
|
|
7669
|
+
}
|
|
7670
|
+
return null;
|
|
7671
|
+
}
|
|
7672
|
+
|
|
7673
|
+
function prdWorkflowClientObservationRows(root, scopedRoot, tapdId) {
|
|
7674
|
+
const state = prdWorkflowReadClientStateWithFallback(root, scopedRoot, tapdId);
|
|
7675
|
+
return Object.values(state.clients || {})
|
|
7676
|
+
.filter((item) => item && typeof item === "object" && !Array.isArray(item))
|
|
7677
|
+
.sort((a, b) => Date.parse(b.reportedAt || b.observedAt || "") - Date.parse(a.reportedAt || a.observedAt || ""));
|
|
7678
|
+
}
|
|
7679
|
+
|
|
7680
|
+
function prdWorkflowLatestClientSnapshot(root, scopedRoot, tapdId) {
|
|
7681
|
+
const latest = prdWorkflowClientObservationRows(root, scopedRoot, tapdId)[0];
|
|
7682
|
+
return latest?.snapshot && typeof latest.snapshot === "object" && !Array.isArray(latest.snapshot)
|
|
7683
|
+
? latest.snapshot
|
|
7684
|
+
: null;
|
|
7685
|
+
}
|
|
7686
|
+
|
|
7687
|
+
function prdWorkflowMergedClientIssues(clientRows = []) {
|
|
7688
|
+
const out = [];
|
|
7689
|
+
const seen = new Map();
|
|
7690
|
+
for (const client of clientRows) {
|
|
7691
|
+
const issues = Array.isArray(client.snapshot?.issues) ? client.snapshot.issues : Array.isArray(client.issues) ? client.issues : [];
|
|
7692
|
+
for (const issue of issues) {
|
|
7693
|
+
if (!issue || typeof issue !== "object" || Array.isArray(issue)) continue;
|
|
7694
|
+
const key = String(issue.key || issue.issueKey || issue.issue_key || issue.id || issue.title || "").trim();
|
|
7695
|
+
const platform = String(issue.platform || "").trim();
|
|
7696
|
+
const mergeKey = [key || "issue", platform || "all"].join("|");
|
|
7697
|
+
const prevIndex = seen.get(mergeKey);
|
|
7698
|
+
const next = {
|
|
7699
|
+
...issue,
|
|
7700
|
+
key: key || issue.key,
|
|
7701
|
+
platform: platform || issue.platform,
|
|
7702
|
+
observedBy: client.clientId || "",
|
|
7703
|
+
observedAt: client.observedAt || client.reportedAt || "",
|
|
7704
|
+
};
|
|
7705
|
+
if (prevIndex != null) out[prevIndex] = { ...out[prevIndex], ...next };
|
|
7706
|
+
else {
|
|
7707
|
+
seen.set(mergeKey, out.length);
|
|
7708
|
+
out.push(next);
|
|
7709
|
+
}
|
|
7710
|
+
}
|
|
7711
|
+
}
|
|
7712
|
+
return out;
|
|
7713
|
+
}
|
|
7714
|
+
|
|
7715
|
+
function prdWorkflowMaterializeSnapshot(root, scopedRoot, tapdId, userCtx = {}, opts = {}) {
|
|
7716
|
+
const flowSource = String(opts.flowSource || "user").trim() || "user";
|
|
7717
|
+
const flowId = String(opts.flowId || "").trim();
|
|
7718
|
+
const project = prdWorkflowReadProjectStateWithFallback(root, scopedRoot, tapdId);
|
|
7719
|
+
const legacy = prdWorkflowReadCachedSnapshotWithFallback(root, scopedRoot, tapdId);
|
|
7720
|
+
const latestClient = prdWorkflowLatestClientSnapshot(root, scopedRoot, tapdId);
|
|
7721
|
+
const base = (
|
|
7722
|
+
project?.snapshot ||
|
|
7723
|
+
latestClient ||
|
|
7724
|
+
legacy?.snapshot ||
|
|
7725
|
+
prdWorkflowFallbackSnapshot(tapdId, "client_snapshot_required", "等待客户端 prd-flow skill 上报 Workflow snapshot", {
|
|
7726
|
+
optionalGaps: [{
|
|
7727
|
+
severity: "warn",
|
|
7728
|
+
text: "Workflow 服务端不会执行客户端 workspace 里的 prd-flow。请在客户端运行 prd-flow current <tapd_id>,并用 AGENTFLOW_BASE_URL + AGENTFLOW_TOKEN 上报到 /api/prd-workflow/snapshot。",
|
|
7729
|
+
}],
|
|
7730
|
+
sources: { executionMode: "client-report" },
|
|
7731
|
+
})
|
|
7732
|
+
);
|
|
7733
|
+
const clientObservations = prdWorkflowClientObservationRows(root, scopedRoot, tapdId).map((item) => ({
|
|
7734
|
+
clientId: item.clientId,
|
|
7735
|
+
userId: item.userId || "",
|
|
7736
|
+
phase: item.phase || "",
|
|
7737
|
+
pointer: item.pointer || "",
|
|
7738
|
+
revision: item.revision || "",
|
|
7739
|
+
observedAt: item.observedAt || "",
|
|
7740
|
+
reportedAt: item.reportedAt || "",
|
|
7741
|
+
nextAction: item.nextAction || null,
|
|
7742
|
+
cacheScope: item.cacheScope || "",
|
|
7743
|
+
}));
|
|
7744
|
+
const clientRows = prdWorkflowClientObservationRows(root, scopedRoot, tapdId);
|
|
7745
|
+
const clientIssues = prdWorkflowMergedClientIssues(clientRows);
|
|
7746
|
+
const materialized = prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, {
|
|
7747
|
+
...base,
|
|
7748
|
+
issues: project?.snapshot ? base.issues : clientIssues.length ? clientIssues : base.issues,
|
|
7749
|
+
issueGroups: project?.snapshot ? base.issueGroups : clientIssues.length ? clientIssues : base.issueGroups,
|
|
7750
|
+
collaboration: prdWorkflowCollaborationState(userCtx, flowSource, flowId, tapdId),
|
|
7751
|
+
clientObservations,
|
|
7752
|
+
clients: clientObservations,
|
|
7753
|
+
sources: {
|
|
7754
|
+
...(base.sources && typeof base.sources === "object" ? base.sources : {}),
|
|
7755
|
+
projectionMode: project?.snapshot ? "project" : latestClient ? "client-observations" : legacy?.snapshot ? "legacy-cache" : "empty",
|
|
7756
|
+
projectCacheScope: project?.cacheScope || "",
|
|
7757
|
+
legacyCacheScope: legacy?.cacheScope || "",
|
|
7758
|
+
clientsUpdatedAt: prdWorkflowReadClientStateWithFallback(root, scopedRoot, tapdId).updatedAt || "",
|
|
7759
|
+
checkedAt: new Date().toISOString(),
|
|
7760
|
+
},
|
|
7761
|
+
});
|
|
7762
|
+
if (!project?.snapshot && latestClient) {
|
|
7763
|
+
materialized.optionalGaps = [
|
|
7764
|
+
...(Array.isArray(materialized.optionalGaps) ? materialized.optionalGaps : []),
|
|
7765
|
+
{ severity: "info", text: "当前展示来自最新客户端观察;尚未形成独立 project materialized view。" },
|
|
7766
|
+
];
|
|
7767
|
+
}
|
|
7768
|
+
return materialized;
|
|
7769
|
+
}
|
|
7770
|
+
|
|
7771
|
+
function prdWorkflowNormalizeStoredRuntimeEvent(tapdId, event = {}) {
|
|
7772
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return null;
|
|
7773
|
+
const out = { ...event };
|
|
7774
|
+
const type = String(out.type || out.kind || "").trim();
|
|
7775
|
+
const source = String(out.source || "").trim();
|
|
7776
|
+
const idem = String(out.idempotencyKey || out.idempotency_key || "").trim();
|
|
7777
|
+
const status = String(out.status || "").trim().toLowerCase();
|
|
7778
|
+
const isSnapshotObservation =
|
|
7779
|
+
idem.startsWith("snapshot-action:") ||
|
|
7780
|
+
(!out.truth && source === "prd-flow-client" && type === "workflow-action");
|
|
7781
|
+
if (isSnapshotObservation) {
|
|
7782
|
+
out.truth = out.truth || "observation";
|
|
7783
|
+
out.persistence = out.persistence || "runtime";
|
|
7784
|
+
out.authority = out.authority || "client";
|
|
7785
|
+
if (["done", "success", "completed", "passed"].includes(status)) out.status = "observed";
|
|
7786
|
+
} else {
|
|
7787
|
+
out.truth = out.truth || (type === "review-link" ? "runtime_event" : "runtime_event");
|
|
7788
|
+
out.persistence = out.persistence || "runtime";
|
|
7789
|
+
out.authority = out.authority || source || "agentflow";
|
|
7790
|
+
}
|
|
7791
|
+
if (type === "review-link") {
|
|
7792
|
+
const durability = String(out.durability || "").trim().toLowerCase();
|
|
7793
|
+
const sourceArtifact = out.sourceArtifact && typeof out.sourceArtifact === "object" && !Array.isArray(out.sourceArtifact)
|
|
7794
|
+
? out.sourceArtifact
|
|
7795
|
+
: {
|
|
7796
|
+
kind: durability === "durable" ? "ai-doc" : "local-draft",
|
|
7797
|
+
durability: durability || "temporary",
|
|
7798
|
+
};
|
|
7799
|
+
out.truth = out.truth || "runtime_event";
|
|
7800
|
+
out.persistence = "runtime";
|
|
7801
|
+
out.sourceArtifact = sourceArtifact;
|
|
7802
|
+
const normalizeReviewRef = (item) => {
|
|
7803
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
7804
|
+
const kind = String(item.kind || "").trim();
|
|
7805
|
+
const hasReviewUrl = /\/api\/prd-workflow\/review\//.test(String(item.url || item.href || ""));
|
|
7806
|
+
if (!kind && !hasReviewUrl) return item;
|
|
7807
|
+
return {
|
|
7808
|
+
...item,
|
|
7809
|
+
persistence: item.persistence || "runtime",
|
|
7810
|
+
source: item.source && typeof item.source === "object" && !Array.isArray(item.source) ? item.source : sourceArtifact,
|
|
7811
|
+
};
|
|
7812
|
+
};
|
|
7813
|
+
if (Array.isArray(out.artifacts)) out.artifacts = out.artifacts.map(normalizeReviewRef);
|
|
7814
|
+
if (Array.isArray(out.links)) out.links = out.links.map(normalizeReviewRef);
|
|
7815
|
+
}
|
|
7816
|
+
out.tapdId = String(out.tapdId || out.tapd_id || tapdId || "");
|
|
7817
|
+
return out;
|
|
7818
|
+
}
|
|
7819
|
+
|
|
7820
|
+
function prdWorkflowReadRuntimeEvents(scopedRoot, tapdId) {
|
|
7821
|
+
try {
|
|
7822
|
+
const p = prdWorkflowEventsPath(scopedRoot, tapdId);
|
|
7823
|
+
if (!fs.existsSync(p)) return { version: 1, tapdId: String(tapdId || ""), events: [] };
|
|
7824
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
7825
|
+
const events = Array.isArray(data?.events)
|
|
7826
|
+
? data.events
|
|
7827
|
+
.map((item) => prdWorkflowNormalizeStoredRuntimeEvent(data?.tapdId || tapdId, item))
|
|
7828
|
+
.filter((item) => item && typeof item === "object")
|
|
7829
|
+
: [];
|
|
7830
|
+
return {
|
|
7831
|
+
version: 1,
|
|
7832
|
+
tapdId: String(data?.tapdId || tapdId || ""),
|
|
7833
|
+
updatedAt: data?.updatedAt || "",
|
|
7834
|
+
events,
|
|
7835
|
+
};
|
|
7836
|
+
} catch {
|
|
7837
|
+
return { version: 1, tapdId: String(tapdId || ""), events: [] };
|
|
7838
|
+
}
|
|
7839
|
+
}
|
|
7840
|
+
|
|
7841
|
+
function prdWorkflowWriteRuntimeEvents(scopedRoot, tapdId, events) {
|
|
7842
|
+
const p = prdWorkflowEventsPath(scopedRoot, tapdId);
|
|
7843
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
7844
|
+
const data = {
|
|
7845
|
+
version: 1,
|
|
7846
|
+
tapdId: String(tapdId || ""),
|
|
7847
|
+
updatedAt: new Date().toISOString(),
|
|
7848
|
+
events: Array.isArray(events) ? events.slice(-PRD_WORKFLOW_RUNTIME_EVENTS_MAX) : [],
|
|
7849
|
+
};
|
|
7850
|
+
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
7851
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
7852
|
+
fs.renameSync(tmp, p);
|
|
7853
|
+
return data;
|
|
7854
|
+
}
|
|
7855
|
+
|
|
7856
|
+
function prdWorkflowRuntimeEventStatus(type, status) {
|
|
7857
|
+
const s = String(status || "").trim().toLowerCase();
|
|
7858
|
+
if (s) return s;
|
|
7859
|
+
const t = String(type || "").trim().toLowerCase();
|
|
7860
|
+
if (t.includes("done") || t.includes("success") || t.includes("completed")) return "done";
|
|
7861
|
+
if (t.includes("error") || t.includes("failed") || t.includes("conflict")) return "error";
|
|
7862
|
+
if (t.includes("start") || t.includes("running")) return "running";
|
|
7863
|
+
return "pending";
|
|
7864
|
+
}
|
|
7865
|
+
|
|
7866
|
+
function prdWorkflowRuntimeEventCanonicalStage(event = {}) {
|
|
7867
|
+
const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
|
|
7868
|
+
const action = String(event.action || event.actionId || event.action_id || "").trim();
|
|
7869
|
+
const rawStage = String(event.stage || event.stageKey || event.stage_key || event.phase || event.code || action || "").trim();
|
|
7870
|
+
const text = [rawStage, action, event.code, event.type, event.title].map((value) => String(value || "")).join(" ").toLowerCase();
|
|
7871
|
+
if (issue) {
|
|
7872
|
+
if (/plan_draft_local|submit-plan|plan-doc/.test(text)) return `issue-plan:${issue}`;
|
|
7873
|
+
if (/gitlab_issue_missing|ensure-gitlab-issue/.test(text)) return `issue-gitlab:${issue}`;
|
|
7874
|
+
}
|
|
7875
|
+
return rawStage || action;
|
|
7876
|
+
}
|
|
7877
|
+
|
|
7878
|
+
function prdWorkflowRuntimeEventId(event = {}) {
|
|
7879
|
+
const stage = prdWorkflowRuntimeEventCanonicalStage(event);
|
|
7880
|
+
const action = String(event.action || event.actionId || event.action_id || "").trim();
|
|
7881
|
+
const scope = String(event.scope || "").trim();
|
|
7882
|
+
const platform = String(event.platform || "").trim();
|
|
7883
|
+
const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
|
|
7884
|
+
if ((stage || action) && aggregateByStage) {
|
|
7885
|
+
const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
|
|
7886
|
+
const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
7887
|
+
return `stage_${prdWorkflowSafeStateId(key)}`;
|
|
7888
|
+
}
|
|
7889
|
+
const existing = String(event.id || event.eventId || event.event_id || "").trim();
|
|
7890
|
+
if (existing) return existing.slice(0, 160);
|
|
7891
|
+
if (stage || action) {
|
|
7892
|
+
const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
|
|
7893
|
+
const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
7894
|
+
return `stage_${prdWorkflowSafeStateId(key)}`;
|
|
7895
|
+
}
|
|
7896
|
+
return `evt_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
|
|
7897
|
+
}
|
|
7898
|
+
|
|
7899
|
+
function prdWorkflowCompactRuntimeValue(value, maxChars = 24000) {
|
|
7900
|
+
if (value == null) return value;
|
|
7901
|
+
try {
|
|
7902
|
+
const text = JSON.stringify(value);
|
|
7903
|
+
if (!text || text.length <= maxChars) return value;
|
|
7904
|
+
return { truncated: true, preview: text.slice(0, maxChars) };
|
|
7905
|
+
} catch {
|
|
7906
|
+
const text = String(value || "");
|
|
7907
|
+
return text.length <= maxChars ? text : { truncated: true, preview: text.slice(0, maxChars) };
|
|
7908
|
+
}
|
|
7909
|
+
}
|
|
7910
|
+
|
|
7911
|
+
function prdWorkflowNormalizeRuntimeEvent(tapdId, event = {}) {
|
|
7912
|
+
const now = new Date().toISOString();
|
|
7913
|
+
const type = String(event.type || event.kind || "workflow-event").trim().slice(0, 120);
|
|
7914
|
+
const action = String(event.action || event.actionId || event.action_id || "").trim().slice(0, 160);
|
|
7915
|
+
const stage = String(event.stage || event.stageKey || event.stage_key || event.phase || action || "").trim().slice(0, 160);
|
|
7916
|
+
const scope = String(event.scope || "").trim().slice(0, 80);
|
|
7917
|
+
const platform = String(event.platform || "").trim().slice(0, 80);
|
|
7918
|
+
const status = prdWorkflowRuntimeEventStatus(type, event.status);
|
|
7919
|
+
const source = String(event.source || "agentflow").trim().slice(0, 80) || "agentflow";
|
|
7920
|
+
const truth = String(
|
|
7921
|
+
event.truth ||
|
|
7922
|
+
event.stateTruth ||
|
|
7923
|
+
event.state_truth ||
|
|
7924
|
+
(type === "review-link" ? "runtime_event" : source === "prd-flow-client" && type === "workflow-action" ? "observation" : "runtime_event"),
|
|
7925
|
+
).trim();
|
|
7926
|
+
const persistence = String(event.persistence || event.storage || "runtime").trim();
|
|
7927
|
+
const authority = String(event.authority || (truth === "observation" ? "client" : source)).trim();
|
|
7928
|
+
const entry = {
|
|
7929
|
+
...event,
|
|
7930
|
+
id: prdWorkflowRuntimeEventId(event),
|
|
7931
|
+
source,
|
|
7932
|
+
runtime: event.runtime !== false,
|
|
7933
|
+
type,
|
|
7934
|
+
tapdId: String(event.tapdId || event.tapd_id || tapdId || ""),
|
|
7935
|
+
status,
|
|
7936
|
+
truth,
|
|
7937
|
+
persistence,
|
|
7938
|
+
authority,
|
|
7939
|
+
updatedAt: now,
|
|
7940
|
+
};
|
|
7941
|
+
const sourceEventId = String(event.id || event.eventId || event.event_id || "").trim();
|
|
7942
|
+
if (sourceEventId && sourceEventId !== entry.id) entry.sourceEventId = sourceEventId.slice(0, 160);
|
|
7943
|
+
if (action) {
|
|
7944
|
+
entry.action = action;
|
|
7945
|
+
entry.actionId = String(event.actionId || event.action_id || action);
|
|
7946
|
+
}
|
|
7947
|
+
if (stage) {
|
|
7948
|
+
entry.stage = stage;
|
|
7949
|
+
entry.stageKey = String(event.stageKey || event.stage_key || stage);
|
|
7950
|
+
}
|
|
7951
|
+
if (scope) entry.scope = scope;
|
|
7952
|
+
if (platform) entry.platform = platform;
|
|
7953
|
+
if (!entry.createdAt) entry.createdAt = event.startedAt || now;
|
|
7954
|
+
if (!entry.title && (stage || action)) entry.title = stage || action;
|
|
7955
|
+
if (!entry.detail && event.message) entry.detail = String(event.message || "").slice(0, 4000);
|
|
7956
|
+
if (entry.rawOutput) entry.rawOutput = String(entry.rawOutput).slice(0, 12000);
|
|
7957
|
+
if (entry.error) entry.error = String(entry.error).slice(0, 4000);
|
|
7958
|
+
if (entry.output != null) entry.output = prdWorkflowCompactRuntimeValue(entry.output);
|
|
7959
|
+
if (entry.result != null) entry.result = prdWorkflowCompactRuntimeValue(entry.result);
|
|
7960
|
+
const history = Array.isArray(event.idempotencyHistory) ? event.idempotencyHistory : [];
|
|
7961
|
+
const idem = String(event.idempotencyKey || "").trim();
|
|
7962
|
+
if (idem && !history.includes(idem)) entry.idempotencyHistory = [...history, idem].slice(-50);
|
|
7963
|
+
return entry;
|
|
7964
|
+
}
|
|
7965
|
+
|
|
7966
|
+
function prdWorkflowMergeRuntimeEventArrays(left, right) {
|
|
7967
|
+
const out = [];
|
|
7968
|
+
const seen = new Set();
|
|
7969
|
+
for (const value of [...(Array.isArray(left) ? left : []), ...(Array.isArray(right) ? right : [])]) {
|
|
7970
|
+
if (value == null) continue;
|
|
7971
|
+
let key = "";
|
|
7972
|
+
try {
|
|
7973
|
+
key = typeof value === "string" ? value : JSON.stringify(value);
|
|
7974
|
+
} catch {
|
|
7975
|
+
key = String(value);
|
|
7976
|
+
}
|
|
7977
|
+
if (seen.has(key)) continue;
|
|
7978
|
+
seen.add(key);
|
|
7979
|
+
out.push(value);
|
|
7980
|
+
}
|
|
7981
|
+
return out;
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
function prdWorkflowRuntimeEventArtifactSignature(event = {}) {
|
|
7985
|
+
const explicit = String(event.artifactHash || event.artifact_hash || event.hash || "").trim();
|
|
7986
|
+
if (explicit) return explicit;
|
|
7987
|
+
const parts = [
|
|
7988
|
+
event.artifact,
|
|
7989
|
+
event.artifactUrl || event.artifact_url,
|
|
7990
|
+
event.url,
|
|
7991
|
+
event.reviewUrl || event.review_url,
|
|
7992
|
+
event.planDoc || event.plan_doc,
|
|
7993
|
+
].map((value) => String(value || "").trim()).filter(Boolean);
|
|
7994
|
+
if (parts.length) return parts.join("|");
|
|
7995
|
+
const artifacts = Array.isArray(event.artifacts) ? event.artifacts : [];
|
|
7996
|
+
const urls = artifacts
|
|
7997
|
+
.map((item) => String(item?.url || item?.href || item?.path || item?.label || "").trim())
|
|
7998
|
+
.filter(Boolean);
|
|
7999
|
+
return urls.length ? urls.join("|") : "";
|
|
8000
|
+
}
|
|
8001
|
+
|
|
8002
|
+
function prdWorkflowRuntimeEventShouldConflictOnArtifact(event = {}) {
|
|
8003
|
+
if (event.conflictOnArtifact === true || event.conflict_on_artifact === true) return true;
|
|
8004
|
+
if (event.conflictOnArtifact === false || event.conflict_on_artifact === false) return false;
|
|
8005
|
+
if (String(event.type || "") === "review-link") return false;
|
|
8006
|
+
if ((Array.isArray(event.artifacts) ? event.artifacts : []).some((item) => String(item?.kind || "") === "temporary-review")) return false;
|
|
8007
|
+
const stage = String(event.stage || event.stageKey || event.stage_key || event.action || "").toLowerCase();
|
|
8008
|
+
return /submit-plan|project-plan|tech-design|plan/.test(stage);
|
|
8009
|
+
}
|
|
8010
|
+
|
|
8011
|
+
function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
|
|
8012
|
+
try {
|
|
8013
|
+
const current = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
|
|
8014
|
+
const entry = prdWorkflowNormalizeRuntimeEvent(tapdId, event);
|
|
8015
|
+
const entryIdem = String(entry.idempotencyKey || "").trim();
|
|
8016
|
+
const index = current.events.findIndex((item) => {
|
|
8017
|
+
if (String(item?.id || "") === entry.id) return true;
|
|
8018
|
+
if (!entryIdem) return false;
|
|
8019
|
+
if (String(item?.idempotencyKey || "").trim() === entryIdem) return true;
|
|
8020
|
+
return Array.isArray(item?.idempotencyHistory) && item.idempotencyHistory.includes(entryIdem);
|
|
8021
|
+
});
|
|
8022
|
+
const events = [...current.events];
|
|
8023
|
+
if (index >= 0) {
|
|
8024
|
+
const prevArtifact = prdWorkflowRuntimeEventArtifactSignature(events[index]);
|
|
8025
|
+
const nextArtifact = prdWorkflowRuntimeEventArtifactSignature(entry);
|
|
8026
|
+
const artifactConflict = prevArtifact && nextArtifact && prevArtifact !== nextArtifact &&
|
|
8027
|
+
prdWorkflowRuntimeEventShouldConflictOnArtifact(events[index]) &&
|
|
8028
|
+
prdWorkflowRuntimeEventShouldConflictOnArtifact(entry);
|
|
8029
|
+
const idempotencyHistory = [
|
|
8030
|
+
...(events[index].idempotencyKey ? [events[index].idempotencyKey] : []),
|
|
8031
|
+
...(entry.idempotencyKey ? [entry.idempotencyKey] : []),
|
|
8032
|
+
...(Array.isArray(events[index].idempotencyHistory) ? events[index].idempotencyHistory : []),
|
|
8033
|
+
...(Array.isArray(entry.idempotencyHistory) ? entry.idempotencyHistory : []),
|
|
8034
|
+
];
|
|
8035
|
+
events[index] = {
|
|
8036
|
+
...events[index],
|
|
8037
|
+
...entry,
|
|
8038
|
+
links: prdWorkflowMergeRuntimeEventArrays(events[index].links, entry.links),
|
|
8039
|
+
artifacts: prdWorkflowMergeRuntimeEventArrays(events[index].artifacts, entry.artifacts),
|
|
8040
|
+
outputs: prdWorkflowMergeRuntimeEventArrays(events[index].outputs, entry.outputs),
|
|
8041
|
+
results: prdWorkflowMergeRuntimeEventArrays(events[index].results, entry.results),
|
|
8042
|
+
createdAt: events[index].createdAt || entry.createdAt,
|
|
8043
|
+
startedAt: events[index].startedAt || entry.startedAt,
|
|
8044
|
+
idempotencyHistory: [...new Set(idempotencyHistory)].slice(-50),
|
|
8045
|
+
};
|
|
8046
|
+
if (artifactConflict) {
|
|
8047
|
+
events[index] = {
|
|
8048
|
+
...events[index],
|
|
8049
|
+
type: "same-platform-stage-conflict",
|
|
8050
|
+
status: "conflict",
|
|
8051
|
+
conflict: {
|
|
8052
|
+
type: "same-platform-stage-conflict",
|
|
8053
|
+
previousArtifact: prevArtifact,
|
|
8054
|
+
incomingArtifact: nextArtifact,
|
|
8055
|
+
message: "同一 issue/platform/stage 上报了不同产物,需要本地 agent 拉取后 review。",
|
|
8056
|
+
},
|
|
8057
|
+
};
|
|
8058
|
+
}
|
|
8059
|
+
} else {
|
|
8060
|
+
events.push(entry);
|
|
8061
|
+
}
|
|
8062
|
+
prdWorkflowWriteRuntimeEvents(scopedRoot, tapdId, events);
|
|
8063
|
+
return entry;
|
|
8064
|
+
} catch {
|
|
8065
|
+
return null;
|
|
8066
|
+
}
|
|
8067
|
+
}
|
|
8068
|
+
|
|
8069
|
+
function prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey) {
|
|
8070
|
+
const key = String(idempotencyKey || "").trim();
|
|
8071
|
+
if (!key) return null;
|
|
8072
|
+
const events = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId).events;
|
|
8073
|
+
return [...events].reverse().find((event) => (
|
|
8074
|
+
(String(event?.idempotencyKey || "") === key || (Array.isArray(event?.idempotencyHistory) && event.idempotencyHistory.includes(key))) &&
|
|
8075
|
+
["done", "success", "completed"].includes(String(event?.status || "").toLowerCase())
|
|
8076
|
+
)) || null;
|
|
8077
|
+
}
|
|
8078
|
+
|
|
8079
|
+
function prdWorkflowRuntimeEventDedupeKey(event = {}, index = 0) {
|
|
8080
|
+
const stage = prdWorkflowRuntimeEventCanonicalStage(event);
|
|
8081
|
+
const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
|
|
8082
|
+
if (stage) {
|
|
8083
|
+
const issue = event?.issueKey || event?.issue_key || event?.issue;
|
|
8084
|
+
const platform = event?.platform;
|
|
8085
|
+
if (aggregateByStage || issue || platform) {
|
|
8086
|
+
return ["stage", event?.scope, issue, platform, stage]
|
|
8087
|
+
.map((value) => String(value || "").trim())
|
|
8088
|
+
.join(":");
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
const id = String(event?.id || event?.eventId || event?.event_id || "").trim();
|
|
8092
|
+
if (id) return `id:${id}`;
|
|
8093
|
+
if (stage) {
|
|
8094
|
+
return ["stage", event?.scope, event?.issueKey || event?.issue_key || event?.issue, event?.platform, stage]
|
|
8095
|
+
.map((value) => String(value || "").trim())
|
|
8096
|
+
.join(":");
|
|
8097
|
+
}
|
|
8098
|
+
return `idx:${index}`;
|
|
8099
|
+
}
|
|
8100
|
+
|
|
8101
|
+
function prdWorkflowMergeRuntimeEventList(snapshotEvents = [], runtimeEvents = []) {
|
|
8102
|
+
const out = [];
|
|
8103
|
+
const seen = new Map();
|
|
8104
|
+
for (const event of [...(Array.isArray(snapshotEvents) ? snapshotEvents : []), ...(Array.isArray(runtimeEvents) ? runtimeEvents : [])]) {
|
|
8105
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) continue;
|
|
8106
|
+
const key = prdWorkflowRuntimeEventDedupeKey(event, out.length);
|
|
8107
|
+
const index = seen.get(key);
|
|
8108
|
+
if (index == null) {
|
|
8109
|
+
seen.set(key, out.length);
|
|
8110
|
+
out.push(event);
|
|
8111
|
+
} else {
|
|
8112
|
+
out[index] = {
|
|
8113
|
+
...out[index],
|
|
8114
|
+
...event,
|
|
8115
|
+
links: prdWorkflowMergeRuntimeEventArrays(out[index].links, event.links),
|
|
8116
|
+
artifacts: prdWorkflowMergeRuntimeEventArrays(out[index].artifacts, event.artifacts),
|
|
8117
|
+
outputs: prdWorkflowMergeRuntimeEventArrays(out[index].outputs, event.outputs),
|
|
8118
|
+
results: prdWorkflowMergeRuntimeEventArrays(out[index].results, event.results),
|
|
8119
|
+
};
|
|
8120
|
+
}
|
|
8121
|
+
}
|
|
8122
|
+
return out;
|
|
8123
|
+
}
|
|
8124
|
+
|
|
8125
|
+
function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
8126
|
+
const runtime = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
|
|
8127
|
+
const runtimeEvents = runtime.events;
|
|
8128
|
+
if (!runtimeEvents.length) return snapshot;
|
|
8129
|
+
const events = prdWorkflowMergeRuntimeEventList(snapshot?.events, runtimeEvents);
|
|
8130
|
+
return {
|
|
8131
|
+
...snapshot,
|
|
8132
|
+
runtimeEvents,
|
|
8133
|
+
events,
|
|
8134
|
+
sources: {
|
|
8135
|
+
...(snapshot?.sources && typeof snapshot.sources === "object" ? snapshot.sources : {}),
|
|
8136
|
+
runtimeEventsUpdatedAt: runtime.updatedAt || "",
|
|
8137
|
+
},
|
|
8138
|
+
};
|
|
8139
|
+
}
|
|
8140
|
+
|
|
8141
|
+
function prdWorkflowMockSnapshot(scopedRoot, tapdId = "mock-prd") {
|
|
8142
|
+
const id = String(tapdId || "mock-prd");
|
|
8143
|
+
const now = new Date().toISOString();
|
|
8144
|
+
return prdWorkflowMergeRuntimeEvents(scopedRoot, id, {
|
|
8145
|
+
tapdId: id,
|
|
8146
|
+
phase: "implementing",
|
|
8147
|
+
pointer: "实现中:Tunnel ConfigV3 支持",
|
|
8148
|
+
revision: "mock:tapd-ai-doc-gitlab-runtime",
|
|
8149
|
+
actions: [
|
|
8150
|
+
{
|
|
8151
|
+
id: "stage_tech_design",
|
|
8152
|
+
stage: "tech_design",
|
|
8153
|
+
action: "submit-tech-design",
|
|
8154
|
+
title: "确认技术方案",
|
|
8155
|
+
detail: "tech_design.md 与 TAPD baseline 已归档",
|
|
8156
|
+
status: "done",
|
|
8157
|
+
updatedAt: now,
|
|
8158
|
+
artifacts: [{ label: "技术方案", url: "https://example.com/ai-doc/stories/mock/tech_design.md" }],
|
|
8159
|
+
},
|
|
8160
|
+
{
|
|
8161
|
+
id: "stage_plan_android",
|
|
8162
|
+
stage: "submit-plan",
|
|
8163
|
+
action: "submit-plan",
|
|
8164
|
+
issueKey: "tunnel-config-v3",
|
|
8165
|
+
title: "提交 Android 实施计划",
|
|
8166
|
+
detail: "计划已确认,GitLab Issue 已绑定",
|
|
8167
|
+
status: "done",
|
|
8168
|
+
updatedAt: now,
|
|
8169
|
+
artifacts: [{ label: "Android Plan", url: "https://example.com/ai-doc/stories/mock/android-plan.md" }],
|
|
8170
|
+
},
|
|
8171
|
+
{
|
|
8172
|
+
id: "stage_implementation_tunnel-config-v3",
|
|
8173
|
+
stage: "implementation",
|
|
8174
|
+
action: "mark",
|
|
8175
|
+
issueKey: "tunnel-config-v3",
|
|
8176
|
+
title: "实现 Tunnel ConfigV3",
|
|
8177
|
+
detail: "Android MR 已创建,iOS 待处理",
|
|
8178
|
+
status: "current",
|
|
8179
|
+
updatedAt: now,
|
|
8180
|
+
dryRunSupported: true,
|
|
8181
|
+
},
|
|
8182
|
+
],
|
|
8183
|
+
nextAction: {
|
|
8184
|
+
action: "mark",
|
|
8185
|
+
actionId: "mark-impl-mr",
|
|
8186
|
+
stage: "implementation",
|
|
8187
|
+
issueKey: "tunnel-config-v3",
|
|
8188
|
+
title: "记录实现 MR",
|
|
8189
|
+
detail: "验证 GitLab Issue/MR 关联后写入 Workflow runtime",
|
|
8190
|
+
dryRunSupported: true,
|
|
8191
|
+
},
|
|
8192
|
+
epics: [
|
|
8193
|
+
{
|
|
8194
|
+
key: "network-governance",
|
|
8195
|
+
title: "网络治理",
|
|
8196
|
+
issues: [
|
|
8197
|
+
{
|
|
8198
|
+
key: "tunnel-config-v3",
|
|
8199
|
+
title: "Tunnel ConfigV3 URI 云控支持",
|
|
8200
|
+
platform: "all",
|
|
8201
|
+
status: "implementing",
|
|
8202
|
+
issueUrl: "https://example.com/gitlab/issues/101",
|
|
8203
|
+
planDocUrl: "https://example.com/ai-doc/stories/mock/android-plan.md",
|
|
8204
|
+
mrs: [
|
|
8205
|
+
{ label: "Android MR", platform: "android", url: "https://example.com/gitlab/mr/1", status: "opened" },
|
|
8206
|
+
{ label: "iOS MR", platform: "ios", url: "https://example.com/gitlab/mr/2", status: "todo" },
|
|
8207
|
+
],
|
|
8208
|
+
},
|
|
8209
|
+
{
|
|
8210
|
+
key: "self-test-package",
|
|
8211
|
+
parentKey: "tunnel-config-v3",
|
|
8212
|
+
title: "自测包与验证记录",
|
|
8213
|
+
platform: "all",
|
|
8214
|
+
status: "pending",
|
|
8215
|
+
links: [{ label: "临时 review", url: "https://example.com/review/mock" }],
|
|
8216
|
+
},
|
|
8217
|
+
],
|
|
8218
|
+
},
|
|
8219
|
+
],
|
|
8220
|
+
issues: [
|
|
8221
|
+
{
|
|
8222
|
+
key: "tunnel-config-v3",
|
|
8223
|
+
epicKey: "network-governance",
|
|
8224
|
+
title: "Tunnel ConfigV3 URI 云控支持",
|
|
8225
|
+
platform: "all",
|
|
8226
|
+
status: "implementing",
|
|
8227
|
+
},
|
|
8228
|
+
],
|
|
8229
|
+
artifacts: [
|
|
8230
|
+
{ label: "TAPD 需求", kind: "tapd", url: "https://example.com/tapd/mock" },
|
|
8231
|
+
{ label: "技术方案", kind: "ai-doc", url: "https://example.com/ai-doc/stories/mock/tech_design.md" },
|
|
8232
|
+
],
|
|
8233
|
+
optionalGaps: [{ severity: "info", text: "Mock snapshot: 用于验证 Workflow 阶段、Epic/Issue 层级和 MR 归类。" }],
|
|
8234
|
+
sources: { checkedAt: now, mock: true },
|
|
8235
|
+
});
|
|
8236
|
+
}
|
|
8237
|
+
|
|
8238
|
+
function prdWorkflowWriteCachedSnapshot(scopedRoot, tapdId, snapshot) {
|
|
8239
|
+
try {
|
|
8240
|
+
const p = prdWorkflowCachePath(scopedRoot, tapdId);
|
|
8241
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
8242
|
+
const data = {
|
|
8243
|
+
version: 1,
|
|
8244
|
+
tapdId: String(tapdId || ""),
|
|
8245
|
+
updatedAt: new Date().toISOString(),
|
|
8246
|
+
snapshot,
|
|
8247
|
+
sources: {
|
|
8248
|
+
truth: "projection",
|
|
8249
|
+
authority: "agentflow-runtime",
|
|
8250
|
+
persistence: "cache",
|
|
8251
|
+
},
|
|
8252
|
+
};
|
|
8253
|
+
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
8254
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
8255
|
+
fs.renameSync(tmp, p);
|
|
8256
|
+
return data;
|
|
8257
|
+
} catch {
|
|
8258
|
+
return null;
|
|
8259
|
+
}
|
|
8260
|
+
}
|
|
8261
|
+
|
|
8262
|
+
function prdWorkflowFallbackWithCache(scopedRoot, tapdId, fallback, userCtx = {}, flowSource = "user", flowId = "") {
|
|
8263
|
+
const cached = prdWorkflowReadCachedSnapshot(scopedRoot, tapdId);
|
|
8264
|
+
const snapshot = cached?.snapshot && typeof cached.snapshot === "object" && !Array.isArray(cached.snapshot)
|
|
8265
|
+
? cached.snapshot
|
|
8266
|
+
: null;
|
|
8267
|
+
if (!snapshot) {
|
|
8268
|
+
return {
|
|
8269
|
+
...fallback,
|
|
8270
|
+
collaboration: prdWorkflowCollaborationState(userCtx, flowSource, flowId, tapdId),
|
|
8271
|
+
};
|
|
8272
|
+
}
|
|
8273
|
+
const optionalGaps = [
|
|
8274
|
+
...(Array.isArray(fallback?.optionalGaps) ? fallback.optionalGaps : []),
|
|
8275
|
+
...(Array.isArray(snapshot.optionalGaps) ? snapshot.optionalGaps : []),
|
|
8276
|
+
];
|
|
8277
|
+
return prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, {
|
|
8278
|
+
...snapshot,
|
|
8279
|
+
stale: true,
|
|
8280
|
+
runtimeStatus: fallback?.phase || "unavailable",
|
|
8281
|
+
runtimeMessage: fallback?.pointer || fallback?.error || "",
|
|
8282
|
+
optionalGaps,
|
|
8283
|
+
collaboration: prdWorkflowCollaborationState(userCtx, flowSource, flowId, tapdId),
|
|
8284
|
+
sources: {
|
|
8285
|
+
...(snapshot.sources && typeof snapshot.sources === "object" ? snapshot.sources : {}),
|
|
8286
|
+
cacheUpdatedAt: cached.updatedAt || "",
|
|
8287
|
+
checkedAt: new Date().toISOString(),
|
|
8288
|
+
},
|
|
8289
|
+
});
|
|
8290
|
+
}
|
|
8291
|
+
|
|
8292
|
+
function prdWorkflowWithAgentflowTokenDiagnostic(snapshot, sessionToken = "") {
|
|
8293
|
+
const current = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) ? snapshot : {};
|
|
8294
|
+
const tokenPresent = Boolean(String(sessionToken || "").trim());
|
|
8295
|
+
const optionalGaps = Array.isArray(current.optionalGaps) ? [...current.optionalGaps] : [];
|
|
8296
|
+
const hasTokenHint = optionalGaps.some((gap) => /AgentFlow runtime token|AGENTFLOW_BASE_URL|AGENTFLOW_TOKEN|session token/i.test(String(gap?.text || gap || "")));
|
|
8297
|
+
if (!hasTokenHint) {
|
|
8298
|
+
optionalGaps.push({
|
|
8299
|
+
severity: "info",
|
|
8300
|
+
text: tokenPresent
|
|
8301
|
+
? "当前浏览器会话已有 AgentFlow token。客户端 prd-flow skill/CLI 同步 Workflow 时还需要 AGENTFLOW_BASE_URL,并使用 AGENTFLOW_TOKEN 或 PRD_FLOW_RUNTIME_EVENT_TOKEN 上报 snapshot/event;不要把 token 写入 ai-doc 或 prd-flow config。"
|
|
8302
|
+
: "当前请求没有 AgentFlow session token。客户端 prd-flow skill/CLI 要同步 Workflow 时,需要在客户端 .env 配置 AGENTFLOW_BASE_URL 和 AGENTFLOW_TOKEN,或配置 PRD_FLOW_RUNTIME_BASE_URL 和 PRD_FLOW_RUNTIME_EVENT_TOKEN。",
|
|
8303
|
+
});
|
|
8304
|
+
}
|
|
8305
|
+
return {
|
|
8306
|
+
...current,
|
|
8307
|
+
optionalGaps,
|
|
8308
|
+
sources: {
|
|
8309
|
+
...(current.sources && typeof current.sources === "object" ? current.sources : {}),
|
|
8310
|
+
agentflowRuntimeToken: tokenPresent ? "session" : "not-required-for-local-ui",
|
|
8311
|
+
},
|
|
8312
|
+
};
|
|
8313
|
+
}
|
|
8314
|
+
|
|
8315
|
+
function prdWorkflowAllowServerExec() {
|
|
8316
|
+
return parseBool(process.env.AGENTFLOW_PRD_WORKFLOW_SERVER_EXEC, false);
|
|
8317
|
+
}
|
|
8318
|
+
|
|
8319
|
+
function prdWorkflowParseJson(stdout) {
|
|
8320
|
+
const text = String(stdout || "").trim();
|
|
8321
|
+
if (!text) return null;
|
|
8322
|
+
try {
|
|
8323
|
+
return JSON.parse(text);
|
|
8324
|
+
} catch (_) {}
|
|
8325
|
+
const first = text.indexOf("{");
|
|
8326
|
+
const last = text.lastIndexOf("}");
|
|
8327
|
+
if (first >= 0 && last > first) {
|
|
8328
|
+
try {
|
|
8329
|
+
return JSON.parse(text.slice(first, last + 1));
|
|
8330
|
+
} catch (_) {}
|
|
8331
|
+
}
|
|
8332
|
+
return null;
|
|
8333
|
+
}
|
|
8334
|
+
|
|
8335
|
+
function prdWorkflowFirstArray(...values) {
|
|
8336
|
+
const value = values.find((item) => Array.isArray(item));
|
|
8337
|
+
return value ? [...value] : [];
|
|
8338
|
+
}
|
|
8339
|
+
|
|
8340
|
+
function prdWorkflowSplitCommand(command) {
|
|
8341
|
+
const text = String(command || "").trim();
|
|
8342
|
+
if (!text || /[\0\r\n]/.test(text) || text.length > 4000) return [];
|
|
8343
|
+
const tokens = [];
|
|
8344
|
+
let current = "";
|
|
8345
|
+
let quote = "";
|
|
8346
|
+
let escaping = false;
|
|
8347
|
+
for (const ch of text) {
|
|
8348
|
+
if (escaping) {
|
|
8349
|
+
current += ch;
|
|
8350
|
+
escaping = false;
|
|
8351
|
+
continue;
|
|
8352
|
+
}
|
|
8353
|
+
if (ch === "\\") {
|
|
8354
|
+
escaping = true;
|
|
8355
|
+
continue;
|
|
8356
|
+
}
|
|
8357
|
+
if (quote) {
|
|
8358
|
+
if (ch === quote) quote = "";
|
|
8359
|
+
else current += ch;
|
|
8360
|
+
continue;
|
|
8361
|
+
}
|
|
8362
|
+
if (ch === "'" || ch === '"') {
|
|
8363
|
+
quote = ch;
|
|
8364
|
+
continue;
|
|
8365
|
+
}
|
|
8366
|
+
if (/\s/.test(ch)) {
|
|
8367
|
+
if (current) {
|
|
8368
|
+
tokens.push(current);
|
|
8369
|
+
current = "";
|
|
8370
|
+
}
|
|
8371
|
+
continue;
|
|
8372
|
+
}
|
|
8373
|
+
current += ch;
|
|
8374
|
+
}
|
|
8375
|
+
if (escaping) current += "\\";
|
|
8376
|
+
if (quote) return [];
|
|
8377
|
+
if (current) tokens.push(current);
|
|
8378
|
+
return tokens.filter(Boolean);
|
|
8379
|
+
}
|
|
8380
|
+
|
|
8381
|
+
function prdWorkflowCommandArgs(command) {
|
|
8382
|
+
const tokens = prdWorkflowSplitCommand(command);
|
|
8383
|
+
if (!tokens.length) return [];
|
|
8384
|
+
const first = tokens[0] || "";
|
|
8385
|
+
const start = path.basename(first) === "prd-flow" ? 1 : 0;
|
|
8386
|
+
const args = tokens.slice(start);
|
|
8387
|
+
const action = String(args[0] || "");
|
|
8388
|
+
if (!action || action.startsWith("-") || /[\/\\]/.test(action)) return [];
|
|
8389
|
+
return args;
|
|
8390
|
+
}
|
|
8391
|
+
|
|
8392
|
+
function prdWorkflowCommandTapdId(args = []) {
|
|
8393
|
+
for (const arg of args.slice(1)) {
|
|
8394
|
+
const value = String(arg || "").trim();
|
|
8395
|
+
if (!value || value.startsWith("-")) continue;
|
|
8396
|
+
return value;
|
|
8397
|
+
}
|
|
8398
|
+
return "";
|
|
8399
|
+
}
|
|
8400
|
+
|
|
8401
|
+
function prdWorkflowNormalizeNextAction(parsed = {}, fallbackTapdId = "") {
|
|
8402
|
+
const raw = parsed.nextAction || parsed.next_action || parsed.currentAction || parsed.current_action || parsed.next || null;
|
|
8403
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
8404
|
+
const command = String(raw.command || raw.nextCommand || raw.next_command || "").trim();
|
|
8405
|
+
const args = prdWorkflowCommandArgs(command);
|
|
8406
|
+
const action = String(raw.action || raw.actionId || raw.action_id || args[0] || raw.code || raw.id || "").trim();
|
|
8407
|
+
const issue = raw.issue && typeof raw.issue === "object" && !Array.isArray(raw.issue)
|
|
8408
|
+
? String(raw.issue.key || raw.issue.id || raw.issue.title || "").trim()
|
|
8409
|
+
: String(raw.issue || raw.issueKey || raw.issue_key || "").trim();
|
|
8410
|
+
const title = String(raw.title || raw.label || raw.name || raw.code || action || "下一步").trim();
|
|
8411
|
+
return {
|
|
8412
|
+
...raw,
|
|
8413
|
+
id: String(raw.id || raw.code || action || "next"),
|
|
8414
|
+
action,
|
|
8415
|
+
actionId: String(raw.actionId || raw.action_id || action),
|
|
8416
|
+
command,
|
|
8417
|
+
label: String(raw.label || title),
|
|
8418
|
+
title,
|
|
8419
|
+
detail: String(raw.detail || raw.description || raw.user_hint || raw.userHint || ""),
|
|
8420
|
+
issueKey: issue,
|
|
8421
|
+
tapdId: String(raw.tapdId || raw.tapd_id || prdWorkflowCommandTapdId(args) || fallbackTapdId || ""),
|
|
8422
|
+
dryRunSupported: raw.dryRunSupported !== false && raw.dry_run_supported !== false,
|
|
8423
|
+
dryRunMode: command ? "preview" : raw.dryRunMode || raw.dry_run_mode || "",
|
|
8424
|
+
};
|
|
8425
|
+
}
|
|
8426
|
+
|
|
8427
|
+
async function runPrdWorkflowCommand(root, scopedRoot, args = [], userCtx = {}, options = {}) {
|
|
8428
|
+
const cli = prdWorkflowResolveCli(root, scopedRoot);
|
|
8429
|
+
const cwd = scopedRoot || root || process.cwd();
|
|
8430
|
+
const env = runtimeEnvForUser(userCtx, {
|
|
8431
|
+
PRD_FLOW_WORKSPACE: path.join(cwd, ".workspace", "prd-flow"),
|
|
8432
|
+
...(options.env && typeof options.env === "object" && !Array.isArray(options.env) ? options.env : {}),
|
|
8433
|
+
});
|
|
8434
|
+
const result = await execFileBuffered(cli.command, args, {
|
|
8435
|
+
cwd,
|
|
8436
|
+
env,
|
|
8437
|
+
timeout: Number(options.timeout || 120000),
|
|
8438
|
+
maxBuffer: Number(options.maxBuffer || 4 * 1024 * 1024),
|
|
8439
|
+
});
|
|
8440
|
+
return { ...result, cli };
|
|
8441
|
+
}
|
|
8442
|
+
|
|
8443
|
+
function prdWorkflowSnapshotFromParsed(scopedRoot, tapdId, parsed = {}, userCtx = {}, opts = {}) {
|
|
8444
|
+
const id = String(tapdId || parsed?.tapdId || parsed?.tapd_id || parsed?.prd?.tapd_id || parsed?.prd?.tapdId || "").trim();
|
|
8445
|
+
const flowSource = String(opts.flowSource || "user").trim() || "user";
|
|
8446
|
+
const flowId = String(opts.flowId || "").trim();
|
|
8447
|
+
const nextAction = prdWorkflowNormalizeNextAction(parsed, id);
|
|
8448
|
+
const prd = parsed.prd && typeof parsed.prd === "object" && !Array.isArray(parsed.prd) ? parsed.prd : {};
|
|
8449
|
+
const actions = prdWorkflowFirstArray(parsed.actions, parsed.workflowActions, parsed.workflow_actions);
|
|
8450
|
+
const epics = prdWorkflowFirstArray(parsed.epics, parsed.epicGroups, parsed.epic_groups, prd.epics, prd.epicGroups, prd.epic_groups);
|
|
8451
|
+
const issues = prdWorkflowFirstArray(parsed.issues, parsed.issueGroups, parsed.issue_groups, prd.issues, prd.issueGroups, prd.issue_groups);
|
|
8452
|
+
const optionalGaps = prdWorkflowFirstArray(parsed.optionalGaps, parsed.optional_gaps);
|
|
8453
|
+
if (!actions.length && !nextAction) {
|
|
8454
|
+
optionalGaps.push({ severity: "warn", text: "prd-flow current --json 未返回 actions/nextAction,Workflow 时间线只能显示 runtime 或空状态。" });
|
|
8455
|
+
}
|
|
8456
|
+
if (!epics.length && !issues.length) {
|
|
8457
|
+
optionalGaps.push({ severity: "info", text: "prd-flow current --json 未返回 epics/issues,Issue 整合区暂为空。" });
|
|
8458
|
+
}
|
|
8459
|
+
const snapshot = {
|
|
8460
|
+
tapdId: String(parsed.tapdId || parsed.tapd_id || prd.tapd_id || prd.tapdId || id),
|
|
8461
|
+
phase: String(parsed.phase || parsed.workflow_stage || nextAction?.id || parsed.next?.code || "unknown"),
|
|
8462
|
+
pointer: String(parsed.pointer || parsed.current || parsed.status || nextAction?.title || ""),
|
|
8463
|
+
revision: String(parsed.revision || prd.revision || parsed.next?.revision || ""),
|
|
8464
|
+
nextAction,
|
|
8465
|
+
actions,
|
|
8466
|
+
workflowActions: actions,
|
|
8467
|
+
timeline: prdWorkflowFirstArray(parsed.timeline),
|
|
8468
|
+
events: prdWorkflowFirstArray(parsed.events),
|
|
8469
|
+
history: prdWorkflowFirstArray(parsed.history),
|
|
8470
|
+
milestones: prdWorkflowFirstArray(parsed.milestones),
|
|
8471
|
+
epics,
|
|
8472
|
+
epicGroups: epics,
|
|
8473
|
+
issues,
|
|
8474
|
+
issueGroups: issues,
|
|
8475
|
+
artifacts: prdWorkflowFirstArray(parsed.artifacts, parsed.outputs),
|
|
8476
|
+
dependencies: prdWorkflowFirstArray(parsed.dependencies),
|
|
8477
|
+
optionalGaps,
|
|
8478
|
+
sources: parsed.sources && typeof parsed.sources === "object" ? parsed.sources : {},
|
|
8479
|
+
raw: parsed,
|
|
8480
|
+
collaboration: prdWorkflowCollaborationState(userCtx, flowSource, flowId, id),
|
|
8481
|
+
};
|
|
8482
|
+
snapshot.revision = prdWorkflowSnapshotRevision(snapshot);
|
|
8483
|
+
return snapshot;
|
|
8484
|
+
}
|
|
8485
|
+
|
|
8486
|
+
async function prdWorkflowSnapshot(root, scopedRoot, tapdId, userCtx = {}, opts = {}) {
|
|
8487
|
+
const id = String(tapdId || "").trim();
|
|
8488
|
+
const flowSource = String(opts.flowSource || "user").trim() || "user";
|
|
8489
|
+
const flowId = String(opts.flowId || "").trim();
|
|
8490
|
+
if (!id) {
|
|
8491
|
+
return {
|
|
8492
|
+
...prdWorkflowFallbackSnapshot("", "unselected", "输入 TAPD ID 后读取 Workflow 状态"),
|
|
8493
|
+
collaboration: prdWorkflowCollaborationState(userCtx, flowSource, flowId, ""),
|
|
8494
|
+
};
|
|
8495
|
+
}
|
|
8496
|
+
if (!prdWorkflowAllowServerExec()) {
|
|
8497
|
+
return prdWorkflowMaterializeSnapshot(root, scopedRoot, id, userCtx, { flowSource, flowId });
|
|
8498
|
+
}
|
|
8499
|
+
try {
|
|
8500
|
+
const result = await runPrdWorkflowCommand(root, scopedRoot, ["current", id, "--json"], userCtx, { timeout: 120000 });
|
|
8501
|
+
const parsed = prdWorkflowParseJson(result.stdout);
|
|
8502
|
+
if (!parsed) {
|
|
8503
|
+
return prdWorkflowFallbackWithCache(scopedRoot, id, prdWorkflowFallbackSnapshot(id, "json_unsupported", "prd-flow current --json 暂不可用或未返回 JSON", {
|
|
8504
|
+
rawOutput: String(result.stdout || result.stderr || "").slice(0, 8000),
|
|
8505
|
+
cli: result.cli,
|
|
8506
|
+
}), userCtx, flowSource, flowId);
|
|
8507
|
+
}
|
|
8508
|
+
const snapshot = {
|
|
8509
|
+
...prdWorkflowSnapshotFromParsed(scopedRoot, id, parsed, userCtx, opts),
|
|
8510
|
+
cli: result.cli,
|
|
8511
|
+
};
|
|
8512
|
+
const mergedSnapshot = prdWorkflowMergeRuntimeEvents(scopedRoot, id, snapshot);
|
|
8513
|
+
prdWorkflowWriteCachedSnapshot(scopedRoot, id, mergedSnapshot);
|
|
8514
|
+
return mergedSnapshot;
|
|
8515
|
+
} catch (e) {
|
|
8516
|
+
const code = e?.code || "";
|
|
8517
|
+
const stdout = String(e?.stdout || "");
|
|
8518
|
+
const stderr = String(e?.stderr || "");
|
|
8519
|
+
const missing = code === "ENOENT";
|
|
8520
|
+
return prdWorkflowFallbackWithCache(scopedRoot, id, prdWorkflowFallbackSnapshot(
|
|
8521
|
+
id,
|
|
8522
|
+
missing ? "unavailable" : "command_failed",
|
|
8523
|
+
missing ? "未找到 prd-flow CLI,请先在 workspace 配置 .workspace/prd-flow/bin/prd-flow 或设置 PRD_FLOW_CLI" : `prd-flow current --json 执行失败:${String(e?.message || e)}`,
|
|
8524
|
+
{
|
|
8525
|
+
rawOutput: `${stdout}${stdout && stderr ? "\n" : ""}${stderr}`.slice(0, 8000),
|
|
8526
|
+
error: String(e?.message || e),
|
|
8527
|
+
},
|
|
8528
|
+
), userCtx, flowSource, flowId);
|
|
8529
|
+
}
|
|
8530
|
+
}
|
|
8531
|
+
|
|
8532
|
+
function normalizePrdWorkflowActionArgs(payload = {}) {
|
|
8533
|
+
const command = String(payload.command || payload.nextCommand || payload.next_command || "").trim();
|
|
8534
|
+
const commandArgs = prdWorkflowCommandArgs(command);
|
|
8535
|
+
const action = String(payload.action || payload.actionId || commandArgs[0] || "").trim();
|
|
8536
|
+
if (!action || action.startsWith("-") || /[\0\r\n]/.test(action) || action.length > 160) {
|
|
8537
|
+
return { error: "Invalid prd-flow action" };
|
|
8538
|
+
}
|
|
8539
|
+
const tapdId = String(payload.tapdId || payload.tapd_id || prdWorkflowCommandTapdId(commandArgs)).trim();
|
|
8540
|
+
if (!tapdId) return { error: "Missing tapdId" };
|
|
8541
|
+
if (commandArgs.length) {
|
|
8542
|
+
const expectedRevision = String(payload.expectedRevision || "").trim();
|
|
8543
|
+
const idempotencyKey = String(payload.idempotencyKey || "").trim();
|
|
8544
|
+
const args = [...commandArgs];
|
|
8545
|
+
if (expectedRevision && !args.includes("--expected-revision")) args.push("--expected-revision", expectedRevision);
|
|
8546
|
+
if (idempotencyKey && !args.includes("--idempotency-key")) args.push("--idempotency-key", idempotencyKey);
|
|
8547
|
+
return {
|
|
8548
|
+
action,
|
|
8549
|
+
tapdId,
|
|
8550
|
+
args,
|
|
8551
|
+
idempotencyKey,
|
|
8552
|
+
command,
|
|
8553
|
+
previewOnly: payload.dryRun === true || payload.dry_run === true,
|
|
8554
|
+
fromCommand: true,
|
|
8555
|
+
};
|
|
8556
|
+
}
|
|
8557
|
+
const args = [action, tapdId];
|
|
8558
|
+
const issue = String(payload.issueKey || payload.issue || "").trim();
|
|
8559
|
+
if (issue) args.push("--issue", issue);
|
|
8560
|
+
const summary = String(payload.summary || "").trim();
|
|
8561
|
+
if (summary && action === "start-fix") args.push("--summary", summary);
|
|
8562
|
+
const testEnv = String(payload.testEnv || payload.test_environment || "").trim();
|
|
8563
|
+
if (testEnv && action === "submit-test") args.push("--test-env", testEnv);
|
|
8564
|
+
const mr = String(payload.mr || payload.url || "").trim();
|
|
8565
|
+
if (mr && action === "submit-test") args.push("--mr", mr);
|
|
8566
|
+
if (payload.confirm === true) args.push("--confirm");
|
|
8567
|
+
if (payload.dryRun === true || payload.dry_run === true) args.push("--dry-run");
|
|
8568
|
+
if (payload.allowMissingImplementation === true) args.push("--allow-missing-implementation");
|
|
8569
|
+
const expectedRevision = String(payload.expectedRevision || "").trim();
|
|
8570
|
+
if (expectedRevision) args.push("--expected-revision", expectedRevision);
|
|
8571
|
+
const idempotencyKey = String(payload.idempotencyKey || "").trim();
|
|
8572
|
+
if (idempotencyKey) args.push("--idempotency-key", idempotencyKey);
|
|
8573
|
+
args.push("--json");
|
|
8574
|
+
return { action, tapdId, args, idempotencyKey };
|
|
8575
|
+
}
|
|
8576
|
+
|
|
8577
|
+
function prdWorkflowActionText(payload = {}, normalized = {}) {
|
|
8578
|
+
return [
|
|
8579
|
+
normalized?.action,
|
|
8580
|
+
payload.action,
|
|
8581
|
+
payload.actionId,
|
|
8582
|
+
payload.action_id,
|
|
8583
|
+
payload.command,
|
|
8584
|
+
payload.marker,
|
|
8585
|
+
payload.flag,
|
|
8586
|
+
payload.stage,
|
|
8587
|
+
payload.stageKey,
|
|
8588
|
+
payload.stage_key,
|
|
8589
|
+
payload.title,
|
|
8590
|
+
payload.label,
|
|
8591
|
+
].map((value) => String(value || "").trim()).filter(Boolean).join(" ");
|
|
8592
|
+
}
|
|
8593
|
+
|
|
8594
|
+
function prdWorkflowFirstString(...values) {
|
|
8595
|
+
for (const value of values) {
|
|
8596
|
+
const text = String(value || "").trim();
|
|
8597
|
+
if (text) return text;
|
|
8598
|
+
}
|
|
8599
|
+
return "";
|
|
8600
|
+
}
|
|
8601
|
+
|
|
8602
|
+
function prdWorkflowMarkerEventSpec(payload = {}, normalized = {}) {
|
|
8603
|
+
const text = prdWorkflowActionText(payload, normalized).toLowerCase().replace(/[_\s]+/g, "-");
|
|
8604
|
+
const explicitRuntimeOnly = payload.runtimeOnly === true || payload.runtime_only === true ||
|
|
8605
|
+
payload.markerOnly === true || payload.marker_only === true;
|
|
8606
|
+
const specs = [
|
|
8607
|
+
{ re: /(^|-)mark-impl-mr($|-)|(^|-)record-impl-mr($|-)|--impl-mr\b/, stage: "implementation", title: "记录实现 MR", kind: "impl-mr" },
|
|
8608
|
+
{ re: /(^|-)mark-fix-mr($|-)|(^|-)record-fix-mr($|-)|--fix-mr\b/, stage: "bugfix", title: "记录修复 MR", kind: "fix-mr" },
|
|
8609
|
+
{ re: /(^|-)mark-impl-done($|-)|--impl-done\b/, stage: "implementation", title: "实现完成标记", kind: "impl-done" },
|
|
8610
|
+
{ re: /(^|-)mark-impl-merged($|-)|--impl-merged\b/, stage: "implementation", title: "实现 MR 合并标记", kind: "impl-merged" },
|
|
8611
|
+
{ re: /(^|-)mark-integration-mr($|-)|(^|-)record-integration-mr($|-)|--integration-mr\b/, stage: "submit-test", title: "记录集成 MR", kind: "integration-mr" },
|
|
8612
|
+
{ re: /(^|-)mark-integrated($|-)|--integrated\b/, stage: "submit-test", title: "集成完成标记", kind: "integrated" },
|
|
8613
|
+
{ re: /(^|-)retry-jenkins($|-)|(^|-)retry-jenkins-build($|-)|(^|-)record-jenkins($|-)/, stage: "self-test", title: "Jenkins 构建记录", kind: "jenkins" },
|
|
8614
|
+
{ re: /(^|-)record-test-mr($|-)|(^|-)test-mr($|-)/, stage: "submit-test", title: "记录提测 MR", kind: "test-mr" },
|
|
8615
|
+
];
|
|
8616
|
+
let matched = specs.find((spec) => spec.re.test(text));
|
|
8617
|
+
if (!matched && explicitRuntimeOnly) {
|
|
8618
|
+
matched = {
|
|
8619
|
+
stage: prdWorkflowFirstString(payload.stageKey, payload.stage_key, payload.stage, normalized.action, payload.action, "workflow"),
|
|
8620
|
+
title: prdWorkflowFirstString(payload.title, payload.label, normalized.action, payload.action, "Workflow runtime action"),
|
|
8621
|
+
kind: "runtime",
|
|
8622
|
+
};
|
|
8623
|
+
}
|
|
8624
|
+
if (!matched) return null;
|
|
8625
|
+
const issueKey = prdWorkflowFirstString(payload.issueKey, payload.issue_key, payload.issue);
|
|
8626
|
+
const url = prdWorkflowFirstString(payload.url, payload.href, payload.mr, payload.mrUrl, payload.mr_url, payload.mergeRequestUrl, payload.merge_request_url);
|
|
8627
|
+
const links = Array.isArray(payload.links) ? payload.links : [];
|
|
8628
|
+
const artifacts = Array.isArray(payload.artifacts) ? payload.artifacts : [];
|
|
8629
|
+
const title = prdWorkflowFirstString(payload.title, payload.label, matched.title);
|
|
8630
|
+
const stage = prdWorkflowFirstString(payload.stageKey, payload.stage_key, payload.stage, matched.stage);
|
|
8631
|
+
const linkArtifacts = url ? [{ kind: matched.kind, label: title, url }] : [];
|
|
8632
|
+
return {
|
|
8633
|
+
runtimeOnly: true,
|
|
8634
|
+
kind: matched.kind,
|
|
8635
|
+
stage,
|
|
8636
|
+
title,
|
|
8637
|
+
issueKey,
|
|
8638
|
+
detail: prdWorkflowFirstString(payload.detail, payload.description, payload.summary, url ? "记录外部系统事实,不写 ai-doc marker" : "记录运行态事实,不写 ai-doc marker"),
|
|
8639
|
+
links,
|
|
8640
|
+
artifacts: [...artifacts, ...linkArtifacts],
|
|
8641
|
+
};
|
|
8642
|
+
}
|
|
8643
|
+
|
|
8644
|
+
function prunePrdWorkflowIdempotency() {
|
|
8645
|
+
if (prdWorkflowIdempotency.size <= PRD_WORKFLOW_IDEMPOTENCY_MAX) return;
|
|
8646
|
+
const entries = Array.from(prdWorkflowIdempotency.entries())
|
|
8647
|
+
.sort((a, b) => Number(a[1]?.at || 0) - Number(b[1]?.at || 0));
|
|
8648
|
+
for (const [key] of entries.slice(0, Math.max(1, entries.length - PRD_WORKFLOW_IDEMPOTENCY_MAX))) {
|
|
8649
|
+
prdWorkflowIdempotency.delete(key);
|
|
8650
|
+
}
|
|
8651
|
+
}
|
|
8652
|
+
|
|
8653
|
+
function workspaceIntervalMinutesToCron(intervalMinutes) {
|
|
8654
|
+
const n = Number(intervalMinutes);
|
|
8655
|
+
if (!Number.isFinite(n) || n <= 0) return "0 9 * * *";
|
|
8656
|
+
const minutes = Math.max(1, Math.min(1440, Math.round(n)));
|
|
8657
|
+
if (minutes < 60) return `*/${minutes} * * * *`;
|
|
8658
|
+
if (minutes === 60) return "0 * * * *";
|
|
8659
|
+
if (minutes < 1440 && minutes % 60 === 0) return `0 */${minutes / 60} * * *`;
|
|
8660
|
+
return "0 9 * * *";
|
|
8661
|
+
}
|
|
8662
|
+
|
|
8663
|
+
function workspaceRunKey(userCtx, flowSource, flowId) {
|
|
8664
|
+
return `${userCtx?.userId || ""}:${flowSource || "user"}:${flowId}`;
|
|
8665
|
+
}
|
|
8666
|
+
|
|
8667
|
+
function workspaceRunEntryKey(scopeKey, runId) {
|
|
8668
|
+
return `${scopeKey}:${String(runId || "").trim() || runLedgerId("workspace")}`;
|
|
8669
|
+
}
|
|
8670
|
+
|
|
8671
|
+
function workspaceRuntimeNodeLabel(graph, nodeId, fallback = "Workspace Run") {
|
|
8672
|
+
const id = String(nodeId || "").trim();
|
|
8673
|
+
const instance = graph?.instances && typeof graph.instances === "object" ? graph.instances[id] : null;
|
|
8674
|
+
const label = String(instance?.label || "").trim();
|
|
8675
|
+
return label || id || fallback;
|
|
8676
|
+
}
|
|
8677
|
+
|
|
8678
|
+
function workspaceActiveRunsForScope(scopeKey) {
|
|
8679
|
+
const key = String(scopeKey || "");
|
|
8680
|
+
return Array.from(activeWorkspaceRuns.entries())
|
|
8681
|
+
.filter(([, entry]) => String(entry?.scopeKey || "") === key);
|
|
8682
|
+
}
|
|
8683
|
+
|
|
8684
|
+
function workspaceRunPlanNodeIds(runNodeId, plan) {
|
|
8685
|
+
return Array.from(new Set([
|
|
8686
|
+
String(runNodeId || "").trim(),
|
|
8687
|
+
...(Array.isArray(plan?.order) ? plan.order : []),
|
|
8688
|
+
...(Array.isArray(plan?.pauseNodeIds) ? plan.pauseNodeIds : []),
|
|
8689
|
+
].map((id) => String(id || "").trim()).filter(Boolean)));
|
|
8690
|
+
}
|
|
8691
|
+
|
|
8692
|
+
function workspaceFindActiveRunConflict(scopeKey, plannedNodeIds) {
|
|
8693
|
+
const planned = new Set((plannedNodeIds || []).map((id) => String(id || "").trim()).filter(Boolean));
|
|
8694
|
+
for (const [key, entry] of workspaceActiveRunsForScope(scopeKey)) {
|
|
8695
|
+
const activeIds = Array.isArray(entry?.plannedNodeIds) ? entry.plannedNodeIds : [];
|
|
8696
|
+
if (!activeIds.length) {
|
|
8697
|
+
return { key, entry, conflictNodeIds: [] };
|
|
8698
|
+
}
|
|
8699
|
+
const conflictNodeIds = activeIds
|
|
8700
|
+
.map((id) => String(id || "").trim())
|
|
8701
|
+
.filter((id) => id && planned.has(id));
|
|
8702
|
+
if (conflictNodeIds.length) return { key, entry, conflictNodeIds };
|
|
8703
|
+
}
|
|
8704
|
+
return null;
|
|
8705
|
+
}
|
|
8706
|
+
|
|
8707
|
+
function normalizeWorkspaceScheduledRunConfig(raw) {
|
|
8708
|
+
let parsed = {};
|
|
8709
|
+
const text = String(raw || "").trim();
|
|
8710
|
+
if (text) {
|
|
8711
|
+
try {
|
|
8712
|
+
parsed = JSON.parse(text);
|
|
8713
|
+
} catch {
|
|
8714
|
+
parsed = {};
|
|
8715
|
+
}
|
|
8716
|
+
}
|
|
8717
|
+
const intervalMinutes = Number(parsed.intervalMinutes);
|
|
8718
|
+
const migratedCron = workspaceIntervalMinutesToCron(intervalMinutes);
|
|
8719
|
+
const cron = typeof parsed.cron === "string" && parsed.cron.trim()
|
|
8720
|
+
? parsed.cron.trim()
|
|
8721
|
+
: migratedCron;
|
|
8722
|
+
const timezone = typeof parsed.timezone === "string" && parsed.timezone.trim()
|
|
8723
|
+
? parsed.timezone.trim()
|
|
8724
|
+
: "Asia/Shanghai";
|
|
8725
|
+
const targetRunNodeId = typeof parsed.targetRunNodeId === "string" ? parsed.targetRunNodeId.trim() : "";
|
|
8726
|
+
const overlapPolicy = parsed.overlapPolicy === "skip" ? "skip" : "skip";
|
|
8727
|
+
return {
|
|
8728
|
+
enabled: parsed.enabled === true,
|
|
8729
|
+
cron,
|
|
8730
|
+
timezone,
|
|
8731
|
+
targetRunNodeId,
|
|
8732
|
+
overlapPolicy,
|
|
8733
|
+
};
|
|
8734
|
+
}
|
|
8735
|
+
|
|
8736
|
+
function workspaceScheduleNextRunAt(config, fromDate = new Date()) {
|
|
8737
|
+
if (!config?.enabled || !config?.cron) return null;
|
|
8738
|
+
return Date.parse(computeNextRunAt(config.cron, config.timezone || "Asia/Shanghai", fromDate));
|
|
8739
|
+
}
|
|
8740
|
+
|
|
8741
|
+
function workspaceSchedulesPath() {
|
|
8742
|
+
return path.join(getAgentflowDataRoot(), WORKSPACE_SCHEDULES_FILENAME);
|
|
8743
|
+
}
|
|
8744
|
+
|
|
8745
|
+
function readWorkspaceScheduleRegistry() {
|
|
8746
|
+
const filePath = workspaceSchedulesPath();
|
|
8747
|
+
if (!fs.existsSync(filePath)) return { version: 1, schedules: {} };
|
|
8748
|
+
try {
|
|
8749
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
8750
|
+
return {
|
|
8751
|
+
version: 1,
|
|
8752
|
+
schedules: parsed?.schedules && typeof parsed.schedules === "object" && !Array.isArray(parsed.schedules)
|
|
8753
|
+
? parsed.schedules
|
|
8754
|
+
: {},
|
|
8755
|
+
};
|
|
8756
|
+
} catch {
|
|
8757
|
+
return { version: 1, schedules: {} };
|
|
8758
|
+
}
|
|
8759
|
+
}
|
|
8760
|
+
|
|
8761
|
+
function writeWorkspaceScheduleRegistry(registry) {
|
|
8762
|
+
const filePath = workspaceSchedulesPath();
|
|
8763
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8764
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
8765
|
+
version: 1,
|
|
8766
|
+
updatedAt: new Date().toISOString(),
|
|
8767
|
+
schedules: registry?.schedules && typeof registry.schedules === "object" ? registry.schedules : {},
|
|
8768
|
+
}, null, 2) + "\n", "utf-8");
|
|
8769
|
+
}
|
|
8770
|
+
|
|
8771
|
+
function workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId) {
|
|
8772
|
+
return [
|
|
8773
|
+
String(userId || ""),
|
|
8774
|
+
String(flowSource || "user"),
|
|
8775
|
+
String(flowId || ""),
|
|
8776
|
+
String(scheduleNodeId || ""),
|
|
8777
|
+
].join(":");
|
|
8778
|
+
}
|
|
8779
|
+
|
|
8780
|
+
function listWorkspaceScheduleStatusesForFlow(userCtx = {}, flowSource = "user", flowId = "") {
|
|
8781
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
8782
|
+
const userId = String(userCtx.userId || "");
|
|
8783
|
+
return Object.values(registry.schedules || {})
|
|
8784
|
+
.filter((entry) => (
|
|
8785
|
+
String(entry?.userId || "") === userId &&
|
|
8786
|
+
String(entry?.flowSource || "user") === String(flowSource || "user") &&
|
|
8787
|
+
String(entry?.flowId || "") === String(flowId || "")
|
|
8788
|
+
))
|
|
8789
|
+
.sort((a, b) => String(a.scheduleNodeId || a.runNodeId || "").localeCompare(String(b.scheduleNodeId || b.runNodeId || "")));
|
|
8790
|
+
}
|
|
8791
|
+
|
|
8792
|
+
function listWorkspaceScheduleStatuses(root, userCtx = {}) {
|
|
8793
|
+
const registry = readWorkspaceScheduleRegistry();
|
|
8794
|
+
const userId = String(userCtx.userId || "");
|
|
8795
|
+
const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
|
|
8796
|
+
.filter((flow) => !flow.archived && !isReadonlyBuiltinFlowSource(flow.source || "user"));
|
|
8797
|
+
const rows = [];
|
|
8798
|
+
for (const flow of flows) {
|
|
8799
|
+
const flowId = String(flow.id || "");
|
|
8800
|
+
const flowSource = String(flow.source || "user");
|
|
8801
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource }, userCtx);
|
|
8802
|
+
if (scoped.error || !scoped.root) continue;
|
|
8803
|
+
let graph;
|
|
8804
|
+
try {
|
|
8805
|
+
graph = readWorkspaceGraph(scoped.root).graph;
|
|
8806
|
+
} catch {
|
|
8807
|
+
continue;
|
|
8808
|
+
}
|
|
8809
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
8810
|
+
for (const [scheduleNodeId, instance] of Object.entries(instances)) {
|
|
8811
|
+
if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
|
|
8812
|
+
const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
|
|
8813
|
+
const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
|
|
8814
|
+
const current = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
|
|
8815
|
+
const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
|
|
8816
|
+
const scopeKey = workspaceRunKey(userCtx, flowSource, flowId);
|
|
8817
|
+
const running = workspaceActiveRunsForScope(scopeKey).some(([, active]) => (
|
|
6899
8818
|
active?.scheduled === true &&
|
|
6900
8819
|
String(active?.runNodeId || "") === String(targetRunNodeId || scheduleNodeId)
|
|
6901
8820
|
));
|
|
@@ -7430,33 +9349,718 @@ export function startUiServer({
|
|
|
7430
9349
|
const uiPort = port;
|
|
7431
9350
|
const uiConfig = { hideCommunityLinks: Boolean(hideCommunityLinks) };
|
|
7432
9351
|
|
|
7433
|
-
const server = http.createServer(async (req, res) => {
|
|
7434
|
-
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
7435
|
-
const reqStart = Date.now();
|
|
7436
|
-
log.debug(`[ui] ${req.method} ${url.pathname}${url.search || ""}`);
|
|
9352
|
+
const server = http.createServer(async (req, res) => {
|
|
9353
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
9354
|
+
const reqStart = Date.now();
|
|
9355
|
+
log.debug(`[ui] ${req.method} ${url.pathname}${url.search || ""}`);
|
|
9356
|
+
|
|
9357
|
+
const origEnd = res.end.bind(res);
|
|
9358
|
+
res.end = function (...args) {
|
|
9359
|
+
log.debug(`[ui] ${req.method} ${url.pathname} → ${res.statusCode} (${Date.now() - reqStart}ms)`);
|
|
9360
|
+
return origEnd(...args);
|
|
9361
|
+
};
|
|
9362
|
+
|
|
9363
|
+
if (url.pathname === "/api/auth/me" && req.method === "GET") {
|
|
9364
|
+
const user = getAuthUserFromRequest(req);
|
|
9365
|
+
const allowed = user ? isAuthUserAllowed(user) : true;
|
|
9366
|
+
const allowlist = readUserAllowlist();
|
|
9367
|
+
json(res, 200, {
|
|
9368
|
+
authenticated: Boolean(user && allowed),
|
|
9369
|
+
user: user && allowed ? user : null,
|
|
9370
|
+
setupRequired: authSetupRequired(),
|
|
9371
|
+
allowlistEnabled: allowlist.enabled,
|
|
9372
|
+
forbidden: Boolean(user && !allowed),
|
|
9373
|
+
error: user && !allowed ? "用户不在白名单中,请联系管理员开通访问权限" : "",
|
|
9374
|
+
});
|
|
9375
|
+
return;
|
|
9376
|
+
}
|
|
9377
|
+
|
|
9378
|
+
if (url.pathname === "/api/auth/login" && req.method === "POST") {
|
|
9379
|
+
let payload;
|
|
9380
|
+
try {
|
|
9381
|
+
payload = JSON.parse(await readBody(req));
|
|
9382
|
+
} catch {
|
|
9383
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
9384
|
+
return;
|
|
9385
|
+
}
|
|
9386
|
+
const result = loginOrCreateUser(payload?.username, payload?.password);
|
|
9387
|
+
if (!result.ok) {
|
|
9388
|
+
json(res, result.forbidden ? 403 : 401, { error: result.error || "Login failed", setupRequired: authSetupRequired() });
|
|
9389
|
+
return;
|
|
9390
|
+
}
|
|
9391
|
+
const body = JSON.stringify({ authenticated: true, user: result.user, setupRequired: false, migration: result.migration || null });
|
|
9392
|
+
res.writeHead(200, {
|
|
9393
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
9394
|
+
"Content-Length": Buffer.byteLength(body),
|
|
9395
|
+
"Set-Cookie": buildSessionCookie(result.token),
|
|
9396
|
+
});
|
|
9397
|
+
res.end(body);
|
|
9398
|
+
return;
|
|
9399
|
+
}
|
|
9400
|
+
|
|
9401
|
+
if (url.pathname === "/api/auth/logout" && req.method === "POST") {
|
|
9402
|
+
logoutRequest(req);
|
|
9403
|
+
const body = JSON.stringify({ ok: true });
|
|
9404
|
+
res.writeHead(200, {
|
|
9405
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
9406
|
+
"Content-Length": Buffer.byteLength(body),
|
|
9407
|
+
"Set-Cookie": buildClearSessionCookie(),
|
|
9408
|
+
});
|
|
9409
|
+
res.end(body);
|
|
9410
|
+
return;
|
|
9411
|
+
}
|
|
9412
|
+
|
|
9413
|
+
const authUser = getAuthUserFromRequest(req);
|
|
9414
|
+
const userCtx = authUser ? { userId: authUser.userId, isAdmin: Boolean(authUser.isAdmin) } : {};
|
|
9415
|
+
if (req.method === "GET" && url.pathname === "/api/auth/session-token") {
|
|
9416
|
+
if (!authUser?.userId) {
|
|
9417
|
+
json(res, 401, { error: "Unauthorized" });
|
|
9418
|
+
return;
|
|
9419
|
+
}
|
|
9420
|
+
json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
|
|
9421
|
+
return;
|
|
9422
|
+
}
|
|
9423
|
+
if (req.method === "GET" && url.pathname === "/api/prd-workflow/snapshot") {
|
|
9424
|
+
try {
|
|
9425
|
+
const tapdId = String(url.searchParams.get("tapdId") || "").trim();
|
|
9426
|
+
const flowId = String(url.searchParams.get("flowId") || "").trim();
|
|
9427
|
+
const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
|
|
9428
|
+
const archived = url.searchParams.get("archived") === "1";
|
|
9429
|
+
let scopedRoot = root;
|
|
9430
|
+
if (flowId) {
|
|
9431
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource, archived }, userCtx);
|
|
9432
|
+
if (scoped.error) {
|
|
9433
|
+
json(res, 400, { error: scoped.error });
|
|
9434
|
+
return;
|
|
9435
|
+
}
|
|
9436
|
+
scopedRoot = scoped.root;
|
|
9437
|
+
}
|
|
9438
|
+
const useMock = url.searchParams.get("mock") === "1" || parseBool(process.env.AGENTFLOW_PRD_WORKFLOW_MOCK, false);
|
|
9439
|
+
const runtimeOnly = url.searchParams.get("runtimeOnly") === "1" ||
|
|
9440
|
+
url.searchParams.get("runtime_only") === "1" ||
|
|
9441
|
+
url.searchParams.get("cached") === "1";
|
|
9442
|
+
const baseSnapshot = useMock
|
|
9443
|
+
? prdWorkflowMockSnapshot(scopedRoot, tapdId || "mock-prd")
|
|
9444
|
+
: runtimeOnly
|
|
9445
|
+
? prdWorkflowMaterializeSnapshot(root, scopedRoot, tapdId, userCtx, { flowSource, flowId })
|
|
9446
|
+
: await prdWorkflowSnapshot(root, scopedRoot, tapdId, userCtx, { flowSource, flowId });
|
|
9447
|
+
const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
9448
|
+
baseSnapshot,
|
|
9449
|
+
getSessionTokenFromRequest(req) || "",
|
|
9450
|
+
);
|
|
9451
|
+
json(res, 200, { ok: true, snapshot });
|
|
9452
|
+
} catch (e) {
|
|
9453
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9454
|
+
}
|
|
9455
|
+
return;
|
|
9456
|
+
}
|
|
9457
|
+
|
|
9458
|
+
if (req.method === "POST" && url.pathname === "/api/prd-workflow/snapshot") {
|
|
9459
|
+
let payload;
|
|
9460
|
+
try {
|
|
9461
|
+
payload = JSON.parse(await readBody(req));
|
|
9462
|
+
} catch {
|
|
9463
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
9464
|
+
return;
|
|
9465
|
+
}
|
|
9466
|
+
try {
|
|
9467
|
+
const tapdId = String(payload.tapdId || payload.tapd_id || payload?.snapshot?.tapdId || payload?.snapshot?.tapd_id || payload?.snapshot?.prd?.tapd_id || "").trim();
|
|
9468
|
+
if (!tapdId) {
|
|
9469
|
+
json(res, 400, { error: "Missing tapdId" });
|
|
9470
|
+
return;
|
|
9471
|
+
}
|
|
9472
|
+
const flowId = String(payload.flowId || "").trim();
|
|
9473
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
9474
|
+
const archived = payload.archived === true || payload.flowArchived === true;
|
|
9475
|
+
let scopedRoot = root;
|
|
9476
|
+
if (flowId) {
|
|
9477
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource, archived }, userCtx);
|
|
9478
|
+
if (scoped.error) {
|
|
9479
|
+
json(res, 400, { error: scoped.error });
|
|
9480
|
+
return;
|
|
9481
|
+
}
|
|
9482
|
+
scopedRoot = scoped.root;
|
|
9483
|
+
}
|
|
9484
|
+
const rawSnapshot = payload.snapshot && typeof payload.snapshot === "object" && !Array.isArray(payload.snapshot)
|
|
9485
|
+
? payload.snapshot
|
|
9486
|
+
: payload.prd || payload.next ? payload : null;
|
|
9487
|
+
if (!rawSnapshot) {
|
|
9488
|
+
json(res, 400, { error: "Missing snapshot" });
|
|
9489
|
+
return;
|
|
9490
|
+
}
|
|
9491
|
+
const normalizedSnapshot = {
|
|
9492
|
+
...prdWorkflowSnapshotFromParsed(scopedRoot, tapdId, rawSnapshot, userCtx, { flowSource, flowId }),
|
|
9493
|
+
clientReportedAt: new Date().toISOString(),
|
|
9494
|
+
sources: {
|
|
9495
|
+
...(rawSnapshot.sources && typeof rawSnapshot.sources === "object" ? rawSnapshot.sources : {}),
|
|
9496
|
+
executionMode: "client-report",
|
|
9497
|
+
},
|
|
9498
|
+
};
|
|
9499
|
+
const reportMeta = prdWorkflowSnapshotMetaFromReport(payload, rawSnapshot, req, userCtx);
|
|
9500
|
+
const reportSource = {
|
|
9501
|
+
...(normalizedSnapshot.sources && typeof normalizedSnapshot.sources === "object" ? normalizedSnapshot.sources : {}),
|
|
9502
|
+
executionMode: "client-report",
|
|
9503
|
+
truth: "observation",
|
|
9504
|
+
authority: "client",
|
|
9505
|
+
persistence: "runtime",
|
|
9506
|
+
clientId: reportMeta.clientId,
|
|
9507
|
+
clientUserId: reportMeta.userId,
|
|
9508
|
+
clientReportedAt: reportMeta.reportedAt,
|
|
9509
|
+
clientObservedAt: reportMeta.observedAt,
|
|
9510
|
+
baseRevision: reportMeta.baseRevision,
|
|
9511
|
+
scope: reportMeta.scope,
|
|
9512
|
+
platform: reportMeta.platform,
|
|
9513
|
+
issueKey: reportMeta.issueKey,
|
|
9514
|
+
stageKey: reportMeta.stageKey,
|
|
9515
|
+
};
|
|
9516
|
+
const storedObservationSnapshot = prdWorkflowStoredObservationSnapshot(normalizedSnapshot, reportSource);
|
|
9517
|
+
prdWorkflowWriteClientObservation(scopedRoot, tapdId, reportMeta, storedObservationSnapshot);
|
|
9518
|
+
|
|
9519
|
+
const projectFactSource = reportMeta.scope === "project"
|
|
9520
|
+
? prdWorkflowProjectFactSource(payload, rawSnapshot)
|
|
9521
|
+
: null;
|
|
9522
|
+
const projectFactSnapshot = projectFactSource
|
|
9523
|
+
? prdWorkflowStoredObservationSnapshot(normalizedSnapshot, {
|
|
9524
|
+
...reportSource,
|
|
9525
|
+
...projectFactSource,
|
|
9526
|
+
})
|
|
9527
|
+
: null;
|
|
9528
|
+
const projectRecord = prdWorkflowReadProjectStateWithFallback(root, scopedRoot, tapdId);
|
|
9529
|
+
const projectConflict = projectFactSnapshot
|
|
9530
|
+
? prdWorkflowSnapshotReportConflict(projectRecord, projectFactSnapshot, reportMeta)
|
|
9531
|
+
: null;
|
|
9532
|
+
if (projectConflict) {
|
|
9533
|
+
prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
|
|
9534
|
+
id: "stage_project_plan_conflict",
|
|
9535
|
+
type: "project-plan-conflict",
|
|
9536
|
+
scope: "project",
|
|
9537
|
+
stage: reportMeta.stageKey || "project-plan",
|
|
9538
|
+
title: "主 Project Plan 冲突",
|
|
9539
|
+
detail: projectConflict.message,
|
|
9540
|
+
status: "conflict",
|
|
9541
|
+
source: "agentflow",
|
|
9542
|
+
expectedRevision: projectConflict.expectedRevision || reportMeta.baseRevision || "",
|
|
9543
|
+
currentRevision: projectConflict.currentRevision || "",
|
|
9544
|
+
incomingRevision: projectConflict.incomingRevision || projectFactSnapshot.revision || "",
|
|
9545
|
+
clientId: reportMeta.clientId,
|
|
9546
|
+
observedAt: reportMeta.observedAt,
|
|
9547
|
+
currentSnapshot: prdWorkflowCompactRuntimeValue(projectRecord?.snapshot || null, 12000),
|
|
9548
|
+
incomingSnapshot: prdWorkflowCompactRuntimeValue(projectFactSnapshot, 12000),
|
|
9549
|
+
});
|
|
9550
|
+
const currentSnapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
9551
|
+
prdWorkflowMaterializeSnapshot(root, scopedRoot, tapdId, userCtx, { flowSource, flowId }),
|
|
9552
|
+
getSessionTokenFromRequest(req) || "",
|
|
9553
|
+
);
|
|
9554
|
+
json(res, 409, {
|
|
9555
|
+
ok: false,
|
|
9556
|
+
error: projectConflict.message,
|
|
9557
|
+
conflict: {
|
|
9558
|
+
...projectConflict,
|
|
9559
|
+
tapdId,
|
|
9560
|
+
type: "project-plan-conflict",
|
|
9561
|
+
currentPhase: String(currentSnapshot?.phase || ""),
|
|
9562
|
+
currentPointer: String(currentSnapshot?.pointer || ""),
|
|
9563
|
+
},
|
|
9564
|
+
snapshot: currentSnapshot,
|
|
9565
|
+
});
|
|
9566
|
+
return;
|
|
9567
|
+
}
|
|
9568
|
+
if (projectFactSnapshot) {
|
|
9569
|
+
prdWorkflowWriteProjectState(scopedRoot, tapdId, projectFactSnapshot, {
|
|
9570
|
+
sources: {
|
|
9571
|
+
...projectFactSource,
|
|
9572
|
+
clientId: reportMeta.clientId,
|
|
9573
|
+
observedAt: reportMeta.observedAt,
|
|
9574
|
+
},
|
|
9575
|
+
});
|
|
9576
|
+
}
|
|
9577
|
+
const materialized = prdWorkflowMaterializeSnapshot(root, scopedRoot, tapdId, userCtx, { flowSource, flowId });
|
|
9578
|
+
const withDiagnostic = prdWorkflowWithAgentflowTokenDiagnostic(materialized, getSessionTokenFromRequest(req) || "");
|
|
9579
|
+
prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "snapshot-report", tapdId, snapshot: withDiagnostic });
|
|
9580
|
+
json(res, 200, { ok: true, snapshot: withDiagnostic });
|
|
9581
|
+
} catch (e) {
|
|
9582
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9583
|
+
}
|
|
9584
|
+
return;
|
|
9585
|
+
}
|
|
9586
|
+
|
|
9587
|
+
if (req.method === "POST" && url.pathname === "/api/prd-workflow/action") {
|
|
9588
|
+
let payload;
|
|
9589
|
+
try {
|
|
9590
|
+
payload = JSON.parse(await readBody(req));
|
|
9591
|
+
} catch {
|
|
9592
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
9593
|
+
return;
|
|
9594
|
+
}
|
|
9595
|
+
let actionScopedRoot = root;
|
|
9596
|
+
let normalizedForCatch = null;
|
|
9597
|
+
let actionRunId = "";
|
|
9598
|
+
try {
|
|
9599
|
+
const flowId = String(payload.flowId || "").trim();
|
|
9600
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
9601
|
+
const archived = payload.archived === true || payload.flowArchived === true;
|
|
9602
|
+
let scopedRoot = root;
|
|
9603
|
+
if (flowId) {
|
|
9604
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource, archived }, userCtx);
|
|
9605
|
+
if (scoped.error) {
|
|
9606
|
+
json(res, 400, { error: scoped.error });
|
|
9607
|
+
return;
|
|
9608
|
+
}
|
|
9609
|
+
scopedRoot = scoped.root;
|
|
9610
|
+
}
|
|
9611
|
+
actionScopedRoot = scopedRoot;
|
|
9612
|
+
const normalized = normalizePrdWorkflowActionArgs(payload);
|
|
9613
|
+
normalizedForCatch = normalized;
|
|
9614
|
+
if (normalized.error) {
|
|
9615
|
+
json(res, 400, { error: normalized.error });
|
|
9616
|
+
return;
|
|
9617
|
+
}
|
|
9618
|
+
const idem = String(normalized.idempotencyKey || "").trim();
|
|
9619
|
+
const idemKey = idem ? prdWorkflowKey(userCtx, flowSource, flowId, `${normalized.tapdId}:${idem}`) : "";
|
|
9620
|
+
if (idemKey && prdWorkflowIdempotency.has(idemKey)) {
|
|
9621
|
+
json(res, 200, { ok: true, alreadyApplied: true, ...prdWorkflowIdempotency.get(idemKey)?.result });
|
|
9622
|
+
return;
|
|
9623
|
+
}
|
|
9624
|
+
const completedEvent = prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, normalized.tapdId, idem);
|
|
9625
|
+
if (completedEvent) {
|
|
9626
|
+
const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
9627
|
+
await prdWorkflowSnapshot(root, scopedRoot, normalized.tapdId, userCtx, { flowSource, flowId }),
|
|
9628
|
+
getSessionTokenFromRequest(req) || "",
|
|
9629
|
+
);
|
|
9630
|
+
const result = {
|
|
9631
|
+
ok: true,
|
|
9632
|
+
alreadyApplied: true,
|
|
9633
|
+
action: normalized.action,
|
|
9634
|
+
tapdId: normalized.tapdId,
|
|
9635
|
+
output: completedEvent.output || null,
|
|
9636
|
+
rawOutput: completedEvent.rawOutput || "",
|
|
9637
|
+
snapshot,
|
|
9638
|
+
};
|
|
9639
|
+
if (idemKey) {
|
|
9640
|
+
prdWorkflowIdempotency.set(idemKey, { at: Date.now(), result });
|
|
9641
|
+
prunePrdWorkflowIdempotency();
|
|
9642
|
+
}
|
|
9643
|
+
json(res, 200, result);
|
|
9644
|
+
return;
|
|
9645
|
+
}
|
|
9646
|
+
const eventKey = prdWorkflowKey(userCtx, flowSource, flowId, normalized.tapdId);
|
|
9647
|
+
if (prdWorkflowActionLocks.has(eventKey)) {
|
|
9648
|
+
json(res, 409, { error: "Another workflow action is already running for this TAPD ID" });
|
|
9649
|
+
return;
|
|
9650
|
+
}
|
|
9651
|
+
const startedAtMs = Date.now();
|
|
9652
|
+
const startedAt = new Date(startedAtMs).toISOString();
|
|
9653
|
+
const issueKey = String(payload?.issueKey || payload?.issue_key || payload?.issue || "").trim();
|
|
9654
|
+
const dryRun = payload?.dryRun === true || payload?.dry_run === true;
|
|
9655
|
+
const stageKey = String(payload?.stageKey || payload?.stage_key || payload?.stage || payload?.phase || normalized.action || "").trim();
|
|
9656
|
+
const actionTitle = String(payload?.title || payload?.label || payload?.actionLabel || payload?.action_label || stageKey || normalized.action).trim();
|
|
9657
|
+
actionRunId = `stage_${prdWorkflowSafeStateId([stageKey || normalized.action, issueKey].filter(Boolean).join(":"))}`;
|
|
9658
|
+
prdWorkflowActionLocks.set(eventKey, {
|
|
9659
|
+
action: normalized.action,
|
|
9660
|
+
tapdId: normalized.tapdId,
|
|
9661
|
+
title: actionTitle,
|
|
9662
|
+
stage: stageKey || normalized.action,
|
|
9663
|
+
issueKey,
|
|
9664
|
+
startedAt: startedAtMs,
|
|
9665
|
+
id: actionRunId,
|
|
9666
|
+
userId: String(userCtx?.userId || ""),
|
|
9667
|
+
});
|
|
9668
|
+
let result;
|
|
9669
|
+
try {
|
|
9670
|
+
const forceRuntimeMarker = payload?.runtimeOnly === true || payload?.runtime_only === true ||
|
|
9671
|
+
payload?.markerOnly === true || payload?.marker_only === true;
|
|
9672
|
+
const markerEvent = (!normalized.fromCommand || forceRuntimeMarker)
|
|
9673
|
+
? prdWorkflowMarkerEventSpec(payload, normalized)
|
|
9674
|
+
: null;
|
|
9675
|
+
if (!dryRun && (markerEvent || normalized.fromCommand)) {
|
|
9676
|
+
const expectedRevision = String(payload?.expectedRevision || "").trim();
|
|
9677
|
+
if (expectedRevision) {
|
|
9678
|
+
const latestForMarker = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
9679
|
+
await prdWorkflowSnapshot(root, scopedRoot, normalized.tapdId, userCtx, { flowSource, flowId }),
|
|
9680
|
+
getSessionTokenFromRequest(req) || "",
|
|
9681
|
+
);
|
|
9682
|
+
const latestRevision = String(latestForMarker?.revision || "").trim();
|
|
9683
|
+
if (latestRevision && latestRevision !== expectedRevision) {
|
|
9684
|
+
const err = new Error(`expected revision ${expectedRevision} but current revision is ${latestRevision}`);
|
|
9685
|
+
err.latestSnapshot = latestForMarker;
|
|
9686
|
+
throw err;
|
|
9687
|
+
}
|
|
9688
|
+
}
|
|
9689
|
+
}
|
|
9690
|
+
const startEvent = prdWorkflowAppendRuntimeEvent(scopedRoot, normalized.tapdId, {
|
|
9691
|
+
id: actionRunId,
|
|
9692
|
+
type: "action-start",
|
|
9693
|
+
action: normalized.action,
|
|
9694
|
+
stage: markerEvent?.stage || stageKey || normalized.action,
|
|
9695
|
+
title: markerEvent?.title || actionTitle,
|
|
9696
|
+
detail: dryRun ? "预演中" : "执行中",
|
|
9697
|
+
status: "running",
|
|
9698
|
+
startedAt,
|
|
9699
|
+
dryRun,
|
|
9700
|
+
issueKey: markerEvent?.issueKey || issueKey,
|
|
9701
|
+
expectedRevision: payload?.expectedRevision || "",
|
|
9702
|
+
idempotencyKey: idem,
|
|
9703
|
+
});
|
|
9704
|
+
prdWorkflowBroadcast(eventKey, startEvent || { type: "action-start", action: normalized.action, tapdId: normalized.tapdId });
|
|
9705
|
+
if (markerEvent) {
|
|
9706
|
+
const output = {
|
|
9707
|
+
runtimeOnly: true,
|
|
9708
|
+
kind: markerEvent.kind,
|
|
9709
|
+
message: dryRun
|
|
9710
|
+
? "该动作将记录为 Workflow runtime event,不会写 ai-doc marker commit。"
|
|
9711
|
+
: "已记录为 Workflow runtime event,未写 ai-doc marker commit。",
|
|
9712
|
+
stage: markerEvent.stage,
|
|
9713
|
+
issueKey: markerEvent.issueKey || issueKey,
|
|
9714
|
+
artifacts: markerEvent.artifacts,
|
|
9715
|
+
links: markerEvent.links,
|
|
9716
|
+
};
|
|
9717
|
+
prdWorkflowAppendRuntimeEvent(scopedRoot, normalized.tapdId, {
|
|
9718
|
+
id: actionRunId,
|
|
9719
|
+
type: dryRun ? "action-preview" : "action-done",
|
|
9720
|
+
source: "agentflow",
|
|
9721
|
+
action: normalized.action,
|
|
9722
|
+
stage: markerEvent.stage || stageKey || normalized.action,
|
|
9723
|
+
title: markerEvent.title || actionTitle,
|
|
9724
|
+
detail: markerEvent.detail,
|
|
9725
|
+
status: dryRun ? "current" : "done",
|
|
9726
|
+
startedAt,
|
|
9727
|
+
completedAt: new Date().toISOString(),
|
|
9728
|
+
dryRun,
|
|
9729
|
+
issueKey: markerEvent.issueKey || issueKey,
|
|
9730
|
+
expectedRevision: payload?.expectedRevision || "",
|
|
9731
|
+
idempotencyKey: idem,
|
|
9732
|
+
output,
|
|
9733
|
+
artifacts: markerEvent.artifacts,
|
|
9734
|
+
links: markerEvent.links,
|
|
9735
|
+
});
|
|
9736
|
+
const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
9737
|
+
await prdWorkflowSnapshot(root, scopedRoot, normalized.tapdId, userCtx, { flowSource, flowId }),
|
|
9738
|
+
getSessionTokenFromRequest(req) || "",
|
|
9739
|
+
);
|
|
9740
|
+
result = {
|
|
9741
|
+
ok: true,
|
|
9742
|
+
runtimeOnly: true,
|
|
9743
|
+
action: normalized.action,
|
|
9744
|
+
tapdId: normalized.tapdId,
|
|
9745
|
+
output,
|
|
9746
|
+
rawOutput: "",
|
|
9747
|
+
snapshot,
|
|
9748
|
+
};
|
|
9749
|
+
if (idemKey) {
|
|
9750
|
+
prdWorkflowIdempotency.set(idemKey, { at: Date.now(), result });
|
|
9751
|
+
prunePrdWorkflowIdempotency();
|
|
9752
|
+
}
|
|
9753
|
+
prdWorkflowBroadcast(eventKey, { type: "action-done", action: normalized.action, tapdId: normalized.tapdId, snapshot });
|
|
9754
|
+
json(res, 200, result);
|
|
9755
|
+
return;
|
|
9756
|
+
}
|
|
9757
|
+
if (normalized.fromCommand && dryRun) {
|
|
9758
|
+
const output = {
|
|
9759
|
+
preview: true,
|
|
9760
|
+
command: normalized.command || `prd-flow ${normalized.args.join(" ")}`,
|
|
9761
|
+
message: "预演模式只展示将执行的客户端 prd-flow 命令;确认后会登记 action request,等待客户端 skill 执行并上报结果。",
|
|
9762
|
+
args: normalized.args,
|
|
9763
|
+
};
|
|
9764
|
+
prdWorkflowAppendRuntimeEvent(scopedRoot, normalized.tapdId, {
|
|
9765
|
+
id: actionRunId,
|
|
9766
|
+
type: "action-preview",
|
|
9767
|
+
source: "agentflow",
|
|
9768
|
+
action: normalized.action,
|
|
9769
|
+
stage: stageKey || normalized.action,
|
|
9770
|
+
title: actionTitle,
|
|
9771
|
+
detail: output.message,
|
|
9772
|
+
status: "current",
|
|
9773
|
+
startedAt,
|
|
9774
|
+
completedAt: new Date().toISOString(),
|
|
9775
|
+
dryRun,
|
|
9776
|
+
issueKey,
|
|
9777
|
+
expectedRevision: payload?.expectedRevision || "",
|
|
9778
|
+
idempotencyKey: idem,
|
|
9779
|
+
output,
|
|
9780
|
+
});
|
|
9781
|
+
const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
9782
|
+
await prdWorkflowSnapshot(root, scopedRoot, normalized.tapdId, userCtx, { flowSource, flowId }),
|
|
9783
|
+
getSessionTokenFromRequest(req) || "",
|
|
9784
|
+
);
|
|
9785
|
+
result = {
|
|
9786
|
+
ok: true,
|
|
9787
|
+
preview: true,
|
|
9788
|
+
action: normalized.action,
|
|
9789
|
+
tapdId: normalized.tapdId,
|
|
9790
|
+
output,
|
|
9791
|
+
rawOutput: "",
|
|
9792
|
+
snapshot,
|
|
9793
|
+
};
|
|
9794
|
+
prdWorkflowBroadcast(eventKey, { type: "action-preview", action: normalized.action, tapdId: normalized.tapdId, snapshot });
|
|
9795
|
+
json(res, 200, result);
|
|
9796
|
+
return;
|
|
9797
|
+
}
|
|
9798
|
+
if (normalized.fromCommand && !prdWorkflowAllowServerExec()) {
|
|
9799
|
+
const output = {
|
|
9800
|
+
clientExecutionRequired: true,
|
|
9801
|
+
command: normalized.command || `prd-flow ${normalized.args.join(" ")}`,
|
|
9802
|
+
message: "已登记 Workflow action request;服务端不会执行客户端 prd-flow。请客户端 skill 使用 AGENTFLOW_BASE_URL + AGENTFLOW_TOKEN 执行该命令并上报 snapshot/event。",
|
|
9803
|
+
args: normalized.args,
|
|
9804
|
+
};
|
|
9805
|
+
prdWorkflowAppendRuntimeEvent(scopedRoot, normalized.tapdId, {
|
|
9806
|
+
id: actionRunId,
|
|
9807
|
+
type: "action-request",
|
|
9808
|
+
source: "agentflow",
|
|
9809
|
+
action: normalized.action,
|
|
9810
|
+
stage: stageKey || normalized.action,
|
|
9811
|
+
title: actionTitle,
|
|
9812
|
+
detail: output.message,
|
|
9813
|
+
status: "current",
|
|
9814
|
+
startedAt,
|
|
9815
|
+
completedAt: new Date().toISOString(),
|
|
9816
|
+
dryRun: false,
|
|
9817
|
+
issueKey,
|
|
9818
|
+
expectedRevision: payload?.expectedRevision || "",
|
|
9819
|
+
idempotencyKey: idem,
|
|
9820
|
+
output,
|
|
9821
|
+
command: output.command,
|
|
9822
|
+
});
|
|
9823
|
+
const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
9824
|
+
await prdWorkflowSnapshot(root, scopedRoot, normalized.tapdId, userCtx, { flowSource, flowId }),
|
|
9825
|
+
getSessionTokenFromRequest(req) || "",
|
|
9826
|
+
);
|
|
9827
|
+
result = {
|
|
9828
|
+
ok: true,
|
|
9829
|
+
actionRequested: true,
|
|
9830
|
+
action: normalized.action,
|
|
9831
|
+
tapdId: normalized.tapdId,
|
|
9832
|
+
output,
|
|
9833
|
+
rawOutput: "",
|
|
9834
|
+
snapshot,
|
|
9835
|
+
};
|
|
9836
|
+
if (idemKey) {
|
|
9837
|
+
prdWorkflowIdempotency.set(idemKey, { at: Date.now(), result });
|
|
9838
|
+
prunePrdWorkflowIdempotency();
|
|
9839
|
+
}
|
|
9840
|
+
prdWorkflowBroadcast(eventKey, { type: "action-request", action: normalized.action, tapdId: normalized.tapdId, snapshot });
|
|
9841
|
+
json(res, 200, result);
|
|
9842
|
+
return;
|
|
9843
|
+
}
|
|
9844
|
+
const runtimeEventUrl = `${serverPublicBaseUrl(req, host, uiPort)}/api/prd-workflow/event`;
|
|
9845
|
+
const commandResult = await runPrdWorkflowCommand(root, scopedRoot, normalized.args, userCtx, {
|
|
9846
|
+
timeout: 300000,
|
|
9847
|
+
env: {
|
|
9848
|
+
PRD_FLOW_RUNTIME_EVENT_URL: runtimeEventUrl,
|
|
9849
|
+
PRD_FLOW_RUNTIME_EVENT_TOKEN: getSessionTokenFromRequest(req) || "",
|
|
9850
|
+
PRD_FLOW_RUNTIME_TAPD_ID: normalized.tapdId,
|
|
9851
|
+
PRD_FLOW_RUNTIME_STAGE_KEY: stageKey || normalized.action,
|
|
9852
|
+
PRD_FLOW_RUNTIME_ISSUE_KEY: issueKey,
|
|
9853
|
+
PRD_FLOW_RUNTIME_FLOW_ID: flowId,
|
|
9854
|
+
PRD_FLOW_RUNTIME_FLOW_SOURCE: flowSource,
|
|
9855
|
+
PRD_FLOW_MARKER_POLICY: "runtime-only",
|
|
9856
|
+
PRD_FLOW_SUPPRESS_AI_DOC_MARKERS: "1",
|
|
9857
|
+
},
|
|
9858
|
+
});
|
|
9859
|
+
const parsed = prdWorkflowParseJson(commandResult.stdout);
|
|
9860
|
+
const rawOutput = parsed ? "" : String(commandResult.stdout || commandResult.stderr || "").slice(0, 12000);
|
|
9861
|
+
prdWorkflowAppendRuntimeEvent(scopedRoot, normalized.tapdId, {
|
|
9862
|
+
id: actionRunId,
|
|
9863
|
+
type: "action-done",
|
|
9864
|
+
action: normalized.action,
|
|
9865
|
+
stage: stageKey || normalized.action,
|
|
9866
|
+
title: actionTitle,
|
|
9867
|
+
detail: parsed?.message || parsed?.summary || (dryRun ? "预演完成,等待确认" : "阶段完成"),
|
|
9868
|
+
status: dryRun ? "current" : "done",
|
|
9869
|
+
startedAt,
|
|
9870
|
+
completedAt: new Date().toISOString(),
|
|
9871
|
+
dryRun,
|
|
9872
|
+
issueKey,
|
|
9873
|
+
expectedRevision: payload?.expectedRevision || "",
|
|
9874
|
+
idempotencyKey: idem,
|
|
9875
|
+
output: parsed || null,
|
|
9876
|
+
rawOutput,
|
|
9877
|
+
artifacts: Array.isArray(parsed?.artifacts) ? parsed.artifacts : [],
|
|
9878
|
+
links: Array.isArray(parsed?.links) ? parsed.links : [],
|
|
9879
|
+
});
|
|
9880
|
+
const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
9881
|
+
await prdWorkflowSnapshot(root, scopedRoot, normalized.tapdId, userCtx, { flowSource, flowId }),
|
|
9882
|
+
getSessionTokenFromRequest(req) || "",
|
|
9883
|
+
);
|
|
9884
|
+
result = {
|
|
9885
|
+
ok: true,
|
|
9886
|
+
action: normalized.action,
|
|
9887
|
+
tapdId: normalized.tapdId,
|
|
9888
|
+
output: parsed || null,
|
|
9889
|
+
rawOutput,
|
|
9890
|
+
snapshot,
|
|
9891
|
+
};
|
|
9892
|
+
if (idemKey) {
|
|
9893
|
+
prdWorkflowIdempotency.set(idemKey, { at: Date.now(), result });
|
|
9894
|
+
prunePrdWorkflowIdempotency();
|
|
9895
|
+
}
|
|
9896
|
+
prdWorkflowBroadcast(eventKey, { type: "action-done", action: normalized.action, tapdId: normalized.tapdId, snapshot });
|
|
9897
|
+
} finally {
|
|
9898
|
+
prdWorkflowActionLocks.delete(eventKey);
|
|
9899
|
+
}
|
|
9900
|
+
json(res, 200, result);
|
|
9901
|
+
} catch (e) {
|
|
9902
|
+
const status = /expected revision|stale|conflict|not allow|precondition/i.test(String(e?.message || e)) ? 409 : 500;
|
|
9903
|
+
const tapdId = String(normalizedForCatch?.tapdId || payload?.tapdId || payload?.tapd_id || "").trim();
|
|
9904
|
+
const flowId = String(payload?.flowId || "").trim();
|
|
9905
|
+
const flowSource = String(payload?.flowSource || "user").trim() || "user";
|
|
9906
|
+
const errorText = (e && e.message) || String(e);
|
|
9907
|
+
if (tapdId) {
|
|
9908
|
+
const issueKey = String(payload?.issueKey || payload?.issue_key || payload?.issue || "").trim();
|
|
9909
|
+
const stageKey = String(payload?.stageKey || payload?.stage_key || payload?.stage || payload?.phase || normalizedForCatch?.action || payload?.action || payload?.actionId || "").trim();
|
|
9910
|
+
prdWorkflowAppendRuntimeEvent(actionScopedRoot, tapdId, {
|
|
9911
|
+
id: actionRunId || `stage_${prdWorkflowSafeStateId([stageKey || payload?.action || payload?.actionId || "workflow-action", issueKey].filter(Boolean).join(":"))}`,
|
|
9912
|
+
type: status === 409 ? "action-conflict" : "action-error",
|
|
9913
|
+
action: String(normalizedForCatch?.action || payload?.action || payload?.actionId || ""),
|
|
9914
|
+
stage: stageKey || String(normalizedForCatch?.action || payload?.action || payload?.actionId || ""),
|
|
9915
|
+
title: String(payload?.title || payload?.label || normalizedForCatch?.action || payload?.action || payload?.actionId || "workflow action"),
|
|
9916
|
+
detail: errorText,
|
|
9917
|
+
status: status === 409 ? "conflict" : "error",
|
|
9918
|
+
completedAt: new Date().toISOString(),
|
|
9919
|
+
dryRun: payload?.dryRun === true || payload?.dry_run === true,
|
|
9920
|
+
issueKey,
|
|
9921
|
+
expectedRevision: payload?.expectedRevision || "",
|
|
9922
|
+
idempotencyKey: payload?.idempotencyKey || "",
|
|
9923
|
+
error: errorText,
|
|
9924
|
+
rawOutput: `${String(e?.stdout || "")}${String(e?.stderr || "")}`.slice(0, 12000),
|
|
9925
|
+
});
|
|
9926
|
+
}
|
|
9927
|
+
prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), {
|
|
9928
|
+
type: "action-error",
|
|
9929
|
+
action: String(payload?.action || payload?.actionId || ""),
|
|
9930
|
+
tapdId,
|
|
9931
|
+
error: errorText,
|
|
9932
|
+
});
|
|
9933
|
+
let latestSnapshot = null;
|
|
9934
|
+
if (status === 409 && tapdId) {
|
|
9935
|
+
try {
|
|
9936
|
+
latestSnapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
9937
|
+
await prdWorkflowSnapshot(root, actionScopedRoot, tapdId, userCtx, { flowSource, flowId }),
|
|
9938
|
+
getSessionTokenFromRequest(req) || "",
|
|
9939
|
+
);
|
|
9940
|
+
} catch (_) {}
|
|
9941
|
+
}
|
|
9942
|
+
const conflict = status === 409 ? {
|
|
9943
|
+
action: String(normalizedForCatch?.action || payload?.action || payload?.actionId || ""),
|
|
9944
|
+
tapdId,
|
|
9945
|
+
expectedRevision: String(payload?.expectedRevision || ""),
|
|
9946
|
+
currentRevision: String(latestSnapshot?.revision || ""),
|
|
9947
|
+
currentPhase: String(latestSnapshot?.phase || ""),
|
|
9948
|
+
currentPointer: String(latestSnapshot?.pointer || ""),
|
|
9949
|
+
message: errorText,
|
|
9950
|
+
} : null;
|
|
9951
|
+
json(res, status, {
|
|
9952
|
+
error: errorText,
|
|
9953
|
+
rawOutput: `${String(e?.stdout || "")}${String(e?.stderr || "")}`.slice(0, 12000),
|
|
9954
|
+
snapshot: latestSnapshot,
|
|
9955
|
+
conflict,
|
|
9956
|
+
});
|
|
9957
|
+
}
|
|
9958
|
+
return;
|
|
9959
|
+
}
|
|
7437
9960
|
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
9961
|
+
if (req.method === "GET" && url.pathname === "/api/prd-workflow/idempotency") {
|
|
9962
|
+
try {
|
|
9963
|
+
const tapdId = String(url.searchParams.get("tapdId") || url.searchParams.get("tapd_id") || "").trim();
|
|
9964
|
+
const idempotencyKey = String(url.searchParams.get("key") || url.searchParams.get("idempotencyKey") || "").trim();
|
|
9965
|
+
if (!tapdId) {
|
|
9966
|
+
json(res, 400, { error: "Missing tapdId" });
|
|
9967
|
+
return;
|
|
9968
|
+
}
|
|
9969
|
+
if (!idempotencyKey) {
|
|
9970
|
+
json(res, 400, { error: "Missing idempotency key" });
|
|
9971
|
+
return;
|
|
9972
|
+
}
|
|
9973
|
+
const flowId = String(url.searchParams.get("flowId") || "").trim();
|
|
9974
|
+
const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
|
|
9975
|
+
const archived = url.searchParams.get("archived") === "1";
|
|
9976
|
+
let scopedRoot = root;
|
|
9977
|
+
if (flowId) {
|
|
9978
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource, archived }, userCtx);
|
|
9979
|
+
if (scoped.error) {
|
|
9980
|
+
json(res, 400, { error: scoped.error });
|
|
9981
|
+
return;
|
|
9982
|
+
}
|
|
9983
|
+
scopedRoot = scoped.root;
|
|
9984
|
+
}
|
|
9985
|
+
const event = prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey);
|
|
9986
|
+
json(res, 200, {
|
|
9987
|
+
ok: true,
|
|
9988
|
+
found: !!event,
|
|
9989
|
+
result: event?.output || event?.result || null,
|
|
9990
|
+
rawOutput: event?.rawOutput || "",
|
|
9991
|
+
event: event || null,
|
|
9992
|
+
});
|
|
9993
|
+
} catch (e) {
|
|
9994
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
9995
|
+
}
|
|
9996
|
+
return;
|
|
9997
|
+
}
|
|
7443
9998
|
|
|
7444
|
-
if (
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
9999
|
+
if (req.method === "POST" && url.pathname === "/api/prd-workflow/idempotency") {
|
|
10000
|
+
let payload;
|
|
10001
|
+
try {
|
|
10002
|
+
payload = JSON.parse(await readBody(req));
|
|
10003
|
+
} catch {
|
|
10004
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
10005
|
+
return;
|
|
10006
|
+
}
|
|
10007
|
+
try {
|
|
10008
|
+
const tapdId = String(payload.tapdId || payload.tapd_id || "").trim();
|
|
10009
|
+
const idempotencyKey = String(payload.key || payload.idempotencyKey || payload.idempotency_key || "").trim();
|
|
10010
|
+
if (!tapdId) {
|
|
10011
|
+
json(res, 400, { error: "Missing tapdId" });
|
|
10012
|
+
return;
|
|
10013
|
+
}
|
|
10014
|
+
if (!idempotencyKey) {
|
|
10015
|
+
json(res, 400, { error: "Missing idempotency key" });
|
|
10016
|
+
return;
|
|
10017
|
+
}
|
|
10018
|
+
const flowId = String(payload.flowId || "").trim();
|
|
10019
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
10020
|
+
const archived = payload.archived === true || payload.flowArchived === true;
|
|
10021
|
+
let scopedRoot = root;
|
|
10022
|
+
if (flowId) {
|
|
10023
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource, archived }, userCtx);
|
|
10024
|
+
if (scoped.error) {
|
|
10025
|
+
json(res, 400, { error: scoped.error });
|
|
10026
|
+
return;
|
|
10027
|
+
}
|
|
10028
|
+
scopedRoot = scoped.root;
|
|
10029
|
+
}
|
|
10030
|
+
const existing = prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey);
|
|
10031
|
+
if (existing) {
|
|
10032
|
+
json(res, 200, { ok: true, found: true, event: existing, result: existing.output || existing.result || null });
|
|
10033
|
+
return;
|
|
10034
|
+
}
|
|
10035
|
+
const command = String(payload.command || "").slice(0, 1000);
|
|
10036
|
+
const result = payload.result && typeof payload.result === "object" && !Array.isArray(payload.result)
|
|
10037
|
+
? payload.result
|
|
10038
|
+
: { message: String(payload.message || "already completed") };
|
|
10039
|
+
const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
|
|
10040
|
+
type: "idempotent-command-completed",
|
|
10041
|
+
source: "prd-flow-client",
|
|
10042
|
+
auxiliary: true,
|
|
10043
|
+
aggregateByStage: false,
|
|
10044
|
+
conflictOnArtifact: false,
|
|
10045
|
+
action: payload.action || "",
|
|
10046
|
+
stage: payload.stage || payload.stageKey || payload.stage_key || "idempotency",
|
|
10047
|
+
title: payload.title || "prd-flow command completed",
|
|
10048
|
+
detail: command ? `Command completed: ${command}` : "Command completed",
|
|
10049
|
+
status: "done",
|
|
10050
|
+
completedAt: new Date().toISOString(),
|
|
10051
|
+
idempotencyKey,
|
|
10052
|
+
command,
|
|
10053
|
+
output: result,
|
|
10054
|
+
result,
|
|
10055
|
+
});
|
|
10056
|
+
json(res, 200, { ok: true, found: true, event, result });
|
|
10057
|
+
} catch (e) {
|
|
10058
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
10059
|
+
}
|
|
7456
10060
|
return;
|
|
7457
10061
|
}
|
|
7458
10062
|
|
|
7459
|
-
if (
|
|
10063
|
+
if (req.method === "POST" && url.pathname === "/api/prd-workflow/event") {
|
|
7460
10064
|
let payload;
|
|
7461
10065
|
try {
|
|
7462
10066
|
payload = JSON.parse(await readBody(req));
|
|
@@ -7464,43 +10068,209 @@ export function startUiServer({
|
|
|
7464
10068
|
json(res, 400, { error: "Invalid JSON body" });
|
|
7465
10069
|
return;
|
|
7466
10070
|
}
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
10071
|
+
try {
|
|
10072
|
+
const tapdId = String(payload.tapdId || payload.tapd_id || "").trim();
|
|
10073
|
+
if (!tapdId) {
|
|
10074
|
+
json(res, 400, { error: "Missing tapdId" });
|
|
10075
|
+
return;
|
|
10076
|
+
}
|
|
10077
|
+
const flowId = String(payload.flowId || "").trim();
|
|
10078
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
10079
|
+
const archived = payload.archived === true || payload.flowArchived === true;
|
|
10080
|
+
let scopedRoot = root;
|
|
10081
|
+
if (flowId) {
|
|
10082
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource, archived }, userCtx);
|
|
10083
|
+
if (scoped.error) {
|
|
10084
|
+
json(res, 400, { error: scoped.error });
|
|
10085
|
+
return;
|
|
10086
|
+
}
|
|
10087
|
+
scopedRoot = scoped.root;
|
|
10088
|
+
}
|
|
10089
|
+
const eventPayload = payload.event && typeof payload.event === "object" && !Array.isArray(payload.event)
|
|
10090
|
+
? payload.event
|
|
10091
|
+
: payload;
|
|
10092
|
+
const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
|
|
10093
|
+
...eventPayload,
|
|
10094
|
+
tapdId,
|
|
10095
|
+
type: eventPayload.type || "workflow-event",
|
|
10096
|
+
});
|
|
10097
|
+
const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
10098
|
+
await prdWorkflowSnapshot(root, scopedRoot, tapdId, userCtx, { flowSource, flowId }),
|
|
10099
|
+
getSessionTokenFromRequest(req) || "",
|
|
10100
|
+
);
|
|
10101
|
+
prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "runtime-event", tapdId, event, snapshot });
|
|
10102
|
+
json(res, 200, { ok: true, event, snapshot });
|
|
10103
|
+
} catch (e) {
|
|
10104
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
10105
|
+
}
|
|
10106
|
+
return;
|
|
10107
|
+
}
|
|
10108
|
+
|
|
10109
|
+
if (req.method === "POST" && url.pathname === "/api/prd-workflow/review-link") {
|
|
10110
|
+
let payload;
|
|
10111
|
+
try {
|
|
10112
|
+
payload = JSON.parse(await readBody(req));
|
|
10113
|
+
} catch {
|
|
10114
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
7470
10115
|
return;
|
|
7471
10116
|
}
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
10117
|
+
try {
|
|
10118
|
+
const tapdId = String(payload.tapdId || payload.tapd_id || "").trim();
|
|
10119
|
+
if (!tapdId) {
|
|
10120
|
+
json(res, 400, { error: "Missing tapdId" });
|
|
10121
|
+
return;
|
|
10122
|
+
}
|
|
10123
|
+
const flowId = String(payload.flowId || "").trim();
|
|
10124
|
+
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
10125
|
+
const archived = payload.archived === true || payload.flowArchived === true;
|
|
10126
|
+
let scopedRoot = root;
|
|
10127
|
+
if (flowId) {
|
|
10128
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource, archived }, userCtx);
|
|
10129
|
+
if (scoped.error) {
|
|
10130
|
+
json(res, 400, { error: scoped.error });
|
|
10131
|
+
return;
|
|
10132
|
+
}
|
|
10133
|
+
scopedRoot = scoped.root;
|
|
10134
|
+
}
|
|
10135
|
+
const review = prdWorkflowCreateReview(scopedRoot, tapdId, payload, serverPublicBaseUrl(req, host, uiPort, payload));
|
|
10136
|
+
const query = new URLSearchParams();
|
|
10137
|
+
if (flowId) query.set("flowId", flowId);
|
|
10138
|
+
if (flowSource) query.set("flowSource", flowSource);
|
|
10139
|
+
if (archived) query.set("archived", "1");
|
|
10140
|
+
const reviewUrl = query.toString() ? `${review.url}?${query.toString()}` : review.url;
|
|
10141
|
+
const durability = review.durability || "temporary";
|
|
10142
|
+
const reviewSource = review.source && typeof review.source === "object" && !Array.isArray(review.source)
|
|
10143
|
+
? review.source
|
|
10144
|
+
: { kind: durability === "durable" ? "ai-doc" : "local-draft", durability };
|
|
10145
|
+
const artifact = {
|
|
10146
|
+
label: payload.artifactLabel || "Markdown Review",
|
|
10147
|
+
kind: durability === "temporary" ? "temporary-review" : "review",
|
|
10148
|
+
persistence: "runtime",
|
|
10149
|
+
durability,
|
|
10150
|
+
source: reviewSource,
|
|
10151
|
+
confirmed: payload.confirmed === true || payload.confirmed === "1",
|
|
10152
|
+
url: reviewUrl,
|
|
10153
|
+
expiresAt: review.expiresAt || "",
|
|
10154
|
+
};
|
|
10155
|
+
const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
|
|
10156
|
+
type: "review-link",
|
|
10157
|
+
auxiliary: true,
|
|
10158
|
+
conflictOnArtifact: false,
|
|
10159
|
+
truth: "runtime_event",
|
|
10160
|
+
persistence: "runtime",
|
|
10161
|
+
action: payload.action || payload.actionId || "",
|
|
10162
|
+
stage: payload.stage || payload.stageKey || payload.stage_key || "review",
|
|
10163
|
+
title: payload.title || "临时 Markdown Review",
|
|
10164
|
+
detail: "已生成临时 Markdown review 链接",
|
|
10165
|
+
status: "current",
|
|
10166
|
+
issueKey: payload.issueKey || payload.issue_key || "",
|
|
10167
|
+
durability,
|
|
10168
|
+
sourceArtifact: reviewSource,
|
|
10169
|
+
expiresAt: review.expiresAt || "",
|
|
10170
|
+
artifacts: [artifact],
|
|
10171
|
+
links: [{ label: "Markdown Review", url: reviewUrl, persistence: "runtime", durability, source: reviewSource, expiresAt: review.expiresAt || "" }],
|
|
10172
|
+
reviewId: review.id,
|
|
10173
|
+
});
|
|
10174
|
+
const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
10175
|
+
await prdWorkflowSnapshot(root, scopedRoot, tapdId, userCtx, { flowSource, flowId }),
|
|
10176
|
+
getSessionTokenFromRequest(req) || "",
|
|
10177
|
+
);
|
|
10178
|
+
prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "review-link", tapdId, event, snapshot });
|
|
10179
|
+
json(res, 200, { ok: true, review: { ...review, url: reviewUrl }, event, snapshot });
|
|
10180
|
+
} catch (e) {
|
|
10181
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
10182
|
+
}
|
|
7479
10183
|
return;
|
|
7480
10184
|
}
|
|
7481
10185
|
|
|
7482
|
-
if (
|
|
7483
|
-
|
|
7484
|
-
const
|
|
10186
|
+
if (req.method === "GET" && url.pathname === "/api/prd-workflow/events") {
|
|
10187
|
+
const tapdId = String(url.searchParams.get("tapdId") || "").trim();
|
|
10188
|
+
const flowId = String(url.searchParams.get("flowId") || "").trim();
|
|
10189
|
+
const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
|
|
10190
|
+
const key = prdWorkflowKey(userCtx, flowSource, flowId, tapdId);
|
|
10191
|
+
let set = prdWorkflowSubscribers.get(key);
|
|
10192
|
+
if (!set) {
|
|
10193
|
+
set = new Set();
|
|
10194
|
+
prdWorkflowSubscribers.set(key, set);
|
|
10195
|
+
}
|
|
7485
10196
|
res.writeHead(200, {
|
|
7486
|
-
"Content-Type": "
|
|
7487
|
-
"
|
|
7488
|
-
"
|
|
10197
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
10198
|
+
"Cache-Control": "no-cache, no-transform",
|
|
10199
|
+
Connection: "keep-alive",
|
|
10200
|
+
"X-Content-Type-Options": "nosniff",
|
|
7489
10201
|
});
|
|
7490
|
-
res.
|
|
10202
|
+
res.write(": connected\n\n");
|
|
10203
|
+
set.add(res);
|
|
10204
|
+
const detach = () => {
|
|
10205
|
+
try {
|
|
10206
|
+
set.delete(res);
|
|
10207
|
+
if (set.size === 0) prdWorkflowSubscribers.delete(key);
|
|
10208
|
+
} catch (_) {}
|
|
10209
|
+
};
|
|
10210
|
+
req.on("close", detach);
|
|
10211
|
+
res.on("close", detach);
|
|
7491
10212
|
return;
|
|
7492
10213
|
}
|
|
7493
10214
|
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
10215
|
+
if (req.method === "GET" && url.pathname.startsWith("/api/prd-workflow/review/")) {
|
|
10216
|
+
try {
|
|
10217
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
10218
|
+
const tapdId = decodeURIComponent(parts[3] || "");
|
|
10219
|
+
const reviewId = decodeURIComponent(parts[4] || "");
|
|
10220
|
+
if (!tapdId || !reviewId) {
|
|
10221
|
+
res.writeHead(404);
|
|
10222
|
+
res.end("Not found");
|
|
10223
|
+
return;
|
|
10224
|
+
}
|
|
10225
|
+
const flowId = String(url.searchParams.get("flowId") || "").trim();
|
|
10226
|
+
const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
|
|
10227
|
+
const archived = url.searchParams.get("archived") === "1";
|
|
10228
|
+
let scopedRoot = root;
|
|
10229
|
+
if (flowId) {
|
|
10230
|
+
const scoped = resolveWorkspaceScopeRoot(root, { flowId, flowSource, archived }, userCtx);
|
|
10231
|
+
if (scoped.error) {
|
|
10232
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
10233
|
+
res.end(scoped.error);
|
|
10234
|
+
return;
|
|
10235
|
+
}
|
|
10236
|
+
scopedRoot = scoped.root;
|
|
10237
|
+
}
|
|
10238
|
+
const paths = prdWorkflowReviewPaths(scopedRoot, tapdId, reviewId);
|
|
10239
|
+
if (!fs.existsSync(paths.markdownPath) || !fs.statSync(paths.markdownPath).isFile()) {
|
|
10240
|
+
res.writeHead(404);
|
|
10241
|
+
res.end("Not found");
|
|
10242
|
+
return;
|
|
10243
|
+
}
|
|
10244
|
+
const markdown = fs.readFileSync(paths.markdownPath, "utf-8");
|
|
10245
|
+
let meta = {};
|
|
10246
|
+
try {
|
|
10247
|
+
if (fs.existsSync(paths.metaPath)) meta = JSON.parse(fs.readFileSync(paths.metaPath, "utf-8"));
|
|
10248
|
+
} catch (_) {}
|
|
10249
|
+
const expiresMs = Date.parse(meta?.expiresAt || "");
|
|
10250
|
+
if (Number.isFinite(expiresMs) && expiresMs < Date.now()) {
|
|
10251
|
+
res.writeHead(410, { "Content-Type": "text/plain; charset=utf-8" });
|
|
10252
|
+
res.end("Review link expired");
|
|
10253
|
+
return;
|
|
10254
|
+
}
|
|
10255
|
+
const rawParams = new URLSearchParams(url.searchParams);
|
|
10256
|
+
rawParams.set("raw", "1");
|
|
10257
|
+
meta = { ...(meta && typeof meta === "object" && !Array.isArray(meta) ? meta : {}), rawHref: `${url.pathname}?${rawParams.toString()}` };
|
|
10258
|
+
if (url.searchParams.get("raw") === "1") {
|
|
10259
|
+
const data = Buffer.from(markdown, "utf-8");
|
|
10260
|
+
res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Content-Length": data.length });
|
|
10261
|
+
res.end(data);
|
|
10262
|
+
return;
|
|
10263
|
+
}
|
|
10264
|
+
const html = Buffer.from(prdWorkflowReviewHtml(meta.title || "PRD Workflow Review", markdown, meta), "utf-8");
|
|
10265
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Length": html.length });
|
|
10266
|
+
res.end(html);
|
|
10267
|
+
} catch (e) {
|
|
10268
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
10269
|
+
res.end((e && e.message) || String(e));
|
|
7500
10270
|
}
|
|
7501
|
-
json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
|
|
7502
10271
|
return;
|
|
7503
10272
|
}
|
|
10273
|
+
|
|
7504
10274
|
if (req.method === "GET" && url.pathname === "/api/display/shares") {
|
|
7505
10275
|
try {
|
|
7506
10276
|
if (!authUser?.userId) {
|
|
@@ -9236,6 +12006,52 @@ export function startUiServer({
|
|
|
9236
12006
|
return;
|
|
9237
12007
|
}
|
|
9238
12008
|
|
|
12009
|
+
if (req.method === "GET" && url.pathname === "/api/model-visibility") {
|
|
12010
|
+
if (!authUser?.isAdmin) {
|
|
12011
|
+
json(res, 403, { error: "Admin permission required" });
|
|
12012
|
+
return;
|
|
12013
|
+
}
|
|
12014
|
+
try {
|
|
12015
|
+
const allModelLists = readModelListsFromDisk(root, { raw: true });
|
|
12016
|
+
const hiddenModels = readHiddenModelConfig();
|
|
12017
|
+
json(res, 200, {
|
|
12018
|
+
allModelLists,
|
|
12019
|
+
modelLists: applyModelVisibility(allModelLists, hiddenModels),
|
|
12020
|
+
hiddenModels,
|
|
12021
|
+
});
|
|
12022
|
+
} catch (e) {
|
|
12023
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
12024
|
+
}
|
|
12025
|
+
return;
|
|
12026
|
+
}
|
|
12027
|
+
|
|
12028
|
+
if (req.method === "POST" && url.pathname === "/api/model-visibility") {
|
|
12029
|
+
if (!authUser?.isAdmin) {
|
|
12030
|
+
json(res, 403, { error: "Admin permission required" });
|
|
12031
|
+
return;
|
|
12032
|
+
}
|
|
12033
|
+
let payload;
|
|
12034
|
+
try {
|
|
12035
|
+
payload = JSON.parse(await readBody(req));
|
|
12036
|
+
} catch {
|
|
12037
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
12038
|
+
return;
|
|
12039
|
+
}
|
|
12040
|
+
try {
|
|
12041
|
+
const hiddenModels = writeHiddenModelConfig(payload?.hiddenModels || {});
|
|
12042
|
+
const allModelLists = readModelListsFromDisk(root, { raw: true });
|
|
12043
|
+
json(res, 200, {
|
|
12044
|
+
success: true,
|
|
12045
|
+
allModelLists,
|
|
12046
|
+
modelLists: applyModelVisibility(allModelLists, hiddenModels),
|
|
12047
|
+
hiddenModels,
|
|
12048
|
+
});
|
|
12049
|
+
} catch (e) {
|
|
12050
|
+
json(res, 500, { error: (e && e.message) || String(e) });
|
|
12051
|
+
}
|
|
12052
|
+
return;
|
|
12053
|
+
}
|
|
12054
|
+
|
|
9239
12055
|
if (req.method === "GET" && url.pathname === "/api/ui-context") {
|
|
9240
12056
|
try {
|
|
9241
12057
|
json(res, 200, { workspaceRoot: root, ...uiConfig });
|