dsh-knj-workflow 2026.9.21 → 2026.9.70
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/README.md +2 -1
- package/lib/client.js +655 -50
- package/lib/file-preview.test.js +75 -0
- package/lib/graph.js +266 -6
- package/lib/graph.test.js +200 -1
- package/lib/index.d.ts +23 -0
- package/lib/index.js +189 -16
- package/lib/index.test.js +48 -1
- package/lib/orchestrator.js +25 -7
- package/lib/orchestrator.test.js +62 -0
- package/lib/repro-decide-order.test.js +73 -0
- package/lib/repro-paused-runs.test.js +112 -0
- package/lib/ui-regression.test.js +81 -0
- package/package.json +12 -12
package/lib/client.js
CHANGED
|
@@ -20,6 +20,14 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
// workspaces 服务(apply 时注入):用于「新建任务」工作目录下拉
|
|
21
21
|
let _workspaces = null;
|
|
22
22
|
let _ctx = null; // client root ctx(动态取服务,避免 apply 时机问题)
|
|
23
|
+
/** sessions 可能在本插件 apply 之后才被 Cordis 提供;使用时动态重取,避免把初始 null 永久缓存。 */
|
|
24
|
+
function getSessionsService() {
|
|
25
|
+
try {
|
|
26
|
+
const s = _ctx?.get?.('sessions') || _sessions;
|
|
27
|
+
if (s) _sessions = s;
|
|
28
|
+
return s || null;
|
|
29
|
+
} catch { return _sessions || null; }
|
|
30
|
+
}
|
|
23
31
|
|
|
24
32
|
async function api(path, opts = {}) {
|
|
25
33
|
const res = await fetch(`${PREFIX}${path}`, {
|
|
@@ -137,13 +145,201 @@ window.__ModuleLoader__.load({
|
|
|
137
145
|
failed: { label: '失败', color: 'var(--dsw-static-red-500)' },
|
|
138
146
|
skipped: { label: '跳过', color: 'var(--dsw-static-neutral-bluish-400)' },
|
|
139
147
|
};
|
|
148
|
+
/** 运行效果图专用状态色(含展示态 passed/waiting);色值为静态语义色,深浅主题通用 */
|
|
149
|
+
const RUN_STATUS = {
|
|
150
|
+
pending: { label: '待执行', color: '#8b95a5', deep: '#8b95a5' },
|
|
151
|
+
running: { label: '运行中', color: '#2f6bff', deep: '#2556d6' },
|
|
152
|
+
done: { label: '完成', color: '#16a34a', deep: '#15803d' },
|
|
153
|
+
failed: { label: '失败', color: '#dc2626', deep: '#b91c1c' },
|
|
154
|
+
skipped: { label: '跳过', color: '#9aa3af', deep: '#9aa3af' },
|
|
155
|
+
waiting: { label: '人工待审', color: '#7c5cff', deep: '#5b3fd4' },
|
|
156
|
+
passed: { label: '已执行', color: '#16a34a', deep: '#15803d' },
|
|
157
|
+
};
|
|
158
|
+
/** 运行效果图:从一次任务实例推导每个画布节点的运行态(镜像 graph.js deriveRunGraphStates,两处需同步修改)。
|
|
159
|
+
* stageStates 只覆盖 task 执行节点;start/human/gateway/end 缺失时按任务级状态补全。返回 { nodeId: status }。 */
|
|
160
|
+
function knjDeriveRunStates(task) {
|
|
161
|
+
const wf = task?.workflowSnapshot;
|
|
162
|
+
const nodes = Array.isArray(wf?.nodes) ? wf.nodes : [];
|
|
163
|
+
const edges = Array.isArray(wf?.edges) ? wf.edges : [];
|
|
164
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
165
|
+
const stages = Array.isArray(task?.stageStates) ? task.stageStates : [];
|
|
166
|
+
const stageOf = new Map(stages.map((s) => [s.id, s]));
|
|
167
|
+
const taskStatus = task?.status;
|
|
168
|
+
const humanWaiting = taskStatus === 'waiting-human' ? task?.humanState?.humanId : null;
|
|
169
|
+
const executed = new Set();
|
|
170
|
+
for (const s of stages) if (['done', 'failed', 'running', 'skipped'].includes(s.status)) executed.add(s.id);
|
|
171
|
+
if (humanWaiting) executed.add(humanWaiting);
|
|
172
|
+
const succ = new Map(nodes.map((n) => [n.id, []]));
|
|
173
|
+
for (const e of edges) if (e?.from && byId.has(e.from) && e.to && byId.has(e.to)) succ.get(e.from).push(e.to);
|
|
174
|
+
for (const n of nodes) {
|
|
175
|
+
if (n.type !== 'human') continue;
|
|
176
|
+
for (const r of humanRoutes(n)) if (r?.to && byId.has(r.to)) succ.get(n.id).push(r.to);
|
|
177
|
+
}
|
|
178
|
+
const out = {};
|
|
179
|
+
for (const n of nodes) {
|
|
180
|
+
const id = n.id;
|
|
181
|
+
if (stageOf.has(id)) { out[id] = stageOf.get(id).status; continue; }
|
|
182
|
+
if (n.type === 'start') { out[id] = taskStatus && taskStatus !== 'pending' ? 'passed' : 'pending'; continue; }
|
|
183
|
+
if (n.type === 'end') { out[id] = taskStatus === 'success' ? 'done' : 'pending'; continue; }
|
|
184
|
+
if (n.type === 'human') { out[id] = id === humanWaiting ? 'waiting' : taskStatus === 'success' ? 'done' : 'pending'; continue; }
|
|
185
|
+
if (n.type === 'gateway-xor' || n.type === 'gateway-and') {
|
|
186
|
+
out[id] = (succ.get(id) || []).some((to) => executed.has(to)) ? 'passed' : 'pending';
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
out[id] = 'pending';
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 运行效果图节点详情:错误框样式 */
|
|
195
|
+
function detailErr() {
|
|
196
|
+
return { background: 'rgba(220,38,38,.07)', border: '1px solid rgba(220,38,38,.3)', color: '#b91c1c', borderRadius: 6, padding: '6px 8px', fontSize: 12, wordBreak: 'break-all' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 入边槽位:同一目标有多条入边时,沿目标边界分配不同落点,保证箭头不重叠。 */
|
|
200
|
+
function incomingPortSlot(wf, targetId, sourceId) {
|
|
201
|
+
const ids = [];
|
|
202
|
+
for (const e of (wf?.edges || [])) if (e?.to === targetId && e?.from) ids.push(e.from);
|
|
203
|
+
for (const n of (wf?.nodes || [])) if (n.type === 'human') for (const r of humanRoutes(n)) if (r?.to === targetId) ids.push(n.id);
|
|
204
|
+
const unique = [...new Set(ids)];
|
|
205
|
+
const index = Math.max(0, unique.indexOf(sourceId));
|
|
206
|
+
return { slot: index, count: Math.max(1, unique.length) };
|
|
207
|
+
}
|
|
208
|
+
/** 连线进出端口:按相对角度选择左右/上下边,并按入边槽位分配落点。 */
|
|
209
|
+
function flowPorts(f, t, slot = 0, count = 1) {
|
|
210
|
+
const hw = (n) => (n.type === 'gateway-xor' || n.type === 'gateway-and' || n.type === 'start' || n.type === 'end' ? 30 : 60) + 1;
|
|
211
|
+
const hh = (n) => (n.type === 'gateway-xor' || n.type === 'gateway-and' ? 30 : (n.type === 'start' || n.type === 'end' ? 18 : 22)) + 1;
|
|
212
|
+
const dx = (t.x || 0) - (f.x || 0), dy = (t.y || 0) - (f.y || 0);
|
|
213
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
214
|
+
const spread = (slot - (count - 1) / 2) * 12;
|
|
215
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
216
|
+
const right = dx >= 0;
|
|
217
|
+
const sy = f.y + clamp(dy * 0.18 + spread * 0.2, -hh(f) + 5, hh(f) - 5);
|
|
218
|
+
const ty = t.y + clamp(dy * 0.12 + spread, -hh(t) + 5, hh(t) - 5);
|
|
219
|
+
return { sx: f.x + (right ? hw(f) : -hw(f)), sy, tx: t.x - (right ? hw(t) : -hw(t)) - (right ? 6 : -6), ty };
|
|
220
|
+
}
|
|
221
|
+
const down = dy >= 0;
|
|
222
|
+
const sx = f.x + clamp(dx * 0.18 + spread * 0.2, -hw(f) + 5, hw(f) - 5);
|
|
223
|
+
const tx = t.x + clamp(dx * 0.12 + spread, -hw(t) + 5, hw(t) - 5);
|
|
224
|
+
return { sx, sy: f.y + (down ? hh(f) : -hh(f)), tx, ty: t.y - (down ? hh(t) : -hh(t)) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 连线避让的障碍卡片:给定画布全部节点(含端点),产出 routeEdge 的 obstacles(除端点外的卡,含留白)。 */
|
|
228
|
+
function flowObstacles(nodes, fromId, toId) {
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const n of nodes) {
|
|
231
|
+
if (n.id === fromId || n.id === toId) continue;
|
|
232
|
+
const t = n.type || 'task';
|
|
233
|
+
const hx = (t === 'gateway-xor' || t === 'gateway-and' || t === 'start' || t === 'end' ? 30 : 60) + 12;
|
|
234
|
+
const hy = (t === 'gateway-xor' || t === 'gateway-and') ? 36 : (t === 'start' || t === 'end' ? 26 : 30);
|
|
235
|
+
out.push({ x: n.x, y: n.y, hx, hy });
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 连线避让骨架(决策层):镜像 graph.js routeEdgeKnee(两处需同步)。直通→2 点;3 段竖列避卡;
|
|
241
|
+
* 走廊被同层卡堵死 → V-H-V 上下包抄。返回 { pts, midX, midY }。 */
|
|
242
|
+
function routeEdgeKnee(sx, sy, tx, ty, obstacles = []) {
|
|
243
|
+
const clearH = (y, x0, x1) => {
|
|
244
|
+
const a = Math.min(x0, x1) + 3, b = Math.max(x0, x1) - 3;
|
|
245
|
+
if (b - a < 8) return true;
|
|
246
|
+
return !obstacles.some((o) => y >= o.y - o.hy && y <= o.y + o.hy && o.x - o.hx < b && o.x + o.hx > a);
|
|
247
|
+
};
|
|
248
|
+
const clearV = (x, y0, y1) => {
|
|
249
|
+
const a = Math.min(y0, y1), b = Math.max(y0, y1);
|
|
250
|
+
return !obstacles.some((o) => o.x - o.hx < x && o.x + o.hx > x && o.y - o.hy < b && o.y + o.hy > a);
|
|
251
|
+
};
|
|
252
|
+
const x0 = Math.min(sx, tx), x1 = Math.max(sx, tx);
|
|
253
|
+
if (Math.abs(sy - ty) < 4 && clearH(sy, sx, tx)) return { pts: [[sx, sy], [tx, ty]], midX: (sx + tx) / 2, midY: sy };
|
|
254
|
+
for (let x = x1 - 46; x >= x0 + 46; x -= 10) {
|
|
255
|
+
if (!clearV(x, sy, ty)) continue;
|
|
256
|
+
if (!clearH(sy, sx, x)) continue;
|
|
257
|
+
if (!clearH(ty, x, tx)) continue;
|
|
258
|
+
return { pts: [[sx, sy], [x, sy], [x, ty], [tx, ty]], midX: (x + tx) / 2, midY: ty };
|
|
259
|
+
}
|
|
260
|
+
const gaps = [];
|
|
261
|
+
for (let dy = 60; dy <= 150; dy += 30) gaps.push(sy - dy, sy + dy);
|
|
262
|
+
for (const y2 of gaps) {
|
|
263
|
+
if (!clearH(y2, sx, tx)) continue;
|
|
264
|
+
if (!clearV(sx, sy, y2)) continue;
|
|
265
|
+
if (!clearV(tx, y2, ty)) continue;
|
|
266
|
+
return { pts: [[sx, sy], [sx, y2], [tx, y2], [tx, ty]], midX: (sx + tx) / 2, midY: y2 };
|
|
267
|
+
}
|
|
268
|
+
const X = Math.round((x0 + x1) / 2);
|
|
269
|
+
return { pts: [[sx, sy], [X, sy], [X, ty], [tx, ty]], midX: (X + tx) / 2, midY: ty };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 智能平滑曲线(镜像 graph.js smoothCurvePath):骨架点 Catmull-Rom 三次贝塞尔穿过,端口水平进出 */
|
|
273
|
+
function smoothCurvePath(pts, extend = 26) {
|
|
274
|
+
if (!pts || pts.length < 2) return '';
|
|
275
|
+
if (pts.length === 2) return `M ${pts[0][0]} ${pts[0][1]} L ${pts[1][0]} ${pts[1][1]}`;
|
|
276
|
+
const p0 = pts[0], pn = pts[pts.length - 1];
|
|
277
|
+
const all = [[p0[0] - extend, p0[1]], ...pts, [pn[0] + extend, pn[1]]];
|
|
278
|
+
let d = `M ${p0[0]} ${p0[1]}`;
|
|
279
|
+
for (let i = 0; i < all.length - 3; i++) {
|
|
280
|
+
const A = all[i], B = all[i + 1], C2 = all[i + 2], D = all[i + 3];
|
|
281
|
+
const c1x = B[0] + (C2[0] - A[0]) / 6, c1y = B[1] + (C2[1] - A[1]) / 6;
|
|
282
|
+
const c2x = C2[0] - (D[0] - B[0]) / 6, c2y = C2[1] - (D[1] - B[1]) / 6;
|
|
283
|
+
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${C2[0]} ${C2[1]}`;
|
|
284
|
+
}
|
|
285
|
+
return d;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** 手工箭头形状:绕开 SVG marker 渲染可靠性问题。在终点 (ax,ay) 沿方向 (dx,dy) 画三角(L 长 H 高)。 */
|
|
289
|
+
function arrowShape(ax, ay, dx, dy, L, H) {
|
|
290
|
+
const px = -dy, py = dx;
|
|
291
|
+
const x1 = ax - dx * L + px * H / 2, y1 = ay - dy * L + py * H / 2;
|
|
292
|
+
const x2 = ax - dx * L - px * H / 2, y2 = ay - dy * L - py * H / 2;
|
|
293
|
+
return `M ${ax} ${ay} L ${x1} ${y1} L ${x2} ${y2} Z`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 直连优先路由(镜像 graph.js routeEdge):能直连(采样确认不碰卡)→ 同层横线或第一版优雅 S 贝塞尔;
|
|
297
|
+
* 真被卡片阻挡 → 避让骨架 routeEdgeKnee + 平滑样条 smoothCurvePath。返回 { d, midX, midY, ax, ay, dx, dy }。 */
|
|
298
|
+
function routeEdge(sx, sy, tx, ty, obstacles = [], arrowL = 0) {
|
|
299
|
+
let hit = false;
|
|
300
|
+
if (obstacles.length) {
|
|
301
|
+
const horizontal = Math.abs(tx - sx) >= Math.abs(ty - sy);
|
|
302
|
+
const mx = (sx + tx) / 2, my = (sy + ty) / 2;
|
|
303
|
+
for (let k = 0; k <= 20; k++) {
|
|
304
|
+
const t = k / 20, u = 1 - t;
|
|
305
|
+
const x = horizontal ? u * u * u * sx + 3 * u * u * t * mx + 3 * u * t * t * mx + t * t * t * tx : u * u * u * sx + 3 * u * u * t * sx + 3 * u * t * t * tx + t * t * t * tx;
|
|
306
|
+
const y = horizontal ? u * u * u * sy + 3 * u * u * t * sy + 3 * u * t * t * ty + t * t * t * ty : u * u * u * sy + 3 * u * u * t * my + 3 * u * t * t * my + t * t * t * ty;
|
|
307
|
+
for (const o of obstacles) if (x >= o.x - o.hx && x <= o.x + o.hx && y >= o.y - o.hy && y <= o.y + o.hy) { hit = true; break; }
|
|
308
|
+
if (hit) break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!hit) {
|
|
312
|
+
if (Math.abs(sy - ty) < 4) {
|
|
313
|
+
const s = Math.sign(tx - sx) || 1, ex = tx - s * arrowL;
|
|
314
|
+
return { d: `M ${sx} ${sy} H ${ex}`, midX: (sx + ex) / 2, midY: sy, ax: tx, ay: ty, dx: s, dy: 0 };
|
|
315
|
+
}
|
|
316
|
+
const ddx = tx - sx, ddy = ty - sy, len = Math.hypot(ddx, ddy) || 1;
|
|
317
|
+
const ex = tx - ddx / len * arrowL, ey = ty - ddy / len * arrowL;
|
|
318
|
+
if (Math.abs(ddx) >= Math.abs(ddy)) {
|
|
319
|
+
const mx = (sx + ex) / 2;
|
|
320
|
+
return { d: `M ${sx} ${sy} C ${mx} ${sy}, ${mx} ${ey}, ${ex} ${ey}`, midX: mx, midY: (sy + ey) / 2, ax: tx, ay: ty, dx: ddx / len, dy: ddy / len };
|
|
321
|
+
}
|
|
322
|
+
const my = (sy + ey) / 2;
|
|
323
|
+
return { d: `M ${sx} ${sy} C ${sx} ${my}, ${ex} ${my}, ${ex} ${ey}`, midX: (sx + ex) / 2, midY: my, ax: tx, ay: ty, dx: ddx / len, dy: ddy / len };
|
|
324
|
+
}
|
|
325
|
+
const knee = routeEdgeKnee(sx, sy, tx, ty, obstacles);
|
|
326
|
+
const last = knee.pts[knee.pts.length - 1], prev = knee.pts[knee.pts.length - 2];
|
|
327
|
+
const ddx = last[0] - prev[0], ddy = last[1] - prev[1], len = Math.hypot(ddx, ddy) || 1;
|
|
328
|
+
const ex = last[0] - ddx / len * arrowL, ey = last[1] - ddy / len * arrowL;
|
|
329
|
+
if (knee.pts.length === 2) {
|
|
330
|
+
const s = Math.sign(ddx) || 1;
|
|
331
|
+
return { d: `M ${prev[0]} ${prev[1]} H ${ex}`, midX: knee.midX, midY: knee.midY, ax: tx, ay: ty, dx: s, dy: 0 };
|
|
332
|
+
}
|
|
333
|
+
const tailPts = knee.pts.map((p, i) => i === knee.pts.length - 1 ? [ex, ey] : p);
|
|
334
|
+
return { d: smoothCurvePath(tailPts), midX: knee.midX, midY: knee.midY, ax: tx, ay: ty, dx: ddx / len, dy: ddy / len };
|
|
335
|
+
}
|
|
140
336
|
|
|
141
337
|
const css = `
|
|
142
338
|
/* dsh-knj-workflow 样式:跟随 DSH 原生设计令牌(--dsw-*)与字号阶梯
|
|
143
339
|
原生字号体系:标题 16/500 · 正文 14/400(lh22) · 辅助 12/400(lh18)
|
|
144
340
|
原生按钮:36px 高胶囊 · 输入:10px 圆角 + border-l2 + bg-layer-1 */
|
|
145
341
|
.knj-overlay{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-2);z-index:9999;display:flex;align-items:center;justify-content:center}
|
|
146
|
-
.knj-panel{background:var(--dsw-specific-menu);border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;width:min(
|
|
342
|
+
.knj-panel{background:var(--dsw-specific-menu);border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;width:min(1280px,96vw);max-height:92vh;display:flex;flex-direction:column;box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary)}
|
|
147
343
|
.knj-head{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
148
344
|
.knj-head h2{margin:0;font-size:16px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
149
345
|
.knj-close{margin-left:auto;background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;height:32px;padding:0 14px;cursor:pointer;font-size:14px;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center}
|
|
@@ -342,6 +538,17 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
342
538
|
react_dom_client.createRoot(overlayRoot).render(react.createElement(Overlay, { initialTab: tab || 'tasks' }));
|
|
343
539
|
}
|
|
344
540
|
|
|
541
|
+
async function openScheduledWorkflowTask(taskId) {
|
|
542
|
+
openDevBoard();
|
|
543
|
+
try {
|
|
544
|
+
const { task } = await api('/tasks/' + taskId);
|
|
545
|
+
if (devBoardRef) devBoardRef.openTaskDetail(task);
|
|
546
|
+
else pendingScheduledWorkflowTask = task;
|
|
547
|
+
} catch (e) {
|
|
548
|
+
toast(e.message);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
345
552
|
class Overlay extends react.Component {
|
|
346
553
|
constructor(props) {
|
|
347
554
|
super(props);
|
|
@@ -1121,16 +1328,16 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
1121
1328
|
const maxX = Math.max(700, ...xs) + 200, maxY = Math.max(420, ...ys) + 200;
|
|
1122
1329
|
return react.createElement('svg', { className: 'knj-ge-svg', width: maxX * this.state.zoom, height: maxY * this.state.zoom },
|
|
1123
1330
|
react.createElement('defs', null,
|
|
1124
|
-
react.createElement('marker', { id: 'knj-arrow', viewBox: '0 0
|
|
1125
|
-
react.createElement('path', { d: 'M0
|
|
1126
|
-
react.createElement('marker', { id: 'knj-arrow-red', viewBox: '0 0
|
|
1127
|
-
react.createElement('path', { d: 'M0
|
|
1128
|
-
react.createElement('marker', { id: 'knj-arrow-green', viewBox: '0 0
|
|
1129
|
-
react.createElement('path', { d: 'M0
|
|
1130
|
-
react.createElement('marker', { id: 'knj-arrow-blue', viewBox: '0 0
|
|
1131
|
-
react.createElement('path', { d: 'M0
|
|
1132
|
-
react.createElement('marker', { id: 'knj-arrow-orange', viewBox: '0 0
|
|
1133
|
-
react.createElement('path', { d: 'M0
|
|
1331
|
+
react.createElement('marker', { id: 'knj-arrow', viewBox: '0 0 12 7', refX: 11, refY: 3.5, markerWidth: 12, orient: 'auto' },
|
|
1332
|
+
react.createElement('path', { d: 'M0 0L12 3.5L0 7z', fill: '#5b6472' })),
|
|
1333
|
+
react.createElement('marker', { id: 'knj-arrow-red', viewBox: '0 0 12 7', refX: 11, refY: 3.5, markerWidth: 12, orient: 'auto' },
|
|
1334
|
+
react.createElement('path', { d: 'M0 0L12 3.5L0 7z', fill: '#dc2626' })),
|
|
1335
|
+
react.createElement('marker', { id: 'knj-arrow-green', viewBox: '0 0 12 7', refX: 11, refY: 3.5, markerWidth: 12, orient: 'auto' },
|
|
1336
|
+
react.createElement('path', { d: 'M0 0L12 3.5L0 7z', fill: '#16a34a' })),
|
|
1337
|
+
react.createElement('marker', { id: 'knj-arrow-blue', viewBox: '0 0 12 7', refX: 11, refY: 3.5, markerWidth: 12, orient: 'auto' },
|
|
1338
|
+
react.createElement('path', { d: 'M0 0L12 3.5L0 7z', fill: '#2f6bff' })),
|
|
1339
|
+
react.createElement('marker', { id: 'knj-arrow-orange', viewBox: '0 0 12 7', refX: 11, refY: 3.5, markerWidth: 12, orient: 'auto' },
|
|
1340
|
+
react.createElement('path', { d: 'M0 0L12 3.5L0 7z', fill: '#d97706' })),
|
|
1134
1341
|
),
|
|
1135
1342
|
// 空白区域:按下拖拽平移画布
|
|
1136
1343
|
react.createElement('rect', { x: 0, y: 0, width: maxX, height: maxY, fill: 'transparent', onMouseDown: (e) => this.startPan(e), style: { cursor: 'grab' } }),
|
|
@@ -1161,17 +1368,20 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
1161
1368
|
const key = e.from + '->' + e.to;
|
|
1162
1369
|
const isSel = this.state.selectedEdge === key;
|
|
1163
1370
|
const cyc = this.isUpstream(e.to, e.from); // 环边:to 可达 from(如人工驳回后重跑链路)
|
|
1164
|
-
const
|
|
1165
|
-
const
|
|
1166
|
-
const
|
|
1371
|
+
const incoming = incomingPortSlot(wf, e.to, e.from);
|
|
1372
|
+
const { sx, sy, tx, ty } = flowPorts(from, to, incoming.slot, incoming.count);
|
|
1373
|
+
const route = routeEdge(sx, sy, tx, ty, flowObstacles(wf.nodes, e.from, e.to), 16);
|
|
1374
|
+
const mx = route.midX, my = route.midY; // 折线标签位置(目标侧水平段中点)
|
|
1375
|
+
const d = route.d;
|
|
1167
1376
|
const pick = (ev) => { ev.stopPropagation(); this.setState({ selectedEdge: key, selectedId: null }); };
|
|
1168
1377
|
return react.createElement('g', { key },
|
|
1169
1378
|
// 透明点击区域(加宽,便于选边)
|
|
1170
1379
|
react.createElement('path', { d, fill: 'none', stroke: 'transparent', strokeWidth: 16, onClick: pick, style: { cursor: 'pointer' } }),
|
|
1171
|
-
//
|
|
1172
|
-
react.createElement('path', { d, fill: 'none', stroke: cyc ? '#dc2626' : (isSel ? '#2f6bff' : '#5b6472'), strokeWidth: isSel ? 3 : 2, ...(cyc ? { strokeDasharray: '5 3' } : {})
|
|
1380
|
+
// 可见线(箭头手工绘制,见下)
|
|
1381
|
+
react.createElement('path', { d, fill: 'none', stroke: cyc ? '#dc2626' : (isSel ? '#2f6bff' : '#5b6472'), strokeWidth: isSel ? 3 : 2, ...(cyc ? { strokeDasharray: '5 3' } : {}) }),
|
|
1382
|
+
react.createElement('path', { d: arrowShape(route.ax, route.ay, route.dx, route.dy, 16, 10), fill: cyc ? '#dc2626' : (isSel ? '#2f6bff' : '#5b6472') }),
|
|
1173
1383
|
(e.when || e.default) ? react.createElement('g', { onClick: pick, style: { cursor: 'pointer' } },
|
|
1174
|
-
react.createElement('rect', { x: mx - 44, y: my - 11, width: 88, height: 18, rx: 4, fill: '
|
|
1384
|
+
react.createElement('rect', { x: mx - 44, y: my - 11, width: 88, height: 18, rx: 4, style: { fill: 'var(--dsw-alias-bg-base)' }, stroke: e.default ? '#16a34a' : (isSel ? '#2f6bff' : '#e2e6ec') }),
|
|
1175
1385
|
react.createElement('text', { x: mx, y: my + 1, fontSize: 10, textAnchor: 'middle', fill: e.default ? '#16a34a' : '#5b6472' },
|
|
1176
1386
|
e.default && e.when ? `default · ${e.when.field} ${e.when.op} ${JSON.stringify(e.when.value)}` : (e.default ? 'default' : (e.when ? `${e.when.field} ${e.when.op} ${JSON.stringify(e.when.value)}` : ''))),
|
|
1177
1387
|
) : null,
|
|
@@ -1195,18 +1405,21 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
1195
1405
|
const color = routeColor(he.route);
|
|
1196
1406
|
const key = 'human:' + he.from + ':' + he.index;
|
|
1197
1407
|
const isSel = this.state.selectedEdge === key;
|
|
1198
|
-
const
|
|
1199
|
-
const
|
|
1200
|
-
const
|
|
1408
|
+
const incoming = incomingPortSlot(wf, he.to, he.from);
|
|
1409
|
+
const { sx, sy, tx, ty } = flowPorts(from, to, incoming.slot, incoming.count);
|
|
1410
|
+
const route = routeEdge(sx, sy, tx, ty, flowObstacles(wf.nodes, he.from, he.to), 16);
|
|
1411
|
+
const mx = route.midX, my = route.midY;
|
|
1412
|
+
const d = route.d;
|
|
1201
1413
|
const pick = (ev) => { ev.stopPropagation(); this.setState({ selectedEdge: key, selectedId: null }); };
|
|
1202
1414
|
const label = he.route.label || '去向';
|
|
1203
1415
|
const markerEnd = ({ success: 'url(#knj-arrow-green)', danger: 'url(#knj-arrow-red)', warning: 'url(#knj-arrow-orange)', info: 'url(#knj-arrow-blue)', neutral: 'url(#knj-arrow)' })[he.route.tone] || 'url(#knj-arrow-blue)';
|
|
1204
1416
|
return react.createElement('g', { key },
|
|
1205
1417
|
// 透明点击区域(加宽,便于选中)
|
|
1206
1418
|
react.createElement('path', { d, fill: 'none', stroke: 'transparent', strokeWidth: 16, onClick: pick, style: { cursor: 'pointer' } }),
|
|
1207
|
-
react.createElement('path', { d, fill: 'none', stroke: color, strokeWidth: isSel ? 3 : 2, strokeDasharray: '5 4'
|
|
1419
|
+
react.createElement('path', { d, fill: 'none', stroke: color, strokeWidth: isSel ? 3 : 2, strokeDasharray: '5 4' }),
|
|
1420
|
+
react.createElement('path', { d: arrowShape(route.ax, route.ay, route.dx, route.dy, 16, 10), fill: color }),
|
|
1208
1421
|
react.createElement('g', { onClick: pick, style: { cursor: 'pointer' } },
|
|
1209
|
-
react.createElement('rect', { x: mx - 26, y: my - 11, width: 52, height: 16, rx: 3, fill: '
|
|
1422
|
+
react.createElement('rect', { x: mx - 26, y: my - 11, width: 52, height: 16, rx: 3, style: { fill: 'var(--dsw-alias-bg-base)' }, stroke: isSel ? '#2f6bff' : color, strokeWidth: isSel ? 2 : 1 }),
|
|
1210
1423
|
react.createElement('text', { x: mx, y: my + 1, fontSize: 10, textAnchor: 'middle', fill: color }, label.length > 5 ? label.slice(0, 5) + '…' : label),
|
|
1211
1424
|
),
|
|
1212
1425
|
);
|
|
@@ -1220,26 +1433,28 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
1220
1433
|
if (n.type === 'gateway-xor' || n.type === 'gateway-and') {
|
|
1221
1434
|
const half = 30;
|
|
1222
1435
|
return react.createElement('g', null,
|
|
1223
|
-
react.createElement('path', { d: `M ${n.x} ${n.y - half} L ${n.x + half} ${n.y} L ${n.x} ${n.y + half} L ${n.x - half} ${n.y} Z`, fill: '
|
|
1436
|
+
react.createElement('path', { d: `M ${n.x} ${n.y - half} L ${n.x + half} ${n.y} L ${n.x} ${n.y + half} L ${n.x - half} ${n.y} Z`, style: { fill: 'var(--dsw-alias-bg-base)', filter: 'drop-shadow(0 1px 1px rgba(16,20,28,.06))' }, stroke: isSel ? '#2f6bff' : c, strokeWidth: isSel ? 2.6 : 1.6 }),
|
|
1224
1437
|
react.createElement('path', { d: n.type === 'gateway-xor' ? `M ${n.x - 9} ${n.y - 9} h18 M ${n.x - 9} ${n.y + 9} h18` : `M ${n.x - 9} ${n.y} h18`, stroke: c, strokeWidth: 1.3 }),
|
|
1225
1438
|
);
|
|
1226
1439
|
}
|
|
1227
1440
|
const w = 120, h = 44;
|
|
1228
|
-
if (n.type === '
|
|
1229
|
-
|
|
1441
|
+
if (n.type === 'task' || n.type === 'human') {
|
|
1442
|
+
const bodyRect = react.createElement('rect', { x: n.x - w / 2, y: n.y - h / 2, width: w, height: h, rx: 9, style: { fill: n.type === 'human' ? 'color-mix(in srgb, #7c5cff 10%, var(--dsw-alias-bg-base))' : 'var(--dsw-alias-bg-base)', filter: 'drop-shadow(0 1px 1px rgba(16,20,28,.06)) drop-shadow(0 3px 8px rgba(16,20,28,.05))' }, stroke: isSel ? '#2f6bff' : (needTarget ? '#dc2626' : c), strokeWidth: isSel ? 2.6 : (needTarget ? 2 : 1.6) });
|
|
1443
|
+
const typeBar = react.createElement('rect', { x: n.x - w / 2 + 1, y: n.y - h / 2 + 1, width: 3.5, height: h - 2, rx: 1.75, fill: n.type === 'human' ? '#7c5cff' : c, opacity: needTarget ? 0.9 : 0.85 });
|
|
1444
|
+
return [bodyRect, typeBar];
|
|
1230
1445
|
}
|
|
1231
|
-
return react.createElement('rect', { x: n.x -
|
|
1446
|
+
return react.createElement('rect', { x: n.x - 30, y: n.y - 18, width: 60, height: 36, rx: 18, style: { fill: 'var(--dsw-alias-bg-base)', filter: 'drop-shadow(0 1px 1px rgba(16,20,28,.06))' }, stroke: isSel ? '#2f6bff' : c, strokeWidth: isSel ? 2.4 : 1.5 });
|
|
1232
1447
|
})();
|
|
1233
1448
|
const label = (() => {
|
|
1234
1449
|
if (n.type === 'gateway-xor' || n.type === 'gateway-and') return null;
|
|
1235
1450
|
const txt = n.type === 'start' ? '开始' : n.type === 'end' ? '结束' : (n.title || n.id);
|
|
1236
|
-
return react.createElement('text', { x: n.x, y: n.y + 4, fontSize: 12, textAnchor: 'middle', fill: '
|
|
1451
|
+
return react.createElement('text', { x: n.x, y: n.y + 4, fontSize: 12.5, fontWeight: 600, textAnchor: 'middle', style: { fill: 'var(--dsw-alias-label-primary)' } }, txt.length > 10 ? txt.slice(0, 10) + '…' : txt);
|
|
1237
1452
|
})();
|
|
1238
1453
|
const isHover = this.state.linking && this.state.hoverId === n.id;
|
|
1239
|
-
// 节点类型图标(左上角):task 齿轮 / human
|
|
1454
|
+
// 节点类型图标(左上角):task 齿轮 / human 小人(用节点描边色;14px 与运行图同语言)
|
|
1240
1455
|
const typeIcon = (n.type === 'task' || n.type === 'human')
|
|
1241
|
-
? react.createElement('g', { transform: `translate(${n.x - 52}, ${n.y -
|
|
1242
|
-
react.createElement('svg', { width:
|
|
1456
|
+
? react.createElement('g', { transform: `translate(${n.x - 52}, ${n.y - 15})` },
|
|
1457
|
+
react.createElement('svg', { width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none' },
|
|
1243
1458
|
n.type === 'human'
|
|
1244
1459
|
? [
|
|
1245
1460
|
react.createElement('circle', { key: 'h1', cx: 8, cy: 5.5, r: 3, stroke: c, strokeWidth: 1.6 }),
|
|
@@ -1253,7 +1468,7 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
1253
1468
|
)
|
|
1254
1469
|
: null;
|
|
1255
1470
|
return react.createElement('g', { key: n.id, onMouseDown: (e) => this.startDrag(n.id, e), onClick: (e) => { e.stopPropagation(); this.setState({ selectedId: n.id, selectedEdge: null }); }, style: { cursor: 'move' } },
|
|
1256
|
-
react.createElement('rect', { x: n.x -
|
|
1471
|
+
react.createElement('rect', { x: n.x - 70, y: n.y - 30, width: 140, height: 60, fill: isHover ? 'rgba(47,107,255,0.06)' : 'transparent', stroke: 'none', rx: 10 }),
|
|
1257
1472
|
body,
|
|
1258
1473
|
label,
|
|
1259
1474
|
typeIcon,
|
|
@@ -1501,8 +1716,8 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
1501
1716
|
this.renderHookSection(node, 'posthook', 'posthook(执行后动作)'),
|
|
1502
1717
|
) : node.type === 'human' ? react.createElement('div', null,
|
|
1503
1718
|
humanRoutesIncomplete(node) ? react.createElement('div', { style: { fontSize: 12, color: '#dc2626', marginBottom: 8, lineHeight: 1.6 } }, '⚠ 至少需要一个完整去向(标签 + 目标节点),保存会被拦截,画布上该节点显示红色警示。') : null,
|
|
1504
|
-
fieldLabel('展示产物(上游节点 id
|
|
1505
|
-
react.createElement('input', { className: 'knj-input', placeholder: '如 verify', value: node.displayFrom || '', onChange: (e) => this.patchNode(node.id, { displayFrom: e.target.value }) }),
|
|
1719
|
+
fieldLabel('展示产物(上游节点 id 或文件路径)', '人工审批时展示给审批人的内容,两种填法:\n· 上游节点 id(如 verify)——展示该节点产物;若产物字段声明为 file/dir 类型,会渲染成可点击预览(md / 目录浏览)\n· 文件路径(如 ${cwd}/docs/设计.md 或 docs/设计.md)——直接预览该文件/目录(md 渲染,目录可逐级浏览)', '展示上游节点产物,或直接填文件路径预览(如 ${cwd}/docs/设计.md)'),
|
|
1720
|
+
react.createElement('input', { className: 'knj-input', placeholder: '如 verify 或 ${cwd}/docs/设计.md', value: node.displayFrom || '', onChange: (e) => this.patchNode(node.id, { displayFrom: e.target.value }) }),
|
|
1506
1721
|
fieldLabel('去向(审批按钮,可多条)', '运行到本节点时暂停等待人工:每个去向是一个按钮,点击后流程走到对应目标。\n每条 = 标签(按钮文字)+ 目标节点 + 色调。填写的意见会注入目标节点。\n旧配置的「通过/驳回」自动兼容为两条去向。', '每个去向 = 一个审批按钮;可附意见注入目标节点'),
|
|
1507
1722
|
humanRoutes(node).map((route, i) => this.renderRouteRow(node, route, i)),
|
|
1508
1723
|
react.createElement('button', { className: 'knj-btn', style: { alignSelf: 'flex-start', marginTop: 6 }, onClick: () => this.addRoute(node.id) }, '+ 添加去向'),
|
|
@@ -1518,7 +1733,7 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
1518
1733
|
const fields = Object.keys(props);
|
|
1519
1734
|
return react.createElement('div', null,
|
|
1520
1735
|
react.createElement('div', { className: 'knj-form-label', style: { marginTop: 10 } }, '节点输出(本阶段产物)',
|
|
1521
|
-
react.createElement('span', { onMouseEnter: (e) => this.showTip(e, '定义本节点完成后输出的字段。\n下游阶段用 ${编码.字段} 引用(如 ${complexity.level}),网关用字段值做分支判断(如 level eq high)。\n布尔 / 数组 / 枚举建议显式指定类型,判断才可靠。\n并行执行(parallel
|
|
1736
|
+
react.createElement('span', { onMouseEnter: (e) => this.showTip(e, '定义本节点完成后输出的字段。\n下游阶段用 ${编码.字段} 引用(如 ${complexity.level}),网关用字段值做分支判断(如 level eq high)。\n布尔 / 数组 / 枚举建议显式指定类型,判断才可靠。\n并行执行(parallel)时结果为数组。\n\nfile / dir:字段值是文件/目录路径(如 ${cwd}/docs/设计.md)——人工审批展示时该字段会渲染为可点击预览(md 渲染 / 目录浏览),不当作普通文本。'), onMouseLeave: () => this.hideTip(), style: { cursor: 'help', color: 'var(--dsw-alias-label-tertiary)', marginLeft: 6, fontSize: 12 } }, 'ⓘ'),
|
|
1522
1737
|
),
|
|
1523
1738
|
react.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', lineHeight: 1.5, marginBottom: 4 } }, '本节点的输出字段:下游 ${编码.字段} 引用、网关分支判断;布尔/数组/枚举建议指定类型'),
|
|
1524
1739
|
react.createElement('input', {
|
|
@@ -1547,6 +1762,8 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
1547
1762
|
react.createElement('option', { value: 'number' }, 'number'),
|
|
1548
1763
|
react.createElement('option', { value: 'boolean' }, 'boolean'),
|
|
1549
1764
|
react.createElement('option', { value: 'array' }, 'array'),
|
|
1765
|
+
react.createElement('option', { value: 'file' }, 'file'),
|
|
1766
|
+
react.createElement('option', { value: 'dir' }, 'dir'),
|
|
1550
1767
|
),
|
|
1551
1768
|
type === 'string' ? react.createElement('input', { className: 'knj-input', style: { flex: 1, minWidth: 0 }, placeholder: '枚举(逗号分隔,可选)', defaultValue: (schema?.enum || []).join(','), onBlur: (e) => this.patchOutputFieldEnum(node.id, field, e.target.value) }) : null,
|
|
1552
1769
|
),
|
|
@@ -2077,8 +2294,9 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
2077
2294
|
super(props);
|
|
2078
2295
|
this.state = { tab: 'tasks', view: 'table', tasks: [], workflows: [], archived: [], error: null, search: '', showArchive: false, detail: null };
|
|
2079
2296
|
}
|
|
2080
|
-
componentDidMount() { this.refresh(); this._timer = setInterval(() => this.refresh(), 3000); }
|
|
2081
|
-
componentWillUnmount() { clearInterval(this._timer); }
|
|
2297
|
+
componentDidMount() { devBoardRef = this; if (pendingScheduledWorkflowTask) { this.openTaskDetail(pendingScheduledWorkflowTask); pendingScheduledWorkflowTask = null; } this.refresh(); this._timer = setInterval(() => this.refresh(), 3000); }
|
|
2298
|
+
componentWillUnmount() { clearInterval(this._timer); if (devBoardRef === this) devBoardRef = null; }
|
|
2299
|
+
openTaskDetail(task) { this.setState({ tab: 'tasks', showArchive: false, detail: task }); }
|
|
2082
2300
|
async refresh() {
|
|
2083
2301
|
try {
|
|
2084
2302
|
const [t, w, a] = await Promise.all([api('/tasks'), api('/workflows'), api('/tasks?archived=1')]);
|
|
@@ -2161,6 +2379,8 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
2161
2379
|
}
|
|
2162
2380
|
|
|
2163
2381
|
let devBoardRoot = null;
|
|
2382
|
+
let devBoardRef = null;
|
|
2383
|
+
let pendingScheduledWorkflowTask = null;
|
|
2164
2384
|
let devBoardRootHandle = null; // React root 句柄(关闭时需 unmount 清定时器,不能只 remove DOM)
|
|
2165
2385
|
let _closeDevBoard = null; // 收起开发任务看板(跳转子会话前先收起,避免盖住)
|
|
2166
2386
|
/** 主区接管(SPEC-board-redesign):中栏注入看板容器 + html data 属性隐藏会话,会话子树保活 */
|
|
@@ -2278,6 +2498,14 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
2278
2498
|
|
|
2279
2499
|
// 「工作流」入口改为 DOM 注入到「任务看板」入口之后(原任务看板入口处),不再占用 knj-menu 折叠位。
|
|
2280
2500
|
ctx.effect(() => { mountWorkflowEntry(); return () => { if (workflowEntryDisposer) workflowEntryDisposer(); }; }, 'dsh-knj-workflow: 工作流入口');
|
|
2501
|
+
ctx.effect(() => {
|
|
2502
|
+
const onOpenTask = (event) => {
|
|
2503
|
+
const taskId = event && event.detail && event.detail.taskId;
|
|
2504
|
+
if (typeof taskId === 'string' && taskId.trim() !== '') void openScheduledWorkflowTask(taskId);
|
|
2505
|
+
};
|
|
2506
|
+
window.addEventListener('dsh-knj-workflow:open-task', onOpenTask);
|
|
2507
|
+
return () => window.removeEventListener('dsh-knj-workflow:open-task', onOpenTask);
|
|
2508
|
+
}, 'dsh-knj-workflow: scheduler task navigation');
|
|
2281
2509
|
|
|
2282
2510
|
// 「开发任务」已迁移到左侧工具栏入口 + 主区看板(见 工作流入口 / openDevBoard),
|
|
2283
2511
|
// 不再注册 conversation.view 页签。
|
|
@@ -2451,14 +2679,15 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
2451
2679
|
class SidebarTaskDetail extends react.Component {
|
|
2452
2680
|
constructor(props) {
|
|
2453
2681
|
super(props);
|
|
2454
|
-
this.state = { results: null, rejectFeedback: '', expanded: null, fileView: null };
|
|
2682
|
+
this.state = { results: null, rejectFeedback: '', expanded: null, fileView: null, preview: null, viewMode: 'graph', graphSel: null, graphZoom: null, fitW: null };
|
|
2455
2683
|
}
|
|
2456
2684
|
componentDidMount() { this.loadResults(); this.preloadCatalog(); }
|
|
2457
2685
|
/** 预加载 parent 的子会话 catalog:让「查看」点击时直接 open、无需每次 await 慢 RPC */
|
|
2458
2686
|
async preloadCatalog() {
|
|
2459
2687
|
const { task } = this.props;
|
|
2460
|
-
|
|
2461
|
-
|
|
2688
|
+
const sessions = getSessionsService();
|
|
2689
|
+
if (!sessions || !task.parentSessionId) return;
|
|
2690
|
+
try { await sessions.refreshSubagents(task.parentSessionId); } catch {}
|
|
2462
2691
|
}
|
|
2463
2692
|
async loadResults() {
|
|
2464
2693
|
try {
|
|
@@ -2500,6 +2729,197 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
2500
2729
|
const m = Math.floor(sec / 60), r = sec % 60;
|
|
2501
2730
|
return `${m}m${r}s`;
|
|
2502
2731
|
}
|
|
2732
|
+
/** 单独补拉某阶段产物(stage 文件兜底;结果并入 state.results 触发重渲) */
|
|
2733
|
+
async loadStageResult(stageId) {
|
|
2734
|
+
if (this.resultFor(stageId) != null) return;
|
|
2735
|
+
try {
|
|
2736
|
+
const r = await api('/tasks/' + this.props.task.id + '/stages/' + stageId);
|
|
2737
|
+
if (r && r.data != null && typeof r.data === 'object') {
|
|
2738
|
+
this.setState((s) => ({ results: { ...(s.results || {}), [stageId]: r.data } }));
|
|
2739
|
+
}
|
|
2740
|
+
} catch { /* 无产物文件则保持空 */ }
|
|
2741
|
+
}
|
|
2742
|
+
/** 选中运行图节点时的详情(与 renderNodeCard 展开内容同源;data 来自 task/stageStates/懒加载产物) */
|
|
2743
|
+
renderGraphDetail() {
|
|
2744
|
+
const { task } = this.props;
|
|
2745
|
+
const sel = this.state.graphSel;
|
|
2746
|
+
const stages = task.stageStates || [];
|
|
2747
|
+
const derived = knjDeriveRunStates(task);
|
|
2748
|
+
const st = RUN_STATUS[derived[sel] || 'pending'];
|
|
2749
|
+
const stage = stages.find((x) => x.id === sel) || null;
|
|
2750
|
+
const node = (task.workflowSnapshot?.nodes || []).find((n) => n.id === sel);
|
|
2751
|
+
const result = this.resultFor(sel);
|
|
2752
|
+
const meta = st;
|
|
2753
|
+
const body = [];
|
|
2754
|
+
if (stage && stage.status === 'failed') {
|
|
2755
|
+
body.push(react.createElement('div', { key: 'e', style: { ...detailErr(), marginBottom: 8 } }, stage.error || '节点执行失败'));
|
|
2756
|
+
body.push(react.createElement('button', { key: 'rr', className: 'knj-btn danger', style: { fontSize: 12, height: 26, borderRadius: 999, padding: '0 12px', marginRight: 'auto' }, onClick: () => this.rerunStage(sel) }, '重跑该阶段'));
|
|
2757
|
+
} else if (stage && stage.status === 'running') {
|
|
2758
|
+
body.push(react.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
|
|
2759
|
+
react.createElement('span', { style: { width: 12, height: 12, borderRadius: 99, border: '2px solid rgba(47,107,255,.25)', borderTopColor: '#2f6bff', animation: 'knjSpin 1s linear infinite' } }),
|
|
2760
|
+
'子代理执行中…产物完成后显示在此'));
|
|
2761
|
+
} else if (sel && derived[sel] === 'waiting') {
|
|
2762
|
+
const routes = humanRoutes(node);
|
|
2763
|
+
body.push(react.createElement('div', { key: 'w1', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', lineHeight: 1.7 } },
|
|
2764
|
+
'任务正等待人工审批。去向:', routes.map((r) => `${r.label} → ${r.to}`).join(' / '), '。请在上方审批卡查看待审产物并作出选择。'));
|
|
2765
|
+
} else if (result && typeof result === 'object') {
|
|
2766
|
+
body.push(react.createElement('div', { key: 'f1', className: 'knj-node-sec' }, '产物(输出字段)'), this.renderResultFields(result), this.renderFiles(sel, result));
|
|
2767
|
+
if ((stage?.sessionIds || []).length) body.push(this.renderSessionsRow(stage));
|
|
2768
|
+
} else if (stage && stage.status === 'done') {
|
|
2769
|
+
body.push(react.createElement('div', { key: 'l1', style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } }, '节点已完成(产物为空或暂未取到)。'));
|
|
2770
|
+
if ((stage?.sessionIds || []).length) body.push(this.renderSessionsRow(stage));
|
|
2771
|
+
} else if (derived[sel] === 'skipped') {
|
|
2772
|
+
body.push(react.createElement('div', { key: 's1', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, '该分支未被实际执行(网关未走到此路径),节点已标记为跳过。'));
|
|
2773
|
+
} else if (node && (node.type === 'start' || node.type === 'end' || node.type === 'gateway-xor' || node.type === 'gateway-and')) {
|
|
2774
|
+
body.push(react.createElement('div', { key: 'n1', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, node.type === 'end' ? '流程结束节点。' : node.type === 'start' ? '流程开始节点。' : '网关/人工类节点不产生独立产物,仅标记执行路径已通过。'));
|
|
2775
|
+
} else {
|
|
2776
|
+
body.push(react.createElement('div', { key: 'p1', style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } }, '待执行:流程尚未运行到该节点。'));
|
|
2777
|
+
}
|
|
2778
|
+
return react.createElement('div', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 10, overflow: 'hidden', marginTop: 8, background: 'var(--dsw-alias-bg-base)' } },
|
|
2779
|
+
react.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '7px 12px', background: 'var(--dsw-alias-fill-2)', borderBottom: '1px solid var(--dsw-alias-border-l2)', fontSize: 12.5, fontWeight: 600 } },
|
|
2780
|
+
react.createElement('span', { style: { width: 3, height: 14, borderRadius: 2, background: meta.color, display: 'inline-block' } }),
|
|
2781
|
+
react.createElement('span', null, `${sel} · ${meta.label}`),
|
|
2782
|
+
react.createElement('button', { style: { marginLeft: 'auto', border: 'none', background: 'none', color: 'var(--dsw-alias-label-tertiary)', cursor: 'pointer', fontSize: 16, lineHeight: 1, padding: '0 4px' }, onClick: () => this.setState({ graphSel: null }) }, '×')),
|
|
2783
|
+
react.createElement('div', { style: { padding: '10px 12px', fontSize: 12.5 } }, body));
|
|
2784
|
+
}
|
|
2785
|
+
renderSessionsRow(stage) {
|
|
2786
|
+
return react.createElement('div', { key: 'sess', className: 'knj-node-sec' },
|
|
2787
|
+
'执行记录(子会话)',
|
|
2788
|
+
(stage.sessionIds || []).map((sid) => react.createElement('div', { key: sid, className: 'knj-row', style: { marginTop: 6, gap: 8 } },
|
|
2789
|
+
react.createElement('code', { style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', flex: 1, wordBreak: 'break-all' } }, '👤 ' + sid),
|
|
2790
|
+
react.createElement('button', { className: 'knj-btn', style: { flexShrink: 0 }, title: '跳转到该子代理会话查看完整执行细节', onClick: () => this.viewSubagent(sid) }, '查看会话'),
|
|
2791
|
+
)));
|
|
2792
|
+
}
|
|
2793
|
+
/** 运行图空白区域按住拖动:平移滚动视口,不改变节点坐标或流程布局。 */
|
|
2794
|
+
startRunCanvasPan(e) {
|
|
2795
|
+
if (e.button !== 0 || !this._runCanvasEl) return;
|
|
2796
|
+
this._runPan = { x: e.clientX, y: e.clientY, left: this._runCanvasEl.scrollLeft, top: this._runCanvasEl.scrollTop };
|
|
2797
|
+
this._runPanMoved = false;
|
|
2798
|
+
this._runCanvasEl.style.cursor = 'grabbing';
|
|
2799
|
+
e.preventDefault();
|
|
2800
|
+
}
|
|
2801
|
+
moveRunCanvasPan(e) {
|
|
2802
|
+
if (!this._runPan || !this._runCanvasEl) return;
|
|
2803
|
+
const dx = e.clientX - this._runPan.x, dy = e.clientY - this._runPan.y;
|
|
2804
|
+
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) this._runPanMoved = true;
|
|
2805
|
+
this._runCanvasEl.scrollLeft = this._runPan.left - dx;
|
|
2806
|
+
this._runCanvasEl.scrollTop = this._runPan.top - dy;
|
|
2807
|
+
}
|
|
2808
|
+
endRunCanvasPan() {
|
|
2809
|
+
if (!this._runPan) return;
|
|
2810
|
+
this._runPan = null;
|
|
2811
|
+
if (this._runCanvasEl) this._runCanvasEl.style.cursor = 'grab';
|
|
2812
|
+
}
|
|
2813
|
+
/** 运行效果图画布:workflowSnapshot 拓扑 + 推导状态 → SVG(全 inline style,样式语言与 V2 设计稿一致) */
|
|
2814
|
+
renderRunCanvas() {
|
|
2815
|
+
const { task } = this.props;
|
|
2816
|
+
const wf = task?.workflowSnapshot;
|
|
2817
|
+
const nodes = Array.isArray(wf?.nodes) ? wf.nodes : [];
|
|
2818
|
+
if (!nodes.length) return null;
|
|
2819
|
+
const derived = knjDeriveRunStates(task);
|
|
2820
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
2821
|
+
const edges = Array.isArray(wf?.edges) ? wf.edges : [];
|
|
2822
|
+
// 画布几何
|
|
2823
|
+
const xs = nodes.map((n) => n.x || 0), ys = nodes.map((n) => n.y || 0);
|
|
2824
|
+
const W = Math.max(720, ...xs) + 120, H = Math.max(300, ...ys) + 90;
|
|
2825
|
+
const wrapW = this.state.fitW || 780;
|
|
2826
|
+
const fit = Math.max(0.6, Math.min((wrapW - 20) / W, 400 / H, 1));
|
|
2827
|
+
const zoom = this.state.graphZoom == null ? fit : this.state.graphZoom;
|
|
2828
|
+
const pct = Math.round(zoom * 100);
|
|
2829
|
+
const tool = (label, title, onClick, active) => react.createElement('button', { onClick, title, style: { height: 24, padding: '0 9px', fontSize: 11.5, borderRadius: 6, border: active ? '1px solid #2f6bff' : '1px solid var(--dsw-alias-border-l2)', background: active ? 'rgba(47,107,255,.08)' : 'var(--dsw-alias-bg-base)', color: active ? '#2f6bff' : 'var(--dsw-alias-label-secondary)', cursor: 'pointer' } }, label);
|
|
2830
|
+
const halfW = (t) => (t === 'gateway-xor' || t === 'gateway-and' || t === 'start' || t === 'end') ? 30 : 60;
|
|
2831
|
+
const nodeColor = (n) => ({ task: '#5b6472', human: '#7c5cff', 'gateway-xor': '#2f6bff', 'gateway-and': '#16a34a', start: '#8b95a5', end: '#8b95a5' }[n.type] || '#5b6472');
|
|
2832
|
+
const statusOf = (n) => derived[n.id] || 'pending';
|
|
2833
|
+
// 连线集合:edges + human routes
|
|
2834
|
+
const lines = [];
|
|
2835
|
+
for (const e of edges) if (e?.from && e?.to && byId.has(e.from) && byId.has(e.to)) lines.push({ from: e.from, to: e.to, label: e.when ? `${e.when.field} ${e.when.op} ${JSON.stringify(e.when.value)}` : e.default ? 'default' : null, color: '#8b95a5', dashed: false, faint: false });
|
|
2836
|
+
for (const n of nodes) {
|
|
2837
|
+
if (n.type !== 'human') continue;
|
|
2838
|
+
humanRoutes(n).forEach((r, i) => { if (r?.to && byId.has(r.to)) lines.push({ from: n.id, to: r.to, label: r.label || '去向', color: routeColor(r), dashed: true, faint: false }); });
|
|
2839
|
+
}
|
|
2840
|
+
const svgKids = [
|
|
2841
|
+
react.createElement('defs', { key: 'd' },
|
|
2842
|
+
[['a', '#8b95a5'], ['g', '#16a34a'], ['r', '#dc2626'], ['b', '#2f6bff']].map(([k, c]) => react.createElement('marker', { key: k, id: 'knj-rg-' + k, viewBox: '0 0 12 7', refX: 11, refY: 3.5, markerWidth: 12, orient: 'auto' }, react.createElement('path', { d: 'M0 0L12 3.5L0 7z', fill: c })))),
|
|
2843
|
+
...lines.map((ln, i) => {
|
|
2844
|
+
const f = byId.get(ln.from), t = byId.get(ln.to);
|
|
2845
|
+
const incoming = incomingPortSlot(wf, ln.to, ln.from);
|
|
2846
|
+
const { sx, sy, tx, ty } = flowPorts(f, t, incoming.slot, incoming.count);
|
|
2847
|
+
const route = routeEdge(sx, sy, tx, ty, flowObstacles(nodes, ln.from, ln.to), 13);
|
|
2848
|
+
const mx = route.midX, my = route.midY;
|
|
2849
|
+
const d = route.d;
|
|
2850
|
+
const key = 'e' + i;
|
|
2851
|
+
const stroke = ln.faint ? '#c3cad4' : ln.color;
|
|
2852
|
+
const kids = [react.createElement('path', { key: key + 'p', d, fill: 'none', stroke, strokeWidth: ln.faint ? 1.4 : 1.7, strokeDasharray: ln.dashed ? '5 4' : undefined, opacity: ln.faint ? 0.5 : 1 }),
|
|
2853
|
+
react.createElement('path', { key: key + 'a', d: arrowShape(route.ax, route.ay, route.dx, route.dy, 13, 8), fill: stroke, opacity: ln.faint ? 0.5 : 1 })];
|
|
2854
|
+
if (ln.label) kids.push(react.createElement('rect', { key: key + 'b', x: mx - 26, y: my - 9, width: 52, height: 17, rx: 8.5, fill: 'var(--dsw-alias-bg-base)', stroke: ln.color, strokeWidth: 1 }),
|
|
2855
|
+
react.createElement('text', { key: key + 't', x: mx, y: my + 3.5, fontSize: 9.5, textAnchor: 'middle', fill: ln.color, style: { userSelect: 'none' } }, ln.label.length > 5 ? ln.label.slice(0, 5) + '…' : ln.label));
|
|
2856
|
+
return react.createElement('g', { key }, kids);
|
|
2857
|
+
}),
|
|
2858
|
+
...nodes.map((n) => this.runSvgNode(n, statusOf(n), nodeColor(n), zoom)),
|
|
2859
|
+
];
|
|
2860
|
+
return react.createElement('div', { style: { marginTop: 6 } },
|
|
2861
|
+
react.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, margin: '10px 0 2px' } },
|
|
2862
|
+
tool('适应', '按画布适配视图', () => this.setState({ graphZoom: null }), this.state.graphZoom == null),
|
|
2863
|
+
tool('100%', '', () => this.setState({ graphZoom: 1 }), this.state.graphZoom === 1),
|
|
2864
|
+
tool('−', '缩小', () => this.setState((s) => ({ graphZoom: Math.max(0.3, (s.graphZoom == null ? fit : s.graphZoom) / 1.2) }))),
|
|
2865
|
+
tool('+', '放大', () => this.setState((s) => ({ graphZoom: Math.min(2.5, (s.graphZoom == null ? fit : s.graphZoom) * 1.2) }))),
|
|
2866
|
+
react.createElement('span', { style: { marginLeft: 'auto', fontSize: 11, color: 'var(--dsw-alias-label-tertiary)' } }, `${pct}% · 点击节点看详情`),
|
|
2867
|
+
),
|
|
2868
|
+
react.createElement('div', { ref: (el) => { this._runCanvasEl = el; if (el && this.state.fitW == null) { const w = el.clientWidth; if (w > 200) this.setState({ fitW: w }); } }, style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 10, overflow: 'auto', background: 'var(--dsw-alias-bg-base)', minHeight: 300, maxHeight: '56vh', position: 'relative', cursor: 'grab', userSelect: 'none' }, onMouseDown: (e) => this.startRunCanvasPan(e), onMouseMove: (e) => this.moveRunCanvasPan(e), onMouseUp: () => this.endRunCanvasPan(), onMouseLeave: () => this.endRunCanvasPan(), onWheel: (e) => { e.preventDefault(); this.setState((s) => ({ graphZoom: Math.max(0.3, Math.min(2.5, (s.graphZoom == null ? fit : s.graphZoom) * (e.deltaY < 0 ? 1.1 : 0.9))) })); } },
|
|
2869
|
+
react.createElement('div', { onClick: () => { if (!this._runPanMoved) this.setState({ graphSel: null }); this._runPanMoved = false; } },
|
|
2870
|
+
react.createElement('svg', { width: Math.round(W * zoom), height: Math.round(H * zoom), viewBox: `0 0 ${W} ${H}`, preserveAspectRatio: 'xMinYMin meet', style: { display: 'block', background: 'var(--dsw-alias-bg-base)' } }, svgKids),
|
|
2871
|
+
),
|
|
2872
|
+
),
|
|
2873
|
+
this.state.graphSel ? this.renderGraphDetail() : null,
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
/** 单个节点 SVG(类型定底 + 左缘状态条 + 右上状态图标;编排画布同语言,此处为运行态着色版) */
|
|
2877
|
+
runSvgNode(n, status, typeColor, zoom) {
|
|
2878
|
+
const sel = this.state.graphSel === n.id;
|
|
2879
|
+
const meta = RUN_STATUS[status];
|
|
2880
|
+
const c = meta.color;
|
|
2881
|
+
const isSkipped = status === 'skipped';
|
|
2882
|
+
const dim = isSkipped ? 0.55 : 1;
|
|
2883
|
+
const isTask = n.type === 'task' || n.type === 'human';
|
|
2884
|
+
const kids = [];
|
|
2885
|
+
const w = 124, hh = 46;
|
|
2886
|
+
// 形状 + 左条
|
|
2887
|
+
if (n.type === 'gateway-xor' || n.type === 'gateway-and') {
|
|
2888
|
+
const h = 30;
|
|
2889
|
+
kids.push(react.createElement('polygon', { key: 'b', points: `${n.x},${n.y - h} ${n.x + h},${n.y} ${n.x},${n.y + h} ${n.x - h},${n.y}`, fill: '#eef4ff', stroke: isSkipped ? '#b6bdc7' : (status === 'pending' ? '#8b95a5' : c), strokeWidth: 1.6, strokeDasharray: isSkipped ? '4 3' : undefined, opacity: dim }));
|
|
2890
|
+
kids.push(react.createElement('path', { key: 'g', d: n.type === 'gateway-and' ? `M ${n.x - 10} ${n.y} h20` : `M ${n.x - 10} ${n.y - 10} h20 M ${n.x - 10} ${n.y + 10} h20`, stroke: c, strokeWidth: 1.5, opacity: dim }));
|
|
2891
|
+
kids.push(react.createElement('text', { key: 't', x: n.x, y: n.y - 40, fontSize: 10.5, textAnchor: 'middle', fill: 'var(--dsw-alias-label-secondary)', style: { userSelect: 'none' } }, (n.title || n.id).length > 9 ? (n.title || n.id).slice(0, 9) + '…' : (n.title || n.id)));
|
|
2892
|
+
} else if (n.type === 'start' || n.type === 'end') {
|
|
2893
|
+
kids.push(react.createElement('rect', { key: 'b', x: n.x - 31, y: n.y - 18, width: 62, height: 36, rx: 18, fill: 'var(--dsw-alias-bg-base)', stroke: isSkipped ? '#b6bdc7' : (status === 'pending' ? '#8b95a5' : c), strokeWidth: 1.4, strokeDasharray: isSkipped ? '4 3' : undefined, opacity: dim }));
|
|
2894
|
+
kids.push(react.createElement('text', { key: 't', x: n.x, y: n.y + 4, fontSize: 12, textAnchor: 'middle', fill: 'var(--dsw-alias-label-secondary)' }, n.type === 'start' ? '开始' : '结束'));
|
|
2895
|
+
} else {
|
|
2896
|
+
kids.push(react.createElement('rect', { key: 'b', x: n.x - w / 2, y: n.y - hh / 2, width: w, height: hh, rx: 9, fill: n.type === 'human' ? '#f3efff' : 'var(--dsw-alias-bg-base)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 1, opacity: dim }));
|
|
2897
|
+
kids.push(react.createElement('rect', { key: 'bar', x: n.x - w / 2 + 1, y: n.y - hh / 2 + 1, width: 3.5, height: hh - 2, rx: 1.75, fill: isSkipped ? '#9aa3af' : c, strokeDasharray: isSkipped ? '3 2' : undefined }));
|
|
2898
|
+
kids.push(react.createElement('text', { key: 'tt', x: n.x + 5, y: n.y - 1, fontSize: 13, fontWeight: 600, textAnchor: 'middle', fill: 'var(--dsw-alias-label-primary)', style: { userSelect: 'none' } }, (n.title || n.id).length > 11 ? (n.title || n.id).slice(0, 11) + '…' : (n.title || n.id)));
|
|
2899
|
+
const sub = status === 'running' ? '运行中' : status === 'waiting' ? '等待人工审批' : (status === 'passed' ? '已过' : meta.label);
|
|
2900
|
+
kids.push(react.createElement('text', { key: 'sb', x: n.x + 5, y: n.y + 14, fontSize: 10, textAnchor: 'middle', fill: isSkipped ? 'var(--dsw-alias-label-tertiary)' : c, style: { userSelect: 'none' } }, sub));
|
|
2901
|
+
// 右上状态图标(14px 圆底)
|
|
2902
|
+
const ix = n.x + 52, iy = n.y - 16;
|
|
2903
|
+
if (status === 'done' || status === 'failed' || status === 'running' || status === 'waiting' || status === 'skipped') {
|
|
2904
|
+
kids.push(react.createElement('circle', { key: 'ic', cx: ix, cy: iy, r: 8, fill: isSkipped ? '#9aa3af' : c }));
|
|
2905
|
+
if (status === 'done') kids.push(react.createElement('path', { key: 'ic2', d: `M ${ix - 3} ${iy} l 2.1 2.1 l 4 -4.1`, stroke: '#fff', strokeWidth: 1.6, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
|
2906
|
+
else if (status === 'failed') kids.push(react.createElement('path', { key: 'ic2', d: `M ${ix} ${iy - 3} v 2.6 M ${ix} ${iy + 1.6} v .01`, stroke: '#fff', strokeWidth: 1.6, strokeLinecap: 'round' }));
|
|
2907
|
+
else if (status === 'skipped') kids.push(react.createElement('path', { key: 'ic2', d: `M ${ix - 3.2} ${iy} h 6.4`, stroke: '#fff', strokeWidth: 1.6, strokeLinecap: 'round' }));
|
|
2908
|
+
else if (status === 'waiting') kids.push(react.createElement('circle', { key: 'ic2', cx: ix, cy: iy - 1.5, r: 2.2, fill: 'none', stroke: '#fff', strokeWidth: 1.2 }), react.createElement('path', { key: 'ic3', d: `M ${ix - 3.6} ${iy + 4.6} c 0 -2.1 1.7 -3.1 3.6 -3.1 s 3.6 1 3.6 3.1`, stroke: '#fff', strokeWidth: 1.2, fill: 'none', strokeLinecap: 'round' }));
|
|
2909
|
+
}
|
|
2910
|
+
// running 光环(静态两层,替代动画,避免依赖注入 keyframes)
|
|
2911
|
+
if (status === 'running') kids.push(react.createElement('circle', { key: 'halo', cx: n.x, cy: n.y, r: 33, fill: 'none', stroke: '#2f6bff', strokeWidth: 1.6, opacity: 0.35 }));
|
|
2912
|
+
}
|
|
2913
|
+
const isSel = sel;
|
|
2914
|
+
const hit = react.createElement('rect', { x: n.x - 70, y: n.y - 38, width: 140, height: 76, rx: 12, fill: 'transparent', onClick: (e) => { e.stopPropagation(); this.setState({ graphSel: n.id }); const stage = (this.props.task.stageStates || []).find((x) => x.id === n.id); if (stage && stage.status === 'done' && this.resultFor(n.id) == null) this.loadStageResult(n.id); }, style: { cursor: 'pointer' } });
|
|
2915
|
+
const title = `${n.title || n.id} · ${meta.label}`;
|
|
2916
|
+
return react.createElement('g', { key: n.id, opacity: isSkipped ? 0.65 : 1, onMouseDown: (e) => e.stopPropagation() },
|
|
2917
|
+
react.createElement('title', null, title),
|
|
2918
|
+
hit,
|
|
2919
|
+
...kids,
|
|
2920
|
+
isSel ? react.createElement('rect', { key: 'hal', x: n.x - w / 2 - 3, y: n.y - hh / 2 - 3, width: w + 6, height: hh + 6, rx: 11, fill: 'none', stroke: '#2f6bff', strokeWidth: 2 }) : null,
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2503
2923
|
/** 流程节点卡片:状态圆点 + 标题 + 耗时,点击展开产物与执行记录(整合,不再平铺三个区块) */
|
|
2504
2924
|
renderNodeCard(s, i) {
|
|
2505
2925
|
const meta = STAGE_META[s.status] || STAGE_META.pending;
|
|
@@ -2594,26 +3014,199 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
2594
3014
|
this.setState({ fileView: { stageId, path: r.path, content: r.content } });
|
|
2595
3015
|
} catch (e) { toast('读取失败:' + e.message); }
|
|
2596
3016
|
}
|
|
3017
|
+
/** 优先用 dsh-better-sidebar(若已安装)在右侧栏打开文件预览;成功返回 true,未安装则返回 false(调用方回退内嵌预览)。
|
|
3018
|
+
* path 支持三种形态:相对 cwd 路径(docs/x.md)、${cwd}/... 前缀、盘符绝对路径(若在 cwd 内)。
|
|
3019
|
+
* 打开的是 better-sidebar 内置 editor tab(按 path 去重):md/图片/HTML/PDF/代码由其现成渲染,
|
|
3020
|
+
* content open 会自动展开右栏;同步调用,无 await 挂起风险。 */
|
|
3021
|
+
tryOpenSidebar(path, label) {
|
|
3022
|
+
const { task } = this.props;
|
|
3023
|
+
if (!path || !String(path).trim()) return false;
|
|
3024
|
+
const bs = _ctx && _ctx.get ? _ctx.get('betterSidebar') : null;
|
|
3025
|
+
if (!bs || typeof bs.openTab !== 'function') return false;
|
|
3026
|
+
let rel = String(path).trim();
|
|
3027
|
+
rel = rel.replace(/^\$\{cwd\}(\/|\\)?/, '');
|
|
3028
|
+
const cwd = task && task.cwd ? String(task.cwd).replace(/\\/g, '/') : null;
|
|
3029
|
+
if (cwd && /^[A-Za-z]:/.test(rel)) {
|
|
3030
|
+
const relNorm = rel.replace(/\\/g, '/');
|
|
3031
|
+
if (relNorm.toLowerCase().startsWith(cwd.toLowerCase())) rel = relNorm.slice(cwd.length).replace(/^\/+/, '');
|
|
3032
|
+
}
|
|
3033
|
+
const abs = cwd ? `${cwd}/${rel}` : rel;
|
|
3034
|
+
try {
|
|
3035
|
+
bs.openTab({ type: 'editor', path: abs, title: label || String(abs).split(/[\\/]/).pop() });
|
|
3036
|
+
return true;
|
|
3037
|
+
} catch (e) { return false; }
|
|
3038
|
+
}
|
|
3039
|
+
/** 审批预览:打开一个文件/目录(scope 用于渲染在审批卡内)。文件按类型渲染,目录列条目可逐级点开。
|
|
3040
|
+
* path 支持三种形态:相对 cwd 路径(docs/x.md)、${cwd}/... 前缀、盘符绝对路径(若在 cwd 内)。 */
|
|
3041
|
+
async openPreview(path, label, scope = 'human') {
|
|
3042
|
+
const { task } = this.props;
|
|
3043
|
+
if (!path || !String(path).trim()) { toast('路径为空'); return; }
|
|
3044
|
+
let rel = String(path).trim();
|
|
3045
|
+
// ${cwd} → 相对路径(Host 端以 task.cwd 为根解析)
|
|
3046
|
+
rel = rel.replace(/^\$\{cwd\}(\/|\\)?/, '');
|
|
3047
|
+
// 绝对盘符路径且在 cwd 内 → 手工转相对(浏览器端无 node:path)
|
|
3048
|
+
const cwd = task && task.cwd ? String(task.cwd).replace(/\\/g, '/') : null;
|
|
3049
|
+
if (cwd && /^[A-Za-z]:/.test(rel)) {
|
|
3050
|
+
const relNorm = rel.replace(/\\/g, '/');
|
|
3051
|
+
if (relNorm.toLowerCase().startsWith(cwd.toLowerCase())) {
|
|
3052
|
+
rel = relNorm.slice(cwd.length).replace(/^\/+/, '');
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
try {
|
|
3056
|
+
const r = await api('/tasks/' + task.id + '/file?path=' + encodeURIComponent(rel));
|
|
3057
|
+
this.setState({ preview: { scope, path: r.path, isDir: !!r.isDir, entries: r.entries || null, content: r.content ?? null, meta: r.meta || null, tooLarge: !!r.tooLarge, label: label || path } });
|
|
3058
|
+
} catch (e) { toast('打开失败:' + e.message); }
|
|
3059
|
+
}
|
|
3060
|
+
/** 简易 markdown 渲染(标题/列表/代码块/引用/加粗/链接;无 XSS 风险:只转义后套结构,不注入 HTML 事件) */
|
|
3061
|
+
renderMarkdown(text) {
|
|
3062
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
3063
|
+
const out = [];
|
|
3064
|
+
let inCode = false, codeBuf = [];
|
|
3065
|
+
const pushCode = () => {
|
|
3066
|
+
if (codeBuf.length) { out.push(react.createElement('pre', { key: 'c' + out.length, style: { background: 'var(--dsw-alias-fill-2,#f4f5f7)', padding: 8, borderRadius: 6, fontSize: 12, overflow: 'auto', maxHeight: 300, margin: '4px 0' } }, codeBuf.join('\n'))); codeBuf = []; }
|
|
3067
|
+
};
|
|
3068
|
+
for (const raw of lines) {
|
|
3069
|
+
const line = raw.replace(/\s+$/, '');
|
|
3070
|
+
if (line.startsWith('```')) {
|
|
3071
|
+
if (inCode) { pushCode(); inCode = false; }
|
|
3072
|
+
else inCode = true;
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
if (inCode) { codeBuf.push(line); continue; }
|
|
3076
|
+
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
3077
|
+
const inline = (s) => s
|
|
3078
|
+
.replace(/`([^`]+)`/g, (m, c) => `<code>${esc(c)}</code>`)
|
|
3079
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
3080
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
|
|
3081
|
+
const style = { fontSize: 13, lineHeight: 1.7, color: 'var(--dsw-alias-label-primary)', margin: '2px 0', wordBreak: 'break-word' };
|
|
3082
|
+
if (/^#{1,4}\s/.test(line)) {
|
|
3083
|
+
const lv = line.match(/^#+/)[0].length;
|
|
3084
|
+
out.push(react.createElement('div', { key: 'h' + out.length, style: { ...style, fontWeight: 600, fontSize: lv === 1 ? 15 : 14, marginTop: lv === 1 ? 6 : 4 } }, react.createElement('span', { dangerouslySetInnerHTML: { __html: inline(line.replace(/^#+\s*/, '')) } })));
|
|
3085
|
+
} else if (/^\s*[-*]\s+/.test(line)) {
|
|
3086
|
+
out.push(react.createElement('div', { key: 'li' + out.length, style: { ...style, paddingLeft: 14 } }, '• ' + react.createElement('span', { dangerouslySetInnerHTML: { __html: inline(line.replace(/^\s*[-*]\s+/, '')) } })));
|
|
3087
|
+
} else if (/^\s*\d+\.\s+/.test(line)) {
|
|
3088
|
+
out.push(react.createElement('div', { key: 'li' + out.length, style: { ...style, paddingLeft: 14 } }, react.createElement('span', { dangerouslySetInnerHTML: { __html: inline(line.replace(/^\s*\d+\.\s+/, '')) } })));
|
|
3089
|
+
} else if (/^>/.test(line)) {
|
|
3090
|
+
out.push(react.createElement('div', { key: 'q' + out.length, style: { ...style, borderLeft: '3px solid var(--dsw-alias-border-l2)', paddingLeft: 8, color: 'var(--dsw-alias-label-secondary)' } }, react.createElement('span', { dangerouslySetInnerHTML: { __html: inline(line.replace(/^>\s?/, '')) } })));
|
|
3091
|
+
} else if (!line.trim()) {
|
|
3092
|
+
out.push(react.createElement('div', { key: 's' + out.length, style: { height: 6 } }));
|
|
3093
|
+
} else {
|
|
3094
|
+
out.push(react.createElement('div', { key: 'p' + out.length, style }, react.createElement('span', { dangerouslySetInnerHTML: { __html: inline(line) } })));
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
pushCode();
|
|
3098
|
+
return react.createElement('div', { style: { maxHeight: 320, overflow: 'auto' } }, out);
|
|
3099
|
+
}
|
|
3100
|
+
/** 文件图标(按扩展名给个粗略区分) */
|
|
3101
|
+
fileIcon(ext, isDir) {
|
|
3102
|
+
if (isDir) return '📁';
|
|
3103
|
+
if (!ext) return '📄';
|
|
3104
|
+
const m = { md: '📝', json: '{ }', yml: '⚙', yaml: '⚙', txt: '📄', html: '🌐', png: '🖼', jpg: '🖼', jpeg: '🖼', pdf: '📕' };
|
|
3105
|
+
return m[ext] || '📄';
|
|
3106
|
+
}
|
|
3107
|
+
/** 审批预览面板:目录列表 / md 渲染 / 文本 / 大文件元信息 */
|
|
3108
|
+
renderPreviewPanel(pv) {
|
|
3109
|
+
const close = react.createElement('button', { className: 'knj-btn', style: { alignSelf: 'flex-start', marginTop: 8 }, onClick: () => this.setState({ preview: null }) }, '关闭预览');
|
|
3110
|
+
if (pv.isDir) {
|
|
3111
|
+
const sorted = [...(pv.entries || [])].sort((a, b) => (a.isDir === b.isDir ? 0 : a.isDir ? -1 : 1));
|
|
3112
|
+
return react.createElement('div', null,
|
|
3113
|
+
react.createElement('div', { className: 'knj-node-sec' }, `目录:${pv.path}`),
|
|
3114
|
+
sorted.map((e) => react.createElement('div', { key: e.path, className: 'knj-row', style: { marginTop: 4, gap: 8 } },
|
|
3115
|
+
react.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: e.path },
|
|
3116
|
+
`${this.fileIcon(e.ext, e.isDir)} ${e.name}${e.isDir ? '/' : e.ext === 'md' || e.ext === 'json' || e.ext === 'txt' ? '' : e.size ? ` (${e.size}B)` : ''}`),
|
|
3117
|
+
react.createElement('button', { className: 'knj-btn', style: { flexShrink: 0, borderColor: 'var(--dsw-static-blue-600)', color: 'var(--dsw-static-blue-600)' }, onClick: () => { if (e.isDir || !this.tryOpenSidebar(e.path, e.name)) this.openPreview(e.path, e.name); } }, e.isDir ? '进入' : '查看'),
|
|
3118
|
+
)),
|
|
3119
|
+
close,
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
if (pv.tooLarge) {
|
|
3123
|
+
return react.createElement('div', null,
|
|
3124
|
+
react.createElement('div', { className: 'knj-node-sec' }, `文件:${pv.path}`),
|
|
3125
|
+
react.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: 4 } }, `${pv.meta?.name || pv.path} · ${pv.meta?.size || ''}B · 超过 200KB 预览上限,请在工作区打开`),
|
|
3126
|
+
close,
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
if (pv.content == null) return null;
|
|
3130
|
+
const ext = String(pv.path || '').split('.').pop().toLowerCase();
|
|
3131
|
+
const isMd = ext === 'md' || ext === 'markdown';
|
|
3132
|
+
return react.createElement('div', null,
|
|
3133
|
+
react.createElement('div', { className: 'knj-node-sec', style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
3134
|
+
react.createElement('span', null, isMd ? 'Markdown 预览' : '文件内容'),
|
|
3135
|
+
react.createElement('span', { style: { fontWeight: 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, pv.path),
|
|
3136
|
+
),
|
|
3137
|
+
isMd
|
|
3138
|
+
? this.renderMarkdown(pv.content)
|
|
3139
|
+
: react.createElement('pre', { style: { maxHeight: 320, fontSize: 12, lineHeight: 1.5, whiteSpace: 'pre-wrap', wordBreak: 'break-all' } }, pv.content),
|
|
3140
|
+
close,
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
3143
|
+
/** 判断字符串是否看起来是文件路径(displayFrom 支持直接填 ${cwd}/xx.md) */
|
|
3144
|
+
looksLikePathStr(s) {
|
|
3145
|
+
if (typeof s !== 'string' || !s.trim()) return false;
|
|
3146
|
+
const v = s.trim();
|
|
3147
|
+
if (v.includes(' ')) return false;
|
|
3148
|
+
return /^\$\{cwd\}/.test(v) || /^([A-Za-z]:[\\/]|\/|\.{1,2}[\\/])/.test(v) || /^\S+\.\w{1,12}$/.test(v);
|
|
3149
|
+
}
|
|
2597
3150
|
renderHumanCard(task) {
|
|
2598
3151
|
const humanNode = (task.workflowSnapshot?.nodes || []).find((n) => n.id === task.humanState?.humanId);
|
|
2599
3152
|
const routes = humanRoutes(humanNode);
|
|
2600
3153
|
const displayFrom = humanNode?.displayFrom;
|
|
2601
3154
|
const results = this.state.results || task.humanState?.results || {};
|
|
2602
|
-
|
|
3155
|
+
// displayFrom 双模式:
|
|
3156
|
+
// 1) 上游节点 id(如 verify)→ 展示该节点产物,其中 file/dir 类型字段渲染为可点击预览
|
|
3157
|
+
// 2) 文件路径(如 ${cwd}/docs/设计.md)→ 直接作为文件预览源
|
|
3158
|
+
const isPathDisplay = this.looksLikePathStr(displayFrom);
|
|
3159
|
+
const display = (!isPathDisplay && displayFrom) ? results[displayFrom] : null;
|
|
3160
|
+
const displayNode = (!isPathDisplay && displayFrom)
|
|
3161
|
+
? (task.workflowSnapshot?.nodes || []).find((n) => n.id === displayFrom)
|
|
3162
|
+
: null;
|
|
2603
3163
|
const btnStyle = (route) => {
|
|
2604
3164
|
const map = { success: 'var(--dsw-static-green-500)', danger: 'var(--dsw-static-red-500)', warning: 'var(--dsw-static-amber-500)', info: 'var(--dsw-static-blue-600)', neutral: 'var(--dsw-alias-label-secondary)' };
|
|
2605
3165
|
const c = map[route.tone] || map.info;
|
|
2606
3166
|
return { flex: 1, borderColor: c, color: c };
|
|
2607
3167
|
};
|
|
3168
|
+
const preview = this.state.preview && this.state.preview.scope === 'human' ? this.state.preview : null;
|
|
3169
|
+
// 从展示节点产物里收集 file/dir 类型字段(按该节点 output schema 判定);值非空字符串才作为文件入口
|
|
3170
|
+
const fileFields = [];
|
|
3171
|
+
if (display && typeof display === 'object' && !Array.isArray(display) && displayNode?.body?.output?.properties) {
|
|
3172
|
+
const props = displayNode.body.output.properties;
|
|
3173
|
+
for (const [k, v] of Object.entries(display)) {
|
|
3174
|
+
const t = props[k]?.type;
|
|
3175
|
+
if ((t === 'file' || t === 'dir') && typeof v === 'string' && v.trim()) {
|
|
3176
|
+
fileFields.push({ key: k, value: v.trim(), isDir: t === 'dir' });
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
2608
3180
|
return react.createElement('div', { className: 'knj-card', style: { flexDirection: 'column', alignItems: 'stretch', borderColor: '#7c5cff', background: '#f4f1ff', marginBottom: 12 } },
|
|
2609
3181
|
react.createElement('div', { className: 'knj-row' },
|
|
2610
3182
|
react.createElement('span', { style: { fontWeight: 600, color: '#7c5cff', fontSize: 13 } }, '等待人工处理'),
|
|
2611
3183
|
react.createElement('span', { className: 'knj-badge', style: { background: '#7c5cff' } }, '待审批'),
|
|
2612
3184
|
),
|
|
2613
3185
|
react.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: 8, lineHeight: 1.6 } }, routes.length > 0 ? '任务已运行到人工审批节点。选择一个去向继续(可附意见)。' : '任务已运行到人工审批节点(未配置去向)。'),
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
3186
|
+
// ---- 展示区:文件路径直开 / file-dir 字段 / JSON 文本兜底 ----
|
|
3187
|
+
isPathDisplay
|
|
3188
|
+
? (() => {
|
|
3189
|
+
const btn = react.createElement('button', { className: 'knj-btn', style: { alignSelf: 'flex-start', marginTop: 8, borderColor: 'var(--dsw-static-blue-600)', color: 'var(--dsw-static-blue-600)' }, onClick: () => { if (!this.tryOpenSidebar(displayFrom, displayFrom)) this.openPreview(displayFrom, displayFrom); }, title: '已装 dsh-better-sidebar 时在右侧栏完整预览' }, '查看:' + displayFrom);
|
|
3190
|
+
return preview && preview.path === displayFrom
|
|
3191
|
+
? react.createElement('div', { style: { marginTop: 8, padding: 10, background: '#fff', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } }, this.renderPreviewPanel(preview))
|
|
3192
|
+
: btn;
|
|
3193
|
+
})()
|
|
3194
|
+
: display
|
|
3195
|
+
? react.createElement('div', { style: { marginTop: 8 } },
|
|
3196
|
+
fileFields.length > 0
|
|
3197
|
+
? react.createElement('div', null,
|
|
3198
|
+
react.createElement('div', { className: 'knj-node-sec' }, '待审产物(点击预览)'),
|
|
3199
|
+
fileFields.map((f) => react.createElement('div', { key: f.key, className: 'knj-row', style: { marginTop: 6, gap: 8 } },
|
|
3200
|
+
react.createElement('span', { role: 'button', tabIndex: 0, title: (f.isDir ? '浏览目录:' : '预览文件:') + f.value, style: { fontSize: 12, color: 'var(--dsw-static-blue-600)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }, onClick: () => { if (f.isDir || !this.tryOpenSidebar(f.value, f.key)) this.openPreview(f.value, f.key, 'human'); }, onKeyDown: (e) => { if (e.key === 'Enter') { if (f.isDir || !this.tryOpenSidebar(f.value, f.key)) this.openPreview(f.value, f.key, 'human'); } } }, `${this.fileIcon(f.value.split('.').pop(), f.isDir)} ${f.key}:${f.value}`),
|
|
3201
|
+
)),
|
|
3202
|
+
)
|
|
3203
|
+
: react.createElement('div', { style: { padding: 10, background: '#fff', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } },
|
|
3204
|
+
react.createElement('div', { className: 'knj-node-sec' }, `产物(${displayFrom})`),
|
|
3205
|
+
react.createElement('pre', { style: { fontSize: 12, lineHeight: 1.5, overflow: 'auto', maxHeight: 200, whiteSpace: 'pre-wrap', wordBreak: 'break-all', color: 'var(--dsw-alias-label-primary)', margin: 0 } }, JSON.stringify(display, null, 2)),
|
|
3206
|
+
),
|
|
3207
|
+
preview ? react.createElement('div', { style: { marginTop: 8, padding: 10, background: '#fff', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8 } }, this.renderPreviewPanel(preview)) : null,
|
|
3208
|
+
)
|
|
3209
|
+
: react.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginTop: 8 } }, '(未配置展示产物或产物为空)'),
|
|
2617
3210
|
react.createElement('textarea', { className: 'knj-textarea', style: { minHeight: 56, marginTop: 10 }, placeholder: '意见(可选,去向目标节点会看到)', value: this.state.rejectFeedback, onChange: (e) => this.setState({ rejectFeedback: e.target.value }) }),
|
|
2618
3211
|
react.createElement('div', { className: 'knj-row', style: { marginTop: 12, gap: 10 } },
|
|
2619
3212
|
routes.map((route) => react.createElement('button', { key: route.label + ':' + route.to, className: 'knj-btn', style: btnStyle(route), onClick: () => this.decide(route.label) }, route.label || '(未命名去向)')),
|
|
@@ -2646,10 +3239,20 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
2646
3239
|
react.createElement('div', { className: 'sub', style: { marginTop: 4 } }, `进度 ${pct}% · 完成 ${doneCount}/${relevant.length} 阶段${steps.length - relevant.length ? `(跳过 ${steps.length - relevant.length})` : ''}`),
|
|
2647
3240
|
// 人工审批卡片
|
|
2648
3241
|
task.status === 'waiting-human' ? this.renderHumanCard(task) : null,
|
|
2649
|
-
//
|
|
3242
|
+
// 流程进度:流程图 / 列表 切换(图 = 编排拓扑 + 运行态着色,V2 视觉语言)
|
|
2650
3243
|
react.createElement('div', { className: 'knj-form-section', style: { marginTop: 12 } },
|
|
2651
|
-
react.createElement('div', { className: 'knj-form-label'
|
|
2652
|
-
|
|
3244
|
+
react.createElement('div', { className: 'knj-form-label', style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } },
|
|
3245
|
+
react.createElement('span', null, `流程进度(${steps.length})`),
|
|
3246
|
+
react.createElement('div', { style: { display: 'inline-flex', background: 'var(--dsw-alias-fill-2)', borderRadius: 8, padding: 2, gap: 2 } },
|
|
3247
|
+
['graph', 'list'].map((m) => react.createElement('button', { key: m, onClick: () => this.setState({ viewMode: m }), style: { border: 'none', background: this.state.viewMode === m ? 'var(--dsw-alias-bg-base)' : 'transparent', fontSize: 12.5, padding: '3px 12px', borderRadius: 6, cursor: 'pointer', color: this.state.viewMode === m ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)', fontWeight: this.state.viewMode === m ? 600 : 400, boxShadow: this.state.viewMode === m ? '0 1px 3px rgba(0,0,0,.1)' : 'none' } }, m === 'graph' ? '流程图' : '列表'))),
|
|
3248
|
+
this.state.viewMode === 'graph' ? react.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', fontSize: 11, color: 'var(--dsw-alias-label-secondary)' } },
|
|
3249
|
+
Object.keys(RUN_STATUS).filter((k) => k !== 'passed').map((k) => react.createElement('span', { key: k, style: { display: 'inline-flex', alignItems: 'center', gap: 5 } },
|
|
3250
|
+
react.createElement('span', { style: { width: 3, height: 12, borderRadius: 2, background: RUN_STATUS[k].color, display: 'inline-block' } }), RUN_STATUS[k].label)),
|
|
3251
|
+
) : null,
|
|
3252
|
+
),
|
|
3253
|
+
steps.length > 0 && (this.state.viewMode === 'list' || !(task.workflowSnapshot?.nodes || []).length)
|
|
3254
|
+
? steps.map((s, i) => this.renderNodeCard(s, i))
|
|
3255
|
+
: this.renderRunCanvas(),
|
|
2653
3256
|
),
|
|
2654
3257
|
// 操作按钮
|
|
2655
3258
|
task.status === 'pending'
|
|
@@ -2671,18 +3274,20 @@ table.knj-tbl{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
|
2671
3274
|
}
|
|
2672
3275
|
/** 把某个节点 subagent 会话跳回 DSH 原生会话查看(只读查看执行细节) */
|
|
2673
3276
|
async viewSubagent(childId) {
|
|
2674
|
-
|
|
3277
|
+
// 不使用 apply 阶段可能为 null 的旧缓存;点击时从当前 ctx 动态取 sessions。
|
|
3278
|
+
const sessions = getSessionsService();
|
|
3279
|
+
if (!sessions) { toast('sessions 服务暂不可用,请稍后重试'); return; }
|
|
2675
3280
|
try {
|
|
2676
3281
|
// 先收起开发任务弹窗,再跳子会话,避免弹窗盖住子会话
|
|
2677
3282
|
_closeDevBoard?.();
|
|
2678
3283
|
// catalog 已在 preloadCatalog 后台加载,这里直接 open(select 内部会 navigationAddress 解析)。
|
|
2679
3284
|
// 若 catalog 尚未就绪导致 open 抛错,回退刷新一次再跳。
|
|
2680
3285
|
try {
|
|
2681
|
-
|
|
3286
|
+
sessions.open(childId);
|
|
2682
3287
|
} catch (firstError) {
|
|
2683
3288
|
const { task } = this.props;
|
|
2684
|
-
if (task.parentSessionId) await
|
|
2685
|
-
|
|
3289
|
+
if (task.parentSessionId) await sessions.refreshSubagents(task.parentSessionId);
|
|
3290
|
+
sessions.open(childId);
|
|
2686
3291
|
}
|
|
2687
3292
|
} catch (e) { toast('跳转失败:' + (e && e.message ? e.message : e)); }
|
|
2688
3293
|
}
|