@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/lib/opencode.js CHANGED
@@ -1,11 +1,15 @@
1
1
  /**
2
- * opencode.js —— opencode serve 生命周期管理
2
+ * opencode.js —— AI CLI serve 生命周期管理(opencode / deveco-code)
3
3
  *
4
4
  * 策略(A4):
5
- * - 探测:操作前 checkHealth()(SDK session.list())
6
- * - 拉起:CLI 自持 spawn('opencode serve'),透传 stderr 前几行;已在运行则复用不接管
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):opencode 需在项目上一级目录执行 opencode serve;
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 fs from 'node:fs';
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 OpencodeManager({ projectRoot });
52
+ * const mgr = new CliServeManager({ projectRoot });
24
53
  * await mgr.ensureRunning(); // 探测 -> 若未运行则拉起
25
54
  * await mgr.stop(); // 若由本 CLI 拉起则杀掉
26
55
  */
27
- export class OpencodeManager {
56
+ export class CliServeManager {
28
57
  /**
29
58
  * @param {object} opts
30
- * @param {string} opts.projectRoot - 项目根(opencode serve 需在上一级目录运行)
31
- * @param {number} [opts.port] - opencode 端口(默认 4096)
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.port = opts.port || 4096;
36
- this.baseUrl = `http://localhost:${this.port}`;
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
- * 探测 opencode 是否已在运行。
45
- * 通过 HTTP /health session.list 连通性判断。
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: `opencode 服务正常 (${this.baseUrl})` };
90
+ return { ok: true, message: `${this.cliType} 服务正常 (${this.baseUrl})` };
57
91
  }
58
- return { ok: false, message: `opencode 服务响应异常 (HTTP ${res.status})` };
92
+ return { ok: false, message: `${this.cliType} 服务响应异常 (HTTP ${res.status})` };
59
93
  } catch {
60
- return { ok: false, message: `opencode 服务未运行 (${this.baseUrl})` };
94
+ return { ok: false, message: `${this.cliType} 服务未运行 (${this.baseUrl})` };
61
95
  }
62
96
  }
63
97
 
64
98
  /**
65
- * 确保 opencode serve 正在运行。
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(`尝试启动 opencode serve (cwd=${cwd})...`);
120
+ this._log(`尝试启动 ${this.bin} serve --port ${this.port} (cwd=${cwd})...`);
78
121
 
79
122
  try {
80
- const child = spawn('opencode', ['serve'], {
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: `已启动 opencode serve (${this.baseUrl})`, spawned: true };
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: 'opencode serve 启动超时或失败' + (stderrSnippet ? `:\n ${stderrSnippet}` : ''),
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: `启动 opencode serve 失败: ${err.message}`,
163
+ message: `启动 ${this.cliType} serve 失败: ${err.message}`,
121
164
  spawned: false,
122
165
  };
123
166
  }
124
167
  }
125
168
 
126
169
  /**
127
- * 停止由本 CLI 拉起的 opencode 进程。
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 拉起的 opencode serve (pid=${pid})`);
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(`[opencode] ${msg}\n`);
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, cliType: 'opencode', verbose });
156
+ const ctx = r.createRunContext({ projectRoot, dataDir, skillDir, verbose });
146
157
 
147
- // 挂载 opencode 生命周期
148
- const oc = await _ensureOpcodene(ctx, u, jsonMode, out);
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, cliType: 'opencode', verbose });
225
- const oc = await _ensureOpcodene(ctx, u, jsonMode, out);
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
- /** 确保 opencode serve 运行(run/start 需要执行环境;load/progress 不需要)。 */
414
- async function _ensureOpcodene(ctx, u, jsonMode, out) {
415
- const { OpencodeManager } = await import('./opencode.js');
416
- const oc = new OpencodeManager({ projectRoot: ctx.projectRoot });
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('请先启动 opencode serve(项目上一级目录: opencode serve)后重试。\n');
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
- this.cliType = opts.cliType || 'opencode';
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 若不注入,会惰性 new CLIController('opencode')
136
- // 其默认 MessageArchive 基于 vendor/Server 相对路径上溯 → 落到 cli/db/Session(数据错位)。
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,