@ohos-cpf/3rdloop 0.0.7 → 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 -26
- package/lib/cli.js +33 -25
- 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/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/Routes/server.js +3 -3
- 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 });
|