@ohos-cpf/3rdloop 0.0.4 → 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.
- package/README.md +119 -128
- package/lib/cli.js +25 -0
- package/lib/serve.js +268 -0
- package/lib/update.js +46 -6
- package/lib/web-ext.js +454 -0
- package/lib/web.js +664 -0
- package/package.json +2 -1
- package/vendor/Server/Skills/arkts-code-use/SKILL.md +270 -0
- package/vendor/Server/Skills/arkts-code-use/assets/TEMPLATES.md +367 -0
- package/vendor/Server/Skills/arkts-code-use/references/API_VERIFICATION.md +144 -0
- package/vendor/Server/Skills/arkts-code-use/references/ARKTS_RULES.md +240 -0
- package/vendor/Server/Skills/arkts-code-use/references/CODE_PATTERNS.md +431 -0
- package/vendor/Server/Skills/arkts-code-use/references/SYNTAX_CHECK_GUIDE.md +164 -0
- package/vendor/Server/Skills/arkts-code-use/scripts/verify-arkts.cjs +428 -0
- package/vendor/Server/Skills/gitcode-repo-fork/SKILL.md +310 -0
- package/vendor/Server/Skills/gitcode-repo-fork/assets/FORK_REPORT_TEMPLATE.md +113 -0
- package/vendor/Server/Skills/gitcode-repo-fork/references/FORK_DECISION_GUIDE.md +124 -0
- package/vendor/Server/Skills/gitcode-repo-fork/references/GITCODE_FORK_API.md +95 -0
- package/vendor/Server/Skills/gitcode-repo-fork/scripts/gitcode-fork.cjs +285 -0
- package/vendor/VERSION +3 -3
- package/web/css/arktslibrarycheck.css +322 -0
- package/web/css/codecheck.css +464 -0
- package/web/css/flutterlibrarycheck.css +322 -0
- package/web/css/knowledge.css +332 -0
- package/web/css/loop.css +578 -0
- package/web/css/md-reader.css +240 -0
- package/web/css/rnlibrarycheck.css +322 -0
- package/web/css/theme.css +702 -0
- package/web/index.html +713 -0
- package/web/js/arktslibrarycheck.js +1413 -0
- package/web/js/codecheck.js +1039 -0
- package/web/js/flutterlibrarycheck.js +1364 -0
- package/web/js/health.js +69 -0
- package/web/js/knowledge.js +358 -0
- package/web/js/loop.js +1102 -0
- package/web/js/md-reader.js +435 -0
- package/web/js/navigation.js +238 -0
- package/web/js/rnlibrarycheck.js +1378 -0
- package/web/js/stats.js +110 -0
- package/web/js/theme.js +46 -0
- package/web/js/utils.js +228 -0
- package/web/knowledge.html +146 -0
- package/web/loop.html +219 -0
|
@@ -0,0 +1,1378 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* rnlibrarycheck.js — RN 仓库检视模块(前端编排)
|
|
3
|
+
*
|
|
4
|
+
* 设计理念(与 flutterlibrarycheck.js 一致):后端不感知定制化,前端直接调用
|
|
5
|
+
* 通用 Orchestrator API 完成固定编排。
|
|
6
|
+
*
|
|
7
|
+
* 编排流程(参考 AIweb RNLibraryCheck:接口分析先行 + 检视维度并发):
|
|
8
|
+
* 步骤 1 rn-library-interface-analyzer — 双仓库克隆 + 接口规格分析
|
|
9
|
+
* 步骤 2~7 stability / consistency / memoryLeak / — 6 个检视维度并发执行
|
|
10
|
+
* documentation / testCoverage / (dependId=1 fan-out)
|
|
11
|
+
* knownIssue(独立扣分项)
|
|
12
|
+
*
|
|
13
|
+
* 每个检视 SKILL 输出 0-100 评分:
|
|
14
|
+
* - 首选:SKILL 按约定写 meta .json(stepId{X}_loop{L}_meta_{N}.json),
|
|
15
|
+
* 后端收割为 step.metrics(如 { stabilityScore: 85 }),随 progress / SSE 推送,
|
|
16
|
+
* 终态后合并入注册表 metadata(历史列表直接回填,参考 prcheck 的 mergeLevel 机制)
|
|
17
|
+
* - 兜底:读取检视报告文本,正则解析 `**综合评分:XX/100**`
|
|
18
|
+
*
|
|
19
|
+
* RN 与 Flutter 的关键差异:
|
|
20
|
+
* - 双仓库入参:libraryName + ohosRepoUrl(鸿蒙化)+ upstreamRepoUrl(上游原库),
|
|
21
|
+
* 可选 rndocUrl(RN 使用文档)/ rnRntUrl(RNT 测试 demo 地址)
|
|
22
|
+
* - 源码目录:{outputDir}/source/ohos/(鸿蒙化)+ {outputDir}/source/RN/(上游原库)
|
|
23
|
+
* - 无编码质量(codeQuality)维度,本地 6 维检视(knownIssue 为独立扣分项,不参与加权)
|
|
24
|
+
* - 权重与 Flutter 一致:稳定性 20% / 一致性 20% / 内存泄露 20% / 文档质量 15% / 用例覆盖 25%;
|
|
25
|
+
* 已知问题命中一条总分扣 30 分(独立扣分项)
|
|
26
|
+
*
|
|
27
|
+
* 前端编排时序:
|
|
28
|
+
* 1. 构造 7 步 taskDef(specifiedSkill 固定指定 SKILL,跳过自动匹配)
|
|
29
|
+
* 2. POST /api/orchestrator/load — 加载任务定义
|
|
30
|
+
* 3. POST /api/orchestrator/start — 启动编排(步骤 2~6 依赖步骤 1 自动并发)
|
|
31
|
+
* 4. GET /stream/tasks/:taskId — SSE 实时监控(orch:step:status 含 metrics)
|
|
32
|
+
* 5. GET /api/orchestrator/progress?taskId= — 轮询进度(SSE 补充)
|
|
33
|
+
* 6. GET /api/flexrunner/steps?taskId= — 解析报告文件(懒加载)
|
|
34
|
+
* 7. GET /api/flexrunner/file?path= — 读取报告 / 评分兜底解析
|
|
35
|
+
* 8. POST /api/orchestrator/abort — 取消任务
|
|
36
|
+
*
|
|
37
|
+
* 依赖:utils.js(apiFetch / createPagination / escapeHtml / toast / $)
|
|
38
|
+
* codecheck.css(cc-* 通用卡片样式)+ rnlibrarycheck.css(rn-* 扩展样式)
|
|
39
|
+
* md-reader.js(MarkdownReader)、loop.js(showSessionModal)
|
|
40
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
41
|
+
'use strict';
|
|
42
|
+
|
|
43
|
+
/* ── 编排配置:7 个 SKILL 步骤(stepId 固定,历史恢复依赖此顺序)───────── */
|
|
44
|
+
var _RN_STEP_DEFS = [
|
|
45
|
+
{ stepId: 1, key: 'interface', skill: 'rn-library-interface-analyzer', num: '①', name: '接口分析', desc: '双仓库克隆 + 上游 API/TurboModule 接口规格', report: '接口规格说明.md', scoreKey: null },
|
|
46
|
+
{ stepId: 2, key: 'stability', skill: 'rn-library-stability-check', num: '②', name: '稳定性检视', desc: '异常处理、资源释放、并发安全', report: '稳定性检视报告.md', scoreKey: 'stabilityScore' },
|
|
47
|
+
{ stepId: 3, key: 'consistency', skill: 'rn-library-consistency-check', num: '③', name: '一致性检视', desc: '函数级实现一致性、TurboModule 桥接', report: '一致性检视报告.md', scoreKey: 'consistencyScore' },
|
|
48
|
+
{ stepId: 4, key: 'memoryleak', skill: 'rn-library-memory-leak-check', num: '④', name: '内存泄露检视', desc: 'NAPI 引用、JSI 绑定、事件监听', report: '内存泄露检视报告.md', scoreKey: 'memoryLeakScore' },
|
|
49
|
+
{ stepId: 5, key: 'documentation', skill: 'rn-library-documentation-quality-check', num: '⑤', name: '文档质量检视', desc: 'API 注释、适配说明、示例质量', report: '文档质量检视报告.md', scoreKey: 'documentationScore' },
|
|
50
|
+
{ stepId: 6, key: 'testcoverage', skill: 'rn-library-test-coverage-check', num: '⑥', name: '用例覆盖度检视', desc: '测试覆盖率、Demo 覆盖率', report: '用例覆盖度检视报告.md', scoreKey: 'testCoverageScore' },
|
|
51
|
+
{ stepId: 7, key: 'knownissue', skill: 'rn-library-known-issue-check', num: '⑦', name: '已知问题检视', desc: '历史案例规则回归,独立扣分项(命中一条扣 30 分)', report: '已知问题检视报告.md', scoreKey: 'knownIssueScore' },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/* 评分权重(与 AIweb RNLibraryCheck 综合评分口径一致,权重和 = 1;knownissue 为独立扣分项不参与加权) */
|
|
55
|
+
var _RN_WEIGHTS = { stability: 0.20, consistency: 0.20, memoryleak: 0.20, documentation: 0.15, testcoverage: 0.25 };
|
|
56
|
+
|
|
57
|
+
/* 用例覆盖度检视的附属脚本报告 */
|
|
58
|
+
var _RN_EXTRA_REPORTS = {
|
|
59
|
+
testcoverage: [
|
|
60
|
+
{ key: 'testcoverageRaw', report: 'test-coverage-report.md', label: '测试覆盖率' },
|
|
61
|
+
{ key: 'democoverage', report: 'demo-coverage-report.md', label: 'Demo覆盖率' },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/* ── 状态 ────────────────────────────────────────────────────────────── */
|
|
66
|
+
var _rnCounter = 0;
|
|
67
|
+
var _rnTasks = {}; // idx -> { taskId, libraryName, ohosRepoUrl, upstreamRepoUrl, rndocUrl, rnRntUrl,
|
|
68
|
+
// sse, timer, pollTimer, startTime, startedAt, finishedAt,
|
|
69
|
+
// scores: { stability, consistency, memoryleak, documentation, testcoverage, overall },
|
|
70
|
+
// stepStatus: { interface: 'pending', ... },
|
|
71
|
+
// reportPaths: { interface: '/abs/接口规格说明.md', ... }, filesResolved }
|
|
72
|
+
var _rnPager = null;
|
|
73
|
+
var _rnInited = false;
|
|
74
|
+
|
|
75
|
+
/* ── 阶段标签(映射 Orchestrator 状态)──────────────────────────────── */
|
|
76
|
+
var _RN_STAGE_LABELS = {
|
|
77
|
+
pending: '等待中',
|
|
78
|
+
ready: '就绪',
|
|
79
|
+
running: '检视中…',
|
|
80
|
+
completed: '检视完成 ✓',
|
|
81
|
+
failed: '检视失败',
|
|
82
|
+
aborted: '已停止',
|
|
83
|
+
error: '错误',
|
|
84
|
+
idle: '空闲',
|
|
85
|
+
loaded: '已加载',
|
|
86
|
+
deadlocked: '死锁',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
var _RN_STEP_STATUS_LABELS = {
|
|
90
|
+
pending: '等待中…',
|
|
91
|
+
ready: '就绪…',
|
|
92
|
+
running: '正在检视…',
|
|
93
|
+
completed: '已完成 ✓',
|
|
94
|
+
failed: '失败 ✗',
|
|
95
|
+
skipped: '已跳过',
|
|
96
|
+
blocked: '已阻塞',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
100
|
+
* 工具函数
|
|
101
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
102
|
+
|
|
103
|
+
function _rnEsc(s) {
|
|
104
|
+
if (typeof escapeHtml === 'function') return escapeHtml(s);
|
|
105
|
+
if (s == null) return '';
|
|
106
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _rnFormatTime(isoString) {
|
|
110
|
+
if (!isoString) return '';
|
|
111
|
+
try {
|
|
112
|
+
return new Date(isoString).toLocaleString('zh-CN', {
|
|
113
|
+
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
114
|
+
});
|
|
115
|
+
} catch (e) { return isoString; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** taskId(rncheck_{libraryName}_{ts36})→ 库名 */
|
|
119
|
+
function _rnTaskIdToLibraryName(taskId) {
|
|
120
|
+
if (!taskId) return '—';
|
|
121
|
+
var rest = taskId.replace(/^rncheck_/, '');
|
|
122
|
+
var parts = rest.split('_');
|
|
123
|
+
if (parts.length >= 2) {
|
|
124
|
+
parts.pop(); // 去掉末尾时间戳段
|
|
125
|
+
return parts.join('_');
|
|
126
|
+
}
|
|
127
|
+
return rest.length > 40 ? rest.substring(0, 40) + '…' : rest;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 从报告文本解析 0-100 评分(兜底,同 AIweb _parseRNLibraryScore 正则)。
|
|
132
|
+
*/
|
|
133
|
+
function _rnParseScoreFromReport(content) {
|
|
134
|
+
if (!content) return null;
|
|
135
|
+
var m = content.match(/\*?\*?综合评分[::\s]*(\d{1,3})\s*\/\s*100/i);
|
|
136
|
+
if (!m) m = content.match(/(?:【总体评分】|\*?\*?评分)[::\s]*(\d{1,3})\s*\/\s*100/i);
|
|
137
|
+
if (!m) m = content.match(/得分[::\s]*(\d{1,3})/i);
|
|
138
|
+
if (!m) return null;
|
|
139
|
+
var v = parseInt(m[1], 10);
|
|
140
|
+
if (!isFinite(v)) return null;
|
|
141
|
+
return Math.max(0, Math.min(100, v));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 从步骤 metrics 提取评分。优先维度专用键(历史列表合并无同名冲突),
|
|
146
|
+
* 兼容通用 score 键。
|
|
147
|
+
*/
|
|
148
|
+
function _rnExtractStepScore(stepDef, metrics) {
|
|
149
|
+
if (!stepDef.scoreKey || !metrics || typeof metrics !== 'object') return null;
|
|
150
|
+
var v = metrics[stepDef.scoreKey];
|
|
151
|
+
if (v == null) v = metrics.score;
|
|
152
|
+
v = Number(v);
|
|
153
|
+
if (!isFinite(v)) return null;
|
|
154
|
+
return Math.max(0, Math.min(100, Math.round(v)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 综合评分:5 个维度评分齐全时按权重计算(口径同 AIweb),否则返回 null。
|
|
159
|
+
*/
|
|
160
|
+
function _rnComputeOverall(scores) {
|
|
161
|
+
var dims = ['stability', 'consistency', 'memoryleak', 'documentation', 'testcoverage'];
|
|
162
|
+
for (var i = 0; i < dims.length; i++) {
|
|
163
|
+
if (scores[dims[i]] == null) return null;
|
|
164
|
+
}
|
|
165
|
+
return Math.round(
|
|
166
|
+
scores.stability * _RN_WEIGHTS.stability +
|
|
167
|
+
scores.consistency * _RN_WEIGHTS.consistency +
|
|
168
|
+
scores.memoryleak * _RN_WEIGHTS.memoryleak +
|
|
169
|
+
scores.documentation * _RN_WEIGHTS.documentation +
|
|
170
|
+
scores.testcoverage * _RN_WEIGHTS.testcoverage
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _rnGuardSubmit(btn, loadingText, normalText) {
|
|
175
|
+
if (!btn || btn.disabled) return false;
|
|
176
|
+
btn.disabled = true;
|
|
177
|
+
btn.textContent = loadingText;
|
|
178
|
+
btn._rnNormalText = normalText;
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _rnGuardSubmitRestore(btn) {
|
|
183
|
+
if (!btn) return;
|
|
184
|
+
var normalText = btn._rnNormalText || '提交';
|
|
185
|
+
setTimeout(function () {
|
|
186
|
+
if (btn.disabled) { btn.disabled = false; btn.textContent = normalText; }
|
|
187
|
+
}, 5000);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function _rnGuardSubmitReset(btn) {
|
|
191
|
+
if (!btn) return;
|
|
192
|
+
btn.disabled = false;
|
|
193
|
+
btn.textContent = btn._rnNormalText || '提交';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _rnShowErr(msg) {
|
|
197
|
+
var el = $('rn-error');
|
|
198
|
+
if (!el) return;
|
|
199
|
+
el.style.display = 'block';
|
|
200
|
+
el.textContent = '错误:' + msg;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
204
|
+
* 面板初始化(懒加载,由 Nav.register 触发)
|
|
205
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
206
|
+
|
|
207
|
+
function initRNLibraryCheckPanel() {
|
|
208
|
+
if (_rnInited) return;
|
|
209
|
+
_rnInited = true;
|
|
210
|
+
|
|
211
|
+
// 历史任务
|
|
212
|
+
_rnInitHistoryPager().then(function (pg) { if (pg) pg.load(1); });
|
|
213
|
+
|
|
214
|
+
// 提交按钮
|
|
215
|
+
var btn = $('rn-submit-btn');
|
|
216
|
+
if (btn) btn.addEventListener('click', doRNLibraryCheck);
|
|
217
|
+
|
|
218
|
+
// 回车提交(三个必填输入框)
|
|
219
|
+
['rn-library-name', 'rn-ohos-repo-url', 'rn-upstream-repo-url'].forEach(function (id) {
|
|
220
|
+
var input = $(id);
|
|
221
|
+
if (input) {
|
|
222
|
+
input.addEventListener('keydown', function (e) {
|
|
223
|
+
if (e.key === 'Enter') doRNLibraryCheck();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
230
|
+
* 提交检视(前端编排:构造 taskDef → load → start)
|
|
231
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
232
|
+
|
|
233
|
+
async function doRNLibraryCheck() {
|
|
234
|
+
var libraryName = $('rn-library-name').value.trim();
|
|
235
|
+
var ohosRepoUrl = $('rn-ohos-repo-url').value.trim();
|
|
236
|
+
var upstreamRepoUrl = $('rn-upstream-repo-url').value.trim();
|
|
237
|
+
var rndocUrl = $('rn-rndoc-url') ? $('rn-rndoc-url').value.trim() : '';
|
|
238
|
+
var rnRntUrl = $('rn-rnt-url') ? $('rn-rnt-url').value.trim() : '';
|
|
239
|
+
var skillDir = '';
|
|
240
|
+
|
|
241
|
+
if (!libraryName) { _rnShowErr('库名不能为空'); return; }
|
|
242
|
+
if (!ohosRepoUrl) { _rnShowErr('鸿蒙化仓库地址不能为空'); return; }
|
|
243
|
+
if (!upstreamRepoUrl) { _rnShowErr('上游原库地址不能为空'); return; }
|
|
244
|
+
|
|
245
|
+
var btn = $('rn-submit-btn');
|
|
246
|
+
if (!_rnGuardSubmit(btn, '提交中…', '▶ 开始检视')) return;
|
|
247
|
+
$('rn-error').style.display = 'none';
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// ── 1. 生成 taskId ──
|
|
251
|
+
var safeName = libraryName.replace(/[^a-zA-Z0-9._-]/g, '_') || 'repo';
|
|
252
|
+
var ts = Date.now().toString(36);
|
|
253
|
+
var taskId = 'rncheck_' + safeName + '_' + ts;
|
|
254
|
+
|
|
255
|
+
// ── 2. 构造 7 步 taskDef ──
|
|
256
|
+
// 步骤 1:接口分析(双仓库克隆 + 接口规格说明.md)
|
|
257
|
+
// 步骤 2~7:6 个检视维度,dependId=1 fan-out 并发(编排器自动并行执行)
|
|
258
|
+
// specifiedSkill 固定指定 SKILL,跳过 SkillSelector 自动匹配
|
|
259
|
+
// body.title 编码双仓库信息,供历史恢复解析(格式: RN仓库检视|{libraryName}|{ohosRepoUrl}|{upstreamRepoUrl})
|
|
260
|
+
var interfaceDesc = [
|
|
261
|
+
'请使用 rn-library-interface-analyzer SKILL 对 RN 三方库「' + libraryName + '」进行接口分析。',
|
|
262
|
+
'',
|
|
263
|
+
'任务参数:',
|
|
264
|
+
'- libraryName: ' + libraryName,
|
|
265
|
+
'- ohosRepoUrl(鸿蒙化仓库): ' + ohosRepoUrl,
|
|
266
|
+
'- upstreamRepoUrl(上游原库): ' + upstreamRepoUrl,
|
|
267
|
+
'- outputDir: 即当前工作目录(见下方"工作目录",绝对路径),所有输出文件放在该目录根下',
|
|
268
|
+
rndocUrl ? '- rndocUrl(RN 使用文档): ' + rndocUrl : '- rndocUrl: 未提供(使用鸿蒙化仓库根目录 README.md)',
|
|
269
|
+
].filter(Boolean).join('\n');
|
|
270
|
+
|
|
271
|
+
var steps = [{
|
|
272
|
+
stepId: 1,
|
|
273
|
+
taskDescription: interfaceDesc,
|
|
274
|
+
dependId: 0,
|
|
275
|
+
nextStepId: 0,
|
|
276
|
+
failStepId: 0,
|
|
277
|
+
maxRetries: 3,
|
|
278
|
+
skillDirectory: skillDir || '',
|
|
279
|
+
specifiedSkill: 'rn-library-interface-analyzer',
|
|
280
|
+
inputReport: '',
|
|
281
|
+
}];
|
|
282
|
+
|
|
283
|
+
// 6 个检视步骤(并发):依赖步骤 1 已克隆的 source/ohos/、source/RN/ 和 接口规格说明.md
|
|
284
|
+
for (var i = 1; i < _RN_STEP_DEFS.length; i++) {
|
|
285
|
+
var def = _RN_STEP_DEFS[i];
|
|
286
|
+
var desc = [
|
|
287
|
+
'请使用 ' + def.skill + ' SKILL 对 RN 三方库「' + libraryName + '」进行' + def.name + ',生成检视报告。',
|
|
288
|
+
'',
|
|
289
|
+
'任务参数:',
|
|
290
|
+
'- libraryName: ' + libraryName,
|
|
291
|
+
'- repoUrl(鸿蒙化仓库): ' + ohosRepoUrl,
|
|
292
|
+
'- upstreamRepoUrl(上游原库): ' + upstreamRepoUrl,
|
|
293
|
+
'- repoName: ' + libraryName,
|
|
294
|
+
'- outputDir: 即当前工作目录(见下方"工作目录",绝对路径)',
|
|
295
|
+
'- interfaceDocPath: {outputDir}/接口规格说明.md(前置接口分析步骤已生成)',
|
|
296
|
+
'- 两个仓库源码已由前置步骤克隆在 {outputDir}/source/ohos/(鸿蒙化)与 {outputDir}/source/RN/(上游原库)目录下,直接分析即可,请勿重新克隆',
|
|
297
|
+
'- 输入报告路径为前置步骤的执行结果记录,仅供参考;接口规格文档以 interfaceDocPath 为准',
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
// 维度专属参数
|
|
301
|
+
if (def.key === 'memoryleak') {
|
|
302
|
+
desc.push('- 编码质量检视报告.md 为可选参考输入,若不存在则跳过该参考,独立完成检视');
|
|
303
|
+
}
|
|
304
|
+
if (def.key === 'documentation' && rndocUrl) {
|
|
305
|
+
desc.push('- rndocUrl(外部文档地址): ' + rndocUrl);
|
|
306
|
+
}
|
|
307
|
+
if (def.key === 'testcoverage' && rnRntUrl) {
|
|
308
|
+
desc.push('- rnRntUrl(测试用例 demo 地址): ' + rnRntUrl);
|
|
309
|
+
}
|
|
310
|
+
if (def.key === 'knownissue') {
|
|
311
|
+
desc.push('- knowledgeDir(已知问题知识库): BadCase/rn(相对 3rdLibraryLoop 仓库根目录;若任务工作目录不在仓库内则使用绝对路径)');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
steps.push({
|
|
315
|
+
stepId: def.stepId,
|
|
316
|
+
taskDescription: desc.join('\n'),
|
|
317
|
+
dependId: 1,
|
|
318
|
+
nextStepId: 0,
|
|
319
|
+
failStepId: 0,
|
|
320
|
+
maxRetries: 3,
|
|
321
|
+
skillDirectory: skillDir || '',
|
|
322
|
+
specifiedSkill: def.skill,
|
|
323
|
+
inputReport: '',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
var taskDef = {
|
|
328
|
+
taskId: taskId,
|
|
329
|
+
title: 'RN仓库检视|' + libraryName + '|' + ohosRepoUrl + '|' + upstreamRepoUrl,
|
|
330
|
+
skillDirectory: skillDir || '',
|
|
331
|
+
loopCount: 0,
|
|
332
|
+
steps: steps,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// ── 3. POST /api/orchestrator/load ──
|
|
336
|
+
var loadR = await apiFetch('POST', '/api/orchestrator/load', taskDef);
|
|
337
|
+
if (loadR.status >= 400 || !loadR.data || loadR.data.status !== 'ok') {
|
|
338
|
+
_rnShowErr((loadR.data && (loadR.data.error || loadR.data.message)) || ('HTTP ' + loadR.status));
|
|
339
|
+
_rnGuardSubmitReset(btn);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── 4. POST /api/orchestrator/start ──
|
|
344
|
+
var startR = await apiFetch('POST', '/api/orchestrator/start', { taskId: taskId });
|
|
345
|
+
if (startR.status >= 400 || !startR.data || startR.data.status !== 'ok') {
|
|
346
|
+
_rnShowErr((startR.data && (startR.data.error || startR.data.message)) || ('HTTP ' + startR.status));
|
|
347
|
+
_rnGuardSubmitReset(btn);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── 5. 创建任务卡片 + SSE + 轮询 ──
|
|
352
|
+
var idx = ++_rnCounter;
|
|
353
|
+
_rnTasks[idx] = _rnNewTaskState(taskId, libraryName, ohosRepoUrl, upstreamRepoUrl, rndocUrl, rnRntUrl);
|
|
354
|
+
|
|
355
|
+
_rnCreateCard(idx, new Date().toISOString(), false);
|
|
356
|
+
_rnConnectSSE(idx);
|
|
357
|
+
_rnStartProgressPoll(idx);
|
|
358
|
+
|
|
359
|
+
_rnGuardSubmitRestore(btn);
|
|
360
|
+
toast('success', '检视任务已启动: ' + libraryName);
|
|
361
|
+
|
|
362
|
+
} catch (e) {
|
|
363
|
+
_rnShowErr(e.message);
|
|
364
|
+
_rnGuardSubmitReset(btn);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function _rnNewTaskState(taskId, libraryName, ohosRepoUrl, upstreamRepoUrl, rndocUrl, rnRntUrl) {
|
|
369
|
+
return {
|
|
370
|
+
taskId: taskId,
|
|
371
|
+
libraryName: libraryName,
|
|
372
|
+
ohosRepoUrl: ohosRepoUrl,
|
|
373
|
+
upstreamRepoUrl: upstreamRepoUrl,
|
|
374
|
+
rndocUrl: rndocUrl || '',
|
|
375
|
+
rnRntUrl: rnRntUrl || '',
|
|
376
|
+
sse: null,
|
|
377
|
+
timer: null,
|
|
378
|
+
pollTimer: null,
|
|
379
|
+
startTime: Date.now(),
|
|
380
|
+
startedAt: null,
|
|
381
|
+
finishedAt: null,
|
|
382
|
+
filesResolved: false,
|
|
383
|
+
_lastResolveAt: 0,
|
|
384
|
+
_resolving: false,
|
|
385
|
+
_progressLoaded: false,
|
|
386
|
+
_terminalDone: false,
|
|
387
|
+
mdFileCount: 0, // taskDir 下的 .md 文件数(驱动"下载全部报告"按钮)
|
|
388
|
+
reportPaths: {}, // 各维度专属报告文件(如 稳定性检视报告.md)
|
|
389
|
+
resultPaths: {}, // 各步骤 result .md 兜底路径(部分 SKILL 会把报告合入 result 文件)
|
|
390
|
+
scores: { stability: null, consistency: null, memoryleak: null, documentation: null, testcoverage: null, knownissue: null, overall: null },
|
|
391
|
+
scoreReasons: {}, // 各维度评分判定依据(来自 meta .json 的 scoreReason,悬浮提示展示)
|
|
392
|
+
stepStatus: {},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
397
|
+
* 任务卡片渲染
|
|
398
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
399
|
+
|
|
400
|
+
function _rnCreateCard(idx, createdAt, isHistory) {
|
|
401
|
+
var task = _rnTasks[idx];
|
|
402
|
+
var emptyEl = $('rn-tasks-empty');
|
|
403
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
404
|
+
var countEl = $('rn-task-count');
|
|
405
|
+
if (countEl) countEl.textContent = Object.keys(_rnTasks).length + ' 个任务';
|
|
406
|
+
|
|
407
|
+
var timeLabel = createdAt ? _rnFormatTime(createdAt) : '';
|
|
408
|
+
|
|
409
|
+
var card = document.createElement('div');
|
|
410
|
+
card.className = 'cc-task-card running';
|
|
411
|
+
card.id = 'rn-card-' + idx;
|
|
412
|
+
card.innerHTML =
|
|
413
|
+
'<div class="cc-card-header" onclick="toggleRNCard(' + idx + ')">' +
|
|
414
|
+
'<span class="cc-chevron">▶</span>' +
|
|
415
|
+
'<span class="cc-run-dot"></span>' +
|
|
416
|
+
'<span class="cc-lib">' + _rnEsc(task.libraryName) + '</span>' +
|
|
417
|
+
'<span class="cc-type-badge" style="background:rgba(175,82,222,0.12);color:var(--purple)">RN</span>' +
|
|
418
|
+
'<span class="cc-spacer"></span>' +
|
|
419
|
+
'<span class="cc-time" id="rn-' + idx + '-time">' + _rnEsc(timeLabel) + '</span>' +
|
|
420
|
+
'<span class="cc-merge-badge" id="rn-' + idx + '-overall-badge" style="display:none"></span>' +
|
|
421
|
+
'<span class="cc-stage-badge s-pending" id="rn-' + idx + '-badge">等待中</span>' +
|
|
422
|
+
'<button class="cc-stop-btn" id="rn-' + idx + '-stop" onclick="event.stopPropagation();stopRNTask(' + idx + ')" title="取消检视">■ 取消</button>' +
|
|
423
|
+
'<span class="cc-elapsed" id="rn-' + idx + '-elapsed" style="display:none">0s</span>' +
|
|
424
|
+
'</div>' +
|
|
425
|
+
'<div class="cc-card-body" id="rn-' + idx + '-body"' + (isHistory ? ' style="display:none"' : '') + '>' +
|
|
426
|
+
'<div class="cc-meta" title="' + _rnEsc(task.ohosRepoUrl) + '">' +
|
|
427
|
+
'<span class="cc-meta-label">鸿蒙化仓库:</span>' +
|
|
428
|
+
'<span class="cc-meta-value">' + _rnEsc(task.ohosRepoUrl) + '</span>' +
|
|
429
|
+
'</div>' +
|
|
430
|
+
'<div class="cc-meta" title="' + _rnEsc(task.upstreamRepoUrl) + '">' +
|
|
431
|
+
'<span class="cc-meta-label">上游原库:</span>' +
|
|
432
|
+
'<span class="cc-meta-value">' + _rnEsc(task.upstreamRepoUrl) + '</span>' +
|
|
433
|
+
'</div>' +
|
|
434
|
+
'<div class="rn-score-panel" id="rn-' + idx + '-score-panel" style="display:none">' +
|
|
435
|
+
'<div class="rn-score-panel-title">📊 检视评分概览</div>' +
|
|
436
|
+
'<div class="rn-score-overall-row">' +
|
|
437
|
+
'<span class="rn-score-label">综合评分</span>' +
|
|
438
|
+
'<span class="rn-score-value" id="rn-' + idx + '-overall-score" style="display:none">--</span>' +
|
|
439
|
+
'<span class="rn-score-weights">稳定性 20% · 一致性 20% · 内存泄露 20% · 文档质量 15% · 用例覆盖 25% · 已知问题独立扣分</span>' +
|
|
440
|
+
'</div>' +
|
|
441
|
+
'<div class="rn-score-grid">' +
|
|
442
|
+
'<div class="rn-score-item"><span class="rn-score-item-label">稳定性</span><span class="rn-score-item-value" id="rn-' + idx + '-dim-stability">--</span></div>' +
|
|
443
|
+
'<div class="rn-score-item"><span class="rn-score-item-label">一致性</span><span class="rn-score-item-value" id="rn-' + idx + '-dim-consistency">--</span></div>' +
|
|
444
|
+
'<div class="rn-score-item"><span class="rn-score-item-label">内存泄露</span><span class="rn-score-item-value" id="rn-' + idx + '-dim-memoryleak">--</span></div>' +
|
|
445
|
+
'<div class="rn-score-item"><span class="rn-score-item-label">文档质量</span><span class="rn-score-item-value" id="rn-' + idx + '-dim-documentation">--</span></div>' +
|
|
446
|
+
'<div class="rn-score-item"><span class="rn-score-item-label">用例覆盖</span><span class="rn-score-item-value" id="rn-' + idx + '-dim-testcoverage">--</span></div>' +
|
|
447
|
+
'<div class="rn-score-item"><span class="rn-score-item-label">已知问题</span><span class="rn-score-item-value" id="rn-' + idx + '-dim-knownissue">--</span></div>' +
|
|
448
|
+
'</div>' +
|
|
449
|
+
'</div>' +
|
|
450
|
+
'<div class="rn-steps-panel">' +
|
|
451
|
+
'<div class="rn-steps-title-row">' +
|
|
452
|
+
'<div class="rn-steps-title">🔍 检视流程进度</div>' +
|
|
453
|
+
'<button class="rn-archive-btn" id="rn-' + idx + '-archive-btn" onclick="event.stopPropagation();downloadRNArchive(' + idx + ')" style="display:none" title="打包下载任务目录下的全部 .md 报告文件(ZIP)">📦 下载全部报告</button>' +
|
|
454
|
+
'</div>' +
|
|
455
|
+
'<div class="rn-steps-list">' +
|
|
456
|
+
_RN_STEP_DEFS.map(function (def) { return _rnStepRowHtml(idx, def); }).join('') +
|
|
457
|
+
'</div>' +
|
|
458
|
+
'</div>' +
|
|
459
|
+
'</div>';
|
|
460
|
+
|
|
461
|
+
var list = $('rn-tasks-list');
|
|
462
|
+
list.insertBefore(card, list.firstChild);
|
|
463
|
+
|
|
464
|
+
// 非历史(新提交)任务默认展开
|
|
465
|
+
if (!isHistory) {
|
|
466
|
+
card.classList.add('open');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 历史已完成任务:显示固定时长;进行中任务:启动计时器
|
|
470
|
+
if (isHistory && task.finishedAt && task.startedAt) {
|
|
471
|
+
_rnRenderElapsed(idx);
|
|
472
|
+
} else {
|
|
473
|
+
task.timer = setInterval(function () {
|
|
474
|
+
var s = Math.floor((Date.now() - _rnTasks[idx].startTime) / 1000);
|
|
475
|
+
var el = document.getElementById('rn-' + idx + '-elapsed');
|
|
476
|
+
if (el) el.textContent = s < 60 ? s + 's' : Math.floor(s / 60) + 'm ' + (s % 60) + 's';
|
|
477
|
+
}, 1000);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function _rnStepRowHtml(idx, def) {
|
|
482
|
+
return '<div class="rn-step-row" id="rn-' + idx + '-row-' + def.key + '">' +
|
|
483
|
+
'<div class="rn-step-icon" id="rn-' + idx + '-icon-' + def.key + '">' + def.num + '</div>' +
|
|
484
|
+
'<div class="rn-step-info">' +
|
|
485
|
+
'<div class="rn-step-name">' + _rnEsc(def.name) +
|
|
486
|
+
(def.scoreKey ? ' <span class="rn-step-weight">权重 ' + Math.round(_RN_WEIGHTS[def.key] * 100) + '%</span>' : '') +
|
|
487
|
+
'</div>' +
|
|
488
|
+
'<div class="rn-step-desc" id="rn-' + idx + '-' + def.key + '-txt">等待中…</div>' +
|
|
489
|
+
'</div>' +
|
|
490
|
+
'<div class="rn-step-actions">' +
|
|
491
|
+
'<span class="rn-step-score" id="rn-' + idx + '-score-' + def.key + '" style="display:none">--</span>' +
|
|
492
|
+
'<button class="cc-preview-btn" id="rn-' + idx + '-view-' + def.key + '" onclick="event.stopPropagation();viewRNReport(' + idx + ', \'' + def.key + '\')" style="display:none">↗ 预览</button>' +
|
|
493
|
+
'<button class="cc-download-btn" id="rn-' + idx + '-download-' + def.key + '" onclick="event.stopPropagation();downloadRNReport(' + idx + ', \'' + def.key + '\')" style="display:none">↓ 下载</button>' +
|
|
494
|
+
'<button class="rn-session-btn" id="rn-' + idx + '-session-' + def.key + '" onclick="event.stopPropagation();viewRNSession(' + idx + ', ' + def.stepId + ', \'' + def.key + '\')" style="display:none">💬</button>' +
|
|
495
|
+
'</div>' +
|
|
496
|
+
'</div>';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* ── 卡片折叠(展开时懒加载报告文件与评分兜底)──────────────────────── */
|
|
500
|
+
|
|
501
|
+
function toggleRNCard(idx) {
|
|
502
|
+
var card = document.getElementById('rn-card-' + idx);
|
|
503
|
+
var body = document.getElementById('rn-' + idx + '-body');
|
|
504
|
+
if (!card || !body) return;
|
|
505
|
+
var isOpen = card.classList.toggle('open');
|
|
506
|
+
body.style.display = isOpen ? '' : 'none';
|
|
507
|
+
if (isOpen) {
|
|
508
|
+
var task = _rnTasks[idx];
|
|
509
|
+
if (!task) return;
|
|
510
|
+
if (!task.filesResolved) _rnResolveFiles(idx);
|
|
511
|
+
// 历史终态任务(无 SSE/轮询):展开时加载一次进度,恢复步骤状态与 metrics 评分
|
|
512
|
+
if (!task.pollTimer && !task.sse) _rnLoadProgressOnce(idx);
|
|
513
|
+
_rnFallbackScores(idx);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 一次性加载编排进度(历史终态任务展开时调用):
|
|
519
|
+
* 从磁盘恢复的 orchestration.json 中读取各步骤状态与 metrics 评分。
|
|
520
|
+
*/
|
|
521
|
+
function _rnLoadProgressOnce(idx) {
|
|
522
|
+
var task = _rnTasks[idx];
|
|
523
|
+
if (!task || task._progressLoaded) return;
|
|
524
|
+
task._progressLoaded = true;
|
|
525
|
+
|
|
526
|
+
apiFetch('GET', '/api/orchestrator/progress?taskId=' + encodeURIComponent(task.taskId), null, 15000)
|
|
527
|
+
.then(function (r) {
|
|
528
|
+
if (!r.data || r.data.status !== 'ok' || !r.data.progress) return;
|
|
529
|
+
var progress = r.data.progress;
|
|
530
|
+
_rnUpdateStage(idx, progress.status, progress);
|
|
531
|
+
if (progress.steps) {
|
|
532
|
+
for (var i = 0; i < progress.steps.length; i++) {
|
|
533
|
+
_rnHandleStepStatus(idx, progress.steps[i]);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
_rnRenderElapsed(idx);
|
|
537
|
+
})
|
|
538
|
+
.catch(function () {
|
|
539
|
+
// 失败允许重试(下次展开时再加载)
|
|
540
|
+
if (_rnTasks[idx]) _rnTasks[idx]._progressLoaded = false;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
545
|
+
* SSE 连接(监听 Orchestrator 事件)
|
|
546
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
547
|
+
|
|
548
|
+
function _rnConnectSSE(idx) {
|
|
549
|
+
var task = _rnTasks[idx];
|
|
550
|
+
if (!task) return;
|
|
551
|
+
|
|
552
|
+
if (task.sse && (task.sse.readyState === EventSource.OPEN || task.sse.readyState === EventSource.CONNECTING)) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (task.sse) { var old = task.sse; task.sse = null; old.close(); }
|
|
556
|
+
|
|
557
|
+
var sse = new EventSource('/stream/tasks/' + encodeURIComponent(task.taskId));
|
|
558
|
+
task.sse = sse;
|
|
559
|
+
var sseDone = false;
|
|
560
|
+
|
|
561
|
+
// 步骤状态变更(含 metrics 评分回填)
|
|
562
|
+
sse.addEventListener('orch:step:status', function (e) {
|
|
563
|
+
var data; try { data = JSON.parse(e.data); } catch (err) { data = {}; }
|
|
564
|
+
_rnHandleStepStatus(idx, data);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
sse.addEventListener('orch:task:start', function () {
|
|
568
|
+
_rnUpdateStage(idx, 'running');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
sse.addEventListener('orch:task:complete', function (e) {
|
|
572
|
+
sseDone = true;
|
|
573
|
+
var data; try { data = JSON.parse(e.data); } catch (err) { data = {}; }
|
|
574
|
+
var status = data.status || 'completed';
|
|
575
|
+
_rnUpdateStage(idx, status);
|
|
576
|
+
_rnOnTerminal(idx, status);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
sse.addEventListener('orch:task:error', function () {
|
|
580
|
+
_rnUpdateStage(idx, 'failed');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
sse.addEventListener('orch:task:aborted', function () {
|
|
584
|
+
sseDone = true;
|
|
585
|
+
_rnUpdateStage(idx, 'aborted');
|
|
586
|
+
_rnOnTerminal(idx, 'aborted');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
sse.onerror = function () {
|
|
590
|
+
if (sseDone) { sse.close(); return; }
|
|
591
|
+
// 轮询作为兜底,SSE 断开不额外重连
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* 处理步骤状态事件 / progress 中的步骤快照。
|
|
597
|
+
* @param {number} idx
|
|
598
|
+
* @param {object} step - { stepId, status, metrics, startTime, endTime, resultPath, ... }
|
|
599
|
+
*/
|
|
600
|
+
function _rnHandleStepStatus(idx, step) {
|
|
601
|
+
if (!step || step.stepId == null) return;
|
|
602
|
+
var task = _rnTasks[idx];
|
|
603
|
+
if (!task) return;
|
|
604
|
+
|
|
605
|
+
var def = null;
|
|
606
|
+
for (var i = 0; i < _RN_STEP_DEFS.length; i++) {
|
|
607
|
+
if (String(_RN_STEP_DEFS[i].stepId) === String(step.stepId)) { def = _RN_STEP_DEFS[i]; break; }
|
|
608
|
+
}
|
|
609
|
+
if (!def) return;
|
|
610
|
+
|
|
611
|
+
if (step.startTime) task.startedAt = step.startTime;
|
|
612
|
+
|
|
613
|
+
_rnUpdateStepRow(idx, def, step.status || 'pending');
|
|
614
|
+
|
|
615
|
+
// 评分回填:步骤完成时从 metrics 提取(meta .json 已由后端收割)
|
|
616
|
+
if (step.status === 'completed') {
|
|
617
|
+
var score = _rnExtractStepScore(def, step.metrics);
|
|
618
|
+
var reason = (step.metrics && typeof step.metrics.scoreReason === 'string')
|
|
619
|
+
? step.metrics.scoreReason : null;
|
|
620
|
+
if (score != null) {
|
|
621
|
+
_rnSetStepScore(idx, def, score, reason);
|
|
622
|
+
} else if (reason) {
|
|
623
|
+
task.scoreReasons[def.key] = reason;
|
|
624
|
+
}
|
|
625
|
+
// 步骤完成即尝试解析报告文件(显示预览/下载按钮)
|
|
626
|
+
_rnResolveFiles(idx);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
631
|
+
* 进度轮询(SSE 补充)
|
|
632
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
633
|
+
|
|
634
|
+
function _rnStartProgressPoll(idx) {
|
|
635
|
+
var task = _rnTasks[idx];
|
|
636
|
+
if (!task || task.pollTimer) return;
|
|
637
|
+
|
|
638
|
+
task.pollTimer = setInterval(async function () {
|
|
639
|
+
try {
|
|
640
|
+
var r = await apiFetch('GET', '/api/orchestrator/progress?taskId=' + encodeURIComponent(task.taskId), null, 10000);
|
|
641
|
+
if (!r.data || r.data.status !== 'ok') return;
|
|
642
|
+
|
|
643
|
+
var progress = r.data.progress;
|
|
644
|
+
if (!progress) return;
|
|
645
|
+
|
|
646
|
+
_rnUpdateStage(idx, progress.status, progress);
|
|
647
|
+
|
|
648
|
+
if (progress.steps && progress.steps.length > 0) {
|
|
649
|
+
for (var i = 0; i < progress.steps.length; i++) {
|
|
650
|
+
_rnHandleStepStatus(idx, progress.steps[i]);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (['completed', 'failed', 'aborted', 'deadlocked'].indexOf(progress.status) >= 0) {
|
|
655
|
+
_rnOnTerminal(idx, progress.status);
|
|
656
|
+
}
|
|
657
|
+
} catch (e) { /* 静默失败 */ }
|
|
658
|
+
}, 3000);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function _rnStopProgressPoll(idx) {
|
|
662
|
+
var task = _rnTasks[idx];
|
|
663
|
+
if (task && task.pollTimer) {
|
|
664
|
+
clearInterval(task.pollTimer);
|
|
665
|
+
task.pollTimer = null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
670
|
+
* UI 更新
|
|
671
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
672
|
+
|
|
673
|
+
function _rnUpdateStage(idx, stage, progress) {
|
|
674
|
+
var task = _rnTasks[idx];
|
|
675
|
+
if (!task) return;
|
|
676
|
+
if (progress) {
|
|
677
|
+
if (progress.startTime) task.startedAt = progress.startTime;
|
|
678
|
+
if (progress.endTime) task.finishedAt = progress.endTime;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
var badge = document.getElementById('rn-' + idx + '-badge');
|
|
682
|
+
if (badge) {
|
|
683
|
+
badge.className = 'cc-stage-badge s-' + stage;
|
|
684
|
+
badge.textContent = _RN_STAGE_LABELS[stage] || stage;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function _rnUpdateStepRow(idx, def, status) {
|
|
689
|
+
var task = _rnTasks[idx];
|
|
690
|
+
if (!task) return;
|
|
691
|
+
task.stepStatus[def.key] = status;
|
|
692
|
+
|
|
693
|
+
var icon = document.getElementById('rn-' + idx + '-icon-' + def.key);
|
|
694
|
+
var txt = document.getElementById('rn-' + idx + '-' + def.key + '-txt');
|
|
695
|
+
var row = document.getElementById('rn-' + idx + '-row-' + def.key);
|
|
696
|
+
var sessionBtn = document.getElementById('rn-' + idx + '-session-' + def.key);
|
|
697
|
+
|
|
698
|
+
if (row) row.className = 'rn-step-row ' + status;
|
|
699
|
+
|
|
700
|
+
if (icon) {
|
|
701
|
+
if (status === 'running') {
|
|
702
|
+
icon.className = 'rn-step-icon running';
|
|
703
|
+
icon.textContent = '🔄';
|
|
704
|
+
} else if (status === 'completed') {
|
|
705
|
+
icon.className = 'rn-step-icon completed';
|
|
706
|
+
icon.textContent = '✓';
|
|
707
|
+
} else if (status === 'failed') {
|
|
708
|
+
icon.className = 'rn-step-icon failed';
|
|
709
|
+
icon.textContent = '✗';
|
|
710
|
+
} else if (status === 'skipped' || status === 'blocked') {
|
|
711
|
+
icon.className = 'rn-step-icon skipped';
|
|
712
|
+
icon.textContent = '⊘';
|
|
713
|
+
} else {
|
|
714
|
+
icon.className = 'rn-step-icon';
|
|
715
|
+
icon.textContent = def.num;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (txt) txt.textContent = _RN_STEP_STATUS_LABELS[status] || status;
|
|
719
|
+
|
|
720
|
+
// 对话按钮:已执行(含失败)即可查看会话
|
|
721
|
+
if (sessionBtn && ['completed', 'failed'].indexOf(status) >= 0) {
|
|
722
|
+
sessionBtn.style.display = 'inline-flex';
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 设置步骤评分:更新步骤徽章、评分概览面板、综合评分。
|
|
728
|
+
* @param {number} idx
|
|
729
|
+
* @param {object} def - 步骤定义
|
|
730
|
+
* @param {number} score - 0-100 评分
|
|
731
|
+
* @param {string|null} [reason] - 评分判定依据(meta .json 的 scoreReason)
|
|
732
|
+
*/
|
|
733
|
+
function _rnSetStepScore(idx, def, score, reason) {
|
|
734
|
+
var task = _rnTasks[idx];
|
|
735
|
+
if (!task || score == null || !def.scoreKey) return;
|
|
736
|
+
|
|
737
|
+
// 评分依据先落缓存(已有评分不覆盖,但依据允许补充)
|
|
738
|
+
if (reason && !task.scoreReasons[def.key]) task.scoreReasons[def.key] = reason;
|
|
739
|
+
|
|
740
|
+
// 已有评分不覆盖(避免轮询旧值覆盖新值;0 是合法评分,必须用 != null 判断)
|
|
741
|
+
if (task.scores[def.key] != null) { _rnRenderScorePanel(idx); return; }
|
|
742
|
+
task.scores[def.key] = score;
|
|
743
|
+
|
|
744
|
+
// 步骤行评分徽章
|
|
745
|
+
var scoreEl = document.getElementById('rn-' + idx + '-score-' + def.key);
|
|
746
|
+
if (scoreEl) {
|
|
747
|
+
scoreEl.style.display = 'inline-flex';
|
|
748
|
+
scoreEl.textContent = score + '分';
|
|
749
|
+
_rnApplyScoreStyle(scoreEl, score);
|
|
750
|
+
if (task.scoreReasons[def.key]) {
|
|
751
|
+
scoreEl.title = '评分依据:' + task.scoreReasons[def.key];
|
|
752
|
+
scoreEl.style.cursor = 'help';
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// 评分概览面板
|
|
757
|
+
_rnRenderScorePanel(idx);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** 评分概览面板渲染(维度评分 + 综合评分 + 头部总分徽章;knownissue 为独立扣分项) */
|
|
761
|
+
function _rnRenderScorePanel(idx) {
|
|
762
|
+
var task = _rnTasks[idx];
|
|
763
|
+
if (!task) return;
|
|
764
|
+
|
|
765
|
+
var hasAny = false;
|
|
766
|
+
for (var k in _RN_WEIGHTS) { if (task.scores[k] != null) { hasAny = true; break; } }
|
|
767
|
+
|
|
768
|
+
var panel = document.getElementById('rn-' + idx + '-score-panel');
|
|
769
|
+
if (panel && hasAny) panel.style.display = 'block';
|
|
770
|
+
|
|
771
|
+
var dims = ['stability', 'consistency', 'memoryleak', 'documentation', 'testcoverage', 'knownissue'];
|
|
772
|
+
for (var i = 0; i < dims.length; i++) {
|
|
773
|
+
var key = dims[i];
|
|
774
|
+
var el = document.getElementById('rn-' + idx + '-dim-' + key);
|
|
775
|
+
if (!el) continue;
|
|
776
|
+
if (task.scores[key] != null) {
|
|
777
|
+
el.style.display = 'inline-flex';
|
|
778
|
+
el.textContent = task.scores[key] + '分';
|
|
779
|
+
_rnApplyScoreStyle(el, task.scores[key]);
|
|
780
|
+
}
|
|
781
|
+
// 评分依据悬浮提示(区分"很差"与"一票否决"等严苛规则判定)
|
|
782
|
+
if (task.scoreReasons[key]) {
|
|
783
|
+
el.title = '评分依据:' + task.scoreReasons[key];
|
|
784
|
+
el.style.cursor = 'help';
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// 综合评分(5 维齐全才计算;knownissue 不参与加权,作为独立扣分项:命中一条扣 30 分)
|
|
789
|
+
if (task.scores.overall == null) {
|
|
790
|
+
task.scores.overall = _rnComputeOverall(task.scores);
|
|
791
|
+
}
|
|
792
|
+
if (task.scores.overall != null) {
|
|
793
|
+
var displayOverall = task.scores.overall;
|
|
794
|
+
if (task.scores.knownissue != null && task.scores.knownissue < 100) {
|
|
795
|
+
var hitCount = Math.min(3, Math.round((100 - task.scores.knownissue) / 30));
|
|
796
|
+
displayOverall = Math.max(0, task.scores.overall - 30 * hitCount);
|
|
797
|
+
}
|
|
798
|
+
var overallEl = document.getElementById('rn-' + idx + '-overall-score');
|
|
799
|
+
if (overallEl) {
|
|
800
|
+
overallEl.style.display = 'inline-flex';
|
|
801
|
+
overallEl.textContent = displayOverall + '分';
|
|
802
|
+
_rnApplyScoreStyle(overallEl, displayOverall, true);
|
|
803
|
+
}
|
|
804
|
+
var badge = document.getElementById('rn-' + idx + '-overall-badge');
|
|
805
|
+
if (badge) {
|
|
806
|
+
badge.style.display = 'inline-flex';
|
|
807
|
+
badge.textContent = '综合 ' + displayOverall + '分';
|
|
808
|
+
badge.style.color = displayOverall >= 80 ? 'var(--green)' : displayOverall >= 60 ? 'var(--orange)' : 'var(--red)';
|
|
809
|
+
badge.style.background = displayOverall >= 80 ? 'rgba(52,199,89,0.15)' : displayOverall >= 60 ? 'rgba(255,149,0,0.15)' : 'rgba(255,59,48,0.15)';
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** 评分样式:≥80 绿 / ≥60 橙 / <60 红 */
|
|
815
|
+
function _rnApplyScoreStyle(el, score, isLarge) {
|
|
816
|
+
if (!el) return;
|
|
817
|
+
if (score >= 80) {
|
|
818
|
+
el.style.color = 'var(--green)';
|
|
819
|
+
el.style.background = 'rgba(52,199,89,0.12)';
|
|
820
|
+
el.style.border = isLarge ? '1px solid rgba(52,199,89,0.3)' : 'none';
|
|
821
|
+
} else if (score >= 60) {
|
|
822
|
+
el.style.color = 'var(--orange)';
|
|
823
|
+
el.style.background = 'rgba(255,149,0,0.12)';
|
|
824
|
+
el.style.border = isLarge ? '1px solid rgba(255,149,0,0.3)' : 'none';
|
|
825
|
+
} else {
|
|
826
|
+
el.style.color = 'var(--red)';
|
|
827
|
+
el.style.background = 'rgba(255,59,48,0.12)';
|
|
828
|
+
el.style.border = isLarge ? '1px solid rgba(255,59,48,0.3)' : 'none';
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
833
|
+
* 报告文件解析(懒加载:/api/flexrunner/steps 扫描 taskDir 下的 .md)
|
|
834
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
835
|
+
|
|
836
|
+
async function _rnResolveFiles(idx) {
|
|
837
|
+
var task = _rnTasks[idx];
|
|
838
|
+
if (!task) return;
|
|
839
|
+
|
|
840
|
+
// 全部专属报告已找到 → 幂等返回
|
|
841
|
+
var allFound = true;
|
|
842
|
+
for (var a = 0; a < _RN_STEP_DEFS.length; a++) {
|
|
843
|
+
if (!task.reportPaths[_RN_STEP_DEFS[a].key]) { allFound = false; break; }
|
|
844
|
+
}
|
|
845
|
+
if (allFound) { task.filesResolved = true; _rnShowReportButtons(idx); return; }
|
|
846
|
+
|
|
847
|
+
// 节流:进行中任务最多每 15s 解析一次(报告随步骤完成渐进生成)
|
|
848
|
+
var now = Date.now();
|
|
849
|
+
if (task._resolving) return;
|
|
850
|
+
if (task._lastResolveAt && now - task._lastResolveAt < 15000) {
|
|
851
|
+
_rnShowReportButtons(idx);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
task._lastResolveAt = now;
|
|
855
|
+
task._resolving = true;
|
|
856
|
+
|
|
857
|
+
try {
|
|
858
|
+
var r = await apiFetch('GET', '/api/flexrunner/steps?taskId=' + encodeURIComponent(task.taskId), null, 15000);
|
|
859
|
+
if (!r.data || r.data.status !== 'ok') return;
|
|
860
|
+
|
|
861
|
+
var files = r.data.files || [];
|
|
862
|
+
var found = false;
|
|
863
|
+
|
|
864
|
+
// .md 文件计数(steps 接口只返回 .md),驱动"下载全部报告"按钮
|
|
865
|
+
if (files.length > 0) {
|
|
866
|
+
task.mdFileCount = files.length;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
for (var i = 0; i < files.length; i++) {
|
|
870
|
+
var f = files[i];
|
|
871
|
+
|
|
872
|
+
// 步骤 result .md 兜底路径(部分 SKILL 会把完整报告写入 result 文件)
|
|
873
|
+
// 文件命名: stepId{N}_loop{L}_result_{R}.md,取重试号最大的最新一次
|
|
874
|
+
var resMatch = (f.filename || '').match(/^stepId(\d+)_loop\d+_result_(\d+)\.md$/);
|
|
875
|
+
if (resMatch) {
|
|
876
|
+
var resDef = _rnDefByStepId(resMatch[1]);
|
|
877
|
+
if (resDef) {
|
|
878
|
+
var retry = Number(resMatch[2]) || 0;
|
|
879
|
+
var prev = task.resultPaths[resDef.key];
|
|
880
|
+
if (!prev || retry >= (task._resultRetry || {})[resDef.key]) {
|
|
881
|
+
task.resultPaths[resDef.key] = f.path;
|
|
882
|
+
task._resultRetry = task._resultRetry || {};
|
|
883
|
+
task._resultRetry[resDef.key] = retry;
|
|
884
|
+
found = true;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// 主报告映射(专属报告文件,优先于 result 兜底)
|
|
890
|
+
for (var j = 0; j < _RN_STEP_DEFS.length; j++) {
|
|
891
|
+
var def = _RN_STEP_DEFS[j];
|
|
892
|
+
if (f.filename === def.report && !task.reportPaths[def.key]) {
|
|
893
|
+
task.reportPaths[def.key] = f.path;
|
|
894
|
+
found = true;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// 附属脚本报告(用例覆盖度的测试覆盖率/Demo覆盖率脚本报告)
|
|
898
|
+
for (var key in _RN_EXTRA_REPORTS) {
|
|
899
|
+
var extras = _RN_EXTRA_REPORTS[key];
|
|
900
|
+
for (var m = 0; m < extras.length; m++) {
|
|
901
|
+
if (f.filename === extras[m].report && !task.reportPaths[extras[m].key]) {
|
|
902
|
+
task.reportPaths[extras[m].key] = f.path;
|
|
903
|
+
found = true;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (found) _rnShowReportButtons(idx);
|
|
910
|
+
} catch (e) { /* 静默失败 */ } finally {
|
|
911
|
+
task._resolving = false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** stepId(字符串)→ 步骤定义 */
|
|
916
|
+
function _rnDefByStepId(stepId) {
|
|
917
|
+
for (var i = 0; i < _RN_STEP_DEFS.length; i++) {
|
|
918
|
+
if (String(_RN_STEP_DEFS[i].stepId) === String(stepId)) return _RN_STEP_DEFS[i];
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** 已完成且报告可用的步骤显示预览/下载按钮(专属报告或 result 兜底均可);同步更新"下载全部报告"按钮 */
|
|
924
|
+
function _rnShowReportButtons(idx) {
|
|
925
|
+
var task = _rnTasks[idx];
|
|
926
|
+
if (!task) return;
|
|
927
|
+
|
|
928
|
+
for (var i = 0; i < _RN_STEP_DEFS.length; i++) {
|
|
929
|
+
var def = _RN_STEP_DEFS[i];
|
|
930
|
+
if (task.reportPaths[def.key] || task.resultPaths[def.key]) {
|
|
931
|
+
var viewBtn = document.getElementById('rn-' + idx + '-view-' + def.key);
|
|
932
|
+
var dlBtn = document.getElementById('rn-' + idx + '-download-' + def.key);
|
|
933
|
+
if (viewBtn) viewBtn.style.display = 'inline-flex';
|
|
934
|
+
if (dlBtn) dlBtn.style.display = 'inline-flex';
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
_rnUpdateArchiveButton(idx);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** 更新"下载全部报告"按钮(有 .md 产物时显示,带文件数) */
|
|
942
|
+
function _rnUpdateArchiveButton(idx) {
|
|
943
|
+
var task = _rnTasks[idx];
|
|
944
|
+
if (!task) return;
|
|
945
|
+
var btn = document.getElementById('rn-' + idx + '-archive-btn');
|
|
946
|
+
if (!btn) return;
|
|
947
|
+
var count = task.mdFileCount || 0;
|
|
948
|
+
if (count > 0) {
|
|
949
|
+
btn.style.display = 'inline-flex';
|
|
950
|
+
btn.textContent = '📦 下载全部报告 (' + count + ')';
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
955
|
+
* 评分兜底:读取报告文本解析综合评分(SKILL 未写 meta .json 时)
|
|
956
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
957
|
+
|
|
958
|
+
async function _rnFallbackScores(idx) {
|
|
959
|
+
var task = _rnTasks[idx];
|
|
960
|
+
if (!task) return;
|
|
961
|
+
|
|
962
|
+
// 兜底只针对已完成的检视步骤且尚无评分的维度
|
|
963
|
+
for (var i = 0; i < _RN_STEP_DEFS.length; i++) {
|
|
964
|
+
var def = _RN_STEP_DEFS[i];
|
|
965
|
+
if (!def.scoreKey) continue;
|
|
966
|
+
if (task.scores[def.key] != null) continue;
|
|
967
|
+
if (task.stepStatus[def.key] && task.stepStatus[def.key] !== 'completed') continue;
|
|
968
|
+
|
|
969
|
+
// 需要报告文件路径;未解析过先触发解析(专属报告或 result .md 均可)
|
|
970
|
+
if (!_rnGetReportPath(idx, def.key)) {
|
|
971
|
+
await _rnResolveFiles(idx);
|
|
972
|
+
}
|
|
973
|
+
var path = _rnGetReportPath(idx, def.key);
|
|
974
|
+
if (!path) continue;
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
var r = await apiFetch('GET', '/api/flexrunner/file?path=' + encodeURIComponent(path), null, 15000);
|
|
978
|
+
if (r.data && r.data.status === 'ok' && r.data.content) {
|
|
979
|
+
var score = _rnParseScoreFromReport(r.data.content);
|
|
980
|
+
if (score != null) _rnSetStepScore(idx, def, score);
|
|
981
|
+
}
|
|
982
|
+
} catch (e) { /* 静默失败 */ }
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
987
|
+
* 终态处理
|
|
988
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
989
|
+
|
|
990
|
+
function _rnOnTerminal(idx, status) {
|
|
991
|
+
var task = _rnTasks[idx];
|
|
992
|
+
if (!task || task._terminalDone) return;
|
|
993
|
+
task._terminalDone = true;
|
|
994
|
+
|
|
995
|
+
_rnStopProgressPoll(idx);
|
|
996
|
+
_rnFinishSSE(idx);
|
|
997
|
+
_rnResolveFiles(idx).then(function () {
|
|
998
|
+
_rnFallbackScores(idx);
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function _rnFinishSSE(idx) {
|
|
1003
|
+
var task = _rnTasks[idx];
|
|
1004
|
+
if (!task) return;
|
|
1005
|
+
if (task.sse) { try { task.sse.close(); } catch (e) {} task.sse = null; }
|
|
1006
|
+
if (task.timer) { clearInterval(task.timer); task.timer = null; }
|
|
1007
|
+
|
|
1008
|
+
_rnRenderElapsed(idx);
|
|
1009
|
+
|
|
1010
|
+
var card = document.getElementById('rn-card-' + idx);
|
|
1011
|
+
if (card) {
|
|
1012
|
+
card.classList.remove('running');
|
|
1013
|
+
var badge = document.getElementById('rn-' + idx + '-badge');
|
|
1014
|
+
if (badge) {
|
|
1015
|
+
if (badge.classList.contains('s-completed')) card.classList.add('cc-done');
|
|
1016
|
+
else if (badge.classList.contains('s-failed') || badge.classList.contains('s-error') || badge.classList.contains('s-deadlocked')) card.classList.add('cc-failed');
|
|
1017
|
+
else if (badge.classList.contains('s-aborted')) card.classList.add('cc-cancelled');
|
|
1018
|
+
}
|
|
1019
|
+
var stopBtn = document.getElementById('rn-' + idx + '-stop');
|
|
1020
|
+
if (stopBtn) stopBtn.style.display = 'none';
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/** 已完成任务:用 startedAt/finishedAt 计算固定时长 */
|
|
1025
|
+
function _rnRenderElapsed(idx) {
|
|
1026
|
+
var task = _rnTasks[idx];
|
|
1027
|
+
if (!task) return;
|
|
1028
|
+
var elapsedEl = document.getElementById('rn-' + idx + '-elapsed');
|
|
1029
|
+
if (!elapsedEl) return;
|
|
1030
|
+
|
|
1031
|
+
var durationMs = 0;
|
|
1032
|
+
if (task.finishedAt && task.startedAt) {
|
|
1033
|
+
durationMs = new Date(task.finishedAt).getTime() - new Date(task.startedAt).getTime();
|
|
1034
|
+
} else if (task.startedAt) {
|
|
1035
|
+
durationMs = Date.now() - new Date(task.startedAt).getTime();
|
|
1036
|
+
} else {
|
|
1037
|
+
durationMs = Date.now() - task.startTime;
|
|
1038
|
+
}
|
|
1039
|
+
if (durationMs < 0) durationMs = 0;
|
|
1040
|
+
var sec = Math.floor(durationMs / 1000);
|
|
1041
|
+
elapsedEl.textContent = sec < 60 ? sec + 's' : Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
|
|
1042
|
+
elapsedEl.style.display = 'inline';
|
|
1043
|
+
|
|
1044
|
+
var timeEl = document.getElementById('rn-' + idx + '-time');
|
|
1045
|
+
if (timeEl && task.startedAt) timeEl.textContent = _rnFormatTime(task.startedAt);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
1049
|
+
* 停止检视
|
|
1050
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
1051
|
+
|
|
1052
|
+
async function stopRNTask(idx) {
|
|
1053
|
+
var task = _rnTasks[idx];
|
|
1054
|
+
if (!task) return;
|
|
1055
|
+
var btn = document.getElementById('rn-' + idx + '-stop');
|
|
1056
|
+
if (btn) { btn.disabled = true; btn.textContent = '取消中…'; }
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
var r = await apiFetch('POST', '/api/orchestrator/abort', { taskId: task.taskId });
|
|
1060
|
+
if (r.status >= 400) {
|
|
1061
|
+
toast('error', '取消失败:' + ((r.data && r.data.error) || r.status));
|
|
1062
|
+
if (btn) { btn.disabled = false; btn.textContent = '■ 取消'; }
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
toast('error', '取消请求异常:' + e.message);
|
|
1067
|
+
if (btn) { btn.disabled = false; btn.textContent = '■ 取消'; }
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
toast('warn', '检视任务已取消');
|
|
1072
|
+
_rnUpdateStage(idx, 'aborted');
|
|
1073
|
+
_rnOnTerminal(idx, 'aborted');
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
1077
|
+
* 报告预览 / 下载
|
|
1078
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
1079
|
+
|
|
1080
|
+
/** stepKey 支持主维度键 + 附属报告键(testcoverageRaw / democoverage);优先专属报告,回退步骤 result .md */
|
|
1081
|
+
function _rnGetReportPath(idx, stepKey) {
|
|
1082
|
+
var task = _rnTasks[idx];
|
|
1083
|
+
if (!task) return null;
|
|
1084
|
+
return task.reportPaths[stepKey] || task.resultPaths[stepKey] || null;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function _rnGetReportTitle(idx, stepKey) {
|
|
1088
|
+
var task = _rnTasks[idx];
|
|
1089
|
+
if (!task) return '检视报告';
|
|
1090
|
+
for (var i = 0; i < _RN_STEP_DEFS.length; i++) {
|
|
1091
|
+
if (_RN_STEP_DEFS[i].key === stepKey) {
|
|
1092
|
+
return _RN_STEP_DEFS[i].name + ' - ' + task.libraryName;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
for (var k in _RN_EXTRA_REPORTS) {
|
|
1096
|
+
var extras = _RN_EXTRA_REPORTS[k];
|
|
1097
|
+
for (var m = 0; m < extras.length; m++) {
|
|
1098
|
+
if (extras[m].key === stepKey) return extras[m].label + ' - ' + task.libraryName;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return '检视报告 - ' + task.libraryName;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
async function viewRNReport(idx, stepKey) {
|
|
1105
|
+
var task = _rnTasks[idx];
|
|
1106
|
+
if (!task) return;
|
|
1107
|
+
if (!task.reportPaths[stepKey]) await _rnResolveFiles(idx);
|
|
1108
|
+
|
|
1109
|
+
var path = _rnGetReportPath(idx, stepKey);
|
|
1110
|
+
if (!path) { toast('warn', '报告尚未生成'); return; }
|
|
1111
|
+
|
|
1112
|
+
try {
|
|
1113
|
+
var r = await apiFetch('GET', '/api/flexrunner/file?path=' + encodeURIComponent(path), null, 20000);
|
|
1114
|
+
if (!r.data || r.data.status !== 'ok') { toast('error', '读取报告失败'); return; }
|
|
1115
|
+
|
|
1116
|
+
var title = _rnGetReportTitle(idx, stepKey);
|
|
1117
|
+
if (typeof MarkdownReader !== 'undefined' && MarkdownReader.open) {
|
|
1118
|
+
MarkdownReader.open(title, r.data.content);
|
|
1119
|
+
} else if (typeof showModal === 'function') {
|
|
1120
|
+
showModal(title, r.data.content);
|
|
1121
|
+
} else {
|
|
1122
|
+
var w = window.open('', '_blank');
|
|
1123
|
+
if (w) {
|
|
1124
|
+
w.document.write('<pre style="white-space:pre-wrap;font-family:monospace;font-size:13px;padding:20px;">' +
|
|
1125
|
+
_rnEsc(r.data.content) + '</pre>');
|
|
1126
|
+
w.document.title = title;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
toast('error', '预览失败: ' + e.message);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function downloadRNReport(idx, stepKey) {
|
|
1135
|
+
var task = _rnTasks[idx];
|
|
1136
|
+
if (!task) return;
|
|
1137
|
+
if (!task.reportPaths[stepKey]) await _rnResolveFiles(idx);
|
|
1138
|
+
|
|
1139
|
+
var path = _rnGetReportPath(idx, stepKey);
|
|
1140
|
+
if (!path) { toast('warn', '报告尚未生成'); return; }
|
|
1141
|
+
|
|
1142
|
+
var filename = path.split(/[/\\]/).pop();
|
|
1143
|
+
try {
|
|
1144
|
+
var r = await fetch('/api/flexrunner/file?path=' + encodeURIComponent(path));
|
|
1145
|
+
if (!r.ok) { toast('error', '下载失败: HTTP ' + r.status); return; }
|
|
1146
|
+
var blob = await r.blob();
|
|
1147
|
+
var url = URL.createObjectURL(blob);
|
|
1148
|
+
var a = document.createElement('a');
|
|
1149
|
+
a.href = url;
|
|
1150
|
+
a.download = filename;
|
|
1151
|
+
a.click();
|
|
1152
|
+
URL.revokeObjectURL(url);
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
toast('error', '下载失败: ' + e.message);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
1159
|
+
* 打包下载全部报告(任务目录下全部 .md → ZIP)
|
|
1160
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
1161
|
+
|
|
1162
|
+
async function downloadRNArchive(idx) {
|
|
1163
|
+
var task = _rnTasks[idx];
|
|
1164
|
+
if (!task) return;
|
|
1165
|
+
|
|
1166
|
+
var btn = document.getElementById('rn-' + idx + '-archive-btn');
|
|
1167
|
+
var restoreBtn = function () {
|
|
1168
|
+
if (btn) {
|
|
1169
|
+
btn.disabled = false;
|
|
1170
|
+
btn.textContent = '📦 下载全部报告' + (task.mdFileCount ? ' (' + task.mdFileCount + ')' : '');
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
if (btn) { btn.disabled = true; btn.textContent = '⏳ 打包中…'; }
|
|
1174
|
+
|
|
1175
|
+
try {
|
|
1176
|
+
var res = await fetch('/api/flexrunner/archive?taskId=' + encodeURIComponent(task.taskId));
|
|
1177
|
+
if (!res.ok) {
|
|
1178
|
+
var body = null;
|
|
1179
|
+
try { body = await res.json(); } catch (e) { /* 非 JSON 响应 */ }
|
|
1180
|
+
toast('warn', (body && (body.error || body.message)) || ('下载失败: HTTP ' + res.status));
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
var blob = await res.blob();
|
|
1185
|
+
var a = document.createElement('a');
|
|
1186
|
+
a.href = URL.createObjectURL(blob);
|
|
1187
|
+
a.download = task.taskId + '.zip';
|
|
1188
|
+
a.style.display = 'none';
|
|
1189
|
+
document.body.appendChild(a);
|
|
1190
|
+
a.click();
|
|
1191
|
+
URL.revokeObjectURL(a.href);
|
|
1192
|
+
document.body.removeChild(a);
|
|
1193
|
+
toast('success', '已打包下载 ' + (task.mdFileCount || '') + ' 个报告文件');
|
|
1194
|
+
} catch (e) {
|
|
1195
|
+
toast('error', '下载失败: ' + e.message);
|
|
1196
|
+
} finally {
|
|
1197
|
+
restoreBtn();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
1202
|
+
* 查看步骤对话(每步一个 opencode Session)
|
|
1203
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
1204
|
+
|
|
1205
|
+
async function viewRNSession(idx, stepId, stepKey) {
|
|
1206
|
+
var task = _rnTasks[idx];
|
|
1207
|
+
if (!task) return;
|
|
1208
|
+
|
|
1209
|
+
var btn = document.getElementById('rn-' + idx + '-session-' + stepKey);
|
|
1210
|
+
if (btn) { btn.disabled = true; btn.textContent = '…'; }
|
|
1211
|
+
|
|
1212
|
+
try {
|
|
1213
|
+
var url = '/api/flexrunner/session?taskId=' + encodeURIComponent(task.taskId) + '&stepId=' + encodeURIComponent(stepId);
|
|
1214
|
+
var r = await apiFetch('GET', url, null, 15000);
|
|
1215
|
+
if (btn) { btn.disabled = false; btn.textContent = '💬'; }
|
|
1216
|
+
|
|
1217
|
+
if (!r.data || r.data.status !== 'ok') {
|
|
1218
|
+
toast('info', '暂无对话记录');
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
var messages = r.data.messages || [];
|
|
1223
|
+
if (messages.length === 0) {
|
|
1224
|
+
toast('info', '暂无对话消息');
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
var title = '对话记录 - ' + task.libraryName + ' 步骤' + stepId;
|
|
1229
|
+
if (typeof showSessionModal === 'function') {
|
|
1230
|
+
showSessionModal(title, messages);
|
|
1231
|
+
} else {
|
|
1232
|
+
var w = window.open('', '_blank');
|
|
1233
|
+
if (w) {
|
|
1234
|
+
var html = '<style>body{font-family:monospace;font-size:13px;padding:20px;max-width:900px;margin:0 auto}</style>';
|
|
1235
|
+
messages.forEach(function (m) {
|
|
1236
|
+
var role = m.role || (m.info && m.info.role) || 'unknown';
|
|
1237
|
+
var content = m.content != null ? m.content : ((m.parts && m.parts[0] && m.parts[0].text) || '');
|
|
1238
|
+
if (!content || content.trim() === '') return;
|
|
1239
|
+
html += '<div style="margin-bottom:16px;border-left:3px solid #ccc;padding-left:12px"><b style="color:' +
|
|
1240
|
+
(role === 'user' ? '#0066cc' : role === 'assistant' ? '#00772e' : '#767676') + '">' + _rnEsc(role) +
|
|
1241
|
+
'</b><pre style="white-space:pre-wrap;margin-top:4px">' + _rnEsc(content) + '</pre></div>';
|
|
1242
|
+
});
|
|
1243
|
+
w.document.write(html);
|
|
1244
|
+
w.document.title = title;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
} catch (e) {
|
|
1248
|
+
if (btn) { btn.disabled = false; btn.textContent = '💬'; }
|
|
1249
|
+
toast('error', '获取对话失败: ' + e.message);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
1254
|
+
* 历史任务(分页,/api/orchestrator/tasks 过滤 rncheck_ 前缀)
|
|
1255
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* 渲染一页历史任务卡片。
|
|
1259
|
+
* 数据全部来自列表响应(参考 prcheck:零逐任务请求):
|
|
1260
|
+
* - status:终态由后端在任务结束时回写注册表
|
|
1261
|
+
* - metadata:各步骤 meta .json 收割合并(stabilityScore / consistencyScore / ...)
|
|
1262
|
+
* 仅活跃状态(running/pending/ready/loaded/idle)的任务才连接 SSE + 轮询。
|
|
1263
|
+
*/
|
|
1264
|
+
function _rnRenderHistoryPage(tasks, isFirstPage) {
|
|
1265
|
+
// 过滤出 rncheck_ 前缀的任务
|
|
1266
|
+
var rnTasks = tasks.filter(function (t) {
|
|
1267
|
+
var tid = t.taskId || t.task_id || '';
|
|
1268
|
+
return tid.startsWith('rncheck_');
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
var emptyEl = $('rn-tasks-empty');
|
|
1272
|
+
// 分页器会清空列表容器 innerHTML,空提示元素可能被移除,需要时重建
|
|
1273
|
+
if (!emptyEl) {
|
|
1274
|
+
var listEl = $('rn-tasks-list');
|
|
1275
|
+
if (listEl) {
|
|
1276
|
+
emptyEl = document.createElement('div');
|
|
1277
|
+
emptyEl.id = 'rn-tasks-empty';
|
|
1278
|
+
emptyEl.className = 'cc-empty-hint';
|
|
1279
|
+
emptyEl.textContent = '暂无检视任务,提交检视参数后将在此显示';
|
|
1280
|
+
listEl.appendChild(emptyEl);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (emptyEl) emptyEl.style.display = rnTasks.length > 0 ? 'none' : '';
|
|
1284
|
+
|
|
1285
|
+
// 计数按过滤后的渲染卡片数显示(注册表 total 含其他类型任务)
|
|
1286
|
+
var countEl2 = $('rn-task-count');
|
|
1287
|
+
if (countEl2) countEl2.textContent = Object.keys(_rnTasks).length + ' 个任务';
|
|
1288
|
+
|
|
1289
|
+
// 后端按 created_at 降序返回(最新在前);卡片用 insertBefore 前插,
|
|
1290
|
+
// 反转遍历:最旧的先渲染、被前插到下面 → 最新的最终在最上面
|
|
1291
|
+
rnTasks.reverse();
|
|
1292
|
+
for (var i = 0; i < rnTasks.length; i++) {
|
|
1293
|
+
var t = rnTasks[i];
|
|
1294
|
+
var tid = t.taskId || t.task_id;
|
|
1295
|
+
var status = t.status || 'idle';
|
|
1296
|
+
var startTime = t.created_at || t.createdAt || t.startTime;
|
|
1297
|
+
var endTime = t.endTime || (t.progress && t.progress.endTime);
|
|
1298
|
+
|
|
1299
|
+
// 从 description(body.title)解析双仓库信息
|
|
1300
|
+
// 格式: RN仓库检视|{libraryName}|{ohosRepoUrl}|{upstreamRepoUrl}
|
|
1301
|
+
var desc = t.description || t.title || '';
|
|
1302
|
+
var libraryName = '', ohosRepoUrl = '', upstreamRepoUrl = '';
|
|
1303
|
+
if (desc.indexOf('RN仓库检视|') === 0) {
|
|
1304
|
+
var segs = desc.substring('RN仓库检视|'.length).split('|');
|
|
1305
|
+
libraryName = segs[0] || '';
|
|
1306
|
+
ohosRepoUrl = segs[1] || '';
|
|
1307
|
+
upstreamRepoUrl = segs[2] || '';
|
|
1308
|
+
} else if (desc) {
|
|
1309
|
+
libraryName = desc;
|
|
1310
|
+
}
|
|
1311
|
+
if (!libraryName) libraryName = _rnTaskIdToLibraryName(tid);
|
|
1312
|
+
|
|
1313
|
+
var idx = ++_rnCounter;
|
|
1314
|
+
_rnTasks[idx] = _rnNewTaskState(tid, libraryName, ohosRepoUrl, upstreamRepoUrl, '', '');
|
|
1315
|
+
_rnTasks[idx].startTime = startTime ? new Date(startTime).getTime() : Date.now();
|
|
1316
|
+
_rnTasks[idx].startedAt = startTime || null;
|
|
1317
|
+
_rnTasks[idx].finishedAt = endTime || null;
|
|
1318
|
+
|
|
1319
|
+
_rnCreateCard(idx, startTime, true);
|
|
1320
|
+
_rnUpdateStage(idx, status);
|
|
1321
|
+
|
|
1322
|
+
// 终态任务:从注册表 metadata 回填各维度评分(参考 prcheck 的 mergeLevel 回填)
|
|
1323
|
+
var meta = (t.metadata && typeof t.metadata === 'object') ? t.metadata : null;
|
|
1324
|
+
if (meta) {
|
|
1325
|
+
var keyMap = {
|
|
1326
|
+
stabilityScore: 'stability',
|
|
1327
|
+
consistencyScore: 'consistency',
|
|
1328
|
+
memoryLeakScore: 'memoryleak',
|
|
1329
|
+
documentationScore: 'documentation',
|
|
1330
|
+
testCoverageScore: 'testcoverage',
|
|
1331
|
+
knownIssueScore: 'knownissue',
|
|
1332
|
+
};
|
|
1333
|
+
for (var scoreKey in keyMap) {
|
|
1334
|
+
var dimKey = keyMap[scoreKey];
|
|
1335
|
+
var v = Number(meta[scoreKey]);
|
|
1336
|
+
if (isFinite(v)) {
|
|
1337
|
+
var def = null;
|
|
1338
|
+
for (var d = 0; d < _RN_STEP_DEFS.length; d++) {
|
|
1339
|
+
if (_RN_STEP_DEFS[d].key === dimKey) { def = _RN_STEP_DEFS[d]; break; }
|
|
1340
|
+
}
|
|
1341
|
+
if (def) {
|
|
1342
|
+
_rnSetStepScore(idx, def, Math.max(0, Math.min(100, Math.round(v))));
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// 活跃任务才连接 SSE + 轮询;终态任务折叠展示
|
|
1349
|
+
var activeStages = ['running', 'pending', 'ready', 'loaded', 'idle'];
|
|
1350
|
+
if (activeStages.indexOf(status) >= 0) {
|
|
1351
|
+
_rnConnectSSE(idx);
|
|
1352
|
+
_rnStartProgressPoll(idx);
|
|
1353
|
+
} else {
|
|
1354
|
+
_rnFinishSSE(idx);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function _rnInitHistoryPager() {
|
|
1360
|
+
if (_rnPager) return Promise.resolve(_rnPager);
|
|
1361
|
+
_rnPager = createPagination({
|
|
1362
|
+
// taskIdPrefix 前缀过滤:只分页查询 rncheck_ 任务(后端 SQL 过滤,
|
|
1363
|
+
// 避免与 codecheck / fluttercheck 等其他编排任务混页)
|
|
1364
|
+
apiPath: '/api/orchestrator/tasks?taskIdPrefix=rncheck_',
|
|
1365
|
+
listContainerId: 'rn-tasks-list',
|
|
1366
|
+
emptyId: 'rn-tasks-empty',
|
|
1367
|
+
countId: 'rn-task-count',
|
|
1368
|
+
pagerId: 'rn-tasks-pager',
|
|
1369
|
+
renderPage: _rnRenderHistoryPage,
|
|
1370
|
+
pageSize: 20,
|
|
1371
|
+
});
|
|
1372
|
+
return Promise.resolve(_rnPager);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/* ── 注册面板初始化 ──────────────────────────────────────────────────── */
|
|
1376
|
+
if (typeof Nav !== 'undefined' && Nav.register) {
|
|
1377
|
+
Nav.register('p-rnlibrarycheck', initRNLibraryCheckPanel);
|
|
1378
|
+
}
|