@ohos-cpf/3rdloop 0.0.6 → 0.0.8
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 +20 -27
- package/lib/cli.js +34 -27
- package/lib/config-cmd.js +1 -1
- package/lib/config.js +32 -0
- package/lib/doctor.js +162 -45
- package/lib/opencode.js +75 -29
- package/lib/orch.js +32 -10
- package/lib/runner.js +8 -5
- package/lib/serve.js +162 -149
- package/lib/step.js +15 -4
- package/lib/update.js +6 -45
- package/lib/web.js +107 -265
- package/lib/workflow.js +18 -8
- package/package.json +1 -1
- package/vendor/Server/CLI/cli.js +3 -3
- 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/Routes/server.js +3 -3
- package/vendor/Server/TestCheck/TestCheck.js +1 -1
- package/vendor/VERSION +3 -3
|
@@ -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}, ` +
|
|
@@ -38,7 +38,7 @@ import KnowledgeImportController from './controllers/KnowledgeImportController.j
|
|
|
38
38
|
* 创建并启动 HTTP 服务
|
|
39
39
|
* @param {object} [options]
|
|
40
40
|
* @param {number} [options.port=3000] - 监听端口
|
|
41
|
-
* @param {string} [options.cliType] - CLI 类型,缺省时取环境变量 CLI_TYPE,仍未设置则 '
|
|
41
|
+
* @param {string} [options.cliType] - CLI 类型,缺省时取环境变量 CLI_TYPE,仍未设置则 'deveco-code'
|
|
42
42
|
* @param {object} [options.cliOptions={}] - CLI 适配器配置
|
|
43
43
|
* @param {CLIController} [options.cli] - 已存在的 CLI 实例(优先于 cliType/cliOptions)
|
|
44
44
|
* @returns {Promise<http.Server>}
|
|
@@ -46,7 +46,7 @@ import KnowledgeImportController from './controllers/KnowledgeImportController.j
|
|
|
46
46
|
export async function createServer(options = {}) {
|
|
47
47
|
const {
|
|
48
48
|
port = 3000,
|
|
49
|
-
cliType = process.env.CLI_TYPE || '
|
|
49
|
+
cliType = process.env.CLI_TYPE || 'deveco-code',
|
|
50
50
|
cliOptions = {},
|
|
51
51
|
cli = null
|
|
52
52
|
} = options;
|
|
@@ -192,7 +192,7 @@ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
|
192
192
|
const portIdx = args.indexOf('--port');
|
|
193
193
|
const typeIdx = args.indexOf('--type');
|
|
194
194
|
const port = portIdx !== -1 ? Number(args[portIdx + 1]) : 3000;
|
|
195
|
-
const cliType = typeIdx !== -1 ? args[typeIdx + 1] : (process.env.CLI_TYPE || '
|
|
195
|
+
const cliType = typeIdx !== -1 ? args[typeIdx + 1] : (process.env.CLI_TYPE || 'deveco-code');
|
|
196
196
|
|
|
197
197
|
createServer({ port, cliType }).catch(err => {
|
|
198
198
|
console.error('[Server] 启动失败:', err);
|
|
@@ -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.8
|
|
2
|
+
built=2026-09-04T06:08:23.422Z
|
|
3
|
+
sha=9b9e6f3164fa158c
|