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