@ohos-cpf/3rdloop 0.0.4 → 0.0.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.
Files changed (47) hide show
  1. package/README.md +119 -128
  2. package/lib/cli.js +25 -0
  3. package/lib/serve.js +280 -0
  4. package/lib/update.js +46 -6
  5. package/lib/web-ext.js +454 -0
  6. package/lib/web.js +664 -0
  7. package/package.json +2 -1
  8. package/vendor/Server/CLI/opencode/index.js +2 -2
  9. package/vendor/Server/DbUse/StorageManager.js +7 -1
  10. package/vendor/Server/Routes/controllers/FlexRunnerController.js +92 -0
  11. package/vendor/Server/Routes/routes/flexrunner.js +2 -0
  12. package/vendor/Server/Skills/arkts-code-use/SKILL.md +270 -0
  13. package/vendor/Server/Skills/arkts-code-use/assets/TEMPLATES.md +367 -0
  14. package/vendor/Server/Skills/arkts-code-use/references/API_VERIFICATION.md +144 -0
  15. package/vendor/Server/Skills/arkts-code-use/references/ARKTS_RULES.md +240 -0
  16. package/vendor/Server/Skills/arkts-code-use/references/CODE_PATTERNS.md +431 -0
  17. package/vendor/Server/Skills/arkts-code-use/references/SYNTAX_CHECK_GUIDE.md +164 -0
  18. package/vendor/Server/Skills/arkts-code-use/scripts/verify-arkts.cjs +428 -0
  19. package/vendor/Server/Skills/gitcode-repo-fork/SKILL.md +310 -0
  20. package/vendor/Server/Skills/gitcode-repo-fork/assets/FORK_REPORT_TEMPLATE.md +113 -0
  21. package/vendor/Server/Skills/gitcode-repo-fork/references/FORK_DECISION_GUIDE.md +124 -0
  22. package/vendor/Server/Skills/gitcode-repo-fork/references/GITCODE_FORK_API.md +95 -0
  23. package/vendor/Server/Skills/gitcode-repo-fork/scripts/gitcode-fork.cjs +285 -0
  24. package/vendor/VERSION +3 -3
  25. package/web/css/arktslibrarycheck.css +322 -0
  26. package/web/css/codecheck.css +464 -0
  27. package/web/css/flutterlibrarycheck.css +322 -0
  28. package/web/css/knowledge.css +332 -0
  29. package/web/css/loop.css +578 -0
  30. package/web/css/md-reader.css +240 -0
  31. package/web/css/rnlibrarycheck.css +322 -0
  32. package/web/css/theme.css +702 -0
  33. package/web/index.html +713 -0
  34. package/web/js/arktslibrarycheck.js +1495 -0
  35. package/web/js/codecheck.js +1098 -0
  36. package/web/js/flutterlibrarycheck.js +1447 -0
  37. package/web/js/health.js +69 -0
  38. package/web/js/knowledge.js +358 -0
  39. package/web/js/loop.js +1102 -0
  40. package/web/js/md-reader.js +435 -0
  41. package/web/js/navigation.js +238 -0
  42. package/web/js/rnlibrarycheck.js +1462 -0
  43. package/web/js/stats.js +110 -0
  44. package/web/js/theme.js +46 -0
  45. package/web/js/utils.js +228 -0
  46. package/web/knowledge.html +146 -0
  47. package/web/loop.html +219 -0
