@ohos-cpf/3rdloop 0.0.1 → 0.0.2

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/update.js ADDED
@@ -0,0 +1,280 @@
1
+ /**
2
+ * update.js —— 自更新(3rdloop update)
3
+ *
4
+ * 职责:
5
+ * - 检测本地版本(getCliVersion)与 npm registry 最新版
6
+ * - 识别开发态(npm link 软链)与正式全局安装
7
+ * - 交互确认后通过 npm install -g 重装
8
+ *
9
+ * 交互/输出契约:
10
+ * - 进度/日志 → stderr;--json 结构化结果 → stdout
11
+ * - 非 TTY 下交互确认自动拒绝(需 --yes 配合脚本化更新)
12
+ *
13
+ * 安全:
14
+ * - registry 默认尊重用户 npm 配置(镜像源),--registry 可覆盖
15
+ * - 全部经 child_process.execFileSync 调 npm,避免 shell 注入
16
+ */
17
+
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import readline from 'node:readline';
21
+ import { execFileSync } from 'node:child_process';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ // 本文件在 cli/lib/ 下
25
+ const CLI_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const PKG_NAME = '@ohos-cpf/3rdloop';
27
+ const BIN_ABS = path.join(CLI_ROOT, 'bin', '3rdloop.mjs');
28
+
29
+ // ── 工具 ─────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * 轻量语义化版本比较。返回负数/零/正数;pre-release 视为低于正式版。
33
+ */
34
+ export function compareVersions(a, b) {
35
+ const parse = (v) => String(v || '')
36
+ .trim()
37
+ .replace(/^v/i, '')
38
+ .split(/[\.+\-]/)
39
+ .map((part, idx) => idx < 3 ? parseInt(part, 10) || 0 : part);
40
+ const pa = parse(a);
41
+ const pb = parse(b);
42
+ for (let i = 0; i < 3; i++) {
43
+ const na = pa[i] || 0;
44
+ const nb = pb[i] || 0;
45
+ if (na !== nb) return na - nb;
46
+ }
47
+ // 无 pre-release(第 4 部分为空 / 纯数字)视为正式版,大于带 pre 的版本
48
+ const preA = pa[3];
49
+ const preB = pb[3];
50
+ if (!preA && preB) return 1;
51
+ if (preA && !preB) return -1;
52
+ return String(preA).localeCompare(String(preB));
53
+ }
54
+
55
+ /**
56
+ * 检测当前命令是否为 npm link 软链(开发态)。
57
+ *
58
+ * 原理:npm link 的全局 bin 是指向仓库 bin 的软链;真实全局安装则是
59
+ * node_modules 内的副本。比较 process.argv[1] 与仓库内 bin 的 realpath 是否相同。
60
+ */
61
+ export function detectLinkMode(argv1 = process.argv[1]) {
62
+ try {
63
+ const realArgv = fs.realpathSync(argv1 || '');
64
+ const realLocal = fs.realpathSync(BIN_ABS);
65
+ return realArgv === realLocal;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 获取最新版本。
73
+ * 优先 `npm view`(尊重用户 .npmrc 镜像配置);失败回退 packument API。
74
+ * @returns {Promise<string | null>} latest 版本号;查询失败返回 null
75
+ */
76
+ export async function fetchLatestVersion({ registry } = {}) {
77
+ const args = ['view', PKG_NAME, 'version'];
78
+ if (registry) args.push('--registry', registry);
79
+ try {
80
+ const out = execFileSync('npm', args, {
81
+ encoding: 'utf-8',
82
+ stdio: 'pipe',
83
+ timeout: 30000,
84
+ });
85
+ const v = out.trim().split('\n')[0];
86
+ return v || null;
87
+ } catch { /* 回退 packument API */ }
88
+
89
+ // packument API 兜底(尊重用户 npm 镜像配置)
90
+ const registryHost = registry || _detectNpmRegistry();
91
+ try {
92
+ const enc = encodeURIComponent(PKG_NAME);
93
+ const url = `${registryHost.replace(/\/$/, '')}/${enc}/latest`;
94
+ const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
95
+ if (!res.ok) return null;
96
+ const data = await res.json();
97
+ return data.version || null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /** 从用户 npm 配置读取 registry(无 --registry 时使用镜像源)。 */
104
+ function _detectNpmRegistry() {
105
+ try {
106
+ const out = execFileSync('npm', ['config', 'get', 'registry'], {
107
+ encoding: 'utf-8',
108
+ stdio: 'pipe',
109
+ timeout: 10000,
110
+ });
111
+ const v = out.trim();
112
+ return v && v !== 'undefined' ? v : 'https://registry.npmjs.org/';
113
+ } catch {
114
+ return 'https://registry.npmjs.org/';
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 交互确认是否更新。非 TTY 自动视为不确认。
120
+ * @returns {Promise<boolean>}
121
+ */
122
+ export async function confirmUpdate({ current, latest } = {}) {
123
+ if (!process.stdin.isTTY) return false;
124
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
125
+ return new Promise((resolve) => {
126
+ rl.question(` 检测到新版本 ${latest}(当前 ${current}),是否更新? [y/N] `, (ans) => {
127
+ rl.close();
128
+ resolve(/^y(es)?$/i.test(ans.trim()));
129
+ });
130
+ });
131
+ }
132
+
133
+ /**
134
+ * 执行更新。
135
+ * @param {object} opts
136
+ * @param {string} opts.latest 目标版本(registry latest)
137
+ * @param {string} [opts.registry] registry 覆盖
138
+ * @param {boolean} [opts.force] 开发态也强制替换
139
+ * @param {boolean} [opts.isLink] 是否 link 模式(可注入便于测试)
140
+ */
141
+ export function performUpdate({ latest, registry, force = false, isLink = detectLinkMode() } = {}) {
142
+ const installArgs = ['install', '-g', `${PKG_NAME}@latest`];
143
+ if (registry) installArgs.push('--registry', registry);
144
+
145
+ // 开发态(npm link):默认警告并跳过,仅提示 git pull
146
+ if (isLink && !force) {
147
+ process.stderr.write(
148
+ `\n[update] 检测到当前为开发态(npm link),已跳过自更新。\n` +
149
+ ` 开发态请用 git pull 更新仓库源码(数据目录 ~/.3lib/3rdloop/db 不受影响)。\n` +
150
+ ` 如需强制用正式包替换链接,可加 --force。\n`
151
+ );
152
+ return { updated: false, skipped: 'link-mode' };
153
+ }
154
+
155
+ // 开发态 + --force:先解除链接
156
+ if (isLink && force) {
157
+ process.stderr.write(`[update] 检测到 npm link,先解除链接...\n`);
158
+ execFileSync('npm', ['unlink', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
159
+ }
160
+
161
+ process.stderr.write(`[update] 卸载旧版本(@${PKG_NAME})...\n`);
162
+ try {
163
+ execFileSync('npm', ['uninstall', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
164
+ } catch { /* 卸载失败不阻断重装 */ }
165
+
166
+ process.stderr.write(`[update] 安装最新版本...\n`);
167
+ execFileSync('npm', installArgs, { encoding: 'utf-8', stdio: 'inherit' });
168
+
169
+ return { updated: true, version: latest };
170
+ }
171
+
172
+ /**
173
+ * update 命令主入口。
174
+ * @param {object} opts
175
+ * @param {boolean} opts.jsonMode
176
+ * @param {string[]} opts.rest 位置参数 + flags
177
+ * @returns {Promise<{ok: boolean, skipLink?: boolean, message?: string, current?: string, latest?: string}>}
178
+ */
179
+ export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
180
+ const checkOnly = rest.includes('--check');
181
+ const yes = rest.includes('--yes') || rest.includes('-y');
182
+ const force = rest.includes('--force');
183
+ const regIdx = rest.indexOf('--registry');
184
+ const registry = regIdx !== -1 ? rest[regIdx + 1] : undefined;
185
+
186
+ // update 用法说明(--help,离线可测,不触碰网络)
187
+ if (rest.includes('--help') || rest.includes('-h')) {
188
+ const help = [
189
+ '用法: 3rdloop update [--check] [--yes] [--force] [--registry <url>]',
190
+ '',
191
+ ' 默认: 检测本地版本与 npm registry 最新版,差异存在时交互确认后重装',
192
+ ' --check 仅检查是否有新版本,不执行更新',
193
+ ' --yes, -y 跳过交互确认直接更新(非 TTY 必须)',
194
+ ' --force 开发态(npm link)也强制替换为 registry 正式包',
195
+ ' --registry <url> 覆盖 npm registry(默认尊重用户 npm 镜像配置)',
196
+ ' 注意: 数据目录 ~/.3lib/3rdloop/db 与 SKILL 配置不受更新影响',
197
+ ].join('\n');
198
+ process.stdout.write(help + '\n');
199
+ return { ok: true, helped: true };
200
+ }
201
+
202
+ // 拒绝多余的位置参数(保持命令简单)
203
+ const unknowns = rest.filter(a => !a.startsWith('--'));
204
+ if (unknowns.length > 0) {
205
+ process.stderr.write(`未知参数: ${unknowns.join(' ')}(update 不接受位置参数)\n`);
206
+ if (jsonMode) process.stdout.write(JSON.stringify({ ok: false, error: '未知参数' }, null, 2) + '\n');
207
+ return { ok: false, error: '未知参数' };
208
+ }
209
+
210
+ const run = await import('./config.js');
211
+ const current = run.getCliVersion();
212
+
213
+ const latest = await fetchLatestVersion({ registry });
214
+ if (!latest) {
215
+ const msg = `无法查询 ${PKG_NAME} 的 registry 最新版本(网络或镜像配置问题)`;
216
+ process.stderr.write(`\n[update] ${msg}\n`);
217
+ if (jsonMode) process.stdout.write(JSON.stringify({ ok: false, error: msg }, null, 2) + '\n');
218
+ return { ok: false, error: msg };
219
+ }
220
+
221
+ if (jsonMode) {
222
+ process.stdout.write(JSON.stringify({
223
+ ok: true,
224
+ latest,
225
+ current,
226
+ upToDate: compareVersions(current, latest) >= 0,
227
+ }, null, 2) + '\n');
228
+ if (!checkOnly) {
229
+ // --json 下不交互,直接更新(用户显式要求命令行为,等价 --yes)
230
+ try {
231
+ const r = performUpdate({ latest, registry, force });
232
+ process.stdout.write(JSON.stringify({ ...r, ok: true }, null, 2) + '\n');
233
+ return { ok: true, ...r };
234
+ } catch (err) {
235
+ const msg = `更新失败: ${err.message}`;
236
+ process.stderr.write(`\n[update] ${msg}\n`);
237
+ return { ok: false, error: msg };
238
+ }
239
+ }
240
+ return { ok: true, current, latest };
241
+ }
242
+
243
+ // ── 非 JSON 模式 ──────────────────────────────────────────
244
+ process.stderr.write(`当前版本: ${current} 最新版本: ${latest}\n`);
245
+
246
+ if (compareVersions(current, latest) >= 0) {
247
+ process.stderr.write(`\n[update] 已是最新版本,无需更新。\n`);
248
+ return { ok: true, upToDate: true, current, latest };
249
+ }
250
+
251
+ if (checkOnly) {
252
+ process.stderr.write(`\n[update] 可更新到 ${latest}(使用 "3rdloop update" 执行更新)\n`);
253
+ return { ok: true, current, latest };
254
+ }
255
+
256
+ // 交互确认(非 TTY 自动拒绝;--yes 跳过)
257
+ const confirmed = yes ? true : await confirmUpdate({ current, latest });
258
+
259
+ if (jsonMode) {
260
+ if (!confirmed) {
261
+ process.stdout.write(JSON.stringify({ ok: true, confirmed: false }, null, 2) + '\n');
262
+ return { ok: true, skipped: 'declined' };
263
+ }
264
+ }
265
+
266
+ if (!confirmed) {
267
+ process.stderr.write(`已取消更新。\n`);
268
+ return { ok: true, skipped: 'declined' };
269
+ }
270
+
271
+ process.stderr.write(`\n`);
272
+ try {
273
+ const r = performUpdate({ latest, registry, force });
274
+ return { ok: true, ...r };
275
+ } catch (err) {
276
+ const msg = `更新失败: ${err.message}`;
277
+ process.stderr.write(`\n[update] ${msg}\n`);
278
+ return { ok: false, error: msg };
279
+ }
280
+ }
package/lib/workflow.js CHANGED
@@ -8,6 +8,8 @@
8
8
  * - 复用 runner.js 的 orchLoad / orchStart 链路执行编排,沿用 orch.js 的
9
9
  * 信号处理、进度渲染与退出码约定(passed=0 / failed=2 / TIMEOUT=3 / SIGINT=130 / SIGTERM=143)
10
10
  * - --print-taskdef dry-run:只构造并打印/落盘 taskDef,不触碰引擎
11
+ * - afterRun 钩子:工作流声明对象上可选的 async 函数,编排结束后执行
12
+ * (成功/失败均调用;入参 { params, taskDef, result, dataDir, cwd };抛错只记录不改退出码)
11
13
  *
12
14
  * 产品形态与责任边界:
13
15
  * - 本期只提供机制(注册/分发/taskDef 构造/编排执行),不预置业务工作流
@@ -88,7 +90,7 @@ export async function cmdWorkflow({ wf, wfWords, flags, env }) {
88
90
  // ── 构造 taskDef ────────────────────────────────────────────────
89
91
  let taskDef;
90
92
  try {
91
- taskDef = await wf.buildTaskDef(params, { dataDir });
93
+ taskDef = await wf.buildTaskDef(params, { dataDir, cwd: process.cwd() });
92
94
  } catch (err) {
93
95
  process.stderr.write(`工作流 "${wf.name}" 构造 taskDef 失败: ${err.message}\n`);
94
96
  return EXIT.ERROR;
@@ -174,6 +176,22 @@ export async function cmdWorkflow({ wf, wfWords, flags, env }) {
174
176
  await r.orchLoad(ctx, taskDef);
175
177
  const result = await _startAndRender(ctx, r, taskDef.taskId, { jsonMode, quiet, verbose, out, u, started });
176
178
 
179
+ // ── afterRun 钩子:工作流声明的后置动作(如报告复制)────────
180
+ // 成功/失败均执行(由工作流自身决定是否动作);抛错只记录,不改变退出码。
181
+ if (typeof wf.afterRun === 'function') {
182
+ try {
183
+ await wf.afterRun({
184
+ params,
185
+ taskDef,
186
+ result,
187
+ dataDir,
188
+ cwd: process.cwd(),
189
+ });
190
+ } catch (hookErr) {
191
+ process.stderr.write(`[workflow] afterRun 钩子执行失败(忽略): ${hookErr.message}\n`);
192
+ }
193
+ }
194
+
177
195
  if (jsonMode) {
178
196
  out({
179
197
  ok: result.passed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohos-cpf/3rdloop",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "3rdLibraryLoop 三方库自动化检视 CLI:嵌入式复用核心引擎,任务生命周期管理(submit/run/wait/progress/abort/list/result/doctor)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -61,7 +61,7 @@ export class SkillExecutor {
61
61
  async getCLI() {
62
62
  if (!this._cli) {
63
63
  const CLIController = await _loadCLIController();
64
- this._cli = new CLIController('opencode');
64
+ this._cli = new CLIController();
65
65
  }
66
66
  return this._cli;
67
67
  }
@@ -90,7 +90,7 @@ export class SkillExecutor {
90
90
  // ── 1. 健康检查 ──
91
91
  const health = await cli.checkHealth();
92
92
  if (health.status !== 'ok') {
93
- throw new Error(`opencode 服务不可用: ${health.message || 'health check failed'}`);
93
+ throw new Error(`CLI(${cli.getType()}) 服务不可用: ${health.message || 'health check failed'}`);
94
94
  }
95
95
 
96
96
  // ── 1b. 确保工作目录存在 ──
@@ -1,13 +1,35 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import OpenCodeCLI from './opencode/index.js';
4
+ import DevEcoCodeCLI from './deveco-code/index.js';
2
5
  import { StatsService } from '../library/StatsService.js';
3
6
 
7
+ // 加载 Server/.env(本文件在 CLI/ 下,上一级目录为 Server/)。
8
+ // dotenv 为可选依赖:未安装时跳过(如纯 mock 测试场景)。
9
+ // dotenv 默认不覆盖已存在的环境变量,因此 shell export / --type 参数优先级仍高于 .env。
10
+ try {
11
+ const dotenv = (await import('dotenv')).default;
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
14
+ } catch {
15
+ // dotenv 未安装或 .env 不存在,跳过
16
+ }
17
+
4
18
  /**
5
19
  * 通用 CLI 控制类
6
20
  *
7
- * 封装 opencode-cli,对外暴露统一的 Session 管理接口。
21
+ * 封装 AI 编程助手 CLI(opencode / deveco-code),对外暴露统一的 Session 管理接口。
22
+ *
23
+ * 默认 CLI 类型可在 Server/.env 中配置 CLI_TYPE=opencode|deveco-code,
24
+ * 也可通过 shell 环境变量 CLI_TYPE 指定(优先级高于 .env),均未设置时默认 'opencode'。
25
+ * 显式传入 type 参数优先级最高。
26
+ *
27
+ * 连接配置同样支持 .env:OPENCODE_HOST/OPENCODE_PORT、DEVECO_HOST/DEVECO_PORT。
8
28
  *
9
29
  * @example
10
- * const cli = new CLIController('opencode', { host: 'localhost', port: '4096' });
30
+ * const cli = new CLIController('opencode'); // 连接 localhost:4096
31
+ * const cli = new CLIController('deveco-code'); // 连接 localhost:4096(两者默认端口一致)
32
+ * const cli = new CLIController(); // 使用 .env / 环境变量 CLI_TYPE 指定的默认类型
11
33
  *
12
34
  * // 通用接口
13
35
  * await cli.checkHealth();
@@ -21,7 +43,8 @@ import { StatsService } from '../library/StatsService.js';
21
43
  */
22
44
  class CLIController {
23
45
  /**
24
- * @param {'opencode'} type - CLI 类型
46
+ * @param {'opencode'|'deveco-code'} [type] - CLI 类型,缺省时取环境变量 CLI_TYPE
47
+ * (shell 环境变量 > Server/.env),仍未设置则 'opencode'
25
48
  * @param {object} [options={}] - 传递给对应适配器的配置项
26
49
  *
27
50
  * opencode 选项:
@@ -29,19 +52,28 @@ class CLIController {
29
52
  * options.port {string} - 服务端口(默认 4096)
30
53
  * options.baseUrl {string} - 完整 URL,优先级高于 host/port
31
54
  *
55
+ * deveco-code 选项:
56
+ * options.host {string} - 服务主机(默认 localhost)
57
+ * options.port {string} - 服务端口(默认 4096,与 opencode 一致;两者需同时
58
+ * 运行时可通过 DEVECO_PORT 指定不同端口)
59
+ * options.baseUrl {string} - 完整 URL,优先级高于 host/port
60
+ *
32
61
  * 公共选项:
33
62
  * options.archive - MessageArchive 实例(消息存档与卡死检测),不传则适配器内部自建
34
63
  */
35
- constructor(type = 'opencode', options = {}) {
64
+ constructor(type = process.env.CLI_TYPE || 'opencode', options = {}) {
36
65
  this.type = type;
37
66
 
38
67
  switch (type) {
39
68
  case 'opencode':
40
69
  this.adapter = new OpenCodeCLI(options);
41
70
  break;
71
+ case 'deveco-code':
72
+ this.adapter = new DevEcoCodeCLI(options);
73
+ break;
42
74
  default:
43
75
  throw new Error(
44
- `不支持的 CLI 类型: "${type}",可选值为 'opencode'`
76
+ `不支持的 CLI 类型: "${type}",可选值为 'opencode'、'deveco-code'`
45
77
  );
46
78
  }
47
79
  }
@@ -224,7 +256,7 @@ class CLIController {
224
256
 
225
257
  /**
226
258
  * 返回当前使用的 CLI 类型
227
- * @returns {'opencode'}
259
+ * @returns {'opencode'|'deveco-code'}
228
260
  */
229
261
  getType() {
230
262
  return this.type;
@@ -232,12 +264,12 @@ class CLIController {
232
264
 
233
265
  /**
234
266
  * 返回底层适配器实例(供高级用途)
235
- * @returns {OpenCodeCLI}
267
+ * @returns {OpenCodeCLI|DevEcoCodeCLI}
236
268
  */
237
269
  getAdapter() {
238
270
  return this.adapter;
239
271
  }
240
272
  }
241
273
 
242
- export { CLIController, OpenCodeCLI };
274
+ export { CLIController, OpenCodeCLI, DevEcoCodeCLI };
243
275
  export default CLIController;
@@ -0,0 +1,71 @@
1
+ import OpenCodeCLI from '../opencode/index.js';
2
+
3
+ /**
4
+ * DevEco Code CLI 适配器
5
+ *
6
+ * 对接华为开源的 DevEco Code(https://gitcode.com/openharmony-sig/deveco-code),
7
+ * 面向 HarmonyOS 开发场景的 AI Agent 工具(npm 包 @deveco/deveco-code)。
8
+ *
9
+ * DevEco Code 基于开源项目 OpenCode 扩展开发,其 headless 服务(`deveco serve`)
10
+ * 保留与 opencode 完全一致的 HTTP API(/session、/session/{id}/message、
11
+ * /session/{id}/prompt_async、/session/status 等),因此本适配器直接继承
12
+ * OpenCodeCLI,复用其全部 Session 管理逻辑,仅覆盖连接配置与身份标识:
13
+ *
14
+ * - 环境变量:DEVECO_HOST / DEVECO_PORT(优先级低于 options.host / options.port)
15
+ * - 默认端口:4096(与 opencode 一致,两者同一时间只运行一个,无需改端口配置;
16
+ * 若需与 opencode 服务同时运行,可通过 DEVECO_PORT 指定其他端口)
17
+ * - 存档 meta.cliType:'deveco-code'
18
+ *
19
+ * 服务启动方式:
20
+ * deveco serve (默认即监听 4096)
21
+ * deveco serve --port X (需共存/端口冲突时显式指定)
22
+ * (注意:`deveco serve` 在 4096 被占用且未指定 --port 时会随机分配端口)
23
+ *
24
+ * @example
25
+ * const cli = new CLIController('deveco-code'); // 连接 localhost:4096
26
+ * await cli.checkHealth();
27
+ */
28
+ class DevEcoCodeCLI extends OpenCodeCLI {
29
+ /** 存档 meta.cliType 标识 */
30
+ static _cliType = 'deveco-code';
31
+
32
+ /** 日志器名称 */
33
+ static _loggerName = 'DevEcoCodeCLI';
34
+
35
+ /**
36
+ * @param {object} options
37
+ * @param {string} [options.host] - deveco 服务主机(默认 DEVECO_HOST 或 localhost)
38
+ * @param {string} [options.port] - deveco 服务端口(默认 DEVECO_PORT 或 4096)
39
+ * @param {string} [options.baseUrl] - 完整 baseUrl,优先级高于 host/port
40
+ * @param {import('../../DbUse/MessageArchive.js').default} [options.archive] - 消息存档服务
41
+ */
42
+ constructor(options = {}) {
43
+ super({
44
+ ...options,
45
+ host: options.host || process.env.DEVECO_HOST || 'localhost',
46
+ port: options.port || process.env.DEVECO_PORT || '4096'
47
+ });
48
+ // cliType / _loggerName 已通过 static 覆盖,在父类构造期间即正确初始化
49
+ }
50
+
51
+ // ─── 运行状态检查 ──────────────────────────────────────────────────────────
52
+
53
+ /**
54
+ * 检查 deveco 服务是否正常运行(复用父类轻量健康检查逻辑,仅覆盖提示文案)
55
+ */
56
+ async checkHealth() {
57
+ try {
58
+ await this.client.session.list();
59
+ return { status: 'ok', message: 'DevEco Code CLI is healthy', target: this.baseUrl };
60
+ } catch (error) {
61
+ this._log('warn', `checkHealth: 连接失败 — ${error.message}`);
62
+ return {
63
+ status: 'error',
64
+ message: `Health check failed: ${error.message}`,
65
+ error: error.message
66
+ };
67
+ }
68
+ }
69
+ }
70
+
71
+ export default DevEcoCodeCLI;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * DevEcoCodeCLI 快捷导出
3
+ * 方便通过 import DevEcoCodeCLI from './cli/deveco-code.js' 引用
4
+ */
5
+ export { default } from './deveco-code/index.js';
@@ -14,6 +14,17 @@ import { createLogger } from '../../Routes/library/logger.js';
14
14
  * OPENCODE_PORT - opencode 服务端口(默认 4096)
15
15
  */
16
16
  class OpenCodeCLI {
17
+ /**
18
+ * CLI 类型标识(写入存档 meta,供导出/审计区分底层 CLI)。
19
+ * 子类通过 static 字段覆盖,父类构造期间即可生效。
20
+ */
21
+ static _cliType = 'opencode';
22
+
23
+ /**
24
+ * 日志器名称。子类通过 static 字段覆盖,父类构造期间即可生效。
25
+ */
26
+ static _loggerName = 'OpenCodeCLI';
27
+
17
28
  /**
18
29
  * @param {object} options
19
30
  * @param {string} [options.host] - opencode 服务主机
@@ -26,6 +37,9 @@ class OpenCodeCLI {
26
37
  this.port = options.port || process.env.OPENCODE_PORT || '4096';
27
38
  this.baseUrl = options.baseUrl || `http://${this.host}:${this.port}`;
28
39
  this.client = null;
40
+ // CLI 类型标识 / 日志器名称(经 static 覆盖,子类构造期间即正确)
41
+ this.cliType = this.constructor._cliType;
42
+ this._loggerName = this.constructor._loggerName;
29
43
  // 消息存档:用于保留对话消息、判断 opencode 是否长时间无新响应(防卡死)
30
44
  this.archive = options.archive ?? new MessageArchive();
31
45
  // 记录各 session 首次进入 busy/retry 的时间戳(ms),用于"busy 停滞兜底"判定
@@ -329,7 +343,7 @@ class OpenCodeCLI {
329
343
  await this.archive.patchMeta(sessionId, {
330
344
  sessionId,
331
345
  title,
332
- cliType: 'opencode',
346
+ cliType: this.cliType,
333
347
  createdAt: nowCST(),
334
348
  status: 'idle'
335
349
  }).catch(e => {
@@ -784,7 +798,7 @@ class OpenCodeCLI {
784
798
  // ─── 内部方法 ──────────────────────────────────────────────────────────────
785
799
 
786
800
  _log(level, msg) {
787
- const logger = createLogger('OpenCodeCLI');
801
+ const logger = createLogger(this._loggerName);
788
802
  switch (level) {
789
803
  case 'error':
790
804
  logger.error(`❌ ${msg}`);
@@ -402,7 +402,7 @@ class FlexRunner {
402
402
  this.#cli = cli;
403
403
  } else {
404
404
  const CLIController = await _loadCLIController();
405
- this.#cli = new CLIController('opencode', {});
405
+ this.#cli = new CLIController();
406
406
  }
407
407
 
408
408
  if (skillSelector) {
@@ -815,7 +815,7 @@ class FlexRunner {
815
815
  // ── 1. 健康检查 ──
816
816
  const health = await this.#cli.checkHealth();
817
817
  if (health.status !== 'ok') {
818
- this._log('error', `opencode 服务不可用: ${health.message}`);
818
+ this._log('error', `CLI(${this.#cli.getType()}) 服务不可用: ${health.message}`);
819
819
  return false;
820
820
  }
821
821
 
@@ -101,7 +101,7 @@ class KnowledgeImporter {
101
101
  async _getCLI() {
102
102
  if (!this._cli) {
103
103
  const CLIController = await _loadCLIController();
104
- this._cli = new CLIController('opencode', {});
104
+ this._cli = new CLIController();
105
105
  }
106
106
  return this._cli;
107
107
  }
@@ -251,7 +251,7 @@ class LoopEngine {
251
251
  let cli = null;
252
252
  try {
253
253
  const mod = await import('../CLI/cli.js');
254
- cli = new mod.CLIController('opencode', {});
254
+ cli = new mod.CLIController();
255
255
  } catch (err) {
256
256
  this._log('warn', `_abortSessions: 加载 CLIController 失败: ${err.message}`);
257
257
  return;
@@ -30,14 +30,15 @@ import KnowledgeImportController from './controllers/KnowledgeImportController.j
30
30
  * const server = await createServer({ port: 3000 });
31
31
  *
32
32
  * 或直接运行:
33
- * node server.js --port 3000 --type opencode
33
+ * node server.js --port 3000 --type deveco-code
34
+ * (也可用环境变量指定默认 CLI:CLI_TYPE=deveco-code node server.js --port 3000)
34
35
  */
35
36
 
36
37
  /**
37
38
  * 创建并启动 HTTP 服务
38
39
  * @param {object} [options]
39
40
  * @param {number} [options.port=3000] - 监听端口
40
- * @param {string} [options.cliType='opencode'] - CLI 类型
41
+ * @param {string} [options.cliType] - CLI 类型,缺省时取环境变量 CLI_TYPE,仍未设置则 'opencode'
41
42
  * @param {object} [options.cliOptions={}] - CLI 适配器配置
42
43
  * @param {CLIController} [options.cli] - 已存在的 CLI 实例(优先于 cliType/cliOptions)
43
44
  * @returns {Promise<http.Server>}
@@ -45,7 +46,7 @@ import KnowledgeImportController from './controllers/KnowledgeImportController.j
45
46
  export async function createServer(options = {}) {
46
47
  const {
47
48
  port = 3000,
48
- cliType = 'opencode',
49
+ cliType = process.env.CLI_TYPE || 'opencode',
49
50
  cliOptions = {},
50
51
  cli = null
51
52
  } = options;
@@ -191,7 +192,7 @@ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
191
192
  const portIdx = args.indexOf('--port');
192
193
  const typeIdx = args.indexOf('--type');
193
194
  const port = portIdx !== -1 ? Number(args[portIdx + 1]) : 3000;
194
- const cliType = typeIdx !== -1 ? args[typeIdx + 1] : 'opencode';
195
+ const cliType = typeIdx !== -1 ? args[typeIdx + 1] : (process.env.CLI_TYPE || 'opencode');
195
196
 
196
197
  createServer({ port, cliType }).catch(err => {
197
198
  console.error('[Server] 启动失败:', err);