@ohos-cpf/3rdloop 0.0.4 → 0.0.6

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.
Files changed (47) hide show
  1. package/README.md +119 -128
  2. package/lib/cli.js +25 -0
  3. package/lib/serve.js +280 -0
  4. package/lib/update.js +46 -6
  5. package/lib/web-ext.js +454 -0
  6. package/lib/web.js +664 -0
  7. package/package.json +2 -1
  8. package/vendor/Server/CLI/opencode/index.js +2 -2
  9. package/vendor/Server/DbUse/StorageManager.js +7 -1
  10. package/vendor/Server/Routes/controllers/FlexRunnerController.js +92 -0
  11. package/vendor/Server/Routes/routes/flexrunner.js +2 -0
  12. package/vendor/Server/Skills/arkts-code-use/SKILL.md +270 -0
  13. package/vendor/Server/Skills/arkts-code-use/assets/TEMPLATES.md +367 -0
  14. package/vendor/Server/Skills/arkts-code-use/references/API_VERIFICATION.md +144 -0
  15. package/vendor/Server/Skills/arkts-code-use/references/ARKTS_RULES.md +240 -0
  16. package/vendor/Server/Skills/arkts-code-use/references/CODE_PATTERNS.md +431 -0
  17. package/vendor/Server/Skills/arkts-code-use/references/SYNTAX_CHECK_GUIDE.md +164 -0
  18. package/vendor/Server/Skills/arkts-code-use/scripts/verify-arkts.cjs +428 -0
  19. package/vendor/Server/Skills/gitcode-repo-fork/SKILL.md +310 -0
  20. package/vendor/Server/Skills/gitcode-repo-fork/assets/FORK_REPORT_TEMPLATE.md +113 -0
  21. package/vendor/Server/Skills/gitcode-repo-fork/references/FORK_DECISION_GUIDE.md +124 -0
  22. package/vendor/Server/Skills/gitcode-repo-fork/references/GITCODE_FORK_API.md +95 -0
  23. package/vendor/Server/Skills/gitcode-repo-fork/scripts/gitcode-fork.cjs +285 -0
  24. package/vendor/VERSION +3 -3
  25. package/web/css/arktslibrarycheck.css +322 -0
  26. package/web/css/codecheck.css +464 -0
  27. package/web/css/flutterlibrarycheck.css +322 -0
  28. package/web/css/knowledge.css +332 -0
  29. package/web/css/loop.css +578 -0
  30. package/web/css/md-reader.css +240 -0
  31. package/web/css/rnlibrarycheck.css +322 -0
  32. package/web/css/theme.css +702 -0
  33. package/web/index.html +713 -0
  34. package/web/js/arktslibrarycheck.js +1495 -0
  35. package/web/js/codecheck.js +1098 -0
  36. package/web/js/flutterlibrarycheck.js +1447 -0
  37. package/web/js/health.js +69 -0
  38. package/web/js/knowledge.js +358 -0
  39. package/web/js/loop.js +1102 -0
  40. package/web/js/md-reader.js +435 -0
  41. package/web/js/navigation.js +238 -0
  42. package/web/js/rnlibrarycheck.js +1462 -0
  43. package/web/js/stats.js +110 -0
  44. package/web/js/theme.js +46 -0
  45. package/web/js/utils.js +228 -0
  46. package/web/knowledge.html +146 -0
  47. package/web/loop.html +219 -0
