@ohos-cpf/3rdloop 0.0.3 → 0.0.5

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.
Files changed (46) hide show
  1. package/README.md +124 -87
  2. package/lib/cli.js +25 -0
  3. package/lib/config-cmd.js +1 -1
  4. package/lib/serve.js +268 -0
  5. package/lib/update.js +46 -6
  6. package/lib/web-ext.js +454 -0
  7. package/lib/web.js +664 -0
  8. package/package.json +2 -1
  9. package/vendor/Server/Agent/SkillSelector/README.md +11 -11
  10. package/vendor/Server/Agent/SkillSelector/llm/llmClient.js +3 -3
  11. package/vendor/Server/Skills/arkts-code-use/SKILL.md +270 -0
  12. package/vendor/Server/Skills/arkts-code-use/assets/TEMPLATES.md +367 -0
  13. package/vendor/Server/Skills/arkts-code-use/references/API_VERIFICATION.md +144 -0
  14. package/vendor/Server/Skills/arkts-code-use/references/ARKTS_RULES.md +240 -0
  15. package/vendor/Server/Skills/arkts-code-use/references/CODE_PATTERNS.md +431 -0
  16. package/vendor/Server/Skills/arkts-code-use/references/SYNTAX_CHECK_GUIDE.md +164 -0
  17. package/vendor/Server/Skills/arkts-code-use/scripts/verify-arkts.cjs +428 -0
  18. package/vendor/Server/Skills/gitcode-repo-fork/SKILL.md +310 -0
  19. package/vendor/Server/Skills/gitcode-repo-fork/assets/FORK_REPORT_TEMPLATE.md +113 -0
  20. package/vendor/Server/Skills/gitcode-repo-fork/references/FORK_DECISION_GUIDE.md +124 -0
  21. package/vendor/Server/Skills/gitcode-repo-fork/references/GITCODE_FORK_API.md +95 -0
  22. package/vendor/Server/Skills/gitcode-repo-fork/scripts/gitcode-fork.cjs +285 -0
  23. package/vendor/VERSION +3 -3
  24. package/web/css/arktslibrarycheck.css +322 -0
  25. package/web/css/codecheck.css +464 -0
  26. package/web/css/flutterlibrarycheck.css +322 -0
  27. package/web/css/knowledge.css +332 -0
  28. package/web/css/loop.css +578 -0
  29. package/web/css/md-reader.css +240 -0
  30. package/web/css/rnlibrarycheck.css +322 -0
  31. package/web/css/theme.css +702 -0
  32. package/web/index.html +713 -0
  33. package/web/js/arktslibrarycheck.js +1413 -0
  34. package/web/js/codecheck.js +1039 -0
  35. package/web/js/flutterlibrarycheck.js +1364 -0
  36. package/web/js/health.js +69 -0
  37. package/web/js/knowledge.js +358 -0
  38. package/web/js/loop.js +1102 -0
  39. package/web/js/md-reader.js +435 -0
  40. package/web/js/navigation.js +238 -0
  41. package/web/js/rnlibrarycheck.js +1378 -0
  42. package/web/js/stats.js +110 -0
  43. package/web/js/theme.js +46 -0
  44. package/web/js/utils.js +228 -0
  45. package/web/knowledge.html +146 -0
  46. package/web/loop.html +219 -0
