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