@ohos-cpf/3rdloop 0.0.4 → 0.0.5

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 (43) hide show
  1. package/README.md +119 -128
  2. package/lib/cli.js +25 -0
  3. package/lib/serve.js +268 -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/Skills/arkts-code-use/SKILL.md +270 -0
  9. package/vendor/Server/Skills/arkts-code-use/assets/TEMPLATES.md +367 -0
  10. package/vendor/Server/Skills/arkts-code-use/references/API_VERIFICATION.md +144 -0
  11. package/vendor/Server/Skills/arkts-code-use/references/ARKTS_RULES.md +240 -0
  12. package/vendor/Server/Skills/arkts-code-use/references/CODE_PATTERNS.md +431 -0
  13. package/vendor/Server/Skills/arkts-code-use/references/SYNTAX_CHECK_GUIDE.md +164 -0
  14. package/vendor/Server/Skills/arkts-code-use/scripts/verify-arkts.cjs +428 -0
  15. package/vendor/Server/Skills/gitcode-repo-fork/SKILL.md +310 -0
  16. package/vendor/Server/Skills/gitcode-repo-fork/assets/FORK_REPORT_TEMPLATE.md +113 -0
  17. package/vendor/Server/Skills/gitcode-repo-fork/references/FORK_DECISION_GUIDE.md +124 -0
  18. package/vendor/Server/Skills/gitcode-repo-fork/references/GITCODE_FORK_API.md +95 -0
  19. package/vendor/Server/Skills/gitcode-repo-fork/scripts/gitcode-fork.cjs +285 -0
  20. package/vendor/VERSION +3 -3
  21. package/web/css/arktslibrarycheck.css +322 -0
  22. package/web/css/codecheck.css +464 -0
  23. package/web/css/flutterlibrarycheck.css +322 -0
  24. package/web/css/knowledge.css +332 -0
  25. package/web/css/loop.css +578 -0
  26. package/web/css/md-reader.css +240 -0
  27. package/web/css/rnlibrarycheck.css +322 -0
  28. package/web/css/theme.css +702 -0
  29. package/web/index.html +713 -0
  30. package/web/js/arktslibrarycheck.js +1413 -0
  31. package/web/js/codecheck.js +1039 -0
  32. package/web/js/flutterlibrarycheck.js +1364 -0
  33. package/web/js/health.js +69 -0
  34. package/web/js/knowledge.js +358 -0
  35. package/web/js/loop.js +1102 -0
  36. package/web/js/md-reader.js +435 -0
  37. package/web/js/navigation.js +238 -0
  38. package/web/js/rnlibrarycheck.js +1378 -0
  39. package/web/js/stats.js +110 -0
  40. package/web/js/theme.js +46 -0
  41. package/web/js/utils.js +228 -0
  42. package/web/knowledge.html +146 -0
  43. package/web/loop.html +219 -0
package/lib/update.js CHANGED
@@ -12,7 +12,9 @@
12
12
  *
13
13
  * 安全:
14
14
  * - registry 默认尊重用户 npm 配置(镜像源),--registry 可覆盖
15
- * - 全部经 child_process.execFileSync 调 npm,避免 shell 注入
15
+ * - 全部经 runNpm 调 npm:POSIX 保持 execFileSync 直接调用(无 shell),
16
+ * win32 因 npm 是 npm.cmd shim 需 shell:true(cmd 按 PATHEXT 解析),
17
+ * 该分支对用户可控的 --registry 值做白名单校验防 shell 注入
16
18
  */
17
19
 
18
20
  import fs from 'node:fs';
@@ -26,6 +28,44 @@ const CLI_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
26
28
  const PKG_NAME = '@ohos-cpf/3rdloop';
27
29
  const BIN_ABS = path.join(CLI_ROOT, 'bin', '3rdloop.mjs');
28
30
 
