@ohos-cpf/3rdloop 0.0.5 → 0.0.7
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 +0 -1
- package/lib/cli.js +4 -5
- package/lib/serve.js +14 -2
- package/lib/update.js +6 -45
- package/package.json +1 -1
- package/vendor/Server/CLI/opencode/index.js +3 -3
- package/vendor/Server/DbUse/StorageManager.js +7 -1
- package/vendor/Server/FlexRunner/FlexRunner.js +10 -10
- package/vendor/Server/Orchestrator/Orchestrator.js +39 -0
- package/vendor/Server/Routes/controllers/FlexRunnerController.js +92 -0
- package/vendor/Server/Routes/controllers/OrchestratorController.js +92 -4
- package/vendor/Server/Routes/routes/flexrunner.js +2 -0
- package/vendor/Server/TestCheck/TestCheck.js +1 -1
- package/vendor/VERSION +3 -3
- package/web/js/arktslibrarycheck.js +82 -0
- package/web/js/codecheck.js +59 -0
- package/web/js/flutterlibrarycheck.js +84 -1
- package/web/js/rnlibrarycheck.js +85 -1
package/README.md
CHANGED
|
@@ -260,7 +260,6 @@ LLM 凭据变量(`DASHSCOPE_API_KEY` / `LLM_MODEL` 等):按上文场景二
|
|
|
260
260
|
3rdloop update --registry https://registry.npmjs.org/
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
-
- 以 `npm link` 开发方式安装(连接到仓库源码)时,`update` 默认跳过并提示用 `git pull` 更新源码,加 `--force` 可强制替换为 registry 正式包
|
|
264
263
|
- 更新只替换程序文件,**数据目录** `~/.3lib/3rdloop/db` **与** `~/.3lib/.env` **配置不受影响**
|
|
265
264
|
|
|
266
265
|
|
package/lib/cli.js
CHANGED
|
@@ -145,7 +145,7 @@ export async function runCli(argv) {
|
|
|
145
145
|
return cmdConfig({ flags, rest, projectRoot, user3libHome, jsonMode, out });
|
|
146
146
|
|
|
147
147
|
case 'serve':
|
|
148
|
-
return cmdServe({ flags, rest, jsonMode });
|
|
148
|
+
return cmdServe({ flags, rest, jsonMode, dataDir });
|
|
149
149
|
|
|
150
150
|
case 'update':
|
|
151
151
|
return cmdUpdate({ flags, rest, jsonMode, out });
|
|
@@ -226,9 +226,9 @@ async function cmdUpdate({ flags, rest, jsonMode, out }) {
|
|
|
226
226
|
|
|
227
227
|
// ─── serve(启动 Server HTTP 后端,供前端展示)─────────────────────────
|
|
228
228
|
|
|
229
|
-
async function cmdServe({ flags, rest, jsonMode }) {
|
|
229
|
+
async function cmdServe({ flags, rest, jsonMode, dataDir }) {
|
|
230
230
|
const { cmdServe: impl } = await import('./serve.js');
|
|
231
|
-
return impl({ rest: flags.rest, jsonMode });
|
|
231
|
+
return impl({ rest: flags.rest, jsonMode, dataDir });
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
// ─── run / submit ──────────────────────────────────────────────────
|
|
@@ -660,7 +660,7 @@ function printHelp() {
|
|
|
660
660
|
3rdloop run <工作流名> [--flag <值> ...] 以固定编排执行工作流(阻塞到终态)
|
|
661
661
|
3rdloop config list|get|set|unset ... 配置 .env 配置项(默认读写 ~/.3lib/.env)
|
|
662
662
|
3rdloop serve [--port N] [-w|--web] [--web-port N] 启动 Server HTTP 后端(供前端展示)
|
|
663
|
-
3rdloop update [--check] [--yes] [--
|
|
663
|
+
3rdloop update [--check] [--yes] [--registry <url>] 自更新到 npm 最新版
|
|
664
664
|
3rdloop web [--port N] [--backend <url>] 启动 Web 前端服务
|
|
665
665
|
3rdloop web install-ext <dir> 安装扩展(tag/issue/prcheck 等二级功能)
|
|
666
666
|
3rdloop web list-ext | remove-ext <name> 扩展管理
|
|
@@ -687,7 +687,6 @@ config 子命令:
|
|
|
687
687
|
update 专用选项:
|
|
688
688
|
--check 仅检查是否有新版本,不执行更新
|
|
689
689
|
--yes, -y 跳过交互确认直接更新
|
|
690
|
-
--force 开发态(npm link)也强制替换为 registry 正式包
|
|
691
690
|
--registry <url> 覆盖 npm registry(默认尊重用户 npm 镜像配置)
|
|
692
691
|
注意: 数据目录 ~/.3lib/3rdloop/db 与 SKILL 配置不受更新影响
|
|
693
692
|
|
package/lib/serve.js
CHANGED
|
@@ -88,9 +88,10 @@ function _parseServeArgs(rest = []) {
|
|
|
88
88
|
* @param {object} opts
|
|
89
89
|
* @param {string[]} opts.rest - flags.rest(含 serve 专用参数)
|
|
90
90
|
* @param {boolean} opts.jsonMode
|
|
91
|
+
* @param {string} [opts.dataDir] - CLI 解析后的数据目录(--data-dir / 3LIB_DATA_DIR / 默认)
|
|
91
92
|
* @returns {Promise<number>} 退出码(serve 为长驻命令,仅 --help/启动失败正常返回)
|
|
92
93
|
*/
|
|
93
|
-
export async function cmdServe({ rest = [], jsonMode = false }) {
|
|
94
|
+
export async function cmdServe({ rest = [], jsonMode = false, dataDir = '' }) {
|
|
94
95
|
// --help / -h:打印用法
|
|
95
96
|
if (rest.includes('--help') || rest.includes('-h')) {
|
|
96
97
|
_printServeHelp();
|
|
@@ -106,6 +107,15 @@ export async function cmdServe({ rest = [], jsonMode = false }) {
|
|
|
106
107
|
return 1;
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
// ── 数据目录对齐 CLI(A3):与 run/submit 共用同一 dataDir ──────
|
|
111
|
+
// 通过 env 注入而非逐层传参:Server 内部所有自建 createStorage()
|
|
112
|
+
// 都经 StorageManager 默认根目录解析,env 一处生效(含 FlexRunner /
|
|
113
|
+
// KnowledgeManager / MessageArchive 等惰性自建实例)。
|
|
114
|
+
// 必须在 import Server 模块前设置(覆盖部分模块加载期构造的实例)。
|
|
115
|
+
if (dataDir) {
|
|
116
|
+
process.env['3LIB_DATA_DIR'] = dataDir;
|
|
117
|
+
}
|
|
118
|
+
|
|
109
119
|
const serverModule = await _importServerModule();
|
|
110
120
|
|
|
111
121
|
// json 模式下纯化 stdout:Server 内部 console.log/info 重定向到 stderr
|
|
@@ -141,7 +151,9 @@ export async function cmdServe({ rest = [], jsonMode = false }) {
|
|
|
141
151
|
|
|
142
152
|
try {
|
|
143
153
|
// cliType 由环境决定(shell > .env > 默认 opencode),与 CLI 嵌入式一致;
|
|
144
|
-
// dataDir
|
|
154
|
+
// dataDir 已通过 3LIB_DATA_DIR env 注入(见上),Server 内部所有默认
|
|
155
|
+
// createStorage() 均落到用户数据目录;skillDir 沿用包内 vendor 默认
|
|
156
|
+
// (npm 更新自动获得新 SKILL,不与 CLI 的 3LIB_SKILL_DIR 联动)
|
|
145
157
|
const server = await serverModule.createServer({ port: serverPort });
|
|
146
158
|
|
|
147
159
|
if (jsonMode) {
|
package/lib/update.js
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 职责:
|
|
5
5
|
* - 检测本地版本(getCliVersion)与 npm registry 最新版
|
|
6
|
-
* -
|
|
7
|
-
* - 交互确认后通过 npm install -g 重装
|
|
6
|
+
* - 交互确认后卸载旧版 + npm install -g 重装最新版
|
|
8
7
|
*
|
|
9
8
|
* 交互/输出契约:
|
|
10
9
|
* - 进度/日志 → stderr;--json 结构化结果 → stdout
|
|
@@ -17,7 +16,6 @@
|
|
|
17
16
|
* 该分支对用户可控的 --registry 值做白名单校验防 shell 注入
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
|
-
import fs from 'node:fs';
|
|
21
19
|
import path from 'node:path';
|
|
22
20
|
import readline from 'node:readline';
|
|
23
21
|
import { execFileSync } from 'node:child_process';
|
|
@@ -26,7 +24,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
26
24
|
// 本文件在 cli/lib/ 下
|
|
27
25
|
const CLI_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
26
|
const PKG_NAME = '@ohos-cpf/3rdloop';
|
|
29
|
-
const BIN_ABS = path.join(CLI_ROOT, 'bin', '3rdloop.mjs');
|
|
30
27
|
|
|
31
28
|
// ── npm 调用封装 ─────────────────────────────────────────────────
|
|
32
29
|
|
|
@@ -92,22 +89,6 @@ export function compareVersions(a, b) {
|
|
|
92
89
|
return String(preA).localeCompare(String(preB));
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
/**
|
|
96
|
-
* 检测当前命令是否为 npm link 软链(开发态)。
|
|
97
|
-
*
|
|
98
|
-
* 原理:npm link 的全局 bin 是指向仓库 bin 的软链;真实全局安装则是
|
|
99
|
-
* node_modules 内的副本。比较 process.argv[1] 与仓库内 bin 的 realpath 是否相同。
|
|
100
|
-
*/
|
|
101
|
-
export function detectLinkMode(argv1 = process.argv[1]) {
|
|
102
|
-
try {
|
|
103
|
-
const realArgv = fs.realpathSync(argv1 || '');
|
|
104
|
-
const realLocal = fs.realpathSync(BIN_ABS);
|
|
105
|
-
return realArgv === realLocal;
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
92
|
/**
|
|
112
93
|
* 获取最新版本。
|
|
113
94
|
* 优先 `npm view`(尊重用户 .npmrc 镜像配置);失败回退 packument API。
|
|
@@ -171,33 +152,15 @@ export async function confirmUpdate({ current, latest } = {}) {
|
|
|
171
152
|
}
|
|
172
153
|
|
|
173
154
|
/**
|
|
174
|
-
*
|
|
155
|
+
* 执行更新:卸载旧版后通过 npm install -g 重装最新版。
|
|
175
156
|
* @param {object} opts
|
|
176
157
|
* @param {string} opts.latest 目标版本(registry latest)
|
|
177
158
|
* @param {string} [opts.registry] registry 覆盖
|
|
178
|
-
* @param {boolean} [opts.force] 开发态也强制替换
|
|
179
|
-
* @param {boolean} [opts.isLink] 是否 link 模式(可注入便于测试)
|
|
180
159
|
*/
|
|
181
|
-
export function performUpdate({ latest, registry
|
|
160
|
+
export function performUpdate({ latest, registry } = {}) {
|
|
182
161
|
const installArgs = ['install', '-g', `${PKG_NAME}@latest`];
|
|
183
162
|
if (registry) installArgs.push('--registry', registry);
|
|
184
163
|
|
|
185
|
-
// 开发态(npm link):默认警告并跳过,仅提示 git pull
|
|
186
|
-
if (isLink && !force) {
|
|
187
|
-
process.stderr.write(
|
|
188
|
-
`\n[update] 检测到当前为开发态(npm link),已跳过自更新。\n` +
|
|
189
|
-
` 开发态请用 git pull 更新仓库源码(数据目录 ~/.3lib/3rdloop/db 不受影响)。\n` +
|
|
190
|
-
` 如需强制用正式包替换链接,可加 --force。\n`
|
|
191
|
-
);
|
|
192
|
-
return { updated: false, skipped: 'link-mode' };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// 开发态 + --force:先解除链接
|
|
196
|
-
if (isLink && force) {
|
|
197
|
-
process.stderr.write(`[update] 检测到 npm link,先解除链接...\n`);
|
|
198
|
-
runNpm(['unlink', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
164
|
process.stderr.write(`[update] 卸载旧版本(@${PKG_NAME})...\n`);
|
|
202
165
|
try {
|
|
203
166
|
runNpm(['uninstall', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
|
|
@@ -219,19 +182,17 @@ export function performUpdate({ latest, registry, force = false, isLink = detect
|
|
|
219
182
|
export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
|
|
220
183
|
const checkOnly = rest.includes('--check');
|
|
221
184
|
const yes = rest.includes('--yes') || rest.includes('-y');
|
|
222
|
-
const force = rest.includes('--force');
|
|
223
185
|
const regIdx = rest.indexOf('--registry');
|
|
224
186
|
const registry = regIdx !== -1 ? rest[regIdx + 1] : undefined;
|
|
225
187
|
|
|
226
188
|
// update 用法说明(--help,离线可测,不触碰网络)
|
|
227
189
|
if (rest.includes('--help') || rest.includes('-h')) {
|
|
228
190
|
const help = [
|
|
229
|
-
'用法: 3rdloop update [--check] [--yes] [--
|
|
191
|
+
'用法: 3rdloop update [--check] [--yes] [--registry <url>]',
|
|
230
192
|
'',
|
|
231
193
|
' 默认: 检测本地版本与 npm registry 最新版,差异存在时交互确认后重装',
|
|
232
194
|
' --check 仅检查是否有新版本,不执行更新',
|
|
233
195
|
' --yes, -y 跳过交互确认直接更新(非 TTY 必须)',
|
|
234
|
-
' --force 开发态(npm link)也强制替换为 registry 正式包',
|
|
235
196
|
' --registry <url> 覆盖 npm registry(默认尊重用户 npm 镜像配置)',
|
|
236
197
|
' 注意: 数据目录 ~/.3lib/3rdloop/db 与 SKILL 配置不受更新影响',
|
|
237
198
|
].join('\n');
|
|
@@ -268,7 +229,7 @@ export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
|
|
|
268
229
|
if (!checkOnly) {
|
|
269
230
|
// --json 下不交互,直接更新(用户显式要求命令行为,等价 --yes)
|
|
270
231
|
try {
|
|
271
|
-
const r = performUpdate({ latest, registry
|
|
232
|
+
const r = performUpdate({ latest, registry });
|
|
272
233
|
process.stdout.write(JSON.stringify({ ...r, ok: true }, null, 2) + '\n');
|
|
273
234
|
return { ok: true, ...r };
|
|
274
235
|
} catch (err) {
|
|
@@ -310,7 +271,7 @@ export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
|
|
|
310
271
|
|
|
311
272
|
process.stderr.write(`\n`);
|
|
312
273
|
try {
|
|
313
|
-
const r = performUpdate({ latest, registry
|
|
274
|
+
const r = performUpdate({ latest, registry });
|
|
314
275
|
return { ok: true, ...r };
|
|
315
276
|
} catch (err) {
|
|
316
277
|
const msg = `更新失败: ${err.message}`;
|
package/package.json
CHANGED
|
@@ -388,7 +388,7 @@ class OpenCodeCLI {
|
|
|
388
388
|
try {
|
|
389
389
|
statusMap = await this.client.session.status({ query: {} });
|
|
390
390
|
} catch (e) {
|
|
391
|
-
this._log('
|
|
391
|
+
this._log('debug', `checkSessionStatus: session.status() 失败,降级为拉消息: ${e.message}`);
|
|
392
392
|
}
|
|
393
393
|
const rawStatus = statusMap?.data?.[sessionId];
|
|
394
394
|
if (rawStatus && (rawStatus.type === 'busy' || rawStatus.type === 'retry')) {
|
|
@@ -487,7 +487,7 @@ class OpenCodeCLI {
|
|
|
487
487
|
hasNewMessage: !!checkRes.hasNewMessage
|
|
488
488
|
};
|
|
489
489
|
} catch (error) {
|
|
490
|
-
this._log('
|
|
490
|
+
this._log('debug', `checkSessionStatus 异常 id=${sessionId}: ${error.message}`);
|
|
491
491
|
return { status: 'unknown' };
|
|
492
492
|
}
|
|
493
493
|
}
|
|
@@ -639,7 +639,7 @@ class OpenCodeCLI {
|
|
|
639
639
|
sessionId,
|
|
640
640
|
data.map(m => this._toArchiveMessage(m))
|
|
641
641
|
);
|
|
642
|
-
this._log('
|
|
642
|
+
this._log('debug',
|
|
643
643
|
`自动归档 id=${sessionId} reason=${reason} — appended=${archRes.appended} skipped=${archRes.skipped}`);
|
|
644
644
|
} catch (e) {
|
|
645
645
|
this._log('warn', `自动归档失败 id=${sessionId} reason=${reason}: ${e.message}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, dirname } from 'node:path';
|
|
1
|
+
import { join, dirname, resolve } from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
|
|
4
4
|
import JsonAdapter from './adapters/JsonAdapter.js';
|
|
@@ -36,6 +36,12 @@ class StorageManager {
|
|
|
36
36
|
constructor(options = {}) {
|
|
37
37
|
if (options.rootDir) {
|
|
38
38
|
this.rootDir = options.rootDir;
|
|
39
|
+
} else if (process.env['3LIB_DATA_DIR']) {
|
|
40
|
+
// CLI 数据目录策略(与 WEB_STATS_ROOT 同模式):
|
|
41
|
+
// 3rdloop serve/run 等命令将解析后的数据目录注入 env,
|
|
42
|
+
// 使内部所有自建 createStorage()(FlexRunner/KnowledgeManager/
|
|
43
|
+
// MessageArchive 等)统一落到用户数据目录,而非模块相对路径。
|
|
44
|
+
this.rootDir = resolve(process.env['3LIB_DATA_DIR']);
|
|
39
45
|
} else {
|
|
40
46
|
// 本文件 (Server/DbUse/) 上溯 3 层到 workspace 根 → db/
|
|
41
47
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -846,7 +846,7 @@ class FlexRunner {
|
|
|
846
846
|
return false;
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
-
this._log('
|
|
849
|
+
this._log('debug', `invokeSkill 完成 — sessionId: ${sessionId}`);
|
|
850
850
|
return true;
|
|
851
851
|
} catch (err) {
|
|
852
852
|
this._log('error', `invokeSkill 异常: ${err.message}`);
|
|
@@ -907,18 +907,18 @@ class FlexRunner {
|
|
|
907
907
|
|
|
908
908
|
try {
|
|
909
909
|
const content = await fs.readFile(summaryFilePath, 'utf-8');
|
|
910
|
-
this._log('
|
|
910
|
+
this._log('debug', `persistKnowledge: 已读取经验文件 (${content.length} 字符)`);
|
|
911
911
|
|
|
912
912
|
// 1. 将经验内容持久化到步骤记录
|
|
913
913
|
await this._persistStep({ knowledgeSummary: content });
|
|
914
|
-
this._log('
|
|
914
|
+
this._log('debug', 'persistKnowledge: 经验已持久化到步骤记录');
|
|
915
915
|
|
|
916
916
|
// 2. 登记到知识沉淀系统
|
|
917
917
|
if (this.#knowledgeManager) {
|
|
918
918
|
// 避免重复登记:先检查是否已存在
|
|
919
919
|
const existing = await this.#knowledgeManager.findByDocPath(summaryFilePath);
|
|
920
920
|
if (existing) {
|
|
921
|
-
this._log('
|
|
921
|
+
this._log('debug', `persistKnowledge: 经验已登记过,跳重复登记 — id: ${existing.id}`);
|
|
922
922
|
} else {
|
|
923
923
|
const record = await this.#knowledgeManager.addExperience({
|
|
924
924
|
docPath: summaryFilePath,
|
|
@@ -929,7 +929,7 @@ class FlexRunner {
|
|
|
929
929
|
skillDir: this.#matchedSkillDir,
|
|
930
930
|
taskDescription: this.#taskDescription
|
|
931
931
|
});
|
|
932
|
-
this._log('
|
|
932
|
+
this._log('debug', `persistKnowledge: 经验已登记到知识沉淀系统 — id: ${record.id}`);
|
|
933
933
|
}
|
|
934
934
|
} else {
|
|
935
935
|
this._log('warn', 'persistKnowledge: KnowledgeManager 未初始化,跳过知识沉淀登记');
|
|
@@ -1112,12 +1112,12 @@ class FlexRunner {
|
|
|
1112
1112
|
const value = raw.replace(/^[#>*\-\s]+/, '').trim();
|
|
1113
1113
|
|
|
1114
1114
|
if (value === '成功') {
|
|
1115
|
-
this._log('
|
|
1115
|
+
this._log('debug', `checkResult: 执行状态 = 成功 (${resultFilePath})`);
|
|
1116
1116
|
return true;
|
|
1117
1117
|
}
|
|
1118
1118
|
|
|
1119
1119
|
if (value === '失败') {
|
|
1120
|
-
this._log('
|
|
1120
|
+
this._log('error', `checkResult: 执行状态 = 失败 (${resultFilePath})`);
|
|
1121
1121
|
return false;
|
|
1122
1122
|
}
|
|
1123
1123
|
|
|
@@ -1578,7 +1578,7 @@ class FlexRunner {
|
|
|
1578
1578
|
|
|
1579
1579
|
// 等待 AI 回复期间收到终止请求 → 不再等待文件生成(session 已被远程中止)
|
|
1580
1580
|
if (this.#aborted) {
|
|
1581
|
-
this._log('
|
|
1581
|
+
this._log('warn', '_summarizeExperience: 等待回复期间被终止,放弃经验总结');
|
|
1582
1582
|
return false;
|
|
1583
1583
|
}
|
|
1584
1584
|
|
|
@@ -1587,7 +1587,7 @@ class FlexRunner {
|
|
|
1587
1587
|
return false;
|
|
1588
1588
|
}
|
|
1589
1589
|
|
|
1590
|
-
this._log('
|
|
1590
|
+
this._log('debug',
|
|
1591
1591
|
`_summarizeExperience: AI 回复完成,content 长度 ${result.content?.length || 0},` +
|
|
1592
1592
|
`等待 Summary 文件生成…`
|
|
1593
1593
|
);
|
|
@@ -1601,7 +1601,7 @@ class FlexRunner {
|
|
|
1601
1601
|
return false;
|
|
1602
1602
|
}
|
|
1603
1603
|
|
|
1604
|
-
this._log('
|
|
1604
|
+
this._log('debug', `经验总结完成 — Summary 文件已生成: ${summaryFilePath}`);
|
|
1605
1605
|
return true;
|
|
1606
1606
|
} catch (err) {
|
|
1607
1607
|
this._log('error', `_summarizeExperience 异常: ${err.message}`);
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import path from 'node:path';
|
|
39
|
+
import { hostname } from 'node:os';
|
|
39
40
|
|
|
40
41
|
import { FlexRunner } from '../FlexRunner/FlexRunner.js';
|
|
41
42
|
import { StepNode, NodeStatus } from './StepNode.js';
|
|
@@ -409,6 +410,10 @@ class Orchestrator {
|
|
|
409
410
|
this._startTime = nowCST();
|
|
410
411
|
this._emit(SSE_EVENT.TASK_START, this.getProgress());
|
|
411
412
|
await this._persistState();
|
|
413
|
+
// 写入属主标记(pid/hostname):标识本编排由当前进程执行。
|
|
414
|
+
// 服务重启时 restoreFromDisk 据此判定 running 任务的归属——
|
|
415
|
+
// 属主进程仍存活(如 3rdloop run 嵌入式 CLI)则跳过,不误标 FAILED。
|
|
416
|
+
await this._writeOwnerFile();
|
|
412
417
|
|
|
413
418
|
try {
|
|
414
419
|
// 初始就绪:将所有入口步骤标记为 ready
|
|
@@ -460,6 +465,8 @@ class Orchestrator {
|
|
|
460
465
|
|
|
461
466
|
this._log('info', `── 编排执行结束: status=${this._status} ──`);
|
|
462
467
|
await this._persistState();
|
|
468
|
+
// 已到终态,属主标记失效,清理(先持久化终态再清理,崩溃窗口内重启仍可正确判定)
|
|
469
|
+
await this._deleteOwnerFile();
|
|
463
470
|
return { status: this._status, summary: this.getProgress() };
|
|
464
471
|
}
|
|
465
472
|
|
|
@@ -1275,6 +1282,38 @@ class Orchestrator {
|
|
|
1275
1282
|
await this._persistChain;
|
|
1276
1283
|
}
|
|
1277
1284
|
|
|
1285
|
+
/**
|
|
1286
|
+
* 写入属主标记文件(db/task/{taskId}/owner.json)。
|
|
1287
|
+
*
|
|
1288
|
+
* 记录执行本编排的进程 pid 与 hostname。服务重启时
|
|
1289
|
+
* OrchestratorController.restoreFromDisk 据此判定 running 状态任务的归属:
|
|
1290
|
+
* 属主进程仍存活(如 3rdloop run 嵌入式 CLI 在独立进程执行)则跳过恢复,
|
|
1291
|
+
* 不标记 FAILED、不写 SQLite 中断记录、不回写注册表。
|
|
1292
|
+
* 静默失败,不影响编排主流程。
|
|
1293
|
+
* @private
|
|
1294
|
+
*/
|
|
1295
|
+
async _writeOwnerFile() {
|
|
1296
|
+
if (!this.storage || !this._taskId) return;
|
|
1297
|
+
try {
|
|
1298
|
+
await this.storage.json('task').write(`${this._taskId}/owner`, {
|
|
1299
|
+
pid: process.pid,
|
|
1300
|
+
hostname: hostname(),
|
|
1301
|
+
wroteAt: new Date().toISOString()
|
|
1302
|
+
});
|
|
1303
|
+
} catch { /* 静默失败 */ }
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* 删除属主标记文件(编排到终态时调用)。
|
|
1308
|
+
* @private
|
|
1309
|
+
*/
|
|
1310
|
+
async _deleteOwnerFile() {
|
|
1311
|
+
if (!this.storage || !this._taskId) return;
|
|
1312
|
+
try {
|
|
1313
|
+
await this.storage.json('task').delete(`${this._taskId}/owner`);
|
|
1314
|
+
} catch { /* 静默失败 */ }
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1278
1317
|
/**
|
|
1279
1318
|
* 从持久化文件恢复编排状态。
|
|
1280
1319
|
*
|
|
@@ -776,6 +776,77 @@ class FlexRunnerController {
|
|
|
776
776
|
}
|
|
777
777
|
}
|
|
778
778
|
|
|
779
|
+
/**
|
|
780
|
+
* POST /api/flexrunner/clean-source
|
|
781
|
+
* 删除任务目录下的源码克隆目录({taskDir}/source/),释放存储空间。
|
|
782
|
+
*
|
|
783
|
+
* 仓库检视(ArkTS/Flutter/RN)与 PR 代码检视(arkts/flutter/rn-code-check)均把
|
|
784
|
+
* 仓库源码克隆到任务目录的 source/ 固定路径;检视结束后源码不再被引用
|
|
785
|
+
* (检视报告 .md、meta .json、orchestration.json 等过程文件均在 taskDir 根目录),
|
|
786
|
+
* 由前端在任务终态时调用本接口清理。
|
|
787
|
+
*
|
|
788
|
+
* 安全约束:
|
|
789
|
+
* - 只删除固定子目录名 source(不接受任意路径参数,防误删报告/过程文件)
|
|
790
|
+
* - taskDir 经 _resolveTaskDir 解析(内存实例 → 编排元信息 → 磁盘白名单兜底)
|
|
791
|
+
* - 内存中编排器仍有步骤执行(pending/ready/running)时拒绝清理
|
|
792
|
+
* - source/ 不存在时幂等返回 cleaned=false(如 tpc-cpp-check 不克隆源码)
|
|
793
|
+
*
|
|
794
|
+
* Body: { taskId }
|
|
795
|
+
*/
|
|
796
|
+
async cleanSource(req, res) {
|
|
797
|
+
const body = req.body || {};
|
|
798
|
+
const { taskId } = body;
|
|
799
|
+
if (!taskId) {
|
|
800
|
+
return res.error(400, 'taskId 为必填项');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
// 活跃任务保护:内存中编排器仍有步骤在执行时拒绝(防止误删执行中正在使用的源码)
|
|
805
|
+
if (this._orchestratorController) {
|
|
806
|
+
try {
|
|
807
|
+
const steps = this._orchestratorController.getTaskSteps(taskId);
|
|
808
|
+
const active = steps.some(s =>
|
|
809
|
+
['pending', 'ready', 'running'].includes(String(s.status || '').toLowerCase()));
|
|
810
|
+
if (active) {
|
|
811
|
+
return res.error(409, '任务仍在执行中,暂不能清理源码');
|
|
812
|
+
}
|
|
813
|
+
} catch { /* 内存无实例(已结束 / 重启后磁盘恢复)→ 继续清理 */ }
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const taskDir = await this._resolveTaskDir(taskId);
|
|
817
|
+
if (!taskDir) {
|
|
818
|
+
return res.error(404, `未找到任务目录: taskId=${taskId}`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const sourceDir = path.join(taskDir, 'source');
|
|
822
|
+
|
|
823
|
+
let st = null;
|
|
824
|
+
try { st = await fs.stat(sourceDir); } catch { /* 目录不存在 */ }
|
|
825
|
+
if (!st || !st.isDirectory()) {
|
|
826
|
+
return res.json({
|
|
827
|
+
status: 'ok',
|
|
828
|
+
cleaned: false,
|
|
829
|
+
freedBytes: 0,
|
|
830
|
+
message: '任务目录下无源码目录(source/ 不存在),无需清理',
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// 统计释放空间(统计失败不阻塞删除,按 0 返回)
|
|
835
|
+
const freedBytes = await this._dirSize(sourceDir).catch(() => 0);
|
|
836
|
+
await fs.rm(sourceDir, { recursive: true, force: true });
|
|
837
|
+
|
|
838
|
+
res.json({
|
|
839
|
+
status: 'ok',
|
|
840
|
+
cleaned: true,
|
|
841
|
+
freedBytes,
|
|
842
|
+
path: sourceDir,
|
|
843
|
+
message: '已删除源码目录 source/,报告与过程文件保留',
|
|
844
|
+
});
|
|
845
|
+
} catch (err) {
|
|
846
|
+
res.error(500, `源码清理失败: ${err.message}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
779
850
|
/**
|
|
780
851
|
* 解析任务工作目录(供 archive 打包等场景使用)。
|
|
781
852
|
*
|
|
@@ -823,6 +894,27 @@ class FlexRunnerController {
|
|
|
823
894
|
return null;
|
|
824
895
|
}
|
|
825
896
|
|
|
897
|
+
/**
|
|
898
|
+
* 递归统计目录占用大小(字节)。符号链接不计入(防循环遍历)。
|
|
899
|
+
* @param {string} dir
|
|
900
|
+
* @returns {Promise<number>}
|
|
901
|
+
* @private
|
|
902
|
+
*/
|
|
903
|
+
async _dirSize(dir) {
|
|
904
|
+
let total = 0;
|
|
905
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
906
|
+
for (const e of entries) {
|
|
907
|
+
const fp = path.join(dir, e.name);
|
|
908
|
+
if (e.isDirectory()) {
|
|
909
|
+
total += await this._dirSize(fp);
|
|
910
|
+
} else if (e.isFile()) {
|
|
911
|
+
const st = await fs.stat(fp).catch(() => null);
|
|
912
|
+
if (st) total += st.size;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return total;
|
|
916
|
+
}
|
|
917
|
+
|
|
826
918
|
/**
|
|
827
919
|
* GET /api/flexrunner/runners
|
|
828
920
|
* 列出所有已注册的 FlexRunner 实例
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import fs from 'node:fs';
|
|
21
|
+
import { hostname } from 'node:os';
|
|
21
22
|
import { Orchestrator, OrchestratorStatus } from '../../Orchestrator/Orchestrator.js';
|
|
22
23
|
import { StorageManager } from '../../DbUse/index.js';
|
|
23
24
|
|
|
@@ -291,11 +292,26 @@ class OrchestratorController {
|
|
|
291
292
|
|
|
292
293
|
this._orchestrators.set(taskId, orch);
|
|
293
294
|
|
|
294
|
-
// 注册任务摘要到注册表(SQLite
|
|
295
|
+
// 注册任务摘要到注册表(SQLite),供历史列表分页查询。
|
|
296
|
+
// 注意:任务已注册时保留原 task_type —— 大循环任务由
|
|
297
|
+
// LoopEngineController.start 先注册为 'loop',随后 LoopEngine 内部
|
|
298
|
+
// 调用本 load() 加载编排,若无条件 upsert 'orchestrator' 会覆盖
|
|
299
|
+
// 任务类型;一旦进程中断(finally 未执行改回),任务将无法在
|
|
300
|
+
// 大循环历史页(taskType='loop' 过滤)中显示。
|
|
301
|
+
const registry = this._getRegistry();
|
|
302
|
+
let taskType = 'orchestrator';
|
|
303
|
+
if (registry) {
|
|
304
|
+
try {
|
|
305
|
+
const registered = await registry.get(taskId);
|
|
306
|
+
if (registered && registered.task_type) {
|
|
307
|
+
taskType = registered.task_type;
|
|
308
|
+
}
|
|
309
|
+
} catch { /* 查询失败按新任务处理 */ }
|
|
310
|
+
}
|
|
295
311
|
const firstStep = body.steps[0] || {};
|
|
296
312
|
this._registerTask({
|
|
297
313
|
taskId,
|
|
298
|
-
taskType
|
|
314
|
+
taskType,
|
|
299
315
|
status: 'loaded',
|
|
300
316
|
title: (firstStep.taskDescription || body.title || '').slice(0, 200),
|
|
301
317
|
description: body.title || firstStep.taskDescription || '',
|
|
@@ -751,6 +767,52 @@ class OrchestratorController {
|
|
|
751
767
|
}
|
|
752
768
|
}
|
|
753
769
|
|
|
770
|
+
/**
|
|
771
|
+
* 判断 running 状态的持久化任务是否有仍存活的"外部属主进程"。
|
|
772
|
+
*
|
|
773
|
+
* owner.json 由 Orchestrator.start() 写入(pid/hostname),终态时删除:
|
|
774
|
+
* - 无 owner.json → 老任务(机制上线前创建)或已正常收尾 → false(维持中断语义)
|
|
775
|
+
* - pid 已死(同机)→ 属主进程已崩溃,清理残留文件后返回 false
|
|
776
|
+
* - pid 存活且非当前进程 → 外部进程(如 3rdloop run)仍在执行 → true
|
|
777
|
+
* - hostname 不同(共享数据目录跨机部署)→ 本机 pid 探测无意义,保守返回 true
|
|
778
|
+
*
|
|
779
|
+
* @param {string} taskId
|
|
780
|
+
* @returns {Promise<boolean>}
|
|
781
|
+
* @private
|
|
782
|
+
*/
|
|
783
|
+
async _hasLiveExternalOwner(taskId) {
|
|
784
|
+
let owner = null;
|
|
785
|
+
try {
|
|
786
|
+
owner = await this.storage.json('task').read(`${taskId}/owner`);
|
|
787
|
+
} catch {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
if (!owner || typeof owner.pid !== 'number') return false;
|
|
791
|
+
|
|
792
|
+
// 本进程残留(防御:restoreFromDisk 运行期本进程不可能有在途编排)
|
|
793
|
+
if (owner.pid === process.pid) return false;
|
|
794
|
+
|
|
795
|
+
// 异机属主:pid 属于另一台机器,本机探测会误判无关进程,保守视为存活
|
|
796
|
+
if (owner.hostname && owner.hostname !== hostname()) return true;
|
|
797
|
+
|
|
798
|
+
// 同机:探测属主进程死活(EPERM = 进程存在但属其他用户)
|
|
799
|
+
let alive = false;
|
|
800
|
+
try {
|
|
801
|
+
process.kill(owner.pid, 0);
|
|
802
|
+
alive = true;
|
|
803
|
+
} catch (err) {
|
|
804
|
+
alive = err.code === 'EPERM';
|
|
805
|
+
}
|
|
806
|
+
if (!alive) {
|
|
807
|
+
// 属主已崩溃但未及清理 → 删除残留,交由调用方维持 RUNNING→FAILED 语义
|
|
808
|
+
try {
|
|
809
|
+
await this.storage.json('task').delete(`${taskId}/owner`);
|
|
810
|
+
} catch { /* ignore */ }
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
|
|
754
816
|
/**
|
|
755
817
|
* 从磁盘恢复所有持久化的编排任务到内存。
|
|
756
818
|
*
|
|
@@ -762,6 +824,12 @@ class OrchestratorController {
|
|
|
762
824
|
* registry 停留在 loaded/running 的记录修正为恢复后的终态(含 metrics、
|
|
763
825
|
* interrupted 标记);未入库的老任务补录摘要。
|
|
764
826
|
*
|
|
827
|
+
* 跨进程属主检测(RUNNING 状态专属):
|
|
828
|
+
* 3rdloop run(嵌入式 CLI)在独立进程中执行编排、共享同一 db/ 目录,
|
|
829
|
+
* 服务重启不能把这类"仍在执行中"的任务误判为崩溃残留。依据 owner.json
|
|
830
|
+
* (Orchestrator.start 写入、终态删除)判定:属主进程仍存活 → 跳过该任务
|
|
831
|
+
* (不恢复入内存、不标 FAILED、不改注册表),由属主进程自行推进与收尾。
|
|
832
|
+
*
|
|
765
833
|
* @returns {Promise<{restored: string[], failed: string[]}>}
|
|
766
834
|
*/
|
|
767
835
|
async restoreFromDisk() {
|
|
@@ -785,6 +853,14 @@ class OrchestratorController {
|
|
|
785
853
|
continue;
|
|
786
854
|
}
|
|
787
855
|
|
|
856
|
+
// ── 跨进程属主检测:running 任务可能属于另一存活进程 ──
|
|
857
|
+
if (state.status === 'running' && await this._hasLiveExternalOwner(taskId)) {
|
|
858
|
+
console.log(
|
|
859
|
+
`[OrchestratorController] 跳过 running 任务(属主进程仍在执行,疑似 3rdloop run 嵌入式任务): taskId=${taskId}`
|
|
860
|
+
);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
|
|
788
864
|
const wasRunning = state.status === 'running';
|
|
789
865
|
const orch = this._createOrchestrator();
|
|
790
866
|
await orch.restoreState(state);
|
|
@@ -853,11 +929,19 @@ class OrchestratorController {
|
|
|
853
929
|
const status = progress.status || 'failed';
|
|
854
930
|
const firstStep = (state.steps || [])[0] || {};
|
|
855
931
|
|
|
932
|
+
// task_type 自愈:任务目录存在 loop.json 说明是大循环任务
|
|
933
|
+
// (LoopEngine 产物)。中断场景下 LoopEngineController.start 的
|
|
934
|
+
// finally 未执行,task_type 可能停留在被编排 load() 覆盖后的
|
|
935
|
+
// 'orchestrator' —— 修正为 'loop',否则大循环历史页
|
|
936
|
+
// (taskType='loop' 过滤)看不到该任务。
|
|
937
|
+
const isLoopTask = state.taskDir
|
|
938
|
+
&& fs.existsSync(path.join(state.taskDir, 'loop.json'));
|
|
939
|
+
|
|
856
940
|
if (!existing) {
|
|
857
941
|
// 老任务未入库(TaskRegistryStore 上线前创建):补录摘要,保留原始创建时间
|
|
858
942
|
await registry.upsert({
|
|
859
943
|
taskId,
|
|
860
|
-
taskType: 'orchestrator',
|
|
944
|
+
taskType: isLoopTask ? 'loop' : 'orchestrator',
|
|
861
945
|
status,
|
|
862
946
|
title: (firstStep.taskDescription || taskId).slice(0, 200),
|
|
863
947
|
description: firstStep.taskDescription || '',
|
|
@@ -865,7 +949,11 @@ class OrchestratorController {
|
|
|
865
949
|
metadata
|
|
866
950
|
});
|
|
867
951
|
} else {
|
|
868
|
-
|
|
952
|
+
const patchBody = { status, metadata };
|
|
953
|
+
if (isLoopTask && existing.task_type !== 'loop') {
|
|
954
|
+
patchBody.task_type = 'loop';
|
|
955
|
+
}
|
|
956
|
+
await registry.patch(taskId, patchBody);
|
|
869
957
|
}
|
|
870
958
|
console.log(
|
|
871
959
|
`[OrchestratorController] 注册表已修正: taskId=${taskId}, ` +
|
|
@@ -21,6 +21,7 @@ import FlexRunnerController from '../controllers/FlexRunnerController.js';
|
|
|
21
21
|
* GET /steps — 列出任务步骤和文件
|
|
22
22
|
* GET /file — 读取指定文件内容
|
|
23
23
|
* GET /archive — 打包下载任务目录下全部 .md 报告(ZIP)
|
|
24
|
+
* POST /clean-source — 删除任务目录下源码克隆目录 source/(释放存储,保留报告/过程文件)
|
|
24
25
|
* GET /runners — 列出所有已注册实例
|
|
25
26
|
* DELETE /runner — 移除一个实例
|
|
26
27
|
*
|
|
@@ -51,6 +52,7 @@ export function createFlexRunnerRouter(dependencies) {
|
|
|
51
52
|
router.get('/steps', controller.steps.bind(controller));
|
|
52
53
|
router.get('/file', controller.file.bind(controller));
|
|
53
54
|
router.get('/archive', controller.archive.bind(controller));
|
|
55
|
+
router.post('/clean-source', controller.cleanSource.bind(controller));
|
|
54
56
|
router.get('/runners', controller.runners.bind(controller));
|
|
55
57
|
|
|
56
58
|
// ── 控制与检查 ──
|
|
@@ -451,7 +451,7 @@ class TestCheck {
|
|
|
451
451
|
this._log('warn', `审核 Session ${sessionId} 未在超时时间内完成`);
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
this._log('
|
|
454
|
+
this._log('debug', `invokeSkill 完成 — sessionId: ${sessionId}, completed: ${completed}`);
|
|
455
455
|
return { sessionId, executed: completed };
|
|
456
456
|
}
|
|
457
457
|
|
package/vendor/VERSION
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
0.0.
|
|
2
|
-
built=2026-09-
|
|
3
|
-
sha=
|
|
1
|
+
0.0.7
|
|
2
|
+
built=2026-09-03T07:18:44.123Z
|
|
3
|
+
sha=f1a4be02fe03f0c6
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* 步骤 2 arkts-library-interface-spec — 接口规格分析(复用已克隆源码)
|
|
10
10
|
* 步骤 3~6 stability / memoryLeak / — 4 个检视维度并发执行
|
|
11
11
|
* documentation / testCoverage (dependId=2 fan-out)
|
|
12
|
+
* 步骤 ⑦ 源码清理(前端动作,非编排步骤) — 终态后删除 source/ 释放存储,
|
|
13
|
+
* 仅删源码,报告与过程文件保留
|
|
12
14
|
*
|
|
13
15
|
* 每个检视 SKILL 输出 0-100 评分:
|
|
14
16
|
* - 首选:SKILL 按约定写 meta .json(stepId{X}_loop{L}_meta_{N}.json),
|
|
@@ -32,6 +34,7 @@
|
|
|
32
34
|
* 6. GET /api/flexrunner/steps?taskId= — 解析报告文件(懒加载)
|
|
33
35
|
* 7. GET /api/flexrunner/file?path= — 读取报告 / 评分兜底解析
|
|
34
36
|
* 8. POST /api/orchestrator/abort — 取消任务
|
|
37
|
+
* 9. POST /api/flexrunner/clean-source — 终态后清理源码(仅删 source/,报告保留)
|
|
35
38
|
*
|
|
36
39
|
* 依赖:utils.js(apiFetch / createPagination / escapeHtml / toast / $)
|
|
37
40
|
* codecheck.css(cc-* 通用卡片样式)+ arktslibrarycheck.css(akt-* 扩展样式)
|
|
@@ -47,6 +50,8 @@ var _AKT_STEP_DEFS = [
|
|
|
47
50
|
{ stepId: 4, key: 'memoryleak', skill: 'arkts-library-memory-leak-check', num: '④', name: '内存泄露检视', desc: '生命周期、NAPI 引用、事件订阅', report: '内存泄露检视报告.md', scoreKey: 'memoryLeakScore' },
|
|
48
51
|
{ stepId: 5, key: 'documentation', skill: 'arkts-library-documentation-quality-check', num: '⑤', name: '文档质量检视', desc: '完整性、准确性、鸿蒙化专项', report: '文档质量检视报告.md', scoreKey: 'documentationScore' },
|
|
49
52
|
{ stepId: 6, key: 'testcoverage', skill: 'arkts-library-test-coverage-check', num: '⑥', name: '用例覆盖度检视', desc: '接口覆盖率、测试质量', report: '用例覆盖度检视报告.md', scoreKey: 'testCoverageScore' },
|
|
53
|
+
// ⑦ 源码清理:纯前端动作(uiOnly,不进 taskDef),终态后调用 /api/flexrunner/clean-source 删除 source/
|
|
54
|
+
{ stepId: 7, key: 'cleansource', skill: '', num: '⑦', name: '源码清理', desc: '检视完成删除 source/ 源码释放存储,保留报告与过程文件', report: null, scoreKey: null, uiOnly: true },
|
|
50
55
|
];
|
|
51
56
|
|
|
52
57
|
/* 评分权重(与 AIweb ArktsLibraryCheck 综合评分口径一致,权重和 = 1) */
|
|
@@ -340,6 +345,7 @@ async function doArktsLibraryCheck() {
|
|
|
340
345
|
// 4 个检视步骤(并发):依赖步骤 1 已克隆的 source/ 和 步骤 2 生成的 接口规格说明.md
|
|
341
346
|
for (var i = 2; i < _AKT_STEP_DEFS.length; i++) {
|
|
342
347
|
var def = _AKT_STEP_DEFS[i];
|
|
348
|
+
if (def.uiOnly) continue; // 源码清理为纯前端动作,不进编排 taskDef
|
|
343
349
|
var desc = [
|
|
344
350
|
'请使用 ' + def.skill + ' SKILL 对 ArkTS 三方库「' + repoInfo.repoName + '」进行' + def.name + ',生成检视报告。',
|
|
345
351
|
'',
|
|
@@ -431,6 +437,7 @@ function _aktNewTaskState(taskId, repoName, repoUrl) {
|
|
|
431
437
|
_resolving: false,
|
|
432
438
|
_progressLoaded: false,
|
|
433
439
|
_terminalDone: false,
|
|
440
|
+
_cleanStarted: false, // 源码清理是否已触发(幂等防重)
|
|
434
441
|
mdFileCount: 0, // taskDir 下的 .md 文件数(驱动"下载全部报告"按钮)
|
|
435
442
|
reportPaths: {}, // 各维度专属报告文件(如 稳定性检视报告.md)
|
|
436
443
|
resultPaths: {}, // 各步骤 result .md 兜底路径(部分 SKILL 会把报告合入 result 文件)
|
|
@@ -1035,6 +1042,8 @@ function _aktOnTerminal(idx, status) {
|
|
|
1035
1042
|
_aktResolveFiles(idx).then(function () {
|
|
1036
1043
|
_aktFallbackScores(idx);
|
|
1037
1044
|
});
|
|
1045
|
+
// 最后环节:清理源码(仅删 taskDir/source/,报告 .md / meta .json 等过程文件保留)
|
|
1046
|
+
_aktCleanSource(idx);
|
|
1038
1047
|
}
|
|
1039
1048
|
|
|
1040
1049
|
function _aktFinishSSE(idx) {
|
|
@@ -1083,6 +1092,77 @@ function _aktRenderElapsed(idx) {
|
|
|
1083
1092
|
if (timeEl && task.startedAt) timeEl.textContent = _aktFormatTime(task.startedAt);
|
|
1084
1093
|
}
|
|
1085
1094
|
|
|
1095
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
1096
|
+
* 源码清理(检视最后环节:删除 taskDir/source/,节省存储)
|
|
1097
|
+
*
|
|
1098
|
+
* 后端 POST /api/flexrunner/clean-source 只删除任务目录下固定的 source/ 子目录
|
|
1099
|
+
* (源码克隆路径),检视报告 .md、meta .json、orchestration.json 等文档与
|
|
1100
|
+
* 过程文件均在 taskDir 根目录,不受影响。
|
|
1101
|
+
* 触发时机:任务终态(_aktOnTerminal);历史终态任务渲染时静默补删
|
|
1102
|
+
* (覆盖检视期间页面被关闭、清理未触发的场景)。
|
|
1103
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
1104
|
+
|
|
1105
|
+
/** 字节数格式化:B / KB / MB / GB */
|
|
1106
|
+
function _aktFormatBytes(n) {
|
|
1107
|
+
if (!isFinite(n) || n <= 0) return '0 B';
|
|
1108
|
+
var units = ['B', 'KB', 'MB', 'GB'];
|
|
1109
|
+
var i = 0;
|
|
1110
|
+
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
|
1111
|
+
return (i === 0 ? Math.round(n) : n.toFixed(1)) + ' ' + units[i];
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** 更新⑦"源码清理"步骤行状态(running / completed / failed) */
|
|
1115
|
+
function _aktSetCleanStatus(idx, status, freedBytes) {
|
|
1116
|
+
var row = document.getElementById('akt-' + idx + '-row-cleansource');
|
|
1117
|
+
var icon = document.getElementById('akt-' + idx + '-icon-cleansource');
|
|
1118
|
+
var txt = document.getElementById('akt-' + idx + '-cleansource-txt');
|
|
1119
|
+
if (row) row.className = 'akt-step-row ' + status;
|
|
1120
|
+
if (icon) {
|
|
1121
|
+
if (status === 'running') { icon.className = 'akt-step-icon running'; icon.textContent = '🧹'; }
|
|
1122
|
+
else if (status === 'completed') { icon.className = 'akt-step-icon completed'; icon.textContent = '✓'; }
|
|
1123
|
+
else if (status === 'failed') { icon.className = 'akt-step-icon failed'; icon.textContent = '✗'; }
|
|
1124
|
+
}
|
|
1125
|
+
if (txt) {
|
|
1126
|
+
if (status === 'running') txt.textContent = '正在清理源码…';
|
|
1127
|
+
else if (status === 'completed') txt.textContent = freedBytes != null ? '已清理源码,释放 ' + _aktFormatBytes(freedBytes) : '源码已清理';
|
|
1128
|
+
else if (status === 'failed') txt.textContent = '清理失败';
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* 调用后端清理源码(幂等,失败允许下次触发重试)。
|
|
1134
|
+
* @param {number} idx
|
|
1135
|
+
* @param {boolean} [silent] - 静默模式(历史补删,不弹 toast)
|
|
1136
|
+
*/
|
|
1137
|
+
function _aktCleanSource(idx, silent) {
|
|
1138
|
+
var task = _aktTasks[idx];
|
|
1139
|
+
if (!task || task._cleanStarted) return;
|
|
1140
|
+
task._cleanStarted = true;
|
|
1141
|
+
|
|
1142
|
+
_aktSetCleanStatus(idx, 'running');
|
|
1143
|
+
apiFetch('POST', '/api/flexrunner/clean-source', { taskId: task.taskId }, 60000)
|
|
1144
|
+
.then(function (r) {
|
|
1145
|
+
if (r.data && r.data.status === 'ok' && r.data.cleaned) {
|
|
1146
|
+
_aktSetCleanStatus(idx, 'completed', r.data.freedBytes);
|
|
1147
|
+
if (!silent) {
|
|
1148
|
+
toast('info', '已清理源码(' + _aktEsc(task.repoName) + '),释放 ' + _aktFormatBytes(r.data.freedBytes) + ' 存储空间');
|
|
1149
|
+
}
|
|
1150
|
+
} else if (r.data && r.data.status === 'ok') {
|
|
1151
|
+
// 无源码可清(未克隆 / 已清理过)→ 幂等完成
|
|
1152
|
+
_aktSetCleanStatus(idx, 'completed');
|
|
1153
|
+
} else {
|
|
1154
|
+
// HTTP 错误(如 409 任务仍在执行)→ 失败可重试
|
|
1155
|
+
task._cleanStarted = false;
|
|
1156
|
+
_aktSetCleanStatus(idx, 'failed');
|
|
1157
|
+
}
|
|
1158
|
+
})
|
|
1159
|
+
.catch(function () {
|
|
1160
|
+
// 清理失败:重置标记,下次触发(如历史重新渲染)时重试
|
|
1161
|
+
task._cleanStarted = false;
|
|
1162
|
+
_aktSetCleanStatus(idx, 'failed');
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1086
1166
|
/* ════════════════════════════════════════════════════════════════════
|
|
1087
1167
|
* 停止检视
|
|
1088
1168
|
* ════════════════════════════════════════════════════════════════════ */
|
|
@@ -1387,6 +1467,8 @@ function _aktRenderHistoryPage(tasks, isFirstPage) {
|
|
|
1387
1467
|
_aktStartProgressPoll(idx);
|
|
1388
1468
|
} else {
|
|
1389
1469
|
_aktFinishSSE(idx);
|
|
1470
|
+
// 终态历史任务:静默补删源码(覆盖检视期间页面关闭、终态清理未触发的场景)
|
|
1471
|
+
_aktCleanSource(idx, true);
|
|
1390
1472
|
}
|
|
1391
1473
|
}
|
|
1392
1474
|
}
|
package/web/js/codecheck.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* 6. GET /api/flexrunner/steps?taskId= — 获取结果文件列表
|
|
14
14
|
* 7. GET /api/flexrunner/file?path= — 读取检视报告
|
|
15
15
|
* 8. POST /api/orchestrator/abort — 取消任务
|
|
16
|
+
* 9. POST /api/flexrunner/clean-source — 终态后清理源码(仅删 source/,报告保留)
|
|
16
17
|
*
|
|
17
18
|
* 依赖:utils.js(apiFetch / API / createPagination / escapeHtml / toast / $)
|
|
18
19
|
* ════════════════════════════════════════════════════════════════════ */
|
|
@@ -322,6 +323,7 @@ function _renderCCHistoryPage(tasks, isFirstPage) {
|
|
|
322
323
|
startedAt: startTime || null,
|
|
323
324
|
finishedAt: endTime || null,
|
|
324
325
|
resultPath: null,
|
|
326
|
+
_cleanStarted: false, // 源码清理是否已触发(幂等防重)
|
|
325
327
|
// 列表响应中的结构化指标(终态任务直接渲染徽章,无需额外请求)
|
|
326
328
|
metrics: t.metadata || null,
|
|
327
329
|
};
|
|
@@ -372,6 +374,8 @@ function _handleTaskStage(idx, stage) {
|
|
|
372
374
|
_startProgressPoll(idx);
|
|
373
375
|
} else {
|
|
374
376
|
_finishCCSSE(idx);
|
|
377
|
+
// 终态历史任务:静默补删源码(覆盖检视期间页面关闭、终态清理未触发的场景)
|
|
378
|
+
_ccCleanSource(idx, true);
|
|
375
379
|
// 终态任务不主动拉文件(上千任务时列表渲染零额外请求);
|
|
376
380
|
// 展开卡片 / 点击预览/下载时才懒加载(toggleCCCard / viewCCReport / downloadCCReport)
|
|
377
381
|
}
|
|
@@ -460,6 +464,7 @@ async function doCodeCheck() {
|
|
|
460
464
|
startedAt: null,
|
|
461
465
|
finishedAt: null,
|
|
462
466
|
resultPath: null,
|
|
467
|
+
_cleanStarted: false, // 源码清理是否已触发(幂等防重)
|
|
463
468
|
};
|
|
464
469
|
|
|
465
470
|
_createCCCard(idx, displayLabel, prUrl, libType, new Date().toISOString());
|
|
@@ -516,6 +521,8 @@ function _startProgressPoll(idx) {
|
|
|
516
521
|
_stopProgressPoll(idx);
|
|
517
522
|
_finishCCSSE(idx);
|
|
518
523
|
_loadCCResultFiles(idx);
|
|
524
|
+
// 最后环节:清理源码(仅删 taskDir/source/,报告与过程文件保留)
|
|
525
|
+
_ccCleanSource(idx);
|
|
519
526
|
}
|
|
520
527
|
} catch (e) { /* 静默失败 */ }
|
|
521
528
|
}, 3000);
|
|
@@ -610,6 +617,52 @@ async function _loadCCResultFiles(idx) {
|
|
|
610
617
|
} catch (e) { /* 静默失败 */ }
|
|
611
618
|
}
|
|
612
619
|
|
|
620
|
+
/* ── 源码清理(检视最后环节:删除 taskDir/source/,节省存储)──────────
|
|
621
|
+
*
|
|
622
|
+
* arkts/flutter/rn-code-check 检视过程中会把 PR 目标仓库克隆到任务目录的
|
|
623
|
+
* source/ 下,检视终态后源码不再被引用(报告 .md / meta .json 等过程文件
|
|
624
|
+
* 均在 taskDir 根目录,不受影响);tpc-cpp-check 不克隆源码,后端幂等返回。
|
|
625
|
+
* 触发时机:任务终态(轮询 / SSE complete / 取消);历史终态任务渲染时
|
|
626
|
+
* 静默补删(覆盖检视期间页面被关闭、清理未触发的场景)。
|
|
627
|
+
* ──────────────────────────────────────────────────────────────────── */
|
|
628
|
+
|
|
629
|
+
/** 字节数格式化:B / KB / MB / GB */
|
|
630
|
+
function _ccFormatBytes(n) {
|
|
631
|
+
if (!isFinite(n) || n <= 0) return '0 B';
|
|
632
|
+
var units = ['B', 'KB', 'MB', 'GB'];
|
|
633
|
+
var i = 0;
|
|
634
|
+
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
|
635
|
+
return (i === 0 ? Math.round(n) : n.toFixed(1)) + ' ' + units[i];
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 调用后端清理源码(幂等,失败允许下次触发重试)。
|
|
640
|
+
* @param {number} idx
|
|
641
|
+
* @param {boolean} [silent] - 静默模式(历史补删 / 取消场景,不弹 toast)
|
|
642
|
+
*/
|
|
643
|
+
function _ccCleanSource(idx, silent) {
|
|
644
|
+
var task = _ccTasks[idx];
|
|
645
|
+
if (!task || task._cleanStarted) return;
|
|
646
|
+
task._cleanStarted = true;
|
|
647
|
+
|
|
648
|
+
apiFetch('POST', '/api/flexrunner/clean-source', { taskId: task.taskId }, 60000)
|
|
649
|
+
.then(function (r) {
|
|
650
|
+
if (r.data && r.data.status === 'ok' && r.data.cleaned) {
|
|
651
|
+
if (!silent) {
|
|
652
|
+
toast('info', '已清理检视源码,释放 ' + _ccFormatBytes(r.data.freedBytes) + ' 存储空间');
|
|
653
|
+
}
|
|
654
|
+
} else if (!(r.data && r.data.status === 'ok')) {
|
|
655
|
+
// HTTP 错误(如 409 任务仍在执行)→ 失败可重试
|
|
656
|
+
task._cleanStarted = false;
|
|
657
|
+
}
|
|
658
|
+
// ok 且 cleaned=false:无源码可清(未克隆 / 已清理过)→ 幂等完成,不提示
|
|
659
|
+
})
|
|
660
|
+
.catch(function () {
|
|
661
|
+
// 清理失败:重置标记,下次触发(如历史重新渲染)时重试
|
|
662
|
+
task._cleanStarted = false;
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
613
666
|
/* ── 创建检视任务卡片 ────────────────────────────────────────────────── */
|
|
614
667
|
function _createCCCard(idx, displayLabel, prUrl, libType, createdAt, isHistory) {
|
|
615
668
|
var emptyEl = $('cc-tasks-empty');
|
|
@@ -764,6 +817,8 @@ function _connectCCSSE(idx) {
|
|
|
764
817
|
_stopProgressPoll(idx);
|
|
765
818
|
_finishCCSSE(idx);
|
|
766
819
|
_loadCCResultFiles(idx);
|
|
820
|
+
// 最后环节:清理源码(仅删 taskDir/source/,报告与过程文件保留)
|
|
821
|
+
_ccCleanSource(idx);
|
|
767
822
|
});
|
|
768
823
|
|
|
769
824
|
// 编排错误
|
|
@@ -777,6 +832,8 @@ function _connectCCSSE(idx) {
|
|
|
777
832
|
_updateCCStage(idx, 'aborted', {});
|
|
778
833
|
_stopProgressPoll(idx);
|
|
779
834
|
_finishCCSSE(idx);
|
|
835
|
+
// 终止场景:静默清理源码(已有"已取消"提示,避免 toast 叠加)
|
|
836
|
+
_ccCleanSource(idx, true);
|
|
780
837
|
});
|
|
781
838
|
|
|
782
839
|
// 连接建立
|
|
@@ -920,6 +977,8 @@ async function stopCCTask(idx) {
|
|
|
920
977
|
_updateCCStage(idx, 'aborted', {});
|
|
921
978
|
_stopProgressPoll(idx);
|
|
922
979
|
_finishCCSSE(idx);
|
|
980
|
+
// 终止场景:静默清理源码(SSE aborted 监听会幂等兜底)
|
|
981
|
+
_ccCleanSource(idx, true);
|
|
923
982
|
}
|
|
924
983
|
|
|
925
984
|
/* ── 报告下载 ────────────────────────────────────────────────────────── */
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* 步骤 1 flutter-interface — 克隆仓库 + 接口规格分析
|
|
9
9
|
* 步骤 2~6 stability / consistency / memoryLeak / — 5 个检视维度并发执行
|
|
10
10
|
* documentation / testCoverage (dependId=1 fan-out)
|
|
11
|
+
* 步骤 ⑦ 源码清理(前端动作,非编排步骤) — 终态后删除 source/ 释放存储,
|
|
12
|
+
* 仅删源码,报告与过程文件保留
|
|
11
13
|
*
|
|
12
14
|
* 每个检视 SKILL 输出 0-100 评分:
|
|
13
15
|
* - 首选:SKILL 按约定写 meta .json(stepId{X}_loop{L}_meta_{N}.json),
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
* 6. GET /api/flexrunner/steps?taskId= — 解析报告文件(懒加载)
|
|
25
27
|
* 7. GET /api/flexrunner/file?path= — 读取报告 / 评分兜底解析
|
|
26
28
|
* 8. POST /api/orchestrator/abort — 取消任务
|
|
29
|
+
* 9. POST /api/flexrunner/clean-source — 终态后清理源码(仅删 source/,报告保留)
|
|
27
30
|
*
|
|
28
31
|
* 依赖:utils.js(apiFetch / createPagination / escapeHtml / toast / $)
|
|
29
32
|
* codecheck.css(cc-* 通用卡片样式)+ flutterlibrarycheck.css(flc-* 扩展样式)
|
|
@@ -39,6 +42,8 @@ var _FLC_STEP_DEFS = [
|
|
|
39
42
|
{ stepId: 4, key: 'memoryleak', skill: 'flutter-library-memory-leak-check', num: '④', name: '内存泄露检视', desc: '生命周期、资源释放、Stream 管理', report: '内存泄露检视报告.md', scoreKey: 'memoryLeakScore' },
|
|
40
43
|
{ stepId: 5, key: 'documentation', skill: 'flutter-library-documentation-quality-check', num: '⑤', name: '文档质量检视', desc: '鸿蒙化文档完整性、准确性、易读性', report: '文档质量检视报告.md', scoreKey: 'documentationScore' },
|
|
41
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 },
|
|
42
47
|
];
|
|
43
48
|
|
|
44
49
|
/* 评分权重(与 AIweb FlutterLibraryCheck 综合评分口径一致,权重和 = 1) */
|
|
@@ -301,6 +306,7 @@ async function doFlutterLibraryCheck() {
|
|
|
301
306
|
// 5 个检视步骤(并发):依赖步骤 1 已克隆的 source/ 和 接口规格说明.md
|
|
302
307
|
for (var i = 1; i < _FLC_STEP_DEFS.length; i++) {
|
|
303
308
|
var def = _FLC_STEP_DEFS[i];
|
|
309
|
+
if (def.uiOnly) continue; // 源码清理为纯前端动作,不进编排 taskDef
|
|
304
310
|
var desc = [
|
|
305
311
|
'请使用 ' + def.skill + ' SKILL 对 Flutter 三方库「' + repoInfo.repoName + '」进行' + def.name + ',生成检视报告。',
|
|
306
312
|
'',
|
|
@@ -383,6 +389,7 @@ function _flcNewTaskState(taskId, repoUrl, repoName) {
|
|
|
383
389
|
_resolving: false,
|
|
384
390
|
_progressLoaded: false,
|
|
385
391
|
_terminalDone: false,
|
|
392
|
+
_cleanStarted: false, // 源码清理是否已触发(幂等防重)
|
|
386
393
|
mdFileCount: 0, // taskDir 下的 .md 文件数(驱动"下载全部报告"按钮)
|
|
387
394
|
reportPaths: {}, // 各维度专属报告文件(如 稳定性检视报告.md)
|
|
388
395
|
resultPaths: {}, // 各步骤 result .md 兜底路径(部分 SKILL 会把报告合入 result 文件)
|
|
@@ -826,9 +833,10 @@ async function _flcResolveFiles(idx) {
|
|
|
826
833
|
var task = _flcTasks[idx];
|
|
827
834
|
if (!task) return;
|
|
828
835
|
|
|
829
|
-
// 全部专属报告已找到 →
|
|
836
|
+
// 全部专属报告已找到 → 幂等返回(uiOnly / 无报告步骤跳过,如⑦源码清理)
|
|
830
837
|
var allFound = true;
|
|
831
838
|
for (var a = 0; a < _FLC_STEP_DEFS.length; a++) {
|
|
839
|
+
if (_FLC_STEP_DEFS[a].uiOnly || !_FLC_STEP_DEFS[a].report) continue;
|
|
832
840
|
if (!task.reportPaths[_FLC_STEP_DEFS[a].key]) { allFound = false; break; }
|
|
833
841
|
}
|
|
834
842
|
if (allFound) { task.filesResolved = true; _flcShowReportButtons(idx); return; }
|
|
@@ -986,6 +994,8 @@ function _flcOnTerminal(idx, status) {
|
|
|
986
994
|
_flcResolveFiles(idx).then(function () {
|
|
987
995
|
_flcFallbackScores(idx);
|
|
988
996
|
});
|
|
997
|
+
// 最后环节:清理源码(仅删 taskDir/source/,报告 .md / meta .json 等过程文件保留)
|
|
998
|
+
_flcCleanSource(idx);
|
|
989
999
|
}
|
|
990
1000
|
|
|
991
1001
|
function _flcFinishSSE(idx) {
|
|
@@ -1034,6 +1044,77 @@ function _flcRenderElapsed(idx) {
|
|
|
1034
1044
|
if (timeEl && task.startedAt) timeEl.textContent = _flcFormatTime(task.startedAt);
|
|
1035
1045
|
}
|
|
1036
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
|
+
|
|
1037
1118
|
/* ════════════════════════════════════════════════════════════════════
|
|
1038
1119
|
* 停止检视
|
|
1039
1120
|
* ════════════════════════════════════════════════════════════════════ */
|
|
@@ -1338,6 +1419,8 @@ function _flcRenderHistoryPage(tasks, isFirstPage) {
|
|
|
1338
1419
|
_flcStartProgressPoll(idx);
|
|
1339
1420
|
} else {
|
|
1340
1421
|
_flcFinishSSE(idx);
|
|
1422
|
+
// 终态历史任务:静默补删源码(覆盖检视期间页面关闭、终态清理未触发的场景)
|
|
1423
|
+
_flcCleanSource(idx, true);
|
|
1341
1424
|
}
|
|
1342
1425
|
}
|
|
1343
1426
|
}
|
package/web/js/rnlibrarycheck.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* 步骤 2~7 stability / consistency / memoryLeak / — 6 个检视维度并发执行
|
|
10
10
|
* documentation / testCoverage / (dependId=1 fan-out)
|
|
11
11
|
* knownIssue(独立扣分项)
|
|
12
|
+
* 步骤 ⑧ 源码清理(前端动作,非编排步骤) — 终态后删除 source/ 释放存储
|
|
13
|
+
* (ohos/ + RN/),仅删源码,
|
|
14
|
+
* 报告与过程文件保留
|
|
12
15
|
*
|
|
13
16
|
* 每个检视 SKILL 输出 0-100 评分:
|
|
14
17
|
* - 首选:SKILL 按约定写 meta .json(stepId{X}_loop{L}_meta_{N}.json),
|
|
@@ -33,6 +36,7 @@
|
|
|
33
36
|
* 6. GET /api/flexrunner/steps?taskId= — 解析报告文件(懒加载)
|
|
34
37
|
* 7. GET /api/flexrunner/file?path= — 读取报告 / 评分兜底解析
|
|
35
38
|
* 8. POST /api/orchestrator/abort — 取消任务
|
|
39
|
+
* 9. POST /api/flexrunner/clean-source — 终态后清理源码(仅删 source/,报告保留)
|
|
36
40
|
*
|
|
37
41
|
* 依赖:utils.js(apiFetch / createPagination / escapeHtml / toast / $)
|
|
38
42
|
* codecheck.css(cc-* 通用卡片样式)+ rnlibrarycheck.css(rn-* 扩展样式)
|
|
@@ -49,6 +53,8 @@ var _RN_STEP_DEFS = [
|
|
|
49
53
|
{ stepId: 5, key: 'documentation', skill: 'rn-library-documentation-quality-check', num: '⑤', name: '文档质量检视', desc: 'API 注释、适配说明、示例质量', report: '文档质量检视报告.md', scoreKey: 'documentationScore' },
|
|
50
54
|
{ stepId: 6, key: 'testcoverage', skill: 'rn-library-test-coverage-check', num: '⑥', name: '用例覆盖度检视', desc: '测试覆盖率、Demo 覆盖率', report: '用例覆盖度检视报告.md', scoreKey: 'testCoverageScore' },
|
|
51
55
|
{ stepId: 7, key: 'knownissue', skill: 'rn-library-known-issue-check', num: '⑦', name: '已知问题检视', desc: '历史案例规则回归,独立扣分项(命中一条扣 30 分)', report: '已知问题检视报告.md', scoreKey: 'knownIssueScore' },
|
|
56
|
+
// ⑧ 源码清理:纯前端动作(uiOnly,不进 taskDef),终态后调用 /api/flexrunner/clean-source 删除 source/(ohos/ + RN/)
|
|
57
|
+
{ stepId: 8, key: 'cleansource', skill: '', num: '⑧', name: '源码清理', desc: '检视完成删除 source/ 源码释放存储,保留报告与过程文件', report: null, scoreKey: null, uiOnly: true },
|
|
52
58
|
];
|
|
53
59
|
|
|
54
60
|
/* 评分权重(与 AIweb RNLibraryCheck 综合评分口径一致,权重和 = 1;knownissue 为独立扣分项不参与加权) */
|
|
@@ -283,6 +289,7 @@ async function doRNLibraryCheck() {
|
|
|
283
289
|
// 6 个检视步骤(并发):依赖步骤 1 已克隆的 source/ohos/、source/RN/ 和 接口规格说明.md
|
|
284
290
|
for (var i = 1; i < _RN_STEP_DEFS.length; i++) {
|
|
285
291
|
var def = _RN_STEP_DEFS[i];
|
|
292
|
+
if (def.uiOnly) continue; // 源码清理为纯前端动作,不进编排 taskDef
|
|
286
293
|
var desc = [
|
|
287
294
|
'请使用 ' + def.skill + ' SKILL 对 RN 三方库「' + libraryName + '」进行' + def.name + ',生成检视报告。',
|
|
288
295
|
'',
|
|
@@ -384,6 +391,7 @@ function _rnNewTaskState(taskId, libraryName, ohosRepoUrl, upstreamRepoUrl, rndo
|
|
|
384
391
|
_resolving: false,
|
|
385
392
|
_progressLoaded: false,
|
|
386
393
|
_terminalDone: false,
|
|
394
|
+
_cleanStarted: false, // 源码清理是否已触发(幂等防重)
|
|
387
395
|
mdFileCount: 0, // taskDir 下的 .md 文件数(驱动"下载全部报告"按钮)
|
|
388
396
|
reportPaths: {}, // 各维度专属报告文件(如 稳定性检视报告.md)
|
|
389
397
|
resultPaths: {}, // 各步骤 result .md 兜底路径(部分 SKILL 会把报告合入 result 文件)
|
|
@@ -837,9 +845,10 @@ async function _rnResolveFiles(idx) {
|
|
|
837
845
|
var task = _rnTasks[idx];
|
|
838
846
|
if (!task) return;
|
|
839
847
|
|
|
840
|
-
// 全部专属报告已找到 →
|
|
848
|
+
// 全部专属报告已找到 → 幂等返回(uiOnly / 无报告步骤跳过,如⑧源码清理)
|
|
841
849
|
var allFound = true;
|
|
842
850
|
for (var a = 0; a < _RN_STEP_DEFS.length; a++) {
|
|
851
|
+
if (_RN_STEP_DEFS[a].uiOnly || !_RN_STEP_DEFS[a].report) continue;
|
|
843
852
|
if (!task.reportPaths[_RN_STEP_DEFS[a].key]) { allFound = false; break; }
|
|
844
853
|
}
|
|
845
854
|
if (allFound) { task.filesResolved = true; _rnShowReportButtons(idx); return; }
|
|
@@ -997,6 +1006,8 @@ function _rnOnTerminal(idx, status) {
|
|
|
997
1006
|
_rnResolveFiles(idx).then(function () {
|
|
998
1007
|
_rnFallbackScores(idx);
|
|
999
1008
|
});
|
|
1009
|
+
// 最后环节:清理源码(仅删 taskDir/source/(ohos/ + RN/),报告 .md / meta .json 等过程文件保留)
|
|
1010
|
+
_rnCleanSource(idx);
|
|
1000
1011
|
}
|
|
1001
1012
|
|
|
1002
1013
|
function _rnFinishSSE(idx) {
|
|
@@ -1045,6 +1056,77 @@ function _rnRenderElapsed(idx) {
|
|
|
1045
1056
|
if (timeEl && task.startedAt) timeEl.textContent = _rnFormatTime(task.startedAt);
|
|
1046
1057
|
}
|
|
1047
1058
|
|
|
1059
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
1060
|
+
* 源码清理(检视最后环节:删除 taskDir/source/,节省存储)
|
|
1061
|
+
*
|
|
1062
|
+
* 后端 POST /api/flexrunner/clean-source 只删除任务目录下固定的 source/ 子目录
|
|
1063
|
+
* (RN 双仓库源码克隆路径,含 ohos/ 与 RN/),检视报告 .md、meta .json、
|
|
1064
|
+
* orchestration.json 等文档与过程文件均在 taskDir 根目录,不受影响。
|
|
1065
|
+
* 触发时机:任务终态(_rnOnTerminal);历史终态任务渲染时静默补删
|
|
1066
|
+
* (覆盖检视期间页面被关闭、清理未触发的场景)。
|
|
1067
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
1068
|
+
|
|
1069
|
+
/** 字节数格式化:B / KB / MB / GB */
|
|
1070
|
+
function _rnFormatBytes(n) {
|
|
1071
|
+
if (!isFinite(n) || n <= 0) return '0 B';
|
|
1072
|
+
var units = ['B', 'KB', 'MB', 'GB'];
|
|
1073
|
+
var i = 0;
|
|
1074
|
+
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
|
1075
|
+
return (i === 0 ? Math.round(n) : n.toFixed(1)) + ' ' + units[i];
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/** 更新⑧"源码清理"步骤行状态(running / completed / failed) */
|
|
1079
|
+
function _rnSetCleanStatus(idx, status, freedBytes) {
|
|
1080
|
+
var row = document.getElementById('rn-' + idx + '-row-cleansource');
|
|
1081
|
+
var icon = document.getElementById('rn-' + idx + '-icon-cleansource');
|
|
1082
|
+
var txt = document.getElementById('rn-' + idx + '-cleansource-txt');
|
|
1083
|
+
if (row) row.className = 'rn-step-row ' + status;
|
|
1084
|
+
if (icon) {
|
|
1085
|
+
if (status === 'running') { icon.className = 'rn-step-icon running'; icon.textContent = '🧹'; }
|
|
1086
|
+
else if (status === 'completed') { icon.className = 'rn-step-icon completed'; icon.textContent = '✓'; }
|
|
1087
|
+
else if (status === 'failed') { icon.className = 'rn-step-icon failed'; icon.textContent = '✗'; }
|
|
1088
|
+
}
|
|
1089
|
+
if (txt) {
|
|
1090
|
+
if (status === 'running') txt.textContent = '正在清理源码…';
|
|
1091
|
+
else if (status === 'completed') txt.textContent = freedBytes != null ? '已清理源码,释放 ' + _rnFormatBytes(freedBytes) : '源码已清理';
|
|
1092
|
+
else if (status === 'failed') txt.textContent = '清理失败';
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* 调用后端清理源码(幂等,失败允许下次触发重试)。
|
|
1098
|
+
* @param {number} idx
|
|
1099
|
+
* @param {boolean} [silent] - 静默模式(历史补删,不弹 toast)
|
|
1100
|
+
*/
|
|
1101
|
+
function _rnCleanSource(idx, silent) {
|
|
1102
|
+
var task = _rnTasks[idx];
|
|
1103
|
+
if (!task || task._cleanStarted) return;
|
|
1104
|
+
task._cleanStarted = true;
|
|
1105
|
+
|
|
1106
|
+
_rnSetCleanStatus(idx, 'running');
|
|
1107
|
+
apiFetch('POST', '/api/flexrunner/clean-source', { taskId: task.taskId }, 60000)
|
|
1108
|
+
.then(function (r) {
|
|
1109
|
+
if (r.data && r.data.status === 'ok' && r.data.cleaned) {
|
|
1110
|
+
_rnSetCleanStatus(idx, 'completed', r.data.freedBytes);
|
|
1111
|
+
if (!silent) {
|
|
1112
|
+
toast('info', '已清理源码(' + _rnEsc(task.libraryName) + '),释放 ' + _rnFormatBytes(r.data.freedBytes) + ' 存储空间');
|
|
1113
|
+
}
|
|
1114
|
+
} else if (r.data && r.data.status === 'ok') {
|
|
1115
|
+
// 无源码可清(未克隆 / 已清理过)→ 幂等完成
|
|
1116
|
+
_rnSetCleanStatus(idx, 'completed');
|
|
1117
|
+
} else {
|
|
1118
|
+
// HTTP 错误(如 409 任务仍在执行)→ 失败可重试
|
|
1119
|
+
task._cleanStarted = false;
|
|
1120
|
+
_rnSetCleanStatus(idx, 'failed');
|
|
1121
|
+
}
|
|
1122
|
+
})
|
|
1123
|
+
.catch(function () {
|
|
1124
|
+
// 清理失败:重置标记,下次触发(如历史重新渲染)时重试
|
|
1125
|
+
task._cleanStarted = false;
|
|
1126
|
+
_rnSetCleanStatus(idx, 'failed');
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1048
1130
|
/* ════════════════════════════════════════════════════════════════════
|
|
1049
1131
|
* 停止检视
|
|
1050
1132
|
* ════════════════════════════════════════════════════════════════════ */
|
|
@@ -1352,6 +1434,8 @@ function _rnRenderHistoryPage(tasks, isFirstPage) {
|
|
|
1352
1434
|
_rnStartProgressPoll(idx);
|
|
1353
1435
|
} else {
|
|
1354
1436
|
_rnFinishSSE(idx);
|
|
1437
|
+
// 终态历史任务:静默补删源码(覆盖检视期间页面关闭、终态清理未触发的场景)
|
|
1438
|
+
_rnCleanSource(idx, true);
|
|
1355
1439
|
}
|
|
1356
1440
|
}
|
|
1357
1441
|
}
|