plugin-cluster-manager 1.1.10 → 1.1.11

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 (54) hide show
  1. package/client.js +1 -0
  2. package/dist/client/Doctor.d.ts +2 -0
  3. package/dist/client/NginxCacheManager.d.ts +2 -0
  4. package/dist/client/index.js +1 -1
  5. package/dist/client/utils/clientSafeCache.d.ts +3 -0
  6. package/dist/client/utils/requestDedupInterceptor.d.ts +2 -0
  7. package/dist/externalVersion.js +5 -5
  8. package/dist/locale/en-US.json +97 -1
  9. package/dist/locale/vi-VN.json +98 -1
  10. package/dist/locale/zh-CN.json +98 -1
  11. package/dist/server/actions/cache-monitor.d.ts +10 -0
  12. package/dist/server/actions/cache-monitor.js +301 -0
  13. package/dist/server/actions/cluster-nodes.d.ts +15 -0
  14. package/dist/server/actions/cluster-nodes.js +394 -10
  15. package/dist/server/actions/doctor.d.ts +82 -0
  16. package/dist/server/actions/doctor.js +1250 -0
  17. package/dist/server/collections/cluster-manager-doctor-runs.d.ts +3 -0
  18. package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
  19. package/dist/server/collections/cluster-manager-doctor.d.ts +18 -0
  20. package/dist/server/collections/cluster-manager-doctor.js +44 -0
  21. package/dist/server/hooks/cacheInvalidationHooks.d.ts +1 -0
  22. package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
  23. package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +2 -0
  24. package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
  25. package/dist/server/orchestrator/PackageManager.js +20 -16
  26. package/dist/server/plugin.js +61 -8
  27. package/dist/server/utils/versionManager.d.ts +10 -0
  28. package/dist/server/utils/versionManager.js +91 -0
  29. package/package.json +41 -41
  30. package/server.js +1 -0
  31. package/src/client/CacheMonitor.tsx +166 -179
  32. package/src/client/ClusterManagerLayout.tsx +48 -42
  33. package/src/client/ClusterNodes.tsx +691 -418
  34. package/src/client/Doctor.tsx +559 -0
  35. package/src/client/NginxCacheManager.tsx +415 -0
  36. package/src/client/PluginOperations.tsx +234 -234
  37. package/src/client/index.tsx +22 -14
  38. package/src/client/utils/clientSafeCache.ts +41 -0
  39. package/src/client/utils/requestDedupInterceptor.ts +213 -0
  40. package/src/locale/en-US.json +97 -1
  41. package/src/locale/vi-VN.json +98 -1
  42. package/src/locale/zh-CN.json +98 -1
  43. package/src/server/__tests__/doctor.test.ts +53 -0
  44. package/src/server/actions/acl-cache.ts +272 -272
  45. package/src/server/actions/cache-monitor.ts +453 -116
  46. package/src/server/actions/cluster-nodes.ts +882 -378
  47. package/src/server/actions/doctor.ts +1540 -0
  48. package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
  49. package/src/server/collections/cluster-manager-doctor.ts +19 -0
  50. package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
  51. package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
  52. package/src/server/orchestrator/PackageManager.ts +19 -15
  53. package/src/server/plugin.ts +338 -263
  54. package/src/server/utils/versionManager.ts +69 -0
@@ -40,7 +40,6 @@ __export(cluster_nodes_exports, {
40
40
  readLocalLogs: () => readLocalLogs
41
41
  });
42
42
  module.exports = __toCommonJS(cluster_nodes_exports);
43
- var import_server = require("@nocobase/server");
44
43
  var import_os = __toESM(require("os"));
45
44
  var import_fs = require("fs");
46
45
  var import_path = __toESM(require("path"));
@@ -48,11 +47,162 @@ var import_crypto = __toESM(require("crypto"));
48
47
  var import_redis_node_registry = require("../adapters/redis-node-registry");
49
48
  var import_redis = require("../utils/redis");
50
49
  var import_node = require("../utils/node");
50
+ var import_packages = require("../../shared/packages");
51
51
  const LOG_RESPONSE_KEY_PREFIX = "cluster-manager:log-response:";