package/web/js/loop.js ADDED
@@ -0,0 +1,1102 @@
1
+ /* ════════════════════════════════════════════════════════════════════
2
+ * AI Loop 大循环控制台 — 主脚本
3
+ * 共享工具函数($, escapeHtml, toast, API 等)由 utils.js 统一提供
4
+ * ════════════════════════════════════════════════════════════════════ */
5
+ 'use strict';
6
+
7
+ /* ─── 全局状态 ──────────────────────────────────────────────────────── */
8
+ var State = {
9
+ loops: [], filteredLoops: [], selectedTaskId: null,
10
+ currentProgress: null, orchProgress: null, selectedStepId: null,
11
+ activeDetailTab: 'detail', brainPlan: null,
12
+ sseSource: null, pollTimer: null,
13
+ activeMainTab: 'rounds',
14
+ graphNodes: [], graphEdges: [], nodePositions: {},
15
+ graphTransform: { scale: 1, tx: 0, ty: 0 },
16
+ graphInteract: null,
17
+ selectedLoop: null
18
+ };
19
+
20
+ /* ─── loop 专属工具函数 ─────────────────────────────────────────────── */
21
+ function copySessionId(sid) { if (!sid) return; try { navigator.clipboard.writeText(sid); } catch(e) {} }
22
+
23
+ /* ─── 任务列表 ──────────────────────────────────────────────────────── */
24
+ async function loadLoopList() {
25
+ try {
26
+ var d = await API.get('/api/loopengine/list');
27
+ if (d && d.status === 'ok') {
28
+ State.loops = d.loops || [];
29
+ filterLoops();
30
+ } else {
31
+ State.loops = [];
32
+ $('loopList').innerHTML = '<div class="detail-empty">加载失败: ' + escapeHtml(d && d.message || '未知错误') + '</div>';
33
+ $('loopCount').textContent = '0';
34
+ }
35
+ } catch(err) {
36
+ State.loops = [];
37
+ $('loopList').innerHTML = '<div class="detail-empty">后端不可达: ' + escapeHtml(err.message) + '</div>';
38
+ $('loopCount').textContent = '0';
39
+ }
40
+ }
41
+
42
+ function filterLoops() {
43
+ var q = $('loopSearch').value.trim().toLowerCase();
44
+ State.filteredLoops = State.loops.filter(function(l) {
45
+ var id = (l.taskId||'').toLowerCase(), st = (l.status||'').toLowerCase();
46
+ return !q || id.indexOf(q) !== -1 || st.indexOf(q) !== -1;
47
+ });
48
+ renderLoopList();
49
+ }
50
+
51
+ function renderLoopList() {
52
+ var list = $('loopList'); $('loopCount').textContent = State.filteredLoops.length;
53
+ if (State.filteredLoops.length === 0) { list.innerHTML = '<div class="detail-empty">暂无大循环任务</div>'; return; }
54
+ list.innerHTML = State.filteredLoops.map(function(l) {
55
+ var st = l.status || 'idle', act = l.taskId === State.selectedTaskId, t = l.startTime || l.endTime || '';
56
+ var desc = l.taskDescription ? escapeHtml(l.taskDescription) : '';
57
+ var badge = l.isSingleExecution ? '<span class="status-badge single-exec">⚡ 单次</span>' : '';
58
+ var loopInfo = l.isSingleExecution ? '单次执行' : ((l.currentLoop||0) + '/' + (l.maxLoops||3) + ' 轮');
59
+ return '<div class="task-item ' + (act?'active':'') + '" onclick="selectLoop(\'' + escapeHtml(l.taskId) + '\')">' +
60
+ '<div class="task-id">' + escapeHtml(l.taskId) + '</div>' +
61
+ (desc ? '<div class="task-desc">' + desc + '</div>' : '') +
62
+ '<div class="task-meta"><span class="status-badge ' + st + '">' + statusLabel(st) + '</span>' + badge +
63
+ '<span class="task-time">' + loopInfo + '</span>' +
64
+ (t ? '<span class="task-time">' + formatTime(t) + '</span>' : '') + '</div></div>';
65
+ }).join('');
66
+ }
67
+
68
+ /* ─── 任务选择 & 数据加载 ────────────────────────────────────────────── */
69
+ async function selectLoop(taskId) {
70
+ State.selectedTaskId = taskId; State.selectedStepId = null; State.selectedLoop = null; State._convRenderedStep=null;
71
+ // 从列表数据缓存该任务的初始描述(用户最初输入的内容)。
72
+ // 启动新任务时列表可能尚未刷新,selectLoop 会在 loadLoopList 完成后被再次
73
+ // 调用或由调用方直接回填 selectedTaskDesc,避免概览/轮次显示空描述。
74
+ var item = State.loops.filter(function(l){ return l.taskId === taskId; })[0];
75
+ if (item && item.taskDescription) State.selectedTaskDesc = item.taskDescription;
76
+ stopPolling(); renderLoopList();
77
+ await loadAllData(taskId); connectSSE(taskId);
78
+ }
79
+
80
+ async function loadAllData(taskId) {
81
+ await Promise.all([ loadLoopProgress(taskId), loadOrchestratorProgress(taskId), loadBrainPlan(taskId) ]);
82
+ }
83
+
84
+ /**
85
+ * 加载指定轮次的编排进度和 Brain 计划。
86
+ * @param {string} taskId
87
+ * @param {number} loop - 轮次序号(0-based)
88
+ */
89
+ async function loadRoundData(taskId, loop) {
90
+ State.selectedLoop = loop;
91
+ State.selectedStepId = null;
92
+ await Promise.all([ loadOrchestratorProgress(taskId, loop), loadBrainPlan(taskId, loop) ]);
93
+ }
94
+
95
+ async function loadLoopProgress(taskId, opts) {
96
+ try {
97
+ var d = await API.get('/api/loopengine/progress?taskId=' + encodeURIComponent(taskId));
98
+ if (d.status === 'ok' && d.progress) {
99
+ State.currentProgress = d.progress;
100
+ // 静默模式:用户正在查看历史轮次时,只更新概览栏,不重建轮次 DOM(避免打断用户视图)
101
+ var silent = opts && opts.silent;
102
+ renderLoopOverview();
103
+ if (!silent) renderRounds(); else refreshRoundCardBadges();
104
+ var s = d.progress.status;
105
+ if (s === 'planning' || s === 'executing' || s === 'checking' || s === 'aborting') startPolling(taskId);
106
+ }
107
+ } catch(err) { toast('error', '加载进度失败: ' + err.message); }
108
+ }
109
+
110
+ /**
111
+ * 静默刷新:只更新轮次卡片上的状态徽章和时间,不重建 DOM,不打断用户展开状态。
112
+ */
113
+ function refreshRoundCardBadges() {
114
+ var p = State.currentProgress; if (!p || !p.rounds) return;
115
+ p.rounds.forEach(function(r) {
116
+ var card = document.querySelector('.round-card[data-loop="' + r.loop + '"]');
117
+ if (!card) return;
118
+ // 更新状态徽章
119
+ var badge = card.querySelector('.status-badge');
120
+ if (badge) {
121
+ if (r.passed) { badge.className = 'status-badge passed'; badge.textContent = '✅ 通过'; }
122
+ else if (r.error) { badge.className = 'status-badge error'; badge.textContent = '❌ 异常'; }
123
+ }
124
+ // 更新时间
125
+ var timeEl = card.querySelector('.round-time');
126
+ if (timeEl && r.endTime) {
127
+ var t = (r.startTime?formatTime(r.startTime):'—') + ' → ' + formatTime(r.endTime);
128
+ timeEl.textContent = t;
129
+ }
130
+ });
131
+ }
132
+
133
+ /* ─── 循环概览渲染 ──────────────────────────────────────────────────── */
134
+ function renderLoopOverview() {
135
+ var p = State.currentProgress; if (!p) return;
136
+ $('loopOverview').style.display = 'block';
137
+ $('ovTaskId').textContent = p.taskId;
138
+ $('ovDesc').textContent = State.selectedTaskDesc || p.taskDescription || '—';
139
+ $('ovStatus').className = 'status-badge ' + (p.status||'idle');
140
+ $('ovStatus').textContent = statusLabel(p.status);
141
+ var rounds = p.rounds || [];
142
+ var passed = rounds.filter(function(r){return r.passed;}).length;
143
+ var isSingle = p.isSingleExecution;
144
+ var stats = [
145
+ isSingle
146
+ ? '<span class="stat-item">⚡ <strong>单次执行</strong></span>'
147
+ : '<span class="stat-item">轮次: <strong>' + (p.currentLoop||0) + '/' + (p.maxLoops||3) + '</strong></span>',
148
+ '<span class="stat-item">已完成: <strong>' + rounds.length + '</strong></span>',
149
+ '<span class="stat-item">通过: <strong>' + passed + '</strong></span>',
150
+ '<span class="stat-item">开始: <strong>' + formatTime(p.startTime) + '</strong></span>'
151
+ ];
152
+ if (p.endTime) stats.push('<span class="stat-item">结束: <strong>' + formatTime(p.endTime) + '</strong></span>');
153
+ if (p.error) stats.push('<span class="stat-item" style="color:var(--red);">错误: <strong>' + escapeHtml(p.error) + '</strong></span>');
154
+ $('ovStats').innerHTML = stats.join('');
155
+ // 终止中:保持终止按钮可见但置灰(终止请求已受理,等待循环收尾)
156
+ if (p.status==='aborting') {
157
+ $('btnLoopAbort').disabled = true;
158
+ $('btnLoopAbort').textContent = '⏳ 任务中止中...';
159
+ } else {
160
+ var act = p.status==='planning'||p.status==='executing'||p.status==='checking';
161
+ $('btnLoopAbort').disabled = !act;
162
+ $('btnLoopAbort').textContent = '⏹ 终止循环';
163
+ }
164
+ }
165
+
166
+ /* ─── 轮次时间线渲染 ────────────────────────────────────────────────── */
167
+ function renderRounds() {
168
+ var p = State.currentProgress;
169
+ if (!p || !p.rounds || p.rounds.length === 0) {
170
+ if (p && (p.status==='planning'||p.status==='executing'||p.status==='checking'||p.status==='aborting')) {
171
+ // 后端刚启动 startLoop(异步)但 round[0] 尚未 push 到 ctx.rounds 的瞬态:
172
+ // 即便 rounds 为空也要展示初始任务描述,避免首屏空窗需手动重选任务才出现内容
173
+ var init0 = State.selectedTaskDesc || p.taskDescription || '';
174
+ var bodyHtml = '';
175
+ if (init0) {
176
+ bodyHtml = '<div class="round-body">' +
177
+ '<div class="phase-block"><div class="phase-header"><span class="phase-icon">📝</span> 初始任务描述</div>' +
178
+ '<div class="phase-content" style="font-size:12px;line-height:1.6;background:var(--code-bg);padding:10px;border-radius:6px;white-space:pre-wrap;word-break:break-all;">' + escapeHtml(init0) + '</div></div>' +
179
+ '<div class="phase-block"><div class="phase-header"><span class="phase-icon">🧠</span> Brain 初始拆解</div><div class="phase-content"><span class="muted">拆解中...</span></div></div>' +
180
+ '</div>';
181
+ }
182
+ $('roundsSection').innerHTML = '<h3>🔄 循环轮次时间线</h3>' +
183
+ '<div class="round-card running expanded" data-loop="' + (p.currentLoop||0) + '"><div class="round-header"><div class="round-num">1</div>' +
184
+ '<div class="round-title">第 1 轮 (' + statusLabel(p.status) + '...)</div>' +
185
+ '<span class="status-badge ' + p.status + '">' + statusLabel(p.status) + '</span>' +
186
+ '<span class="round-time">进行中</span></div>' + bodyHtml + '</div>';
187
+ } else { $('roundsSection').innerHTML = '<div class="rounds-empty">暂无轮次记录</div>'; }
188
+ return;
189
+ }
190
+ var rounds = p.rounds.slice();
191
+ var act = p.status==='planning'||p.status==='executing'||p.status==='checking'||p.status==='aborting';
192
+ if (act && (rounds.length===0 || rounds[rounds.length-1].endTime)) {
193
+ rounds.push({ loop: p.currentLoop||0, passed:false, startTime:null, endTime:null, error:null, _inProgress:true, _currentPhase:p.status });
194
+ }
195
+ $('roundsSection').innerHTML = '<h3>🔄 循环轮次时间线 (' + rounds.length + ' 轮)</h3>' +
196
+ rounds.map(function(r){ return renderRoundCard(r); }).join('');
197
+ document.querySelectorAll('.round-header').forEach(function(el){
198
+ el.onclick = function(){
199
+ var card = el.parentElement;
200
+ var wasExpanded = card.classList.contains('expanded');
201
+ // 先折叠所有卡片
202
+ document.querySelectorAll('.round-card').forEach(function(c){ c.classList.remove('expanded'); });
203
+ if (!wasExpanded) {
204
+ card.classList.add('expanded');
205
+ // 加载该轮编排数据和 Brain 计划
206
+ var loopVal = card.getAttribute('data-loop');
207
+ if (loopVal != null && State.selectedTaskId) {
208
+ loadRoundData(State.selectedTaskId, Number(loopVal));
209
+ }
210
+ }
211
+ };
212
+ });
213
+ // 展开逻辑:用户已选择查看某个历史轮次 → 展开对应卡片;否则展开最新轮次
214
+ var cards = document.querySelectorAll('.round-card');
215
+ if (cards.length > 0) {
216
+ var targetLoop = State.selectedLoop;
217
+ var targetCard = null;
218
+ if (targetLoop != null) {
219
+ targetCard = document.querySelector('.round-card[data-loop="' + targetLoop + '"]');
220
+ }
221
+ if (!targetCard) targetCard = cards[cards.length - 1];
222
+ targetCard.classList.add('expanded');
223
+ }
224
+ }
225
+
226
+ function renderRoundCard(r) {
227
+ var isRun = r._inProgress || (!r.endTime && !r.passed && r.error===null);
228
+ var cls = r.passed ? 'passed' : (r.error ? 'failed' : (isRun ? 'running' : 'failed'));
229
+ var numCls = r.passed ? 'passed' : 'failed';
230
+ var ph = r._inProgress ? ' (' + statusLabel(r._currentPhase) + '...)' : '';
231
+ var t = (r.startTime?formatTime(r.startTime):'—') + (r.endTime?' → '+formatTime(r.endTime):' → 进行中');
232
+ var h = '<div class="round-card ' + cls + '" data-loop="' + r.loop + '"><div class="round-header">';
233
+ h += '<div class="round-num ' + numCls + '">' + (r.loop+1) + '</div>';
234
+ h += '<div class="round-title">第 ' + (r.loop+1) + ' 轮' + ph + '</div>';
235
+ if (r.passed) h += '<span class="status-badge passed">✅ 通过</span>';
236
+ else if (r.error) h += '<span class="status-badge error">❌ 异常</span>';
237
+ else if (isRun) h += '<span class="status-badge ' + (r._currentPhase||'executing') + '">' + statusLabel(r._currentPhase||'executing') + '</span>';
238
+ else h += '<span class="status-badge failed">❌ 未通过</span>';
239
+ h += '<span class="round-time">' + t + '</span><span class="round-expand-icon">▶</span></div>';
240
+ h += '<div class="round-body">';
241
+ var initDesc = State.selectedTaskDesc || (State.currentProgress && State.currentProgress.taskDescription);
242
+ if (initDesc) h += '<div class="phase-block"><div class="phase-header"><span class="phase-icon">📝</span> 初始任务描述</div>' +
243
+ '<div class="phase-content" style="font-size:12px;line-height:1.6;background:var(--code-bg);padding:10px;border-radius:6px;white-space:pre-wrap;word-break:break-all;">' + escapeHtml(initDesc) + '</div></div>';
244
+ h += renderBrainPhase(r); h += renderOrchestratorPhase(r); h += renderCheckPhase(r);
245
+ if (r.error) h += '<div class="phase-block"><div class="phase-header"><span class="phase-icon">⚠️</span> 异常</div><div class="phase-content" style="color:var(--red);">' + escapeHtml(r.error) + '</div></div>';
246
+ h += '</div></div>';
247
+ return h;
248
+ }
249
+
250
+ function renderBrainPhase(r) {
251
+ var h = '<div class="phase-block"><div class="phase-header"><span class="phase-icon">🧠</span> Brain ' + (r.planningIsReplan ? '重规划' : '初始拆解') + '</div><div class="phase-content">';
252
+ if (!r.planningPlanId && !r._inProgress) h += '<span class="muted">无拆解数据</span>';
253
+ else if (r._inProgress && !r.planningPlanId) h += '<span class="muted">拆解中...</span>';
254
+ else {
255
+ h += '<div class="kv"><span class="k">Plan ID</span><span class="v">' + escapeHtml(r.planningPlanId||'—') + '</span>';
256
+ h += '<span class="k">类型</span><span class="v">' + (r.planningIsReplan ? '🔄 重规划 (第'+r.loop+'轮)' : '✨ 初始拆解') + '</span>';
257
+ if (r.planningStepCount!=null) h += '<span class="k">步骤数</span><span class="v">' + r.planningStepCount + '</span>';
258
+ if (r.planningIntent) h += '<span class="k">任务意图</span><span class="v">' + escapeHtml(r.planningIntent) + '</span>';
259
+ if (r.planningDomain) h += '<span class="k">领域</span><span class="v">' + escapeHtml(r.planningDomain) + '</span>';
260
+ if (r.planningComplexity) h += '<span class="k">复杂度</span><span class="v">' + escapeHtml(r.planningComplexity) + '</span>';
261
+ h += '</div>';
262
+ if (r.planningFailureAnalysis) h += '<div style="margin-top:8px;padding:6px 8px;background:var(--red-light);border-radius:5px;font-size:11px;"><strong>失败分析:</strong> ' + escapeHtml(r.planningFailureAnalysis) + '</div>';
263
+ if (r.planningAdjustmentStrategy) h += '<div style="margin-top:4px;padding:6px 8px;background:var(--orange-light);border-radius:5px;font-size:11px;"><strong>调整策略:</strong> ' + escapeHtml(r.planningAdjustmentStrategy) + '</div>';
264
+ h += '<div style="margin-top:8px;"><button class="secondary sm" onclick="switchToBrainTab(' + r.loop + ')">🧠 查看 Brain 拆解详情</button></div>';
265
+ }
266
+ h += '</div></div>'; return h;
267
+ }
268
+
269
+ function renderOrchestratorPhase(r) {
270
+ var st = r.orchestratorStatus;
271
+ var h = '<div class="phase-block"><div class="phase-header"><span class="phase-icon">🔀</span> Orchestrator 编排执行</div><div class="phase-content">';
272
+ if (!st && r._inProgress && r._currentPhase==='planning') h += '<span class="muted">等待 Brain 拆解完成...</span>';
273
+ else if (!st && r._inProgress) h += '<span class="muted">执行中...</span>';
274
+ else if (!st) h += '<span class="muted">无编排数据</span>';
275
+ else {
276
+ h += '<div class="kv"><span class="k">编排状态</span><span class="v"><span class="status-badge ' + st + '">' + statusLabel(st) + '</span></span></div>';
277
+ h += '<div style="margin-top:8px;"><button class="secondary sm" onclick="switchToOrchTab(' + r.loop + ')">🔀 查看步骤编排图</button></div>';
278
+ }
279
+ h += '</div></div>'; return h;
280
+ }
281
+
282
+ function renderCheckPhase(r) {
283
+ var h = '<div class="phase-block"><div class="phase-header"><span class="phase-icon">🔍</span> TestCheck 准出审核</div><div class="phase-content">';
284
+ if (!r.checkId && r._inProgress && r._currentPhase!=='checking') h += '<span class="muted">等待编排完成...</span>';
285
+ else if (!r.checkId && r._inProgress) h += '<span class="muted">审核中...</span>';
286
+ else if (!r.checkId) h += '<span class="muted">未执行审核</span>';
287
+ else {
288
+ if (r.checkPassed===true) h += '<div style="margin-bottom:6px;"><span class="status-badge passed">✅ 准出通过</span></div>';
289
+ else if (r.checkPassed===false) h += '<div style="margin-bottom:6px;"><span class="status-badge failed">❌ 准出未通过</span></div>';
290
+ else h += '<div style="margin-bottom:6px;"><span class="status-badge ' + (r.checkStatus||'idle') + '">' + statusLabel(r.checkStatus) + '</span></div>';
291
+ h += '<div class="kv"><span class="k">Check ID</span><span class="v">' + escapeHtml(r.checkId||'—') + '</span>';
292
+ h += '<span class="k">审核 Session</span><span class="v">' + escapeHtml(r.checkSessionId||'—') + '</span></div>';
293
+ if (r.checkSummary) h += '<div style="margin-bottom:8px;padding:6px 8px;background:var(--card);border-radius:5px;font-size:11px;line-height:1.6;"><strong>审核摘要:</strong> ' + escapeHtml(r.checkSummary) + '</div>';
294
+ h += '<button class="secondary sm" onclick="loadCheckDetail(\'' + escapeHtml(State.selectedTaskId) + '\', ' + r.loop + ')">📄 查看审核详情</button>';
295
+ h += ' <button class="secondary sm" onclick="loadCheckSession(\'' + escapeHtml(State.selectedTaskId) + '\', ' + r.loop + ')">💬 查看审核对话</button>';
296
+ }
297
+ h += '</div></div>'; return h;
298
+ }
299
+
300
+ /* ─── 编排图 ────────────────────────────────────────────────────────── */
301
+ async function loadOrchestratorProgress(taskId, loop) {
302
+ try {
303
+ var url = '/api/orchestrator/progress?taskId=' + encodeURIComponent(taskId);
304
+ if (loop != null) url += '&loop=' + encodeURIComponent(loop);
305
+ var d = await API.get(url);
306
+ if (d.status==='ok' && d.progress) { State.orchProgress = d.progress; renderGraph(); renderDetailPanel(); }
307
+ else { State.orchProgress = null; renderGraph(); renderDetailPanel(); }
308
+ } catch(err) { State.orchProgress = null; renderGraph(); renderDetailPanel(); }
309
+ }
310
+
311
+ function getDepIds(dep) {
312
+ if (Array.isArray(dep)) return dep.map(String).filter(function(x){ return x!=='0' && x!==''; });
313
+ if (dep==null || dep==='0' || dep==='') return [];
314
+ return [String(dep)];
315
+ }
316
+
317
+ function buildGraphData() {
318
+ var p = State.orchProgress;
319
+ if (!p || !p.steps || p.steps.length===0) { State.graphNodes=[]; State.graphEdges=[]; return; }
320
+ State.graphNodes = p.steps.map(function(s) {
321
+ return { id: String(s.stepId), stepId: s.stepId, status: s.status||'pending', retryCount: s.retryCount||0, maxRetries: s.maxRetries||3,
322
+ taskDescription: s.taskDescription||'', dependId: s.dependId, nextStepId: String(s.nextStepId||'0'), failStepId: String(s.failStepId||'0'),
323
+ sessionId: s.sessionId||'', resultPath: s.resultPath||'', error: s.error||'', startTime: s.startTime, endTime: s.endTime, history: s.history||[],
324
+ canRetry: s.canRetry!==false, skillDirectory: s.skillDirectory||'', taskDir: s.taskDir||'', inputReport: s.inputReport||'' };
325
+ });
326
+ State.graphEdges = [];
327
+ var ids = {}; State.graphNodes.forEach(function(n){ ids[n.id]=true; });
328
+ State.graphNodes.forEach(function(n) {
329
+ // dependId 支持单值或数组(fan-in 聚合),逐项生成依赖边
330
+ getDepIds(n.dependId).forEach(function(dep) {
331
+ if (ids[dep]) State.graphEdges.push({ from:dep, to:n.id, type:'depend', label:'依赖' });
332
+ });
333
+ if (n.nextStepId && n.nextStepId!=='0' && ids[n.nextStepId]) State.graphEdges.push({ from:n.id, to:n.nextStepId, type:'success', label:'成功' });
334
+ if (n.failStepId && n.failStepId!=='0' && ids[n.failStepId]) State.graphEdges.push({ from:n.id, to:n.failStepId, type:'fail', label:'失败' });
335
+ });
336
+ }
337
+
338
+ function layoutGraph() {
339
+ var nodes = State.graphNodes; if (nodes.length===0) { State.nodePositions={}; return; }
340
+ var map = {}; nodes.forEach(function(n){ map[n.id]=n; });
341
+ var levels = {};
342
+ function getLvl(id, vis) { if (!vis) vis={}; if (vis[id]) return 0; if (levels[id]!==undefined) return levels[id]; var n=map[id]; if(!n) return 0; vis[id]=true; var deps=getDepIds(n.dependId).filter(function(d){return map[d];}); if(deps.length===0){levels[id]=0;return 0;} var l=0; deps.forEach(function(d){ var dl=getLvl(d,vis)+1; if(dl>l)l=dl; }); levels[id]=l; return l; }
343
+ nodes.forEach(function(n){ getLvl(n.id,{}); });
344
+ var chg=true, iter=0;
345
+ while (chg && iter<10) { chg=false; iter++; nodes.forEach(function(n){ if(n.nextStepId&&n.nextStepId!=='0'&&map[n.nextStepId]){ var exp=(levels[n.id]||0)+1; if((levels[n.nextStepId]||0)<exp){levels[n.nextStepId]=exp;chg=true;} } }); }
346
+ var maxL=0; for (var k in levels) { if(levels[k]>maxL) maxL=levels[k]; }
347
+ var groups={}; for (var i=0;i<=maxL;i++) groups[i]=[];
348
+ nodes.forEach(function(n){ var l=levels[n.id]||0; if(!groups[l]) groups[l]=[]; groups[l].push(n); });
349
+ var NW=180,NH=80,GX=60,GY=80,PD=40; State.nodePositions={};
350
+ for (var lvl=0; lvl<=maxL; lvl++) { (groups[lvl]||[]).forEach(function(n,idx){ State.nodePositions[n.id]={ x:PD+idx*(NW+GX), y:PD+lvl*(NH+GY), w:NW, h:NH, cx:PD+idx*(NW+GX)+NW/2, cy:PD+lvl*(NH+GY)+NH/2 }; }); }
351
+ }
352
+
353
+ function renderGraph() {
354
+ buildGraphData(); layoutGraph();
355
+ var svg=$('graphSvg'), empty=$('graphEmpty');
356
+ if (State.graphNodes.length===0) { svg.style.display='none'; empty.style.display='flex'; $('graphHint').textContent=''; return; }
357
+ svg.style.display='block'; empty.style.display='none';
358
+ var maxX=0,maxY=0; for (var id in State.nodePositions) { var p=State.nodePositions[id]; if(p.x+p.w>maxX)maxX=p.x+p.w; if(p.y+p.h>maxY)maxY=p.y+p.h; }
359
+ var w=maxX+60, h=maxY+60;
360
+ svg.setAttribute('viewBox','0 0 '+w+' '+h);
361
+ svg.setAttribute('preserveAspectRatio','xMinYMin meet');
362
+ var c='';
363
+ State.graphEdges.forEach(function(e) {
364
+ var f=State.nodePositions[e.from], t=State.nodePositions[e.to]; if(!f||!t) return;
365
+ var x1=f.cx, y1=f.y+f.h, x2=t.cx, y2=t.y, dy=y2-y1, my=y1+dy/2;
366
+ var path='M '+x1+' '+y1+' C '+x1+' '+my+', '+x2+' '+my+', '+x2+' '+y2;
367
+ var cls=e.type==='success'?'success':e.type==='fail'?'fail':'depend';
368
+ var mk=e.type==='success'?'url(#arrow-success)':e.type==='fail'?'url(#arrow-fail)':'url(#arrow-depend)';
369
+ c += '<path class="edge-line '+cls+'" d="'+path+'" marker-end="'+mk+'" />';
370
+ c += '<text class="edge-label" x="'+((x1+x2)/2)+'" y="'+(my-3)+'" text-anchor="middle">'+escapeHtml(e.label)+'</text>';
371
+ });
372
+ State.graphNodes.forEach(function(n) {
373
+ var p=State.nodePositions[n.id]; if(!p) return;
374
+ var sel=String(State.selectedStepId)===n.id, st=n.status||'pending';
375
+ var rt=n.retryCount>0?'🔄 '+n.retryCount+'/'+n.maxRetries:'';
376
+ c += '<g class="node-group '+(sel?'selected':'')+'" data-step-id="'+escapeHtml(n.id)+'" onclick="selectStep(\''+escapeHtml(n.id)+'\')">';
377
+ c += '<rect class="node-rect '+st+'" x="'+p.x+'" y="'+p.y+'" width="'+p.w+'" height="'+p.h+'" rx="8" />';
378
+ if (st==='running') c += '<rect class="running-ring" x="'+p.x+'" y="'+p.y+'" width="'+p.w+'" height="'+p.h+'" rx="8" fill="none" stroke-width="2.5" stroke-dasharray="8,4" />';
379
+ c += '<rect class="node-drag-handle" x="'+(p.x+p.w-18)+'" y="'+(p.y+6)+'" width="12" height="6" />';
380
+ c += '<text class="node-text node-id" x="'+(p.x+10)+'" y="'+(p.y+22)+'">步骤 '+escapeHtml(n.stepId)+'</text>';
381
+ c += '<text class="node-text node-status" x="'+(p.x+10)+'" y="'+(p.y+38)+'">'+statusLabel(n.status)+'</text>';
382
+ if (rt) c += '<text class="node-text node-retry" x="'+(p.x+10)+'" y="'+(p.y+52)+'">'+rt+'</text>';
383
+ c += '<text class="node-text node-retry" x="'+(p.x+10)+'" y="'+(p.y+68)+'">'+escapeHtml(truncate(n.taskDescription,18))+'</text>';
384
+ c += '</g>';
385
+ });
386
+ var vp=$('graphViewport');
387
+ if (vp) vp.innerHTML=c; else { svg.innerHTML='<g class="graph-viewport" id="graphViewport">'+c+'</g>'; }
388
+ applyGraphTransform();
389
+ $('graphHint').textContent = State.graphNodes.length+' 个步骤, '+State.graphEdges.length+' 条关系' +
390
+ (State.selectedLoop != null ? ' (第 ' + (State.selectedLoop + 1) + ' 轮)' : '');
391
+ }
392
+
393
+ /* ─── 图表交互:缩放 / 平移 / 节点拖拽 ────────────────────────────────── */
394
+ function applyGraphTransform() {
395
+ var vp=$('graphViewport'); if(!vp) return;
396
+ var t=State.graphTransform;
397
+ vp.setAttribute('transform','translate('+t.tx+','+t.ty+') scale('+t.scale+')');
398
+ var zd=$('zoomDisplay'); if(zd) zd.textContent=Math.round(t.scale*100)+'%';
399
+ }
400
+
401
+ function graphZoomBy(factor, cx, cy) {
402
+ var t=State.graphTransform, svg=$('graphSvg'); if(!svg) return;
403
+ var rect=svg.getBoundingClientRect();
404
+ if(cx==null) cx=rect.width/2;
405
+ if(cy==null) cy=rect.height/2;
406
+ var vb=svg.viewBox.baseVal, sx=vb.width/rect.width, sy=vb.height/rect.height;
407
+ var s=Math.max(sx,sy);
408
+ var oldScale=t.scale;
409
+ var newScale=Math.max(0.2, Math.min(3, oldScale*factor));
410
+ if(newScale===oldScale) return;
411
+ var wx=cx*s, wy=cy*s;
412
+ t.tx=wx-(wx-t.tx)*(newScale/oldScale);
413
+ t.ty=wy-(wy-t.ty)*(newScale/oldScale);
414
+ t.scale=newScale;
415
+ applyGraphTransform();
416
+ }
417
+
418
+ function graphResetTransform() {
419
+ State.graphTransform={scale:1,tx:0,ty:0};
420
+ applyGraphTransform();
421
+ }
422
+
423
+ function graphFitToView() {
424
+ var svg=$('graphSvg'); if(!svg||State.graphNodes.length===0) return;
425
+ var rect=svg.getBoundingClientRect(); if(rect.width===0) return;
426
+ var maxX=0,maxY=0;
427
+ for(var id in State.nodePositions){var p=State.nodePositions[id];if(p.x+p.w>maxX)maxX=p.x+p.w;if(p.y+p.h>maxY)maxY=p.y+p.h;}
428
+ var vbW=parseFloat(svg.getAttribute('viewBox').split(' ')[2])||maxX+60;
429
+ var vbH=parseFloat(svg.getAttribute('viewBox').split(' ')[3])||maxY+60;
430
+ var pad=30, fitScale=Math.min((rect.width-2*pad)/vbW, (rect.height-2*pad)/vbH, 1);
431
+ fitScale=Math.max(0.2,fitScale);
432
+ State.graphTransform={scale:fitScale,tx:pad,ty:pad};
433
+ applyGraphTransform();
434
+ }
435
+
436
+ function initGraphInteraction() {
437
+ var canvas=$('graphCanvas'), svg=$('graphSvg');
438
+ if(!canvas||!svg) return;
439
+ var st=null;
440
+
441
+ function getSvgPoint(e) {
442
+ var rect=svg.getBoundingClientRect();
443
+ var vb=svg.viewBox.baseVal, s=Math.max(vb.width/rect.width, vb.height/rect.height);
444
+ return { x:(e.clientX-rect.left)*s, y:(e.clientY-rect.top)*s };
445
+ }
446
+
447
+ canvas.addEventListener('wheel',function(e){
448
+ e.preventDefault();
449
+ if(State.graphNodes.length===0) return;
450
+ var pt=getSvgPoint(e);
451
+ var factor=e.deltaY<0?1.1:0.9;
452
+ graphZoomBy(factor, pt.x, pt.y);
453
+ },{passive:false});
454
+
455
+ canvas.addEventListener('mousedown',function(e){
456
+ if(State.graphNodes.length===0) return;
457
+ var pt=getSvgPoint(e);
458
+ var target=e.target;
459
+ var nodeG=null;
460
+ while(target&&target!==canvas){ if(target.classList&&target.classList.contains('node-group')){nodeG=target;break;} target=target.parentNode; }
461
+ var isDragHandle=e.target.classList&&e.target.classList.contains('node-drag-handle');
462
+ if(nodeG&&(isDragHandle||e.shiftKey)) {
463
+ var sid=nodeG.getAttribute('data-step-id');
464
+ var pos=State.nodePositions[sid]; if(!pos) return;
465
+ st={mode:'node',stepId:sid,startX:pt.x,startY:pt.y,origX:pos.x,origY:pos.y};
466
+ canvas.classList.add('node-drag'); nodeG.classList.add('dragging');
467
+ } else {
468
+ st={mode:'pan',startX:pt.x,startY:pt.y,origTx:State.graphTransform.tx,origTy:State.graphTransform.ty};
469
+ canvas.classList.add('panning');
470
+ }
471
+ });
472
+
473
+ document.addEventListener('mousemove',function(e){
474
+ if(!st) return;
475
+ var pt=getSvgPoint(e);
476
+ if(st.mode==='pan') {
477
+ State.graphTransform.tx=st.origTx+(pt.x-st.startX);
478
+ State.graphTransform.ty=st.origTy+(pt.y-st.startY);
479
+ applyGraphTransform();
480
+ } else if(st.mode==='node') {
481
+ var pos=State.nodePositions[st.stepId]; if(!pos) return;
482
+ var dx=pt.x-st.startX, dy=pt.y-st.startY;
483
+ pos.x=st.origX+dx; pos.y=st.origY+dy;
484
+ pos.cx=pos.x+pos.w/2; pos.cy=pos.y+pos.h/2;
485
+ renderGraph();
486
+ var vp=$('graphViewport'); if(vp) vp.setAttribute('transform','translate('+State.graphTransform.tx+','+State.graphTransform.ty+') scale('+State.graphTransform.scale+')');
487
+ }
488
+ });
489
+
490
+ document.addEventListener('mouseup',function(){
491
+ if(!st) return;
492
+ if(st.mode==='node') {
493
+ var ng=canvas.querySelector('.node-group.dragging');
494
+ if(ng) ng.classList.remove('dragging');
495
+ }
496
+ canvas.classList.remove('panning','node-drag');
497
+ st=null;
498
+ });
499
+
500
+ canvas.addEventListener('contextmenu',function(e){
501
+ if(State.graphNodes.length>0) e.preventDefault();
502
+ });
503
+ }
504
+
505
+ /* ─── 步骤选择 & 详情面板 ────────────────────────────────────────────── */
506
+ function selectStep(stepId) { State.selectedStepId=stepId; State._convRenderedStep=null; renderGraph(); renderDetailPanel(); }
507
+
508
+ function getStep() { if(!State.orchProgress||!State.selectedStepId) return null; return State.orchProgress.steps.find(function(s){return String(s.stepId)===String(State.selectedStepId);}); }
509
+
510
+ function renderDetailPanel() {
511
+ var s=getStep();
512
+ if (!s) { $('detailContent').innerHTML='<div class="detail-empty">请选择一个步骤节点</div>'; return; }
513
+ // 对话 tab 内容由用户手动获取消息,SSE 触发的重渲染不应冲掉已有内容
514
+ // 仅在步骤切换或 tab 切换时重建对话面板
515
+ if (State.activeDetailTab==='conversation') {
516
+ if (State._convRenderedStep === String(s.stepId) && $('msgList')) return;
517
+ State._convRenderedStep = String(s.stepId);
518
+ renderConversationTab(s);
519
+ return;
520
+ }
521
+ if (State.activeDetailTab==='detail') renderDetailTab(s);
522
+ else if (State.activeDetailTab==='control') renderControlTab(s);
523
+ else if (State.activeDetailTab==='files') renderFilesTab(s);
524
+ }
525
+
526
+ function renderDetailTab(step) {
527
+ var st=step.status||'pending', cr=step.canRetry!==false, hist=step.history||[];
528
+ var h='<div class="detail-section"><h4>基本信息</h4><div class="kv-list">'+
529
+ '<div class="k">步骤 ID</div><div class="v">'+escapeHtml(step.stepId)+'</div>'+
530
+ '<div class="k">状态</div><div class="v"><span class="status-badge '+st+'">'+statusLabel(st)+'</span></div>'+
531
+ '<div class="k">可重试</div><div class="v">'+(cr?'✅ 是':'❌ 否')+'</div>'+
532
+ '<div class="k">重试次数</div><div class="v">'+(step.retryCount||0)+' / '+(step.maxRetries||3)+'</div>'+
533
+ '<div class="k">开始时间</div><div class="v">'+formatTime(step.startTime)+'</div>'+
534
+ '<div class="k">结束时间</div><div class="v">'+formatTime(step.endTime)+'</div>'+
535
+ '<div class="k">Session</div><div class="v"><span id="detailSessionVal">'+escapeHtml(step.sessionId||'(无)')+'</span>'+(hist.length>1?'<div class="muted" style="font-size:11px;margin-top:2px;">最新一次(共 '+hist.length+' 次重试,见下方历史详情)</div>':'')+'</div>'+
536
+ '<div class="k">结果路径</div><div class="v">'+escapeHtml(step.resultPath||'(无)')+'</div>'+
537
+ '</div></div>'+
538
+ '<div class="detail-section"><h4>任务描述</h4><div style="font-size:12px;line-height:1.6;background:var(--code-bg);padding:10px;border-radius:6px;">'+escapeHtml(step.taskDescription||'—')+'</div></div>'+
539
+ '<div class="detail-section"><h4>步骤关系</h4><div class="kv-list">'+
540
+ '<div class="k">依赖步骤</div><div class="v">'+(getDepIds(step.dependId).length>0?'步骤 '+escapeHtml(getDepIds(step.dependId).join(', ')):'无 (入口)')+'</div>'+
541
+ '<div class="k">依赖数</div><div class="v">'+getDepIds(step.dependId).length+' 个前置</div>'+
542
+ '<div class="k">成功→下一步</div><div class="v">'+(step.nextStepId&&step.nextStepId!=='0'?'步骤 '+escapeHtml(step.nextStepId):'无 (结束)')+'</div>'+
543
+ '<div class="k">失败→回退</div><div class="v">'+(step.failStepId&&step.failStepId!=='0'?'步骤 '+escapeHtml(step.failStepId):'无 (终止)')+'</div>'+
544
+ '</div></div>';
545
+ if (step.error) h += '<div class="detail-section"><h4 style="color:var(--red);">错误信息</h4><div style="font-size:12px;color:var(--red);background:var(--red-light);padding:10px;border-radius:6px;">'+escapeHtml(step.error)+'</div></div>';
546
+ if (hist.length>0) {
547
+ h+='<div class="detail-section"><h4>执行历史 ('+hist.length+')</h4><div class="history-list">';
548
+ hist.forEach(function(hi){
549
+ var sid=hi.sessionId||'';
550
+ h+='<div class="history-item">'+
551
+ '<span class="history-attempt">第 '+hi.attempt+' 次</span>'+
552
+ '<span style="color:'+(hi.success?'var(--green)':'var(--red)')+';">'+(hi.success?'✅ 成功':'❌ 失败')+'</span>'+
553
+ '<span class="muted">'+formatTime(hi.startTime)+' → '+formatTime(hi.endTime)+'</span>'+
554
+ (sid?'<div class="history-session">Session: <a href="#" onclick="copySessionId(\''+escapeHtml(sid)+'\');return false;" title="点击复制">'+escapeHtml(sid)+'</a></div>':'')+
555
+ (hi.resultPath?'<div class="history-result muted">'+escapeHtml(hi.resultPath)+'</div>':'')+
556
+ '</div>';
557
+ });
558
+ h+='</div></div>';
559
+ }
560
+ $('detailContent').innerHTML=h;
561
+
562
+ // 运行中的步骤若 sessionId 为空,从活跃 runner 实时补充(后端通过 SSE 推送可能存在时序差)
563
+ if (st==='running' && !step.sessionId && step.stepId!=null) {
564
+ fetchSessionIdForStep(step.stepId);
565
+ }
566
+ }
567
+
568
+ /**
569
+ * 从 /api/flexrunner/status 获取运行中步骤的实时 sessionId 并回填详情面板。
570
+ * @param {string|number} sid - 步骤 ID
571
+ */
572
+ async function fetchSessionIdForStep(sid) {
573
+ try {
574
+ var tid=State.selectedTaskId;
575
+ if(!tid) return;
576
+ var d=await API.get('/api/flexrunner/status?taskId='+encodeURIComponent(tid)+'&stepId='+encodeURIComponent(sid));
577
+ if(d.status==='ok' && d.runner && d.runner.sessionId
578
+ && String(State.selectedStepId)===String(sid)) {
579
+ var el=document.getElementById('detailSessionVal');
580
+ if(el) el.textContent=d.runner.sessionId;
581
+ }
582
+ } catch(err){ /* 静默失败,不影响主流程 */ }
583
+ }
584
+
585
+ /* ─── 对话 Tab ──────────────────────────────────────────────────────── */
586
+ async function renderConversationTab(step) {
587
+ var tid=State.selectedTaskId, sid=step.stepId, loop=State.selectedLoop;
588
+ $('detailContent').innerHTML='<div class="detail-section"><h4>会话交互</h4>'+
589
+ '<div class="form-group"><textarea id="convInput" placeholder="输入要发送的消息..." style="min-height:60px;"></textarea></div>'+
590
+ '<div class="detail-actions"><button class="primary sm" id="btnSend">📨 发送</button></div></div>'+
591
+ '<div class="detail-section"><h4>消息列表</h4>'+
592
+ '<div class="detail-actions" style="margin-bottom:8px;"><button class="secondary sm" id="btnGetLive">获取实时消息</button><button class="secondary sm" id="btnGetSession">获取会话归档</button></div>'+
593
+ '<div class="msg-list" id="msgList"><div class="muted" style="font-size:12px;">点击按钮获取消息...</div></div></div>';
594
+ $('btnSend').onclick=function(){sendMsg(tid,sid,loop);};
595
+ $('btnGetLive').onclick=function(){getLiveMsgs(tid,sid,loop);};
596
+ $('btnGetSession').onclick=function(){getSessionArchive(tid,sid,loop);};
597
+ }
598
+
599
+ async function getLiveMsgs(tid,sid,loop) {
600
+ try {
601
+ var url='/api/flexrunner/live-messages?taskId='+encodeURIComponent(tid)+'&stepId='+encodeURIComponent(sid);
602
+ if(loop!=null) url+='&loop='+encodeURIComponent(loop);
603
+ var d=await API.get(url); if(d.status==='ok') renderMessages(d.messages||[]);
604
+ }
605
+ catch(err){ toast('error','获取消息失败: '+err.message); }
606
+ }
607
+
608
+ async function getSessionArchive(tid,sid,loop) {
609
+ try {
610
+ var url='/api/flexrunner/session?taskId='+encodeURIComponent(tid)+'&stepId='+encodeURIComponent(sid);
611
+ if(loop!=null) url+='&loop='+encodeURIComponent(loop);
612
+ var d=await API.get(url); if(d.status==='ok'){ renderMessages(d.messages||[]); toast('info','会话归档: '+(d.messages||[]).length+' 条消息'); }
613
+ }
614
+ catch(err){ toast('error','获取归档失败: '+err.message); }
615
+ }
616
+
617
+ async function sendMsg(tid,sid,loop) {
618
+ var m=$('convInput').value.trim(); if(!m){toast('warn','请先输入消息');return;}
619
+ var btn=$('btnSend'); btn.disabled=true; btn.textContent='⏳ 发送中...';
620
+ try {
621
+ var body={taskId:tid,stepId:sid,message:m};
622
+ if(loop!=null) body.loop=loop;
623
+ var d=await API.post('/api/flexrunner/inject-prompt',body); if(d.status==='ok'){toast('success','消息已发送');$('convInput').value='';getLiveMsgs(tid,sid,loop);} else toast('warn',d.message||'发送失败');
624
+ }
625
+ catch(err){ toast('error','请求失败: '+err.message); }
626
+ finally { btn.disabled=false; btn.textContent='📨 发送'; }
627
+ }
628
+
629
+ function renderMessages(msgs) {
630
+ var list=$('msgList'); if(!list) return;
631
+ if(!msgs||msgs.length===0){list.innerHTML='<div class="muted" style="font-size:12px;">暂无消息</div>';return;}
632
+ list.innerHTML=msgs.map(function(m) {
633
+ var role=m.role||(m.info&&m.info.role)||'unknown';
634
+ // 显式取 content,空字符串不 fallback 到 JSON.stringify
635
+ var content = m.content != null ? m.content : ((m.parts&&m.parts[0]&&m.parts[0].text) || '');
636
+ if (!content || content.trim()==='') return '';
637
+ var t=m.timestamp||''; var rc=role==='user'?'user':role==='assistant'?'assistant':'system';
638
+ return '<div class="msg-item '+rc+'"><div class="role">'+escapeHtml(role)+'</div><div>'+escapeHtml(content)+'</div>'+(t?'<div class="time">'+escapeHtml(t)+'</div>':'')+'</div>';
639
+ }).filter(Boolean).join('');
640
+ }
641
+
642
+ /* ─── 控制 Tab ──────────────────────────────────────────────────────── */
643
+ function renderControlTab(step) {
644
+ var tid=State.selectedTaskId, sid=step.stepId;
645
+ var ce=step.canRetry!==false&&step.status!=='running', ir=step.status==='running';
646
+ $('detailContent').innerHTML='<div class="detail-section"><h4>FlexRunner 控制</h4>'+
647
+ '<div class="kv-list" style="margin-bottom:12px;"><div class="k">任务</div><div class="v">'+escapeHtml(tid)+'</div><div class="k">步骤</div><div class="v">'+escapeHtml(sid)+'</div><div class="k">状态</div><div class="v"><span class="status-badge '+step.status+'">'+statusLabel(step.status)+'</span></div></div>'+
648
+ '<div class="detail-actions"><button class="primary" id="btnExec" '+(ce?'':'disabled')+'>▶ 执行单次</button><button class="success" id="btnExecLoop" '+(ce?'':'disabled')+'>🔁 自动循环</button></div>'+
649
+ '<div class="detail-actions" style="margin-top:6px;"><button class="warn" id="btnPause" '+(ir?'':'disabled')+'>⏸ 暂停</button><button class="secondary" id="btnResume" '+(ir?'':'disabled')+'>▶ 继续</button><button class="danger" id="btnAbort" '+(ir?'':'disabled')+'>⏹ 终止</button></div>'+
650
+ '<div class="detail-actions" style="margin-top:6px;"><button class="secondary" id="btnCheckRes">📋 检查结果</button><button class="secondary" id="btnRefreshRunner">🔄 刷新状态</button></div></div>'+
651
+ '<div class="detail-section"><h4>执行输出</h4><div class="msg-list" id="controlOutput" style="font-family:\'SF Mono\',Monaco,monospace;font-size:11px;background:var(--code-bg);padding:10px;border-radius:6px;min-height:60px;max-height:300px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;">等待操作...</div></div>';
652
+ $('btnExec').onclick=function(){execSingle(tid,sid);};
653
+ $('btnExecLoop').onclick=function(){execLoop(tid,sid);};
654
+ $('btnPause').onclick=function(){runnerControl(tid,sid,'pause');};
655
+ $('btnResume').onclick=function(){runnerControl(tid,sid,'resume');};
656
+ $('btnAbort').onclick=function(){runnerControl(tid,sid,'abort');};
657
+ $('btnCheckRes').onclick=function(){checkResultFile(tid,sid);};
658
+ $('btnRefreshRunner').onclick=function(){refreshRunnerStatus(tid,sid);};
659
+ }
660
+
661
+ function logControl(msg) {
662
+ var o=$('controlOutput'); if(!o) return; if(o.textContent==='等待操作...') o.textContent='';
663
+ var t=new Date().toLocaleTimeString('zh-CN'); o.textContent+='['+t+'] '+msg+'\n'; o.scrollTop=o.scrollHeight;
664
+ }
665
+
666
+ async function execSingle(tid,sid) {
667
+ logControl('执行单次...'); try { var d=await API.post('/api/flexrunner/execute',{taskId:tid,stepId:sid}); if(d.status==='ok'){var r=d.result;logControl('完成: success='+r.success+', nextStepId='+(r.nextStepId||'(结束)'));toast(r.success?'success':'warn','执行'+(r.success?'成功':'失败'));} else {logControl('失败: '+(d.error||JSON.stringify(d)));toast('error','执行失败');} await loadOrchestratorProgress(tid); } catch(err){ logControl('请求失败: '+err.message); toast('error',err.message); }
668
+ }
669
+
670
+ async function execLoop(tid,sid) {
671
+ logControl('开始自动循环...'); try { var d=await API.post('/api/flexrunner/execute-loop',{taskId:tid,stepId:sid}); if(d.results)d.results.forEach(function(r){logControl('第'+r.attempt+'次: success='+r.success);}); logControl('循环结束: 共'+d.totalAttempts+'次, 最终='+(d.finalResult?d.finalResult.success:'?')); toast(d.finalResult&&d.finalResult.success?'success':'error','循环结束: '+d.totalAttempts+'次'); await loadOrchestratorProgress(tid); } catch(err){ logControl('请求失败: '+err.message); toast('error',err.message); }
672
+ }
673
+
674
+ async function runnerControl(tid,sid,type) {
675
+ logControl('控制: '+type); try { var d=await API.post('/api/flexrunner/control',{taskId:tid,stepId:sid,type:type}); logControl(type+': '+d.message); toast(d.status==='ok'?'success':'warn',d.message); await loadOrchestratorProgress(tid); } catch(err){ logControl('请求失败: '+err.message); }
676
+ }
677
+
678
+ async function checkResultFile(tid,sid) {
679
+ logControl('检查结果文件...'); try { var d=await API.post('/api/flexrunner/check-result',{taskId:tid,stepId:sid}); if(d.status==='ok'){logControl('结果: '+(d.success?'成功':'失败')+' — '+d.resultFilePath);if(d.content){var fn=(d.resultFilePath||'结果文件').split(/[/\\]/).pop();var ext=(fn.split('.').pop()||'').toLowerCase();if(ext==='md'||ext==='markdown')MarkdownReader.open(fn,d.content);else showModal('结果文件',d.content);}} else logControl('失败: '+d.error); } catch(err){ logControl('请求失败: '+err.message); }
680
+ }
681
+
682
+ async function refreshRunnerStatus(tid,sid) {
683
+ try { var d=await API.get('/api/flexrunner/status?taskId='+encodeURIComponent(tid)+'&stepId='+encodeURIComponent(sid)); if(d.status==='ok'){toast('info','状态已刷新');await loadOrchestratorProgress(tid);} } catch(err){ toast('error',err.message); }
684
+ }
685
+
686
+ /* ─── 文件 Tab ──────────────────────────────────────────────────────── */
687
+ async function renderFilesTab(step) {
688
+ var tid=State.selectedTaskId, loop=State.selectedLoop;
689
+ $('detailContent').innerHTML='<div class="detail-section"><h4>加载文件列表...</h4></div>';
690
+ try {
691
+ var url='/api/flexrunner/steps?taskId='+encodeURIComponent(tid)+'&stepId='+encodeURIComponent(step.stepId);
692
+ if(loop!=null) url+='&loop='+encodeURIComponent(loop);
693
+ var d=await API.get(url);
694
+ if(d.status==='ok'){
695
+ var files=d.files||[];
696
+ $('detailContent').innerHTML='<div class="detail-section"><h4>步骤 '+escapeHtml(step.stepId)+' 的文件 ('+files.length+')</h4>'+
697
+ (files.length===0?'<div class="muted" style="font-size:12px;">暂无文件</div>':
698
+ '<div class="msg-list">'+files.map(function(f){
699
+ var label=f.loop!=null?('L'+f.loop+' — '):'';
700
+ return '<div class="history-item" style="cursor:pointer;" onclick="viewFile(\''+escapeAttr(f.path)+'\')"><span>📄 '+label+escapeHtml(f.filename)+'</span><span class="muted">'+f.size+' bytes</span></div>';
701
+ }).join('')+'</div>')+'</div>'+
702
+ '<div class="detail-section"><h4>所有任务文件</h4><button class="secondary sm" id="btnListAllFiles">查看全部文件</button><div class="msg-list" id="allFilesList" style="margin-top:8px;"></div></div>';
703
+ $('btnListAllFiles').onclick=function(){listAllFiles(tid,loop);};
704
+ }
705
+ } catch(err){ toast('error',err.message); }
706
+ }
707
+
708
+ async function listAllFiles(tid,loop) {
709
+ try {
710
+ var url='/api/flexrunner/steps?taskId='+encodeURIComponent(tid);
711
+ if(loop!=null) url+='&loop='+encodeURIComponent(loop);
712
+ var d=await API.get(url);
713
+ if(d.status==='ok'){
714
+ var files=d.files||[];
715
+ $('allFilesList').innerHTML=files.map(function(f){
716
+ var label=f.loop!=null?('L'+f.loop+' — '):'';
717
+ return '<div class="history-item" style="cursor:pointer;" onclick="viewFile(\''+escapeAttr(f.path)+'\')"><span>📄 '+label+'步骤'+escapeHtml(f.stepId)+'/'+escapeHtml(f.filename)+'</span><span class="muted">'+f.size+'B</span></div>';
718
+ }).join('');
719
+ }
720
+ } catch(err){ toast('error',err.message); }
721
+ }
722
+
723
+ async function viewFile(fp) {
724
+ try {
725
+ var d = await API.get('/api/flexrunner/file?path=' + encodeURIComponent(fp));
726
+ if (d.status !== 'ok') { toast('error', d.error); return; }
727
+ var filename = fp.split(/[/\\]/).pop();
728
+ var ext = filename.split('.').pop().toLowerCase();
729
+ if (ext === 'md' || ext === 'markdown') {
730
+ MarkdownReader.open(filename, d.content);
731
+ } else {
732
+ showModal(filename, d.content);
733
+ }
734
+ } catch(err) {
735
+ toast('error', err.message);
736
+ }
737
+ }
738
+
739
+ /* ─── Brain 拆解 ────────────────────────────────────────────────────── */
740
+ async function loadBrainPlan(tid, loop) {
741
+ try {
742
+ var url = '/api/brain/plan?taskId='+encodeURIComponent(tid);
743
+ if (loop != null) url += '&loop=' + encodeURIComponent(loop);
744
+ var d = await API.get(url);
745
+ if(d.status==='ok'&&d.plan){State.brainPlan=d.plan;renderBrainSection();} else {State.brainPlan=null;renderBrainSection();}
746
+ } catch(err) { State.brainPlan=null; renderBrainSection(); }
747
+ }
748
+
749
+ function renderBrainSection() {
750
+ var plan=State.brainPlan;
751
+ if (!plan) { $('brainSection').innerHTML='<div class="rounds-empty">暂无 Brain 拆解数据</div>'; return; }
752
+ var td=plan.taskDef||{}, steps=td.steps||[], an=plan.analysis||{}, valid=plan.validation&&plan.validation.valid;
753
+ var ac=td.acceptanceCriteria||an.acceptanceCriteria||[];
754
+ // 拆解时绑定 SKILL 的步骤(specifiedSkill 非空 → 执行时直接使用该 SKILL,跳过自动匹配)
755
+ var skillMatched = steps.filter(function(s){ return s.specifiedSkill && String(s.specifiedSkill).trim()!==''; });
756
+ var h='';
757
+ h+='<div class="brain-card"><h3>📊 任务分析</h3><div class="kv-grid">';
758
+ h+='<div class="k">Plan ID</div><div class="v">'+escapeHtml(plan.planId||'—')+'</div>';
759
+ h+='<div class="k">Task ID</div><div class="v">'+escapeHtml(plan.taskId||td.taskId||'—')+'</div>';
760
+ h+='<div class="k">校验</div><div class="v">'+(valid?'<span class="badge ok">通过</span>':'<span class="badge error">未通过</span>')+'</div>';
761
+ h+='<div class="k">创建时间</div><div class="v">'+escapeHtml(plan.createdAt||'—')+'</div>';
762
+ h+='<div class="k">轮次</div><div class="v">'+(plan.isReplan?'第 '+(plan.loopCount||0)+' 轮 (重规划)':'第 0 轮 (初始)')+'</div>';
763
+ h+='<div class="k">分析 Session</div><div class="v">'+escapeHtml(plan.analysisSessionId||'—')+'</div>';
764
+ h+='<div class="k">拆解 Session</div><div class="v">'+escapeHtml(plan.decompositionSessionId||'—')+'</div>';
765
+ h+='</div>';
766
+ if (an.intent) h+='<div style="margin-top:10px;font-size:12px;padding:8px;background:var(--code-bg);border-radius:6px;"><strong>任务意图:</strong> '+escapeHtml(an.intent)+'</div>';
767
+ h+='<div style="display:flex;gap:16px;margin-top:10px;flex-wrap:wrap;">';
768
+ if(an.domain) h+='<span class="stat-item">领域: <strong>'+escapeHtml(an.domain)+'</strong></span>';
769
+ if(an.complexity) h+='<span class="stat-item">复杂度: <strong>'+escapeHtml(an.complexity)+'</strong></span>';
770
+ if(an.estimatedSteps) h+='<span class="stat-item">预计步骤: <strong>'+an.estimatedSteps+'</strong></span>';
771
+ h+='<span class="stat-item">SKILL 匹配: <strong style="color:'+(skillMatched.length>0?'var(--green)':'var(--text2)')+';">'+skillMatched.length+'/'+steps.length+'</strong></span>';
772
+ h+='</div>';
773
+ if(an.keywords&&an.keywords.length>0) h+='<div style="margin-top:8px;font-size:11px;color:var(--text2);">关键词: '+escapeHtml(an.keywords.join(', '))+'</div>';
774
+ if(an.techStack&&an.techStack.length>0) h+='<div style="margin-top:4px;font-size:11px;color:var(--text2);">技术栈: '+escapeHtml(an.techStack.join(', '))+'</div>';
775
+ h+='</div>';
776
+ if (ac.length>0) {
777
+ h+='<div class="brain-card"><h3>✅ 验收标准 ('+ac.length+' 条)</h3><div class="criteria-list">';
778
+ ac.forEach(function(c){ h+='<div class="criteria-item satisfied"><span class="criteria-icon">📋</span><div class="criteria-text"><div class="criterion">'+escapeHtml(c.criterion||'')+'</div><div class="reason">类型: '+escapeHtml(c.type||'—')+' | 验证: '+escapeHtml(c.verification||'—')+'</div></div></div>'; });
779
+ h+='</div></div>';
780
+ }
781
+ h+='<div class="brain-card"><h3>📋 拆解步骤 ('+steps.length+' 步 · SKILL 匹配 '+skillMatched.length+'/'+steps.length+')</h3><div class="step-flow">';
782
+ steps.forEach(function(s,idx){
783
+ var dep=getDepIds(s.dependId).length>0?'依赖步骤 '+getDepIds(s.dependId).join(', '):'入口步骤';
784
+ var nxt=s.nextStepId&&s.nextStepId!=='0'?'→ 步骤 '+s.nextStepId:'→ 结束';
785
+ var fail=s.failStepId&&s.failStepId!=='0'?' | 失败→步骤 '+s.failStepId:'';
786
+ var skName = s.specifiedSkill ? String(s.specifiedSkill).trim() : '';
787
+ var skillBadge = skName
788
+ ? '<span class="skill-badge matched" title="拆解时已匹配 SKILL,执行时直接使用(跳过自动匹配)">🎯 SKILL: '+escapeHtml(skName)+'</span>'
789
+ : '<span class="skill-badge unmatched" title="拆解时未匹配 SKILL,执行时由 SkillSelector 自动匹配">未匹配 SKILL(执行时自动匹配)</span>';
790
+ h+='<div class="step-flow-item"><div class="step-num">'+(idx+1)+'</div><div class="step-info"><div class="step-desc">'+escapeHtml(s.taskDescription||'')+'</div><div class="step-skill-row">'+skillBadge+'</div><div class="step-meta">'+dep+' · '+nxt+fail+' · 重试'+(s.maxRetries||3)+'次</div></div></div>';
791
+ });
792
+ h+='</div></div>';
793
+ if (!valid&&plan.validation&&plan.validation.issues&&plan.validation.issues.length>0) {
794
+ h+='<div class="brain-card"><h3 style="color:var(--red);">⚠️ 校验问题</h3><div style="font-size:12px;color:var(--red);">';
795
+ plan.validation.issues.forEach(function(i){h+='<div>• '+escapeHtml(i)+'</div>';});
796
+ h+='</div></div>';
797
+ }
798
+ h+='<div class="brain-card"><h3>📦 原始 JSON</h3><button class="secondary sm" onclick="showModal(\'Plan JSON\', JSON.stringify(State.brainPlan, null, 2))">查看完整 JSON</button></div>';
799
+ $('brainSection').innerHTML=h;
800
+ }
801
+
802
+ /* ─── 审核详情 & 终止 ──────────────────────────────────────────────── */
803
+ async function loadCheckDetail(tid,loop) {
804
+ try { var d=await API.get('/api/loopengine/progress?taskId='+encodeURIComponent(tid)); if(d.status==='ok'&&d.progress){ var r=(d.progress.rounds||[]).find(function(x){return x.loop===loop;}); if(r){ var c='=== 第 '+(loop+1)+' 轮准出审核 ===\n\n'; c+='Check ID: '+(r.checkId||'—')+'\n'; c+='状态: '+statusLabel(r.checkStatus)+'\n'; c+='通过: '+(r.checkPassed?'是':'否')+'\n\n'; c+='审核摘要:\n'+(r.checkSummary||'—')+'\n'; showModal('审核详情 - 第'+(loop+1)+'轮',c); } else toast('error','未找到第'+(loop+1)+'轮审核数据'); } } catch(err){ toast('error','加载审核详情失败: '+err.message); }
805
+ }
806
+
807
+ async function loadCheckSession(tid, loop) {
808
+ try {
809
+ var d = await API.get('/api/loopengine/check-session?taskId=' + encodeURIComponent(tid) + '&loop=' + encodeURIComponent(loop));
810
+ if (d.status === 'ok') {
811
+ var msgs = d.messages || [];
812
+ var title = '审核对话 - 第' + (loop + 1) + '轮';
813
+ if (d.sessionId) title += ' (' + d.sessionId.substring(0, 20) + '...)';
814
+ showSessionModal(title, msgs);
815
+ } else {
816
+ toast('error', d.error || '获取审核对话失败');
817
+ }
818
+ } catch(err) {
819
+ toast('error', '获取审核对话失败: ' + err.message);
820
+ }
821
+ }
822
+
823
+ async function abortLoop() {
824
+ var tid=State.selectedTaskId; if(!tid) return;
825
+ var btn=$('btnLoopAbort');
826
+ // 立即置灰防重复点击;请求返回由后端 aborting 状态接管按钮文案与置灰
827
+ if (btn) { btn.disabled=true; btn.textContent='⏳ 任务中止中...'; }
828
+ try {
829
+ var d=await API.post('/api/loopengine/abort',{taskId:tid});
830
+ // 后端仅受理终止请求(异步生效),随后状态转为 aborting → aborted;
831
+ // toast 只提示受理结果,终态由轮询/SSE 驱动的界面更新呈现
832
+ toast(d.status==='ok'?'warn':'error',d.message||'终止请求已发出');
833
+ await loadLoopProgress(tid);
834
+ loadLoopList();
835
+ } catch(err){
836
+ toast('error',err.message);
837
+ if (btn) { btn.disabled=false; btn.textContent='⏹ 终止循环'; }
838
+ }
839
+ // 注意:成功受理后不在 finally 里恢复按钮 —
840
+ // renderLoopOverview 会依据后端 aborting/终态持续接管按钮显示,
841
+ // 直到循环真正结束(aborted 等)才由状态渲染恢复为「终止循环」
842
+ }
843
+
844
+ /* ─── 轮询 & SSE ────────────────────────────────────────────────────── */
845
+ function startPolling(tid) {
846
+ stopPolling();
847
+ State.pollTimer=setInterval(async function(){
848
+ // 用户正在查看历史轮次 → 静默刷新(只更新徽章,不重建 DOM)
849
+ // 用户在最新轮 → 正常刷新
850
+ var viewingHistory = State.selectedLoop != null;
851
+ await loadLoopProgress(tid, { silent: viewingHistory });
852
+ if (!viewingHistory) {
853
+ await loadOrchestratorProgress(tid);
854
+ }
855
+ loadLoopList();
856
+ // 终止中(aborting)也持续轮询:等待循环真正收尾转为终态(aborted),
857
+ // 期间状态栏保持「任务中止中」、终止按钮保持置灰
858
+ if(State.currentProgress){var s=State.currentProgress.status;if(s!=='planning'&&s!=='executing'&&s!=='checking'&&s!=='aborting')stopPolling();}
859
+ },3000);
860
+ }
861
+
862
+ function stopPolling() { if(State.pollTimer){clearInterval(State.pollTimer);State.pollTimer=null;} }
863
+
864
+ function closeSSE() {
865
+ if (State.sseSource) {
866
+ try { State.sseSource.close(); } catch(e) {}
867
+ State.sseSource=null;
868
+ }
869
+ }
870
+
871
+ function connectSSE(tid) {
872
+ closeSSE();
873
+ try {
874
+ var es=new EventSource('/stream/tasks/'+encodeURIComponent(tid));
875
+ State.sseSource=es;
876
+ // SSE 事件处理:用户正在查看历史轮次时,进度更新走静默模式(不重建 DOM),
877
+ // 编排图/Brain 计划的更新也跳过,避免打断用户视图。
878
+ // 终止/完成/错误等关键事件始终强制刷新,确保用户看到最终状态。
879
+ es.addEventListener('loop:start',function(){toast('info','大循环已启动');loadLoopProgress(tid,{silent:State.selectedLoop!=null});});
880
+ es.addEventListener('loop:round:start',function(e){try{var d=JSON.parse(e.data);toast('info','第 '+(d.loop+1)+'/'+d.total+' 轮开始');loadLoopProgress(tid,{silent:State.selectedLoop!=null});}catch(x){}});
881
+ es.addEventListener('loop:planning',function(e){try{var d=JSON.parse(e.data);toast('info','第 '+(d.loop+1)+' 轮: Brain '+(d.isReplan?'重规划中':'拆解中')+'...');loadLoopProgress(tid,{silent:State.selectedLoop!=null});if(State.selectedLoop==null)loadBrainPlan(tid);}catch(x){}});
882
+ es.addEventListener('loop:executing',function(e){try{var d=JSON.parse(e.data);toast('info','第 '+(d.loop+1)+' 轮: Orchestrator 执行中...');loadLoopProgress(tid,{silent:State.selectedLoop!=null});if(State.selectedLoop==null)loadOrchestratorProgress(tid);}catch(x){}});
883
+ es.addEventListener('loop:checking',function(e){try{var d=JSON.parse(e.data);toast('info','第 '+(d.loop+1)+' 轮: TestCheck 审核中...');loadLoopProgress(tid,{silent:State.selectedLoop!=null});}catch(x){}});
884
+ es.addEventListener('loop:round:end',function(e){try{var d=JSON.parse(e.data);if(d.passed)toast('success','第 '+(d.loop+1)+' 轮: ✅ 准出通过');else toast('warn','第 '+(d.loop+1)+' 轮: ❌ 准出未通过,进入下一轮');loadLoopProgress(tid,{silent:State.selectedLoop!=null});if(State.selectedLoop==null)loadBrainPlan(tid);loadLoopList();}catch(x){}});
885
+ es.addEventListener('loop:complete',function(e){try{var d=JSON.parse(e.data);if(d.passed)toast('success','🎉 大循环完成: 准出通过! 共 '+d.totalRounds+' 轮');else toast('error','大循环结束: '+d.status+', 共 '+d.totalRounds+' 轮');stopPolling();loadLoopProgress(tid);loadLoopList();}catch(x){}});
886
+ es.addEventListener('loop:aborting',function(){
887
+ // 终止请求受理 → 进入「任务中止中」过渡态,立即刷新状态栏与按钮
888
+ toast('info','任务中止中,等待运行中的会话结束...');
889
+ loadLoopProgress(tid,{silent:State.selectedLoop!=null});
890
+ });
891
+ es.addEventListener('loop:aborted',function(){toast('warn','大循环已终止');
892
+ var btn=$('btnLoopAbort');
893
+ if (btn) { btn.disabled=false; btn.textContent='⏹ 终止循环'; }
894
+ stopPolling();loadLoopProgress(tid);loadLoopList();});
895
+ es.addEventListener('loop:error',function(e){try{var d=JSON.parse(e.data);toast('error','大循环出错: '+(d.error||''));}catch(x){}stopPolling();loadLoopProgress(tid);loadLoopList();});
896
+ es.addEventListener('orch:step:status',function(e){try{var d=JSON.parse(e.data);toast('info','步骤 '+d.stepId+': '+statusLabel(d.status));if(State.selectedLoop==null)loadOrchestratorProgress(tid);}catch(x){}});
897
+ es.addEventListener('orch:task:complete',function(){toast('success','编排任务完成');if(State.selectedLoop==null)loadOrchestratorProgress(tid);loadLoopProgress(tid);loadLoopList();});
898
+ es.addEventListener('orch:task:error',function(){toast('error','编排任务出错');if(State.selectedLoop==null)loadOrchestratorProgress(tid);loadLoopProgress(tid);loadLoopList();});
899
+ es.addEventListener('orch:task:aborted',function(){toast('warn','编排任务已终止');if(State.selectedLoop==null)loadOrchestratorProgress(tid);loadLoopProgress(tid);loadLoopList();});
900
+ es.onerror=function(){
901
+ // 连接错误(含后端限流主动关闭连接):主动关闭并置空,
902
+ // 阻止浏览器对 EventSource 的无限自动重连,避免连接泄漏
903
+ es.close();
904
+ if (State.sseSource === es) State.sseSource=null;
905
+ };
906
+ // 页面刷新/关闭时释放连接,避免刷新后残留订阅(仅注册一次)
907
+ if (!State._sseUnloadBound) {
908
+ window.addEventListener('beforeunload', closeSSE);
909
+ State._sseUnloadBound = true;
910
+ }
911
+ } catch(err){}
912
+ }
913
+
914
+ /* ─── 弹窗 ──────────────────────────────────────────────────────────── */
915
+ function showModal(title,content) { $('modalTitle').textContent=title; $('modalContent').textContent=content; $('modalOverlay').classList.add('show'); }
916
+ function closeModal() { $('modalOverlay').classList.remove('show'); }
917
+
918
+ function showSessionModal(title, msgs) {
919
+ $('modalTitle').textContent = title;
920
+ var el = $('modalContent');
921
+ if (!msgs || msgs.length === 0) {
922
+ el.textContent = '暂无对话消息';
923
+ } else {
924
+ el.innerHTML = msgs.map(function(m) {
925
+ var role = m.role || (m.info && m.info.role) || 'unknown';
926
+ var content = m.content != null ? m.content : ((m.parts && m.parts[0] && m.parts[0].text) || '');
927
+ if (!content || content.trim() === '') return '';
928
+ var t = m.timestamp || '';
929
+ var rc = role === 'user' ? 'user' : role === 'assistant' ? 'assistant' : 'system';
930
+ return '<div class="msg-item ' + rc + '"><div class="role">' + escapeHtml(role) + '</div><div>' + escapeHtml(content) + '</div>' + (t ? '<div class="time">' + escapeHtml(t) + '</div>' : '') + '</div>';
931
+ }).filter(Boolean).join('');
932
+ }
933
+ $('modalOverlay').classList.add('show');
934
+ }
935
+
936
+ function openNewLoopModal() {
937
+ $('newLoopDesc').value=''; $('newLoopCriteria').value=''; $('newLoopMaxLoops').value='3';
938
+ $('newLoopTaskId').value=''; $('newLoopSkillDir').value=''; $('newLoopTaskDir').value='';
939
+ $('advancedSection').classList.remove('show'); $('advancedToggle').classList.remove('open');
940
+ $('newLoopOverlay').classList.add('show');
941
+ }
942
+ function closeNewLoopModal() { $('newLoopOverlay').classList.remove('show'); }
943
+
944
+ async function submitNewLoop() {
945
+ var desc=$('newLoopDesc').value.trim(); if(!desc){toast('warn','请输入任务描述');return;}
946
+ var ml=parseInt($('newLoopMaxLoops').value,10)||3, cr=$('newLoopCriteria').value.trim();
947
+ var tid=$('newLoopTaskId').value.trim()||undefined, sd=$('newLoopSkillDir').value.trim()||undefined, td=$('newLoopTaskDir').value.trim()||undefined;
948
+ var body={taskDescription:desc,maxLoops:ml};
949
+ if(cr)body.acceptanceCriteria=cr; if(tid)body.taskId=tid; if(sd)body.skillDirectory=sd; if(td)body.taskDir=td;
950
+ var btn=$('btnNewLoopSubmit'); btn.disabled=true; btn.textContent='⏳ 启动中...';
951
+ try {
952
+ var d=await API.post('/api/loopengine/start',body);
953
+ btn.disabled=false; btn.textContent='🚀 启动大循环';
954
+ if(d.status!=='ok'){toast('error',d.error||d.message||'启动失败');return;}
955
+ toast('success','大循环已启动: '+d.taskId);
956
+ closeNewLoopModal();
957
+ // 立即回填初始描述(不依赖列表是否已刷新),保证概览/轮次有内容可渲染
958
+ State.selectedTaskDesc = desc;
959
+ // await 侧栏刷新,确保新任务出现在列表中且高亮选中
960
+ await loadLoopList();
961
+ await selectLoop(d.taskId);
962
+ // 启动后立即触发 SSE/轮询后端状态可能仍为 idle(startLoop 异步未推进到 planning)。
963
+ // 短延迟重试一次拉取,让首屏尽快显示真实进度(轮次/状态)。
964
+ setTimeout(function(){
965
+ if (State.selectedTaskId === d.taskId) {
966
+ loadLoopProgress(d.taskId).then(function(){
967
+ // 仍无轮次数据时再等一轮:Brain 拆解刚启动,round[0] 可能尚未 push
968
+ if (!State.currentProgress ||
969
+ (!State.currentProgress.rounds || State.currentProgress.rounds.length===0)) {
970
+ setTimeout(function(){ if (State.selectedTaskId===d.taskId) loadLoopProgress(d.taskId); }, 2500);
971
+ }
972
+ });
973
+ }
974
+ }, 1500);
975
+ }
976
+ catch(err){ btn.disabled=false; btn.textContent='🚀 启动大循环'; toast('error','请求失败: '+err.message); }
977
+ }
978
+
979
+ /* ─── 单次执行弹窗 ────────────────────────────────────────────────── */
980
+ function openNewSingleModal() {
981
+ $('newSingleDesc').value=''; $('newSingleTaskId').value=''; $('newSingleSkillDir').value='';
982
+ $('newSingleTaskDir').value=''; $('newSingleInputReport').value=''; $('newSingleMaxRetries').value='3';
983
+ $('singleAdvancedSection').classList.remove('show'); $('singleAdvancedToggle').classList.remove('open');
984
+ $('newSingleOverlay').classList.add('show');
985
+ }
986
+ function closeNewSingleModal() { $('newSingleOverlay').classList.remove('show'); }
987
+
988
+ async function submitNewSingle() {
989
+ var desc=$('newSingleDesc').value.trim(); if(!desc){toast('warn','请输入任务描述');return;}
990
+ var tid=$('newSingleTaskId').value.trim()||undefined;
991
+ var sd=$('newSingleSkillDir').value.trim()||undefined;
992
+ var td=$('newSingleTaskDir').value.trim()||undefined;
993
+ var ir=$('newSingleInputReport').value.trim()||undefined;
994
+ var mr=parseInt($('newSingleMaxRetries').value,10)||3;
995
+ var body={taskDescription:desc,maxRetries:mr};
996
+ if(tid)body.taskId=tid; if(sd)body.skillDirectory=sd; if(td)body.taskDir=td; if(ir)body.inputReport=ir;
997
+ var btn=$('btnNewSingleSubmit'); btn.disabled=true; btn.textContent='⏳ 启动中...';
998
+ try {
999
+ var d=await API.post('/api/flexrunner/start-single',body);
1000
+ btn.disabled=false; btn.textContent='⚡ 启动单次执行';
1001
+ if(d.status!=='ok'){toast('error',d.error||d.message||'启动失败');return;}
1002
+ toast('success','单次执行已启动: '+d.taskId);
1003
+ closeNewSingleModal();
1004
+ State.selectedTaskDesc = desc;
1005
+ await loadLoopList();
1006
+ await selectLoop(d.taskId);
1007
+ // 单次执行同样在 start 返回后可能还未落 progress,短延迟重试补全首屏
1008
+ setTimeout(function(){
1009
+ if (State.selectedTaskId === d.taskId) {
1010
+ loadOrchestratorProgress(d.taskId).then(function(){
1011
+ if (!State.orchProgress ||
1012
+ (!State.orchProgress.steps || State.orchProgress.steps.length===0)) {
1013
+ setTimeout(function(){ if (State.selectedTaskId===d.taskId) loadOrchestratorProgress(d.taskId); }, 2500);
1014
+ }
1015
+ });
1016
+ }
1017
+ }, 1500);
1018
+ }
1019
+ catch(err){ btn.disabled=false; btn.textContent='⚡ 启动单次执行'; toast('error','请求失败: '+err.message); }
1020
+ }
1021
+
1022
+ /* ─── Tab 切换 ──────────────────────────────────────────────────────── */
1023
+ function switchToOrchTab(loop) {
1024
+ document.querySelectorAll('.main-tab').forEach(function(t){t.classList.remove('active');});
1025
+ document.querySelector('.main-tab[data-tab="orchestrator"]').classList.add('active');
1026
+ document.querySelectorAll('.tab-content').forEach(function(c){c.classList.remove('active');});
1027
+ $('tabOrchestrator').classList.add('active'); State.activeMainTab='orchestrator';
1028
+ if (loop != null && State.selectedTaskId) { loadOrchestratorProgress(State.selectedTaskId, loop); }
1029
+ else { renderGraph(); }
1030
+ }
1031
+ function switchToBrainTab(loop) {
1032
+ document.querySelectorAll('.main-tab').forEach(function(t){t.classList.remove('active');});
1033
+ document.querySelector('.main-tab[data-tab="brain"]').classList.add('active');
1034
+ document.querySelectorAll('.tab-content').forEach(function(c){c.classList.remove('active');});
1035
+ $('tabBrain').classList.add('active'); State.activeMainTab='brain';
1036
+ if (loop != null && State.selectedTaskId) { loadBrainPlan(State.selectedTaskId, loop); }
1037
+ }
1038
+
1039
+ /* ─── 事件绑定 & 初始化 ──────────────────────────────────────────────── */
1040
+ document.querySelectorAll('.main-tab').forEach(function(tab){
1041
+ tab.onclick=function(){
1042
+ document.querySelectorAll('.main-tab').forEach(function(t){t.classList.remove('active');});
1043
+ tab.classList.add('active');
1044
+ document.querySelectorAll('.tab-content').forEach(function(c){c.classList.remove('active');});
1045
+ var tgt=tab.dataset.tab; State.activeMainTab=tgt;
1046
+ if(tgt==='rounds')$('tabRounds').classList.add('active');
1047
+ else if(tgt==='orchestrator')$('tabOrchestrator').classList.add('active');
1048
+ else if(tgt==='brain')$('tabBrain').classList.add('active');
1049
+ };
1050
+ });
1051
+
1052
+ document.querySelectorAll('.detail-tab').forEach(function(tab){
1053
+ tab.onclick=function(){
1054
+ document.querySelectorAll('.detail-tab').forEach(function(t){t.classList.remove('active');});
1055
+ tab.classList.add('active'); State.activeDetailTab=tab.dataset.tab; State._convRenderedStep=null; renderDetailPanel();
1056
+ };
1057
+ });
1058
+
1059
+ $('loopSearch').oninput=filterLoops;
1060
+ $('btnNewLoop').onclick=openNewLoopModal;
1061
+ $('newLoopClose').onclick=closeNewLoopModal;
1062
+ $('newLoopOverlay').onclick=function(e){if(e.target===$('newLoopOverlay'))closeNewLoopModal();};
1063
+ $('btnNewLoopCancel').onclick=closeNewLoopModal;
1064
+ $('btnNewLoopSubmit').onclick=submitNewLoop;
1065
+ $('btnLoopAbort').onclick=abortLoop;
1066
+ $('btnLoopRefresh').onclick=function(){if(State.selectedTaskId){State.selectedLoop=null;loadAllData(State.selectedTaskId);}};
1067
+ $('modalClose').onclick=closeModal;
1068
+ $('modalOverlay').onclick=function(e){if(e.target===$('modalOverlay'))closeModal();};
1069
+ $('advancedToggle').onclick=function(){$('advancedSection').classList.toggle('show');$('advancedToggle').classList.toggle('open');};
1070
+
1071
+ /* ─── 单次执行事件绑定 ─── */
1072
+ $('btnNewSingle').onclick=openNewSingleModal;
1073
+ $('newSingleClose').onclick=closeNewSingleModal;
1074
+ $('newSingleOverlay').onclick=function(e){if(e.target===$('newSingleOverlay'))closeNewSingleModal();};
1075
+ $('btnNewSingleCancel').onclick=closeNewSingleModal;
1076
+ $('btnNewSingleSubmit').onclick=submitNewSingle;
1077
+ $('singleAdvancedToggle').onclick=function(){$('singleAdvancedSection').classList.toggle('show');$('singleAdvancedToggle').classList.toggle('open');};
1078
+ $('btnGraphFit').onclick=graphFitToView;
1079
+ $('btnGraphZoomIn').onclick=function(){graphZoomBy(1.2);};
1080
+ $('btnGraphZoomOut').onclick=function(){graphZoomBy(0.83);};
1081
+ $('btnGraphRefresh').onclick=function(){if(State.selectedTaskId)loadOrchestratorProgress(State.selectedTaskId, State.selectedLoop);};
1082
+ initGraphInteraction();
1083
+
1084
+ // 暴露给 onclick 内联调用
1085
+ window.selectLoop=selectLoop;
1086
+ window.selectStep=selectStep;
1087
+ window.viewFile=viewFile;
1088
+ window.loadCheckDetail=loadCheckDetail;
1089
+ window.loadCheckSession=loadCheckSession;
1090
+ window.switchToOrchTab=switchToOrchTab;
1091
+ window.switchToBrainTab=switchToBrainTab;
1092
+ window.showModal=showModal;
1093
+ window.copySessionId=copySessionId;
1094
+
1095
+ /* ─── 循环控制台懒加载初始化(由 ViewMode.set('loop') 触发) ────────── */
1096
+ var _loopInited = false;
1097
+ function initLoopPanel() {
1098
+ if (_loopInited) return;
1099
+ _loopInited = true;
1100
+ loadLoopList();
1101
+ setInterval(function(){ loadLoopList(); }, 30000);
1102
+ }