package/lib/web.js ADDED
@@ -0,0 +1,664 @@
1
+ /**
2
+ * web.js —— 3rdloop web 命令:启动 Web 前端服务
3
+ *
4
+ * 架构(方案 A + D 组合):
5
+ * - 核心前端资产:仓库 Web/public(开发态)/ cli/web(发布态,prepack 复制)
6
+ * - 二级目录功能(tag/issue/prcheck 等)以"扩展"形式按需启用,不进 npm 包:
7
+ * * 开发态:自动挂载仓库 Web/ext/ 下全部扩展
8
+ * * 发布态:仅挂载 ~/.3lib/3rdloop/web-ext/ 下用户安装的扩展
9
+ * * 任意环境可用 --ext-root 追加自定义扩展根目录
10
+ * - --web-root 可整目录替换核心前端(跑自定义/实验版前端)
11
+ *
12
+ * 路由优先级:
13
+ * 1. GET /api/ext-registry 扩展注册表(本地响应)
14
+ * 2. /<mount> 与 /<mount>/... 扩展页面 / API / 静态资源
15
+ * 3. /api/* 与 /stream* 反向代理到后端 Server(SSE 透传)
16
+ * 4. 其余 核心前端静态文件
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> 移除已安装扩展
24
+ */
25
+
26
+ import http from 'node:http';
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { spawn } from 'node:child_process';
30
+ import { fileURLToPath } from 'node:url';
31
+ import {
32
+ loadExtensions,
33
+ initExtRouter,
34
+ readJsonBody,
35
+ createResShim,
36
+ installExtension,
37
+ removeExtension,
38
+ } from './web-ext.js';
39
+ import { EXIT } from './exit-codes.js';
40
+
41
+ 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
+
45
+ // ─── MIME ─────────────────────────────────────────────────────────
46
+ const MIME = {
47
+ '.html': 'text/html; charset=utf-8',
48
+ '.htm': 'text/html; charset=utf-8',
49
+ '.js': 'application/javascript; charset=utf-8',
50
+ '.mjs': 'application/javascript; charset=utf-8',
51
+ '.cjs': 'application/javascript; charset=utf-8',
52
+ '.css': 'text/css; charset=utf-8',
53
+ '.json': 'application/json; charset=utf-8',
54
+ '.txt': 'text/plain; charset=utf-8',
55
+ '.csv': 'text/csv; charset=utf-8',
56
+ '.md': 'text/markdown; charset=utf-8',
57
+ '.svg': 'image/svg+xml',
58
+ '.png': 'image/png',
59
+ '.jpg': 'image/jpeg',
60
+ '.jpeg': 'image/jpeg',
61
+ '.gif': 'image/gif',
62
+ '.webp': 'image/webp',
63
+ '.ico': 'image/x-icon',
64
+ '.map': 'application/json; charset=utf-8',
65
+ '.woff': 'font/woff',
66
+ '.woff2': 'font/woff2',
67
+ '.ttf': 'font/ttf',
68
+ };
69
+
70
+ // ═══════════════════════════════════════════════════════════════════
71
+ // 命令入口(由 cli.js 分发)
72
+ // ═══════════════════════════════════════════════════════════════════
73
+
74
+ /**
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>} 退出码
83
+ */
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']
95
+ ? path.resolve(process.env['3LIB_WEB_EXT_ROOT'])
96
+ : 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
+ }
215
+
216
+ // ═══════════════════════════════════════════════════════════════════
217
+ // 目录解析
218
+ // ═══════════════════════════════════════════════════════════════════
219
+
220
+ /**
221
+ * 核心前端资产目录。
222
+ * 优先级:--web-root > 3LIB_WEB_ROOT > 仓库 Web/public(开发态)> cli/web(发布态内置)
223
+ */
224
+ function _resolveWebRoot(flagValue) {
225
+ if (flagValue) {
226
+ const dir = path.resolve(flagValue);
227
+ if (!fs.existsSync(dir)) throw new Error(`--web-root 目录不存在: ${dir}`);
228
+ return dir;
229
+ }
230
+ if (process.env['3LIB_WEB_ROOT']) {
231
+ const dir = path.resolve(process.env['3LIB_WEB_ROOT']);
232
+ if (!fs.existsSync(dir)) throw new Error(`3LIB_WEB_ROOT 目录不存在: ${dir}`);
233
+ return dir;
234
+ }
235
+ // 开发态:cli 上一级的 Web/public(源码,始终优先于构建副本)
236
+ const repoWeb = path.resolve(CLI_ROOT, '..', 'Web', 'public');
237
+ if (fs.existsSync(repoWeb)) return repoWeb;
238
+ // 发布态:prepack 复制的内置前端
239
+ const bundled = path.join(CLI_ROOT, 'web');
240
+ if (fs.existsSync(bundled)) return bundled;
241
+
242
+ throw new Error(
243
+ '未找到前端资产目录:期望仓库 Web/public 或 cli/web。' +
244
+ '发布包缺资产请重新安装;本地开发请在仓库内运行,或用 --web-root 指定。'
245
+ );
246
+ }
247
+
248
+ /**
249
+ * 扩展根目录列表(优先级从高到低,同名扩展先注册者生效)。
250
+ * 顺序:--ext-root(可多个)> 3LIB_WEB_EXT_ROOT > 仓库 Web/ext(开发态)> 用户扩展目录。
251
+ */
252
+ function _resolveExtRoots(extRootFlags, userExtRoot) {
253
+ const roots = [];
254
+ const push = (p) => {
255
+ const r = path.resolve(p);
256
+ if (fs.existsSync(r) && !roots.includes(r)) roots.push(r);
257
+ };
258
+
259
+ for (const f of extRootFlags) push(f);
260
+ if (process.env['3LIB_WEB_EXT_ROOT']) push(process.env['3LIB_WEB_EXT_ROOT']);
261
+
262
+ const repoExt = path.resolve(CLI_ROOT, '..', 'Web', 'ext');
263
+ if (fs.existsSync(repoExt)) push(repoExt);
264
+
265
+ if (fs.existsSync(userExtRoot)) push(userExtRoot);
266
+ return roots;
267
+ }
268
+
269
+ // ═══════════════════════════════════════════════════════════════════
270
+ // 服务启动
271
+ // ═══════════════════════════════════════════════════════════════════
272
+
273
+ async function _startServer({ argv, userExtRoot, dataDir }) {
274
+ const warn = (msg) => process.stderr.write(`${msg}\n`);
275
+
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;
282
+ }
283
+
284
+ // 加载扩展 → 应用 --no-ext / --ext 开关 → 初始化路由
285
+ const extRoots = _resolveExtRoots(argv.extRoots, userExtRoot);
286
+ const discovered = loadExtensions(extRoots, warn).extensions;
287
+ const extensions = _applyExtFilter(discovered, argv, warn);
288
+ for (const ext of extensions) {
289
+ ext.router = initExtRouter(ext, { cliRoot: CLI_ROOT, dataDir, logger: warn });
290
+ }
291
+ const mounted = extensions.filter((e) => e.page || e.router);
292
+
293
+ const server = http.createServer((req, res) => {
294
+ _handleRequest(req, res, { webRoot, extensions, backend: argv.backend })
295
+ .catch((err) => {
296
+ process.stderr.write(`[3rdloop web] 请求处理异常: ${err.message}\n`);
297
+ if (!res.headersSent) {
298
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
299
+ }
300
+ if (!res.writableEnded) {
301
+ res.end(JSON.stringify({ success: false, message: 'internal error' }));
302
+ }
303
+ });
304
+ });
305
+
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
+ }
337
+
338
+ if (argv.open) _openBrowser(`http://localhost:${argv.port}`);
339
+ });
340
+
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
+ });
352
+ }
353
+
354
+ // ═══════════════════════════════════════════════════════════════════
355
+ // 请求路由
356
+ // ═══════════════════════════════════════════════════════════════════
357
+
358
+ async function _handleRequest(req, res, { webRoot, extensions, backend }) {
359
+ const url = new URL(req.url, `http://localhost`);
360
+ let pathname;
361
+ try {
362
+ pathname = decodeURIComponent(url.pathname);
363
+ } catch {
364
+ pathname = url.pathname;
365
+ }
366
+ const lower = pathname.toLowerCase();
367
+
368
+ // 1. 扩展注册表(本地响应,前端导航用)
369
+ if (lower === '/api/ext-registry') {
370
+ _sendJson(res, 200, {
371
+ success: true,
372
+ data: extensions
373
+ .filter((e) => e.page || e.router)
374
+ .map(({ name, title, description, mount, order }) => ({ name, title, description, mount, order })),
375
+ });
376
+ return;
377
+ }
378
+
379
+ // 2. 扩展挂载点(页面 / API / 静态资源)
380
+ for (const ext of extensions) {
381
+ if (lower === ext.mount || lower.startsWith(ext.mount + '/')) {
382
+ const subpath = pathname.slice(ext.mount.length) || '/';
383
+ await _handleExtRequest(ext, req, res, subpath, url);
384
+ return;
385
+ }
386
+ }
387
+
388
+ // 3. 后端反向代理(SSE 透传)
389
+ if (lower.startsWith('/api/') || lower.startsWith('/stream')) {
390
+ _proxyToBackend(req, res, backend);
391
+ return;
392
+ }
393
+
394
+ // 4. 核心前端路由别名(与 Web/server.js 行为对齐)
395
+ if (lower === '/knowledge' || lower.startsWith('/knowledge/')) {
396
+ res.writeHead(302, { Location: '/#knowledge' });
397
+ res.end();
398
+ return;
399
+ }
400
+ if (lower === '/loop.html') {
401
+ res.writeHead(302, { Location: '/#p-loop' });
402
+ res.end();
403
+ return;
404
+ }
405
+
406
+ // 5. 核心前端静态文件
407
+ _serveStatic(webRoot, pathname, res);
408
+ }
409
+
410
+ /** 扩展请求:子路径根 → 页面;其次 API 路由;最后 public/ 静态 */
411
+ async function _handleExtRequest(ext, req, res, subpath, url) {
412
+ const method = (req.method || 'GET').toUpperCase();
413
+
414
+ // 子路径根 → 扩展页面
415
+ if (subpath === '/' || subpath === '') {
416
+ if (ext.page && (method === 'GET' || method === 'HEAD')) {
417
+ _serveFile(ext.page, res);
418
+ } else if (ext.page) {
419
+ _sendJson(res, 405, { success: false, message: 'method not allowed' });
420
+ } else {
421
+ _sendJson(res, 404, { success: false, message: 'extension has no page' });
422
+ }
423
+ return;
424
+ }
425
+
426
+ // API 路由分发
427
+ if (ext.router) {
428
+ const route = ext.router.match(method, subpath);
429
+ if (route) {
430
+ let body = {};
431
+ if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
432
+ const result = await readJsonBody(req);
433
+ if (!result.ok) {
434
+ _sendJson(res, 413, { success: false, message: `request body error: ${result.error}` });
435
+ return;
436
+ }
437
+ body = result.body;
438
+ }
439
+
440
+ const reqShim = {
441
+ method,
442
+ url: subpath,
443
+ headers: req.headers,
444
+ params: route.params,
445
+ query: Object.fromEntries(url.searchParams),
446
+ body,
447
+ };
448
+ const resShim = createResShim(res);
449
+ try {
450
+ await route.handler(reqShim, resShim);
451
+ } catch (err) {
452
+ process.stderr.write(`[web-ext] ${ext.name} ${method} ${subpath} 处理失败: ${err.message}\n`);
453
+ if (!res.headersSent) {
454
+ resShim.status(500).json({ success: false, message: `扩展处理失败: ${err.message}` });
455
+ } else if (!res.writableEnded) {
456
+ res.end();
457
+ }
458
+ }
459
+ return;
460
+ }
461
+ }
462
+
463
+ // 静态资源(ext/public/ 下)
464
+ _serveStatic(path.join(ext.dir, 'public'), subpath, res);
465
+ }
466
+
467
+ // ═══════════════════════════════════════════════════════════════════
468
+ // 静态文件 / 代理 / 工具
469
+ // ═══════════════════════════════════════════════════════════════════
470
+
471
+ /** 静态文件托管(默认页 index.html,含路径穿越防护) */
472
+ function _serveStatic(root, pathname, res) {
473
+ const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
474
+ const filePath = path.resolve(root, rel);
475
+
476
+ // 路径穿越防护
477
+ if (filePath !== root && !filePath.startsWith(root + path.sep)) {
478
+ _sendJson(res, 403, { success: false, message: 'forbidden' });
479
+ return;
480
+ }
481
+ _serveFile(filePath, res);
482
+ }
483
+
484
+ function _serveFile(filePath, res) {
485
+ fs.stat(filePath, (err, stat) => {
486
+ if (err || !stat.isFile()) {
487
+ _sendJson(res, 404, { success: false, message: 'not found' });
488
+ return;
489
+ }
490
+ const type = MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
491
+ res.writeHead(200, { 'Content-Type': type, 'Content-Length': stat.size });
492
+ fs.createReadStream(filePath).pipe(res);
493
+ });
494
+ }
495
+
496
+ function _sendJson(res, status, obj) {
497
+ const body = JSON.stringify(obj);
498
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
499
+ res.end(body);
500
+ }
501
+
502
+ /**
503
+ * 反向代理到后端 Server(复用 Web/server.js 语义:SSE 透传、连接清理)。
504
+ */
505
+ function _proxyToBackend(req, res, backendUrl) {
506
+ const target = new URL(req.url, backendUrl);
507
+ const proxyReq = http.request(target, {
508
+ method: req.method,
509
+ headers: { ...req.headers, host: target.host },
510
+ }, (proxyRes) => {
511
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
512
+ proxyRes.pipe(res);
513
+ // 上游(后端)关闭时通知下游,避免连接悬挂
514
+ proxyRes.on('close', () => { if (!res.writableEnded) res.end(); });
515
+ });
516
+
517
+ proxyReq.on('error', (err) => {
518
+ _sendJson(res, 502, {
519
+ status: 'error',
520
+ message: `后端不可达: ${err.message}`,
521
+ backend: backendUrl,
522
+ });
523
+ });
524
+
525
+ // 浏览器断开(含 EventSource.close、页面关闭/刷新)时销毁上游连接,
526
+ // 使后端能感知连接关闭并正确清理,防止连接泄漏
527
+ res.on('close', () => { if (!proxyReq.destroyed) proxyReq.destroy(); });
528
+ req.on('aborted', () => { if (!proxyReq.destroyed) proxyReq.destroy(); });
529
+
530
+ req.pipe(proxyReq);
531
+ }
532
+
533
+ function _openBrowser(url) {
534
+ try {
535
+ const cmd = process.platform === 'darwin' ? 'open'
536
+ : process.platform === 'win32' ? 'cmd'
537
+ : 'xdg-open';
538
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
539
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => {});
540
+ } catch { /* 打开失败不影响服务 */ }
541
+ }
542
+
543
+ // ═══════════════════════════════════════════════════════════════════
544
+ // 子命令:install-ext / list-ext / remove-ext
545
+ // ═══════════════════════════════════════════════════════════════════
546
+
547
+ function _cmdInstallExt(srcDir, { userExtRoot, jsonMode, out }) {
548
+ if (!srcDir) {
549
+ process.stderr.write('用法: 3rdloop web install-ext <扩展目录>\n');
550
+ return EXIT.ERROR;
551
+ }
552
+ try {
553
+ const { name, title, dest } = installExtension(srcDir, userExtRoot);
554
+ if (jsonMode) {
555
+ out({ success: true, name, title, installedTo: dest });
556
+ } else {
557
+ process.stdout.write(`已安装扩展 "${name}"(${title})→ ${dest}\n`);
558
+ }
559
+ return EXIT.OK;
560
+ } catch (err) {
561
+ process.stderr.write(`安装失败: ${err.message}\n`);
562
+ return EXIT.ERROR;
563
+ }
564
+ }
565
+
566
+ function _cmdListExt({ userExtRoot, jsonMode, out }) {
567
+ const warn = (msg) => process.stderr.write(`${msg}\n`);
568
+ const extRoots = _resolveExtRoots([], userExtRoot);
569
+ const { extensions } = loadExtensions(extRoots, warn);
570
+
571
+ const items = extensions.map((e) => ({
572
+ name: e.name,
573
+ title: e.title,
574
+ mount: e.mount,
575
+ order: e.order,
576
+ source: e.dir,
577
+ hasApi: !!e.server,
578
+ hasPage: !!e.page,
579
+ }));
580
+
581
+ if (jsonMode) {
582
+ out({ total: items.length, extensions: items });
583
+ return EXIT.OK;
584
+ }
585
+
586
+ if (items.length === 0) {
587
+ process.stdout.write('暂无可用扩展。\n');
588
+ process.stdout.write(`扩展目录(可手动放置或用 install-ext 安装): ${userExtRoot}\n`);
589
+ process.stdout.write('仓库内置扩展源: <仓库>/Web/ext/,如 3rdloop web install-ext Web/ext/tag\n');
590
+ return EXIT.OK;
591
+ }
592
+
593
+ process.stdout.write(`可用扩展(${items.length} 个):\n`);
594
+ for (const it of items) {
595
+ process.stdout.write(` ${it.name.padEnd(10)} ${it.mount.padEnd(12)} ${it.title}\n`);
596
+ process.stdout.write(` ${' '.repeat(10)} 来源: ${it.source}\n`);
597
+ }
598
+ return EXIT.OK;
599
+ }
600
+
601
+ function _cmdRemoveExt(name, { userExtRoot, jsonMode, out }) {
602
+ if (!name) {
603
+ process.stderr.write('用法: 3rdloop web remove-ext <扩展名>\n');
604
+ return EXIT.ERROR;
605
+ }
606
+ try {
607
+ const removed = removeExtension(name, userExtRoot);
608
+ if (jsonMode) {
609
+ out({ success: true, name, removedFrom: removed });
610
+ } else {
611
+ process.stdout.write(`已移除扩展 "${name}"(${removed})\n`);
612
+ }
613
+ return EXIT.OK;
614
+ } catch (err) {
615
+ process.stderr.write(`移除失败: ${err.message}\n`);
616
+ return EXIT.ERROR;
617
+ }
618
+ }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohos-cpf/3rdloop",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "3rdLibraryLoop 三方库自动化检视 CLI:嵌入式复用核心引擎,任务生命周期管理(submit/run/wait/progress/abort/list/result/doctor)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "bin",
33
33
  "lib",
34
34
  "vendor",
35
+ "web",
35
36
  "workflows",
36
37
  "README.md"
37
38
  ],