dsh-punky-swarm 0.3.6
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 +120 -0
- package/LICENSE +661 -0
- package/README.en.md +281 -0
- package/README.md +281 -0
- package/cordis.patch.yml +59 -0
- package/lib/acps/certs.js +310 -0
- package/lib/acps/discovery-client.js +283 -0
- package/lib/acps/registry-client.js +527 -0
- package/lib/acps/server.js +302 -0
- package/lib/aip/agent-descriptor.js +170 -0
- package/lib/aip/identity.js +469 -0
- package/lib/aip/tool-descriptor.js +93 -0
- package/lib/api.js +266 -0
- package/lib/artifact-types.js +48 -0
- package/lib/assembly/audit-blind-review.js +170 -0
- package/lib/assembly/schema.js +253 -0
- package/lib/assembly.js +41 -0
- package/lib/assets.js +103 -0
- package/lib/bridge/trajectory.js +202 -0
- package/lib/client.js +704 -0
- package/lib/comms/acps-bridge.js +219 -0
- package/lib/comms/aip-format.js +141 -0
- package/lib/comms/budget.js +124 -0
- package/lib/comms/mailbox.js +221 -0
- package/lib/comms/topic-runtime.js +76 -0
- package/lib/comms/topic.js +115 -0
- package/lib/discovery/filter.js +199 -0
- package/lib/discovery/schema.js +157 -0
- package/lib/discovery/service.js +251 -0
- package/lib/hot/config-watch.js +252 -0
- package/lib/index.js +433 -0
- package/lib/lock.js +69 -0
- package/lib/panel/batch-detail.js +163 -0
- package/lib/panel/batch-list.js +84 -0
- package/lib/panel/locales.js +93 -0
- package/lib/panel/main.js +270 -0
- package/lib/panel/stream.js +348 -0
- package/lib/panel/theme.js +103 -0
- package/lib/panel/widgets.js +63 -0
- package/lib/schema.d.ts +192 -0
- package/lib/schema.js +259 -0
- package/lib/schema.ts +398 -0
- package/lib/state/archive.js +174 -0
- package/lib/state/command-exec.js +148 -0
- package/lib/state/constants.js +37 -0
- package/lib/state/corrupt-registry.js +103 -0
- package/lib/state/event-types.js +65 -0
- package/lib/state/gates.d.ts +226 -0
- package/lib/state/gates.js +428 -0
- package/lib/state/gates.ts +348 -0
- package/lib/state/machine-rules.d.ts +14 -0
- package/lib/state/machine-rules.js +76 -0
- package/lib/state/machine-rules.ts +88 -0
- package/lib/state/machine.js +68 -0
- package/lib/state/resume.js +201 -0
- package/lib/state/schema-v3.d.ts +10 -0
- package/lib/state/schema-v3.js +57 -0
- package/lib/state/schema-v3.ts +80 -0
- package/lib/state/store.js +607 -0
- package/lib/state/task-utils.js +40 -0
- package/lib/tools/core.js +323 -0
- package/lib/tools/git-utils.js +44 -0
- package/lib/tools/lane-tools.js +455 -0
- package/lib/tools/log-tools.js +173 -0
- package/lib/tools/mailbox-tools.js +121 -0
- package/lib/tools/merge-agent.js +127 -0
- package/lib/tools/register.js +85 -0
- package/lib/tools/shared.js +32 -0
- package/lib/types/contracts.d.ts +277 -0
- package/lib/types/contracts.js +17 -0
- package/lib/types/contracts.ts +279 -0
- package/lib/verify/evidence.js +249 -0
- package/lib/verify/gate.js +161 -0
- package/lib/verify/mount.js +48 -0
- package/lib/verify/selector.js +101 -0
- package/lib/watch/lane-heartbeat.js +333 -0
- package/lib/wave-plan.d.ts +48 -0
- package/lib/wave-plan.js +473 -0
- package/lib/wave-plan.ts +465 -0
- package/package.json +71 -0
- package/presets/jiufeng/NOTICE +5 -0
- package/presets/jiufeng/agent.cordis.yml +275 -0
- package/presets/jiufeng/preset.yml +3 -0
- package/skills/jiufeng-team/SKILL.md +214 -0
- package/skills/jiufeng-team/references/constitution.md +62 -0
- package/skills/jiufeng-team/references/roles/coder.md +17 -0
- package/skills/jiufeng-team/references/roles/coordinator.md +13 -0
- package/skills/jiufeng-team/references/roles/designer.md +14 -0
- package/skills/jiufeng-team/references/roles/doc-manager.md +13 -0
- package/skills/jiufeng-team/references/roles/manager.md +22 -0
- package/skills/jiufeng-team/references/roles/reviewer.md +19 -0
- package/skills/jiufeng-team/references/roles/supervisor.md +13 -0
- package/skills/jiufeng-team/references/roles/tester.md +13 -0
- package/skills/jiufeng-team/references/templates/call-chain-matrix-template.md +121 -0
- package/skills/jiufeng-team/references/templates/endpoint-behavior-template.md +60 -0
- package/skills/jiufeng-team/references/templates/gap-list.json +33 -0
- package/skills/jiufeng-team/references/templates/leader-decision-pack.md +92 -0
- package/skills/jiufeng-team/references/templates/plan-template.md +61 -0
- package/skills/jiufeng-team/references/templates/success-pattern-seeds.md +126 -0
- package/skills/jiufeng-team/references/workflow.md +98 -0
- package/skills/jiufeng-team/scripts/check-v3-density.mjs +98 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (C) 2025-2026 Punky
|
|
3
|
+
|
|
4
|
+
This program is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU Affero General Public License as
|
|
6
|
+
published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
window.__ModuleLoader__.load({
|
|
19
|
+
id: "dsh-punky-swarm",
|
|
20
|
+
factory: (require) => {
|
|
21
|
+
var module = { exports: {} };
|
|
22
|
+
var exports = module.exports;
|
|
23
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
24
|
+
"use strict";
|
|
25
|
+
// dsh-punky-swarm 蟛蜞集群监控面板客户端(只读):注册 conversation.view 第三分页(对话/轨迹/蟛蜞集群)
|
|
26
|
+
const React = require('react');
|
|
27
|
+
const { useState, useEffect } = React;
|
|
28
|
+
|
|
29
|
+
// ===== [panel-segment] locales.js =====
|
|
30
|
+
const zh = {
|
|
31
|
+
"view.cluster": "蟛蜞集群",
|
|
32
|
+
"live": "实时",
|
|
33
|
+
"refresh.auto": "3s 自动刷新",
|
|
34
|
+
"stream.live": "实时推送",
|
|
35
|
+
"stream.fallback": "已降级 3s 轮询",
|
|
36
|
+
"updated": "更新于",
|
|
37
|
+
"stat.total": "总批次",
|
|
38
|
+
"stat.running": "运行中",
|
|
39
|
+
"stat.done": "已完结",
|
|
40
|
+
"stat.issues": "异常",
|
|
41
|
+
"batch.title": "批次列表",
|
|
42
|
+
"batch.progress": "进度",
|
|
43
|
+
"batch.release": "可自动放行",
|
|
44
|
+
"batch.done": "已完结",
|
|
45
|
+
"lanes": "子任务",
|
|
46
|
+
"events": "事件",
|
|
47
|
+
"concurrency": "并发",
|
|
48
|
+
"event.timeline": "事件时间线",
|
|
49
|
+
"mailbox.title": "收件箱(只读)",
|
|
50
|
+
"mailbox.inbox": "派发",
|
|
51
|
+
"mailbox.broadcast": "广播",
|
|
52
|
+
"mailbox.hint": "暂无未读",
|
|
53
|
+
"empty": "暂无批次",
|
|
54
|
+
"empty.hint": "创建 wave_plan 批次后在此查看",
|
|
55
|
+
"load.error": "加载失败",
|
|
56
|
+
"gate.missing": "缺",
|
|
57
|
+
"attempt": "返工",
|
|
58
|
+
"upgrade": "升级人工",
|
|
59
|
+
"task.deps": "依赖",
|
|
60
|
+
"task.layer": "层",
|
|
61
|
+
"gate.consume": "消费缺失",
|
|
62
|
+
"gate.outputs": "产物缺失",
|
|
63
|
+
"gate.produce": "产出缺失",
|
|
64
|
+
"gate.contract": "契约问题"
|
|
65
|
+
};
|
|
66
|
+
const en = {
|
|
67
|
+
"view.cluster": "Punky swarm",
|
|
68
|
+
"live": "Live",
|
|
69
|
+
"refresh.auto": "3s auto refresh",
|
|
70
|
+
"stream.live": "Live push",
|
|
71
|
+
"stream.fallback": "3s polling fallback",
|
|
72
|
+
"updated": "updated",
|
|
73
|
+
"stat.total": "Batches",
|
|
74
|
+
"stat.running": "Running",
|
|
75
|
+
"stat.done": "Done",
|
|
76
|
+
"stat.issues": "Issues",
|
|
77
|
+
"batch.title": "Batches",
|
|
78
|
+
"batch.progress": "progress",
|
|
79
|
+
"batch.release": "auto-release",
|
|
80
|
+
"batch.done": "done",
|
|
81
|
+
"lanes": "lanes",
|
|
82
|
+
"events": "events",
|
|
83
|
+
"concurrency": "concurrency",
|
|
84
|
+
"event.timeline": "Event timeline",
|
|
85
|
+
"mailbox.title": "Inbox (read-only)",
|
|
86
|
+
"mailbox.inbox": "dispatch",
|
|
87
|
+
"mailbox.broadcast": "broadcast",
|
|
88
|
+
"mailbox.hint": "nothing unread",
|
|
89
|
+
"empty": "No batches",
|
|
90
|
+
"empty.hint": "Create a wave_plan batch to see it here",
|
|
91
|
+
"load.error": "Load failed",
|
|
92
|
+
"gate.missing": "missing",
|
|
93
|
+
"attempt": "rework",
|
|
94
|
+
"upgrade": "escalate",
|
|
95
|
+
"task.deps": "deps",
|
|
96
|
+
"task.layer": "layer",
|
|
97
|
+
"gate.consume": "consume missing",
|
|
98
|
+
"gate.outputs": "outputs missing",
|
|
99
|
+
"gate.produce": "produce missing",
|
|
100
|
+
"gate.contract": "contract problem"
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// module-level translator: zh-first, en fallback (matches the original panel behavior)
|
|
104
|
+
function tt(k) { return zh[k] || en[k] || k; }
|
|
105
|
+
// ===== [panel-segment] theme.js =====
|
|
106
|
+
// ================= theme-aware palette =================
|
|
107
|
+
// DSH 主题开关:body[data-ds-dark-theme](深色)vs 默认浅色。令牌 var(--dsw-alias-*)
|
|
108
|
+
// 会随主题自动切换;这里只负责「令牌缺失时」的兜底色板,并按主题切换重渲染。
|
|
109
|
+
let CURRENT_THEME = 'dark';
|
|
110
|
+
function detectTheme() {
|
|
111
|
+
try {
|
|
112
|
+
if (typeof document !== 'undefined' && document.body) {
|
|
113
|
+
return document.body.hasAttribute('data-ds-dark-theme') ? 'dark' : 'light';
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
return 'dark';
|
|
117
|
+
}
|
|
118
|
+
try { CURRENT_THEME = detectTheme(); } catch {}
|
|
119
|
+
|
|
120
|
+
const P = {
|
|
121
|
+
light: {
|
|
122
|
+
card: '#ffffff', border: '#dfe5ee',
|
|
123
|
+
text: '#1f2937', text2: '#475569', text3: '#64748b', dim: '#64748b',
|
|
124
|
+
accent: '#3b82f6', success: '#15803d', warn: '#b45309', error: '#dc2626', info: '#2563eb',
|
|
125
|
+
skeleton: '#eef2f7', selBg: 'rgba(59,130,246,0.10)',
|
|
126
|
+
chipPending: 'rgba(100,116,139,0.20)', chipRunning: 'rgba(180,83,9,0.12)',
|
|
127
|
+
chipReview: 'rgba(37,99,235,0.10)', chipMerged: 'rgba(21,128,61,0.12)',
|
|
128
|
+
chipFailed: 'rgba(220,38,38,0.10)', chipSkipped: 'rgba(100,116,139,0.16)',
|
|
129
|
+
chipConflict: 'rgba(234,88,12,0.12)', chipIdle: 'rgba(100,116,139,0.14)',
|
|
130
|
+
haloSuccess: 'rgba(21,128,61,0.16)', haloWarn: 'rgba(180,83,9,0.16)',
|
|
131
|
+
gateBg: 'rgba(180,83,9,0.12)', escBg: 'rgba(234,88,12,0.12)', escFg: '#c2410c'
|
|
132
|
+
},
|
|
133
|
+
dark: {
|
|
134
|
+
card: '#141d31', border: '#26304a',
|
|
135
|
+
text: '#e6ebf4', text2: '#a8b3c7', text3: '#7f8ca3', dim: '#8b96ab',
|
|
136
|
+
accent: '#4f8cff', success: '#3fb950', warn: '#d29922', error: '#f85149', info: '#58a6ff',
|
|
137
|
+
skeleton: '#1d2740', selBg: 'rgba(79,140,255,0.12)',
|
|
138
|
+
chipPending: 'rgba(127,140,163,0.22)', chipRunning: 'rgba(210,153,34,0.16)',
|
|
139
|
+
chipReview: 'rgba(88,166,255,0.16)', chipMerged: 'rgba(63,185,80,0.16)',
|
|
140
|
+
chipFailed: 'rgba(248,81,73,0.16)', chipSkipped: 'rgba(127,140,163,0.18)',
|
|
141
|
+
chipConflict: 'rgba(224,104,46,0.18)', chipIdle: 'rgba(127,140,163,0.16)',
|
|
142
|
+
haloSuccess: 'rgba(63,185,80,0.18)', haloWarn: 'rgba(210,153,34,0.18)',
|
|
143
|
+
gateBg: 'rgba(210,153,34,0.16)', escBg: 'rgba(224,104,46,0.16)', escFg: '#e0682e'
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const pal = () => P[CURRENT_THEME] || P.dark;
|
|
147
|
+
|
|
148
|
+
// 令牌优先、兜底随主题:getter 在每次渲染时求值
|
|
149
|
+
const T = {
|
|
150
|
+
get card() { return 'var(--dsw-alias-bg-layer-3, ' + pal().card + ')'; },
|
|
151
|
+
get border() { return 'var(--dsw-alias-border-l1, ' + pal().border + ')'; },
|
|
152
|
+
get text() { return 'var(--dsw-alias-label-primary, ' + pal().text + ')'; },
|
|
153
|
+
get text2() { return 'var(--dsw-alias-label-secondary, ' + pal().text2 + ')'; },
|
|
154
|
+
get text3() { return 'var(--dsw-alias-label-tertiary, ' + pal().text3 + ')'; },
|
|
155
|
+
get dim() { return 'var(--dsw-alias-label-dimmed, ' + pal().dim + ')'; },
|
|
156
|
+
get accent() { return 'var(--dsw-alias-brand-primary, ' + pal().accent + ')'; },
|
|
157
|
+
get success() { return 'var(--dsw-alias-state-success-primary, ' + pal().success + ')'; },
|
|
158
|
+
get warn() { return 'var(--dsw-alias-state-warn-primary, ' + pal().warn + ')'; },
|
|
159
|
+
get error() { return 'var(--dsw-alias-state-error-primary, ' + pal().error + ')'; },
|
|
160
|
+
get info() { return 'var(--dsw-alias-state-business-primary, ' + pal().info + ')'; },
|
|
161
|
+
get skeleton() { return 'var(--dsw-alias-bg-skeleton, ' + pal().skeleton + ')'; },
|
|
162
|
+
get selBg() { return 'var(--dsw-alias-interactive-bg-active, ' + pal().selBg + ')'; },
|
|
163
|
+
get font() { return 'var(--dsw-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif)'; },
|
|
164
|
+
get mono() { return 'var(--dsw-font-mono, "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace)'; }
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function chip(fgKey, bgKey, fgVar) {
|
|
168
|
+
return {
|
|
169
|
+
get fg() { return fgVar ? 'var(' + fgVar + ', ' + pal()[fgKey] + ')' : pal()[fgKey]; },
|
|
170
|
+
get bg() { return pal()[bgKey]; }
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const STATE = {
|
|
174
|
+
pending: chip('text2', 'chipPending', '--dsw-alias-label-secondary'),
|
|
175
|
+
running: chip('warn', 'chipRunning', '--dsw-alias-state-warn-primary'),
|
|
176
|
+
review: chip('info', 'chipReview', '--dsw-alias-state-business-primary'),
|
|
177
|
+
merged: chip('success', 'chipMerged', '--dsw-alias-state-success-primary'),
|
|
178
|
+
failed: chip('error', 'chipFailed', '--dsw-alias-state-error-primary'),
|
|
179
|
+
skipped: chip('text2', 'chipSkipped', '--dsw-alias-label-secondary'),
|
|
180
|
+
conflict: chip('escFg', 'chipConflict', '--dsw-alias-state-warn-primary'),
|
|
181
|
+
idle: chip('text2', 'chipIdle', '--dsw-alias-label-secondary')
|
|
182
|
+
};
|
|
183
|
+
const PHASE = {
|
|
184
|
+
planning: chip('text2', 'chipSkipped', '--dsw-alias-label-secondary'),
|
|
185
|
+
running: chip('warn', 'chipRunning', '--dsw-alias-state-warn-primary'),
|
|
186
|
+
paused: chip('info', 'chipReview', '--dsw-alias-state-business-primary'),
|
|
187
|
+
aborted: chip('error', 'chipFailed', '--dsw-alias-state-error-primary'),
|
|
188
|
+
complete: chip('success', 'chipMerged', '--dsw-alias-state-success-primary')
|
|
189
|
+
};
|
|
190
|
+
// =======================================================
|
|
191
|
+
// ===== [panel-segment] widgets.js =====
|
|
192
|
+
function Chip({ st, children, style }) {
|
|
193
|
+
const c = st || chip('text2', 'chipPending', '--dsw-alias-label-secondary');
|
|
194
|
+
return React.createElement('span', {
|
|
195
|
+
style: Object.assign({
|
|
196
|
+
display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
197
|
+
color: c.fg, background: c.bg,
|
|
198
|
+
borderRadius: 999, padding: '1px 8px', fontSize: 10.5,
|
|
199
|
+
fontWeight: 600, letterSpacing: 0.2, lineHeight: '16px',
|
|
200
|
+
fontFamily: T.mono, whiteSpace: 'nowrap'
|
|
201
|
+
}, style || null)
|
|
202
|
+
}, children);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function Dot({ color }) {
|
|
206
|
+
return React.createElement('span', { style: { width: 6, height: 6, borderRadius: 999, background: color, display: 'inline-block' } });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function Progress({ value, color, height }) {
|
|
210
|
+
const h = height || 4;
|
|
211
|
+
return React.createElement('div', {
|
|
212
|
+
style: { height: h, borderRadius: 999, background: T.skeleton, overflow: 'hidden', flex: 1 }
|
|
213
|
+
}, React.createElement('div', {
|
|
214
|
+
style: { width: Math.max(0, Math.min(100, value)) + '%', height: '100%', borderRadius: 999, background: color || T.accent, transition: 'width .3s ease' }
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function Stat({ label, value, color }) {
|
|
219
|
+
return React.createElement('div', {
|
|
220
|
+
style: Object.assign({}, cardBase, { padding: '6px 10px', minWidth: 0, flex: 1, display: 'flex', flexDirection: 'column', gap: 2 })
|
|
221
|
+
},
|
|
222
|
+
React.createElement('span', { style: { fontSize: 10.5, color: T.text3, lineHeight: 1.2 } }, label),
|
|
223
|
+
React.createElement('span', { style: { fontSize: 17, fontWeight: 700, fontFamily: T.mono, color: color || T.text, lineHeight: 1.15, fontVariantNumeric: 'tabular-nums' } }, value)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function SectionTitle({ children }) {
|
|
228
|
+
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 } },
|
|
229
|
+
React.createElement('span', { style: { width: 3, height: 12, borderRadius: 999, background: T.accent } }),
|
|
230
|
+
React.createElement('span', { style: { fontSize: 11.5, fontWeight: 600, color: T.text, letterSpacing: 0.3 } }, children)
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function Skeleton({ h, w, style }) {
|
|
235
|
+
return React.createElement('div', { className: 'psw-shimmer', style: Object.assign({ height: h || 12, width: w || '100%', borderRadius: 6 }, style || null) });
|
|
236
|
+
}
|
|
237
|
+
// ===== [panel-segment] batch-list.js =====
|
|
238
|
+
function BatchList({ batches, selected, onSelect, loading }) {
|
|
239
|
+
return React.createElement('div', {
|
|
240
|
+
className: 'psw-list psw-scroll',
|
|
241
|
+
style: { width: 276, flex: 'none', display: 'flex', flexDirection: 'column', gap: 8, overflowY: 'auto', paddingRight: 4 }
|
|
242
|
+
},
|
|
243
|
+
React.createElement(SectionTitle, null, tt('batch.title') + ' · ' + (batches ? batches.length : 0)),
|
|
244
|
+
loading && !batches
|
|
245
|
+
? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
246
|
+
React.createElement(Skeleton, { h: 52 }),
|
|
247
|
+
React.createElement(Skeleton, { h: 52 }),
|
|
248
|
+
React.createElement(Skeleton, { h: 52 })
|
|
249
|
+
)
|
|
250
|
+
: batches && batches.length
|
|
251
|
+
? batches.map((b) => {
|
|
252
|
+
const st = PHASE[b.phase] || PHASE.planning;
|
|
253
|
+
const vals = Object.values(b.lanes || {});
|
|
254
|
+
const done = vals.filter((s) => TERMINAL.indexOf(s) >= 0).length;
|
|
255
|
+
const total = vals.length;
|
|
256
|
+
const sel = selected && (typeof selected === 'object'
|
|
257
|
+
? selected.session === b.session && selected.batchId === b.batchId
|
|
258
|
+
: selected === b.batchId);
|
|
259
|
+
return React.createElement('button', {
|
|
260
|
+
key: b.session ? b.session + ':' + b.batchId : b.batchId,
|
|
261
|
+
type: 'button',
|
|
262
|
+
className: 'psw-btn',
|
|
263
|
+
onClick: () => onSelect(b),
|
|
264
|
+
'aria-pressed': sel,
|
|
265
|
+
style: Object.assign({}, cardBase, {
|
|
266
|
+
textAlign: 'left', cursor: 'pointer', padding: '8px 10px',
|
|
267
|
+
display: 'flex', flexDirection: 'column', gap: 6, width: '100%',
|
|
268
|
+
...(sel ? { borderColor: T.accent, boxShadow: '0 0 0 1px ' + T.accent } : {}),
|
|
269
|
+
background: sel ? T.selBg : T.card
|
|
270
|
+
})
|
|
271
|
+
},
|
|
272
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 } },
|
|
273
|
+
React.createElement(Chip, { st: st }, b.phase),
|
|
274
|
+
React.createElement('span', { style: { flex: 1, fontWeight: 700, fontSize: 12.5, fontFamily: T.mono, color: T.text, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, b.batchId)
|
|
275
|
+
),
|
|
276
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
277
|
+
React.createElement(Progress, { value: total ? (done / total) * 100 : 0, color: st.fg, height: 3 }),
|
|
278
|
+
React.createElement('span', { style: { fontSize: 10.5, color: T.text3, fontFamily: T.mono, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' } }, done + '/' + total)
|
|
279
|
+
),
|
|
280
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 10.5, color: T.text3 } },
|
|
281
|
+
b.sessionShort && !b.isOwnSession
|
|
282
|
+
? React.createElement('span', { title: b.session, style: { fontFamily: T.mono, opacity: 0.9, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 110 } }, b.sessionShort + ' ·')
|
|
283
|
+
: null,
|
|
284
|
+
b.autoReleaseable
|
|
285
|
+
? React.createElement(React.Fragment, null, React.createElement(Dot, { color: T.success }), React.createElement('span', null, tt('batch.release')))
|
|
286
|
+
: b.phase === 'complete' || b.phase === 'aborted'
|
|
287
|
+
? React.createElement('span', null, tt('batch.done'))
|
|
288
|
+
: React.createElement('span', null, tt('batch.progress') + ' · ' + (b.concurrency != null ? tt('concurrency') + '=' + b.concurrency : ''))
|
|
289
|
+
)
|
|
290
|
+
);
|
|
291
|
+
})
|
|
292
|
+
: React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, padding: '28px 12px', color: T.text3, textAlign: 'center' } },
|
|
293
|
+
React.createElement('svg', { width: 36, height: 36, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, 'aria-hidden': 'true' },
|
|
294
|
+
React.createElement('rect', { x: 3, y: 3, width: 7, height: 7, rx: 1.5 }),
|
|
295
|
+
React.createElement('rect', { x: 14, y: 3, width: 7, height: 7, rx: 1.5 }),
|
|
296
|
+
React.createElement('rect', { x: 3, y: 14, width: 7, height: 7, rx: 1.5 }),
|
|
297
|
+
React.createElement('rect', { x: 14, y: 14, width: 7, height: 7, rx: 1.5 })
|
|
298
|
+
),
|
|
299
|
+
React.createElement('div', { style: { fontSize: 12.5, fontWeight: 600, color: T.text2 } }, tt('empty')),
|
|
300
|
+
React.createElement('div', { style: { fontSize: 11, lineHeight: 1.5 } }, tt('empty.hint'))
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
// ===== [panel-segment] batch-detail.js =====
|
|
305
|
+
function GateBadge({ gate }) {
|
|
306
|
+
if (!gate || !gate.layer) return null;
|
|
307
|
+
const items = [];
|
|
308
|
+
const seen = {};
|
|
309
|
+
const add = (kind, p) => { if (!seen[p]) { seen[p] = 1; items.push({ kind: kind, path: p }); } };
|
|
310
|
+
for (const p of gate.consumeMissing || []) add('consume', p);
|
|
311
|
+
for (const p of gate.outputsMissing || []) add('outputs', p);
|
|
312
|
+
for (const p of gate.produceMissing || []) add('produce', p);
|
|
313
|
+
for (const p of gate.contractProblems || []) {
|
|
314
|
+
const m = String(p).match(/^(.+?)\s+missing$/i);
|
|
315
|
+
if (m) add('contract', m[1]); else add('contract', String(p));
|
|
316
|
+
}
|
|
317
|
+
if (!items.length) return null;
|
|
318
|
+
const KIND = { consume: tt('gate.consume'), outputs: tt('gate.outputs'), produce: tt('gate.produce'), contract: tt('gate.contract') };
|
|
319
|
+
return React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 4 } },
|
|
320
|
+
items.map((it, i) => React.createElement('span', {
|
|
321
|
+
key: i,
|
|
322
|
+
title: KIND[it.kind] + ': ' + it.path,
|
|
323
|
+
style: { color: T.warn, background: pal().gateBg, borderRadius: 999, padding: '1px 7px', fontSize: 10, fontWeight: 600, fontFamily: T.mono, whiteSpace: 'nowrap' }
|
|
324
|
+
}, tt('gate.missing') + ' ' + it.path))
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function AttemptBadge({ n, upgrade }) {
|
|
329
|
+
if (!n && !upgrade) return null;
|
|
330
|
+
const label = (upgrade ? tt('upgrade') : tt('attempt')) + ' ' + n;
|
|
331
|
+
return React.createElement('span', {
|
|
332
|
+
style: upgrade
|
|
333
|
+
? { color: pal().escFg, background: pal().escBg, borderRadius: 999, padding: '1px 7px', fontSize: 10, fontWeight: 600, fontFamily: T.mono, whiteSpace: 'nowrap' }
|
|
334
|
+
: { color: T.text2, fontSize: 10.5, fontFamily: T.mono, whiteSpace: 'nowrap' }
|
|
335
|
+
}, label);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function LaneCard({ lane, state, attempt, upgrade, gate, meta }) {
|
|
339
|
+
const st = STATE[state] || STATE.pending;
|
|
340
|
+
const m = meta || {};
|
|
341
|
+
const deps = m.deps && m.deps.length ? m.deps : null;
|
|
342
|
+
return React.createElement('div', {
|
|
343
|
+
className: 'psw-card',
|
|
344
|
+
style: Object.assign({}, cardBase, {
|
|
345
|
+
flex: '1 1 168px', padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: 6,
|
|
346
|
+
borderLeft: '3px solid ' + st.fg
|
|
347
|
+
})
|
|
348
|
+
},
|
|
349
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 } },
|
|
350
|
+
React.createElement('span', { style: { fontSize: 11.5, fontWeight: 600, fontFamily: T.mono, color: T.text, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 } }, lane),
|
|
351
|
+
React.createElement(Chip, { st: st }, state)
|
|
352
|
+
),
|
|
353
|
+
m.cmd ? React.createElement('div', {
|
|
354
|
+
style: { fontSize: 11, color: T.text2, lineHeight: 1.45, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }
|
|
355
|
+
}, m.cmd) : null,
|
|
356
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
357
|
+
React.createElement(GateBadge, { gate }),
|
|
358
|
+
React.createElement(AttemptBadge, { n: attempt, upgrade }),
|
|
359
|
+
m.layer ? React.createElement('span', { style: { fontSize: 10, color: T.text3, fontFamily: T.mono } }, tt('task.layer') + ' ' + m.layer) : null,
|
|
360
|
+
deps ? React.createElement('span', { style: { fontSize: 10, color: T.dim, fontFamily: T.mono } }, tt('task.deps') + ': ' + deps.join(', ')) : null
|
|
361
|
+
)
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function EventRow({ e }) {
|
|
366
|
+
const kind = String(e.type || '').toLowerCase();
|
|
367
|
+
const color = kind.indexOf('running') >= 0 ? T.warn : kind.indexOf('fail') >= 0 || kind.indexOf('conflict') >= 0 ? T.error : kind.indexOf('merge') >= 0 ? T.success : kind.indexOf('review') >= 0 ? T.info : T.text3;
|
|
368
|
+
const label = e.type + (e.lane ? ':' + e.lane : '') + (e.from ? ' ' + e.from + '->' + e.to : '');
|
|
369
|
+
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '3px 0' } },
|
|
370
|
+
React.createElement(Dot, { color: color }),
|
|
371
|
+
React.createElement('span', { style: { flex: 1, fontSize: 11.5, color: T.text2, fontFamily: T.mono, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, label),
|
|
372
|
+
React.createElement('span', { style: { fontSize: 10.5, color: T.dim, fontFamily: T.mono, fontVariantNumeric: 'tabular-nums' } }, (e.ts || '').slice(11, 19))
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
function BatchDetail({ d }) {
|
|
376
|
+
if (!d) {
|
|
377
|
+
return React.createElement('div', { style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 10, overflowY: 'auto' } },
|
|
378
|
+
React.createElement(Skeleton, { h: 64 }),
|
|
379
|
+
React.createElement(Skeleton, { h: 40 }),
|
|
380
|
+
React.createElement(Skeleton, { h: 120 })
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const lanes = Object.keys(d.lanes || {});
|
|
384
|
+
const vals = Object.values(d.lanes || {});
|
|
385
|
+
const done = vals.filter((s) => TERMINAL.indexOf(s) >= 0).length;
|
|
386
|
+
const issues = vals.filter((s) => s === 'failed' || s === 'conflict').length;
|
|
387
|
+
const st = PHASE[d.phase] || PHASE.planning;
|
|
388
|
+
const evs = (d.recentEvents || []).slice().reverse();
|
|
389
|
+
const mail = d.mail || { inbox: [], broadcast: [] };
|
|
390
|
+
const laneMeta = {};
|
|
391
|
+
for (const w of d.wavePlan || []) {
|
|
392
|
+
for (const t of w.tasks || []) {
|
|
393
|
+
laneMeta[t.id] = { cmd: t.cmd || '', layer: t.layer || null, role: t.role || null, deps: t.deps || [] };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return React.createElement('div', { className: 'psw-scroll', style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 10, overflowY: 'auto', paddingRight: 4 } },
|
|
397
|
+
React.createElement('div', { className: 'psw-card', style: Object.assign({}, cardBase, { padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 8 }) },
|
|
398
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 } },
|
|
399
|
+
React.createElement(Chip, { st: st }, d.phase + (d.viewSettled ? ' ✓' : '')),
|
|
400
|
+
React.createElement('span', { style: { fontWeight: 700, fontSize: 14, fontFamily: T.mono, color: T.text, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, d.batchId),
|
|
401
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
402
|
+
d.autoReleaseable
|
|
403
|
+
? React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 5, color: T.success, fontSize: 11, fontWeight: 600 } },
|
|
404
|
+
React.createElement(Dot, { color: T.success }), tt('batch.release'))
|
|
405
|
+
: null
|
|
406
|
+
),
|
|
407
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
408
|
+
React.createElement(Progress, { value: lanes.length ? (done / lanes.length) * 100 : 0, color: issues ? T.error : (lanes.length && done === lanes.length) ? T.success : (d.phase === 'running' || d.phase === 'paused') ? T.warn : T.dim }),
|
|
409
|
+
React.createElement('span', { style: { fontSize: 10.5, color: T.text3, fontFamily: T.mono, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' } }, done + '/' + lanes.length)
|
|
410
|
+
),
|
|
411
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, fontSize: 10.5, color: T.text3, flexWrap: 'wrap' } },
|
|
412
|
+
React.createElement('span', null, tt('lanes') + ' ' + lanes.length),
|
|
413
|
+
React.createElement('span', null, tt('events') + ' ' + (d.eventCount != null ? d.eventCount : (d.recentEvents || []).length)),
|
|
414
|
+
React.createElement('span', null, tt('concurrency') + '=' + d.concurrency)
|
|
415
|
+
)
|
|
416
|
+
),
|
|
417
|
+
React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
418
|
+
lanes.map((lane) => React.createElement(LaneCard, {
|
|
419
|
+
key: lane, lane: lane, state: d.lanes[lane],
|
|
420
|
+
attempt: (d.laneAttempts || {})[lane], upgrade: (d.upgrades || {})[lane],
|
|
421
|
+
gate: (d.lanesGate || {})[lane], meta: laneMeta[lane]
|
|
422
|
+
}))
|
|
423
|
+
),
|
|
424
|
+
React.createElement('div', { className: 'psw-card', style: Object.assign({}, cardBase, { padding: '10px 12px' }) },
|
|
425
|
+
React.createElement(SectionTitle, null, tt('event.timeline')),
|
|
426
|
+
evs.length
|
|
427
|
+
? React.createElement('div', { style: { display: 'flex', flexDirection: 'column' } }, evs.map((e, i) => React.createElement(EventRow, { key: i, e })))
|
|
428
|
+
: React.createElement('div', { style: { fontSize: 11.5, color: T.text3, padding: '4px 0' } }, tt('empty'))
|
|
429
|
+
),
|
|
430
|
+
React.createElement('div', { className: 'psw-card', style: Object.assign({}, cardBase, { padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 6 }) },
|
|
431
|
+
React.createElement(SectionTitle, null, tt('mailbox.title')),
|
|
432
|
+
React.createElement('div', { style: { display: 'flex', gap: 8 } },
|
|
433
|
+
React.createElement('div', { style: { flex: 1, display: 'flex', alignItems: 'center', gap: 6, background: T.skeleton, borderRadius: 8, padding: '6px 10px' } },
|
|
434
|
+
React.createElement(Dot, { color: T.info }),
|
|
435
|
+
React.createElement('span', { style: { fontSize: 11, color: T.text2 } }, tt('mailbox.inbox')),
|
|
436
|
+
React.createElement('span', { style: { marginLeft: 'auto', fontWeight: 700, fontSize: 13, fontFamily: T.mono, color: T.text, fontVariantNumeric: 'tabular-nums' } }, mail.inbox.length)
|
|
437
|
+
),
|
|
438
|
+
React.createElement('div', { style: { flex: 1, display: 'flex', alignItems: 'center', gap: 6, background: T.skeleton, borderRadius: 8, padding: '6px 10px' } },
|
|
439
|
+
React.createElement(Dot, { color: T.warn }),
|
|
440
|
+
React.createElement('span', { style: { fontSize: 11, color: T.text2 } }, tt('mailbox.broadcast')),
|
|
441
|
+
React.createElement('span', { style: { marginLeft: 'auto', fontWeight: 700, fontSize: 13, fontFamily: T.mono, color: T.text, fontVariantNumeric: 'tabular-nums' } }, mail.broadcast.length)
|
|
442
|
+
)
|
|
443
|
+
),
|
|
444
|
+
mail.inbox.length + mail.broadcast.length === 0
|
|
445
|
+
? React.createElement('div', { style: { fontSize: 11, color: T.text3 } }, tt('mailbox.hint'))
|
|
446
|
+
: null
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
// ===== [panel-segment] main.js =====
|
|
451
|
+
const NS = 'dsh-punky-swarm';
|
|
452
|
+
const inject = ['slots', 'locale'];
|
|
453
|
+
async function api(path, session) {
|
|
454
|
+
const base = '/api/dsh-punky-swarm' + path;
|
|
455
|
+
const url = session
|
|
456
|
+
? base + (base.includes('?') ? '&' : '?') + 'session=' + encodeURIComponent(session)
|
|
457
|
+
: base;
|
|
458
|
+
const res = await fetch(url);
|
|
459
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
460
|
+
return res.json();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 跨会话聚合:/sessions 取全部有批次的会话 → 逐会话 /batches 合并为统一列表;
|
|
464
|
+
// 当前对话会话(sid)批次优先,其后按会话展示;列表项携带 session 归属字段
|
|
465
|
+
async function aggregateBatches(sid) {
|
|
466
|
+
let sessions = [];
|
|
467
|
+
try {
|
|
468
|
+
const r = await api('/sessions');
|
|
469
|
+
sessions = (r && r.sessions) || [];
|
|
470
|
+
} catch {}
|
|
471
|
+
const ids = [];
|
|
472
|
+
if (sid) ids.push(sid);
|
|
473
|
+
for (const s of sessions) {
|
|
474
|
+
if (s && s.sessionId && s.sessionId !== sid) ids.push(s.sessionId);
|
|
475
|
+
}
|
|
476
|
+
const out = [];
|
|
477
|
+
for (const id of ids) {
|
|
478
|
+
let items = [];
|
|
479
|
+
try {
|
|
480
|
+
const r = await api('/batches', id);
|
|
481
|
+
items = (r && r.batches) || [];
|
|
482
|
+
} catch {}
|
|
483
|
+
for (const b of items) {
|
|
484
|
+
out.push(Object.assign({}, b, {
|
|
485
|
+
session: id,
|
|
486
|
+
sessionShort: String(id).slice(0, 8),
|
|
487
|
+
isOwnSession: id === sid
|
|
488
|
+
}));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 浏览器端 TERMINAL 副本:Node 端单点 = lib/state/constants.js(P1-07 收敛);
|
|
495
|
+
// 面板段经 window.__ModuleLoader__ 拼接执行(无 ESM import 能力),此处为手工同步副本,
|
|
496
|
+
// batch-list/batch-detail 段共享本作用域引用(渲染时求值)。
|
|
497
|
+
const TERMINAL = ['merged', 'failed', 'skipped', 'conflict'];
|
|
498
|
+
const cardBase = {
|
|
499
|
+
get background() { return T.card; },
|
|
500
|
+
get borderWidth() { return 1; },
|
|
501
|
+
get borderStyle() { return 'solid'; },
|
|
502
|
+
get borderColor() { return T.border; },
|
|
503
|
+
borderRadius: 10
|
|
504
|
+
};
|
|
505
|
+
function ClusterWorkbench({ sessionId }) {
|
|
506
|
+
const [batches, setBatches] = useState(null);
|
|
507
|
+
const [sel, setSel] = useState(null);
|
|
508
|
+
const [detail, setDetail] = useState(null);
|
|
509
|
+
const [updated, setUpdated] = useState(null);
|
|
510
|
+
const [mode, setMode] = useState('sse'); // 'sse' | 'poll'(R3 SSE 降级回轮询状态,D6 简化②)
|
|
511
|
+
const [, setThemeTick] = useState(0);
|
|
512
|
+
const sid = sessionId || '';
|
|
513
|
+
|
|
514
|
+
// 跟随 web UI 主题(body[data-ds-dark-theme])切换兜底色板并重渲染
|
|
515
|
+
useEffect(() => {
|
|
516
|
+
const apply = () => {
|
|
517
|
+
const t = detectTheme();
|
|
518
|
+
if (t !== CURRENT_THEME) { CURRENT_THEME = t; setThemeTick((x) => x + 1); }
|
|
519
|
+
};
|
|
520
|
+
let mo = null;
|
|
521
|
+
try {
|
|
522
|
+
if (typeof MutationObserver !== 'undefined' && document.body) {
|
|
523
|
+
mo = new MutationObserver(apply);
|
|
524
|
+
mo.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] });
|
|
525
|
+
}
|
|
526
|
+
} catch {}
|
|
527
|
+
let mq = null;
|
|
528
|
+
try {
|
|
529
|
+
if (typeof matchMedia !== 'undefined') {
|
|
530
|
+
mq = matchMedia('(prefers-color-scheme: light)');
|
|
531
|
+
if (mq.addEventListener) mq.addEventListener('change', apply);
|
|
532
|
+
}
|
|
533
|
+
} catch {}
|
|
534
|
+
return () => {
|
|
535
|
+
if (mo) mo.disconnect();
|
|
536
|
+
if (mq && mq.removeEventListener) mq.removeEventListener('change', apply);
|
|
537
|
+
};
|
|
538
|
+
}, []);
|
|
539
|
+
|
|
540
|
+
// R3 SSE 列表流(设计 §3.3.4):EventSource 主通道 + 3s 轮询降级兜底(D2 保留既有轮询路径不删)。
|
|
541
|
+
// 收到 batch 信号 → eventCount 去重(旧于当前忽略)→ 重跑既有聚合;onerror / 15s 无心跳 → 回退轮询;
|
|
542
|
+
// 重连成功(EventSource 自动重连 / 心跳恢复)→ 停轮询回 SSE。
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
let alive = true;
|
|
545
|
+
let es = null;
|
|
546
|
+
let pollIv = null;
|
|
547
|
+
let lastBeat = 0;
|
|
548
|
+
let degraded = false;
|
|
549
|
+
const seen = new Map(); // batchId -> 已见 eventCount(服务端摘要去重,旧于当前忽略)
|
|
550
|
+
const tick = async () => {
|
|
551
|
+
try {
|
|
552
|
+
const agg = await aggregateBatches(sid);
|
|
553
|
+
if (!alive) return;
|
|
554
|
+
setBatches(agg); setUpdated(new Date());
|
|
555
|
+
for (const b of agg) {
|
|
556
|
+
if (typeof b.eventCount === 'number') seen.set(b.batchId, Math.max(seen.get(b.batchId) || 0, b.eventCount));
|
|
557
|
+
}
|
|
558
|
+
} catch {}
|
|
559
|
+
};
|
|
560
|
+
const startPoll = () => { if (degraded || pollIv) return; degraded = true; setMode('poll'); pollIv = setInterval(tick, 3000); };
|
|
561
|
+
const stopPoll = () => { degraded = false; if (pollIv) { clearInterval(pollIv); pollIv = null; } setMode('sse'); };
|
|
562
|
+
const onBatch = (ev) => {
|
|
563
|
+
lastBeat = Date.now();
|
|
564
|
+
try {
|
|
565
|
+
const d = JSON.parse(ev.data);
|
|
566
|
+
const bid = d.batchId, n = d.eventCount;
|
|
567
|
+
if (bid && typeof n === 'number' && seen.get(bid) != null && n <= seen.get(bid)) return; // 旧于当前忽略
|
|
568
|
+
if (bid && typeof n === 'number') seen.set(bid, n);
|
|
569
|
+
tick();
|
|
570
|
+
} catch { tick(); }
|
|
571
|
+
};
|
|
572
|
+
const onHeartbeat = () => { lastBeat = Date.now(); if (degraded) stopPoll(); }; // 心跳恢复 → 回 SSE
|
|
573
|
+
const connect = () => {
|
|
574
|
+
try {
|
|
575
|
+
if (typeof EventSource === 'undefined' || !sid) { startPoll(); return; }
|
|
576
|
+
es = new EventSource('/api/dsh-punky-swarm/stream?session=' + encodeURIComponent(sid));
|
|
577
|
+
es.addEventListener('batch', onBatch);
|
|
578
|
+
es.addEventListener('heartbeat', onHeartbeat);
|
|
579
|
+
es.onopen = () => { lastBeat = Date.now(); stopPoll(); };
|
|
580
|
+
es.onerror = () => { lastBeat = Date.now(); if (!degraded) startPoll(); }; // 断流 → 轮询兜底(EventSource 自动重连)
|
|
581
|
+
} catch { startPoll(); }
|
|
582
|
+
};
|
|
583
|
+
tick();
|
|
584
|
+
connect();
|
|
585
|
+
const stall = setInterval(() => {
|
|
586
|
+
if (!degraded && es && lastBeat && Date.now() - lastBeat > 15000) startPoll(); // 15s 无心跳 → 降级轮询
|
|
587
|
+
}, 5000);
|
|
588
|
+
return () => { alive = false; if (es) { try { es.close(); } catch {} } if (pollIv) clearInterval(pollIv); clearInterval(stall); };
|
|
589
|
+
}, [sid]);
|
|
590
|
+
// R3 SSE 详情流(设计 §3.3.4):信号 → 回拉 /batch + 双 /mailbox(复用既有逻辑);降级回轮询语义同列表流
|
|
591
|
+
useEffect(() => {
|
|
592
|
+
if (!sel) { setDetail(null); return; }
|
|
593
|
+
let alive = true;
|
|
594
|
+
let es = null;
|
|
595
|
+
let pollIv = null;
|
|
596
|
+
let lastBeat = 0;
|
|
597
|
+
let degraded = false;
|
|
598
|
+
let lastCount = null;
|
|
599
|
+
const tick = async () => {
|
|
600
|
+
try {
|
|
601
|
+
const d = await api('/batch?batchId=' + encodeURIComponent(sel.batchId), sel.session);
|
|
602
|
+
let mail = { inbox: [], broadcast: [] };
|
|
603
|
+
try { mail.inbox = (await api('/mailbox?batchId=' + encodeURIComponent(sel.batchId) + '&box=inbox', sel.session)).items; } catch {}
|
|
604
|
+
try { mail.broadcast = (await api('/mailbox?batchId=' + encodeURIComponent(sel.batchId) + '&box=broadcast', sel.session)).items; } catch {}
|
|
605
|
+
if (alive) { setDetail(Object.assign({}, d, { mail })); lastCount = d.eventCount; }
|
|
606
|
+
} catch {}
|
|
607
|
+
};
|
|
608
|
+
const startPoll = () => { if (degraded || pollIv) return; degraded = true; setMode('poll'); pollIv = setInterval(tick, 3000); };
|
|
609
|
+
const stopPoll = () => { degraded = false; if (pollIv) { clearInterval(pollIv); pollIv = null; } setMode('sse'); };
|
|
610
|
+
const onSignal = (ev) => {
|
|
611
|
+
lastBeat = Date.now();
|
|
612
|
+
try {
|
|
613
|
+
const d = JSON.parse(ev.data);
|
|
614
|
+
if (typeof d.eventCount === 'number' && lastCount != null && d.eventCount <= lastCount) return; // 旧于当前忽略
|
|
615
|
+
tick();
|
|
616
|
+
} catch { tick(); }
|
|
617
|
+
};
|
|
618
|
+
const onHeartbeat = () => { lastBeat = Date.now(); if (degraded) stopPoll(); };
|
|
619
|
+
const connect = () => {
|
|
620
|
+
try {
|
|
621
|
+
if (typeof EventSource === 'undefined' || !sel.session) { startPoll(); return; }
|
|
622
|
+
es = new EventSource('/api/dsh-punky-swarm/stream?session=' + encodeURIComponent(sel.session) + '&batchId=' + encodeURIComponent(sel.batchId));
|
|
623
|
+
es.addEventListener('batch', onSignal);
|
|
624
|
+
es.addEventListener('mailbox', onSignal); // 帧协议 event: mailbox(mailbox 目录变更信号,回拉双 /mailbox)
|
|
625
|
+
es.addEventListener('heartbeat', onHeartbeat);
|
|
626
|
+
es.onopen = () => { lastBeat = Date.now(); stopPoll(); };
|
|
627
|
+
es.onerror = () => { lastBeat = Date.now(); if (!degraded) startPoll(); };
|
|
628
|
+
} catch { startPoll(); }
|
|
629
|
+
};
|
|
630
|
+
tick();
|
|
631
|
+
connect();
|
|
632
|
+
const stall = setInterval(() => {
|
|
633
|
+
if (!degraded && es && lastBeat && Date.now() - lastBeat > 15000) startPoll();
|
|
634
|
+
}, 5000);
|
|
635
|
+
return () => { alive = false; if (es) { try { es.close(); } catch {} } if (pollIv) clearInterval(pollIv); clearInterval(stall); };
|
|
636
|
+
}, [sel]);
|
|
637
|
+
|
|
638
|
+
const list = batches || [];
|
|
639
|
+
const running = list.filter((b) => b.phase === 'running').length;
|
|
640
|
+
const doneCnt = list.filter((b) => b.phase === 'complete').length;
|
|
641
|
+
let issues = 0;
|
|
642
|
+
for (const b of list) {
|
|
643
|
+
const vals = Object.values(b.lanes || {});
|
|
644
|
+
issues += vals.filter((s) => s === 'failed' || s === 'conflict').length;
|
|
645
|
+
}
|
|
646
|
+
const liveColor = !batches ? T.dim : running ? T.warn : T.success;
|
|
647
|
+
const halo = !batches ? 'transparent' : running ? pal().haloWarn : pal().haloSuccess;
|
|
648
|
+
const updatedText = updated
|
|
649
|
+
? updated.toTimeString().slice(0, 8)
|
|
650
|
+
: '--:--:--';
|
|
651
|
+
|
|
652
|
+
return React.createElement('div', {
|
|
653
|
+
'aria-busy': !batches,
|
|
654
|
+
style: {
|
|
655
|
+
flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: 10,
|
|
656
|
+
padding: '14px 16px', color: T.text, fontFamily: T.font
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, minHeight: 28 } },
|
|
660
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
661
|
+
React.createElement('span', { style: { fontSize: 15, fontWeight: 700, letterSpacing: 0.2 } }, tt('view.cluster')),
|
|
662
|
+
React.createElement('span', { className: 'psw-pulse', style: { width: 7, height: 7, borderRadius: 999, background: liveColor, boxShadow: '0 0 0 3px ' + halo } }),
|
|
663
|
+
React.createElement('span', { style: { fontSize: 10, fontWeight: 600, letterSpacing: 0.8, color: liveColor } }, tt('live'))
|
|
664
|
+
),
|
|
665
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
666
|
+
React.createElement('span', { style: { fontSize: 10.5, color: T.text3, fontFamily: T.mono } }, tt(mode === 'poll' ? 'stream.fallback' : 'stream.live')),
|
|
667
|
+
React.createElement('span', { style: { fontSize: 10.5, color: T.text3, fontFamily: T.mono, fontVariantNumeric: 'tabular-nums' } }, '· ' + tt('updated') + ' ' + updatedText)
|
|
668
|
+
),
|
|
669
|
+
React.createElement('div', { role: 'status', 'aria-atomic': 'true', style: { display: 'flex', gap: 8 } },
|
|
670
|
+
React.createElement(Stat, { label: tt('stat.total'), value: list.length }),
|
|
671
|
+
React.createElement(Stat, { label: tt('stat.running'), value: running, color: T.warn }),
|
|
672
|
+
React.createElement(Stat, { label: tt('stat.done'), value: doneCnt, color: T.success }),
|
|
673
|
+
React.createElement(Stat, { label: tt('stat.issues'), value: issues, color: issues ? T.error : T.text3 })
|
|
674
|
+
),
|
|
675
|
+
React.createElement('div', { className: 'psw-panes' },
|
|
676
|
+
React.createElement(BatchList, { batches: batches, selected: sel, onSelect: setSel, loading: true }),
|
|
677
|
+
React.createElement(BatchDetail, { d: detail })
|
|
678
|
+
)
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function apply(ctx) {
|
|
683
|
+
if (typeof document !== 'undefined' && !document.getElementById('dsh-punky-swarm-ui')) {
|
|
684
|
+
const el = document.createElement('style');
|
|
685
|
+
el.id = 'dsh-punky-swarm-ui';
|
|
686
|
+
el.textContent = "body{--psw-shimmer-a:#eef2f7;--psw-shimmer-b:rgba(120,140,170,.16)}\nbody[data-ds-dark-theme]{--psw-shimmer-a:#1d2740;--psw-shimmer-b:rgba(148,163,184,.14)}\n.psw-scroll::-webkit-scrollbar{width:8px;height:8px}\n.psw-scroll::-webkit-scrollbar-thumb{background:var(--dsw-alias-scrollbar-bg-l1,rgba(148,163,184,.35));border-radius:999px}\n.psw-scroll::-webkit-scrollbar-thumb:hover{background:var(--dsw-alias-scrollbar-hover-l1,rgba(148,163,184,.5))}\n.psw-btn{transition:background .15s ease,border-color .15s ease,transform .15s ease}\n.psw-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.09))}\n.psw-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f8cff);outline-offset:2px}\n.psw-card{transition:border-color .15s ease,background .15s ease}\n.psw-pulse{animation:pswPulse 2s ease-in-out infinite}\n@keyframes pswPulse{0%,100%{opacity:1}50%{opacity:.3}}\n.psw-shimmer{background:linear-gradient(90deg,var(--psw-shimmer-a) 30%,var(--psw-shimmer-b) 50%,var(--psw-shimmer-a) 70%);background-size:200% 100%;animation:pswShimmer 1.4s linear infinite}\n@keyframes pswShimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}\n.psw-panes{display:flex;gap:12px;flex:1;min-height:0}\n@media (max-width:760px){.psw-panes{flex-direction:column}.psw-list{width:100%!important;max-height:240px}}\n@media (prefers-reduced-motion:reduce){.psw-pulse,.psw-shimmer{animation:none}.psw-btn,.psw-card{transition:none}}";
|
|
687
|
+
document.head.appendChild(el);
|
|
688
|
+
}
|
|
689
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-punky-swarm: dictionaries');
|
|
690
|
+
const t = ctx.locale.bind(NS);
|
|
691
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
692
|
+
name: 'conversation.view',
|
|
693
|
+
id: 'cluster',
|
|
694
|
+
order: 20,
|
|
695
|
+
locale: NS,
|
|
696
|
+
label: () => t('view.cluster'),
|
|
697
|
+
inject: (sessionId) => ({ sessionId })
|
|
698
|
+
}, ClusterWorkbench));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
module.exports = { apply, inject };
|
|
702
|
+
return module.exports;
|
|
703
|
+
}
|
|
704
|
+
});
|