@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/lib/web.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * web.js —— 3rdloop web 命令:启动 Web 前端服务
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
- * 3rdloop web [--port 8080] [--backend http://127.0.0.1:3000]
20
- * [--web-root <dir>] [--ext-root <dir>]... [--open]
21
- * 3rdloop web install-ext <dir> 安装扩展到 ~/.3lib/3rdloop/web-ext/
22
- * 3rdloop web list-ext 列出可用扩展(含来源与挂载点)
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
- // 命令入口(由 cli.js 分发)
68
+ // 目录解析
72
69
  // ═══════════════════════════════════════════════════════════════════
73
70
 
74
71
  /**
75
- * @param {object} opts
76
- * @param {object} opts.flags - parseGlobalFlags 结果
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 async function cmdWeb({ flags, user3libHome, dataDir, jsonMode, out }) {
85
- // 注意:使用 flags.rest(全局 flags 如 --json/--data-dir 已被 parseGlobalFlags 消费)
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 _resolveWebRoot(flagValue) {
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 _resolveExtRoots(extRootFlags, userExtRoot) {
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
- async function _startServer({ argv, userExtRoot, dataDir }) {
274
- const warn = (msg) => process.stderr.write(`${msg}\n`);
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
- let webRoot;
277
- try {
278
- webRoot = _resolveWebRoot(argv.webRoot);
279
- } catch (err) {
280
- process.stderr.write(`[3rdloop web] ${err.message}\n`);
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 = _resolveExtRoots(argv.extRoots, userExtRoot);
286
- const discovered = loadExtensions(extRoots, warn).extensions;
287
- const extensions = _applyExtFilter(discovered, argv, warn);
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: warn });
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: argv.backend })
202
+ _handleRequest(req, res, { webRoot, extensions, backend })
295
203
  .catch((err) => {
296
- process.stderr.write(`[3rdloop web] 请求处理异常: ${err.message}\n`);
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
- return await new Promise((resolve) => {
307
- server.on('error', (err) => {
308
- if (err.code === 'EADDRINUSE') {
309
- process.stderr.write(`[3rdloop web] 端口 ${argv.port} 已被占用(可用 --port 指定其他端口)\n`);
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
- if (argv.open) _openBrowser(`http://localhost:${argv.port}`);
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
- // 退出码与 CLI 约定对齐:130 SIGINT / 143 SIGTERM
342
- const shutdown = (code) => {
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
- // 子命令:install-ext / list-ext / remove-ext
432
+ // 扩展管理子命令(由 3rdloop serve install-ext/list-ext/remove-ext 分发)
545
433
  // ═══════════════════════════════════════════════════════════════════
546
434
 
547
- function _cmdInstallExt(srcDir, { userExtRoot, jsonMode, out }) {
435
+ export function webExtInstall(srcDir, { userExtRoot, jsonMode, out }) {
548
436
  if (!srcDir) {
549
- process.stderr.write('用法: 3rdloop web install-ext <扩展目录>\n');
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 _cmdListExt({ userExtRoot, jsonMode, out }) {
454
+ export function webExtList({ userExtRoot, jsonMode, out }) {
567
455
  const warn = (msg) => process.stderr.write(`${msg}\n`);
568
- const extRoots = _resolveExtRoots([], userExtRoot);
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 web install-ext Web/ext/tag\n');
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 _cmdRemoveExt(name, { userExtRoot, jsonMode, out }) {
489
+ export function webExtRemove(name, { userExtRoot, jsonMode, out }) {
602
490
  if (!name) {
603
- process.stderr.write('用法: 3rdloop web remove-ext <扩展名>\n');
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
- // 挂载 opencode 生命周期
149
- const oc = await _ensureOpcodene(ctx, jsonMode, out);
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
- /** 确保 opencode serve 运行(正式执行需要;dry-run 不需要)。 */
296
- async function _ensureOpcodene(ctx, jsonMode, out) {
297
- const { OpencodeManager } = await import('./opencode.js');
298
- const oc = new OpencodeManager({ projectRoot: ctx.projectRoot });
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('请先启动 opencode serve(项目上一级目录: opencode serve)后重试。\n');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohos-cpf/3rdloop",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "3rdLibraryLoop 三方库自动化检视 CLI:嵌入式复用核心引擎,任务生命周期管理(submit/run/wait/progress/abort/list/result/doctor)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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),均未设置时默认 'opencode'。
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),仍未设置则 'opencode'
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 || 'opencode', options = {}) {
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('info',
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('info', `invokeSkill 完成 — sessionId: ${sessionId}`);
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('info', `persistKnowledge: 已读取经验文件 (${content.length} 字符)`);
910
+ this._log('debug', `persistKnowledge: 已读取经验文件 (${content.length} 字符)`);
911
911
 
912
912
  // 1. 将经验内容持久化到步骤记录
913
913
  await this._persistStep({ knowledgeSummary: content });
914
- this._log('info', 'persistKnowledge: 经验已持久化到步骤记录');
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('info', `persistKnowledge: 经验已登记过,跳重复登记 — id: ${existing.id}`);
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('info', `persistKnowledge: 经验已登记到知识沉淀系统 — id: ${record.id}`);
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('info', `checkResult: 执行状态 = 成功 (${resultFilePath})`);
1115
+ this._log('debug', `checkResult: 执行状态 = 成功 (${resultFilePath})`);
1116
1116
  return true;
1117
1117
  }
1118
1118
 
1119
1119
  if (value === '失败') {
1120
- this._log('info', `checkResult: 执行状态 = 失败 (${resultFilePath})`);
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('info', '_summarizeExperience: 等待回复期间被终止,放弃经验总结');
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('info',
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('info', `经验总结完成 — Summary 文件已生成: ${summaryFilePath}`);
1604
+ this._log('debug', `经验总结完成 — Summary 文件已生成: ${summaryFilePath}`);
1605
1605
  return true;
1606
1606
  } catch (err) {
1607
1607
  this._log('error', `_summarizeExperience 异常: ${err.message}`);