52
- const LOG_RESPONSE_TTL = 30;
52
+ const LEGACY_MULTI_APP_PLUGINS = ["multi-app-manager", "multi-app-share-collection"];
53
53
  function sleep(ms) {
54
54
  return new Promise((resolve) => setTimeout(resolve, ms));
55
55
  }
56
+ function normalizeList(value) {
57
+ if (!Array.isArray(value)) return [];
58
+ return Array.from(
59
+ new Set(
60
+ value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
61
+ )
62
+ );
63
+ }
64
+ function normalizePackageMap(packages) {
65
+ return {
66
+ apt: normalizeList(packages == null ? void 0 : packages.apt),
67
+ npm: normalizeList(packages == null ? void 0 : packages.npm),
68
+ python: normalizeList(packages == null ? void 0 : packages.python)
69
+ };
70
+ }
71
+ function parseCustomPackages(value) {
72
+ if (!value) {
73
+ return { python: [], node: [], npm: [] };
74
+ }
75
+ let customValue = value;
76
+ if (typeof customValue === "string") {
77
+ try {
78
+ customValue = JSON.parse(customValue);
79
+ } catch {
80
+ return { python: [], node: [], npm: [] };
81
+ }
82
+ }
83
+ if (!customValue || typeof customValue !== "object" || Array.isArray(customValue)) {
84
+ return { python: [], node: [], npm: [] };
85
+ }
86
+ const custom = customValue;
87
+ return {
88
+ python: normalizeList(custom.python),
89
+ node: normalizeList(custom.node),
90
+ npm: normalizeList(custom.npm)
91
+ };
92
+ }
93
+ function parsePackageWhitelist(status) {
94
+ if (!(status == null ? void 0 : status.packageWhitelist)) {
95
+ return { apt: [], npm: [], python: [] };
96
+ }
97
+ let whitelistValue = status.packageWhitelist;
98
+ if (typeof whitelistValue === "string") {
99
+ try {
100
+ whitelistValue = JSON.parse(whitelistValue);
101
+ } catch {
102
+ return { apt: [], npm: [], python: [] };
103
+ }
104
+ }
105
+ if (!whitelistValue || typeof whitelistValue !== "object" || Array.isArray(whitelistValue)) {
106
+ return { apt: [], npm: [], python: [] };
107
+ }
108
+ const whitelist = whitelistValue;
109
+ const npmPackages = [
110
+ ...Array.isArray(whitelist.npm) ? whitelist.npm : [],
111
+ ...Array.isArray(whitelist.node) ? whitelist.node : []
112
+ ];
113
+ return {
114
+ apt: normalizeList(whitelist.apt),
115
+ npm: normalizeList(npmPackages),
116
+ python: normalizeList(whitelist.python)
117
+ };
118
+ }
119
+ function diffPackages(expected, installed) {
120
+ return {
121
+ apt: expected.apt.filter((pkg) => !installed.apt.includes(pkg)),
122
+ npm: expected.npm.filter((pkg) => !installed.npm.includes(pkg)),
123
+ python: expected.python.filter((pkg) => !installed.python.includes(pkg))
124
+ };
125
+ }
126
+ function hasMissingPackages(packages) {
127
+ return packages.apt.length > 0 || packages.npm.length > 0 || packages.python.length > 0;
128
+ }
129
+ function getErrorMessage(error) {
130
+ return error instanceof Error ? error.message : String(error);
131
+ }
132
+ function getNodeRole(node) {
133
+ if (node.isSandbox) {
134
+ return "sandbox";
135
+ }
136
+ const workerMode = node.workerMode || "main";
137
+ return workerMode === "worker" || workerMode === "task" || workerMode === "*" ? "worker" : "app";
138
+ }
139
+ function getReferenceVersion(nodes) {
140
+ var _a;
141
+ const appNode = nodes.find((node) => getNodeRole(node) === "app" && node.appVersion);
142
+ if (appNode == null ? void 0 : appNode.appVersion) {
143
+ return appNode.appVersion;
144
+ }
145
+ const counts = /* @__PURE__ */ new Map();
146
+ for (const node of nodes) {
147
+ if (!node.appVersion) continue;
148
+ counts.set(node.appVersion, (counts.get(node.appVersion) || 0) + 1);
149
+ }
150
+ return ((_a = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]) == null ? void 0 : _a[0]) || null;
151
+ }
152
+ async function getClusterNodes(ctx) {
153
+ var _a, _b;
154
+ const plugin = (_b = (_a = ctx.app.pm) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "plugin-cluster-manager");
155
+ const registry = (plugin == null ? void 0 : plugin.nodeRegistry) ?? new import_redis_node_registry.RedisNodeRegistry(ctx.app);
156
+ return registry.getNodes();
157
+ }
158
+ async function getExpectedPackages(ctx) {
159
+ var _a;
160
+ const repo = ctx.db.getRepository("workerPackagesConfigs");
161
+ const config = await ((_a = repo == null ? void 0 : repo.findOne) == null ? void 0 : _a.call(repo));
162
+ if (!config) {
163
+ return normalizePackageMap((0, import_packages.packagesFromConfig)({}));
164
+ }
165
+ const configured = (0, import_packages.packagesFromConfig)({
166
+ aptPackages: config.get("aptPackages"),
167
+ pythonPackages: config.get("pythonPackages"),
168
+ npmPackages: config.get("npmPackages")
169
+ });
170
+ const custom = parseCustomPackages(config.get("customPackages"));
171
+ return normalizePackageMap({
172
+ apt: configured.apt,
173
+ npm: [...configured.npm || [], ...custom.node || [], ...custom.npm || []],
174
+ python: [...configured.python || [], ...custom.python || []]
175
+ });
176
+ }
177
+ async function readPackageStatus(ctx, node) {
178
+ const redis = (0, import_redis.getRedis)(ctx);
179
+ if (!redis) return null;
180
+ const keys = [
181
+ node.id ? `cluster-manager:pkg-status:${node.id}` : null,
182
+ node.hostname ? `orchestrator:pkg-status:${node.hostname}` : null,
183
+ node.name ? `orchestrator:pkg-status:${node.name}` : null
184
+ ].filter(Boolean);
185
+ for (const key of keys) {
186
+ try {
187
+ const raw = await redis.sendCommand(["GET", key]);
188
+ if (raw && typeof raw === "string") {
189
+ return JSON.parse(raw);
190
+ }
191
+ } catch {
192
+ }
193
+ }
194
+ return null;
195
+ }
196
+ async function getApplicationPluginRows(ctx) {
197
+ const repo = ctx.db.getRepository("applicationPlugins");
198
+ if (!repo) return [];
199
+ const rows = await repo.find({ sort: ["name"] });
200
+ return rows.map((row) => row.toJSON());
201
+ }
202
+ function getPayload(ctx) {
203
+ var _a, _b, _c;
204
+ return ctx.action.params.values || ((_b = (_a = ctx.request) == null ? void 0 : _a.body) == null ? void 0 : _b.values) || ((_c = ctx.request) == null ? void 0 : _c.body) || {};
205
+ }
56
206
  async function readLocalLogs(app, maxLines) {
57
207
  const logBasePath = process.env.LOGGER_BASE_PATH || import_path.default.resolve(process.cwd(), "storage", "logs");
58
208
  const appName = process.env.APP_NAME || app.name || "main";
@@ -129,9 +279,7 @@ const clusterActions = {
129
279
  const plugin = (_b = (_a = ctx.app.pm) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "plugin-cluster-manager");
130
280
  const registry = (plugin == null ? void 0 : plugin.nodeRegistry) ?? new import_redis_node_registry.RedisNodeRegistry(ctx.app);
131
281
  const nodes = await registry.getNodes();
132
- const appNode = nodes.find(
133
- (n) => n.workerMode === "main" || n.workerMode === "" || n.workerMode === "app"
134
- );
282
+ const appNode = nodes.find((n) => n.workerMode === "main" || n.workerMode === "" || n.workerMode === "app");
135
283
  if (appNode == null ? void 0 : appNode.nodeDetails) {
136
284
  ctx.body = appNode.nodeDetails;
137
285
  } else {
@@ -173,12 +321,8 @@ const clusterActions = {
173
321
  * Returns all known cluster environments/nodes (if discovery adapter supports it)
174
322
  */
175
323
  async list(ctx, next) {
176
- var _a, _b;
177
- const supervisor = import_server.AppSupervisor.getInstance();
178
324
  const environments = [];
179
- const plugin = (_b = (_a = ctx.app.pm) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "plugin-cluster-manager");
180
- const registry = (plugin == null ? void 0 : plugin.nodeRegistry) ?? new import_redis_node_registry.RedisNodeRegistry(ctx.app);
181
- const nodes = await registry.getNodes();
325
+ const nodes = await getClusterNodes(ctx);
182
326
  if (nodes && nodes.length > 0) {
183
327
  for (const env of nodes) {
184
328
  environments.push({
@@ -210,6 +354,168 @@ const clusterActions = {
210
354
  ctx.body = { data: environments, meta: { count: environments.length } };
211
355
  await next();
212
356
  },
357
+ /**
358
+ * GET /clusterManagerCluster:drift
359
+ * Reports version/runtime/package drift across active cluster nodes.
360
+ */
361
+ async drift(ctx, next) {
362
+ var _a, _b;
363
+ const nodes = await getClusterNodes(ctx);
364
+ const referenceVersion = getReferenceVersion(nodes);
365
+ const expectedPackages = await getExpectedPackages(ctx);
366
+ const versionDrifts = nodes.filter((node) => node.status !== "offline").filter((node) => referenceVersion && node.appVersion && node.appVersion !== referenceVersion).map((node) => ({
367
+ id: node.id,
368
+ name: node.name,
369
+ hostname: node.hostname,
370
+ role: getNodeRole(node),
371
+ expectedVersion: referenceVersion,
372
+ actualVersion: node.appVersion
373
+ }));
374
+ const runtimeReference = (_b = (_a = nodes.find((node) => getNodeRole(node) === "app")) == null ? void 0 : _a.nodeDetails) == null ? void 0 : _b.node;
375
+ const runtimeDrifts = runtimeReference ? nodes.filter((node) => node.status !== "offline").filter((node) => {
376
+ var _a2;
377
+ const runtime = (_a2 = node.nodeDetails) == null ? void 0 : _a2.node;
378
+ if (!runtime) return false;
379
+ return runtime.nodeVersion !== runtimeReference.nodeVersion || runtime.platform !== runtimeReference.platform || runtime.arch !== runtimeReference.arch;
380
+ }).map((node) => {
381
+ var _a2, _b2, _c, _d, _e, _f;
382
+ return {
383
+ id: node.id,
384
+ name: node.name,
385
+ hostname: node.hostname,
386
+ role: getNodeRole(node),
387
+ expected: {
388
+ nodeVersion: runtimeReference.nodeVersion,
389
+ platform: runtimeReference.platform,
390
+ arch: runtimeReference.arch
391
+ },
392
+ actual: {
393
+ nodeVersion: (_b2 = (_a2 = node.nodeDetails) == null ? void 0 : _a2.node) == null ? void 0 : _b2.nodeVersion,
394
+ platform: (_d = (_c = node.nodeDetails) == null ? void 0 : _c.node) == null ? void 0 : _d.platform,
395
+ arch: (_f = (_e = node.nodeDetails) == null ? void 0 : _e.node) == null ? void 0 : _f.arch
396
+ }
397
+ };
398
+ }) : [];
399
+ const packageDrifts = [];
400
+ for (const node of nodes.filter((item) => item.status !== "offline" && getNodeRole(item) !== "app")) {
401
+ const status = await readPackageStatus(ctx, node);
402
+ const installedPackages = parsePackageWhitelist(status);
403
+ const missingPackages = diffPackages(expectedPackages, installedPackages);
404
+ const hasPackageStatus = Boolean(status);
405
+ const statusOk = (status == null ? void 0 : status.initStatus) === "succeeded";
406
+ if (!hasPackageStatus || !statusOk || hasMissingPackages(missingPackages)) {
407
+ packageDrifts.push({
408
+ id: node.id,
409
+ name: node.name,
410
+ hostname: node.hostname,
411
+ role: getNodeRole(node),
412
+ status: (status == null ? void 0 : status.initStatus) || "unknown",
413
+ lastInitAt: (status == null ? void 0 : status.lastInitAt) || null,
414
+ missingPackages,
415
+ installedPackages,
416
+ initProgressLog: (status == null ? void 0 : status.initProgressLog) || ""
417
+ });
418
+ }
419
+ }
420
+ ctx.body = {
421
+ healthy: versionDrifts.length === 0 && runtimeDrifts.length === 0 && packageDrifts.length === 0,
422
+ referenceVersion,
423
+ expectedPackages,
424
+ versionDrifts,
425
+ runtimeDrifts,
426
+ packageDrifts,
427
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
428
+ summary: {
429
+ nodes: nodes.length,
430
+ versionDrifts: versionDrifts.length,
431
+ runtimeDrifts: runtimeDrifts.length,
432
+ packageDrifts: packageDrifts.length
433
+ }
434
+ };
435
+ await next();
436
+ },
437
+ /**
438
+ * GET /clusterManagerCluster:legacyDiagnostics
439
+ * Detects deprecated legacy multi-app plugins and leftover application records.
440
+ */
441
+ async legacyDiagnostics(ctx, next) {
442
+ var _a, _b;
443
+ const rows = await getApplicationPluginRows(ctx);
444
+ const plugins = LEGACY_MULTI_APP_PLUGINS.map((name) => {
445
+ var _a2, _b2, _c, _d;
446
+ const row = rows.find((item) => item.name === name || item.packageName === `@nocobase/plugin-${name}`);
447
+ const loaded = Boolean(
448
+ ((_b2 = (_a2 = ctx.app.pm) == null ? void 0 : _a2.get) == null ? void 0 : _b2.call(_a2, name)) || ((_d = (_c = ctx.app.pm) == null ? void 0 : _c.get) == null ? void 0 : _d.call(_c, `@nocobase/plugin-${name}`))
449
+ );
450
+ return {
451
+ name,
452
+ packageName: `@nocobase/plugin-${name}`,
453
+ installed: Boolean(row),
454
+ enabled: Boolean(row == null ? void 0 : row.enabled),
455
+ loaded,
456
+ version: row == null ? void 0 : row.version
457
+ };
458
+ });
459
+ let legacyApplicationCount = 0;
460
+ if ((_b = (_a = ctx.db).hasCollection) == null ? void 0 : _b.call(_a, "applications")) {
461
+ try {
462
+ legacyApplicationCount = await ctx.db.getRepository("applications").count();
463
+ } catch {
464
+ legacyApplicationCount = 0;
465
+ }
466
+ }
467
+ const findings = [];
468
+ const manager = plugins.find((plugin) => plugin.name === "multi-app-manager");
469
+ const shareCollection = plugins.find((plugin) => plugin.name === "multi-app-share-collection");
470
+ const appSupervisor = rows.find(
471
+ (item) => item.name === "app-supervisor" || item.packageName === "@nocobase/plugin-app-supervisor"
472
+ );
473
+ if ((manager == null ? void 0 : manager.enabled) || (manager == null ? void 0 : manager.loaded)) {
474
+ findings.push({
475
+ level: "warning",
476
+ code: "legacy_multi_app_manager_active",
477
+ messageKey: "Deprecated multi-app manager is active. It runs apps in shared process memory and should not be used for production cluster isolation.",
478
+ message: "Deprecated multi-app manager is active. It runs apps in shared process memory and should not be used for production cluster isolation."
479
+ });
480
+ }
481
+ if ((shareCollection == null ? void 0 : shareCollection.enabled) || (shareCollection == null ? void 0 : shareCollection.loaded)) {
482
+ findings.push({
483
+ level: "warning",
484
+ code: "legacy_share_collection_active",
485
+ messageKey: "Deprecated multi-app share collection is active. Avoid schema/table sharing for new cluster deployments.",
486
+ message: "Deprecated multi-app share collection is active. Avoid schema/table sharing for new cluster deployments."
487
+ });
488
+ }
489
+ if (legacyApplicationCount > 0) {
490
+ findings.push({
491
+ level: "warning",
492
+ code: "legacy_app_records_found",
493
+ messageKey: "{count} legacy application record(s) were found in the applications collection.",
494
+ messageArgs: { count: legacyApplicationCount },
495
+ message: `${legacyApplicationCount} legacy application record(s) were found in the applications collection.`
496
+ });
497
+ }
498
+ if (!(appSupervisor == null ? void 0 : appSupervisor.enabled)) {
499
+ findings.push({
500
+ level: "info",
501
+ code: "app_supervisor_not_enabled",
502
+ messageKey: "App Supervisor is not enabled. Use it for new multi-application management instead of deprecated multi-app plugins.",
503
+ message: "App Supervisor is not enabled. Use it for new multi-application management instead of deprecated multi-app plugins."
504
+ });
505
+ }
506
+ ctx.body = {
507
+ healthy: findings.every((finding) => finding.level !== "warning"),
508
+ plugins,
509
+ appSupervisor: appSupervisor ? {
510
+ installed: true,
511
+ enabled: Boolean(appSupervisor.enabled),
512
+ version: appSupervisor.version
513
+ } : { installed: false, enabled: false },
514
+ legacyApplicationCount,
515
+ findings
516
+ };
517
+ await next();
518
+ },
213
519
  /**
214
520
  * GET /clusterManagerCluster:health
215
521
  * Health check for all subsystems
@@ -290,6 +596,84 @@ const clusterActions = {
290
596
  }
291
597
  await next();
292
598
  },
599
+ /**
600
+ * POST /clusterManagerCluster:rollingRestart
601
+ * Restarts online nodes one-by-one, optionally filtered by role.
602
+ */
603
+ async rollingRestart(ctx, next) {
604
+ const payload = getPayload(ctx);
605
+ const mode = payload.mode === "soft" ? "soft" : "hard";
606
+ const role = payload.role || "worker";
607
+ const delayMs = Math.min(Math.max(Number(payload.delayMs) || 5e3, 1e3), 6e4);
608
+ const requestedNodeIds = Array.isArray(payload.nodeIds) ? payload.nodeIds.map(String) : [];
609
+ const pubSub = ctx.app.pubSubManager;
610
+ if (!pubSub) {
611
+ ctx.throw(500, "PubSub manager is not initialized. HA requires PUBSUB_ADAPTER_REDIS_URL to be set.");
612
+ }
613
+ const nodes = (await getClusterNodes(ctx)).filter((node) => {
614
+ if (node.status === "offline") return false;
615
+ if (requestedNodeIds.length > 0) return node.id && requestedNodeIds.includes(node.id);
616
+ if (role === "all") return true;
617
+ return getNodeRole(node) === role;
618
+ });
619
+ if (nodes.length === 0) {
620
+ ctx.throw(404, "No online nodes match the rolling restart target.");
621
+ }
622
+ const myNodeId = (0, import_node.getLocalNodeId)(ctx.app);
623
+ const sortedNodes = nodes.sort((a, b) => {
624
+ if (a.id === myNodeId) return 1;
625
+ if (b.id === myNodeId) return -1;
626
+ return String(a.name || a.id).localeCompare(String(b.name || b.id));
627
+ });
628
+ const restartId = import_crypto.default.randomBytes(8).toString("hex");
629
+ const startedAt = Date.now();
630
+ const logger = ctx.app.logger;
631
+ const published = sortedNodes.map((node, index) => ({
632
+ id: node.id,
633
+ name: node.name,
634
+ hostname: node.hostname,
635
+ role: getNodeRole(node),
636
+ mode,
637
+ order: index + 1,
638
+ scheduledDelayMs: index * delayMs,
639
+ scheduledAt: new Date(startedAt + index * delayMs).toISOString()
640
+ }));
641
+ sortedNodes.forEach((node, index) => {
642
+ setTimeout(() => {
643
+ try {
644
+ const publishResult = pubSub.publish(
645
+ "cluster-manager:restart",
646
+ JSON.stringify({
647
+ restartId,
648
+ targetNodeId: node.id,
649
+ hostname: node.hostname,
650
+ mode
651
+ })
652
+ );
653
+ Promise.resolve(publishResult).catch((error) => {
654
+ logger.error(
655
+ `[ClusterManager] Failed to publish rolling restart ${restartId} for ${node.id || node.hostname}: ${getErrorMessage(error)}`
656
+ );
657
+ });
658
+ } catch (error) {
659
+ logger.error(
660
+ `[ClusterManager] Failed to schedule rolling restart ${restartId} for ${node.id || node.hostname}: ${getErrorMessage(error)}`
661
+ );
662
+ }
663
+ }, index * delayMs);
664
+ });
665
+ ctx.body = {
666
+ success: true,
667
+ restartId,
668
+ mode,
669
+ role,
670
+ delayMs,
671
+ scheduled: true,
672
+ estimatedDurationMs: Math.max(0, (sortedNodes.length - 1) * delayMs),
673
+ published
674
+ };
675
+ await next();
676
+ },
293
677
  /**
294
678
  * GET /clusterManagerCluster:logs?targetNodeId=xxx&lines=200
295
679
  *
@@ -0,0 +1,82 @@
1
+ /// <reference types="node" />
2
+ import { Context } from '@nocobase/actions';
3
+ import Application from '@nocobase/server';
4
+ interface DiagnosticLogLine {
5
+ source: string;
6
+ line: string;
7
+ timestamp?: string;
8
+ level?: string;
9
+ message?: string;
10
+ stack?: string;
11
+ }
12
+ interface LogSignature {
13
+ signature: string;
14
+ level: string;
15
+ count: number;
16
+ firstSeen?: string;
17
+ lastSeen?: string;
18
+ sources: string[];
19
+ samples: string[];
20
+ }
21
+ interface LogAnalysis {
22
+ totalLines: number;
23
+ levels: Record<string, number>;
24
+ signatures: LogSignature[];
25
+ }
26
+ interface PluginSnapshot {
27
+ name?: string;
28
+ packageName?: string;
29
+ enabled?: boolean;
30
+ dbVersion?: string;
31
+ loaded: boolean;
32
+ runtimeVersion?: string;
33
+ }
34
+ interface DoctorNodeSnapshot {
35
+ nodeId: string;
36
+ node: {
37
+ hostname: string;
38
+ pid: number;
39
+ workerMode: string;
40
+ role: string;
41
+ appVersion: string;
42
+ nodeVersion: string;
43
+ platform: string;
44
+ arch: string;
45
+ uptime: number;
46
+ isSandbox: boolean;
47
+ };
48
+ memory: NodeJS.MemoryUsage;
49
+ os: {
50
+ totalMemory: number;
51
+ freeMemory: number;
52
+ cpuCount: number;
53
+ loadAvg: number[];
54
+ };
55
+ env: Record<string, string | undefined>;
56
+ plugins: PluginSnapshot[];
57
+ logs: {
58
+ files: Array<{
59
+ file: string;
60
+ lineCount: number;
61
+ }>;
62
+ lines: DiagnosticLogLine[];
63
+ analysis: LogAnalysis;
64
+ };
65
+ collectedAt: string;
66
+ error?: string;
67
+ }
68
+ interface DoctorSnapshotOptions {
69
+ runId?: string;
70
+ sinceMs?: number;
71
+ untilMs?: number;
72
+ maxLines?: number;
73
+ }
74
+ export declare function collectLocalDoctorSnapshot(app: Application, options?: DoctorSnapshotOptions): Promise<DoctorNodeSnapshot>;
75
+ export declare const doctorActions: {
76
+ start(ctx: Context, next: () => Promise<void>): Promise<void>;
77
+ stop(ctx: Context, next: () => Promise<void>): Promise<void>;
78
+ status(ctx: Context, next: () => Promise<void>): Promise<void>;
79
+ report(ctx: Context, next: () => Promise<void>): Promise<void>;
80
+ download(ctx: Context, next: () => Promise<void>): Promise<void>;
81
+ };
82
+ export {};