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
@@ -7,9 +7,11 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ var __create = Object.create;
10
11
  var __defProp = Object.defineProperty;
11
12
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
13
  var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
13
15
  var __hasOwnProp = Object.prototype.hasOwnProperty;
14
16
  var __export = (target, all) => {
15
17
  for (var name in all)
@@ -23,6 +25,14 @@ var __copyProps = (to, from, except, desc) => {
23
25
  }
24
26
  return to;
25
27
  };
28
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
29
+ // If the importer is in node compatibility mode or this is not an ESM
30
+ // file that has been converted to a CommonJS file using a Babel-
31
+ // compatible transform (i.e. "__esModule" has not been set), then set
32
+ // "default" to the CommonJS "module.exports" for node compatibility.
33
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
34
+ mod
35
+ ));
26
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
37
  var cache_monitor_exports = {};
28
38
  __export(cache_monitor_exports, {
@@ -30,6 +40,220 @@ __export(cache_monitor_exports, {
30
40
  });
31
41
  module.exports = __toCommonJS(cache_monitor_exports);
32
42
  var import_redis = require("../utils/redis");
43
+ var import_fs = require("fs");
44
+ var import_path = __toESM(require("path"));
45
+ var import_child_process = require("child_process");
46
+ var import_http = __toESM(require("http"));
47
+ var import_https = __toESM(require("https"));
48
+ async function exists(p) {
49
+ try {
50
+ await import_fs.promises.access(p);
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+ function isSafePath(dirPath) {
57
+ if (!dirPath) return false;
58
+ const resolved = import_path.default.resolve(dirPath);
59
+ const normalized = resolved.toLowerCase().replace(/\\/g, "/");
60
+ const restrictedPatterns = [
61
+ "^/$",
62
+ "^[a-z]:/?$",
63
+ "^/[^/]+$",
64
+ "^[a-z]:/[^/]+$",
65
+ "/windows",
66
+ "/system32",
67
+ "/program files",
68
+ "/etc",
69
+ "/var$",
70
+ "/usr$",
71
+ "/boot",
72
+ "/sys",
73
+ "/proc",
74
+ "/dev",
75
+ "/home$",
76
+ "/root$"
77
+ ];
78
+ for (const pat of restrictedPatterns) {
79
+ const re = new RegExp(pat);
80
+ if (re.test(normalized)) {
81
+ return false;
82
+ }
83
+ }
84
+ const parts = normalized.split("/").filter(Boolean);
85
+ const nonDriveParts = parts.filter((p) => !/^[a-z]:$/.test(p));
86
+ if (nonDriveParts.length < 2) {
87
+ return false;
88
+ }
89
+ return true;
90
+ }
91
+ async function findNginxConfig() {
92
+ const nginxConfPath = await new Promise((resolve) => {
93
+ (0, import_child_process.exec)("nginx -V", (err, stdout, stderr) => {
94
+ if (err) return resolve(null);
95
+ const output = stdout + stderr;
96
+ const match = output.match(/--conf-path=([^\s]+)/);
97
+ if (match && match[1]) {
98
+ resolve(match[1]);
99
+ } else {
100
+ resolve(null);
101
+ }
102
+ });
103
+ });
104
+ if (nginxConfPath && await exists(nginxConfPath)) {
105
+ return nginxConfPath;
106
+ }
107
+ const searchPaths = [
108
+ "/etc/nginx/nginx.conf",
109
+ "/usr/local/nginx/conf/nginx.conf",
110
+ "/usr/local/etc/nginx/nginx.conf",
111
+ "/opt/homebrew/etc/nginx/nginx.conf",
112
+ "C:\\nginx\\conf\\nginx.conf"
113
+ ];
114
+ for (const p of searchPaths) {
115
+ if (await exists(p)) {
116
+ return p;
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ async function readConfigRecursive(configPath, visited = /* @__PURE__ */ new Set()) {
122
+ const resolvedPath = import_path.default.resolve(configPath);
123
+ if (visited.has(resolvedPath)) return "";
124
+ visited.add(resolvedPath);
125
+ try {
126
+ const content = await import_fs.promises.readFile(resolvedPath, "utf8");
127
+ const lines = content.split(/\r?\n/);
128
+ let fullText = content + "\n";
129
+ const configDir = import_path.default.dirname(resolvedPath);
130
+ for (const line of lines) {
131
+ const trimmed = line.trim();
132
+ if (trimmed.startsWith("#")) continue;
133
+ const includeMatch = trimmed.match(/^include\s+([^\s;]+)/);
134
+ if (includeMatch && includeMatch[1]) {
135
+ let includePattern = includeMatch[1].trim().replace(/^["']|["']$/g, "");
136
+ if (!import_path.default.isAbsolute(includePattern)) {
137
+ includePattern = import_path.default.join(configDir, includePattern);
138
+ }
139
+ if (includePattern.includes("*")) {
140
+ try {
141
+ const patternDir = import_path.default.dirname(includePattern);
142
+ const ext = import_path.default.extname(includePattern);
143
+ if (await exists(patternDir)) {
144
+ const files = await import_fs.promises.readdir(patternDir);
145
+ for (const file of files) {
146
+ if (file.endsWith(ext) || includePattern.endsWith("*")) {
147
+ const filePath = import_path.default.join(patternDir, file);
148
+ fullText += await readConfigRecursive(filePath, visited) + "\n";
149
+ }
150
+ }
151
+ }
152
+ } catch {
153
+ }
154
+ } else {
155
+ if (await exists(includePattern)) {
156
+ fullText += await readConfigRecursive(includePattern, visited) + "\n";
157
+ }
158
+ }
159
+ }
160
+ }
161
+ return fullText;
162
+ } catch {
163
+ return "";
164
+ }
165
+ }
166
+ function extractCachePaths(configText) {
167
+ const paths = [];
168
+ const lines = configText.split(/\r?\n/);
169
+ for (const line of lines) {
170
+ const trimmed = line.trim();
171
+ if (trimmed.startsWith("#")) continue;
172
+ const match = trimmed.match(/^(?:proxy|fastcgi|scgi|uwsgi)_cache_path\s+([^\s;]+)/);
173
+ if (match && match[1]) {
174
+ const p = match[1].trim();
175
+ const cleanPath = p.replace(/^["']|["']$/g, "");
176
+ if (cleanPath && !paths.includes(cleanPath)) {
177
+ paths.push(cleanPath);
178
+ }
179
+ }
180
+ }
181
+ return paths;
182
+ }
183
+ async function emptyDirectory(dirPath) {
184
+ if (!isSafePath(dirPath)) {
185
+ return { success: false, clearedCount: 0, error: "Path is classified as unsafe or restricted" };
186
+ }
187
+ try {
188
+ if (!await exists(dirPath)) {
189
+ return { success: false, clearedCount: 0, error: "Directory does not exist" };
190
+ }
191
+ const stats = await import_fs.promises.stat(dirPath);
192
+ if (!stats.isDirectory()) {
193
+ return { success: false, clearedCount: 0, error: "Path is not a directory" };
194
+ }
195
+ const files = await import_fs.promises.readdir(dirPath);
196
+ let clearedCount = 0;
197
+ for (const file of files) {
198
+ const fullPath = import_path.default.join(dirPath, file);
199
+ await import_fs.promises.rm(fullPath, { recursive: true, force: true });
200
+ clearedCount++;
201
+ }
202
+ return { success: true, clearedCount };
203
+ } catch (err) {
204
+ return { success: false, clearedCount: 0, error: err.message };
205
+ }
206
+ }
207
+ function makePurgeRequest(urlStr, method = "PURGE", headers = {}) {
208
+ return new Promise((resolve) => {
209
+ try {
210
+ const url = new URL(urlStr);
211
+ const isHttps = url.protocol === "https:";
212
+ const client = isHttps ? import_https.default : import_http.default;
213
+ const reqHeaders = { ...headers };
214
+ const req = client.request(
215
+ urlStr,
216
+ {
217
+ method,
218
+ headers: reqHeaders,
219
+ timeout: 1e4
220
+ },
221
+ (res) => {
222
+ let body = "";
223
+ res.on("data", (chunk) => {
224
+ body += chunk;
225
+ });
226
+ res.on("end", () => {
227
+ resolve({
228
+ success: (res.statusCode || 0) >= 200 && (res.statusCode || 0) < 300,
229
+ status: res.statusCode,
230
+ data: body
231
+ });
232
+ });
233
+ }
234
+ );
235
+ req.on("error", (err) => {
236
+ resolve({
237
+ success: false,
238
+ error: err.message
239
+ });
240
+ });
241
+ req.on("timeout", () => {
242
+ req.destroy();
243
+ resolve({
244
+ success: false,
245
+ error: "Request timed out after 10 seconds"
246
+ });
247
+ });
248
+ req.end();
249
+ } catch (err) {
250
+ resolve({
251
+ success: false,
252
+ error: err.message
253
+ });
254
+ }
255
+ });
256
+ }
33
257
  const cacheMonitorActions = {
34
258
  /**
35
259
  * GET /clusterManagerCacheMgr:stores
@@ -125,6 +349,83 @@ const cacheMonitorActions = {
125
349
  await cm.flushAll();
126
350
  ctx.body = { success: true };
127
351
  await next();
352
+ },
353
+ /**
354
+ * GET /clusterManagerCacheMgr:nginxCacheStatus
355
+ * Detect if Nginx is installed, locate conf, and auto-load cache paths
356
+ */
357
+ async nginxCacheStatus(ctx, next) {
358
+ let nginxInstalled = false;
359
+ let mainConfigPath = null;
360
+ let detectedPaths = [];
361
+ try {
362
+ const configText = await new Promise((resolve) => {
363
+ (0, import_child_process.exec)("nginx -T", { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
364
+ if (err) {
365
+ resolve(null);
366
+ } else {
367
+ nginxInstalled = true;
368
+ resolve(stdout);
369
+ }
370
+ });
371
+ });
372
+ if (configText) {
373
+ detectedPaths = extractCachePaths(configText);
374
+ mainConfigPath = await findNginxConfig();
375
+ } else {
376
+ mainConfigPath = await findNginxConfig();
377
+ if (mainConfigPath) {
378
+ nginxInstalled = true;
379
+ const fullConfigText = await readConfigRecursive(mainConfigPath);
380
+ detectedPaths = extractCachePaths(fullConfigText);
381
+ }
382
+ }
383
+ } catch (err) {
384
+ }
385
+ ctx.body = {
386
+ nginxInstalled,
387
+ mainConfigPath,
388
+ detectedPaths
389
+ };
390
+ await next();
391
+ },
392
+ /**
393
+ * POST /clusterManagerCacheMgr:clearNginxCache
394
+ * Clear physical cache files or send an HTTP Purge request
395
+ */
396
+ async clearNginxCache(ctx, next) {
397
+ const { method = "directory", directory, url, httpMethod = "PURGE", headers = {} } = ctx.action.params.values || {};
398
+ if (method === "directory") {
399
+ if (!directory) {
400
+ ctx.throw(400, "Directory path is required for physical cache clearing");
401
+ }
402
+ const result = await emptyDirectory(directory);
403
+ if (!result.success) {
404
+ ctx.throw(400, result.error || "Failed to clear cache directory");
405
+ }
406
+ ctx.body = {
407
+ success: true,
408
+ message: `Successfully cleared physical cache directory`,
409
+ clearedCount: result.clearedCount
410
+ };
411
+ } else if (method === "purgeRequest") {
412
+ if (!url) {
413
+ ctx.throw(400, "Purge URL is required for HTTP Purge request method");
414
+ }
415
+ const result = await makePurgeRequest(url, httpMethod, headers);
416
+ if (!result.success) {
417
+ ctx.throw(400, result.error || `HTTP Purge request failed with status: ${result.status}`);
418
+ }
419
+ ctx.body = {
420
+ success: true,
421
+ message: `HTTP Purge request sent successfully`,
422
+ status: result.status,
423
+ data: result.data
424
+ };
425
+ } else {
426
+ ctx.throw(400, `Unknown clearing method: ${method}`);
427
+ }
428
+ await next();
128
429
  }
129
430
  };
130
431
  // Annotate the CommonJS export names for ESM import in node:
@@ -24,6 +24,16 @@ export declare const clusterActions: {
24
24
  * Returns all known cluster environments/nodes (if discovery adapter supports it)
25
25
  */
26
26
  list(ctx: Context, next: () => Promise<void>): Promise<void>;
27
+ /**
28
+ * GET /clusterManagerCluster:drift
29
+ * Reports version/runtime/package drift across active cluster nodes.
30
+ */
31
+ drift(ctx: Context, next: () => Promise<void>): Promise<void>;
32
+ /**
33
+ * GET /clusterManagerCluster:legacyDiagnostics
34
+ * Detects deprecated legacy multi-app plugins and leftover application records.
35
+ */
36
+ legacyDiagnostics(ctx: Context, next: () => Promise<void>): Promise<void>;
27
37
  /**
28
38
  * GET /clusterManagerCluster:health
29
39
  * Health check for all subsystems
@@ -34,6 +44,11 @@ export declare const clusterActions: {
34
44
  * Publishes a restart signal to target nodes orchestrating a soft NocoBase restart or a hard docker daemon rebirth
35
45
  */
36
46
  restart(ctx: Context, next: () => Promise<void>): Promise<void>;
47
+ /**
48
+ * POST /clusterManagerCluster:rollingRestart
49
+ * Restarts online nodes one-by-one, optionally filtered by role.
50
+ */
51
+ rollingRestart(ctx: Context, next: () => Promise<void>): Promise<void>;
37
52
  /**
38
53
  * GET /clusterManagerCluster:logs?targetNodeId=xxx&lines=200
39
54
  *