dsh-workflow-map 0.0.0-stage → 0.3.2
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/CHANGELOG.md +98 -0
- package/LICENSE +21 -0
- package/README.en.md +447 -0
- package/README.md +423 -2
- package/client.js +9673 -0
- package/cordis.patch.yml +11 -0
- package/docs/OPEN-SOURCE-GUIDE.md +256 -0
- package/docs/screenshot-detail.png +0 -0
- package/docs/screenshot.png +0 -0
- package/icon.svg +6 -0
- package/index.js +12 -0
- package/lib/index.js +974 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +65 -4
- package/src/client/Branches.js +241 -0
- package/src/client/FlowColumn.js +572 -0
- package/src/client/FlowEffects.js +677 -0
- package/src/client/HotInject.js +628 -0
- package/src/client/MainFlow.js +359 -0
- package/src/client/NodeCard.js +545 -0
- package/src/client/NodeDetail.js +569 -0
- package/src/client/ProgressUnder.js +216 -0
- package/src/client/RightPage.js +285 -0
- package/src/client/Settings.js +212 -0
- package/src/client/StepColumn.js +479 -0
- package/src/client/StepDetail.js +250 -0
- package/src/client/export.js +280 -0
- package/src/client/panel.js +1911 -0
- package/src/client/pulse.js +126 -0
- package/src/client/store.js +1090 -0
- package/src/client/theme.js +1164 -0
- package/src/fold.mjs +2633 -0
- package/src/orchestrator.mjs +426 -0
- package/src/route.js +621 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StepDetail.js - "what did this step deliver": the PAE step report plus the step's own statistics,
|
|
3
|
+
* inline under the selected step card (and a one-line summary in the column header).
|
|
4
|
+
*
|
|
5
|
+
* Two sources, deliberately kept apart:
|
|
6
|
+
* report plan.stepReports[stepIndex] — summary / artifacts / exit_code / status. PAE
|
|
7
|
+
* rewrites this after every step; a step that has not reported yet says so.
|
|
8
|
+
* stats the step's own nodes (tool count, distinct files, first/last clock) — the
|
|
9
|
+
* counts come from the nodes the host actually sent, and when the host's
|
|
10
|
+
* 300-node cap bit, the block says how many nodes it could not count.
|
|
11
|
+
*
|
|
12
|
+
* There is NO "blank" state: a step without a report renders "no report for this step yet".
|
|
13
|
+
*
|
|
14
|
+
* Pure: `h` and `say` are injected. The open/closed state is a plain number (the step whose
|
|
15
|
+
* detail the user closed) so it survives re-renders without React state in the renderer.
|
|
16
|
+
*
|
|
17
|
+
* Inlined into client.js by tools/build-client.mjs (single-file browser half).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Host/PAE report vocabulary → the panel's status chip. */
|
|
21
|
+
var WF_REPORT_STATUS = new Map([
|
|
22
|
+
["success", "done"],
|
|
23
|
+
["succeeded", "done"],
|
|
24
|
+
["completed", "done"],
|
|
25
|
+
["done", "done"],
|
|
26
|
+
["failed", "failed"],
|
|
27
|
+
["failure", "failed"],
|
|
28
|
+
["error", "failed"],
|
|
29
|
+
["skipped", "skipped"],
|
|
30
|
+
["pending", "pending"],
|
|
31
|
+
["running", "running"],
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function wfReportStatusKey(report) {
|
|
35
|
+
var raw = report && typeof report.status === "string" ? report.status.trim().toLowerCase() : "";
|
|
36
|
+
var status = WF_REPORT_STATUS.get(raw) || "pending";
|
|
37
|
+
return status === "failed"
|
|
38
|
+
? "panel.status.failed"
|
|
39
|
+
: status === "done"
|
|
40
|
+
? "panel.status.done"
|
|
41
|
+
: status === "skipped"
|
|
42
|
+
? "panel.status.skipped"
|
|
43
|
+
: status === "running"
|
|
44
|
+
? "panel.status.running"
|
|
45
|
+
: "panel.status.pending";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The detail for a step index is OPEN by default. `closed` remembers the one step whose
|
|
50
|
+
* detail the user folded away (the "i" button / Escape), so selecting another step opens
|
|
51
|
+
* that step's detail without resurrecting the folded one.
|
|
52
|
+
*/
|
|
53
|
+
function wfStepDetailOpen(closed, stepIndex) {
|
|
54
|
+
var closedIndex = wfFinite(closed);
|
|
55
|
+
var index = wfFinite(stepIndex);
|
|
56
|
+
if (closedIndex === null || index === null) return true;
|
|
57
|
+
return Math.round(closedIndex) !== Math.round(index);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** "i" on the selected step: open ↔ closed. */
|
|
61
|
+
function wfStepDetailToggle(closed, stepIndex) {
|
|
62
|
+
return wfStepDetailOpen(closed, stepIndex) ? Math.round(stepIndex) : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** "i" on another step: select it and open its detail. */
|
|
66
|
+
function wfStepDetailOpenFor(stepIndex) {
|
|
67
|
+
return { selectedStep: Math.round(stepIndex), closedStep: null };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The one-line summary pinned in the left column's header. */
|
|
71
|
+
function wfRenderStepHead(h, detail, say) {
|
|
72
|
+
if (!detail) return null;
|
|
73
|
+
return h(
|
|
74
|
+
"span",
|
|
75
|
+
{ className: "wf-col-sub wf-step-head", key: "step-head", "data-step-index": detail.index },
|
|
76
|
+
say("panel.step.head", {
|
|
77
|
+
n: detail.index + 1,
|
|
78
|
+
tools: detail.tools,
|
|
79
|
+
files: detail.files,
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The block under the selected card. `detail` is built by store.js wfBuildStepDetail:
|
|
86
|
+
* {index, planIndex, title, status, statusKey, tools, files, nodes, truncated,
|
|
87
|
+
* startedAt, endedAt, report|null}.
|
|
88
|
+
*/
|
|
89
|
+
function wfRenderStepDetail(h, detail, say, actions, ctx) {
|
|
90
|
+
if (!detail || wfFinite(detail.index) === null) return null;
|
|
91
|
+
var fire = actions || {};
|
|
92
|
+
var report = detail.report && typeof detail.report === "object" ? detail.report : null;
|
|
93
|
+
var artifacts = report && Array.isArray(report.artifacts) ? report.artifacts : [];
|
|
94
|
+
var startedAt = wfFinite(detail.startedAt);
|
|
95
|
+
var endedAt = wfFinite(detail.endedAt);
|
|
96
|
+
var startClock = wfFormatClock(startedAt);
|
|
97
|
+
var endClock = wfFormatClock(endedAt);
|
|
98
|
+
var duration = startedAt !== null && startedAt > 0 && endedAt !== null && endedAt > startedAt ? endedAt - startedAt : null;
|
|
99
|
+
var exitCode = report && wfFinite(report.exitCode) !== null ? Math.round(report.exitCode) : null;
|
|
100
|
+
|
|
101
|
+
var head = h(
|
|
102
|
+
"div",
|
|
103
|
+
{ className: "wf-step-detail-head", key: "head" },
|
|
104
|
+
h("span", { className: "wf-step-detail-title", key: "title" }, say("panel.step.detailtitle", { n: detail.index + 1 })),
|
|
105
|
+
h(
|
|
106
|
+
"span",
|
|
107
|
+
{ className: "wf-step-detail-state", key: "status", "data-status": detail.status },
|
|
108
|
+
say(detail.statusKey || "panel.status.pending")
|
|
109
|
+
),
|
|
110
|
+
h(
|
|
111
|
+
"button",
|
|
112
|
+
{
|
|
113
|
+
type: "button",
|
|
114
|
+
className: "wf-detail-close",
|
|
115
|
+
key: "close",
|
|
116
|
+
"data-wf-step-close": detail.index,
|
|
117
|
+
title: say("panel.step.detailclose"),
|
|
118
|
+
"aria-label": say("panel.step.detailclose"),
|
|
119
|
+
onClick: function () {
|
|
120
|
+
if (typeof fire.onCloseStepDetail === "function") fire.onCloseStepDetail(detail.index);
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
"×"
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
var reportChildren = [
|
|
128
|
+
h(
|
|
129
|
+
"div",
|
|
130
|
+
{ className: "wf-detail-block-head", key: "head" },
|
|
131
|
+
h("span", { className: "wf-detail-label", key: "label" }, say("panel.step.report")),
|
|
132
|
+
report
|
|
133
|
+
? h("span", { className: "wf-detail-chip", key: "state", "data-state": report.status }, say(wfReportStatusKey(report)))
|
|
134
|
+
: null
|
|
135
|
+
),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
if (!report) {
|
|
139
|
+
reportChildren.push(h("div", { className: "wf-detail-empty", key: "empty" }, say("panel.step.noreport")));
|
|
140
|
+
} else {
|
|
141
|
+
reportChildren.push(
|
|
142
|
+
h(
|
|
143
|
+
"div",
|
|
144
|
+
{ className: "wf-detail-summary-text", key: "summary" },
|
|
145
|
+
report.summary === "" ? say("panel.step.nosummary") : report.summary
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
reportChildren.push(
|
|
149
|
+
h(
|
|
150
|
+
"div",
|
|
151
|
+
{ className: "wf-detail-group", key: "artifacts", "data-artifacts": artifacts.length },
|
|
152
|
+
h(
|
|
153
|
+
"span",
|
|
154
|
+
{ className: "wf-detail-sublabel", key: "label" },
|
|
155
|
+
h("span", { className: "wf-detail-glyph", key: "glyph", "aria-hidden": "true" }, "▤"),
|
|
156
|
+
say("panel.step.artifacts") + " · " + artifacts.length
|
|
157
|
+
),
|
|
158
|
+
artifacts.length === 0
|
|
159
|
+
? h("div", { className: "wf-detail-empty", key: "none" }, say("panel.step.noartifacts"))
|
|
160
|
+
: h(
|
|
161
|
+
"div",
|
|
162
|
+
{ className: "wf-detail-rows", key: "rows" },
|
|
163
|
+
artifacts.map(function (artifact, position) {
|
|
164
|
+
var split = wfDetailPath(artifact);
|
|
165
|
+
return h(
|
|
166
|
+
"button",
|
|
167
|
+
{
|
|
168
|
+
type: "button",
|
|
169
|
+
className: "wf-detail-file",
|
|
170
|
+
key: "artifact-" + position,
|
|
171
|
+
"data-wf-copy": split.path,
|
|
172
|
+
"data-wf-copy-label": split.base,
|
|
173
|
+
"data-copy-state": "idle",
|
|
174
|
+
title: say("panel.detail.copypath") + " — " + split.path,
|
|
175
|
+
"aria-label": say("panel.detail.copypath") + " " + split.path,
|
|
176
|
+
onClick: function (event) {
|
|
177
|
+
if (typeof fire.onCopy === "function") {
|
|
178
|
+
fire.onCopy(split.path, event && event.currentTarget ? event.currentTarget : null);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
h("span", { className: "wf-detail-base", key: "base" }, split.base),
|
|
183
|
+
split.dir === "" ? null : h("span", { className: "wf-detail-dir", key: "dir", title: split.dir }, split.dir),
|
|
184
|
+
h("span", { className: "wf-copy-status", key: "status", "aria-live": "polite" }, "")
|
|
185
|
+
);
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
reportChildren.push(
|
|
191
|
+
h(
|
|
192
|
+
"div",
|
|
193
|
+
{ className: "wf-detail-meta", key: "exit" },
|
|
194
|
+
h("span", { key: "exit" }, say("panel.step.exit") + " " + (exitCode === null ? "—" : String(exitCode)))
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
var reportBlock = h(
|
|
200
|
+
"div",
|
|
201
|
+
{ className: "wf-detail-block", key: "report", "data-block": "report" },
|
|
202
|
+
reportChildren
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
var stats = h(
|
|
206
|
+
"div",
|
|
207
|
+
{ className: "wf-step-detail-stats", key: "stats", "data-truncated": detail.truncated > 0 ? "true" : "false" },
|
|
208
|
+
h("span", { key: "tools" }, say("panel.step.tools", { count: detail.tools })),
|
|
209
|
+
h("span", { key: "files" }, say("panel.step.files", { count: detail.files })),
|
|
210
|
+
h("span", { key: "nodes" }, say("panel.step.nodescount", { count: detail.nodes })),
|
|
211
|
+
detail.truncated > 0
|
|
212
|
+
? h("span", { className: "wf-detail-note", key: "trunc" }, say("panel.step.truncatedcount", { count: detail.truncated }))
|
|
213
|
+
: null
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
var timing =
|
|
217
|
+
startClock === "" && endClock === ""
|
|
218
|
+
? h("div", { className: "wf-detail-meta", key: "time", "data-time": "none" }, say("panel.step.unknowntime"))
|
|
219
|
+
: h(
|
|
220
|
+
"div",
|
|
221
|
+
{ className: "wf-detail-meta", key: "time", "data-time": "known" },
|
|
222
|
+
h("span", { key: "start" }, say("panel.detail.started") + " " + (startClock || "—")),
|
|
223
|
+
h("span", { key: "end" }, say("panel.detail.ended") + " " + (endClock || "—")),
|
|
224
|
+
h(
|
|
225
|
+
"span",
|
|
226
|
+
{ key: "duration" },
|
|
227
|
+
say("panel.detail.duration") + " " + (duration === null ? "—" : wfFormatDuration(duration))
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return h(
|
|
232
|
+
"div",
|
|
233
|
+
{
|
|
234
|
+
className: "wf-step-detail",
|
|
235
|
+
key: "step-detail-" + detail.index,
|
|
236
|
+
id: "wf-step-detail-" + detail.index,
|
|
237
|
+
role: "region",
|
|
238
|
+
"data-dsh-workflow-map": "step-detail",
|
|
239
|
+
"data-step-index": detail.index,
|
|
240
|
+
"data-plan-index": detail.planIndex === null ? undefined : detail.planIndex,
|
|
241
|
+
"data-report": report ? "true" : "false",
|
|
242
|
+
"data-status": detail.status,
|
|
243
|
+
"aria-label": say("panel.step.detailaria", { n: detail.index + 1 }),
|
|
244
|
+
},
|
|
245
|
+
head,
|
|
246
|
+
reportBlock,
|
|
247
|
+
stats,
|
|
248
|
+
timing
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export.js — the workflow snapshot (Markdown / JSON) and its two delivery
|
|
3
|
+
* paths (clipboard, Blob download).
|
|
4
|
+
*
|
|
5
|
+
* Source of truth is the SAME wire view the panel renders plus the already-derived
|
|
6
|
+
* model (statuses, selection, the active node). No second interpretation of the
|
|
7
|
+
* session is invented here: step count and node count come from model.steps /
|
|
8
|
+
* view.steps, and a node's title is exactly what NodeCard.js shows.
|
|
9
|
+
*
|
|
10
|
+
* Object URLs are kept in one module list and revoked when the panel unmounts, so a
|
|
11
|
+
* download does not leak a Blob for the lifetime of the page.
|
|
12
|
+
*
|
|
13
|
+
* Pure apart from wfSnapshotDownload (which receives every browser API through
|
|
14
|
+
* `env` in tests). Inlined into client.js by tools/build-client.mjs.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
var WF_SNAPSHOT_SCHEMA = "dsh-workflow-map.snapshot/v1";
|
|
18
|
+
var WF_SNAPSHOT_URLS = [];
|
|
19
|
+
|
|
20
|
+
function wfSnapshotMime(kind) {
|
|
21
|
+
return kind === "json" ? "application/json;charset=utf-8" : "text/markdown;charset=utf-8";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function wfSnapshotExtension(kind) {
|
|
25
|
+
return kind === "json" ? "json" : "md";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Session ids on this harness look like `session-f069ece7-…`. Drop that prefix,
|
|
30
|
+
* then take the first 8 alphanumerics, so the filename is `workflow-f069ece7.md`
|
|
31
|
+
* instead of the useless `workflow-session-.md` a naive slice would produce.
|
|
32
|
+
*/
|
|
33
|
+
function wfSnapshotTag(sessionId) {
|
|
34
|
+
var flat = typeof sessionId === "string" ? sessionId : "";
|
|
35
|
+
flat = flat.replace(/^session-/, "").replace(/[^A-Za-z0-9]/g, "");
|
|
36
|
+
if (flat === "") flat = "session";
|
|
37
|
+
return flat.slice(0, 8);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function wfSnapshotFileName(sessionId, kind) {
|
|
41
|
+
return "workflow-" + wfSnapshotTag(sessionId) + "." + wfSnapshotExtension(kind);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function wfSnapshotClock(ms) {
|
|
45
|
+
var value = wfFinite(ms);
|
|
46
|
+
if (value === null) return "—";
|
|
47
|
+
var date = new Date(value);
|
|
48
|
+
if (!isFinite(date.getTime())) return "—";
|
|
49
|
+
function pad(value2) {
|
|
50
|
+
var text = String(value2);
|
|
51
|
+
return text.length < 2 ? "0" + text : text;
|
|
52
|
+
}
|
|
53
|
+
return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " +
|
|
54
|
+
pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function wfSnapshotIso(ms) {
|
|
58
|
+
var value = wfFinite(ms);
|
|
59
|
+
if (value === null) return "";
|
|
60
|
+
var date = new Date(value);
|
|
61
|
+
return isFinite(date.getTime()) ? date.toISOString() : "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function wfSnapshotFlat(text) {
|
|
65
|
+
return typeof text === "string" ? text.replace(/\s+/g, " ").trim() : "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whitelist one wire node exactly like store.js does for the panel. */
|
|
69
|
+
function wfSnapshotNode(raw, activeNodeId, say) {
|
|
70
|
+
var node = raw && typeof raw === "object" ? raw : {};
|
|
71
|
+
var category = wfNodeCategory(node);
|
|
72
|
+
var parts = wfNodeTitleParts(node, category, say);
|
|
73
|
+
var title = parts.verb === "" ? parts.target : parts.verb + " · " + parts.target;
|
|
74
|
+
var fileList = [];
|
|
75
|
+
if (Array.isArray(node.files)) {
|
|
76
|
+
for (var i = 0; i < node.files.length; i += 1) {
|
|
77
|
+
var file = node.files[i];
|
|
78
|
+
if (!file || typeof file.path !== "string" || file.path === "") continue;
|
|
79
|
+
fileList.push({ path: file.path, action: typeof file.action === "string" ? file.action : "" });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
var errorText = typeof node.error === "string" ? wfSnapshotFlat(node.error) : "";
|
|
83
|
+
var resultHead = wfSnapshotFlat(node.resultHead);
|
|
84
|
+
var status = errorText !== "" ? "error" : wfNodeStatus(node, activeNodeId);
|
|
85
|
+
if (status === "pending" && resultHead !== "") status = "done";
|
|
86
|
+
return {
|
|
87
|
+
id: typeof node.id === "string" ? node.id : "",
|
|
88
|
+
kind: typeof node.kind === "string" ? node.kind : "other",
|
|
89
|
+
tool: typeof node.tool === "string" ? node.tool : "",
|
|
90
|
+
category: category,
|
|
91
|
+
title: title,
|
|
92
|
+
detail: wfSnapshotFlat(node.detail).slice(0, 300),
|
|
93
|
+
// The host already capped argsText at 200 chars; preserve its newlines because a
|
|
94
|
+
// command's shape is part of what the snapshot is for (the panel shows it pre-wrap).
|
|
95
|
+
argsText: typeof node.argsText === "string" ? node.argsText.slice(0, 200) : "",
|
|
96
|
+
status: status,
|
|
97
|
+
seq: wfFinite(node.seq),
|
|
98
|
+
at: wfFinite(node.at),
|
|
99
|
+
durationMs: wfFinite(node.durationMs),
|
|
100
|
+
files: fileList,
|
|
101
|
+
resultHead: resultHead.slice(0, 200),
|
|
102
|
+
error: errorText.slice(0, 200),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Model steps (derived status) + the view's nodeIds, joined node by node. */
|
|
107
|
+
function wfSnapshotSteps(model, view, say) {
|
|
108
|
+
var viewSteps = view && Array.isArray(view.steps) ? view.steps : [];
|
|
109
|
+
var rawNodes = view && Array.isArray(view.nodes) ? view.nodes : [];
|
|
110
|
+
var byId = Object.create(null);
|
|
111
|
+
for (var n = 0; n < rawNodes.length; n += 1) {
|
|
112
|
+
if (rawNodes[n] && typeof rawNodes[n].id === "string") byId[rawNodes[n].id] = rawNodes[n];
|
|
113
|
+
}
|
|
114
|
+
var steps = model && Array.isArray(model.steps) ? model.steps : [];
|
|
115
|
+
var out = [];
|
|
116
|
+
for (var i = 0; i < steps.length; i += 1) {
|
|
117
|
+
var row = steps[i] || {};
|
|
118
|
+
var wireStep = viewSteps[i] && typeof viewSteps[i] === "object" ? viewSteps[i] : {};
|
|
119
|
+
var ids = Array.isArray(wireStep.nodeIds) ? wireStep.nodeIds : [];
|
|
120
|
+
var activeNodeId =
|
|
121
|
+
model && model.selected === row.index && model.flow && typeof model.flow.activeNodeId === "string"
|
|
122
|
+
? model.flow.activeNodeId
|
|
123
|
+
: null;
|
|
124
|
+
var nodes = [];
|
|
125
|
+
for (var k = 0; k < ids.length; k += 1) {
|
|
126
|
+
var found = byId[ids[k]];
|
|
127
|
+
if (found) nodes.push(wfSnapshotNode(found, activeNodeId, say));
|
|
128
|
+
}
|
|
129
|
+
var nodeCount = wfFinite(row.nodeCount) === null ? nodes.length : Math.round(row.nodeCount);
|
|
130
|
+
out.push({
|
|
131
|
+
index: wfFinite(row.index) === null ? i : row.index,
|
|
132
|
+
planIndex: wfFinite(row.planIndex) === null ? null : row.planIndex,
|
|
133
|
+
title: typeof row.title === "string" ? row.title : "—",
|
|
134
|
+
status: typeof row.status === "string" ? row.status : "pending",
|
|
135
|
+
statusLabel: typeof say === "function" ? say(row.statusKey) : String(row.statusKey || row.status || "—"),
|
|
136
|
+
percent: wfFinite(row.percent),
|
|
137
|
+
percentSource: typeof row.percentSource === "string" ? row.percentSource : "none",
|
|
138
|
+
startedAt: wfFinite(row.startedAt),
|
|
139
|
+
endedAt: wfFinite(row.endedAt),
|
|
140
|
+
nodeCount: nodeCount,
|
|
141
|
+
truncated: Math.max(0, nodeCount - nodes.length),
|
|
142
|
+
nodes: nodes,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The JSON body. `schema` is present even when every other field is empty. */
|
|
149
|
+
function wfBuildSnapshot(model, view, sessionId, nowMs, say) {
|
|
150
|
+
var now = wfFinite(nowMs) === null ? Date.now() : nowMs;
|
|
151
|
+
var steps = wfSnapshotSteps(model, view, say);
|
|
152
|
+
return {
|
|
153
|
+
schema: WF_SNAPSHOT_SCHEMA,
|
|
154
|
+
generatedAt: wfSnapshotIso(now),
|
|
155
|
+
generatedAtMs: Math.round(now),
|
|
156
|
+
sessionId: typeof sessionId === "string" ? sessionId : "",
|
|
157
|
+
task: model && typeof model.task === "string" ? model.task : "",
|
|
158
|
+
progress: model && model.progress ? {
|
|
159
|
+
done: model.progress.done,
|
|
160
|
+
total: model.progress.total,
|
|
161
|
+
percent: model.progress.percent,
|
|
162
|
+
} : { done: 0, total: 0, percent: 0 },
|
|
163
|
+
steps: steps,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function wfSnapshotJson(model, view, sessionId, nowMs, say) {
|
|
168
|
+
return JSON.stringify(wfBuildSnapshot(model, view, sessionId, nowMs, say), null, 2);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Markdown mirrors the panel's visible vocabulary: status, progress, node title. */
|
|
172
|
+
function wfSnapshotMarkdown(model, view, sessionId, nowMs, say) {
|
|
173
|
+
var snapshot = wfBuildSnapshot(model, view, sessionId, nowMs, say);
|
|
174
|
+
var lines = [];
|
|
175
|
+
lines.push("# 总任务");
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push(wfSnapshotFlat(snapshot.task) || "—");
|
|
178
|
+
lines.push("");
|
|
179
|
+
lines.push("- 生成时间:" + wfSnapshotClock(snapshot.generatedAtMs));
|
|
180
|
+
lines.push("- 会话:" + (snapshot.sessionId || "—"));
|
|
181
|
+
lines.push(
|
|
182
|
+
"- 进度:" + snapshot.progress.done + "/" + snapshot.progress.total + " 步 · " + snapshot.progress.percent + "%"
|
|
183
|
+
);
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push("## 分步");
|
|
186
|
+
for (var i = 0; i < snapshot.steps.length; i += 1) {
|
|
187
|
+
var step = snapshot.steps[i];
|
|
188
|
+
var percent = wfFinite(step.percent) === null ? "—" : Math.round(step.percent) + "%";
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push("### " + (i + 1) + ". " + wfSnapshotFlat(step.title) + "(" + step.statusLabel + " · " + percent + ")");
|
|
191
|
+
if (step.nodes.length === 0) {
|
|
192
|
+
lines.push("- (无节点)");
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
for (var n = 0; n < step.nodes.length; n += 1) {
|
|
196
|
+
var node = step.nodes[n];
|
|
197
|
+
var tool = node.tool !== "" ? node.tool : node.kind || "—";
|
|
198
|
+
var files = node.files.length + " 文件";
|
|
199
|
+
var duration = wfFormatDuration(node.durationMs) || "—";
|
|
200
|
+
lines.push("- " + tool + " · " + wfSnapshotFlat(node.title) + " · " + files + " · " + duration);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
lines.push("");
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function wfSnapshotText(kind, model, view, sessionId, nowMs, say) {
|
|
208
|
+
return kind === "json"
|
|
209
|
+
? wfSnapshotJson(model, view, sessionId, nowMs, say)
|
|
210
|
+
: wfSnapshotMarkdown(model, view, sessionId, nowMs, say);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Copy one snapshot and paint the 1.5 s acknowledgement on `element` (the same
|
|
215
|
+
* helper the node detail uses, so the two copy paths cannot diverge).
|
|
216
|
+
*/
|
|
217
|
+
function wfCopySnapshot(kind, model, view, sessionId, nowMs, say, element, env) {
|
|
218
|
+
return wfCopyWithFeedback(wfSnapshotText(kind, model, view, sessionId, nowMs, say), element, say, env);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Download through Blob + URL.createObjectURL. Every browser object is injectable
|
|
223
|
+
* so Node can assert the filename and the revoke path without a DOM.
|
|
224
|
+
*/
|
|
225
|
+
function wfSnapshotDownload(kind, text, sessionId, env) {
|
|
226
|
+
var scope = env || {};
|
|
227
|
+
var doc = scope.document || (typeof document !== "undefined" ? document : null);
|
|
228
|
+
var BlobImpl = scope.Blob || (typeof Blob !== "undefined" ? Blob : null);
|
|
229
|
+
var URLImpl = scope.URL || (typeof URL !== "undefined" ? URL : null);
|
|
230
|
+
if (!doc || typeof BlobImpl !== "function" || !URLImpl || typeof URLImpl.createObjectURL !== "function") {
|
|
231
|
+
return { ok: false, reason: "unsupported", filename: "" };
|
|
232
|
+
}
|
|
233
|
+
if (typeof URLImpl.revokeObjectURL !== "function") {
|
|
234
|
+
return { ok: false, reason: "unsupported", filename: "" };
|
|
235
|
+
}
|
|
236
|
+
var filename = wfSnapshotFileName(sessionId, kind);
|
|
237
|
+
var anchor = doc.createElement("a");
|
|
238
|
+
if (!anchor) return { ok: false, reason: "unsupported", filename: filename };
|
|
239
|
+
var url = URLImpl.createObjectURL(new BlobImpl([text], { type: wfSnapshotMime(kind) }));
|
|
240
|
+
WF_SNAPSHOT_URLS.push(url);
|
|
241
|
+
anchor.href = url;
|
|
242
|
+
anchor.download = filename;
|
|
243
|
+
if (anchor.style) anchor.style.display = "none";
|
|
244
|
+
if (doc.body && typeof doc.body.appendChild === "function") doc.body.appendChild(anchor);
|
|
245
|
+
if (typeof anchor.click === "function") anchor.click();
|
|
246
|
+
if (anchor.parentNode && typeof anchor.parentNode.removeChild === "function") anchor.parentNode.removeChild(anchor);
|
|
247
|
+
return { ok: true, reason: "", filename: filename, url: url };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Release every object URL the panel created. Called from the mount cleanup. */
|
|
251
|
+
function wfReleaseSnapshotUrls(env) {
|
|
252
|
+
var scope = env || {};
|
|
253
|
+
var URLImpl = scope.URL || (typeof URL !== "undefined" ? URL : null);
|
|
254
|
+
var revoked = 0;
|
|
255
|
+
for (var i = 0; i < WF_SNAPSHOT_URLS.length; i += 1) {
|
|
256
|
+
if (URLImpl && typeof URLImpl.revokeObjectURL === "function") {
|
|
257
|
+
try {
|
|
258
|
+
URLImpl.revokeObjectURL(WF_SNAPSHOT_URLS[i]);
|
|
259
|
+
revoked += 1;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
/* an already-revoked URL is not an error */
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
WF_SNAPSHOT_URLS = [];
|
|
266
|
+
return revoked;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function wfSnapshotUrlCount() {
|
|
270
|
+
return WF_SNAPSHOT_URLS.length;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The component's mount cleanup: a Blob URL must not outlive the panel. */
|
|
274
|
+
function wfUseSnapshotUrls() {
|
|
275
|
+
React.useEffect(function () {
|
|
276
|
+
return function () {
|
|
277
|
+
wfReleaseSnapshotUrls();
|
|
278
|
+
};
|
|
279
|
+
}, []);
|
|
280
|
+
}
|