@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/web.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* web.js ——
|
|
2
|
+
* web.js —— Web 前端服务库(由 3rdloop serve 内嵌启动)
|
|
3
3
|
*
|
|
4
4
|
* 架构(方案 A + D 组合):
|
|
5
5
|
* - 核心前端资产:仓库 Web/public(开发态)/ cli/web(发布态,prepack 复制)
|
|
@@ -15,12 +15,11 @@
|
|
|
15
15
|
* 3. /api/* 与 /stream* 反向代理到后端 Server(SSE 透传)
|
|
16
16
|
* 4. 其余 核心前端静态文件
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* 3rdloop web remove-ext <name> 移除已安装扩展
|
|
18
|
+
* 本模块不再提供独立 CLI 命令(原 `3rdloop web` 已合并到 `3rdloop serve`,
|
|
19
|
+
* Web 前端随 serve 默认启动)。对外导出供 serve.js 组合使用:
|
|
20
|
+
* startWebServer() 一站式:解析资产/加载扩展/监听端口
|
|
21
|
+
* resolveUserExtRoot() 用户扩展根目录解析
|
|
22
|
+
* webExtInstall / webExtList / webExtRemove 扩展管理子命令
|
|
24
23
|
*/
|
|
25
24
|
|
|
26
25
|
import http from 'node:http';
|
|
@@ -39,8 +38,6 @@ import {
|
|
|
39
38
|
import { EXIT } from './exit-codes.js';
|
|
40
39
|
|
|
41
40
|
const CLI_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
42
|
-
const DEFAULT_PORT = 8080;
|
|
43
|
-
const DEFAULT_BACKEND = 'http://127.0.0.1:3000';
|
|
44
41
|
|
|
45
42
|
// ─── MIME ─────────────────────────────────────────────────────────
|
|
46
43
|
const MIME = {
|
|
@@ -68,160 +65,24 @@ const MIME = {
|
|
|
68
65
|
};
|
|
69
66
|
|
|
70
67
|
// ═══════════════════════════════════════════════════════════════════
|
|
71
|
-
//
|
|
68
|
+
// 目录解析
|
|
72
69
|
// ═══════════════════════════════════════════════════════════════════
|
|
73
70
|
|
|
74
71
|
/**
|
|
75
|
-
*
|
|
76
|
-
* @param {
|
|
77
|
-
* @param {string[]} opts.rest - 剩余参数(含子命令位置参数与 web 专属 flags)
|
|
78
|
-
* @param {string} opts.user3libHome - 用户 3lib 主目录(~/.3lib)
|
|
79
|
-
* @param {string} opts.dataDir - CLI 数据目录
|
|
80
|
-
* @param {boolean} opts.jsonMode
|
|
81
|
-
* @param {Function} opts.out - --json 输出函数
|
|
82
|
-
* @returns {Promise<number>} 退出码
|
|
72
|
+
* 用户扩展根目录(3LIB_WEB_EXT_ROOT > ~/.3lib/3rdloop/web-ext)。
|
|
73
|
+
* @param {string} user3libHome - 用户 3lib 主目录(~/.3lib)
|
|
83
74
|
*/
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
let argv;
|
|
87
|
-
try {
|
|
88
|
-
argv = _parseWebArgs(flags.rest);
|
|
89
|
-
} catch (err) {
|
|
90
|
-
process.stderr.write(`${err.message}\n`);
|
|
91
|
-
_printWebHelp();
|
|
92
|
-
return EXIT.ERROR;
|
|
93
|
-
}
|
|
94
|
-
const userExtRoot = process.env['3LIB_WEB_EXT_ROOT']
|
|
75
|
+
export function resolveUserExtRoot(user3libHome) {
|
|
76
|
+
return process.env['3LIB_WEB_EXT_ROOT']
|
|
95
77
|
? path.resolve(process.env['3LIB_WEB_EXT_ROOT'])
|
|
96
78
|
: path.join(user3libHome, '3rdloop', 'web-ext');
|
|
97
|
-
|
|
98
|
-
if (argv.help) {
|
|
99
|
-
_printWebHelp();
|
|
100
|
-
return EXIT.OK;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const positional = argv.positionals;
|
|
104
|
-
|
|
105
|
-
// ── 子命令分发 ──
|
|
106
|
-
if (positional[0] === 'install-ext') {
|
|
107
|
-
return _cmdInstallExt(positional[1], { userExtRoot, jsonMode, out });
|
|
108
|
-
}
|
|
109
|
-
if (positional[0] === 'list-ext') {
|
|
110
|
-
return _cmdListExt({ userExtRoot, jsonMode, out });
|
|
111
|
-
}
|
|
112
|
-
if (positional[0] === 'remove-ext') {
|
|
113
|
-
return _cmdRemoveExt(positional[1], { userExtRoot, jsonMode, out });
|
|
114
|
-
}
|
|
115
|
-
if (positional[0] !== undefined) {
|
|
116
|
-
process.stderr.write(`未知子命令: ${positional[0]}\n`);
|
|
117
|
-
_printWebHelp();
|
|
118
|
-
return EXIT.ERROR;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ── 启动 Web 服务 ──
|
|
122
|
-
return _startServer({ argv, userExtRoot, dataDir });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
126
|
-
// 参数解析
|
|
127
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* 解析 web 命令专属参数。
|
|
131
|
-
* parseGlobalFlags 已消费全局 flags(--json/--data-dir 等),这里处理剩余部分。
|
|
132
|
-
* 非法参数直接 throw,由 cmdWeb 统一捕获打印帮助。
|
|
133
|
-
*/
|
|
134
|
-
function _parseWebArgs(rest) {
|
|
135
|
-
const opts = {
|
|
136
|
-
port: DEFAULT_PORT,
|
|
137
|
-
backend: DEFAULT_BACKEND,
|
|
138
|
-
webRoot: '',
|
|
139
|
-
extRoots: [],
|
|
140
|
-
extNames: [],
|
|
141
|
-
noExt: false,
|
|
142
|
-
open: false,
|
|
143
|
-
help: false,
|
|
144
|
-
positionals: [],
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
for (let i = 0; i < rest.length; i++) {
|
|
148
|
-
const arg = rest[i];
|
|
149
|
-
|
|
150
|
-
if (arg === '--port') {
|
|
151
|
-
const raw = rest[++i];
|
|
152
|
-
const n = parseInt(raw, 10);
|
|
153
|
-
if (!Number.isFinite(n) || n <= 0 || n > 65535) {
|
|
154
|
-
throw new Error(`非法端口: "${raw}"`);
|
|
155
|
-
}
|
|
156
|
-
opts.port = n;
|
|
157
|
-
} else if (arg === '--backend') {
|
|
158
|
-
const raw = rest[++i];
|
|
159
|
-
if (!raw || !/^https?:\/\//.test(raw)) {
|
|
160
|
-
throw new Error(`非法后端地址: "${raw}"(须为 http(s):// 开头)`);
|
|
161
|
-
}
|
|
162
|
-
opts.backend = raw.replace(/\/+$/, '');
|
|
163
|
-
} else if (arg === '--web-root') {
|
|
164
|
-
opts.webRoot = rest[++i] || '';
|
|
165
|
-
} else if (arg === '--ext-root') {
|
|
166
|
-
const raw = rest[++i];
|
|
167
|
-
if (raw) opts.extRoots.push(raw);
|
|
168
|
-
} else if (arg === '--no-ext') {
|
|
169
|
-
opts.noExt = true;
|
|
170
|
-
} else if (arg === '--ext') {
|
|
171
|
-
// 白名单:逗号分隔 + 可重复指定(--ext tag --ext issue 等效 --ext tag,issue)
|
|
172
|
-
const raw = rest[++i];
|
|
173
|
-
if (raw) {
|
|
174
|
-
for (const n of raw.split(',')) {
|
|
175
|
-
const t = n.trim();
|
|
176
|
-
if (t) opts.extNames.push(t);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
} else if (arg === '--open') {
|
|
180
|
-
opts.open = true;
|
|
181
|
-
} else if (arg === '--help' || arg === '-h') {
|
|
182
|
-
opts.help = true;
|
|
183
|
-
} else if (arg.startsWith('--')) {
|
|
184
|
-
throw new Error(`未知选项: ${arg}(查看帮助: 3rdloop web --help)`);
|
|
185
|
-
} else {
|
|
186
|
-
opts.positionals.push(arg);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return opts;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* 应用扩展开关过滤:
|
|
194
|
-
* --no-ext → 全部禁用(纯后端操作模式:仅核心工作台 + loop + knowledge)
|
|
195
|
-
* --ext 名称集 → 白名单,仅挂载指定扩展(未发现的名字打 warning)
|
|
196
|
-
* --no-ext 优先于 --ext(同时给出时白名单被忽略)。
|
|
197
|
-
*
|
|
198
|
-
* @param {Array<object>} extensions - loadExtensions 发现的全部扩展
|
|
199
|
-
* @param {object} argv - _parseWebArgs 结果
|
|
200
|
-
* @param {(msg: string) => void} warn
|
|
201
|
-
* @returns {Array<object>} 过滤后的扩展列表
|
|
202
|
-
*/
|
|
203
|
-
function _applyExtFilter(extensions, argv, warn) {
|
|
204
|
-
if (argv.noExt) return [];
|
|
205
|
-
if (argv.extNames.length === 0) return extensions;
|
|
206
|
-
|
|
207
|
-
const known = extensions.map((e) => e.name);
|
|
208
|
-
for (const name of argv.extNames) {
|
|
209
|
-
if (!known.includes(name)) {
|
|
210
|
-
warn(`[web-ext] --ext 指定的扩展 "${name}" 未发现(可用: ${known.join(', ') || '无'})`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return extensions.filter((e) => argv.extNames.includes(e.name));
|
|
214
79
|
}
|
|
215
80
|
|
|
216
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
217
|
-
// 目录解析
|
|
218
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
219
|
-
|
|
220
81
|
/**
|
|
221
82
|
* 核心前端资产目录。
|
|
222
83
|
* 优先级:--web-root > 3LIB_WEB_ROOT > 仓库 Web/public(开发态)> cli/web(发布态内置)
|
|
223
84
|
*/
|
|
224
|
-
function
|
|
85
|
+
export function resolveWebRoot(flagValue) {
|
|
225
86
|
if (flagValue) {
|
|
226
87
|
const dir = path.resolve(flagValue);
|
|
227
88
|
if (!fs.existsSync(dir)) throw new Error(`--web-root 目录不存在: ${dir}`);
|
|
@@ -249,7 +110,7 @@ function _resolveWebRoot(flagValue) {
|
|
|
249
110
|
* 扩展根目录列表(优先级从高到低,同名扩展先注册者生效)。
|
|
250
111
|
* 顺序:--ext-root(可多个)> 3LIB_WEB_EXT_ROOT > 仓库 Web/ext(开发态)> 用户扩展目录。
|
|
251
112
|
*/
|
|
252
|
-
function
|
|
113
|
+
export function resolveExtRoots(extRootFlags, userExtRoot) {
|
|
253
114
|
const roots = [];
|
|
254
115
|
const push = (p) => {
|
|
255
116
|
const r = path.resolve(p);
|
|
@@ -267,33 +128,80 @@ function _resolveExtRoots(extRootFlags, userExtRoot) {
|
|
|
267
128
|
}
|
|
268
129
|
|
|
269
130
|
// ═══════════════════════════════════════════════════════════════════
|
|
270
|
-
//
|
|
131
|
+
// 服务启动(由 3rdloop serve 调用)
|
|
271
132
|
// ═══════════════════════════════════════════════════════════════════
|
|
272
133
|
|
|
273
|
-
|
|
274
|
-
|
|
134
|
+
/**
|
|
135
|
+
* 应用扩展开关过滤:
|
|
136
|
+
* --no-ext → 全部禁用(纯后端操作模式:仅核心工作台 + loop + knowledge)
|
|
137
|
+
* --ext 名称集 → 白名单,仅挂载指定扩展(未发现的名字打 warning)
|
|
138
|
+
* --no-ext 优先于 --ext(同时给出时白名单被忽略)。
|
|
139
|
+
*
|
|
140
|
+
* @param {Array<object>} extensions - loadExtensions 发现的全部扩展
|
|
141
|
+
* @param {object} opts - { noExt, extNames }
|
|
142
|
+
* @param {(msg: string) => void} warn
|
|
143
|
+
* @returns {Array<object>} 过滤后的扩展列表
|
|
144
|
+
*/
|
|
145
|
+
export function applyExtFilter(extensions, { noExt, extNames }, warn) {
|
|
146
|
+
if (noExt) return [];
|
|
147
|
+
if (!extNames || extNames.length === 0) return extensions;
|
|
275
148
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return EXIT.ERROR;
|
|
149
|
+
const known = extensions.map((e) => e.name);
|
|
150
|
+
for (const name of extNames) {
|
|
151
|
+
if (!known.includes(name)) {
|
|
152
|
+
warn(`[web-ext] --ext 指定的扩展 "${name}" 未发现(可用: ${known.join(', ') || '无'})`);
|
|
153
|
+
}
|
|
282
154
|
}
|
|
155
|
+
return extensions.filter((e) => extNames.includes(e.name));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 一站式启动 Web 前端服务:解析前端资产 → 加载扩展 → 创建服务 → 监听端口。
|
|
160
|
+
* 由 3rdloop serve 内嵌调用(后端反代地址指向同进程的 Server 后端)。
|
|
161
|
+
*
|
|
162
|
+
* @param {object} opts
|
|
163
|
+
* @param {number} opts.port - 监听端口(必填)
|
|
164
|
+
* @param {string} opts.backend - 后端 Server 地址(必填,如 http://127.0.0.1:3000)
|
|
165
|
+
* @param {string} [opts.webRootFlag] - --web-root 覆盖
|
|
166
|
+
* @param {string[]} [opts.extRootFlags] - --ext-root 追加扩展根目录
|
|
167
|
+
* @param {string} [opts.userExtRoot] - 用户扩展根目录(resolveUserExtRoot 结果)
|
|
168
|
+
* @param {boolean} [opts.noExt] - 禁用全部扩展
|
|
169
|
+
* @param {string[]} [opts.extNames] - 白名单扩展名
|
|
170
|
+
* @param {string} [opts.dataDir] - CLI 数据目录(扩展数据落 <dataDir>/web-ext/<name>)
|
|
171
|
+
* @param {boolean} [opts.open] - 启动后自动打开浏览器
|
|
172
|
+
* @param {(msg: string) => void} [opts.log] - 日志函数(默认 stderr;--json 模式传 no-op)
|
|
173
|
+
* @param {string} [opts.tag] - 日志前缀标签
|
|
174
|
+
* @returns {Promise<import('node:http').Server>} 已监听的 http.Server(不注册信号处理,生命周期由调用方管理)
|
|
175
|
+
* @throws 前端资产缺失 / 端口被占用(EADDRINUSE)等启动错误
|
|
176
|
+
*/
|
|
177
|
+
export async function startWebServer({
|
|
178
|
+
port,
|
|
179
|
+
backend,
|
|
180
|
+
webRootFlag = '',
|
|
181
|
+
extRootFlags = [],
|
|
182
|
+
userExtRoot = '',
|
|
183
|
+
noExt = false,
|
|
184
|
+
extNames = [],
|
|
185
|
+
dataDir = '',
|
|
186
|
+
open = false,
|
|
187
|
+
log = (msg) => process.stderr.write(`${msg}\n`),
|
|
188
|
+
tag = '3rdloop serve',
|
|
189
|
+
}) {
|
|
190
|
+
const webRoot = resolveWebRoot(webRootFlag);
|
|
283
191
|
|
|
284
192
|
// 加载扩展 → 应用 --no-ext / --ext 开关 → 初始化路由
|
|
285
|
-
const extRoots =
|
|
286
|
-
const discovered = loadExtensions(extRoots,
|
|
287
|
-
const extensions =
|
|
193
|
+
const extRoots = resolveExtRoots(extRootFlags, userExtRoot);
|
|
194
|
+
const discovered = loadExtensions(extRoots, log).extensions;
|
|
195
|
+
const extensions = applyExtFilter(discovered, { noExt, extNames }, log);
|
|
288
196
|
for (const ext of extensions) {
|
|
289
|
-
ext.router = initExtRouter(ext, { cliRoot: CLI_ROOT, dataDir, logger:
|
|
197
|
+
ext.router = initExtRouter(ext, { cliRoot: CLI_ROOT, dataDir, logger: log });
|
|
290
198
|
}
|
|
291
199
|
const mounted = extensions.filter((e) => e.page || e.router);
|
|
292
200
|
|
|
293
201
|
const server = http.createServer((req, res) => {
|
|
294
|
-
_handleRequest(req, res, { webRoot, extensions, backend
|
|
202
|
+
_handleRequest(req, res, { webRoot, extensions, backend })
|
|
295
203
|
.catch((err) => {
|
|
296
|
-
|
|
204
|
+
log(`[${tag}] 请求处理异常: ${err.message}`);
|
|
297
205
|
if (!res.headersSent) {
|
|
298
206
|
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
299
207
|
}
|
|
@@ -303,52 +211,32 @@ async function _startServer({ argv, userExtRoot, dataDir }) {
|
|
|
303
211
|
});
|
|
304
212
|
});
|
|
305
213
|
|
|
306
|
-
|
|
307
|
-
server.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
} else {
|
|
311
|
-
process.stderr.write(`[3rdloop web] 服务启动失败: ${err.message}\n`);
|
|
312
|
-
}
|
|
313
|
-
resolve(EXIT.ERROR);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
server.listen(argv.port, () => {
|
|
317
|
-
process.stderr.write(`[3rdloop web] 前端服务已启动: http://localhost:${argv.port}\n`);
|
|
318
|
-
process.stderr.write(`[3rdloop web] 前端资产: ${webRoot}\n`);
|
|
319
|
-
process.stderr.write(`[3rdloop web] 后端代理: ${argv.backend}(/api/*、/stream*)\n`);
|
|
320
|
-
if (argv.noExt) {
|
|
321
|
-
process.stderr.write('[3rdloop web] 扩展挂载已禁用(--no-ext):仅核心工作台 + loop + knowledge\n');
|
|
322
|
-
} else if (argv.extNames.length > 0) {
|
|
323
|
-
if (mounted.length > 0) {
|
|
324
|
-
const desc = mounted.map((e) => `${e.mount}(${e.title})`).join('、');
|
|
325
|
-
process.stderr.write(`[3rdloop web] 白名单挂载扩展 ${mounted.length}/${discovered.length} 个: ${desc}\n`);
|
|
326
|
-
} else {
|
|
327
|
-
process.stderr.write(`[3rdloop web] 白名单未匹配到任何扩展(发现 ${discovered.length} 个,--ext: ${argv.extNames.join(', ')})\n`);
|
|
328
|
-
}
|
|
329
|
-
} else if (mounted.length > 0) {
|
|
330
|
-
const desc = mounted.map((e) => `${e.mount}(${e.title})`).join('、');
|
|
331
|
-
process.stderr.write(`[3rdloop web] 已挂载扩展 ${mounted.length} 个: ${desc}\n`);
|
|
332
|
-
} else {
|
|
333
|
-
process.stderr.write(
|
|
334
|
-
'[3rdloop web] 未挂载扩展(tag/issue/prcheck 等二级功能可用 "3rdloop web install-ext <dir>" 启用)\n'
|
|
335
|
-
);
|
|
336
|
-
}
|
|
214
|
+
await new Promise((resolve, reject) => {
|
|
215
|
+
server.once('error', reject);
|
|
216
|
+
server.listen(port, () => resolve());
|
|
217
|
+
});
|
|
337
218
|
|
|
338
|
-
|
|
339
|
-
|
|
219
|
+
log(`[${tag}] Web 前端已启动: http://localhost:${port}`);
|
|
220
|
+
log(`[${tag}] 前端资产: ${webRoot}`);
|
|
221
|
+
log(`[${tag}] 后端代理: ${backend}(/api/*、/stream*)`);
|
|
222
|
+
if (noExt) {
|
|
223
|
+
log(`[${tag}] 扩展挂载已禁用(--no-ext):仅核心工作台 + loop + knowledge`);
|
|
224
|
+
} else if (extNames.length > 0) {
|
|
225
|
+
if (mounted.length > 0) {
|
|
226
|
+
const desc = mounted.map((e) => `${e.mount}(${e.title})`).join('、');
|
|
227
|
+
log(`[${tag}] 白名单挂载扩展 ${mounted.length}/${discovered.length} 个: ${desc}`);
|
|
228
|
+
} else {
|
|
229
|
+
log(`[${tag}] 白名单未匹配到任何扩展(发现 ${discovered.length} 个,--ext: ${extNames.join(', ')})`);
|
|
230
|
+
}
|
|
231
|
+
} else if (mounted.length > 0) {
|
|
232
|
+
const desc = mounted.map((e) => `${e.mount}(${e.title})`).join('、');
|
|
233
|
+
log(`[${tag}] 已挂载扩展 ${mounted.length} 个: ${desc}`);
|
|
234
|
+
} else {
|
|
235
|
+
log(`[${tag}] 未挂载扩展(tag/issue/prcheck 等二级功能可用 "3rdloop serve install-ext <dir>" 启用)`);
|
|
236
|
+
}
|
|
340
237
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
process.stderr.write('\n[3rdloop web] 正在关闭…\n');
|
|
344
|
-
server.close(() => process.exit(code));
|
|
345
|
-
// 兜底:close 回调被长连接阻塞时强制退出
|
|
346
|
-
setTimeout(() => process.exit(code), 1500).unref();
|
|
347
|
-
};
|
|
348
|
-
process.on('SIGINT', () => shutdown(130));
|
|
349
|
-
process.on('SIGTERM', () => shutdown(143));
|
|
350
|
-
// cmdWeb 的 Promise 不 resolve:服务常驻,退出走信号处理
|
|
351
|
-
});
|
|
238
|
+
if (open) _openBrowser(`http://localhost:${port}`);
|
|
239
|
+
return server;
|
|
352
240
|
}
|
|
353
241
|
|
|
354
242
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -541,12 +429,12 @@ function _openBrowser(url) {
|
|
|
541
429
|
}
|
|
542
430
|
|
|
543
431
|
// ═══════════════════════════════════════════════════════════════════
|
|
544
|
-
//
|
|
432
|
+
// 扩展管理子命令(由 3rdloop serve install-ext/list-ext/remove-ext 分发)
|
|
545
433
|
// ═══════════════════════════════════════════════════════════════════
|
|
546
434
|
|
|
547
|
-
function
|
|
435
|
+
export function webExtInstall(srcDir, { userExtRoot, jsonMode, out }) {
|
|
548
436
|
if (!srcDir) {
|
|
549
|
-
process.stderr.write('用法: 3rdloop
|
|
437
|
+
process.stderr.write('用法: 3rdloop serve install-ext <扩展目录>\n');
|
|
550
438
|
return EXIT.ERROR;
|
|
551
439
|
}
|
|
552
440
|
try {
|
|
@@ -563,9 +451,9 @@ function _cmdInstallExt(srcDir, { userExtRoot, jsonMode, out }) {
|
|
|
563
451
|
}
|
|
564
452
|
}
|
|
565
453
|
|
|
566
|
-
function
|
|
454
|
+
export function webExtList({ userExtRoot, jsonMode, out }) {
|
|
567
455
|
const warn = (msg) => process.stderr.write(`${msg}\n`);
|
|
568
|
-
const extRoots =
|
|
456
|
+
const extRoots = resolveExtRoots([], userExtRoot);
|
|
569
457
|
const { extensions } = loadExtensions(extRoots, warn);
|
|
570
458
|
|
|
571
459
|
const items = extensions.map((e) => ({
|
|
@@ -586,7 +474,7 @@ function _cmdListExt({ userExtRoot, jsonMode, out }) {
|
|
|
586
474
|
if (items.length === 0) {
|
|
587
475
|
process.stdout.write('暂无可用扩展。\n');
|
|
588
476
|
process.stdout.write(`扩展目录(可手动放置或用 install-ext 安装): ${userExtRoot}\n`);
|
|
589
|
-
process.stdout.write('仓库内置扩展源: <仓库>/Web/ext/,如 3rdloop
|
|
477
|
+
process.stdout.write('仓库内置扩展源: <仓库>/Web/ext/,如 3rdloop serve install-ext Web/ext/tag\n');
|
|
590
478
|
return EXIT.OK;
|
|
591
479
|
}
|
|
592
480
|
|
|
@@ -598,9 +486,9 @@ function _cmdListExt({ userExtRoot, jsonMode, out }) {
|
|
|
598
486
|
return EXIT.OK;
|
|
599
487
|
}
|
|
600
488
|
|
|
601
|
-
function
|
|
489
|
+
export function webExtRemove(name, { userExtRoot, jsonMode, out }) {
|
|
602
490
|
if (!name) {
|
|
603
|
-
process.stderr.write('用法: 3rdloop
|
|
491
|
+
process.stderr.write('用法: 3rdloop serve remove-ext <扩展名>\n');
|
|
604
492
|
return EXIT.ERROR;
|
|
605
493
|
}
|
|
606
494
|
try {
|
|
@@ -616,49 +504,3 @@ function _cmdRemoveExt(name, { userExtRoot, jsonMode, out }) {
|
|
|
616
504
|
return EXIT.ERROR;
|
|
617
505
|
}
|
|
618
506
|
}
|
|
619
|
-
|
|
620
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
621
|
-
// 帮助
|
|
622
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
623
|
-
|
|
624
|
-
function _printWebHelp() {
|
|
625
|
-
process.stdout.write(`
|
|
626
|
-
3rdloop web —— 启动 Web 前端服务(静态托管 + /api 反代 + 扩展挂载)
|
|
627
|
-
|
|
628
|
-
用法:
|
|
629
|
-
3rdloop web [选项] 启动前端服务
|
|
630
|
-
3rdloop web install-ext <dir> 安装扩展到 ~/.3lib/3rdloop/web-ext/
|
|
631
|
-
3rdloop web list-ext 列出可用扩展(含来源与挂载点)
|
|
632
|
-
3rdloop web remove-ext <name> 移除已安装扩展
|
|
633
|
-
|
|
634
|
-
选项:
|
|
635
|
-
--port <N> 监听端口(默认 8080)
|
|
636
|
-
--backend <url> 后端 Server 地址(默认 http://127.0.0.1:3000,/api/* 与 /stream* 反代)
|
|
637
|
-
--web-root <dir> 核心前端资产目录覆盖(默认: 仓库 Web/public,发布包为内置 cli/web)
|
|
638
|
-
--ext-root <dir> 追加扩展根目录(可多次指定;目录下每个子目录是一个扩展)
|
|
639
|
-
--ext <name,...> 白名单:仅挂载指定扩展(逗号分隔,可重复指定;未发现的名字打警告)
|
|
640
|
-
--no-ext 禁用全部扩展(纯后端操作模式;优先于 --ext)
|
|
641
|
-
--open 启动后自动打开浏览器
|
|
642
|
-
-h, --help 本帮助
|
|
643
|
-
|
|
644
|
-
两种典型模式:
|
|
645
|
-
本地全功能(仓库开发态):
|
|
646
|
-
node cli/bin/3rdloop.mjs web
|
|
647
|
-
= 核心前端(Web/public:PR/仓库检视 + loop + knowledge)+ 全部扩展(Web/ext:tag/issue/prcheck)
|
|
648
|
-
纯后端操作(经 3rdloop,不受已安装扩展影响):
|
|
649
|
-
3rdloop web --no-ext
|
|
650
|
-
= 仅核心工作台,导航栏不出现 tag/issue/prcheck 等二级功能入口
|
|
651
|
-
|
|
652
|
-
扩展机制:
|
|
653
|
-
- 扩展 = 自包含目录(manifest.json + router.cjs + public/),提供 /tag、
|
|
654
|
-
/issue、/prcheck 等二级目录功能,不随 npm 包分发
|
|
655
|
-
- 加载来源优先级: --ext-root > 3LIB_WEB_EXT_ROOT > 仓库 Web/ext(开发态)> ~/.3lib/3rdloop/web-ext
|
|
656
|
-
- 扩展数据目录: <数据目录>/web-ext/<name>(跟随 --data-dir)
|
|
657
|
-
- 启用示例: 3rdloop web install-ext Web/ext/tag && 3rdloop web
|
|
658
|
-
- 前端导航栏通过 /api/ext-registry 自动渲染已挂载扩展入口
|
|
659
|
-
|
|
660
|
-
环境变量:
|
|
661
|
-
3LIB_WEB_ROOT 核心前端资产目录(等效 --web-root)
|
|
662
|
-
3LIB_WEB_EXT_ROOT 用户扩展根目录(默认 ~/.3lib/3rdloop/web-ext)
|
|
663
|
-
`);
|
|
664
|
-
}
|
package/lib/workflow.js
CHANGED
|
@@ -134,6 +134,17 @@ export async function cmdWorkflow({ wf, wfWords, flags, env }) {
|
|
|
134
134
|
return EXIT.OK;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// ── 环境预检(正式执行前快速失败;dry-run 已在上方返回,不预检) ──
|
|
138
|
+
{
|
|
139
|
+
const { precheckEnv, reportPrecheckFailures } = await import('./doctor.js');
|
|
140
|
+
const pre = await precheckEnv({ projectRoot: env.projectRoot, dataDir, skillDir: env.skillDir });
|
|
141
|
+
if (!pre.ok) {
|
|
142
|
+
if (jsonMode) out({ ok: false, error: '环境预检未通过', failures: pre.failures });
|
|
143
|
+
else reportPrecheckFailures(pre.failures);
|
|
144
|
+
return EXIT.ERROR;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
137
148
|
// ── 正式执行:复用 orch 链路 ────────────────────────────────────
|
|
138
149
|
// taskId 缺省由 runner 生成(generateTaskId),workflow 可覆盖
|
|
139
150
|
const r = await _runner();
|
|
@@ -141,12 +152,11 @@ export async function cmdWorkflow({ wf, wfWords, flags, env }) {
|
|
|
141
152
|
projectRoot: env.projectRoot,
|
|
142
153
|
dataDir,
|
|
143
154
|
skillDir: env.skillDir,
|
|
144
|
-
cliType: 'opencode',
|
|
145
155
|
verbose,
|
|
146
156
|
});
|
|
147
157
|
|
|
148
|
-
// 挂载
|
|
149
|
-
const oc = await
|
|
158
|
+
// 挂载 AI CLI serve 生命周期(类型由 CLI_TYPE 决定)
|
|
159
|
+
const oc = await _ensureCliServe(ctx, jsonMode, out);
|
|
150
160
|
if (!oc) return EXIT.ERROR;
|
|
151
161
|
|
|
152
162
|
// 信号处理:优雅中止(orchControl abort → kill 子进程)
|
|
@@ -292,15 +302,15 @@ function _queries() {
|
|
|
292
302
|
return import('./queries.js');
|
|
293
303
|
}
|
|
294
304
|
|
|
295
|
-
/** 确保
|
|
296
|
-
async function
|
|
297
|
-
const {
|
|
298
|
-
const oc = new
|
|
305
|
+
/** 确保 AI CLI serve 运行(正式执行需要;dry-run 不需要)。 */
|
|
306
|
+
async function _ensureCliServe(ctx, jsonMode, out) {
|
|
307
|
+
const { CliServeManager } = await import('./opencode.js');
|
|
308
|
+
const oc = new CliServeManager({ projectRoot: ctx.projectRoot, cliType: ctx.cliType });
|
|
299
309
|
const ocHealth = await oc.ensureRunning();
|
|
300
310
|
if (!ocHealth.ok) {
|
|
301
311
|
if (jsonMode) out({ ok: false, error: ocHealth.message });
|
|
302
312
|
process.stderr.write(`\n错误: ${ocHealth.message}\n`);
|
|
303
|
-
process.stderr.write(
|
|
313
|
+
process.stderr.write(`请先启动 ${oc.cliType} serve(项目上一级目录: ${oc.bin} serve)后重试。\n`);
|
|
304
314
|
await oc.stop();
|
|
305
315
|
return null;
|
|
306
316
|
}
|
package/package.json
CHANGED
package/vendor/Server/CLI/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ try {
|
|
|
21
21
|
* 封装 AI 编程助手 CLI(opencode / deveco-code),对外暴露统一的 Session 管理接口。
|
|
22
22
|
*
|
|
23
23
|
* 默认 CLI 类型可在 Server/.env 中配置 CLI_TYPE=opencode|deveco-code,
|
|
24
|
-
* 也可通过 shell 环境变量 CLI_TYPE 指定(优先级高于 .env),均未设置时默认 '
|
|
24
|
+
* 也可通过 shell 环境变量 CLI_TYPE 指定(优先级高于 .env),均未设置时默认 'deveco-code'。
|
|
25
25
|
* 显式传入 type 参数优先级最高。
|
|
26
26
|
*
|
|
27
27
|
* 连接配置同样支持 .env:OPENCODE_HOST/OPENCODE_PORT、DEVECO_HOST/DEVECO_PORT。
|
|
@@ -44,7 +44,7 @@ try {
|
|
|
44
44
|
class CLIController {
|
|
45
45
|
/**
|
|
46
46
|
* @param {'opencode'|'deveco-code'} [type] - CLI 类型,缺省时取环境变量 CLI_TYPE
|
|
47
|
-
* (shell 环境变量 > Server/.env),仍未设置则 '
|
|
47
|
+
* (shell 环境变量 > Server/.env),仍未设置则 'deveco-code'
|
|
48
48
|
* @param {object} [options={}] - 传递给对应适配器的配置项
|
|
49
49
|
*
|
|
50
50
|
* opencode 选项:
|
|
@@ -61,7 +61,7 @@ class CLIController {
|
|
|
61
61
|
* 公共选项:
|
|
62
62
|
* options.archive - MessageArchive 实例(消息存档与卡死检测),不传则适配器内部自建
|
|
63
63
|
*/
|
|
64
|
-
constructor(type = process.env.CLI_TYPE || '
|
|
64
|
+
constructor(type = process.env.CLI_TYPE || 'deveco-code', options = {}) {
|
|
65
65
|
this.type = type;
|
|
66
66
|
|
|
67
67
|
switch (type) {
|
|
@@ -639,7 +639,7 @@ class OpenCodeCLI {
|
|
|
639
639
|
sessionId,
|
|
640
640
|
data.map(m => this._toArchiveMessage(m))
|
|
641
641
|
);
|
|
642
|
-
this._log('
|
|
642
|
+
this._log('debug',
|
|
643
643
|
`自动归档 id=${sessionId} reason=${reason} — appended=${archRes.appended} skipped=${archRes.skipped}`);
|
|
644
644
|
} catch (e) {
|
|
645
645
|
this._log('warn', `自动归档失败 id=${sessionId} reason=${reason}: ${e.message}`);
|
|
@@ -846,7 +846,7 @@ class FlexRunner {
|
|
|
846
846
|
return false;
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
-
this._log('
|
|
849
|
+
this._log('debug', `invokeSkill 完成 — sessionId: ${sessionId}`);
|
|
850
850
|
return true;
|
|
851
851
|
} catch (err) {
|
|
852
852
|
this._log('error', `invokeSkill 异常: ${err.message}`);
|
|
@@ -907,18 +907,18 @@ class FlexRunner {
|
|
|
907
907
|
|
|
908
908
|
try {
|
|
909
909
|
const content = await fs.readFile(summaryFilePath, 'utf-8');
|
|
910
|
-
this._log('
|
|
910
|
+
this._log('debug', `persistKnowledge: 已读取经验文件 (${content.length} 字符)`);
|
|
911
911
|
|
|
912
912
|
// 1. 将经验内容持久化到步骤记录
|
|
913
913
|
await this._persistStep({ knowledgeSummary: content });
|
|
914
|
-
this._log('
|
|
914
|
+
this._log('debug', 'persistKnowledge: 经验已持久化到步骤记录');
|
|
915
915
|
|
|
916
916
|
// 2. 登记到知识沉淀系统
|
|
917
917
|
if (this.#knowledgeManager) {
|
|
918
918
|
// 避免重复登记:先检查是否已存在
|
|
919
919
|
const existing = await this.#knowledgeManager.findByDocPath(summaryFilePath);
|
|
920
920
|
if (existing) {
|
|
921
|
-
this._log('
|
|
921
|
+
this._log('debug', `persistKnowledge: 经验已登记过,跳重复登记 — id: ${existing.id}`);
|
|
922
922
|
} else {
|
|
923
923
|
const record = await this.#knowledgeManager.addExperience({
|
|
924
924
|
docPath: summaryFilePath,
|
|
@@ -929,7 +929,7 @@ class FlexRunner {
|
|
|
929
929
|
skillDir: this.#matchedSkillDir,
|
|
930
930
|
taskDescription: this.#taskDescription
|
|
931
931
|
});
|
|
932
|
-
this._log('
|
|
932
|
+
this._log('debug', `persistKnowledge: 经验已登记到知识沉淀系统 — id: ${record.id}`);
|
|
933
933
|
}
|
|
934
934
|
} else {
|
|
935
935
|
this._log('warn', 'persistKnowledge: KnowledgeManager 未初始化,跳过知识沉淀登记');
|
|
@@ -1112,12 +1112,12 @@ class FlexRunner {
|
|
|
1112
1112
|
const value = raw.replace(/^[#>*\-\s]+/, '').trim();
|
|
1113
1113
|
|
|
1114
1114
|
if (value === '成功') {
|
|
1115
|
-
this._log('
|
|
1115
|
+
this._log('debug', `checkResult: 执行状态 = 成功 (${resultFilePath})`);
|
|
1116
1116
|
return true;
|
|
1117
1117
|
}
|
|
1118
1118
|
|
|
1119
1119
|
if (value === '失败') {
|
|
1120
|
-
this._log('
|
|
1120
|
+
this._log('error', `checkResult: 执行状态 = 失败 (${resultFilePath})`);
|
|
1121
1121
|
return false;
|
|
1122
1122
|
}
|
|
1123
1123
|
|
|
@@ -1578,7 +1578,7 @@ class FlexRunner {
|
|
|
1578
1578
|
|
|
1579
1579
|
// 等待 AI 回复期间收到终止请求 → 不再等待文件生成(session 已被远程中止)
|
|
1580
1580
|
if (this.#aborted) {
|
|
1581
|
-
this._log('
|
|
1581
|
+
this._log('warn', '_summarizeExperience: 等待回复期间被终止,放弃经验总结');
|
|
1582
1582
|
return false;
|
|
1583
1583
|
}
|
|
1584
1584
|
|
|
@@ -1587,7 +1587,7 @@ class FlexRunner {
|
|
|
1587
1587
|
return false;
|
|
1588
1588
|
}
|
|
1589
1589
|
|
|
1590
|
-
this._log('
|
|
1590
|
+
this._log('debug',
|
|
1591
1591
|
`_summarizeExperience: AI 回复完成,content 长度 ${result.content?.length || 0},` +
|
|
1592
1592
|
`等待 Summary 文件生成…`
|
|
1593
1593
|
);
|
|
@@ -1601,7 +1601,7 @@ class FlexRunner {
|
|
|
1601
1601
|
return false;
|
|
1602
1602
|
}
|
|
1603
1603
|
|
|
1604
|
-
this._log('
|
|
1604
|
+
this._log('debug', `经验总结完成 — Summary 文件已生成: ${summaryFilePath}`);
|
|
1605
1605
|
return true;
|
|
1606
1606
|
} catch (err) {
|
|
1607
1607
|
this._log('error', `_summarizeExperience 异常: ${err.message}`);
|