@zhin.js/console 1.0.48 → 1.0.49

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/websocket.js CHANGED
@@ -6,6 +6,34 @@ import { usePlugin } from '@zhin.js/core';
6
6
  // src/websocket.ts
7
7
  var { root, logger } = usePlugin();
8
8
  var ENV_WHITELIST = [".env", ".env.development", ".env.production"];
9
+ var FILE_MANAGER_ALLOWED = [
10
+ "src",
11
+ "plugins",
12
+ "client",
13
+ "package.json",
14
+ "tsconfig.json",
15
+ "zhin.config.yml",
16
+ ".env",
17
+ ".env.development",
18
+ ".env.production",
19
+ "README.md"
20
+ ];
21
+ var FILE_MANAGER_BLOCKED = /* @__PURE__ */ new Set([
22
+ "node_modules",
23
+ ".git",
24
+ ".env.local",
25
+ "data",
26
+ "lib",
27
+ "dist",
28
+ "coverage"
29
+ ]);
30
+ function isPathAllowed(relativePath) {
31
+ if (relativePath.includes("..") || path.isAbsolute(relativePath)) return false;
32
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
33
+ const firstSegment = normalized.split("/")[0];
34
+ if (FILE_MANAGER_BLOCKED.has(firstSegment)) return false;
35
+ return FILE_MANAGER_ALLOWED.some((p) => normalized === p || normalized.startsWith(p + "/"));
36
+ }
9
37
  function resolveConfigKey(pluginName) {
10
38
  const schemaService = root.inject("schema");
11
39
  return schemaService?.resolveConfigKey(pluginName) ?? pluginName;
@@ -236,10 +264,413 @@ async function handleWebSocketMessage(ws, message, webServer) {
236
264
  ws.send(JSON.stringify({ requestId, error: `Failed to save env file: ${error.message}` }));
237
265
  }
238
266
  break;
267
+ // ================================================================
268
+ // 文件管理
269
+ // ================================================================
270
+ case "files:tree":
271
+ try {
272
+ const cwd = process.cwd();
273
+ const tree = buildFileTree(cwd, "", FILE_MANAGER_ALLOWED);
274
+ ws.send(JSON.stringify({ requestId, data: { tree } }));
275
+ } catch (error) {
276
+ ws.send(JSON.stringify({ requestId, error: `Failed to build file tree: ${error.message}` }));
277
+ }
278
+ break;
279
+ case "files:read":
280
+ try {
281
+ const { filePath: fp } = message;
282
+ if (!fp || !isPathAllowed(fp)) {
283
+ ws.send(JSON.stringify({ requestId, error: `Access denied: ${fp}` }));
284
+ break;
285
+ }
286
+ const absPath = path.resolve(process.cwd(), fp);
287
+ if (!fs.existsSync(absPath)) {
288
+ ws.send(JSON.stringify({ requestId, error: `File not found: ${fp}` }));
289
+ break;
290
+ }
291
+ const stat = fs.statSync(absPath);
292
+ if (!stat.isFile()) {
293
+ ws.send(JSON.stringify({ requestId, error: `Not a file: ${fp}` }));
294
+ break;
295
+ }
296
+ if (stat.size > 1024 * 1024) {
297
+ ws.send(JSON.stringify({ requestId, error: `File too large: ${(stat.size / 1024).toFixed(0)}KB (max 1MB)` }));
298
+ break;
299
+ }
300
+ const fileContent = fs.readFileSync(absPath, "utf-8");
301
+ ws.send(JSON.stringify({ requestId, data: { content: fileContent, size: stat.size } }));
302
+ } catch (error) {
303
+ ws.send(JSON.stringify({ requestId, error: `Failed to read file: ${error.message}` }));
304
+ }
305
+ break;
306
+ case "files:save":
307
+ try {
308
+ const { filePath: fp, content: fileContent } = message;
309
+ if (!fp || !isPathAllowed(fp)) {
310
+ ws.send(JSON.stringify({ requestId, error: `Access denied: ${fp}` }));
311
+ break;
312
+ }
313
+ if (typeof fileContent !== "string") {
314
+ ws.send(JSON.stringify({ requestId, error: "content field is required" }));
315
+ break;
316
+ }
317
+ const absPath = path.resolve(process.cwd(), fp);
318
+ const dir = path.dirname(absPath);
319
+ if (!fs.existsSync(dir)) {
320
+ fs.mkdirSync(dir, { recursive: true });
321
+ }
322
+ fs.writeFileSync(absPath, fileContent, "utf-8");
323
+ ws.send(JSON.stringify({ requestId, data: { success: true, message: `\u6587\u4EF6\u5DF2\u4FDD\u5B58: ${fp}` } }));
324
+ } catch (error) {
325
+ ws.send(JSON.stringify({ requestId, error: `Failed to save file: ${error.message}` }));
326
+ }
327
+ break;
328
+ // ================================================================
329
+ // 数据库管理
330
+ // ================================================================
331
+ case "db:info":
332
+ try {
333
+ const dbInfo = getDatabaseInfo();
334
+ ws.send(JSON.stringify({ requestId, data: dbInfo }));
335
+ } catch (error) {
336
+ ws.send(JSON.stringify({ requestId, error: `Failed to get db info: ${error.message}` }));
337
+ }
338
+ break;
339
+ case "db:tables":
340
+ try {
341
+ const tables = getDatabaseTables();
342
+ ws.send(JSON.stringify({ requestId, data: { tables } }));
343
+ } catch (error) {
344
+ ws.send(JSON.stringify({ requestId, error: `Failed to list tables: ${error.message}` }));
345
+ }
346
+ break;
347
+ case "db:select":
348
+ try {
349
+ const { table, page = 1, pageSize = 50, where } = message;
350
+ if (!table) {
351
+ ws.send(JSON.stringify({ requestId, error: "table is required" }));
352
+ break;
353
+ }
354
+ const selectResult = await dbSelect(table, page, pageSize, where);
355
+ ws.send(JSON.stringify({ requestId, data: selectResult }));
356
+ } catch (error) {
357
+ ws.send(JSON.stringify({ requestId, error: `Failed to select: ${error.message}` }));
358
+ }
359
+ break;
360
+ case "db:insert":
361
+ try {
362
+ const { table, row } = message;
363
+ if (!table || !row) {
364
+ ws.send(JSON.stringify({ requestId, error: "table and row are required" }));
365
+ break;
366
+ }
367
+ await dbInsert(table, row);
368
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
369
+ } catch (error) {
370
+ ws.send(JSON.stringify({ requestId, error: `Failed to insert: ${error.message}` }));
371
+ }
372
+ break;
373
+ case "db:update":
374
+ try {
375
+ const { table, row, where: updateWhere } = message;
376
+ if (!table || !row || !updateWhere) {
377
+ ws.send(JSON.stringify({ requestId, error: "table, row, and where are required" }));
378
+ break;
379
+ }
380
+ const affected = await dbUpdate(table, row, updateWhere);
381
+ ws.send(JSON.stringify({ requestId, data: { success: true, affected } }));
382
+ } catch (error) {
383
+ ws.send(JSON.stringify({ requestId, error: `Failed to update: ${error.message}` }));
384
+ }
385
+ break;
386
+ case "db:delete":
387
+ try {
388
+ const { table, where: deleteWhere } = message;
389
+ if (!table || !deleteWhere) {
390
+ ws.send(JSON.stringify({ requestId, error: "table and where are required" }));
391
+ break;
392
+ }
393
+ const deleted = await dbDelete(table, deleteWhere);
394
+ ws.send(JSON.stringify({ requestId, data: { success: true, deleted } }));
395
+ } catch (error) {
396
+ ws.send(JSON.stringify({ requestId, error: `Failed to delete: ${error.message}` }));
397
+ }
398
+ break;
399
+ case "db:drop-table":
400
+ try {
401
+ const { table: dropTableName } = message;
402
+ if (!dropTableName) {
403
+ ws.send(JSON.stringify({ requestId, error: "table is required" }));
404
+ break;
405
+ }
406
+ await dbDropTable(dropTableName);
407
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
408
+ } catch (error) {
409
+ ws.send(JSON.stringify({ requestId, error: `Failed to drop table: ${error.message}` }));
410
+ }
411
+ break;
412
+ // KV 专用操作
413
+ case "db:kv:get":
414
+ try {
415
+ const { table, key } = message;
416
+ if (!table || !key) {
417
+ ws.send(JSON.stringify({ requestId, error: "table and key are required" }));
418
+ break;
419
+ }
420
+ const kvValue = await kvGet(table, key);
421
+ ws.send(JSON.stringify({ requestId, data: { key, value: kvValue } }));
422
+ } catch (error) {
423
+ ws.send(JSON.stringify({ requestId, error: `Failed to get kv: ${error.message}` }));
424
+ }
425
+ break;
426
+ case "db:kv:set":
427
+ try {
428
+ const { table, key, value, ttl } = message;
429
+ if (!table || !key) {
430
+ ws.send(JSON.stringify({ requestId, error: "table and key are required" }));
431
+ break;
432
+ }
433
+ await kvSet(table, key, value, ttl);
434
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
435
+ } catch (error) {
436
+ ws.send(JSON.stringify({ requestId, error: `Failed to set kv: ${error.message}` }));
437
+ }
438
+ break;
439
+ case "db:kv:delete":
440
+ try {
441
+ const { table, key } = message;
442
+ if (!table || !key) {
443
+ ws.send(JSON.stringify({ requestId, error: "table and key are required" }));
444
+ break;
445
+ }
446
+ await kvDelete(table, key);
447
+ ws.send(JSON.stringify({ requestId, data: { success: true } }));
448
+ } catch (error) {
449
+ ws.send(JSON.stringify({ requestId, error: `Failed to delete kv: ${error.message}` }));
450
+ }
451
+ break;
452
+ case "db:kv:entries":
453
+ try {
454
+ const { table } = message;
455
+ if (!table) {
456
+ ws.send(JSON.stringify({ requestId, error: "table is required" }));
457
+ break;
458
+ }
459
+ const kvEntries = await kvGetEntries(table);
460
+ ws.send(JSON.stringify({ requestId, data: { entries: kvEntries } }));
461
+ } catch (error) {
462
+ ws.send(JSON.stringify({ requestId, error: `Failed to get entries: ${error.message}` }));
463
+ }
464
+ break;
239
465
  default:
240
466
  ws.send(JSON.stringify({ requestId, error: `Unknown message type: ${type}` }));
241
467
  }
