@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/lib/serve.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* serve.js —— 3rdloop serve
|
|
2
|
+
* serve.js —— 3rdloop serve:一条命令同时启动 Server HTTP 后端 + Web 前端
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* createServer,不新增任何 daemon
|
|
4
|
+
* 定位:serve 与 Web 前端绑定启动(原独立 `3rdloop web` 命令已合并至此):
|
|
5
|
+
* - 后端:复用 Server/Routes/server.js 的 createServer(默认 3000,不新增任何 daemon/调度逻辑)
|
|
6
|
+
* - 前端:进程内启动 Web 服务(lib/web.js:静态托管 + /api 反代 + 扩展挂载,默认 8080)
|
|
6
7
|
*
|
|
7
8
|
* 行为:
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* 与 run/submit 等命令保持一致;json 模式下本模块自行纯化 stdout
|
|
9
|
+
* - 默认同时启动后端与 Web 前端(--port / 3LIB_SERVER_PORT、--web-port / 3LIB_WEB_PORT)
|
|
10
|
+
* - 复用 CLI 的环境装配(cli.js 已统一执行 .env 链 + stdout 保护),与 run/submit 等命令一致
|
|
11
|
+
* - 扩展管理子命令:install-ext / list-ext / remove-ext(不启动服务)
|
|
12
12
|
* - 端口冲突 / 启动失败给出明确错误,不静默
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { spawn, execSync } from 'node:child_process';
|
|
16
15
|
import fs from 'node:fs';
|
|
17
16
|
import path from 'node:path';
|
|
18
17
|
import { fileURLToPath } from 'node:url';
|
|
19
18
|
import process from 'node:process';
|
|
19
|
+
import {
|
|
20
|
+
startWebServer,
|
|
21
|
+
resolveUserExtRoot,
|
|
22
|
+
webExtInstall,
|
|
23
|
+
webExtList,
|
|
24
|
+
webExtRemove,
|
|
25
|
+
} from './web.js';
|
|
20
26
|
|
|
21
27
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
22
28
|
export const CLI_ROOT = path.resolve(moduleDir, '..');
|
|
@@ -49,60 +55,112 @@ async function _importServerModule() {
|
|
|
49
55
|
return import(url.href);
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
/** Web 静态前端目录(开发期仓库 Web/;发布期 vendor/Web 预留)。 */
|
|
53
|
-
function resolveWebDir() {
|
|
54
|
-
const candidates = [
|
|
55
|
-
path.resolve(CLI_ROOT, '..', 'Web'),
|
|
56
|
-
path.join(CLI_ROOT, 'vendor', 'Web'),
|
|
57
|
-
];
|
|
58
|
-
for (const dir of candidates) {
|
|
59
|
-
if (fs.existsSync(path.join(dir, 'server.js')) && fs.existsSync(path.join(dir, 'public'))) {
|
|
60
|
-
return dir;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
58
|
/**
|
|
67
|
-
* 解析
|
|
68
|
-
*
|
|
69
|
-
* @returns {{
|
|
59
|
+
* 解析 serve 专用参数(不依赖 cli.js 的 parseGlobalFlags,全局 flags 已被其消费)。
|
|
60
|
+
* Web 前端随 serve 默认启动,原 -w/--web 仅作兼容接受(不再有独立语义)。
|
|
61
|
+
* @returns {{ port?: number, webPort?: number, webRoot: string, extRoots: string[], extNames: string[], noExt: boolean, open: boolean, positionals: string[] }}
|
|
70
62
|
*/
|
|
71
63
|
function _parseServeArgs(rest = []) {
|
|
72
|
-
const args = {
|
|
64
|
+
const args = { webRoot: '', extRoots: [], extNames: [], noExt: false, open: false, positionals: [] };
|
|
65
|
+
const parsePort = (raw, name) => {
|
|
66
|
+
const n = parseInt(raw, 10);
|
|
67
|
+
if (!Number.isFinite(n) || n <= 0 || n > 65535) {
|
|
68
|
+
throw new Error(`非法端口: "${raw}"(${name})`);
|
|
69
|
+
}
|
|
70
|
+
return n;
|
|
71
|
+
};
|
|
73
72
|
for (let i = 0; i < rest.length; i++) {
|
|
74
73
|
const arg = rest[i];
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
i
|
|
74
|
+
|
|
75
|
+
if (arg === '-w' || arg === '--web') {
|
|
76
|
+
// 兼容旧参数:Web 前端现已默认启动,提示后忽略
|
|
77
|
+
process.stderr.write('[3rdloop serve] 提示: -w/--web 已无需指定,Web 前端随 serve 默认启动\n');
|
|
78
|
+
} else if (arg === '--port') {
|
|
79
|
+
args.port = parsePort(rest[++i], '--port');
|
|
80
|
+
} else if (arg === '--web-port') {
|
|
81
|
+
args.webPort = parsePort(rest[++i], '--web-port');
|
|
82
|
+
} else if (arg === '--web-root') {
|
|
83
|
+
args.webRoot = rest[++i] || '';
|
|
84
|
+
} else if (arg === '--ext-root') {
|
|
85
|
+
const raw = rest[++i];
|
|
86
|
+
if (raw) args.extRoots.push(raw);
|
|
87
|
+
} else if (arg === '--ext') {
|
|
88
|
+
// 白名单:逗号分隔 + 可重复指定(--ext tag --ext issue 等效 --ext tag,issue)
|
|
89
|
+
const raw = rest[++i];
|
|
90
|
+
if (raw) {
|
|
91
|
+
for (const n of raw.split(',')) {
|
|
92
|
+
const t = n.trim();
|
|
93
|
+
if (t) args.extNames.push(t);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if (arg === '--no-ext') {
|
|
97
|
+
args.noExt = true;
|
|
98
|
+
} else if (arg === '--open') {
|
|
99
|
+
args.open = true;
|
|
100
|
+
} else if (arg.startsWith('--')) {
|
|
101
|
+
throw new Error(`未知选项: ${arg}(查看帮助: 3rdloop serve --help)`);
|
|
102
|
+
} else {
|
|
103
|
+
args.positionals.push(arg);
|
|
81
104
|
}
|
|
82
105
|
}
|
|
83
106
|
return args;
|
|
84
107
|
}
|
|
85
108
|
|
|
109
|
+
/** --json 模式的 stdout 输出函数(非 json 模式为 no-op)。 */
|
|
110
|
+
function _jsonOut(jsonMode) {
|
|
111
|
+
return (data) => {
|
|
112
|
+
if (jsonMode) process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
86
116
|
/**
|
|
87
|
-
* 3rdloop serve
|
|
117
|
+
* 3rdloop serve 命令实现:同时启动 Server HTTP 后端 + Web 前端。
|
|
88
118
|
* @param {object} opts
|
|
89
|
-
* @param {string[]} opts.rest - flags.rest(含 serve
|
|
119
|
+
* @param {string[]} opts.rest - flags.rest(含 serve 专用参数与扩展管理子命令)
|
|
90
120
|
* @param {boolean} opts.jsonMode
|
|
91
121
|
* @param {string} [opts.dataDir] - CLI 解析后的数据目录(--data-dir / 3LIB_DATA_DIR / 默认)
|
|
92
|
-
* @
|
|
122
|
+
* @param {string} [opts.user3libHome] - 用户 3lib 主目录(~/.3lib,扩展根解析用)
|
|
123
|
+
* @returns {Promise<number>} 退出码(serve 为长驻命令,仅 --help/子命令/启动失败正常返回)
|
|
93
124
|
*/
|
|
94
|
-
export async function cmdServe({ rest = [], jsonMode = false, dataDir = '' }) {
|
|
125
|
+
export async function cmdServe({ rest = [], jsonMode = false, dataDir = '', user3libHome = '' }) {
|
|
95
126
|
// --help / -h:打印用法
|
|
96
127
|
if (rest.includes('--help') || rest.includes('-h')) {
|
|
97
128
|
_printServeHelp();
|
|
98
129
|
return 0;
|
|
99
130
|
}
|
|
100
131
|
|
|
101
|
-
|
|
132
|
+
let args;
|
|
133
|
+
try {
|
|
134
|
+
args = _parseServeArgs(rest);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
process.stderr.write(`${err.message}\n`);
|
|
137
|
+
_printServeHelp();
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const userExtRoot = resolveUserExtRoot(user3libHome);
|
|
142
|
+
|
|
143
|
+
// ── 扩展管理子命令(不启动服务) ──
|
|
144
|
+
const [sub, subArg] = args.positionals;
|
|
145
|
+
if (sub === 'install-ext') {
|
|
146
|
+
return webExtInstall(subArg, { userExtRoot, jsonMode, out: _jsonOut(jsonMode) });
|
|
147
|
+
}
|
|
148
|
+
if (sub === 'list-ext') {
|
|
149
|
+
return webExtList({ userExtRoot, jsonMode, out: _jsonOut(jsonMode) });
|
|
150
|
+
}
|
|
151
|
+
if (sub === 'remove-ext') {
|
|
152
|
+
return webExtRemove(subArg, { userExtRoot, jsonMode, out: _jsonOut(jsonMode) });
|
|
153
|
+
}
|
|
154
|
+
if (sub !== undefined) {
|
|
155
|
+
process.stderr.write(`未知子命令: ${sub}\n`);
|
|
156
|
+
_printServeHelp();
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
102
160
|
const serverPort = args.port ?? _envInt('3LIB_SERVER_PORT', 3000);
|
|
103
161
|
const webPort = args.webPort ?? _envInt('3LIB_WEB_PORT', 8080);
|
|
104
162
|
|
|
105
|
-
if (
|
|
163
|
+
if (serverPort === webPort) {
|
|
106
164
|
process.stderr.write(`3rdloop serve: 后端端口与 Web 端口相同 (${serverPort}),请用 --web-port 指定不同端口\n`);
|
|
107
165
|
return 1;
|
|
108
166
|
}
|
|
@@ -118,106 +176,77 @@ export async function cmdServe({ rest = [], jsonMode = false, dataDir = '' }) {
|
|
|
118
176
|
|
|
119
177
|
const serverModule = await _importServerModule();
|
|
120
178
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let webChild = null;
|
|
128
|
-
|
|
129
|
-
// 优雅退出:清理 Web 子进程(Windows taskkill 树)后退出
|
|
130
|
-
const cleanup = () => {
|
|
131
|
-
if (webChild && webChild.pid != null) {
|
|
132
|
-
try {
|
|
133
|
-
if (process.platform === 'win32') {
|
|
134
|
-
execSync(`taskkill /pid ${webChild.pid} /T /F`, { stdio: 'ignore' });
|
|
135
|
-
} else {
|
|
136
|
-
webChild.kill('SIGTERM');
|
|
137
|
-
}
|
|
138
|
-
} catch { /* 进程可能已退出 */ }
|
|
139
|
-
webChild = null;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
179
|
+
// ── 优雅退出 ──
|
|
180
|
+
// 必须在 createServer 之前注册:Server 内部也注册了 SIGINT/SIGTERM 处理器
|
|
181
|
+
// (退出码 0),先注册者先执行,保证 CLI 约定的 130/143 退出码语义。
|
|
182
|
+
let webServer = null;
|
|
142
183
|
let shuttingDown = false;
|
|
143
184
|
const onSignal = (sig) => {
|
|
144
185
|
if (shuttingDown) return;
|
|
145
186
|
shuttingDown = true;
|
|
146
|
-
|
|
187
|
+
try {
|
|
188
|
+
// 立即停止 Web 服务并断开存量连接(含 SSE 反代长连接)
|
|
189
|
+
webServer?.close();
|
|
190
|
+
webServer?.closeAllConnections?.();
|
|
191
|
+
} catch { /* 尽力而为 */ }
|
|
147
192
|
process.exit(sig === 'SIGINT' ? 130 : 143);
|
|
148
193
|
};
|
|
149
194
|
process.once('SIGINT', () => onSignal('SIGINT'));
|
|
150
195
|
process.once('SIGTERM', () => onSignal('SIGTERM'));
|
|
151
196
|
|
|
197
|
+
// json 模式下静默 Web 启动横幅(stdout 只放结构化启动信息)
|
|
198
|
+
const log = jsonMode ? () => {} : (msg) => process.stderr.write(`${msg}\n`);
|
|
199
|
+
|
|
152
200
|
try {
|
|
153
|
-
//
|
|
201
|
+
// 1) Web 前端先监听(后端反代指向本进程后端端口;
|
|
202
|
+
// Web 端口冲突时在后端启动前快速失败)
|
|
203
|
+
webServer = await startWebServer({
|
|
204
|
+
port: webPort,
|
|
205
|
+
backend: `http://127.0.0.1:${serverPort}`,
|
|
206
|
+
webRootFlag: args.webRoot,
|
|
207
|
+
extRootFlags: args.extRoots,
|
|
208
|
+
userExtRoot,
|
|
209
|
+
noExt: args.noExt,
|
|
210
|
+
extNames: args.extNames,
|
|
211
|
+
dataDir,
|
|
212
|
+
open: args.open,
|
|
213
|
+
log,
|
|
214
|
+
tag: '3rdloop serve',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// 2) Server HTTP 后端
|
|
218
|
+
// cliType 由环境决定(shell > .env > 默认 deveco-code),与 CLI 嵌入式一致;
|
|
154
219
|
// dataDir 已通过 3LIB_DATA_DIR env 注入(见上),Server 内部所有默认
|
|
155
220
|
// createStorage() 均落到用户数据目录;skillDir 沿用包内 vendor 默认
|
|
156
221
|
// (npm 更新自动获得新 SKILL,不与 CLI 的 3LIB_SKILL_DIR 联动)
|
|
157
|
-
|
|
222
|
+
await serverModule.createServer({ port: serverPort });
|
|
158
223
|
|
|
159
224
|
if (jsonMode) {
|
|
160
225
|
process.stdout.write(JSON.stringify({
|
|
161
226
|
ok: true,
|
|
162
227
|
type: 'server',
|
|
163
228
|
pid: process.pid,
|
|
164
|
-
port: serverPort,
|
|
165
|
-
url: `http://
|
|
166
|
-
web: args.web ? { port: webPort, url: `http://localhost:${webPort}` } : null,
|
|
229
|
+
backend: { port: serverPort, url: `http://127.0.0.1:${serverPort}` },
|
|
230
|
+
web: { port: webPort, url: `http://localhost:${webPort}` },
|
|
167
231
|
}, null, 2) + '\n');
|
|
168
232
|
} else {
|
|
169
|
-
process.stderr.write(`[3rdloop serve] Server
|
|
233
|
+
process.stderr.write(`[3rdloop serve] Server 后端已启动: http://127.0.0.1:${serverPort}\n`);
|
|
170
234
|
process.stderr.write(`[3rdloop serve] 通过 Ctrl+C 停止\n`);
|
|
171
235
|
}
|
|
172
236
|
|
|
173
|
-
// ── 可选启动 Web 前端(默认不拉起,保持聚焦后端) ──
|
|
174
|
-
if (args.web) {
|
|
175
|
-
await _startWebFrontend(webPort, serverPort, jsonMode, (child) => { webChild = child; });
|
|
176
|
-
}
|
|
177
|
-
|
|
178
237
|
// 长驻:保持进程存活直到收到信号
|
|
179
238
|
await new Promise(() => {});
|
|
180
239
|
return 0;
|
|
181
240
|
} catch (err) {
|
|
182
|
-
|
|
241
|
+
try {
|
|
242
|
+
webServer?.close();
|
|
243
|
+
webServer?.closeAllConnections?.();
|
|
244
|
+
} catch { /* 尽力而为 */ }
|
|
245
|
+
const reason = err?.code === 'EADDRINUSE'
|
|
246
|
+
? `端口 ${err.port ?? '?'} 已被占用,可用 --port / --web-port 指定其他端口`
|
|
247
|
+
: err.message;
|
|
248
|
+
process.stderr.write(`3rdloop serve 启动失败: ${reason}\n`);
|
|
183
249
|
return 1;
|
|
184
|
-
} finally {
|
|
185
|
-
restoreStdout?.();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** 启动 Web 静态前端(反向代理 /api/* → 后端)。 */
|
|
190
|
-
async function _startWebFrontend(webPort, serverPort, jsonMode, onChild) {
|
|
191
|
-
try {
|
|
192
|
-
if (await _isPortOpen(webPort)) {
|
|
193
|
-
process.stderr.write(`[3rdloop serve] Web 端口 ${webPort} 已被占用,跳过拉起前端(可直接访问该地址)\n`);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
} catch { /* 探测失败不阻断 */ }
|
|
197
|
-
|
|
198
|
-
const webDir = resolveWebDir();
|
|
199
|
-
if (!webDir) {
|
|
200
|
-
process.stderr.write('[3rdloop serve] 未找到 Web/ 前端目录,跳过拉起前端\n');
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const child = spawn(process.execPath, [
|
|
205
|
-
path.join(webDir, 'server.js'),
|
|
206
|
-
'--port', String(webPort),
|
|
207
|
-
'--backend', `http://127.0.0.1:${serverPort}`,
|
|
208
|
-
], {
|
|
209
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
210
|
-
shell: false,
|
|
211
|
-
detached: false,
|
|
212
|
-
});
|
|
213
|
-
child.stdout?.on('data', (d) => process.stderr.write(`[web] ${d}`));
|
|
214
|
-
child.stderr?.on('data', (d) => process.stderr.write(`[web] ${d}`));
|
|
215
|
-
child.on('error', (err) => {
|
|
216
|
-
process.stderr.write(`[3rdloop serve] 拉起 Web 前端失败: ${err.message}\n`);
|
|
217
|
-
});
|
|
218
|
-
onChild(child);
|
|
219
|
-
if (!jsonMode) {
|
|
220
|
-
process.stderr.write(`[3rdloop serve] Web 前端已启动: http://localhost:${webPort}\n`);
|
|
221
250
|
}
|
|
222
251
|
}
|
|
223
252
|
|
|
@@ -227,54 +256,38 @@ function _envInt(name, def) {
|
|
|
227
256
|
return Number.isFinite(n) && n > 0 ? n : def;
|
|
228
257
|
}
|
|
229
258
|
|
|
230
|
-
/** 探测端口是否有响应(尽力而为,不阻断)。 */
|
|
231
|
-
function _isPortOpen(port, timeoutMs = 1500) {
|
|
232
|
-
return new Promise((resolve) => {
|
|
233
|
-
const controller = new AbortController();
|
|
234
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
235
|
-
fetch(`http://127.0.0.1:${port}/`, { signal: controller.signal })
|
|
236
|
-
.then(() => resolve(true))
|
|
237
|
-
.catch(() => resolve(false))
|
|
238
|
-
.finally(() => clearTimeout(timer));
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** json 模式下把 Server 内部 console.log/info 重定向到 stderr,返回恢复函数。 */
|
|
243
|
-
function _redirectStdoutLogs() {
|
|
244
|
-
const safeStringify = (a) => {
|
|
245
|
-
try { return JSON.stringify(a); } catch { return String(a); }
|
|
246
|
-
};
|
|
247
|
-
const toStderr = (args) => {
|
|
248
|
-
process.stderr.write(args.map((a) => (typeof a === 'string' ? a : safeStringify(a))).join(' ') + '\n');
|
|
249
|
-
};
|
|
250
|
-
const origLog = console.log;
|
|
251
|
-
const origInfo = console.info;
|
|
252
|
-
console.log = (...args) => toStderr(args);
|
|
253
|
-
console.info = (...args) => toStderr(args);
|
|
254
|
-
return () => {
|
|
255
|
-
console.log = origLog;
|
|
256
|
-
console.info = origInfo;
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
259
|
function _printServeHelp() {
|
|
261
|
-
process.stdout.write(`3rdloop serve ——
|
|
260
|
+
process.stdout.write(`3rdloop serve —— 同时启动 Server HTTP 后端 + Web 前端
|
|
262
261
|
|
|
263
262
|
用法:
|
|
264
|
-
3rdloop serve [
|
|
263
|
+
3rdloop serve [选项] 启动后端(默认 3000)+ Web 前端(默认 8080)
|
|
264
|
+
3rdloop serve install-ext <dir> 安装扩展到 ~/.3lib/3rdloop/web-ext/
|
|
265
|
+
3rdloop serve list-ext 列出可用扩展(含来源与挂载点)
|
|
266
|
+
3rdloop serve remove-ext <name> 移除已安装扩展
|
|
265
267
|
|
|
266
268
|
选项:
|
|
267
|
-
--port N
|
|
268
|
-
|
|
269
|
-
--web-
|
|
270
|
-
--
|
|
271
|
-
|
|
269
|
+
--port <N> 后端端口(默认 3000,可用 3LIB_SERVER_PORT 覆盖)
|
|
270
|
+
--web-port <N> Web 前端端口(默认 8080,可用 3LIB_WEB_PORT 覆盖)
|
|
271
|
+
--web-root <dir> 核心前端资产目录覆盖(默认: 仓库 Web/public,发布包为内置 cli/web)
|
|
272
|
+
--ext-root <dir> 追加扩展根目录(可多次指定;目录下每个子目录是一个扩展)
|
|
273
|
+
--ext <name,...> 白名单:仅挂载指定扩展(逗号分隔,可重复指定;未发现的名字打警告)
|
|
274
|
+
--no-ext 禁用全部扩展(纯核心工作台模式;优先于 --ext)
|
|
275
|
+
--open 启动后自动打开浏览器
|
|
276
|
+
--json 输出结构化启动信息到 stdout(进度/日志走 stderr)
|
|
277
|
+
-h, --help 本帮助
|
|
272
278
|
|
|
273
279
|
说明:
|
|
274
|
-
|
|
280
|
+
Web 前端随 serve 默认启动(原 "3rdloop web" 命令已合并到 serve):
|
|
281
|
+
静态托管核心工作台 + /api/*、/stream* 反代到后端 + 扩展挂载。
|
|
275
282
|
CLI 的 run/submit/wait 等命令保持嵌入式模式,不经此后端。
|
|
276
|
-
退出: Ctrl+C / SIGTERM
|
|
283
|
+
退出: Ctrl+C / SIGTERM 优雅停止(退出码 130/143)。
|
|
284
|
+
|
|
285
|
+
环境变量:
|
|
286
|
+
3LIB_SERVER_PORT 后端端口(等效 --port)
|
|
287
|
+
3LIB_WEB_PORT Web 前端端口(等效 --web-port)
|
|
288
|
+
3LIB_WEB_ROOT 核心前端资产目录(等效 --web-root)
|
|
289
|
+
3LIB_WEB_EXT_ROOT 用户扩展根目录(默认 ~/.3lib/3rdloop/web-ext)
|
|
277
290
|
`);
|
|
278
291
|
}
|
|
279
292
|
|
|
280
|
-
export default cmdServe;
|
|
293
|
+
export default cmdServe;
|
package/lib/step.js
CHANGED
|
@@ -94,11 +94,22 @@ async function cmdStepRun({ flags, rest, dataDir, skillDir, jsonMode, quiet, ver
|
|
|
94
94
|
|
|
95
95
|
const specifiedSkill = _stringFlag(flags, 'specified-skill');
|
|
96
96
|
|
|
97
|
-
//
|
|
97
|
+
// ── 环境预检(引擎执行前快速失败:凭据/目录类问题直接退出码 1) ──
|
|
98
|
+
{
|
|
99
|
+
const { precheckEnv, reportPrecheckFailures } = await import('./doctor.js');
|
|
100
|
+
const pre = await precheckEnv({ projectRoot, dataDir, skillDir });
|
|
101
|
+
if (!pre.ok) {
|
|
102
|
+
if (jsonMode) out({ ok: false, error: '环境预检未通过', taskId, failures: pre.failures });
|
|
103
|
+
else reportPrecheckFailures(pre.failures);
|
|
104
|
+
return EXIT.ERROR;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 挂载 AI CLI serve 生命周期(复用 runner 语义,类型由 CLI_TYPE 决定)
|
|
98
109
|
const r = await _runner();
|
|
99
|
-
const ctx = r.createRunContext({ projectRoot, dataDir, skillDir,
|
|
100
|
-
const {
|
|
101
|
-
const oc = new
|
|
110
|
+
const ctx = r.createRunContext({ projectRoot, dataDir, skillDir, verbose });
|
|
111
|
+
const { CliServeManager } = await import('./opencode.js');
|
|
112
|
+
const oc = new CliServeManager({ projectRoot, cliType: ctx.cliType });
|
|
102
113
|
const ocHealth = await oc.ensureRunning();
|
|
103
114
|
if (!ocHealth.ok) {
|
|
104
115
|
if (jsonMode) out({ ok: false, error: ocHealth.message, taskId });
|
package/lib/update.js
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 职责:
|
|
5
5
|
* - 检测本地版本(getCliVersion)与 npm registry 最新版
|
|
6
|
-
* -
|
|
7
|
-
* - 交互确认后通过 npm install -g 重装
|
|
6
|
+
* - 交互确认后卸载旧版 + npm install -g 重装最新版
|
|
8
7
|
*
|
|
9
8
|
* 交互/输出契约:
|
|
10
9
|
* - 进度/日志 → stderr;--json 结构化结果 → stdout
|
|
@@ -17,7 +16,6 @@
|
|
|
17
16
|
* 该分支对用户可控的 --registry 值做白名单校验防 shell 注入
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
|
-
import fs from 'node:fs';
|
|
21
19
|
import path from 'node:path';
|
|
22
20
|
import readline from 'node:readline';
|
|
23
21
|
import { execFileSync } from 'node:child_process';
|
|
@@ -26,7 +24,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
26
24
|
// 本文件在 cli/lib/ 下
|
|
27
25
|
const CLI_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
26
|
const PKG_NAME = '@ohos-cpf/3rdloop';
|
|
29
|
-
const BIN_ABS = path.join(CLI_ROOT, 'bin', '3rdloop.mjs');
|
|
30
27
|
|
|
31
28
|
// ── npm 调用封装 ─────────────────────────────────────────────────
|
|
32
29
|
|
|
@@ -92,22 +89,6 @@ export function compareVersions(a, b) {
|
|
|
92
89
|
return String(preA).localeCompare(String(preB));
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
/**
|
|
96
|
-
* 检测当前命令是否为 npm link 软链(开发态)。
|
|
97
|
-
*
|
|
98
|
-
* 原理:npm link 的全局 bin 是指向仓库 bin 的软链;真实全局安装则是
|
|
99
|
-
* node_modules 内的副本。比较 process.argv[1] 与仓库内 bin 的 realpath 是否相同。
|
|
100
|
-
*/
|
|
101
|
-
export function detectLinkMode(argv1 = process.argv[1]) {
|
|
102
|
-
try {
|
|
103
|
-
const realArgv = fs.realpathSync(argv1 || '');
|
|
104
|
-
const realLocal = fs.realpathSync(BIN_ABS);
|
|
105
|
-
return realArgv === realLocal;
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
92
|
/**
|
|
112
93
|
* 获取最新版本。
|
|
113
94
|
* 优先 `npm view`(尊重用户 .npmrc 镜像配置);失败回退 packument API。
|
|
@@ -171,33 +152,15 @@ export async function confirmUpdate({ current, latest } = {}) {
|
|
|
171
152
|
}
|
|
172
153
|
|
|
173
154
|
/**
|
|
174
|
-
*
|
|
155
|
+
* 执行更新:卸载旧版后通过 npm install -g 重装最新版。
|
|
175
156
|
* @param {object} opts
|
|
176
157
|
* @param {string} opts.latest 目标版本(registry latest)
|
|
177
158
|
* @param {string} [opts.registry] registry 覆盖
|
|
178
|
-
* @param {boolean} [opts.force] 开发态也强制替换
|
|
179
|
-
* @param {boolean} [opts.isLink] 是否 link 模式(可注入便于测试)
|
|
180
159
|
*/
|
|
181
|
-
export function performUpdate({ latest, registry
|
|
160
|
+
export function performUpdate({ latest, registry } = {}) {
|
|
182
161
|
const installArgs = ['install', '-g', `${PKG_NAME}@latest`];
|
|
183
162
|
if (registry) installArgs.push('--registry', registry);
|
|
184
163
|
|
|
185
|
-
// 开发态(npm link):默认警告并跳过,仅提示 git pull
|
|
186
|
-
if (isLink && !force) {
|
|
187
|
-
process.stderr.write(
|
|
188
|
-
`\n[update] 检测到当前为开发态(npm link),已跳过自更新。\n` +
|
|
189
|
-
` 开发态请用 git pull 更新仓库源码(数据目录 ~/.3lib/3rdloop/db 不受影响)。\n` +
|
|
190
|
-
` 如需强制用正式包替换链接,可加 --force。\n`
|
|
191
|
-
);
|
|
192
|
-
return { updated: false, skipped: 'link-mode' };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// 开发态 + --force:先解除链接
|
|
196
|
-
if (isLink && force) {
|
|
197
|
-
process.stderr.write(`[update] 检测到 npm link,先解除链接...\n`);
|
|
198
|
-
runNpm(['unlink', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
164
|
process.stderr.write(`[update] 卸载旧版本(@${PKG_NAME})...\n`);
|
|
202
165
|
try {
|
|
203
166
|
runNpm(['uninstall', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
|
|
@@ -219,19 +182,17 @@ export function performUpdate({ latest, registry, force = false, isLink = detect
|
|
|
219
182
|
export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
|
|
220
183
|
const checkOnly = rest.includes('--check');
|
|
221
184
|
const yes = rest.includes('--yes') || rest.includes('-y');
|
|
222
|
-
const force = rest.includes('--force');
|
|
223
185
|
const regIdx = rest.indexOf('--registry');
|
|
224
186
|
const registry = regIdx !== -1 ? rest[regIdx + 1] : undefined;
|
|
225
187
|
|
|
226
188
|
// update 用法说明(--help,离线可测,不触碰网络)
|
|
227
189
|
if (rest.includes('--help') || rest.includes('-h')) {
|
|
228
190
|
const help = [
|
|
229
|
-
'用法: 3rdloop update [--check] [--yes] [--
|
|
191
|
+
'用法: 3rdloop update [--check] [--yes] [--registry <url>]',
|
|
230
192
|
'',
|
|
231
193
|
' 默认: 检测本地版本与 npm registry 最新版,差异存在时交互确认后重装',
|
|
232
194
|
' --check 仅检查是否有新版本,不执行更新',
|
|
233
195
|
' --yes, -y 跳过交互确认直接更新(非 TTY 必须)',
|
|
234
|
-
' --force 开发态(npm link)也强制替换为 registry 正式包',
|
|
235
196
|
' --registry <url> 覆盖 npm registry(默认尊重用户 npm 镜像配置)',
|
|
236
197
|
' 注意: 数据目录 ~/.3lib/3rdloop/db 与 SKILL 配置不受更新影响',
|
|
237
198
|
].join('\n');
|
|
@@ -268,7 +229,7 @@ export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
|
|
|
268
229
|
if (!checkOnly) {
|
|
269
230
|
// --json 下不交互,直接更新(用户显式要求命令行为,等价 --yes)
|
|
270
231
|
try {
|
|
271
|
-
const r = performUpdate({ latest, registry
|
|
232
|
+
const r = performUpdate({ latest, registry });
|
|
272
233
|
process.stdout.write(JSON.stringify({ ...r, ok: true }, null, 2) + '\n');
|
|
273
234
|
return { ok: true, ...r };
|
|
274
235
|
} catch (err) {
|
|
@@ -310,7 +271,7 @@ export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
|
|
|
310
271
|
|
|
311
272
|
process.stderr.write(`\n`);
|
|
312
273
|
try {
|
|
313
|
-
const r = performUpdate({ latest, registry
|
|
274
|
+
const r = performUpdate({ latest, registry });
|
|
314
275
|
return { ok: true, ...r };
|
|
315
276
|
} catch (err) {
|
|
316
277
|
const msg = `更新失败: ${err.message}`;
|