31
+ // ── npm 调用封装 ─────────────────────────────────────────────────
32
+
33
+ // --registry 值来自用户输入,win32 走 shell 前做白名单校验
34
+ const SAFE_REGISTRY_RE = /^https?:\/\/[^\s"'&|<>^();%]+$/;
35
+
36
+ /**
37
+ * 跨平台调用 npm。
38
+ *
39
+ * - POSIX(macOS/Linux):shell 为 false,等价于直接 execFileSync('npm', args),
40
+ * macOS 行为与修复前完全一致。
41
+ * - win32:shell 为 true,Node 用 cmd.exe /d /s /c 执行,cmd 按 PATHEXT
42
+ * 找到 npm.cmd(execFileSync 的 CreateProcess 无法直接执行批处理 shim)。
43
+ *
44
+ * 注入防护:仅 win32 分支对 args 中 --registry 的值做 http(s) URL 白名单校验;
45
+ * 其余参数均为代码内常量,无用户输入面。npm 缺失时把原生 ENOENT/EINVAL
46
+ * 转成可读中文提示。
47
+ */
48
+ export function runNpm(args, opts = {}) {
49
+ const shell = process.platform === 'win32';
50
+ if (shell) {
51
+ const regIdx = args.indexOf('--registry');
52
+ const reg = regIdx !== -1 ? args[regIdx + 1] : null;
53
+ if (reg !== undefined && reg !== null && !SAFE_REGISTRY_RE.test(String(reg))) {
54
+ const err = new Error(`非法 --registry 值(仅允许不含空白/引号/元字符的 http(s) URL): ${reg}`);
55
+ err.code = 'INVALID_REGISTRY';
56
+ throw err;
57
+ }
58
+ }
59
+ try {
60
+ return execFileSync('npm', args, { ...opts, shell });
61
+ } catch (err) {
62
+ if (err && (err.code === 'ENOENT' || err.code === 'EINVAL')) {
63
+ throw new Error('未找到 npm 命令,请确认已安装 Node.js 且 npm 在 PATH 中(npm --version 可验证)');
64
+ }
65
+ throw err;
66
+ }
67
+ }
68
+
29
69
  // ── 工具 ─────────────────────────────────────────────────────────
30
70
 
31
71
  /**
@@ -77,7 +117,7 @@ export async function fetchLatestVersion({ registry } = {}) {
77
117
  const args = ['view', PKG_NAME, 'version'];
78
118
  if (registry) args.push('--registry', registry);
79
119
  try {
80
- const out = execFileSync('npm', args, {
120
+ const out = runNpm(args, {
81
121
  encoding: 'utf-8',
82
122
  stdio: 'pipe',
83
123
  timeout: 30000,
@@ -103,7 +143,7 @@ export async function fetchLatestVersion({ registry } = {}) {
103
143
  /** 从用户 npm 配置读取 registry(无 --registry 时使用镜像源)。 */
104
144
  function _detectNpmRegistry() {
105
145
  try {
106
- const out = execFileSync('npm', ['config', 'get', 'registry'], {
146
+ const out = runNpm(['config', 'get', 'registry'], {
107
147
  encoding: 'utf-8',
108
148
  stdio: 'pipe',
109
149
  timeout: 10000,
@@ -155,16 +195,16 @@ export function performUpdate({ latest, registry, force = false, isLink = detect
155
195
  // 开发态 + --force:先解除链接
156
196
  if (isLink && force) {
157
197
  process.stderr.write(`[update] 检测到 npm link,先解除链接...\n`);
158
- execFileSync('npm', ['unlink', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
198
+ runNpm(['unlink', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
159
199
  }
160
200
 
161
201
  process.stderr.write(`[update] 卸载旧版本(@${PKG_NAME})...\n`);
162
202
  try {
163
- execFileSync('npm', ['uninstall', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
203
+ runNpm(['uninstall', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
164
204
  } catch { /* 卸载失败不阻断重装 */ }
165
205
 
166
206
  process.stderr.write(`[update] 安装最新版本...\n`);
167
- execFileSync('npm', installArgs, { encoding: 'utf-8', stdio: 'inherit' });
207
+ runNpm(installArgs, { encoding: 'utf-8', stdio: 'inherit' });
168
208
 
169
209
  return { updated: true, version: latest };
170
210
  }
package/lib/web-ext.js ADDED
@@ -0,0 +1,454 @@
1
+ /**
2
+ * web-ext.js —— 3rdloop web 扩展机制
3
+ *
4
+ * 职责:
5
+ * - 扩展 manifest.json 的读取与校验(name/title/mount/server/page/order)
6
+ * - 扫描扩展根目录(一个根目录下含多个扩展子目录),多根合并、同名去重
7
+ * - 轻量 express 兼容 shim(Router/req/res),使从 AIWeb 移植的
8
+ * TAG / Issue / PrCheck 等二级目录模块几乎零改动即可挂载
9
+ * - install-ext / remove-ext 的目录安装与卸载
10
+ *
11
+ * 扩展目录约定(自包含,既是开发态源码也是安装单元):
12
+ *
13
+ * <ext-root>/<name>/
14
+ * ├── manifest.json # { name, title, description, mount, order, server, page }
15
+ * ├── router.cjs # 可选;module.exports.createExtRouter(deps) → Router
16
+ * ├── *.cjs # router 的本地依赖(database/gitcodeApi 等)
17
+ * └── public/ # 前端静态资源(page 指向其中 html)
18
+ *
19
+ * createExtRouter(deps) 协议:
20
+ * deps.express — shim:{ Router(): Router }(express.Router 兼容)
21
+ * deps.Database — better-sqlite3 的 Database 类(由宿主解析注入)
22
+ * deps.dbDir — 该扩展的数据目录(<dataDir>/web-ext/<name>)
23
+ * deps.serverRoot — 核心引擎 Server 根目录(vendor/Server 或仓库 Server/)
24
+ * deps.logger — 日志函数
25
+ */
26
+
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { createRequire } from 'node:module';
30
+
31
+ // 保留挂载路径:核心前端与后端代理专用,扩展不得占用
32
+ const RESERVED_MOUNT_PREFIXES = ['/api', '/stream'];
33
+
34
+ /** 校验扩展 name:小写字母开头,允许小写字母/数字/连字符 */
35
+ const NAME_RE = /^[a-z][a-z0-9-]*$/;
36
+
37
+ /**
38
+ * 读取并校验扩展 manifest。
39
+ * @param {string} extDir - 扩展目录(含 manifest.json)
40
+ * @returns {{ name: string, title: string, description: string,
41
+ * mount: string, order: number, server: string|null,
42
+ * page: string|null, dir: string }}
43
+ * @throws {Error} manifest 缺失/非法时抛错(含具体原因)
44
+ */
45
+ export function readManifest(extDir) {
46
+ const manifestPath = path.join(extDir, 'manifest.json');
47
+ if (!fs.existsSync(manifestPath)) {
48
+ throw new Error(`缺少 manifest.json: ${manifestPath}`);
49
+ }
50
+
51
+ let raw;
52
+ try {
53
+ raw = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
54
+ } catch (err) {
55
+ throw new Error(`manifest.json 解析失败 [${extDir}]: ${err.message}`);
56
+ }
57
+
58
+ const name = String(raw.name || '').trim();
59
+ if (!NAME_RE.test(name)) {
60
+ throw new Error(`manifest.name 非法 [${extDir}]: "${name}"(须匹配 ${NAME_RE})`);
61
+ }
62
+
63
+ const mount = String(raw.mount || '').trim().toLowerCase();
64
+ if (!/^\/[a-z][a-z0-9-]*$/.test(mount)) {
65
+ throw new Error(`manifest.mount 非法 [${extDir}]: "${raw.mount}"(格式如 "/tag")`);
66
+ }
67
+ if (RESERVED_MOUNT_PREFIXES.some((p) => mount === p || mount.startsWith(p + '/'))) {
68
+ throw new Error(`manifest.mount 保留 [${extDir}]: "${mount}"(不得占用 /api、/stream)`);
69
+ }
70
+
71
+ // server / page 路径必须落在扩展目录内(防穿越)
72
+ const server = raw.server ? path.resolve(extDir, raw.server) : null;
73
+ if (server && !server.startsWith(extDir + path.sep)) {
74
+ throw new Error(`manifest.server 越界 [${extDir}]: "${raw.server}"`);
75
+ }
76
+ if (server && !fs.existsSync(server)) {
77
+ throw new Error(`manifest.server 不存在 [${extDir}]: "${raw.server}"`);
78
+ }
79
+
80
+ const page = raw.page ? path.resolve(extDir, raw.page) : null;
81
+ if (page && !page.startsWith(extDir + path.sep)) {
82
+ throw new Error(`manifest.page 越界 [${extDir}]: "${raw.page}"`);
83
+ }
84
+ if (page && !fs.existsSync(page)) {
85
+ throw new Error(`manifest.page 不存在 [${extDir}]: "${raw.page}"`);
86
+ }
87
+
88
+ return {
89
+ name,
90
+ title: String(raw.title || name).trim(),
91
+ description: String(raw.description || '').trim(),
92
+ mount,
93
+ order: Number.isFinite(Number(raw.order)) ? Number(raw.order) : 100,
94
+ server,
95
+ page,
96
+ dir: path.resolve(extDir),
97
+ };
98
+ }
99
+
100
+ /**
101
+ * 扫描单个扩展根目录(其下每个子目录视为一个扩展)。
102
+ * 单个扩展损坏只产生 warning,不阻断其他扩展。
103
+ *
104
+ * @param {string} rootDir - 扩展根目录
105
+ * @param {(msg: string) => void} [warn] - 警告输出函数
106
+ * @returns {Array<object>} manifest 数组
107
+ */
108
+ export function scanExtRoot(rootDir, warn = () => {}) {
109
+ const exts = [];
110
+ if (!fs.existsSync(rootDir)) return exts;
111
+
112
+ for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
113
+ if (!entry.isDirectory()) continue;
114
+ const extDir = path.join(rootDir, entry.name);
115
+ try {
116
+ exts.push(readManifest(extDir));
117
+ } catch (err) {
118
+ warn(`[web-ext] 跳过无效扩展: ${err.message}`);
119
+ }
120
+ }
121
+ return exts;
122
+ }
123
+
124
+ /**
125
+ * 从多个扩展根目录合并加载扩展(同名去重:先注册者优先)。
126
+ *
127
+ * @param {string[]} extRoots - 扩展根目录列表(优先级从高到低)
128
+ * @param {(msg: string) => void} [warn]
129
+ * @returns {{ extensions: Array<object>, mounts: Map<string, object> }}
130
+ */
131
+ export function loadExtensions(extRoots, warn = () => {}) {
132
+ const extensions = [];
133
+ const byName = new Map();
134
+ const byMount = new Map();
135
+
136
+ for (const root of extRoots) {
137
+ for (const ext of scanExtRoot(root, warn)) {
138
+ if (byName.has(ext.name)) {
139
+ warn(`[web-ext] 扩展重名 "${ext.name}",忽略来自 ${ext.dir} 的副本(保留 ${byName.get(ext.name).dir})`);
140
+ continue;
141
+ }
142
+ const mountOwner = byMount.get(ext.mount);
143
+ if (mountOwner) {
144
+ warn(`[web-ext] 挂载点冲突 "${ext.mount}"(${ext.name} 与 ${mountOwner.name}),忽略 ${ext.name}`);
145
+ continue;
146
+ }
147
+ byName.set(ext.name, ext);
148
+ byMount.set(ext.mount, ext);
149
+ extensions.push(ext);
150
+ }
151
+ }
152
+
153
+ // 排序:order 升序 → name 字典序(导航展示顺序稳定)
154
+ extensions.sort((a, b) => (a.order - b.order) || a.name.localeCompare(b.name));
155
+ return { extensions, mounts: byMount };
156
+ }
157
+
158
+ // ═══════════════════════════════════════════════════════════════════
159
+ // 轻量 express 兼容 shim
160
+ // ═══════════════════════════════════════════════════════════════════
161
+
162
+ /**
163
+ * 编译路由模式为段数组。
164
+ * '/delete/:id' → [{literal:'delete'}, {param:'id'}]
165
+ */
166
+ function compilePattern(pattern) {
167
+ const segs = String(pattern).split('/').filter((s) => s !== '');
168
+ return segs.map((s) => (s.startsWith(':') ? { param: s.slice(1) } : { literal: s }));
169
+ }
170
+
171
+ /** express.Router() 兼容对象(仅支持本项目扩展用到的子集) */
172
+ export class ShimRouter {
173
+ constructor() {
174
+ /** @type {Array<{method: string, segs: Array, handler: Function}>} */
175
+ this._routes = [];
176
+ }
177
+
178
+ get(p, h) { this._add('GET', p, h); }
179
+ post(p, h) { this._add('POST', p, h); }
180
+ put(p, h) { this._add('PUT', p, h); }
181
+ patch(p, h) { this._add('PATCH', p, h); }
182
+ delete(p, h) { this._add('DELETE', p, h); }
183
+
184
+ /** 暂不支持 middleware/子路由挂载,显式报错便于扩展开发者定位 */
185
+ use() {
186
+ throw new Error('3rdloop web 扩展 shim 不支持 router.use()(仅支持 get/post/put/patch/delete 路由)');
187
+ }
188
+
189
+ _add(method, pattern, handler) {
190
+ if (typeof handler !== 'function') {
191
+ throw new Error(`路由 handler 必须是函数: ${method} ${pattern}`);
192
+ }
193
+ this._routes.push({ method, segs: compilePattern(pattern), handler });
194
+ }
195
+
196
+ /**
197
+ * 匹配请求。
198
+ * @param {string} method - HTTP 方法(大写)
199
+ * @param {string} pathname - 相对挂载点的子路径(如 '/list'、'/delete/42')
200
+ * @returns {{ handler: Function, params: object } | null}
201
+ */
202
+ match(method, pathname) {
203
+ const segs = String(pathname).split('/').filter((s) => s !== '');
204
+ for (const route of this._routes) {
205
+ if (route.method !== method) continue;
206
+ if (route.segs.length !== segs.length) continue;
207
+
208
+ const params = {};
209
+ let ok = true;
210
+ for (let i = 0; i < segs.length; i++) {
211
+ const rs = route.segs[i];
212
+ if (rs.literal !== undefined) {
213
+ if (rs.literal !== segs[i]) { ok = false; break; }
214
+ } else {
215
+ try { params[rs.param] = decodeURIComponent(segs[i]); }
216
+ catch { params[rs.param] = segs[i]; }
217
+ }
218
+ }
219
+ if (ok) return { handler: route.handler, params };
220
+ }
221
+ return null;
222
+ }
223
+ }
224
+
225
+ /** express 模块 shim:仅提供 Router 工厂 */
226
+ export function createExpressShim() {
227
+ return { Router: () => new ShimRouter() };
228
+ }
229
+
230
+ /**
231
+ * 读取请求体并解析 JSON(上限 limitBytes,超出返回 413 错误对象)。
232
+ * 非 JSON 内容解析失败时返回 {}(与扩展路由的容错语义一致)。
233
+ *
234
+ * @param {import('node:http').IncomingMessage} req
235
+ * @param {number} [limitBytes]
236
+ * @returns {Promise<{ ok: boolean, body: object, error?: string }>}
237
+ */
238
+ export function readJsonBody(req, limitBytes = 1024 * 1024) {
239
+ return new Promise((resolve) => {
240
+ const chunks = [];
241
+ let size = 0;
242
+ let done = false;
243
+
244
+ req.on('data', (chunk) => {
245
+ if (done) return;
246
+ size += chunk.length;
247
+ if (size > limitBytes) {
248
+ done = true;
249
+ resolve({ ok: false, body: {}, error: 'payload too large' });
250
+ req.destroy();
251
+ return;
252
+ }
253
+ chunks.push(chunk);
254
+ });
255
+ req.on('end', () => {
256
+ if (done) return;
257
+ done = true;
258
+ const raw = Buffer.concat(chunks).toString('utf8');
259
+ if (!raw) return resolve({ ok: true, body: {} });
260
+ try {
261
+ resolve({ ok: true, body: JSON.parse(raw) });
262
+ } catch {
263
+ resolve({ ok: true, body: {} });
264
+ }
265
+ });
266
+ req.on('error', () => {
267
+ if (done) return;
268
+ done = true;
269
+ resolve({ ok: false, body: {}, error: 'request stream error' });
270
+ });
271
+ });
272
+ }
273
+
274
+ /**
275
+ * 构造 express Response 兼容对象(包一层 node ServerResponse)。
276
+ * 支持:status()(链式)、json()、send()、setHeader()、end()、sendFile()。
277
+ */
278
+ export function createResShim(res) {
279
+ return {
280
+ _status: 200,
281
+ status(code) {
282
+ this._status = code;
283
+ return this;
284
+ },
285
+ setHeader(k, v) {
286
+ res.setHeader(k, v);
287
+ return this;
288
+ },
289
+ json(obj) {
290
+ const body = JSON.stringify(obj);
291
+ if (!res.headersSent) {
292
+ res.writeHead(this._status, { 'Content-Type': 'application/json; charset=utf-8' });
293
+ }
294
+ res.end(body);
295
+ },
296
+ send(data) {
297
+ if (data === undefined || data === null) {
298
+ if (!res.headersSent) res.writeHead(this._status);
299
+ res.end();
300
+ return;
301
+ }
302
+ if (typeof data === 'string' || Buffer.isBuffer(data)) {
303
+ // 尊重 handler 已 setHeader 的 Content-Type(如 CSV 导出),否则默认 html
304
+ if (!res.headersSent && !res.getHeader('Content-Type')) {
305
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
306
+ }
307
+ if (!res.headersSent) res.writeHead(this._status);
308
+ res.end(data);
309
+ return;
310
+ }
311
+ // 对象退化为 JSON(与 express send 语义一致)
312
+ this.json(data);
313
+ },
314
+ end(data) {
315
+ if (!res.headersSent) res.writeHead(this._status);
316
+ res.end(data);
317
+ },
318
+ sendFile(absPath) {
319
+ try {
320
+ const data = fs.readFileSync(absPath);
321
+ if (!res.headersSent && !res.getHeader('Content-Type')) {
322
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
323
+ }
324
+ if (!res.headersSent) res.writeHead(this._status);
325
+ res.end(data);
326
+ } catch {
327
+ if (!res.headersSent) res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
328
+ res.end(JSON.stringify({ success: false, message: 'file not found' }));
329
+ }
330
+ },
331
+ };
332
+ }
333
+
334
+ // ═══════════════════════════════════════════════════════════════════
335
+ // 扩展路由初始化
336
+ // ═══════════════════════════════════════════════════════════════════
337
+
338
+ /**
339
+ * 解析核心引擎 Server 根目录(vendor 优先,开发期回退仓库源码)。
340
+ * @param {string} cliRoot
341
+ * @returns {string | null}
342
+ */
343
+ export function resolveServerRoot(cliRoot) {
344
+ const vendorServer = path.join(cliRoot, 'vendor', 'Server');
345
+ if (fs.existsSync(vendorServer)) return vendorServer;
346
+ const repoServer = path.resolve(cliRoot, '..', 'Server');
347
+ if (fs.existsSync(repoServer)) return repoServer;
348
+ return null;
349
+ }
350
+
351
+ /**
352
+ * 初始化扩展的 API 路由(require server 模块并注入依赖)。
353
+ *
354
+ * @param {object} ext - readManifest 返回的扩展描述
355
+ * @param {object} opts
356
+ * @param {string} opts.cliRoot - cli 包根目录(解析 better-sqlite3 / Server)
357
+ * @param {string} opts.dataDir - CLI 数据目录(扩展数据落在 <dataDir>/web-ext/<name>)
358
+ * @param {Function} opts.logger
359
+ * @returns {ShimRouter | null} 路由实例(无 server 或加载失败返回 null)
360
+ */
361
+ export function initExtRouter(ext, { cliRoot, dataDir, logger = console.error }) {
362
+ if (!ext.server) return null;
363
+
364
+ try {
365
+ // 以扩展目录为基准 require(扩展内部相对依赖正常解析;.cjs 无包类型歧义)
366
+ const requireFromExt = createRequire(path.join(ext.dir, 'manifest.json'));
367
+ const mod = requireFromExt(ext.server);
368
+ const factory = mod && (mod.createExtRouter || mod.default);
369
+ if (typeof factory !== 'function') {
370
+ throw new Error(`${ext.server} 未导出 createExtRouter(deps)`);
371
+ }
372
+
373
+ // better-sqlite3 从 cli 包解析(cli 已声明该依赖)
374
+ let Database = null;
375
+ try {
376
+ const requireFromCli = createRequire(path.join(cliRoot, 'package.json'));
377
+ Database = requireFromCli('better-sqlite3');
378
+ } catch (err) {
379
+ logger(`[web-ext] better-sqlite3 解析失败(${ext.name} 将不可用): ${err.message}`);
380
+ }
381
+
382
+ const dbDir = path.join(dataDir, 'web-ext', ext.name);
383
+ const serverRoot = resolveServerRoot(cliRoot);
384
+
385
+ const router = factory({
386
+ express: createExpressShim(),
387
+ Database,
388
+ dbDir,
389
+ serverRoot,
390
+ logger,
391
+ });
392
+ if (!router || typeof router.match !== 'function') {
393
+ throw new Error('createExtRouter 未返回 Router 实例');
394
+ }
395
+ return router;
396
+ } catch (err) {
397
+ logger(`[web-ext] 扩展路由初始化失败 [${ext.name}]: ${err.message}`);
398
+ return null;
399
+ }
400
+ }
401
+
402
+ // ═══════════════════════════════════════════════════════════════════
403
+ // install-ext / remove-ext
404
+ // ═══════════════════════════════════════════════════════════════════
405
+
406
+ /** 安装时排除的文件/目录 */
407
+ const INSTALL_EXCLUDE = new Set(['node_modules', '.git', '.DS_Store', '.env']);
408
+
409
+ /**
410
+ * 安装扩展:校验源目录 manifest 后整目录复制到 <userExtRoot>/<name>/。
411
+ * 已存在同名扩展时先删除再复制(覆盖更新)。
412
+ *
413
+ * @param {string} srcDir - 源扩展目录
414
+ * @param {string} userExtRoot - 用户扩展根目录(安装目标)
415
+ * @returns {{ name: string, title: string, dest: string }}
416
+ * @throws {Error} 校验失败时抛错
417
+ */
418
+ export function installExtension(srcDir, userExtRoot) {
419
+ const manifest = readManifest(path.resolve(srcDir));
420
+ const dest = path.join(userExtRoot, manifest.name);
421
+
422
+ fs.rmSync(dest, { recursive: true, force: true });
423
+ _copyDirFiltered(path.resolve(srcDir), dest);
424
+ return { name: manifest.name, title: manifest.title, dest };
425
+ }
426
+
427
+ /**
428
+ * 移除已安装扩展(仅作用于用户扩展根目录)。
429
+ * @returns {string} 被移除的目录路径
430
+ * @throws {Error} 扩展不存在时抛错
431
+ */
432
+ export function removeExtension(name, userExtRoot) {
433
+ if (!NAME_RE.test(name)) {
434
+ throw new Error(`非法扩展名: "${name}"`);
435
+ }
436
+ const dest = path.join(userExtRoot, name);
437
+ if (!fs.existsSync(dest)) {
438
+ throw new Error(`未安装扩展 "${name}"(查找目录: ${dest})`);
439
+ }
440
+ fs.rmSync(dest, { recursive: true, force: true });
441
+ return dest;
442
+ }
443
+
444
+ function _copyDirFiltered(src, dst) {
445
+ fs.mkdirSync(dst, { recursive: true });
446
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
447
+ if (INSTALL_EXCLUDE.has(entry.name)) continue;
448
+ const s = path.join(src, entry.name);
449
+ const d = path.join(dst, entry.name);
450
+ if (entry.isDirectory()) _copyDirFiltered(s, d);
451
+ else if (entry.isFile()) fs.copyFileSync(s, d);
452
+ // 符号链接等特殊类型跳过(扩展不应包含)
453
+ }
454
+ }