242
468
  }
469
+ function getDb() {
470
+ return root.inject("database");
471
+ }
472
+ function getDbType() {
473
+ const dbFeature = getDb();
474
+ const dialectName = dbFeature.db.dialect.name;
475
+ if (["mongodb"].includes(dialectName)) return "document";
476
+ if (["redis"].includes(dialectName)) return "keyvalue";
477
+ return "related";
478
+ }
479
+ function getDatabaseInfo() {
480
+ const dbFeature = getDb();
481
+ const db = dbFeature.db;
482
+ return {
483
+ dialect: db.dialectName,
484
+ type: getDbType(),
485
+ tables: Array.from(db.models.keys())
486
+ };
487
+ }
488
+ function getDatabaseTables() {
489
+ const dbFeature = getDb();
490
+ const db = dbFeature.db;
491
+ getDbType();
492
+ const tables = [];
493
+ for (const [name] of db.models) {
494
+ const def = db.definitions.get(name);
495
+ tables.push({ name, columns: def ? Object.fromEntries(Object.entries(def).map(([col, colDef]) => [col, colDef])) : void 0 });
496
+ }
497
+ return tables;
498
+ }
499
+ async function dbSelect(table, page, pageSize, where) {
500
+ const dbFeature = getDb();
501
+ const db = dbFeature.db;
502
+ const dbType = getDbType();
503
+ const model = db.models.get(table);
504
+ if (!model) throw new Error(`Table '${table}' not found`);
505
+ if (dbType === "keyvalue") {
506
+ const kvModel = model;
507
+ const allEntries = await kvModel.entries();
508
+ const total2 = allEntries.length;
509
+ const start = (page - 1) * pageSize;
510
+ const rows2 = allEntries.slice(start, start + pageSize).map(([k, v]) => ({ key: k, value: v }));
511
+ return { rows: rows2, total: total2, page, pageSize };
512
+ }
513
+ let selection = model.select();
514
+ if (where && Object.keys(where).length > 0) {
515
+ selection = selection.where(where);
516
+ }
517
+ let total;
518
+ try {
519
+ const countResult = await db.aggregate(table).count("*", "total").where(where || {});
520
+ total = countResult?.[0]?.total ?? 0;
521
+ } catch {
522
+ const all = await model.select();
523
+ total = all.length;
524
+ }
525
+ const offset = (page - 1) * pageSize;
526
+ let query = model.select();
527
+ if (where && Object.keys(where).length > 0) {
528
+ query = query.where(where);
529
+ }
530
+ const rows = await query.limit(pageSize).offset(offset);
531
+ return { rows, total, page, pageSize };
532
+ }
533
+ async function dbInsert(table, row) {
534
+ const dbFeature = getDb();
535
+ const db = dbFeature.db;
536
+ const dbType = getDbType();
537
+ const model = db.models.get(table);
538
+ if (!model) throw new Error(`Table '${table}' not found`);
539
+ if (dbType === "keyvalue") {
540
+ const kvModel = model;
541
+ if (!row.key) throw new Error("key is required for KV insert");
542
+ await kvModel.set(row.key, row.value);
543
+ return;
544
+ }
545
+ if (dbType === "document") {
546
+ await model.create(row);
547
+ return;
548
+ }
549
+ await model.insert(row);
550
+ }
551
+ async function dbUpdate(table, row, where) {
552
+ const dbFeature = getDb();
553
+ const db = dbFeature.db;
554
+ const dbType = getDbType();
555
+ const model = db.models.get(table);
556
+ if (!model) throw new Error(`Table '${table}' not found`);
557
+ if (dbType === "keyvalue") {
558
+ const kvModel = model;
559
+ if (!where.key) throw new Error("key is required for KV update");
560
+ await kvModel.set(where.key, row.value);
561
+ return 1;
562
+ }
563
+ if (dbType === "document") {
564
+ if (where._id) {
565
+ return await model.updateById(where._id, row);
566
+ }
567
+ }
568
+ return await model.update(row).where(where);
569
+ }
570
+ async function dbDelete(table, where) {
571
+ const dbFeature = getDb();
572
+ const db = dbFeature.db;
573
+ const dbType = getDbType();
574
+ const model = db.models.get(table);
575
+ if (!model) throw new Error(`Table '${table}' not found`);
576
+ if (dbType === "keyvalue") {
577
+ const kvModel = model;
578
+ if (!where.key) throw new Error("key is required for KV delete");
579
+ await kvModel.deleteByKey(where.key);
580
+ return 1;
581
+ }
582
+ if (dbType === "document") {
583
+ if (where._id) {
584
+ return await model.deleteById(where._id);
585
+ }
586
+ }
587
+ return await model.delete(where);
588
+ }
589
+ async function dbDropTable(table) {
590
+ const dbFeature = getDb();
591
+ const db = dbFeature.db;
592
+ const model = db.models.get(table);
593
+ if (!model) throw new Error(`Table '${table}' not found`);
594
+ const sql = db.dialect.formatDropTable(table, true);
595
+ await db.query(sql);
596
+ db.models.delete(table);
597
+ db.definitions.delete(table);
598
+ }
599
+ async function kvGet(table, key) {
600
+ const dbFeature = getDb();
601
+ const model = dbFeature.db.models.get(table);
602
+ if (!model) throw new Error(`Bucket '${table}' not found`);
603
+ return await model.get(key);
604
+ }
605
+ async function kvSet(table, key, value, ttl) {
606
+ const dbFeature = getDb();
607
+ const model = dbFeature.db.models.get(table);
608
+ if (!model) throw new Error(`Bucket '${table}' not found`);
609
+ await model.set(key, value, ttl);
610
+ }
611
+ async function kvDelete(table, key) {
612
+ const dbFeature = getDb();
613
+ const model = dbFeature.db.models.get(table);
614
+ if (!model) throw new Error(`Bucket '${table}' not found`);
615
+ await model.deleteByKey(key);
616
+ }
617
+ async function kvGetEntries(table) {
618
+ const dbFeature = getDb();
619
+ const model = dbFeature.db.models.get(table);
620
+ if (!model) throw new Error(`Bucket '${table}' not found`);
621
+ const entries = await model.entries();
622
+ return entries.map(([k, v]) => ({ key: k, value: v }));
623
+ }
624
+ function buildFileTree(cwd, relativePath, allowed) {
625
+ const tree = [];
626
+ path.resolve(cwd, relativePath);
627
+ for (const entry of allowed) {
628
+ const entryRelative = entry;
629
+ if (entryRelative.includes("/")) continue;
630
+ const absPath = path.resolve(cwd, entry);
631
+ if (!fs.existsSync(absPath)) continue;
632
+ const stat = fs.statSync(absPath);
633
+ if (stat.isDirectory()) {
634
+ tree.push({
635
+ name: entryRelative,
636
+ path: entry,
637
+ type: "directory",
638
+ children: buildDirectoryTree(cwd, entry, 3)
639
+ });
640
+ } else if (stat.isFile()) {
641
+ tree.push({ name: entryRelative, path: entry, type: "file" });
642
+ }
643
+ }
644
+ return tree.sort((a, b) => {
645
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
646
+ return a.name.localeCompare(b.name);
647
+ });
648
+ }
649
+ function buildDirectoryTree(cwd, relativePath, maxDepth) {
650
+ if (maxDepth <= 0) return [];
651
+ const absDir = path.resolve(cwd, relativePath);
652
+ if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) return [];
653
+ const entries = fs.readdirSync(absDir, { withFileTypes: true });
654
+ const result = [];
655
+ for (const entry of entries) {
656
+ if (FILE_MANAGER_BLOCKED.has(entry.name) || entry.name.startsWith(".")) continue;
657
+ const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
658
+ if (entry.isDirectory()) {
659
+ result.push({
660
+ name: entry.name,
661
+ path: childRelative,
662
+ type: "directory",
663
+ children: buildDirectoryTree(cwd, childRelative, maxDepth - 1)
664
+ });
665
+ } else if (entry.isFile()) {
666
+ result.push({ name: entry.name, path: childRelative, type: "file" });
667
+ }
668
+ }
669
+ return result.sort((a, b) => {
670
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
671
+ return a.name.localeCompare(b.name);
672
+ });
673
+ }
243
674
  function findPluginByConfigKey(rootPlugin, configKey) {
244
675
  for (const child of rootPlugin.children) {
245
676
  if (child.name === configKey || child.name.endsWith(`-${configKey}`) || child.name.includes(configKey)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/console",
3
- "version": "1.0.48",
3
+ "version": "1.0.49",
4
4
  "description": "Web console service for Zhin.js with real-time monitoring",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "peerDependencies": {
74
74
  "@types/ws": "^8.18.1",
75
- "@zhin.js/client": "^1.0.11",
75
+ "@zhin.js/client": "^1.0.12",
76
76
  "@zhin.js/core": "^1.0.50",
77
77
  "@zhin.js/http": "^1.0.44",
78
78
  "zhin.js": "1.0.50"