@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 CHANGED
@@ -44,10 +44,10 @@ npm i -g @ohos-cpf/3rdloop
44
44
  3rdloop doctor
45
45
  ```
46
46
 
47
- `doctor` 会逐项检查 Node / opencode / `.env` 凭据 / SKILL 目录 / 数据目录,缺什么会明确提示。除此之外还需要了解:
47
+ `doctor` 会逐项检查 Node / CLI_TYPE 配置 / AI CLI(按 `CLI_TYPE` 检测 opencode 或 deveco)/ `.env` 凭据 / SKILL 目录 / 数据目录 / serve 服务连通性,缺什么会明确提示。除此之外还需要了解:
48
48
 
49
49
  - **Node.js** >= 22
50
- - **opencode CLI**:`3rdloop` 会自动拉起 `opencode serve`(在项目上一级目录运行),退出时清理进程树;已存在则复用,一般无需手动管理
50
+ - **AI CLI**(默认 `deveco`,`CLI_TYPE` 可切 `opencode`):`3rdloop` 会自动拉起对应的 `serve` 服务(在项目上一级目录运行),退出时清理进程树;已存在则复用,一般无需手动管理
51
51
  - **LLM 凭据**:`doctor` 提示凭据缺失时,见下面"配置"一节
52
52
 
53
53
  ### 场景二:配置 —— 需要注意哪些配置项
@@ -78,8 +78,9 @@ npm i -g @ohos-cpf/3rdloop
78
78
  # LLM 模型 id
79
79
  3rdloop config set LLM_MODEL glm-5.2
80
80
 
81
- # 默认 CLI 类型(opencode | deveco-code,HarmonyOS 场景切 deveco-code
82
- 3rdloop config set CLI_TYPE deveco-code
81
+ # 默认 CLI 类型(默认 deveco-code;如需切回 opencode
82
+ # 切换后 doctor 按新类型检测,run/step/orch/workflow 拉起对应 serve 并连接
83
+ 3rdloop config set CLI_TYPE opencode
83
84
 
84
85
  # 数据存储目录(默认 ~/.3lib/3rdloop/db,如团队共享目录)
85
86
  3rdloop config set 3LIB_DATA_DIR "D:\code\team\shared-db"
@@ -106,6 +107,8 @@ npm i -g @ohos-cpf/3rdloop
106
107
 
107
108
  `run` 会自动完成"任务拆解 → 编排执行 → 准出审核"的完整大循环,多轮次自动重规划直到通过;退出码 `0` 即通过。
108
109
 
110
+ > **环境预检**:`run`/`submit`/`step run`/`orch run|start` 及工作流在正式执行前会自动做环境预检(Node 版本 / CLI_TYPE / AI CLI / LLM 凭据 / SKILL 目录 / 数据目录)。任一项不通过直接退出码 `1`,并打印对应修复命令(如 `3rdloop config set DASHSCOPE_API_KEY <sk-xxx>`);完整自检用 `3rdloop doctor`。`--print-taskdef` 等 dry-run 不预检。
111
+
109
112
  任务比较耗时、不想干等?提交后立即返回 taskId(进程内继续跑,需保持进程存活),之后可用 `3rdloop progress/wait/result <taskId>` 跟进:
110
113
 
111
114
  ```bash
@@ -134,41 +137,32 @@ npm i -g @ohos-cpf/3rdloop
134
137
 
135
138
  > 提 PR 是非幂等发布操作,失败不会自动重试;执行成功后报告 `ohos-lib-pr-push-report.md` 会自动复制到执行目录。先用 `--print-taskdef` dry-run 可以核对将执行的编排定义。
136
139
 
137
- ### 场景五:用 Web 界面操作(serve / web
138
-
139
- 除了命令行,也可以在浏览器里使用 Web 工作台(提交任务 / 查看循环进度 / PR 检视等),由两个命令提供支撑:
140
+ ### 场景五:用 Web 界面操作(serve)
140
141
 
141
- **`3rdloop serve` —— 启动 Server HTTP 后端**(供前端 / 脚本反向代理调用):
142
+ 除了命令行,也可以在浏览器里使用 Web 工作台(提交任务 / 查看循环进度 / PR 检视等)。`3rdloop serve` 一条命令**同时启动** Server HTTP 后端(默认 3000)与 Web 前端(默认 8080,静态托管 + `/api/*` 反代到后端 + 扩展挂载):
142
143
 
