@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 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] [--force] [--registry <url>] 自更新到 npm 最新版
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/skillDir 沿用 Server 默认(workspace db/),本期不与 CLI 打通
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
- * - 识别开发态(npm link 软链)与正式全局安装
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, force = false, isLink = detectLinkMode() } = {}) {
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] [--force] [--registry <url>]',
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, force });
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, force });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohos-cpf/3rdloop",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "3rdLibraryLoop 三方库自动化检视 CLI:嵌入式复用核心引擎,任务生命周期管理(submit/run/wait/progress/abort/list/result/doctor)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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('warn', `checkSessionStatus: session.status() 失败,降级为拉消息: ${e.message}`);
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('warn', `checkSessionStatus 异常 id=${sessionId}: ${error.message}`);
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('info',
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('info', `invokeSkill 完成 — sessionId: ${sessionId}`);
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('info', `persistKnowledge: 已读取经验文件 (${content.length} 字符)`);
910
+ this._log('debug', `persistKnowledge: 已读取经验文件 (${content.length} 字符)`);
911
911
 
912
912
  // 1. 将经验内容持久化到步骤记录
913
913
  await this._persistStep({ knowledgeSummary: content });
914
- this._log('info', 'persistKnowledge: 经验已持久化到步骤记录');
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('info', `persistKnowledge: 经验已登记过,跳重复登记 — id: ${existing.id}`);
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('info', `persistKnowledge: 经验已登记到知识沉淀系统 — id: ${record.id}`);
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('info', `checkResult: 执行状态 = 成功 (${resultFilePath})`);
1115
+ this._log('debug', `checkResult: 执行状态 = 成功 (${resultFilePath})`);
1116
1116
  return true;
1117
1117
  }
1118
1118
 
1119
1119
  if (value === '失败') {
1120
- this._log('info', `checkResult: 执行状态 = 失败 (${resultFilePath})`);
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('info', '_summarizeExperience: 等待回复期间被终止,放弃经验总结');
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('info',
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('info', `经验总结完成 — Summary 文件已生成: ${summaryFilePath}`);
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: 'orchestrator',
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
- await registry.patch(taskId, { status, metadata });
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('info', `invokeSkill 完成 — sessionId: ${sessionId}, completed: ${completed}`);
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.5
2
- built=2026-09-03T03:43:15.270Z
3
- sha=45ad839069c351a8
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
  }
@@ -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
  }
@@ -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
  }