@@ -0,0 +1,1098 @@
1
+ /* ════════════════════════════════════════════════════════════════════
2
+ * codecheck.js — PR 代码检视模块(前端编排)
3
+ *
4
+ * 设计理念:后端不感知定制化。前端直接调用通用 Orchestrator API
5
+ * 完成固定编排:构造单步骤 taskDef → load → start → SSE 监控 → 读取结果
6
+ *
7
+ * 编排流程:
8
+ * 1. 前端构造 taskDef(单步骤,skillDirectory 指向 code-check SKILL)
9
+ * 2. POST /api/orchestrator/load — 加载任务定义
10
+ * 3. POST /api/orchestrator/start — 启动编排
11
+ * 4. GET /stream/tasks/:taskId — SSE 实时监控
12
+ * 5. GET /api/orchestrator/progress?taskId= — 轮询进度(SSE 补充)
13
+ * 6. GET /api/flexrunner/steps?taskId= — 获取结果文件列表
14
+ * 7. GET /api/flexrunner/file?path= — 读取检视报告
15
+ * 8. POST /api/orchestrator/abort — 取消任务
16
+ * 9. POST /api/flexrunner/clean-source — 终态后清理源码(仅删 source/,报告保留)
17
+ *
18
+ * 依赖:utils.js(apiFetch / API / createPagination / escapeHtml / toast / $)
19
+ * ════════════════════════════════════════════════════════════════════ */
20
+ 'use strict';
21
+
22
+ /* ── SKILL 名称映射(前端固定编排配置)───────────────────────────────── */
23
+ var _CC_SKILL_MAP = {
24
+ arkts: 'arkts-code-check',
25
+ flutter: 'flutter-code-check',
26
+ rn: 'rn-code-check',
27
+ tpccplus: 'tpc-cpp-check',
28
+ };
29
+
30
+ var _CC_SKILL_INFO = {
31
+ 'arkts-code-check': {
32
+ name: 'ArkTS PR 代码检视',
33
+ desc: '自动化检视 ArkTS 三方库 Pull Request,包括获取 PR 差异、深度分析代码变更、生成结构化检视报告',
34
+ },
35
+ 'flutter-code-check': {
36
+ name: 'Flutter PR 代码检视',
37
+ desc: '自动化检视 Flutter 三方库 Pull Request,聚焦鸿蒙端适配代码质量',
38
+ },
39
+ 'rn-code-check': {
40
+ name: 'React Native PR 代码检视',
41
+ desc: '自动化检视 React Native 三方库 Pull Request,聚焦鸿蒙端适配代码质量',
42
+ },
43
+ 'tpc-cpp-check': {
44
+ name: 'TPC C/C++ PR 规范性检视',
45
+ desc: '检视 tpc_c_cplusplus 仓库适配库 PR 的规范性,包括编译脚本(HPKBUILD)、文档格式、目录结构、SHA512SUM 等,并输出结构化检视报告。仅基于 PR 差异和 GitCode API 获取文件内容进行检视,无需克隆整个仓库',
46
+ },
47
+ };
48
+
49
+ /* ── 状态 ────────────────────────────────────────────────────────────── */
50
+ var _ccCounter = 0;
51
+ var _ccTasks = {}; // idx -> { taskId, prUrl, libraryType, sse, timer, startTime, ... }
52
+ var _ccPager = null;
53
+ var _ccInited = false;
54
+
55
+ /* ── 阶段标签(映射 Orchestrator 状态到检视阶段)────────────────────── */
56
+ var _CC_STAGE_LABELS = {
57
+ pending: '等待中',
58
+ ready: '就绪',
59
+ running: '检视中…',
60
+ completed: '检视完成 ✓',
61
+ failed: '检视失败',
62
+ aborted: '已停止',
63
+ error: '错误',
64
+ idle: '空闲',
65
+ loaded: '已加载',
66
+ deadlocked: '死锁',
67
+ };
68
+
69
+ /* ── PR 地址解析 ─────────────────────────────────────────────────────── */
70
+
71
+ /**
72
+ * 从 PR URL 解析出 owner、repo、prNumber。
73
+ *
74
+ * 支持格式:
75
+ * https://gitee.com/openharmony-tpc/community/pulls/123
76
+ * https://gitcode.com/owner/repo/pulls/123
77
+ * https://github.com/owner/repo/pull/123
78
+ *
79
+ * @param {string} prUrl
80
+ * @returns {{owner:string, repo:string, prNumber:string, label:string}}
81
+ */
82
+ function _ccParsePrUrl(prUrl) {
83
+ var fallback = { owner: 'unknown', repo: 'unknown', prNumber: '0', label: 'unknown_unknown_0' };
84
+ if (!prUrl) return fallback;
85
+
86
+ try {
87
+ var url = new URL(prUrl);
88
+ var parts = url.pathname.split('/').filter(Boolean);
89
+ // 典型: /owner/repo/pulls/123 或 /owner/repo/pull/123
90
+ var pullIdx = -1;
91
+ for (var i = 0; i < parts.length; i++) {
92
+ if (/^pulls?$/.test(parts[i])) { pullIdx = i; break; }
93
+ }
94
+ if (pullIdx >= 2) {
95
+ var owner = parts[0];
96
+ var repo = parts[1];
97
+ var prNumber = parts[pullIdx + 1] || '0';
98
+ return {
99
+ owner: owner,
100
+ repo: repo,
101
+ prNumber: prNumber,
102
+ label: owner + '_' + repo + '_' + prNumber,
103
+ };
104
+ }
105
+ // 回退:用 pathname 中的段拼接
106
+ if (parts.length >= 2) {
107
+ return {
108
+ owner: parts[0],
109
+ repo: parts[1],
110
+ prNumber: parts[parts.length - 1] || '0',
111
+ label: parts.slice(0, 3).join('_'),
112
+ };
113
+ }
114
+ } catch (e) { /* 非法 URL */ }
115
+ return fallback;
116
+ }
117
+
118
+ /**
119
+ * 从 taskId 解析出显示名称。
120
+ * taskId 格式: codecheck_{owner}_{repo}_{prNum}_{timestamp}
121
+ * → 显示: {owner}/{repo} #{prNum}
122
+ */
123
+ function _ccTaskIdToLabel(taskId) {
124
+ if (!taskId) return '—';
125
+ // 去掉 codecheck_ 前缀
126
+ var rest = taskId.replace(/^codecheck_/, '');
127
+ var parts = rest.split('_');
128
+ if (parts.length >= 4) {
129
+ // owner_repo_prNum_timestamp → owner/repo #prNum
130
+ return parts[0] + '/' + parts[1] + ' #' + parts[2];
131
+ }
132
+ // 格式不匹配,截断显示
133
+ return rest.length > 40 ? rest.substring(0, 40) + '…' : rest;
134
+ }
135
+
136
+ /**
137
+ * 从 taskId 解析出 owner_repo_prNum 标识。
138
+ */
139
+ function _ccTaskIdToLabelRaw(taskId) {
140
+ if (!taskId) return '';
141
+ var rest = taskId.replace(/^codecheck_/, '');
142
+ var parts = rest.split('_');
143
+ if (parts.length >= 4) {
144
+ return parts[0] + '_' + parts[1] + '_' + parts[2];
145
+ }
146
+ return rest;
147
+ }
148
+
149
+ /* ── 辅助函数 ────────────────────────────────────────────────────────── */
150
+
151
+ function _ccEsc(s) {
152
+ if (typeof escapeHtml === 'function') return escapeHtml(s);
153
+ if (s == null) return '';
154
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
155
+ }
156
+
157
+ function _ccGuardSubmit(btn, loadingText, normalText) {
158
+ if (!btn || btn.disabled) return false;
159
+ btn.disabled = true;
160
+ btn.textContent = loadingText;
161
+ btn._ccNormalText = normalText;
162
+ return true;
163
+ }
164
+
165
+ function _ccGuardSubmitRestore(btn) {
166
+ if (!btn) return;
167
+ var normalText = btn._ccNormalText || '提交';
168
+ setTimeout(function () {
169
+ if (btn.disabled) { btn.disabled = false; btn.textContent = normalText; }
170
+ }, 5000);
171
+ }
172
+
173
+ function _ccGuardSubmitReset(btn) {
174
+ if (!btn) return;
175
+ btn.disabled = false;
176
+ btn.textContent = btn._ccNormalText || '提交';
177
+ }
178
+
179
+ function _ccFormatTime(isoString) {
180
+ if (!isoString) return '';
181
+ try {
182
+ return new Date(isoString).toLocaleString('zh-CN', {
183
+ month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
184
+ });
185
+ } catch (e) { return isoString; }
186
+ }
187
+
188
+ /* ── 面板初始化(懒加载,由 Nav.register 触发)───────────────────────── */
189
+ function initCodeCheckPanel() {
190
+ if (_ccInited) return;
191
+ _ccInited = true;
192
+
193
+ // 渲染 SKILL 介绍
194
+ _renderSkillIntro();
195
+
196
+ // 加载历史任务
197
+ _loadCodeCheckHistory();
198
+
199
+ // 绑定提交按钮
200
+ var btn = $('cc-submit-btn');
201
+ if (btn) btn.addEventListener('click', doCodeCheck);
202
+
203
+ // 绑定库类型切换 → 更新 SKILL 介绍
204
+ var libSelect = $('cc-lib-type');
205
+ if (libSelect) libSelect.addEventListener('change', _renderSkillIntro);
206
+
207
+ // 绑定回车提交
208
+ var urlInput = $('cc-pr-url');
209
+ if (urlInput) {
210
+ urlInput.addEventListener('keydown', function (e) {
211
+ if (e.key === 'Enter') doCodeCheck();
212
+ });
213
+ }
214
+
215
+ // 绑定高级选项折叠
216
+ var advToggle = $('cc-advanced-toggle');
217
+ if (advToggle) {
218
+ advToggle.addEventListener('click', function () {
219
+ var section = $('cc-advanced-section');
220
+ if (section) {
221
+ section.classList.toggle('show');
222
+ advToggle.classList.toggle('open');
223
+ }
224
+ });
225
+ }
226
+ }
227
+
228
+ /* ── SKILL 介绍(静态渲染,无需后端接口)────────────────────────────── */
229
+ function _renderSkillIntro() {
230
+ var container = $('cc-skill-intro');
231
+ if (!container) return;
232
+
233
+ var libType = $('cc-lib-type') ? $('cc-lib-type').value : 'arkts';
234
+ var skillName = _CC_SKILL_MAP[libType] || _CC_SKILL_MAP.arkts;
235
+ var info = _CC_SKILL_INFO[skillName] || { name: skillName, desc: '' };
236
+
237
+ var typeColors = {
238
+ arkts: { bg: 'rgba(0,113,227,0.12)', color: 'var(--blue)', border: 'rgba(0,113,227,0.3)' },
239
+ flutter: { bg: 'var(--flutter-bg)', color: 'var(--flutter)', border: 'var(--flutter-border)' },
240
+ rn: { bg: 'var(--rn-bg)', color: 'var(--rn)', border: 'var(--rn-border)' },
241
+ tpccplus: { bg: 'rgba(175,82,222,0.12)', color: 'var(--purple)', border: 'rgba(175,82,222,0.3)' },
242
+ };
243
+ var cc = typeColors[libType] || typeColors.arkts;
244
+
245
+ container.innerHTML =
246
+ '<div class="cc-skill-header">' +
247
+ '<span class="cc-skill-icon">🛠️</span>' +
248
+ '<span class="cc-skill-title">代码检视 SKILL</span>' +
249
+ '</div>' +
250
+ '<div class="cc-skill-grid">' +
251
+ '<div class="cc-skill-card">' +
252
+ '<div class="cc-skill-card-header">' +
253
+ '<div style="display:flex;align-items:center;gap:8px;flex:1">' +
254
+ '<span class="cc-skill-badge" style="background:' + cc.bg + ';color:' + cc.color + ';border:1px solid ' + cc.border + '">' +
255
+ _ccEsc(libType.toUpperCase()) +
256
+ '</span>' +
257
+ '<span class="cc-skill-name">' + _ccEsc(info.name) + '</span>' +
258
+ '</div>' +
259
+ '</div>' +
260
+ '<div class="cc-skill-card-desc">' + _ccEsc(info.desc) + '</div>' +
261
+ '</div>' +
262
+ '</div>';
263
+ }
264
+
265
+ /* ── 历史任务加载(分页,只靠列表响应渲染,不逐任务调 progress) ──────── */
266
+
267
+ /**
268
+ * 渲染一页历史任务卡片。
269
+ * 数据全部来自 /api/orchestrator/tasks 列表响应:
270
+ * - status:终态由后端在任务结束时回写注册表(_patchRegistryFinalState)
271
+ * - metadata.mergeLevel:结构化指标(后端收割 meta .json 后写入)
272
+ * 仅活跃状态(running/pending/ready/loaded/idle)的任务才连接 SSE + 轮询。
273
+ *
274
+ * @param {object[]} tasks - 一页任务(来自列表 API)
275
+ * @param {boolean} isFirstPage
276
+ */
277
+ function _renderCCHistoryPage(tasks, isFirstPage) {
278
+ // 过滤出 codecheck_ 前缀的任务
279
+ var ccTasks = tasks.filter(function (t) {
280
+ var tid = t.taskId || t.task_id || '';
281
+ return tid.startsWith('codecheck_');
282
+ });
283
+
284
+ var emptyEl = $('cc-tasks-empty');
285
+ if (emptyEl) emptyEl.style.display = ccTasks.length > 0 ? 'none' : '';
286
+
287
+ // 后端按 created_at 降序返回(最新在前);卡片用 insertBefore 前插(后渲染的在上面),
288
+ // 故需反转遍历:最旧的先渲染、被前插到下面 → 最新的最终在最上面
289
+ ccTasks.reverse();
290
+ for (var i = 0; i < ccTasks.length; i++) {
291
+ var t = ccTasks[i];
292
+ var tid = t.taskId || t.task_id;
293
+ var status = t.status || 'idle';
294
+ var startTime = t.created_at || t.createdAt || t.startTime;
295
+ var endTime = t.endTime || (t.progress && t.progress.endTime);
296
+
297
+ // 从 description(body.title)中解析 prUrl 和 libraryType
298
+ // 格式: PR检视|{libraryType}|{prUrl}
299
+ var desc = t.description || t.title || '';
300
+ var prUrl = '';
301
+ var libType = 'arkts';
302
+ if (desc.indexOf('PR检视|') === 0) {
303
+ var segs = desc.split('|');
304
+ if (segs.length >= 3) {
305
+ libType = segs[1] || 'arkts';
306
+ prUrl = segs[2] || '';
307
+ }
308
+ } else if (desc) {
309
+ // 兼容旧格式:description 直接是 prUrl
310
+ prUrl = desc;
311
+ }
312
+
313
+ var displayLabel = _ccTaskIdToLabel(tid);
314
+
315
+ var idx = ++_ccCounter;
316
+ _ccTasks[idx] = {
317
+ taskId: tid,
318
+ prUrl: prUrl,
319
+ libraryType: libType,
320
+ sse: null,
321
+ timer: null,
322
+ startTime: startTime ? new Date(startTime).getTime() : Date.now(),
323
+ startedAt: startTime || null,
324
+ finishedAt: endTime || null,
325
+ resultPath: null,
326
+ _cleanStarted: false, // 源码清理是否已触发(幂等防重)
327
+ // 列表响应中的结构化指标(终态任务直接渲染徽章,无需额外请求)
328
+ metrics: t.metadata || null,
329
+ };
330
+
331
+ _createCCCard(idx, displayLabel, prUrl, libType, startTime, true);
332
+ _updateCCStage(idx, status, { startedAt: startTime, finishedAt: endTime });
333
+
334
+ // 终态任务:直接用列表携带的 metadata 渲染等级徽章(零额外请求)
335
+ var level = _ccExtractMergeLevel(t.metadata || null, null);
336
+ if (level) {
337
+ _updateMergeLevelBadge(idx, level, null);
338
+ }
339
+
340
+ // 活跃任务才连接 SSE + 轮询(终态任务不再逐个调 progress)
341
+ _handleTaskStage(idx, status);
342
+ }
343
+ }
344
+
345
+ /**
346
+ * 初始化检视历史分页器。
347
+ * taskIdPrefix 前缀过滤:只分页查询 codecheck_ 任务(后端 SQL 过滤,
348
+ * 避免与 fluttercheck 等其他编排任务混页)。
349
+ */
350
+ function _initCCHistoryPager() {
351
+ if (_ccPager) return _ccPager;
352
+ _ccPager = createPagination({
353
+ apiPath: '/api/orchestrator/tasks?taskIdPrefix=codecheck_',
354
+ listContainerId: 'cc-tasks-list',
355
+ emptyId: 'cc-tasks-empty',
356
+ countId: 'cc-task-count',
357
+ pagerId: 'cc-tasks-pager',
358
+ renderPage: _renderCCHistoryPage,
359
+ pageSize: 20
360
+ });
361
+ return _ccPager;
362
+ }
363
+
364
+ async function _loadCodeCheckHistory() {
365
+ var pg = _initCCHistoryPager();
366
+ await pg.load(1);
367
+ }
368
+
369
+ /* ── 根据阶段决定后续操作(重连 SSE / 懒加载结果)──────────────────── */
370
+ function _handleTaskStage(idx, stage) {
371
+ var activeStages = ['running', 'pending', 'ready', 'loaded', 'idle'];
372
+ if (activeStages.indexOf(stage) >= 0) {
373
+ _connectCCSSE(idx);
374
+ _startProgressPoll(idx);
375
+ } else {
376
+ _finishCCSSE(idx);
377
+ // 终态历史任务:静默补删源码(覆盖检视期间页面关闭、终态清理未触发的场景)
378
+ _ccCleanSource(idx, true);
379
+ // 终态任务不主动拉文件(上千任务时列表渲染零额外请求);
380
+ // 展开卡片 / 点击预览/下载时才懒加载(toggleCCCard / viewCCReport / downloadCCReport)
381
+ }
382
+ }
383
+
384
+ /* ── 提交代码检视(前端编排:构造 taskDef → load → start)───────────── */
385
+ async function doCodeCheck() {
386
+ var prUrl = $('cc-pr-url').value.trim();
387
+ var libType = $('cc-lib-type').value;
388
+ var skillDir = $('cc-skill-dir') ? $('cc-skill-dir').value.trim() : '';
389
+
390
+ if (!prUrl) { _showCCErr('PR 地址不能为空'); return; }
391
+
392
+ var btn = $('cc-submit-btn');
393
+ if (!_ccGuardSubmit(btn, '提交中…', '▶ 开始检视')) return;
394
+ $('cc-error').style.display = 'none';
395
+
396
+ try {
397
+ // ── 1. 解析 PR 地址,生成 taskId ──
398
+ var prInfo = _ccParsePrUrl(prUrl);
399
+ var ts = Date.now().toString(36);
400
+ var taskId = 'codecheck_' + prInfo.label + '_' + ts;
401
+
402
+ // ── 2. 确定 SKILL ──
403
+ // 按库类型指定对应 SKILL(arkts-code-check / flutter-code-check / rn-code-check),
404
+ // 后端 FlexRunner 收到 specifiedSkill 后跳过自动匹配,直接解析使用该 SKILL
405
+ var skillName = _CC_SKILL_MAP[libType] || _CC_SKILL_MAP.arkts;
406
+
407
+ // ── 3. 构造任务描述 ──
408
+ var taskDescription = [
409
+ '请对以下 PR 进行代码检视。',
410
+ '',
411
+ 'PR 地址: ' + prUrl,
412
+ ].join('\n');
413
+
414
+ // ── 4. 构造 taskDef(单步骤固定编排)──
415
+ // body.title 编码 prUrl 和 libraryType,供历史恢复解析
416
+ // 格式: PR检视|{libraryType}|{prUrl}
417
+ // skillDirectory:用户自定义 SKILL 根目录(可选),留空由后端使用默认 Server/Skills
418
+ // specifiedSkill:按库类型固定指定对应 SKILL,跳过 SkillSelector 自动匹配
419
+ var taskDef = {
420
+ taskId: taskId,
421
+ title: 'PR检视|' + libType + '|' + prUrl,
422
+ skillDirectory: skillDir || '',
423
+ loopCount: 0,
424
+ steps: [{
425
+ stepId: 1,
426
+ taskDescription: taskDescription,
427
+ dependId: 0,
428
+ nextStepId: 0,
429
+ failStepId: 0,
430
+ maxRetries: 3,
431
+ skillDirectory: skillDir || '',
432
+ specifiedSkill: skillName,
433
+ inputReport: '',
434
+ }]
435
+ };
436
+
437
+ // ── 5. POST /api/orchestrator/load ──
438
+ var loadR = await apiFetch('POST', '/api/orchestrator/load', taskDef);
439
+ if (loadR.status >= 400 || !loadR.data || loadR.data.status !== 'ok') {
440
+ _showCCErr((loadR.data && (loadR.data.error || loadR.data.message)) || ('HTTP ' + loadR.status));
441
+ _ccGuardSubmitReset(btn);
442
+ return;
443
+ }
444
+
445
+ // ── 6. POST /api/orchestrator/start ──
446
+ var startR = await apiFetch('POST', '/api/orchestrator/start', { taskId: taskId });
447
+ if (startR.status >= 400 || !startR.data || startR.data.status !== 'ok') {
448
+ _showCCErr((startR.data && (startR.data.error || startR.data.message)) || ('HTTP ' + startR.status));
449
+ _ccGuardSubmitReset(btn);
450
+ return;
451
+ }
452
+
453
+ // ── 7. 创建任务卡片 + 连接 SSE ──
454
+ var idx = ++_ccCounter;
455
+ var displayLabel = prInfo.owner + '/' + prInfo.repo + ' #' + prInfo.prNumber;
456
+ _ccTasks[idx] = {
457
+ taskId: taskId,
458
+ prUrl: prUrl,
459
+ libraryType: libType,
460
+ sse: null,
461
+ timer: null,
462
+ pollTimer: null,
463
+ startTime: Date.now(),
464
+ startedAt: null,
465
+ finishedAt: null,
466
+ resultPath: null,
467
+ _cleanStarted: false, // 源码清理是否已触发(幂等防重)
468
+ };
469
+
470
+ _createCCCard(idx, displayLabel, prUrl, libType, new Date().toISOString());
471
+ _connectCCSSE(idx);
472
+ _startProgressPoll(idx);
473
+
474
+ _ccGuardSubmitRestore(btn);
475
+ toast('success', '检视任务已启动: ' + displayLabel);
476
+
477
+ } catch (e) {
478
+ _showCCErr(e.message);
479
+ _ccGuardSubmitReset(btn);
480
+ }
481
+ }
482
+
483
+ /* ── 进度轮询(SSE 的补充,确保状态同步)────────────────────────────── */
484
+ function _startProgressPoll(idx) {
485
+ var task = _ccTasks[idx];
486
+ if (!task || task.pollTimer) return;
487
+
488
+ task.pollTimer = setInterval(async function () {
489
+ try {
490
+ var r = await apiFetch('GET', '/api/orchestrator/progress?taskId=' + encodeURIComponent(task.taskId), null, 10000);
491
+ if (!r.data || r.data.status !== 'ok') return;
492
+
493
+ var progress = r.data.progress;
494
+ if (!progress) return;
495
+
496
+ // 更新阶段
497
+ var stage = progress.status;
498
+ _updateCCStage(idx, stage, {
499
+ startedAt: progress.startTime,
500
+ finishedAt: progress.endTime,
501
+ });
502
+
503
+ // 检查步骤状态
504
+ if (progress.steps && progress.steps.length > 0) {
505
+ var step = progress.steps[0];
506
+ if (step.status) {
507
+ _updateCCStage(idx, step.status, {
508
+ startedAt: step.startTime,
509
+ finishedAt: step.endTime,
510
+ resultPath: step.resultPath,
511
+ });
512
+ }
513
+ // 结构化指标到达即更新等级徽章(后端收割落库,无需读报告)
514
+ if (step.metrics && step.metrics.mergeLevel) {
515
+ _updateMergeLevelBadge(idx, String(step.metrics.mergeLevel).toUpperCase(), null);
516
+ }
517
+ }
518
+
519
+ // 终态:停止轮询
520
+ if (['completed', 'failed', 'aborted', 'deadlocked'].indexOf(stage) >= 0) {
521
+ _stopProgressPoll(idx);
522
+ _finishCCSSE(idx);
523
+ _loadCCResultFiles(idx);
524
+ // 最后环节:清理源码(仅删 taskDir/source/,报告与过程文件保留)
525
+ _ccCleanSource(idx);
526
+ }
527
+ } catch (e) { /* 静默失败 */ }
528
+ }, 3000);
529
+ }
530
+
531
+ function _stopProgressPoll(idx) {
532
+ var task = _ccTasks[idx];
533
+ if (task && task.pollTimer) {
534
+ clearInterval(task.pollTimer);
535
+ task.pollTimer = null;
536
+ }
537
+ }
538
+
539
+ /* ── 合入等级提取 ────────────────────────────────────────────────────── */
540
+
541
+ /**
542
+ * 从步骤 metrics(后端收割的结构化指标)或报告文本中提取合入等级。
543
+ * 优先级:metrics.mergeLevel(后端落库,页面关闭不丢失)> 报告正则(存量旧任务回退)。
544
+ *
545
+ * @param {object|null} metrics - 步骤结构化指标(progress.steps[0].metrics)
546
+ * @param {string|null} [reportContent] - 检视报告文本(回退解析用)
547
+ * @returns {string|null} PASS / CONDITIONAL / NEED_FIX / REJECT,提取不到返回 null
548
+ */
549
+ function _ccExtractMergeLevel(metrics, reportContent) {
550
+ // ── 优先:结构化指标(SKILL 写入 meta .json,后端收割落库) ──
551
+ if (metrics && typeof metrics === 'object' && metrics.mergeLevel) {
552
+ var level = String(metrics.mergeLevel).toUpperCase();
553
+ if (/^(PASS|CONDITIONAL|NEED_FIX|REJECT)$/.test(level)) return level;
554
+ }
555
+ // ── 回退:报告文本正则(存量旧任务,无 meta 文件) ──
556
+ if (reportContent) {
557
+ // 匹配多种格式:
558
+ // 合入建议:PASS
559
+ // ## 合入建议\n### **NEED_FIX**
560
+ // 合入建议\nNEED_FIX
561
+ var mergeMatch = reportContent.match(/合入建议[^A-Z]*(PASS|CONDITIONAL|NEED_FIX|REJECT)/i);
562
+ if (mergeMatch) return mergeMatch[1].toUpperCase();
563
+ }
564
+ return null;
565
+ }
566
+
567
+ /* ── 加载结果文件(终态懒加载:仅在需要展示下载/预览/等级回退时调用) ──── */
568
+ async function _loadCCResultFiles(idx) {
569
+ var task = _ccTasks[idx];
570
+ if (!task) return;
571
+
572
+ // 已解析过则跳过(幂等)
573
+ if (task.filesResolved) return;
574
+
575
+ try {
576
+ var r = await apiFetch('GET', '/api/flexrunner/steps?taskId=' + encodeURIComponent(task.taskId));
577
+ if (!r.data || r.data.status !== 'ok') return;
578
+
579
+ var files = r.data.files || [];
580
+ if (files.length === 0) return;
581
+
582
+ // 选择预览/下载目标文件:
583
+ // 1. 优先 check-report.md — SKILL 生成的结构化检视报告(用户真正要看的内容)
584
+ // 2. 回退 result .md — 框架生成的执行状态文件(check-report 缺失/旧任务时兜底)
585
+ var reportFile = null;
586
+ var resultFile = null;
587
+ for (var i = files.length - 1; i >= 0; i--) {
588
+ var fname = files[i].filename || '';
589
+ if (!reportFile && fname === 'check-report.md') {
590
+ reportFile = files[i];
591
+ }
592
+ if (!resultFile && fname.indexOf('result') >= 0) {
593
+ resultFile = files[i];
594
+ }
595
+ }
596
+ var targetFile = reportFile || resultFile;
597
+
598
+ if (targetFile) {
599
+ task.resultPath = targetFile.path;
600
+ task.filesResolved = true;
601
+ // 显示下载/预览按钮
602
+ var dlWrap = document.getElementById('cc-' + idx + '-dl-wrap');
603
+ if (dlWrap) dlWrap.style.display = 'flex';
604
+
605
+ // 已从 metrics 拿到等级则不再读报告文本(省流量)
606
+ if (task.metrics && task.metrics.mergeLevel) return;
607
+
608
+ // 回退:读取报告内容提取合入建议(存量旧任务)
609
+ var fileR = await apiFetch('GET', '/api/flexrunner/file?path=' + encodeURIComponent(targetFile.path));
610
+ if (fileR.data && fileR.data.status === 'ok' && fileR.data.content) {
611
+ var level = _ccExtractMergeLevel(null, fileR.data.content);
612
+ if (level) {
613
+ _updateMergeLevelBadge(idx, level, null);
614
+ }
615
+ }
616
+ }
617
+ } catch (e) { /* 静默失败 */ }
618
+ }
619
+
620
+ /* ── 源码清理(检视最后环节:删除 taskDir/source/,节省存储)──────────
621
+ *
622
+ * arkts/flutter/rn-code-check 检视过程中会把 PR 目标仓库克隆到任务目录的
623
+ * source/ 下,检视终态后源码不再被引用(报告 .md / meta .json 等过程文件
624
+ * 均在 taskDir 根目录,不受影响);tpc-cpp-check 不克隆源码,后端幂等返回。
625
+ * 触发时机:任务终态(轮询 / SSE complete / 取消);历史终态任务渲染时
626
+ * 静默补删(覆盖检视期间页面被关闭、清理未触发的场景)。
627
+ * ──────────────────────────────────────────────────────────────────── */
628
+
629
+ /** 字节数格式化:B / KB / MB / GB */
630
+ function _ccFormatBytes(n) {
631
+ if (!isFinite(n) || n <= 0) return '0 B';
632
+ var units = ['B', 'KB', 'MB', 'GB'];
633
+ var i = 0;
634
+ while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
635
+ return (i === 0 ? Math.round(n) : n.toFixed(1)) + ' ' + units[i];
636
+ }
637
+
638
+ /**
639
+ * 调用后端清理源码(幂等,失败允许下次触发重试)。
640
+ * @param {number} idx
641
+ * @param {boolean} [silent] - 静默模式(历史补删 / 取消场景,不弹 toast)
642
+ */
643
+ function _ccCleanSource(idx, silent) {
644
+ var task = _ccTasks[idx];
645
+ if (!task || task._cleanStarted) return;
646
+ task._cleanStarted = true;
647
+
648
+ apiFetch('POST', '/api/flexrunner/clean-source', { taskId: task.taskId }, 60000)
649
+ .then(function (r) {
650
+ if (r.data && r.data.status === 'ok' && r.data.cleaned) {
651
+ if (!silent) {
652
+ toast('info', '已清理检视源码,释放 ' + _ccFormatBytes(r.data.freedBytes) + ' 存储空间');
653
+ }
654
+ } else if (!(r.data && r.data.status === 'ok')) {
655
+ // HTTP 错误(如 409 任务仍在执行)→ 失败可重试
656
+ task._cleanStarted = false;
657
+ }
658
+ // ok 且 cleaned=false:无源码可清(未克隆 / 已清理过)→ 幂等完成,不提示
659
+ })
660
+ .catch(function () {
661
+ // 清理失败:重置标记,下次触发(如历史重新渲染)时重试
662
+ task._cleanStarted = false;
663
+ });
664
+ }
665
+
666
+ /* ── 创建检视任务卡片 ────────────────────────────────────────────────── */
667
+ function _createCCCard(idx, displayLabel, prUrl, libType, createdAt, isHistory) {
668
+ var emptyEl = $('cc-tasks-empty');
669
+ if (emptyEl) emptyEl.style.display = 'none';
670
+ var countEl = $('cc-task-count');
671
+ if (countEl) countEl.textContent = Object.keys(_ccTasks).length + ' 个任务';
672
+
673
+ var typeLabel = {
674
+ arkts: 'ArkTS', flutter: 'Flutter', rn: 'React Native', tpccplus: 'TPC C/C++'
675
+ }[libType] || libType;
676
+
677
+ var typeBadgeStyle = {
678
+ arkts: 'background:rgba(0,113,227,0.12);color:var(--blue)',
679
+ flutter: 'background:var(--flutter-bg);color:var(--flutter)',
680
+ rn: 'background:var(--rn-bg);color:var(--rn)',
681
+ tpccplus: 'background:rgba(175,82,222,0.12);color:var(--purple)',
682
+ }[libType] || 'background:rgba(0,113,227,0.12);color:var(--blue)';
683
+
684
+ var timeLabel = createdAt ? _ccFormatTime(createdAt) : '';
685
+
686
+ var card = document.createElement('div');
687
+ card.className = 'cc-task-card running';
688
+ card.id = 'cc-card-' + idx;
689
+ card.innerHTML =
690
+ '<div class="cc-card-header" onclick="toggleCCCard(' + idx + ')">' +
691
+ '<span class="cc-chevron">▶</span>' +
692
+ '<span class="cc-run-dot"></span>' +
693
+ '<span class="cc-lib">' + _ccEsc(displayLabel) + '</span>' +
694
+ '<span class="cc-type-badge" style="' + typeBadgeStyle + '">' + _ccEsc(typeLabel) + '</span>' +
695
+ '<span class="cc-spacer"></span>' +
696
+ '<span class="cc-time" id="cc-' + idx + '-time">' + _ccEsc(timeLabel) + '</span>' +
697
+ '<span class="cc-merge-badge" id="cc-' + idx + '-merge-badge" style="display:none"></span>' +
698
+ '<span class="cc-stage-badge s-pending" id="cc-' + idx + '-badge">等待中</span>' +
699
+ '<button class="cc-stop-btn" id="cc-' + idx + '-stop" onclick="event.stopPropagation();stopCCTask(' + idx + ')" title="取消检视">■ 取消</button>' +
700
+ '<span class="cc-elapsed" id="cc-' + idx + '-elapsed" style="display:none">0s</span>' +
701
+ '</div>' +
702
+ '<div class="cc-card-body" id="cc-' + idx + '-body">' +
703
+ '<div class="cc-meta" title="' + _ccEsc(prUrl) + '">' +
704
+ '<span class="cc-meta-label">PR:</span>' +
705
+ '<span class="cc-meta-value">' + _ccEsc(prUrl) + '</span>' +
706
+ '</div>' +
707
+ '<div class="cc-pipeline">' +
708
+ '<div style="display:flex;align-items:center;gap:14px">' +
709
+ '<div class="cc-pipe-icon" id="cc-' + idx + '-icon">🔍</div>' +
710
+ '<div style="flex:1">' +
711
+ '<div class="cc-pipe-title">PR 代码检视</div>' +
712
+ '<div class="cc-pipe-stage" id="cc-' + idx + '-stage-txt">等待中…</div>' +
713
+ '</div>' +
714
+ '<div class="cc-dl-wrap" id="cc-' + idx + '-dl-wrap" style="display:none;flex-direction:column;gap:6px;align-items:flex-end">' +
715
+ '<button onclick="downloadCCReport(' + idx + ')" class="cc-download-btn">↓ 下载检视报告</button>' +
716
+ '<button onclick="viewCCReport(' + idx + ')" class="cc-preview-btn">↗ 在线预览</button>' +
717
+ '</div>' +
718
+ '</div>' +
719
+ '</div>' +
720
+ '<div class="cc-actions">' +
721
+ '<button onclick="viewCCSession(' + idx + ')" class="cc-session-btn" id="cc-' + idx + '-session-btn">💬 查看对话</button>' +
722
+ '</div>' +
723
+ '</div>';
724
+
725
+ var list = $('cc-tasks-list');
726
+ list.insertBefore(card, list.firstChild);
727
+
728
+ // 卡片默认折叠
729
+ var body = document.getElementById('cc-' + idx + '-body');
730
+ if (body) body.style.display = 'none';
731
+
732
+ // 已完成的历史任务:显示固定时长
733
+ var task = _ccTasks[idx];
734
+ if (isHistory && task.finishedAt && task.startedAt) {
735
+ var durationMs = new Date(task.finishedAt).getTime() - new Date(task.startedAt).getTime();
736
+ var durationSec = Math.floor(Math.max(0, durationMs) / 1000);
737
+ var elapsedEl = document.getElementById('cc-' + idx + '-elapsed');
738
+ if (elapsedEl) {
739
+ elapsedEl.textContent = durationSec < 60 ? durationSec + 's' : Math.floor(durationSec / 60) + 'm ' + (durationSec % 60) + 's';
740
+ elapsedEl.style.display = 'inline';
741
+ }
742
+ } else {
743
+ // 进行中的任务:启动定时器
744
+ _ccTasks[idx].timer = setInterval(function () {
745
+ var s = Math.floor((Date.now() - _ccTasks[idx].startTime) / 1000);
746
+ var el = document.getElementById('cc-' + idx + '-elapsed');
747
+ if (el) el.textContent = s < 60 ? s + 's' : Math.floor(s / 60) + 'm ' + (s % 60) + 's';
748
+ }, 1000);
749
+ }
750
+ }
751
+
752
+ /* ── 卡片折叠(展开时懒加载结果文件)───────────────────────────────── */
753
+ function toggleCCCard(idx) {
754
+ var card = document.getElementById('cc-card-' + idx);
755
+ var body = document.getElementById('cc-' + idx + '-body');
756
+ if (!card || !body) return;
757
+ var isOpen = card.classList.toggle('open');
758
+ body.style.display = isOpen ? '' : 'none';
759
+ // 展开时才解析结果文件路径(终态懒加载,列表渲染零额外请求)
760
+ if (isOpen) {
761
+ var task = _ccTasks[idx];
762
+ if (task && !task.filesResolved) {
763
+ _loadCCResultFiles(idx);
764
+ }
765
+ }
766
+ }
767
+
768
+ /* ── SSE 连接(监听 Orchestrator 事件)───────────────────────────────── */
769
+ function _connectCCSSE(idx) {
770
+ var task = _ccTasks[idx];
771
+ if (!task) return;
772
+
773
+ if (task.sse && (task.sse.readyState === EventSource.OPEN || task.sse.readyState === EventSource.CONNECTING)) {
774
+ return;
775
+ }
776
+ if (task.sse) { var old = task.sse; task.sse = null; old.close(); }
777
+
778
+ var sse = new EventSource('/stream/tasks/' + encodeURIComponent(task.taskId));
779
+ task.sse = sse;
780
+ var sseDone = false;
781
+
782
+ // 编排步骤状态变更
783
+ sse.addEventListener('orch:step:status', function (e) {
784
+ var data; try { data = JSON.parse(e.data); } catch (err) { data = {}; }
785
+ if (data.status) {
786
+ _updateCCStage(idx, data.status, {
787
+ startedAt: data.startTime,
788
+ finishedAt: data.endTime,
789
+ resultPath: data.resultPath,
790
+ });
791
+ }
792
+ // 结构化指标随步骤状态事件推送(markFinished 后)
793
+ if (data.metrics && data.metrics.mergeLevel) {
794
+ _updateMergeLevelBadge(idx, String(data.metrics.mergeLevel).toUpperCase(), null);
795
+ }
796
+ });
797
+
798
+ // 编排整体进度
799
+ sse.addEventListener('orch:task:progress', function (e) {
800
+ // 仅用于状态同步,不显示日志
801
+ });
802
+
803
+ // 编排启动
804
+ sse.addEventListener('orch:task:start', function (e) {
805
+ _updateCCStage(idx, 'running', {});
806
+ });
807
+
808
+ // 编排完成
809
+ sse.addEventListener('orch:task:complete', function (e) {
810
+ sseDone = true;
811
+ var data; try { data = JSON.parse(e.data); } catch (err) { data = {}; }
812
+ if (data.status) {
813
+ _updateCCStage(idx, data.status, {});
814
+ } else {
815
+ _updateCCStage(idx, 'completed', {});
816
+ }
817
+ _stopProgressPoll(idx);
818
+ _finishCCSSE(idx);
819
+ _loadCCResultFiles(idx);
820
+ // 最后环节:清理源码(仅删 taskDir/source/,报告与过程文件保留)
821
+ _ccCleanSource(idx);
822
+ });
823
+
824
+ // 编排错误
825
+ sse.addEventListener('orch:task:error', function (e) {
826
+ _updateCCStage(idx, 'failed', {});
827
+ });
828
+
829
+ // 编排终止
830
+ sse.addEventListener('orch:task:aborted', function (e) {
831
+ sseDone = true;
832
+ _updateCCStage(idx, 'aborted', {});
833
+ _stopProgressPoll(idx);
834
+ _finishCCSSE(idx);
835
+ // 终止场景:静默清理源码(已有"已取消"提示,避免 toast 叠加)
836
+ _ccCleanSource(idx, true);
837
+ });
838
+
839
+ // 连接建立
840
+ sse.addEventListener('connected', function (e) {
841
+ // 仅维持连接,不显示日志
842
+ });
843
+
844
+ sse.onerror = function () {
845
+ if (sseDone) { sse.close(); return; }
846
+ };
847
+ }
848
+
849
+ /* ── 阶段 UI 更新 ────────────────────────────────────────────────────── */
850
+ function _updateCCStage(idx, stage, extra) {
851
+ extra = extra || {};
852
+ var task = _ccTasks[idx];
853
+ if (task) {
854
+ if (extra.startedAt) task.startedAt = extra.startedAt;
855
+ if (extra.finishedAt) task.finishedAt = extra.finishedAt;
856
+ if (extra.resultPath) task.resultPath = extra.resultPath;
857
+ }
858
+
859
+ var badge = document.getElementById('cc-' + idx + '-badge');
860
+ if (badge) {
861
+ badge.className = 'cc-stage-badge s-' + stage;
862
+ badge.textContent = _CC_STAGE_LABELS[stage] || stage;
863
+ }
864
+
865
+ var stageTxt = document.getElementById('cc-' + idx + '-stage-txt');
866
+ if (stageTxt) stageTxt.textContent = _CC_STAGE_LABELS[stage] || stage;
867
+
868
+ var icon = document.getElementById('cc-' + idx + '-icon');
869
+ if (icon) {
870
+ if (stage === 'completed') {
871
+ icon.style.borderColor = 'var(--green)';
872
+ icon.style.background = 'rgba(52,199,89,0.1)';
873
+ icon.textContent = '✅';
874
+ } else if (stage === 'failed' || stage === 'error' || stage === 'deadlocked') {
875
+ icon.style.borderColor = 'var(--red)';
876
+ icon.style.background = 'rgba(255,59,48,0.1)';
877
+ icon.textContent = '❌';
878
+ } else if (stage === 'aborted') {
879
+ icon.style.borderColor = 'var(--gray)';
880
+ icon.style.background = 'rgba(142,142,147,0.1)';
881
+ icon.textContent = '⏹';
882
+ } else if (stage === 'running') {
883
+ icon.style.borderColor = 'var(--blue)';
884
+ icon.style.background = 'rgba(0,113,227,0.1)';
885
+ icon.textContent = '🔍';
886
+ }
887
+ }
888
+
889
+ // 完成时显示下载按钮
890
+ if (stage === 'completed') {
891
+ var dlWrap = document.getElementById('cc-' + idx + '-dl-wrap');
892
+ if (dlWrap && task && task.resultPath) {
893
+ dlWrap.style.display = 'flex';
894
+ }
895
+ }
896
+ }
897
+
898
+ /* ── 合入建议分级徽章 ────────────────────────────────────────────────── */
899
+ function _updateMergeLevelBadge(idx, mergeLevel, resultUrl) {
900
+ var mergeBadge = document.getElementById('cc-' + idx + '-merge-badge');
901
+ if (!mergeBadge) return;
902
+
903
+ var levelConfig = {
904
+ PASS: { label: '建议合入', color: 'var(--green)', icon: '✅', bg: 'rgba(52,199,89,0.15)' },
905
+ CONDITIONAL: { label: '条件合入', color: 'var(--orange)', icon: '⚠️', bg: 'rgba(255,149,0,0.15)' },
906
+ NEED_FIX: { label: '修改后合入', color: 'var(--orange)', icon: '🔧', bg: 'rgba(255,149,0,0.15)' },
907
+ REJECT: { label: '不允许合入', color: 'var(--red)', icon: '❌', bg: 'rgba(255,59,48,0.15)' },
908
+ };
909
+
910
+ var config = levelConfig[mergeLevel] || { label: mergeLevel, color: 'var(--text3)', icon: '❓', bg: 'rgba(142,142,147,0.15)' };
911
+
912
+ mergeBadge.style.display = 'inline-flex';
913
+ mergeBadge.style.background = config.bg;
914
+ mergeBadge.style.color = config.color;
915
+ mergeBadge.innerHTML = config.icon + ' ' + _ccEsc(config.label);
916
+ }
917
+
918
+ /* ── 完成 SSE 连接后的收尾 ───────────────────────────────────────────── */
919
+ function _finishCCSSE(idx) {
920
+ var task = _ccTasks[idx];
921
+ if (!task) return;
922
+ if (task.sse) { try { task.sse.close(); } catch (e) {} task.sse = null; }
923
+ if (task.timer) { clearInterval(task.timer); task.timer = null; }
924
+
925
+ var timeEl = document.getElementById('cc-' + idx + '-time');
926
+ if (timeEl && task.startedAt) timeEl.textContent = _ccFormatTime(task.startedAt);
927
+
928
+ var elapsedEl = document.getElementById('cc-' + idx + '-elapsed');
929
+ if (elapsedEl) {
930
+ if (task.startedAt && task.finishedAt) {
931
+ var durationMs = new Date(task.finishedAt).getTime() - new Date(task.startedAt).getTime();
932
+ var durationSec = Math.floor(durationMs / 1000);
933
+ elapsedEl.textContent = durationSec < 60 ? durationSec + 's' : Math.floor(durationSec / 60) + 'm ' + (durationSec % 60) + 's';
934
+ } else if (task.startedAt) {
935
+ var ms = Date.now() - new Date(task.startedAt).getTime();
936
+ var sec = Math.floor(ms / 1000);
937
+ elapsedEl.textContent = sec < 60 ? sec + 's' : Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
938
+ }
939
+ elapsedEl.style.display = 'inline';
940
+ }
941
+
942
+ var card = document.getElementById('cc-card-' + idx);
943
+ if (card) {
944
+ card.classList.remove('running');
945
+ var badge = document.getElementById('cc-' + idx + '-badge');
946
+ if (badge) {
947
+ if (badge.classList.contains('s-completed')) card.classList.add('cc-done');
948
+ else if (badge.classList.contains('s-failed') || badge.classList.contains('s-error')) card.classList.add('cc-failed');
949
+ else if (badge.classList.contains('s-aborted')) card.classList.add('cc-cancelled');
950
+ }
951
+ var stopBtn = document.getElementById('cc-' + idx + '-stop');
952
+ if (stopBtn) stopBtn.style.display = 'none';
953
+ }
954
+ }
955
+
956
+ /* ── 停止检视 ────────────────────────────────────────────────────────── */
957
+ async function stopCCTask(idx) {
958
+ var task = _ccTasks[idx];
959
+ if (!task) return;
960
+ var btn = document.getElementById('cc-' + idx + '-stop');
961
+ if (btn) { btn.disabled = true; btn.textContent = '取消中…'; }
962
+
963
+ try {
964
+ var r = await apiFetch('POST', '/api/orchestrator/abort', { taskId: task.taskId });
965
+ if (r.status >= 400) {
966
+ toast('error', '取消失败:' + ((r.data && r.data.error) || r.status));
967
+ if (btn) { btn.disabled = false; btn.textContent = '■ 取消'; }
968
+ return;
969
+ }
970
+ } catch (e) {
971
+ toast('error', '取消请求异常:' + e.message);
972
+ if (btn) { btn.disabled = false; btn.textContent = '■ 取消'; }
973
+ return;
974
+ }
975
+
976
+ toast('warn', '检视任务已取消');
977
+ _updateCCStage(idx, 'aborted', {});
978
+ _stopProgressPoll(idx);
979
+ _finishCCSSE(idx);
980
+ // 终止场景:静默清理源码(SSE aborted 监听会幂等兜底)
981
+ _ccCleanSource(idx, true);
982
+ }
983
+
984
+ /* ── 报告下载 ────────────────────────────────────────────────────────── */
985
+ async function downloadCCReport(idx) {
986
+ var task = _ccTasks[idx];
987
+ if (!task) return;
988
+ // 懒加载兑底:未解析过文件路径时先解析
989
+ if (!task.resultPath) await _loadCCResultFiles(idx);
990
+ if (!task.resultPath) { toast('warn', '检视报告尚未生成'); return; }
991
+
992
+ try {
993
+ var r = await fetch('/api/flexrunner/file?path=' + encodeURIComponent(task.resultPath));
994
+ if (!r.ok) { toast('error', '下载失败: HTTP ' + r.status); return; }
995
+ var blob = await r.blob();
996
+ var url = URL.createObjectURL(blob);
997
+ var a = document.createElement('a');
998
+ a.href = url;
999
+ a.download = 'check-report.md';
1000
+ a.click();
1001
+ URL.revokeObjectURL(url);
1002
+ } catch (e) {
1003
+ toast('error', '下载失败: ' + e.message);
1004
+ }
1005
+ }
1006
+
1007
+ /* ── 报告预览 ────────────────────────────────────────────────────────── */
1008
+ async function viewCCReport(idx) {
1009
+ var task = _ccTasks[idx];
1010
+ if (!task) return;
1011
+ // 懒加载兑底:未解析过文件路径时先解析
1012
+ if (!task.resultPath) await _loadCCResultFiles(idx);
1013
+ if (!task.resultPath) { toast('warn', '检视报告尚未生成'); return; }
1014
+
1015
+ try {
1016
+ var r = await apiFetch('GET', '/api/flexrunner/file?path=' + encodeURIComponent(task.resultPath));
1017
+ if (!r.data || r.data.status !== 'ok') { toast('error', '读取报告失败'); return; }
1018
+
1019
+ var title = '检视报告 - ' + task.taskId;
1020
+ if (typeof MarkdownReader !== 'undefined' && MarkdownReader.open) {
1021
+ // 使用 Markdown 阅读器渲染检视报告(支持表格/代码块/分级标题等)
1022
+ MarkdownReader.open(title, r.data.content);
1023
+ } else if (typeof showModal === 'function') {
1024
+ showModal(title, r.data.content);
1025
+ } else {
1026
+ var w = window.open('', '_blank');
1027
+ if (w) {
1028
+ w.document.write('<pre style="white-space:pre-wrap;font-family:monospace;font-size:13px;padding:20px;">' +
1029
+ _ccEsc(r.data.content) + '</pre>');
1030
+ w.document.title = title;
1031
+ }
1032
+ }
1033
+ } catch (e) {
1034
+ toast('error', '预览失败: ' + e.message);
1035
+ }
1036
+ }
1037
+
1038
+ /* ── 查看对话历史 ────────────────────────────────────────────────────── */
1039
+ async function viewCCSession(idx) {
1040
+ var task = _ccTasks[idx];
1041
+ if (!task) return;
1042
+
1043
+ var btn = document.getElementById('cc-' + idx + '-session-btn');
1044
+ if (btn) { btn.disabled = true; btn.textContent = '加载中…'; }
1045
+
1046
+ try {
1047
+ var url = '/api/flexrunner/session?taskId=' + encodeURIComponent(task.taskId) + '&stepId=1';
1048
+ var r = await apiFetch('GET', url, null, 15000);
1049
+ if (btn) { btn.disabled = false; btn.textContent = '💬 查看对话'; }
1050
+
1051
+ if (!r.data || r.data.status !== 'ok') {
1052
+ toast('error', '获取对话失败');
1053
+ return;
1054
+ }
1055
+
1056
+ var messages = r.data.messages || [];
1057
+ if (messages.length === 0) {
1058
+ toast('info', '暂无对话消息');
1059
+ return;
1060
+ }
1061
+
1062
+ var title = '对话记录 - ' + task.taskId;
1063
+ if (typeof showSessionModal === 'function') {
1064
+ showSessionModal(title, messages);
1065
+ } else {
1066
+ var w = window.open('', '_blank');
1067
+ if (w) {
1068
+ var html = '<style>body{font-family:monospace;font-size:13px;padding:20px;max-width:900px;margin:0 auto}</style>';
1069
+ messages.forEach(function (m) {
1070
+ var role = m.role || (m.info && m.info.role) || 'unknown';
1071
+ var content = m.content != null ? m.content : ((m.parts && m.parts[0] && m.parts[0].text) || '');
1072
+ if (!content || content.trim() === '') return;
1073
+ html += '<div style="margin-bottom:16px;border-left:3px solid #ccc;padding-left:12px"><b style="color:';
1074
+ html += role === 'user' ? '#0066cc' : role === 'assistant' ? '#00772e' : '#767676';
1075
+ html += '">' + _ccEsc(role) + '</b><pre style="white-space:pre-wrap;margin-top:4px">' + _ccEsc(content) + '</pre></div>';
1076
+ });
1077
+ w.document.write(html);
1078
+ w.document.title = title;
1079
+ }
1080
+ }
1081
+ } catch (e) {
1082
+ if (btn) { btn.disabled = false; btn.textContent = '💬 查看对话'; }
1083
+ toast('error', '获取对话失败: ' + e.message);
1084
+ }
1085
+ }
1086
+
1087
+ /* ── 错误提示 ────────────────────────────────────────────────────────── */
1088
+ function _showCCErr(msg) {
1089
+ var el = $('cc-error');
1090
+ if (!el) return;
1091
+ el.style.display = 'block';
1092
+ el.textContent = '错误:' + msg;
1093
+ }
1094
+
1095
+ /* ── 注册面板初始化 ──────────────────────────────────────────────────── */
1096
+ if (typeof Nav !== 'undefined' && Nav.register) {
1097
+ Nav.register('p-codecheck', initCodeCheckPanel);
1098
+ }