143
144
  ```bash
144
- # 只启动后端(默认 http://127.0.0.1:3000,可用 --port / 3LIB_SERVER_PORT 覆盖)
145
+ # 后端(默认 http://127.0.0.1:3000)+ Web 前端(默认 http://localhost:8080)同时启动
145
146
  3rdloop serve
146
147
 
147
- # 后端 + Web 前端一起拉起(前端默认 8080,可用 --web-port / 3LIB_WEB_PORT 覆盖)
148
- 3rdloop serve --web
149
- ```
150
-
151
- > CLI 的 `run`/`submit`/`wait` 等命令保持嵌入式模式,不经此后端;两者可独立使用。
148
+ # 启动后自动打开浏览器
149
+ 3rdloop serve --open
152
150
 
153
- **`3rdloop web` —— 启动 Web 前端服务**(静态托管 + `/api/*` 反代到后端 + 扩展挂载,默认端口 8080):
154
-
155
- ```bash
156
- # 启动并自动打开浏览器(--backend 默认指向 http://127.0.0.1:3000,配合 serve 使用)
157
- 3rdloop web --open
158
-
159
- # 自定义端口 / 后端地址
160
- 3rdloop web --port 8081 --backend http://127.0.0.1:3000
151
+ # 自定义端口(--port 后端 / --web-port 前端,也可用 3LIB_SERVER_PORT / 3LIB_WEB_PORT 覆盖)
152
+ 3rdloop serve --port 3001 --web-port 8081
161
153
 
162
154
  # 禁用全部扩展(纯核心工作台模式)
163
- 3rdloop web --no-ext
155
+ 3rdloop serve --no-ext
164
156
  ```
165
157
 
158
+ > CLI 的 `run`/`submit`/`wait` 等命令保持嵌入式模式,不经此后端。
159
+
166
160
  扩展机制(tag / issue / prcheck 等二级目录功能以扩展形式按需安装,不随 npm 包分发):
167
161
 
168
162
  ```bash
169
- 3rdloop web install-ext <扩展目录> # 安装扩展到 ~/.3lib/3rdloop/web-ext/
170
- 3rdloop web list-ext # 列出可用扩展(含来源与挂载点)
171
- 3rdloop web remove-ext <扩展名> # 移除已安装扩展
163
+ 3rdloop serve install-ext <扩展目录> # 安装扩展到 ~/.3lib/3rdloop/web-ext/
164
+ 3rdloop serve list-ext # 列出可用扩展(含来源与挂载点)
165
+ 3rdloop serve remove-ext <扩展名> # 移除已安装扩展
172
166
  ```
173
167
 
174
168
  更多进阶用法(任务管理、`step` 单步执行、`orch` 自定义编排、工作流注册等),见仓库 `cli/README_DEVELOP.md` 或 `3rdloop <子命令> --help`。
@@ -260,7 +254,6 @@ LLM 凭据变量(`DASHSCOPE_API_KEY` / `LLM_MODEL` 等):按上文场景二
260
254
  3rdloop update --registry https://registry.npmjs.org/
