@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
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 /
|
|
47
|
+
`doctor` 会逐项检查 Node / CLI_TYPE 配置 / AI CLI(按 `CLI_TYPE` 检测 opencode 或 deveco)/ `.env` 凭据 / SKILL 目录 / 数据目录 / serve 服务连通性,缺什么会明确提示。除此之外还需要了解:
|
|
48
48
|
|
|
49
49
|
- **Node.js** >= 22
|
|
50
|
-
- **
|
|
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
|
|
82
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
除了命令行,也可以在浏览器里使用 Web 工作台(提交任务 / 查看循环进度 / PR 检视等),由两个命令提供支撑:
|
|
140
|
+
### 场景五:用 Web 界面操作(serve)
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
除了命令行,也可以在浏览器里使用 Web 工作台(提交任务 / 查看循环进度 / PR 检视等)。`3rdloop serve` 一条命令**同时启动** Server HTTP 后端(默认 3000)与 Web 前端(默认 8080,静态托管 + `/api/*` 反代到后端 + 扩展挂载):
|
|
142
143
|
|
|
143
144
|
```bash
|
|
144
|
-
#
|
|
145
|
+
# 后端(默认 http://127.0.0.1:3000)+ Web 前端(默认 http://localhost:8080)同时启动
|
|
145
146
|
3rdloop serve
|
|
146
147
|
|
|
147
|
-
#
|
|
148
|
-
3rdloop serve --
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
> CLI 的 `run`/`submit`/`wait` 等命令保持嵌入式模式,不经此后端;两者可独立使用。
|
|
148
|
+
# 启动后自动打开浏览器
|
|
149
|
+
3rdloop serve --open
|
|
152
150
|
|
|
153
|
-
|
|
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
|
|
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
|
|
170
|
-
3rdloop
|
|
171
|
-
3rdloop
|
|
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
|
|
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 {
|
|
283
|
-
const oc = new
|
|
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(
|
|
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] [
|
|
663
|
-
|
|
664
|
-
3rdloop
|
|
665
|
-
3rdloop
|
|
666
|
-
3rdloop
|
|
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: '
|
|
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
|
-
*
|
|
4
|
+
* 检查维度(doctor 与预检共用 collectChecks):
|
|
5
5
|
* 1. Node.js 版本(>=22)
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
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.
|
|
10
|
+
* 6. 数据目录可写
|
|
11
|
+
* 7. serve 服务端口连通性(非致命:未运行时 run/orch/step/workflow 会自动拉起;
|
|
12
|
+
* 端口有响应但异常视为失败)
|
|
11
13
|
*
|
|
12
|
-
*
|
|
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
|
|
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<
|
|
71
|
+
* @returns {Promise<Array<{name: string, passed: boolean, detail?: string, fix?: string}>>}
|
|
41
72
|
*/
|
|
42
|
-
|
|
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.
|
|
55
|
-
const
|
|
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:
|
|
58
|
-
passed: !!
|
|
59
|
-
detail:
|
|
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
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
//
|
|
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 :
|
|
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
|
-
//
|
|
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({
|
|
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
|
+
}
|