@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/README.md +160 -340
- package/lib/cli.js +43 -2
- package/lib/config-cmd.js +452 -0
- package/lib/config.js +13 -12
- package/lib/queries.js +1 -1
- package/lib/update.js +280 -0
- package/lib/workflow.js +19 -1
- package/package.json +1 -1
- package/vendor/Server/Brain/SkillExecutor.js +2 -2
- package/vendor/Server/CLI/cli.js +40 -8
- package/vendor/Server/CLI/deveco-code/index.js +71 -0
- package/vendor/Server/CLI/deveco-code.js +5 -0
- package/vendor/Server/CLI/opencode/index.js +16 -2
- package/vendor/Server/FlexRunner/FlexRunner.js +2 -2
- package/vendor/Server/Knowledge/KnowledgeImporter.js +1 -1
- package/vendor/Server/LoopEngine/LoopEngine.js +1 -1
- package/vendor/Server/Routes/server.js +5 -4
- package/vendor/Server/Skills/acceptance-check/SKILL.md +18 -1
- package/vendor/Server/Skills/acceptance-check/references/PHASE2_EVIDENCE_VERIFICATION.md +70 -0
- package/vendor/Server/Skills/arkts-code-check/SKILL.md +9 -2
- package/vendor/Server/Skills/arkts-code-check/references/CODE_CHECK_GUIDE.md +51 -0
- package/vendor/Server/Skills/arkts-code-check/references/PHASE1_INFO_COLLECTION.md +13 -2
- package/vendor/Server/TestCheck/TestCheck.js +2 -2
- package/vendor/VERSION +3 -3
- package/workflows/pr-push.mjs +193 -0
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
|
@@ -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(
|
|
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(`
|
|
93
|
+
throw new Error(`CLI(${cli.getType()}) 服务不可用: ${health.message || 'health check failed'}`);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// ── 1b. 确保工作目录存在 ──
|
package/vendor/Server/CLI/cli.js
CHANGED
|
@@ -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-
|
|
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'
|
|
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;
|
|
@@ -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:
|
|
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(
|
|
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(
|
|
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', `
|
|
818
|
+
this._log('error', `CLI(${this.#cli.getType()}) 服务不可用: ${health.message}`);
|
|
819
819
|
return false;
|
|
820
820
|
}
|
|
821
821
|
|
|
@@ -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(
|
|
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
|
|
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
|
|
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);
|