@ohos-cpf/3rdloop 0.0.7 → 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 -26
- package/lib/cli.js +33 -25
- 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/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/Routes/server.js +3 -3
- package/vendor/VERSION +3 -3
package/lib/opencode.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* opencode.js ——
|
|
2
|
+
* opencode.js —— AI CLI serve 生命周期管理(opencode / deveco-code)
|
|
3
3
|
*
|
|
4
4
|
* 策略(A4):
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
5
|
+
* - 类型:由 CLI_TYPE 决定(显式参数 > 环境变量/.env 链 > 默认 deveco-code),
|
|
6
|
+
* deveco-code 时拉起 `deveco serve`,否则 `opencode serve`
|
|
7
|
+
* (deveco-code 基于 opencode 扩展,HTTP API 同源,/health 通用)
|
|
8
|
+
* - 探测:操作前 checkHealth()(HTTP GET /health)
|
|
9
|
+
* - 拉起:CLI 自持 spawn('<bin> serve --port <port>'),透传 stderr 前几行;
|
|
10
|
+
* 已在运行则复用不接管;远端 host(非 localhost)只探测不拉起
|
|
7
11
|
* - 归属:CLI 自己拉的记 pid,退出时先 abortLoop 再杀子进程
|
|
8
|
-
* - 已知坑(AGENTS.md):
|
|
12
|
+
* - 已知坑(AGENTS.md):serve 需在项目上一级目录执行;
|
|
9
13
|
* 子任务 session 创建时强制指定 cwd 会触发权限问题(已验证 SkillExecutor.drop cwd)
|
|
10
14
|
*
|
|
11
15
|
* 跨平台进程清理:
|
|
@@ -14,26 +18,57 @@
|
|
|
14
18
|
|
|
15
19
|
import { spawn, execSync } from 'node:child_process';
|
|
16
20
|
import path from 'node:path';
|
|
17
|
-
import
|
|
21
|
+
import { resolveCliType } from './config.js';
|
|
22
|
+
|
|
23
|
+
/** CLI 类型 → 可执行文件名 */
|
|
24
|
+
const CLI_BINS = { opencode: 'opencode', 'deveco-code': 'deveco' };
|
|
25
|
+
|
|
26
|
+
/** CLI 类型 → host/port 环境变量(与 Server/CLI 适配器同源) */
|
|
27
|
+
const CLI_HOST_ENV = { opencode: 'OPENCODE_HOST', 'deveco-code': 'DEVECO_HOST' };
|
|
28
|
+
const CLI_PORT_ENV = { opencode: 'OPENCODE_PORT', 'deveco-code': 'DEVECO_PORT' };
|
|
29
|
+
|
|
30
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 解析某 CLI 类型的 serve 连接配置(bin / host / port)。
|
|
34
|
+
*
|
|
35
|
+
* 优先级与 Server/CLI 适配器、check-env.mjs 的 resolveCliServePort 保持一致:
|
|
36
|
+
* 环境变量(含 .env 链注入)> 默认 localhost:4096。
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [cliType] - 未传时按 resolveCliType() 解析
|
|
39
|
+
* @returns {{ cliType: 'opencode'|'deveco-code', bin: string, host: string, port: number }}
|
|
40
|
+
*/
|
|
41
|
+
export function resolveCliServeConfig(cliType) {
|
|
42
|
+
const type = resolveCliType(cliType);
|
|
43
|
+
const port = Number.parseInt(String(process.env[CLI_PORT_ENV[type]] ?? ''), 10) || 4096;
|
|
44
|
+
const host = String(process.env[CLI_HOST_ENV[type]] ?? '').trim() || 'localhost';
|
|
45
|
+
return { cliType: type, bin: CLI_BINS[type], host, port };
|
|
46
|
+
}
|
|
18
47
|
|
|
19
48
|
/**
|
|
20
|
-
* opencode
|
|
49
|
+
* AI CLI serve 服务管理器(opencode / deveco-code 统一)。
|
|
21
50
|
*
|
|
22
51
|
* @example
|
|
23
|
-
* const mgr = new
|
|
52
|
+
* const mgr = new CliServeManager({ projectRoot });
|
|
24
53
|
* await mgr.ensureRunning(); // 探测 -> 若未运行则拉起
|
|
25
54
|
* await mgr.stop(); // 若由本 CLI 拉起则杀掉
|
|
26
55
|
*/
|
|
27
|
-
export class
|
|
56
|
+
export class CliServeManager {
|
|
28
57
|
/**
|
|
29
58
|
* @param {object} opts
|
|
30
|
-
* @param {string} opts.projectRoot - 项目根(
|
|
31
|
-
* @param {
|
|
59
|
+
* @param {string} opts.projectRoot - 项目根(serve 需在上一级目录运行)
|
|
60
|
+
* @param {string} [opts.cliType] - CLI 类型(默认按 CLI_TYPE 环境解析)
|
|
61
|
+
* @param {string} [opts.host] - 服务主机(默认 OPENCODE_HOST/DEVECO_HOST 或 localhost)
|
|
62
|
+
* @param {number} [opts.port] - 服务端口(默认 OPENCODE_PORT/DEVECO_PORT 或 4096)
|
|
32
63
|
*/
|
|
33
64
|
constructor(opts = {}) {
|
|
65
|
+
const cfg = resolveCliServeConfig(opts.cliType);
|
|
66
|
+
this.cliType = cfg.cliType;
|
|
67
|
+
this.bin = cfg.bin;
|
|
34
68
|
this.projectRoot = opts.projectRoot || process.cwd();
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
69
|
+
this.host = opts.host || cfg.host;
|
|
70
|
+
this.port = opts.port || cfg.port;
|
|
71
|
+
this.baseUrl = `http://${this.host}:${this.port}`;
|
|
37
72
|
/** @type {import('node:child_process').ChildProcess|null} */
|
|
38
73
|
this.child = null;
|
|
39
74
|
/** @type {boolean} 是否由本 CLI 拉起(决定退出时是否杀) */
|
|
@@ -41,29 +76,28 @@ export class OpencodeManager {
|
|
|
41
76
|
}
|
|
42
77
|
|
|
43
78
|
/**
|
|
44
|
-
* 探测
|
|
45
|
-
* 通过 HTTP /health
|
|
79
|
+
* 探测 serve 是否已在运行。
|
|
80
|
+
* 通过 HTTP /health 连通性判断(opencode / deveco-code 同源 API)。
|
|
46
81
|
* @returns {Promise<{ ok: boolean, message: string }>}
|
|
47
82
|
*/
|
|
48
83
|
async checkHealth() {
|
|
49
84
|
try {
|
|
50
|
-
// 轻量探测:尝试连接 baseUrl 的 /health(opencode 服务提供)
|
|
51
85
|
const controller = new AbortController();
|
|
52
86
|
const timer = setTimeout(() => controller.abort(), 3000);
|
|
53
87
|
const res = await fetch(`${this.baseUrl}/health`, { signal: controller.signal });
|
|
54
88
|
clearTimeout(timer);
|
|
55
89
|
if (res.ok) {
|
|
56
|
-
return { ok: true, message:
|
|
90
|
+
return { ok: true, message: `${this.cliType} 服务正常 (${this.baseUrl})` };
|
|
57
91
|
}
|
|
58
|
-
return { ok: false, message:
|
|
92
|
+
return { ok: false, message: `${this.cliType} 服务响应异常 (HTTP ${res.status})` };
|
|
59
93
|
} catch {
|
|
60
|
-
return { ok: false, message:
|
|
94
|
+
return { ok: false, message: `${this.cliType} 服务未运行 (${this.baseUrl})` };
|
|
61
95
|
}
|
|
62
96
|
}
|
|
63
97
|
|
|
64
98
|
/**
|
|
65
|
-
* 确保
|
|
66
|
-
*
|
|
99
|
+
* 确保 serve 正在运行。
|
|
100
|
+
* 未运行时自动拉起(在项目上一级目录执行;远端 host 只探测不拉起)。
|
|
67
101
|
* @returns {Promise<{ ok: boolean, message: string, spawned: boolean }>}
|
|
68
102
|
*/
|
|
69
103
|
async ensureRunning() {
|
|
@@ -72,12 +106,21 @@ export class OpencodeManager {
|
|
|
72
106
|
return { ok: true, message: health.message, spawned: false };
|
|
73
107
|
}
|
|
74
108
|
|
|
109
|
+
// 远端 host:无法本地拉起,直接返回探测结果
|
|
110
|
+
if (!LOCAL_HOSTS.has(this.host)) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
message: `${this.cliType} 服务不可达 (${this.baseUrl}),且远端 host 无法自动拉起,请先启动 ${this.bin} serve`,
|
|
114
|
+
spawned: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
75
118
|
// 未运行 → 尝试拉起
|
|
76
119
|
const cwd = path.resolve(this.projectRoot, '..');
|
|
77
|
-
this._log(`尝试启动
|
|
120
|
+
this._log(`尝试启动 ${this.bin} serve --port ${this.port} (cwd=${cwd})...`);
|
|
78
121
|
|
|
79
122
|
try {
|
|
80
|
-
const child = spawn(
|
|
123
|
+
const child = spawn(this.bin, ['serve', '--port', String(this.port)], {
|
|
81
124
|
cwd,
|
|
82
125
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
83
126
|
shell: process.platform === 'win32',
|
|
@@ -100,7 +143,7 @@ export class OpencodeManager {
|
|
|
100
143
|
await this._sleep(1000);
|
|
101
144
|
const healthAgain = await this.checkHealth();
|
|
102
145
|
if (healthAgain.ok) {
|
|
103
|
-
return { ok: true, message: `已启动
|
|
146
|
+
return { ok: true, message: `已启动 ${this.cliType} serve (${this.baseUrl})`, spawned: true };
|
|
104
147
|
}
|
|
105
148
|
// 子进程提前退出则报错
|
|
106
149
|
if (child.exitCode != null) {
|
|
@@ -111,25 +154,25 @@ export class OpencodeManager {
|
|
|
111
154
|
const stderrSnippet = stderrLines.join('\n ');
|
|
112
155
|
return {
|
|
113
156
|
ok: false,
|
|
114
|
-
message:
|
|
157
|
+
message: `${this.cliType} serve 启动超时或失败` + (stderrSnippet ? `:\n ${stderrSnippet}` : ''),
|
|
115
158
|
spawned: true,
|
|
116
159
|
};
|
|
117
160
|
} catch (err) {
|
|
118
161
|
return {
|
|
119
162
|
ok: false,
|
|
120
|
-
message: `启动
|
|
163
|
+
message: `启动 ${this.cliType} serve 失败: ${err.message}`,
|
|
121
164
|
spawned: false,
|
|
122
165
|
};
|
|
123
166
|
}
|
|
124
167
|
}
|
|
125
168
|
|
|
126
169
|
/**
|
|
127
|
-
* 停止由本 CLI 拉起的
|
|
170
|
+
* 停止由本 CLI 拉起的 serve 进程。
|
|
128
171
|
*/
|
|
129
172
|
async stop() {
|
|
130
173
|
if (!this.spawnedByUs || !this.child || this.child.pid == null) return;
|
|
131
174
|
const pid = this.child.pid;
|
|
132
|
-
this._log(`停止由本 CLI 拉起的
|
|
175
|
+
this._log(`停止由本 CLI 拉起的 ${this.cliType} serve (pid=${pid})`);
|
|
133
176
|
try {
|
|
134
177
|
if (process.platform === 'win32') {
|
|
135
178
|
execSync(`taskkill /pid ${pid} /T /F`, { stdio: 'ignore' });
|
|
@@ -151,6 +194,9 @@ export class OpencodeManager {
|
|
|
151
194
|
_sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
152
195
|
|
|
153
196
|
_log(msg) {
|
|
154
|
-
process.stderr.write(`[
|
|
197
|
+
process.stderr.write(`[${this.bin}] ${msg}\n`);
|
|
155
198
|
}
|
|
156
|
-
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 兼容旧名(历史上仅支持 opencode)。新代码请使用 CliServeManager。 */
|
|
202
|
+
export const OpencodeManager = CliServeManager;
|
package/lib/orch.js
CHANGED
|
@@ -130,6 +130,17 @@ async function cmdOrchRun({ flags, rest, projectRoot, dataDir, skillDir, jsonMod
|
|
|
130
130
|
return EXIT.ERROR;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// ── 环境预检(引擎执行前快速失败:凭据/目录类问题直接退出码 1) ──
|
|
134
|
+
{
|
|
135
|
+
const { precheckEnv, reportPrecheckFailures } = await import('./doctor.js');
|
|
136
|
+
const pre = await precheckEnv({ projectRoot, dataDir, skillDir });
|
|
137
|
+
if (!pre.ok) {
|
|
138
|
+
if (jsonMode) out({ ok: false, error: '环境预检未通过', taskId: taskDef.taskId, failures: pre.failures });
|
|
139
|
+
else reportPrecheckFailures(pre.failures);
|
|
140
|
+
return EXIT.ERROR;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
if (!quiet && !jsonMode) {
|
|
134
145
|
u.logProgress(`${u.C.cyan}编排任务已提交: ${u.C.reset}${u.C.bold}${taskDef.taskId}${u.C.reset}`);
|
|
135
146
|
u.logProgress(` 任务定义: ${taskDefFile}`);
|
|
@@ -142,10 +153,10 @@ async function cmdOrchRun({ flags, rest, projectRoot, dataDir, skillDir, jsonMod
|
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
const r = await _runner();
|
|
145
|
-
const ctx = r.createRunContext({ projectRoot, dataDir, skillDir,
|
|
156
|
+
const ctx = r.createRunContext({ projectRoot, dataDir, skillDir, verbose });
|
|
146
157
|
|
|
147
|
-
// 挂载
|
|
148
|
-
const oc = await
|
|
158
|
+
// 挂载 AI CLI serve 生命周期
|
|
159
|
+
const oc = await _ensureCliServe(ctx, u, jsonMode, out);
|
|
149
160
|
if (!oc) return EXIT.ERROR;
|
|
150
161
|
|
|
151
162
|
// 信号处理
|
|
@@ -220,9 +231,20 @@ async function cmdOrchStart({ flags, rest, projectRoot, dataDir, skillDir, jsonM
|
|
|
220
231
|
return EXIT.ERROR;
|
|
221
232
|
}
|
|
222
233
|
|
|
234
|
+
// ── 环境预检(引擎执行前快速失败:凭据/目录类问题直接退出码 1) ──
|
|
235
|
+
{
|
|
236
|
+
const { precheckEnv, reportPrecheckFailures } = await import('./doctor.js');
|
|
237
|
+
const pre = await precheckEnv({ projectRoot, dataDir, skillDir });
|
|
238
|
+
if (!pre.ok) {
|
|
239
|
+
if (jsonMode) out({ ok: false, error: '环境预检未通过', taskId, failures: pre.failures });
|
|
240
|
+
else reportPrecheckFailures(pre.failures);
|
|
241
|
+
return EXIT.ERROR;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
223
245
|
const r = await _runner();
|
|
224
|
-
const ctx = r.createRunContext({ projectRoot, dataDir, skillDir,
|
|
225
|
-
const oc = await
|
|
246
|
+
const ctx = r.createRunContext({ projectRoot, dataDir, skillDir, verbose });
|
|
247
|
+
const oc = await _ensureCliServe(ctx, u, jsonMode, out);
|
|
226
248
|
if (!oc) return EXIT.ERROR;
|
|
227
249
|
|
|
228
250
|
let interrupted = false;
|
|
@@ -410,15 +432,15 @@ async function cmdOrchTasks({ flags, rest, dataDir, jsonMode, out, u }) {
|
|
|
410
432
|
|
|
411
433
|
// ─── 工具 ──────────────────────────────────────────────────────────────────
|
|
412
434
|
|
|
413
|
-
/** 确保
|
|
414
|
-
async function
|
|
415
|
-
const {
|
|
416
|
-
const oc = new
|
|
435
|
+
/** 确保 AI CLI serve 运行(run/start 需要执行环境;load/progress 不需要)。 */
|
|
436
|
+
async function _ensureCliServe(ctx, u, jsonMode, out) {
|
|
437
|
+
const { CliServeManager } = await import('./opencode.js');
|
|
438
|
+
const oc = new CliServeManager({ projectRoot: ctx.projectRoot, cliType: ctx.cliType });
|
|
417
439
|
const ocHealth = await oc.ensureRunning();
|
|
418
440
|
if (!ocHealth.ok) {
|
|
419
441
|
if (jsonMode) out({ ok: false, error: ocHealth.message });
|
|
420
442
|
process.stderr.write(`\n错误: ${ocHealth.message}\n`);
|
|
421
|
-
process.stderr.write(
|
|
443
|
+
process.stderr.write(`请先启动 ${oc.cliType} serve(项目上一级目录: ${oc.bin} serve)后重试。\n`);
|
|
422
444
|
await oc.stop();
|
|
423
445
|
return null;
|
|
424
446
|
}
|
package/lib/runner.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import fs from 'node:fs';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { resolveCliType } from './config.js';
|
|
23
24
|
|
|
24
25
|
// ─── vendor 路径解析 ─────────────────────────────────────────────────────────
|
|
25
26
|
// 优先用 vendor/Server 下的副本(发布态);开发期允许回退到仓库 Server/ 源码。
|
|
@@ -99,14 +100,16 @@ class RunContext {
|
|
|
99
100
|
* @param {string} opts.projectRoot
|
|
100
101
|
* @param {string} opts.dataDir
|
|
101
102
|
* @param {string} opts.skillDir
|
|
102
|
-
* @param {string} opts.cliType
|
|
103
|
+
* @param {string} [opts.cliType] - CLI 类型,缺省时按 CLI_TYPE 解析(默认 deveco-code)
|
|
103
104
|
* @param {boolean} opts.verbose
|
|
104
105
|
*/
|
|
105
106
|
constructor(opts = {}) {
|
|
106
107
|
this.projectRoot = opts.projectRoot || process.cwd();
|
|
107
108
|
this.dataDir = opts.dataDir || '';
|
|
108
109
|
this.skillDir = opts.skillDir || '';
|
|
109
|
-
|
|
110
|
+
// CLI 类型解析:显式参数 > CLI_TYPE(env/.env 链)> deveco-code(默认),
|
|
111
|
+
// 与 Server/Routes/server.js 的 createServer 默认行为一致。
|
|
112
|
+
this.cliType = resolveCliType(opts.cliType);
|
|
110
113
|
this.verbose = !!opts.verbose;
|
|
111
114
|
this._ready = false;
|
|
112
115
|
this.cliRoot = opts.cliRoot || CLI_ROOT;
|
|
@@ -132,9 +135,9 @@ class RunContext {
|
|
|
132
135
|
flexRunnerFactory: null,
|
|
133
136
|
});
|
|
134
137
|
|
|
135
|
-
// 注入共享 cli:Brain/TestCheck
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
+
// 注入共享 cli:Brain/TestCheck 若不注入,会各自惰性 new CLIController()
|
|
139
|
+
// (其默认取 env CLI_TYPE,但 MessageArchive 基于 vendor/Server 相对路径上溯
|
|
140
|
+
// → 落到 cli/db/Session,数据错位)。注入后会话归档统一写用户数据目录。
|
|
138
141
|
this.brainController = new (await this._importBrainController())({
|
|
139
142
|
storage: this.storage,
|
|
140
143
|
skillDirectory: this.skillDir,
|