@ohos-cpf/3rdloop 0.0.6 → 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 +1 -2
- package/lib/update.js +6 -45
- package/package.json +1 -1
- package/vendor/Server/CLI/opencode/index.js +1 -1
- package/vendor/Server/FlexRunner/FlexRunner.js +10 -10
- package/vendor/Server/Orchestrator/Orchestrator.js +39 -0
- package/vendor/Server/Routes/controllers/OrchestratorController.js +92 -4
- package/vendor/Server/TestCheck/TestCheck.js +1 -1
- package/vendor/VERSION +3 -3
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
|
@@ -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/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
|
@@ -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}`);
|
|
@@ -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
|
*
|
|
@@ -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}, ` +
|
|
@@ -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
|