261
255
  ```
262
256
 
263
- - 以 `npm link` 开发方式安装(连接到仓库源码)时,`update` 默认跳过并提示用 `git pull` 更新源码,加 `--force` 可强制替换为 registry 正式包
264
257
  - 更新只替换程序文件,**数据目录** `~/.3lib/3rdloop/db` **与** `~/.3lib/.env` **配置不受影响**
265
258
 
266
259
 
package/lib/cli.js CHANGED
@@ -34,7 +34,6 @@ import {
34
34
  resolveSkillDir,
35
35
  generateTaskId,
36
36
  getCliVersion,
37
- checkNodeVersion,
38
37
  } from './config.js';
39
38
  import { EXIT, TERMINAL_LOOP_STATES } from './exit-codes.js';
40
39
 
@@ -145,16 +144,16 @@ export async function runCli(argv) {
145
144
  return cmdConfig({ flags, rest, projectRoot, user3libHome, jsonMode, out });
146
145
 
147
146
  case 'serve':
148
- return cmdServe({ flags, rest, jsonMode, dataDir });
147
+ return cmdServe({ flags, rest, jsonMode, dataDir, user3libHome });
149
148
 
150
149
  case 'update':
151
150
  return cmdUpdate({ flags, rest, jsonMode, out });
152
151
 
153
- case 'web':
154
- return cmdWeb({ flags, rest, user3libHome, dataDir, jsonMode, out });
155
-
156
152
  default:
157
153
  process.stderr.write(`未知命令: ${cmd}\n`);
154
+ if (cmd === 'web') {
155
+ process.stderr.write('提示: "3rdloop web" 已合并到 "3rdloop serve"(Web 前端随 serve 默认启动,扩展管理用 3rdloop serve install-ext/list-ext/remove-ext)\n');
156
+ }
158
157
  printHelp();
159
158
  return EXIT.ERROR;
160
159
  }
@@ -180,6 +179,19 @@ async function cmdDoctor({ projectRoot, skillDir, dataDir, quiet }) {
180
179
  return ok ? EXIT.OK : EXIT.ERROR;
181
180
  }
182
181
 
182
+ /**
183
+ * 环境预检 + 失败报告(run/submit 等引擎命令共用)。
184
+ * 通过返回 true;失败打印各项修复提示(--json 时输出结构化结果)并返回 false。
185
+ */
186
+ async function _precheckOrReport({ projectRoot, dataDir, skillDir, jsonMode, out }) {
187
+ const { precheckEnv, reportPrecheckFailures } = await import('./doctor.js');
188
+ const pre = await precheckEnv({ projectRoot, dataDir, skillDir });
189
+ if (pre.ok) return true;
190
+ if (jsonMode) out({ ok: false, error: '环境预检未通过', failures: pre.failures });
191
+ else reportPrecheckFailures(pre.failures);
192
+ return false;
193
+ }
194
+
183
195
  // ─── workflows(列出已注册工作流) ────────────────────────────────
184
196
 
185
197
  async function cmdWorkflows({ jsonMode, out }) {
@@ -224,11 +236,11 @@ async function cmdUpdate({ flags, rest, jsonMode, out }) {
224
236
  return result.ok ? EXIT.OK : EXIT.ERROR;
225
237
  }
226
238
 
227
- // ─── serve(启动 Server HTTP 后端,供前端展示)─────────────────────────
239
+ // ─── serve(同时启动 Server HTTP 后端 + Web 前端)─────────────────
228
240
 
229
- async function cmdServe({ flags, rest, jsonMode, dataDir }) {
241
+ async function cmdServe({ flags, rest, jsonMode, dataDir, user3libHome }) {
230
242
  const { cmdServe: impl } = await import('./serve.js');
231
- return impl({ rest: flags.rest, jsonMode, dataDir });
243
+ return impl({ rest: flags.rest, jsonMode, dataDir, user3libHome });
232
244
  }
233
245
 
234
246
  // ─── run / submit ──────────────────────────────────────────────────
@@ -267,6 +279,11 @@ async function submitAndRunImpl({ flags, rest, projectRoot, dataDir, skillDir, j
267
279
  return EXIT.ERROR;
268
280
  }
269
281
 
282
+ // ── 环境预检(引擎执行前快速失败:凭据/目录类问题直接退出码 1,
283
+ // 避免跑到一半才失败、被误报为任务失败退出码 2)──────────────────
284
+ const preOk = await _precheckOrReport({ projectRoot, dataDir, skillDir, jsonMode, out });
285
+ if (!preOk) return EXIT.ERROR;
286
+
270
287
  // skill-dir 可能来自全局 flags 已在外部解析
271
288
  const r = await runner();
272
289
  const taskId = generateTaskId();
@@ -274,20 +291,19 @@ async function submitAndRunImpl({ flags, rest, projectRoot, dataDir, skillDir, j
274
291
  projectRoot,
275
292
  dataDir,
276
293
  skillDir,
277
- cliType: 'opencode',
278
294
  verbose,
279
295
  });
280
296
 
281
- // 挂载 opencode 生命周期
282
- const { OpencodeManager } = await import('./opencode.js');
283
- const oc = new OpencodeManager({ projectRoot });
297
+ // 挂载 AI CLI serve 生命周期(类型由 CLI_TYPE 决定:opencode | deveco-code)
298
+ const { CliServeManager } = await import('./opencode.js');
299
+ const oc = new CliServeManager({ projectRoot, cliType: ctx.cliType });
284
300
  const ocHealth = await oc.ensureRunning();
285
301
  if (!ocHealth.ok) {
286
302
  if (jsonMode) {
287
303
  process.stdout.write(JSON.stringify({ ok: false, error: ocHealth.message, taskId }, null, 2) + '\n');
288
304
  }
289
305
  process.stderr.write(`\n错误: ${ocHealth.message}\n`);
290
- process.stderr.write('请先启动 opencode serve(项目上一级目录: opencode serve)后重试。\n');
306
+ process.stderr.write(`请先启动 ${oc.cliType} serve(项目上一级目录: ${oc.bin} serve)后重试。\n`);
291
307
  await oc.stop();
292
308
  return EXIT.ERROR;
293
309
  }
@@ -623,14 +639,6 @@ async function cmdOrch({ flags, rest, projectRoot, dataDir, skillDir, jsonMode,
623
639
  return impl({ flags, rest, projectRoot, dataDir, skillDir, jsonMode, quiet, verbose, out, u });
624
640
  }
625
641
 
626
- /**
627
- * 3rdloop web 命令二段分发(转发到 lib/web.js:前端服务 + 扩展挂载)。
628
- */
629
- async function cmdWeb({ flags, rest, user3libHome, dataDir, jsonMode, out }) {
630
- const { cmdWeb: impl } = await import('./web.js');
631
- return impl({ flags, rest, user3libHome, dataDir, jsonMode, out });
632
- }
633
-
634
642
  // ─── 帮助 ─────────────────────────────────────────────────────────
635
643
 
636
644
  function printHelp() {
@@ -659,11 +667,11 @@ function printHelp() {
659
667
  3rdloop workflows 列出已注册的固定编排工作流
660
668
  3rdloop run <工作流名> [--flag <值> ...] 以固定编排执行工作流(阻塞到终态)
661
669
  3rdloop config list|get|set|unset ... 配置 .env 配置项(默认读写 ~/.3lib/.env)
662
- 3rdloop serve [--port N] [-w|--web] [--web-port N] 启动 Server HTTP 后端(供前端展示)
663
- 3rdloop update [--check] [--yes] [--force] [--registry <url>] 自更新到 npm 最新版
664
- 3rdloop web [--port N] [--backend <url>] 启动 Web 前端服务
665
- 3rdloop web install-ext <dir> 安装扩展(tag/issue/prcheck 等二级功能)
666
- 3rdloop web list-ext | remove-ext <name> 扩展管理
670
+ 3rdloop serve [--port N] [--web-port N] 启动 Server 后端 + Web 前端(同时启动)
671
+ [--web-root <dir>] [--ext-root <dir>] [--ext <name,...>] [--no-ext] [--open]
672
+ 3rdloop serve install-ext <dir> 安装扩展(tag/issue/prcheck 等二级功能)
673
+ 3rdloop serve list-ext | remove-ext <name> 扩展管理
674
+ 3rdloop update [--check] [--yes] [--registry <url>] 自更新到 npm 最新版
667
675
  3rdloop doctor 环境自检
668
676
  3rdloop version 版本信息
669
677
 
@@ -687,7 +695,6 @@ config 子命令:
687
695
  update 专用选项:
688
696
  --check 仅检查是否有新版本,不执行更新
689
697
  --yes, -y 跳过交互确认直接更新
690
- --force 开发态(npm link)也强制替换为 registry 正式包
691
698
  --registry <url> 覆盖 npm registry(默认尊重用户 npm 镜像配置)
692
699
  注意: 数据目录 ~/.3lib/3rdloop/db 与 SKILL 配置不受更新影响
693
700
 
package/lib/config-cmd.js CHANGED
@@ -90,7 +90,7 @@ export const ENV_KEYS = [
90
90
  { key: 'MAX_CONCURRENT_SESSIONS', label: 'AI Session 并发上限', description: '跨所有编排任务共享的全局并发信号量上限', default: '50', validate: validators.intPositive },
91
91
 
92
92
  // ── CLI / AI 助手后端 ────────────────────────────────────────────
93
- { key: 'CLI_TYPE', label: '默认 CLI 类型', description: 'opencode | deveco-code', default: 'opencode', validate: validators.cliType },
93
+ { key: 'CLI_TYPE', label: '默认 CLI 类型', description: 'opencode | deveco-code', default: 'deveco-code', validate: validators.cliType },
94
94
  { key: 'OPENCODE_HOST', label: 'opencode 主机', description: '连接配置(可选)', default: 'localhost', validate: validators.nonEmpty },
95
95
  { key: 'OPENCODE_PORT', label: 'opencode 端口', description: '连接配置(可选,默认 4096)', default: '4096', validate: validators.portOrEmpty },
96
96
  { key: 'DEVECO_HOST', label: 'deveco 主机', description: '连接配置(可选)', default: 'localhost', validate: validators.nonEmpty },
package/lib/config.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * - 定位数据目录(--data-dir > 3LIB_DATA_DIR > ~/.3lib/3rdloop/db)
8
8
  * - 生成 clinic 安全唯一 taskId(随机位 8)
9
9
  * - SKILL 目录优先级链(--skill-dir > 3LIB_SKILL_DIR > vendor/Skills)
10
+ * - 解析 AI CLI 类型(CLI_TYPE:opencode | deveco-code,默认 deveco-code)
10
11
  *
11
12
  * 数据目录策略:
12
13
  * 用户级统一 ~/.3lib/3rdloop/db(跨平台,由 getUser3libHome 解析),
@@ -204,6 +205,37 @@ export function getUser3libHome() {
204
205
  return path.join(home, '.3lib');
205
206
  }
206
207
 
208
+ /** 支持的 AI CLI 类型 */
209
+ const CLI_TYPES = ['opencode', 'deveco-code'];
210
+ /** 默认 CLI 类型(本项目面向 HarmonyOS 三方库场景,默认 DevEco Code) */
211
+ const DEFAULT_CLI_TYPE = 'deveco-code';
212
+ let _warnedInvalidCliType = false;
213
+
214
+ /**
215
+ * 解析当前生效的 AI CLI 类型(opencode | deveco-code)。
216
+ *
217
+ * 优先级:显式参数 > 环境变量 CLI_TYPE(含 loadEnvChain 注入的
218
+ * 系统环境变量 > ~/.3lib/.env > 项目 Server/.env 链)> 'deveco-code'(默认)。
219
+ * 与 Server/CLI/cli.js、check-env.mjs 的 resolveCliType 语义保持一致。
220
+ *
221
+ * CLI_TYPE 配了无效值时不崩溃:回退默认值并在 stderr 告警一次
222
+ * (doctor 会将其作为失败项显式报告)。
223
+ *
224
+ * @param {string} [override] - 显式类型(优先级最高)
225
+ * @returns {'opencode'|'deveco-code'}
226
+ */
227
+ export function resolveCliType(override) {
228
+ const raw = String(override ?? process.env.CLI_TYPE ?? '').trim();
229
+ if (CLI_TYPES.includes(raw)) return raw;
230
+ if (raw && !_warnedInvalidCliType) {
231
+ _warnedInvalidCliType = true;
232
+ process.stderr.write(
233
+ `[3rdloop] 警告: CLI_TYPE="${raw}" 无效(可选值: ${CLI_TYPES.join(' | ')}),已回退 ${DEFAULT_CLI_TYPE}\n`
234
+ );
235
+ }
236
+ return DEFAULT_CLI_TYPE;
237
+ }
238
+
207
239
  /**
208
240
  * 确定数据目录。
209
241
  *
package/lib/doctor.js CHANGED
@@ -1,21 +1,31 @@
1
1
  /**
2
- * doctor.js —— 环境自检(3rdloop doctor)
2
+ * doctor.js —— 环境自检(3rdloop doctor)+ 引擎前置预检(precheckEnv
3
3
  *
4
- * 复用 check-env.mjs 的检测维度(轻量版,不耦合启动流程):
4
+ * 检查维度(doctor 与预检共用 collectChecks):
5
5
  * 1. Node.js 版本(>=22)
6
- * 2. opencode 是否安装 + 服务是否运行
7
- * 3. .env 配置(项目 Server/.env 或用户 ~/.3lib/.env
8
- * 4. 核心依赖是否可导入(vendor/Server)
6
+ * 2. CLI_TYPE 配置有效性(无效值视为配置错误)
7
+ * 3. AI CLI 可执行文件(按 CLI_TYPE 检测 opencode / deveco
8
+ * 4. LLM 凭据(生效环境 DASHSCOPE_API_KEY;LLM_BASE_URL/LLM_MODEL 引擎有内置默认值,不强制)
9
9
  * 5. Skill 目录可访问
10
- * 6. opencode 端口连通性
10
+ * 6. 数据目录可写
11
+ * 7. serve 服务端口连通性(非致命:未运行时 run/orch/step/workflow 会自动拉起;
12
+ * 端口有响应但异常视为失败)
11
13
  *
12
- * 退出码:全部通过 = 0;存在致命问题 = 1。
14
+ * 每项检查含 fix 字段:不通过时的修复提示(含 3rdloop config set 命令),
15
+ * doctor 与预检失败时均打印,用户照抄即可配置。
16
+ *
17
+ * precheckEnv:run/submit/step run/orch run|start/工作流正式执行前调用,
18
+ * 任一项不通过直接退出码 1(避免跑到一半才因环境问题失败、误报为任务失败退出码 2)。
19
+ *
20
+ * 退出码:doctor 全部通过 = 0;存在致命问题 = 1。
13
21
  */
14
22
 
15
23
  import fs from 'node:fs';
16
24
  import path from 'node:path';
17
25
  import { execSync } from 'node:child_process';
18
- import { C, logLine } from './ui.js';
26
+ import { C } from './ui.js';
27
+ import { resolveCliType } from './config.js';
28
+ import { resolveCliServeConfig } from './opencode.js';
19
29
 
20
30
  /**
21
31
  * 检查命令是否在 PATH 中。
@@ -31,17 +41,37 @@ function which(cmd) {
31
41
  }
32
42
 
33
43
  /**
34
- * 执行环境自检。
35
- *
44
+ * 探测 serve 服务 HTTP /health(2s 超时)。
45
+ * @param {string} baseUrl
46
+ * @returns {Promise<{ state: 'running'|'down'|'error', detail: string }>}
47
+ * running = 服务正常;down = 未运行(执行命令时会自动拉起,非致命);
48
+ * error = 端口有响应但异常(可能是残留的坏进程,视为致命)
49
+ */
50
+ async function probeServe(baseUrl) {
51
+ try {
52
+ const controller = new AbortController();
53
+ const timer = setTimeout(() => controller.abort(), 2000);
54
+ const res = await fetch(`${baseUrl}/health`, { signal: controller.signal });
55
+ clearTimeout(timer);
56
+ return res.ok
57
+ ? { state: 'running', detail: `运行中 (${baseUrl})` }
58
+ : { state: 'error', detail: `响应异常 HTTP ${res.status} (${baseUrl}),可能是残留的异常进程` };
59
+ } catch {
60
+ return { state: 'down', detail: `未运行 (${baseUrl})——执行 run/orch/step/workflow 时会自动拉起` };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 收集全部检查项(doctor 与 precheckEnv 共用)。
66
+ * 每项:{ name, passed, detail, fix }——fix 为不通过时的修复提示。
36
67
  * @param {object} opts
37
68
  * @param {string} opts.projectRoot
38
69
  * @param {string} opts.skillDir
39
70
  * @param {string} opts.dataDir
40
- * @returns {Promise<{ ok: boolean, checks: Array<{name: string, passed: boolean, detail?: string}> }>}
71
+ * @returns {Promise<Array<{name: string, passed: boolean, detail?: string, fix?: string}>>}
41
72
  */
42
- export async function runDoctor(opts = {}) {
73
+ async function collectChecks({ projectRoot, skillDir, dataDir }) {
43
74
  const checks = [];
44
- const { projectRoot, skillDir, dataDir } = opts;
45
75
 
46
76
  // 1. Node.js 版本
47
77
  const nodeMajor = Number(process.version.match(/^v(\d+)/)?.[1] || 0);
@@ -49,66 +79,153 @@ export async function runDoctor(opts = {}) {
49
79
  name: 'Node.js >= 22',
50
80
  passed: nodeMajor >= 22,
51
81
  detail: process.version,
82
+ fix: '请升级 Node.js 至 v22+ 后重试(https://nodejs.org)',
52
83
  });
53
84
 
54
- // 2. opencode 可执行文件
55
- const opencodeBin = which('opencode');
85
+ // 2. CLI_TYPE 配置有效性(解析回退发生在 resolveCliType,这里负责显式报告)
86
+ const rawCliType = String(process.env.CLI_TYPE ?? '').trim();
87
+ const cliType = resolveCliType();
88
+ const cliCfg = resolveCliServeConfig(cliType);
89
+ if (!rawCliType) {
90
+ checks.push({
91
+ name: 'CLI_TYPE 配置',
92
+ passed: true,
93
+ detail: '未设置(默认 deveco-code;如需 opencode 可 3rdloop config set CLI_TYPE opencode)',
94
+ });
95
+ } else if (rawCliType === cliType) {
96
+ checks.push({ name: 'CLI_TYPE 配置', passed: true, detail: rawCliType });
97
+ } else {
98
+ checks.push({
99
+ name: 'CLI_TYPE 配置',
100
+ passed: false,
101
+ detail: `"${rawCliType}" 无效,可选值: opencode | deveco-code(当前回退 ${cliType})`,
102
+ fix: '3rdloop config set CLI_TYPE opencode(可选: opencode | deveco-code),或 3rdloop config unset CLI_TYPE 恢复默认',
103
+ });
104
+ }
105
+
106
+ // 3. AI CLI 可执行文件(按 CLI_TYPE 检测对应二进制)
107
+ const cliBin = which(cliCfg.bin);
108
+ const installHint = cliType === 'deveco-code'
109
+ ? '未找到,请 npm i -g @deveco/deveco-code(首次需 deveco auth login)'
110
+ : '未找到,请 npm i -g opencode-ai';
56
111
  checks.push({
57
- name: 'opencode CLI',
58
- passed: !!opencodeBin,
59
- detail: opencodeBin || '未找到,请 npm i -g opencode-ai',
112
+ name: `${cliType} CLI`,
113
+ passed: !!cliBin,
114
+ detail: cliBin || installHint,
115
+ fix: cliType === 'deveco-code'
116
+ ? 'npm i -g @deveco/deveco-code(安装后首次使用需 deveco auth login)'
117
+ : 'npm i -g opencode-ai',
60
118
  });
61
119
 
62
- // 3. .env 配置(项目 Server/.env 或用户 ~/.3lib/.env
63
- const envPaths = [
64
- path.join(projectRoot, 'Server', '.env'),
65
- path.join(process.env.HOME || process.env.USERPROFILE || '', '.3lib', '.env'),
66
- ];
67
- const foundEnv = envPaths.find(p => fs.existsSync(p));
68
- let envOk = false;
69
- let envDetail = '';
70
- if (foundEnv) {
71
- const content = fs.readFileSync(foundEnv, 'utf8');
72
- const required = ['DASHSCOPE_API_KEY', 'LLM_BASE_URL', 'LLM_MODEL'];
73
- const missing = required.filter(k => !new RegExp(`^\\s*${k}\\s*=\\s*(.+)$`, 'm').test(content));
74
- envOk = missing.length === 0;
75
- envDetail = missing.length
76
- ? `缺少: ${missing.join(', ')} (${foundEnv})`
77
- : `配置完整 (${foundEnv})`;
78
- } else {
79
- envDetail = '未找到 .env(项目 Server/.env 或 ~/.3lib/.env)';
80
- }
81
- checks.push({ name: 'LLM 环境变量', passed: envOk, detail: envDetail });
120
+ // 4. LLM 凭据(生效环境检测:loadEnvChain 已注入 系统环境变量 > ~/.3lib/.env > 项目 Server/.env,
121
+ // 任一层配置即生效。仅强制 DASHSCOPE_API_KEY——LLM_BASE_URL/LLM_MODEL 引擎有内置默认值)
122
+ const apiKey = String(process.env.DASHSCOPE_API_KEY ?? '').trim();
123
+ checks.push({
124
+ name: 'LLM 凭据(DASHSCOPE_API_KEY)',
125
+ passed: !!apiKey,
126
+ detail: apiKey
127
+ ? '已配置(系统环境变量 / ~/.3lib/.env / 项目 Server/.env 任一层)'
128
+ : '未配置(系统环境变量 / ~/.3lib/.env / 项目 Server/.env 均未设置,LLM 调用将失败)',
129
+ fix: '3rdloop config set DASHSCOPE_API_KEY <sk-xxxxxxxx>(从 https://bailian.console.aliyun.com/ 获取)',
130
+ });
82
131
 
83
- // 4. Skill 目录可访问
132
+ // 5. Skill 目录可访问
84
133
  const skillOk = skillDir && fs.existsSync(skillDir);
85
134
  checks.push({
86
135
  name: 'SKILL 目录',
87
136
  passed: !!skillOk,
88
- detail: skillOk ? skillDir : 'SKILL 目录不存在',
137
+ detail: skillOk ? skillDir : `SKILL 目录不存在: ${skillDir}`,
138
+ fix: '3rdloop config set 3LIB_SKILL_DIR <SKILL 目录>;发布包缺 SKILL 请重装: npm i -g @ohos-cpf/3rdloop',
89
139
  });
90
140
 
91
- // 5. 数据目录可写
141
+ // 6. 数据目录可写
92
142
  let dataOk = false;
93
143
  try {
94
144
  fs.mkdirSync(dataDir, { recursive: true });
95
145
  dataOk = true;
96
146
  } catch { /* 不可写 */ }
97
- checks.push({ name: '数据目录', passed: dataOk, detail: dataDir });
147
+ checks.push({
148
+ name: '数据目录',
149
+ passed: dataOk,
150
+ detail: dataOk ? dataDir : `不可写: ${dataDir}`,
151
+ fix: '3rdloop config set 3LIB_DATA_DIR <可写目录>,或运行时加 --data-dir <p>',
152
+ });
153
+
154
+ // 7. serve 服务端口连通性(未运行不算失败:执行命令时会自动拉起;
155
+ // 端口有响应但异常则视为失败,提示清理残留进程)
156
+ const serve = await probeServe(`http://${cliCfg.host}:${cliCfg.port}`);
157
+ checks.push({
158
+ name: `${cliType} serve 服务`,
159
+ passed: serve.state !== 'error',
160
+ detail: serve.detail,
161
+ fix: `端口 ${cliCfg.port} 被异常进程占用,请清理后重试(lsof -i :${cliCfg.port} 定位 / 任务管理器结束进程)`,
162
+ });
163
+
164
+ return checks;
165
+ }
166
+
167
+ /**
168
+ * 执行环境自检(3rdloop doctor 命令实现)。
169
+ *
170
+ * @param {object} opts
171
+ * @param {string} opts.projectRoot
172
+ * @param {string} opts.skillDir
173
+ * @param {string} opts.dataDir
174
+ * @param {boolean} [opts.quiet] - 静默模式(不打印表格,仅返回结果)
175
+ * @returns {Promise<{ ok: boolean, checks: Array<{name: string, passed: boolean, detail?: string, fix?: string}> }>}
176
+ */
177
+ export async function runDoctor(opts = {}) {
178
+ const checks = await collectChecks(opts);
98
179
 
99
180
  // 汇总
100
181
  const ok = checks.every(c => c.passed);
101
182
 
102
- // 打印
183
+ // 打印(失败项附加修复提示)
103
184
  if (!opts.quiet) {
104
- process.stderr.write(`\n${C.bold}3rdloop doctor 环境自检${C.reset}\n`);
185
+ process.stderr.write(`\n${C.bold}3rdloop doctor 环境自检${C.reset}${C.gray}(AI CLI: ${resolveCliType()})${C.reset}\n`);
105
186
  for (const c of checks) {
106
187
  const icon = c.passed ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
107
188
  process.stderr.write(` ${icon} ${c.name}`);
108
189
  if (c.detail) process.stderr.write(` ${C.gray}${c.detail}${C.reset}`);
109
190
  process.stderr.write('\n');
191
+ if (!c.passed && c.fix) {
192
+ process.stderr.write(` ${C.cyan}修复: ${c.fix}${C.reset}\n`);
193
+ }
110
194
  }
111
195
  }
112
196
 
113
197
  return { ok, checks };
114
- }
198
+ }
199
+
200
+ /**
201
+ * 引擎前置预检(run/submit/step run/orch run|start/工作流正式执行前调用)。
202
+ *
203
+ * 与 doctor 同一检查集,语义差异:
204
+ * - serve 未运行不算失败(执行命令时会自动拉起;端口异常仍算失败)
205
+ * - 轻量:仅文件/PATH/端口探测,不拉起任何进程、不触引擎
206
+ *
207
+ * @param {object} opts - { projectRoot, skillDir, dataDir }(与 runDoctor 相同)
208
+ * @returns {Promise<{ ok: boolean, failures: Array<{name: string, detail?: string, fix?: string}> }>}
209
+ */
210
+ export async function precheckEnv(opts = {}) {
211
+ const checks = await collectChecks(opts);
212
+ const failures = checks
213
+ .filter(c => !c.passed)
214
+ .map(({ name, detail, fix }) => ({ name, detail, fix }));
215
+ return { ok: failures.length === 0, failures };
216
+ }
217
+
218
+ /**
219
+ * 预检失败信息打印(stderr,供 cli/workflow/step/orch 共用;--json 模式下调用方自行 out())。
220
+ * @param {Array<{name: string, detail?: string, fix?: string}>} failures
221
+ */
222
+ export function reportPrecheckFailures(failures) {
223
+ process.stderr.write(`\n环境预检未通过(${failures.length} 项),已阻止任务执行:\n`);
224
+ for (const f of failures) {
225
+ process.stderr.write(` ${C.red}✗${C.reset} ${f.name}${f.detail ? ` ${C.gray}${f.detail}${C.reset}` : ''}\n`);
226
+ if (f.fix) {
227
+ process.stderr.write(` ${C.cyan}修复: ${f.fix}${C.reset}\n`);
228
+ }
229
+ }
230
+ process.stderr.write('完整环境自检: 3rdloop doctor\n');
231
+ }