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