plugin-cluster-manager 1.1.11 → 1.1.13

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 (106) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/client-v2/914.5dc1105cf3ada6a6.js +10 -0
  5. package/dist/client-v2/index.js +10 -0
  6. package/dist/externalVersion.js +6 -5
  7. package/dist/locale/en-US.json +138 -124
  8. package/dist/locale/vi-VN.json +139 -125
  9. package/dist/locale/zh-CN.json +140 -125
  10. package/dist/server/actions/cluster-nodes.js +2 -6
  11. package/dist/server/actions/doctor.js +1 -5
  12. package/dist/server/actions/orchestrator.js +37 -0
  13. package/dist/server/actions/queue-mappings.js +107 -0
  14. package/dist/server/collections/worker-queue-mappings.js +106 -0
  15. package/dist/server/orchestrator/PackageManager.js +1 -8
  16. package/dist/server/orchestrator/docker-adapter.js +49 -27
  17. package/dist/server/plugin.js +10 -8
  18. package/dist/server/queue-scanner.js +141 -0
  19. package/dist/server/utils/node.js +30 -2
  20. package/package.json +46 -42
  21. package/src/client/AclCacheManager.tsx +292 -287
  22. package/src/client/ClusterManagerLayout.tsx +6 -0
  23. package/src/client/ClusterNodes.tsx +8 -1
  24. package/src/client/ContainerOrchestrator.tsx +184 -102
  25. package/src/client/QueueAssignment.tsx +355 -0
  26. package/src/client/TaskManager.tsx +194 -187
  27. package/src/client/WorkflowExecutions.tsx +243 -238
  28. package/src/client-v2/plugin.tsx +24 -0
  29. package/src/locale/en-US.json +138 -124
  30. package/src/locale/vi-VN.json +139 -125
  31. package/src/locale/zh-CN.json +140 -125
  32. package/src/server/actions/cluster-nodes.ts +3 -7
  33. package/src/server/actions/doctor.ts +2 -6
  34. package/src/server/actions/orchestrator.ts +54 -2
  35. package/src/server/actions/queue-mappings.ts +94 -0
  36. package/src/server/collections/worker-queue-mappings.ts +85 -0
  37. package/src/server/orchestrator/PackageManager.ts +2 -10
  38. package/src/server/orchestrator/docker-adapter.ts +74 -37
  39. package/src/server/plugin.ts +12 -10
  40. package/src/server/queue-scanner.ts +154 -0
  41. package/src/server/utils/node.ts +48 -0
  42. package/dist/client/AclCacheManager.d.ts +0 -2
  43. package/dist/client/CacheMonitor.d.ts +0 -2
  44. package/dist/client/ClusterManagerLayout.d.ts +0 -2
  45. package/dist/client/ClusterNodes.d.ts +0 -2
  46. package/dist/client/ContainerOrchestrator.d.ts +0 -2
  47. package/dist/client/Doctor.d.ts +0 -2
  48. package/dist/client/EventQueueMonitor.d.ts +0 -2
  49. package/dist/client/LockMonitor.d.ts +0 -2
  50. package/dist/client/NginxCacheManager.d.ts +0 -2
  51. package/dist/client/PackageInstaller.d.ts +0 -2
  52. package/dist/client/PluginOperations.d.ts +0 -2
  53. package/dist/client/RedisMonitor.d.ts +0 -2
  54. package/dist/client/TaskManager.d.ts +0 -2
  55. package/dist/client/WorkflowExecutions.d.ts +0 -2
  56. package/dist/client/index.d.ts +0 -5
  57. package/dist/client/utils/clientSafeCache.d.ts +0 -3
  58. package/dist/client/utils/requestDedupInterceptor.d.ts +0 -2
  59. package/dist/client/utils.d.ts +0 -12
  60. package/dist/index.d.ts +0 -2
  61. package/dist/server/actions/acl-cache.d.ts +0 -53
  62. package/dist/server/actions/cache-monitor.d.ts +0 -33
  63. package/dist/server/actions/cluster-nodes.d.ts +0 -64
  64. package/dist/server/actions/doctor.d.ts +0 -82
  65. package/dist/server/actions/event-queue-monitor.d.ts +0 -13
  66. package/dist/server/actions/lock-monitor.d.ts +0 -19
  67. package/dist/server/actions/orchestrator.d.ts +0 -58
  68. package/dist/server/actions/package-manager.d.ts +0 -6
  69. package/dist/server/actions/plugin-operations.d.ts +0 -6
  70. package/dist/server/actions/redis-monitor.d.ts +0 -12
  71. package/dist/server/actions/tasks.d.ts +0 -7
  72. package/dist/server/actions/workflow-executions.d.ts +0 -7
  73. package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
  74. package/dist/server/adapters/redis-node-registry.d.ts +0 -12
  75. package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
  76. package/dist/server/collections/app.d.ts +0 -8
  77. package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
  78. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
  79. package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
  80. package/dist/server/collections/cluster-manager-doctor-runs.d.ts +0 -3
  81. package/dist/server/collections/cluster-manager-doctor.d.ts +0 -18
  82. package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
  83. package/dist/server/collections/cluster-manager-plugins.d.ts +0 -18
  84. package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
  85. package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
  86. package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
  87. package/dist/server/collections/cluster-manager.d.ts +0 -22
  88. package/dist/server/collections/orchestrator-settings.d.ts +0 -59
  89. package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
  90. package/dist/server/collections/worker-orchestrator.d.ts +0 -22
  91. package/dist/server/collections/worker-packages-configs.d.ts +0 -3
  92. package/dist/server/collections/worker-packages.d.ts +0 -22
  93. package/dist/server/hooks/cacheInvalidationHooks.d.ts +0 -1
  94. package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +0 -2
  95. package/dist/server/orchestrator/PackageManager.d.ts +0 -39
  96. package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
  97. package/dist/server/orchestrator/index.d.ts +0 -4
  98. package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
  99. package/dist/server/orchestrator/leader-election.d.ts +0 -48
  100. package/dist/server/orchestrator/types.d.ts +0 -84
  101. package/dist/server/plugin.d.ts +0 -26
  102. package/dist/server/utils/node.d.ts +0 -6
  103. package/dist/server/utils/redis.d.ts +0 -29
  104. package/dist/server/utils/versionManager.d.ts +0 -10
  105. package/dist/shared/packages.d.ts +0 -23
  106. /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
@@ -17,9 +17,7 @@ function getDockerode() {
17
17
  try {
18
18
  Dockerode = require('dockerode');
19
19
  } catch {
20
- throw new Error(
21
- '[DockerAdapter] "dockerode" package not found. Install it: yarn add dockerode',
22
- );
20
+ throw new Error('[DockerAdapter] "dockerode" package not found. Install it: yarn add dockerode');
23
21
  }
24
22
  }
25
23
  return Dockerode;
@@ -58,11 +56,7 @@ export class DockerAdapter implements IOrchestratorAdapter {
58
56
  const containers = await this.docker.listContainers({
59
57
  all: true,
60
58
  filters: {
61
- label: [
62
- `${LABEL_STACK}=${stack.name}`,
63
- `${LABEL_MANAGED}=true`,
64
- ...this.buildLabelFilters(this.workerLabels),
65
- ],
59
+ label: [`${LABEL_STACK}=${stack.name}`, `${LABEL_MANAGED}=true`, ...this.buildLabelFilters(this.workerLabels)],
66
60
  },
67
61
  });
68
62
 
@@ -105,9 +99,20 @@ export class DockerAdapter implements IOrchestratorAdapter {
105
99
  if (diff > 0) {
106
100
  // Scale UP
107
101
  let targetNetworks = stack.networks && stack.networks.length > 0 ? stack.networks : [];
108
- let targetNetworkMode = stack.networkMode;
102
+ const targetNetworkMode = stack.networkMode;
109
103
  let targetEnvVars = this.buildEnvArray(stack.envVars);
110
104
  let targetVolumes = stack.volumes || [];
105
+ // Default the worker image to whatever the app container is running, so
106
+ // workers stay version-locked with the app even when the stack record
107
+ // has a stale/empty image. An explicit stack.image still wins.
108
+ let targetImage = stack.image;
109
+ // Inherit the app container's startup command/entrypoint so workers boot
110
+ // identically (e.g. source-tarball extraction + `yarn start`). Without
111
+ // this, a worker created from the bare image runs the image default
112
+ // command, skips the app's bootstrap, never finishes booting, and never
113
+ // registers a heartbeat — so it never appears in Cluster Nodes.
114
+ let inheritedCmd: string[] | undefined;
115
+ let inheritedEntrypoint: string[] | undefined;
111
116
 
112
117
  // Auto-detect current container's configuration to inherit networks and env vars
113
118
  try {
@@ -115,50 +120,70 @@ export class DockerAdapter implements IOrchestratorAdapter {
115
120
  const myContainerId = os.hostname();
116
121
  const myContainer = this.docker.getContainer(myContainerId);
117
122
  const myInfo = await myContainer.inspect();
118
-
123
+
124
+ // Inherit the app container's image when the stack does not pin one
125
+ if (!targetImage && myInfo?.Config?.Image) {
126
+ targetImage = myInfo.Config.Image;
127
+ console.log('[DockerAdapter] Inherited image from app container:', targetImage);
128
+ }
129
+
119
130
  // Always inherit Networks so worker can communicate with main app
120
131
  if (myInfo?.NetworkSettings?.Networks) {
121
132
  const inheritedNetworks = Object.keys(myInfo.NetworkSettings.Networks);
122
133
  targetNetworks = Array.from(new Set([...inheritedNetworks, ...targetNetworks]));
123
134
  console.log('[DockerAdapter] Inherited networks:', targetNetworks);
124
135
  }
125
-
136
+
126
137
  // Inherit Environment Variables and merge with stack.envVars
127
138
  if (myInfo?.Config?.Env) {
128
- const envDict: Record<string, string> = {};
129
- myInfo.Config.Env.forEach((e: string) => {
130
- const idx = e.indexOf('=');
131
- if (idx !== -1) {
132
- envDict[e.substring(0, idx)] = e.substring(idx + 1);
133
- }
134
- });
135
- // Overwrite with explicitly defined env vars
136
- Object.assign(envDict, stack.envVars || {});
137
-
138
- targetEnvVars = Object.entries(envDict).map(([k, v]) => `${k}=${v}`);
139
+ const envDict: Record<string, string> = {};
140
+ myInfo.Config.Env.forEach((e: string) => {
141
+ const idx = e.indexOf('=');
142
+ if (idx !== -1) {
143
+ envDict[e.substring(0, idx)] = e.substring(idx + 1);
144
+ }
145
+ });
146
+ // Overwrite with explicitly defined env vars
147
+ Object.assign(envDict, stack.envVars || {});
148
+
149
+ targetEnvVars = Object.entries(envDict).map(([k, v]) => `${k}=${v}`);
139
150
  }
140
151
  // Inherit Volumes (Binds)
141
152
  if (myInfo?.HostConfig?.Binds) {
142
153
  const inheritedBinds = myInfo.HostConfig.Binds as string[];
143
154
  targetVolumes = Array.from(new Set([...inheritedBinds, ...targetVolumes]));
144
155
  }
156
+ // Inherit the startup Cmd/Entrypoint so the worker runs the same bootstrap
157
+ // as the app container (used only when the stack pins no explicit command).
158
+ if (Array.isArray(myInfo?.Config?.Cmd) && myInfo.Config.Cmd.length > 0) {
159
+ inheritedCmd = myInfo.Config.Cmd as string[];
160
+ }
161
+ if (Array.isArray(myInfo?.Config?.Entrypoint) && myInfo.Config.Entrypoint.length > 0) {
162
+ inheritedEntrypoint = myInfo.Config.Entrypoint as string[];
163
+ }
145
164
  } catch (e: any) {
146
165
  // Ignore error if not running in a container or cannot inspect
147
166
  console.error('[DockerAdapter] Failed to inherit container config:', e.message);
148
167
  }
149
168
 
150
169
  // Automatically separate logs for workers to prevent log interleaving with the main app
151
- const hasLoggerBase = targetEnvVars.some(e => e.startsWith('LOGGER_BASE_PATH='));
170
+ const hasLoggerBase = targetEnvVars.some((e) => e.startsWith('LOGGER_BASE_PATH='));
152
171
  if (!hasLoggerBase) {
153
172
  targetEnvVars.push(`LOGGER_BASE_PATH=/app/nocobase/storage/logs/${stack.name}`);
154
173
  }
155
174
 
175
+ if (!targetImage) {
176
+ throw new Error(
177
+ `[DockerAdapter] No image configured for stack "${stack.name}" and the app container image could not be determined.`,
178
+ );
179
+ }
180
+
156
181
  for (let i = 0; i < diff; i++) {
157
182
  const suffix = `${Date.now()}-${Math.random().toString(36).substring(2, 6)}`;
158
183
  const containerName = `${stack.name}-${suffix}`;
159
184
 
160
185
  const createOpts: any = {
161
- Image: stack.image,
186
+ Image: targetImage,
162
187
  name: containerName,
163
188
  Env: targetEnvVars,
164
189
  Labels: {
@@ -174,6 +199,15 @@ export class DockerAdapter implements IOrchestratorAdapter {
174
199
 
175
200
  if (stack.command) {
176
201
  createOpts.Cmd = ['/bin/sh', '-c', stack.command];
202
+ } else {
203
+ // No explicit command: replay the app container's bootstrap so the
204
+ // worker boots NocoBase the same way and registers a heartbeat.
205
+ if (inheritedEntrypoint) {
206
+ createOpts.Entrypoint = inheritedEntrypoint;
207
+ }
208
+ if (inheritedCmd) {
209
+ createOpts.Cmd = inheritedCmd;
210
+ }
177
211
  }
178
212
 
179
213
  if (stack.resourceLimits?.memory) {
@@ -190,7 +224,7 @@ export class DockerAdapter implements IOrchestratorAdapter {
190
224
  createOpts.HostConfig.SecurityOpt = ['no-new-privileges:true'];
191
225
 
192
226
  const container = await this.docker.createContainer(createOpts);
193
-
227
+
194
228
  // Connect to additional networks before starting
195
229
  if (targetNetworks.length > 0) {
196
230
  const startIndex = targetNetworkMode ? 0 : 1;
@@ -199,7 +233,9 @@ export class DockerAdapter implements IOrchestratorAdapter {
199
233
  const net = this.docker.getNetwork(targetNetworks[i]);
200
234
  await net.connect({ Container: container.id });
201
235
  } catch (err: any) {
202
- console.warn(`[DockerAdapter] Failed to connect container ${container.id} to network ${targetNetworks[i]}: ${err.message}`);
236
+ console.warn(
237
+ `[DockerAdapter] Failed to connect container ${container.id} to network ${targetNetworks[i]}: ${err.message}`,
238
+ );
203
239
  }
204
240
  }
205
241
  }
@@ -209,9 +245,7 @@ export class DockerAdapter implements IOrchestratorAdapter {
209
245
  }
210
246
  } else if (diff < 0) {
211
247
  // Scale DOWN — remove newest first (LIFO)
212
- const sorted = running.sort(
213
- (a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
214
- );
248
+ const sorted = running.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
215
249
  const toRemove = sorted.slice(0, Math.abs(diff));
216
250
 
217
251
  for (const c of toRemove) {
@@ -334,14 +368,17 @@ export class DockerAdapter implements IOrchestratorAdapter {
334
368
  .split(',')
335
369
  .map((part) => part.trim())
336
370
  .filter(Boolean)
337
- .reduce((acc, part) => {
338
- const [key, ...valueParts] = part.split('=');
339
- const value = valueParts.join('=');
340
- if (key?.trim() && value?.trim()) {
341
- acc[key.trim()] = value.trim();
342
- }
343
- return acc;
344
- }, {} as Record<string, string>);
371
+ .reduce(
372
+ (acc, part) => {
373
+ const [key, ...valueParts] = part.split('=');
374
+ const value = valueParts.join('=');
375
+ if (key?.trim() && value?.trim()) {
376
+ acc[key.trim()] = value.trim();
377
+ }
378
+ return acc;
379
+ },
380
+ {} as Record<string, string>,
381
+ );
345
382
  }
346
383
 
347
384
  private labelsMatch(labels: Record<string, string>, expected: Record<string, string>): boolean {
@@ -7,7 +7,7 @@ import { redisActions } from './actions/redis-monitor';
7
7
  import { aclCacheActions, createAclCacheMiddleware } from './actions/acl-cache';
8
8
  import { clusterActions, readLocalLogs } from './actions/cluster-nodes';
9
9
  import { getRedisClient } from './utils/redis';
10
- import { getLocalNodeId } from './utils/node';
10
+ import { getLocalNodeId, isWorkerMode } from './utils/node';
11
11
  import { eventQueueActions } from './actions/event-queue-monitor';
12
12
  import { lockActions } from './actions/lock-monitor';
13
13
  import { cacheMonitorActions } from './actions/cache-monitor';
@@ -16,6 +16,7 @@ import { RedisNodeRegistry } from './adapters/redis-node-registry';
16
16
  import { RedisLockAdapter } from './adapters/redis-lock-adapter';
17
17
  import { orchestratorActions } from './actions/orchestrator';
18
18
  import { pluginOperationsActions } from './actions/plugin-operations';
19
+ import { queueMappingsActions } from './actions/queue-mappings';
19
20
  import type { IOrchestratorAdapter } from './orchestrator/types';
20
21
  import { DockerAdapter } from './orchestrator/docker-adapter';
21
22
  import { K8sAdapter } from './orchestrator/k8s-adapter';
@@ -57,11 +58,8 @@ export class PluginClusterManagerServer extends Plugin {
57
58
  this.nodeRegistry?.start();
58
59
 
59
60
  // Automatically install packages on boot for worker nodes
60
- const mode = process.env.WORKER_MODE || 'main';
61
61
  const isWorker =
62
- mode === 'worker' ||
63
- mode === 'task' ||
64
- mode === '*' ||
62
+ isWorkerMode(process.env.WORKER_MODE) ||
65
63
  process.env.APP_ROLE === 'worker' ||
66
64
  process.env.APP_ROLE === 'sandbox';
67
65
  if (isWorker) {
@@ -125,9 +123,7 @@ export class PluginClusterManagerServer extends Plugin {
125
123
 
126
124
  // Workflow hook to trace executing node
127
125
  this.app.db.on('executions.afterSave', async (model: any) => {
128
- const mode = process.env.WORKER_MODE || 'main';
129
- const isWorker = mode === 'worker' || mode === 'task' || mode === '*';
130
- if (isWorker) {
126
+ if (isWorkerMode(process.env.WORKER_MODE)) {
131
127
  const id = model.get('id');
132
128
  const redis = getRedisClient(this.app);
133
129
  if (id && redis) {
@@ -332,6 +328,12 @@ export class PluginClusterManagerServer extends Plugin {
332
328
  actions: pluginOperationsActions,
333
329
  });
334
330
 
331
+ // Queue Mappings (queue-to-worker-stack assignments)
332
+ this.app.resourcer.define({
333
+ name: 'workerQueueMappings',
334
+ actions: queueMappingsActions,
335
+ });
336
+
335
337
  // Install ACL cache middleware inside the ACL chain so cached permissions are not overwritten.
336
338
  const aclCacheMiddleware = createAclCacheMiddleware(this.app);
337
339
  (this.app as any).acl.use(aclCacheMiddleware, {
@@ -380,6 +382,7 @@ export class PluginClusterManagerServer extends Plugin {
380
382
  'orchestratorStacks:*',
381
383
  'workerPackages:*',
382
384
  'clusterManagerPlugins:*',
385
+ 'workerQueueMappings:*',
383
386
  ],
384
387
  });
385
388
 
@@ -558,8 +561,7 @@ export class PluginClusterManagerServer extends Plugin {
558
561
  }
559
562
 
560
563
  private isWorkerOnlyNode(): boolean {
561
- const workerMode = process.env.WORKER_MODE || '';
562
- return workerMode === 'worker' || workerMode === 'task' || workerMode === '*';
564
+ return isWorkerMode(process.env.WORKER_MODE);
563
565
  }
564
566
  }
565
567
 
@@ -0,0 +1,154 @@
1
+ /**
2
+ * QueueScanner — discovers all registered queues in the system.
3
+ *
4
+ * Two sources:
5
+ * 1. EventQueue events (registered via app.eventQueue.subscribe)
6
+ * 2. Redis List-based queues (convention *:plugin-*:queue)
7
+ *
8
+ * Used by the Queue Assignment UI to let admins map queues to worker stacks.
9
+ */
10
+
11
+ import type { Application } from '@nocobase/server';
12
+ import { getRedisClient } from './utils/redis';
13
+
14
+ export type QueueItem = {
15
+ name: string;
16
+ label: string;
17
+ description: string;
18
+ type: 'event-queue' | 'redis-list';
19
+ pending: number | null;
20
+ };
21
+
22
+ const KNOWN_QUEUE_LABELS: Record<string, { label: string; description: string }> = {
23
+ 'workflow:process': {
24
+ label: 'Workflow',
25
+ description: 'Process workflow executions (plugin-workflow)',
26
+ },
27
+ 'async-task:process': {
28
+ label: 'Async Tasks',
29
+ description: 'Execute async tasks (plugin-async-task-manager)',
30
+ },
31
+ 'knowledge-base:document-vectorize': {
32
+ label: 'Document Vectorization',
33
+ description: 'Vectorize knowledge base documents (plugin-knowledge-base)',
34
+ },
35
+ 'git-review:process': {
36
+ label: 'Git Review',
37
+ description: 'AI code review jobs (plugin-git-manager)',
38
+ },
39
+ 'build-guide:process': {
40
+ label: 'Build Guide',
41
+ description: 'Build user guide pages (plugin-build-guide-block)',
42
+ },
43
+ 'build-ui-template:process': {
44
+ label: 'Build UI Template',
45
+ description: 'Build UI template pages (plugin-build-ui-template)',
46
+ },
47
+ };
48
+
49
+ /** Redis key patterns for List-based queues (same as event-queue-monitor.ts) */
50
+ const REDIS_QUEUE_PATTERNS = ['*:plugin-git-manager:review:queue', '*:plugin-build-guide-block:build:queue'];
51
+
52
+ function describeRedisQueueKey(key: string): { label: string; description: string } {
53
+ const parts = String(key).split(':');
54
+ const plugin = parts[parts.length - 3] || 'unknown';
55
+ const queue = parts[parts.length - 2] || key;
56
+ return {
57
+ label: `${queue} (${plugin})`,
58
+ description: `Redis List queue from ${plugin}`,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Discover all queues from EventQueue subscribers.
64
+ */
65
+ function scanEventQueue(app: Application): QueueItem[] {
66
+ const eq = (app as any).eventQueue;
67
+ if (!eq || !eq.events) return [];
68
+
69
+ const events: Map<string, { concurrency?: number; interval?: number; shared?: boolean }> = eq.events;
70
+ const items: QueueItem[] = [];
71
+
72
+ for (const [channel] of events.entries()) {
73
+ const known = KNOWN_QUEUE_LABELS[channel];
74
+ items.push({
75
+ name: channel,
76
+ label: known?.label ?? channel,
77
+ description: known?.description ?? `EventQueue channel: ${channel}`,
78
+ type: 'event-queue',
79
+ pending: null,
80
+ });
81
+ }
82
+
83
+ return items;
84
+ }
85
+
86
+ /**
87
+ * Discover Redis List-based queues via SCAN.
88
+ */
89
+ async function scanRedisQueues(app: Application): Promise<QueueItem[]> {
90
+ const redis = getRedisClient(app);
91
+ if (!redis) {
92
+ return [];
93
+ }
94
+
95
+ const seen = new Set<string>();
96
+ const items: QueueItem[] = [];
97
+
98
+ for (const pattern of REDIS_QUEUE_PATTERNS) {
99
+ try {
100
+ const keys: string[] = await redis.sendCommand(['SCAN', '0', 'MATCH', pattern, 'COUNT', '200']);
101
+ const keyList: string[] = typeof keys[1]?.length === 'number' ? keys[1] : [];
102
+
103
+ for (const key of keyList) {
104
+ if (seen.has(key)) continue;
105
+ seen.add(key);
106
+
107
+ const desc = describeRedisQueueKey(key);
108
+ let pending = 0;
109
+ try {
110
+ pending = Number(await redis.sendCommand(['LLEN', key])) || 0;
111
+ } catch {
112
+ pending = 0;
113
+ }
114
+
115
+ items.push({
116
+ name: key,
117
+ label: desc.label,
118
+ description: desc.description,
119
+ type: 'redis-list',
120
+ pending,
121
+ });
122
+ }
123
+ } catch {
124
+ // SCAN not supported or permission denied
125
+ }
126
+ }
127
+
128
+ return items;
129
+ }
130
+
131
+ /**
132
+ * Full queue scan — merges EventQueue + Redis results.
133
+ */
134
+ export async function scanQueues(app: Application): Promise<{ queues: QueueItem[]; total: number }> {
135
+ const eventQueues = scanEventQueue(app);
136
+ const redisQueues = await scanRedisQueues(app);
137
+
138
+ // Deduplicate: if a queue name appears in both sources, prefer EventQueue
139
+ const seenNames = new Set<string>();
140
+ const merged: QueueItem[] = [];
141
+
142
+ for (const q of eventQueues) {
143
+ merged.push(q);
144
+ seenNames.add(q.name);
145
+ }
146
+ for (const q of redisQueues) {
147
+ if (!seenNames.has(q.name)) {
148
+ merged.push(q);
149
+ seenNames.add(q.name);
150
+ }
151
+ }
152
+
153
+ return { queues: merged, total: merged.length };
154
+ }
@@ -1,5 +1,53 @@
1
1
  import os from 'os';
2
2
 
3
+ export type NodeRole = 'app' | 'worker' | 'sandbox';
4
+
5
+ /**
6
+ * Determine whether a WORKER_MODE value denotes a queue-processing worker
7
+ * (i.e. a node that does NOT serve HTTP), following NocoBase v2.1.x semantics:
8
+ * '' → app node (serves HTTP + every queue)
9
+ * '!' → app node (serves HTTP only)
10
+ * '*' → worker (serves all queues, no HTTP)
11
+ * 'a:b,c:d' → worker (serves the listed queue topics, no HTTP)
12
+ * '-' → transient subprocess (not a long-lived worker)
13
+ * Legacy literals 'worker' / 'task' are still tolerated for older deployments.
14
+ * A combined value containing '!' (e.g. '!,workflow:process') still serves
15
+ * HTTP, so it is treated as an app node.
16
+ */
17
+ export function isWorkerMode(workerMode?: string): boolean {
18
+ const mode = (workerMode ?? process.env.WORKER_MODE ?? '').trim();
19
+ if (!mode || mode === 'main' || mode === 'app') return false;
20
+ if (mode === '-') return false;
21
+ const topics = mode
22
+ .split(',')
23
+ .map((t) => t.trim())
24
+ .filter(Boolean);
25
+ if (topics.includes('!')) return false;
26
+ return true;
27
+ }
28
+
29
+ /**
30
+ * Resolve a node role from its descriptor. APP_ROLE (explicit) wins, then an
31
+ * explicit sandbox flag, then WORKER_MODE parsing. Works for both the local
32
+ * process (pass process.env values) and remote node records read from Redis.
33
+ */
34
+ export function getNodeRoleFrom(opts: { workerMode?: string; appRole?: string; isSandbox?: boolean }): NodeRole {
35
+ if (opts.appRole === 'app' || opts.appRole === 'worker' || opts.appRole === 'sandbox') {
36
+ return opts.appRole;
37
+ }
38
+ if (opts.isSandbox) return 'sandbox';
39
+ return isWorkerMode(opts.workerMode) ? 'worker' : 'app';
40
+ }
41
+
42
+ /** Resolve the role of the current Node.js process from its environment. */
43
+ export function getLocalRole(): NodeRole {
44
+ return getNodeRoleFrom({
45
+ workerMode: process.env.WORKER_MODE,
46
+ appRole: process.env.APP_ROLE,
47
+ isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
48
+ });
49
+ }
50
+
3
51
  /**
4
52
  * Generate a universally unique identifier for this specific Node.js process.
5
53
  * Combines app name, worker mode, hostname, port, and PID to ensure uniqueness
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function AclCacheManager(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function CacheMonitor(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function ClusterManagerLayout(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function ClusterNodes(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function ContainerOrchestrator(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function Doctor(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function EventQueueMonitor(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function LockMonitor(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function NginxCacheManager(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare const PackageInstaller: React.FC;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function PluginOperations(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function RedisMonitor(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function TaskManager(): React.JSX.Element;
@@ -1,2 +0,0 @@
1
- import React from 'react';
2
- export declare function WorkflowExecutions(): React.JSX.Element;
@@ -1,5 +0,0 @@
1
- import { Plugin } from '@nocobase/client';
2
- export declare class PluginClusterManagerClient extends Plugin {
3
- load(): Promise<void>;
4
- }
5
- export default PluginClusterManagerClient;
@@ -1,3 +0,0 @@
1
- import { APIClient } from '@nocobase/client';
2
- export declare function clearClientCache(): void;
3
- export declare function setupClientSafeCache(apiClient: APIClient): void;
@@ -1,2 +0,0 @@
1
- import { APIClient } from '@nocobase/client';
2
- export declare function setupRequestDedupAndCache(apiClient: APIClient): void;
@@ -1,12 +0,0 @@
1
- /**
2
- * Shared i18n hook for the cluster-manager plugin.
3
- */
4
- export declare function useT(): (key: string) => string;
5
- /**
6
- * Format bytes into human-readable string (B, KB, MB, GB).
7
- */
8
- export declare function formatBytes(bytes: number): string;
9
- /**
10
- * Format seconds into human-readable uptime string (e.g., "2d 5h 30m").
11
- */
12
- export declare function formatUptime(seconds: number): string;
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './server';
2
- export { default } from './server';
@@ -1,53 +0,0 @@
1
- import { Context } from '@nocobase/actions';
2
- /**
3
- * In-memory ACL stats counter.
4
- * Tracks total checks, cache hits/misses per role:resource:action.
5
- */
6
- export interface AclCacheStats {
7
- totalChecks: number;
8
- cacheHits: number;
9
- cacheMisses: number;
10
- startedAt: string;
11
- detailByRole: Record<string, {
12
- checks: number;
13
- hits: number;
14
- misses: number;
15
- }>;
16
- }
17
- /**
18
- * Middleware that caches the permission object computed by the ACL middleware.
19
- * Install via: app.acl.use(aclCacheMiddleware, { tag: 'aclCache', before: 'core', after: 'allow-manager' })
20
- *
21
- * FIX: Previously monkey-patched ctx.app.acl.can per-request, which is a race condition
22
- * because acl is a shared singleton across concurrent requests. Now we use a post-check
23
- * approach that reads from cache first and writes after the ACL middleware runs, without
24
- * ever replacing the shared acl.can method.
25
- */
26
- export declare function createAclCacheMiddleware(app: any): (ctx: Context, next: () => Promise<void>) => Promise<void>;
27
- export declare const aclCacheActions: {
28
- /**
29
- * GET /clusterManagerAclCache:stats
30
- * Returns ACL cache hit/miss statistics
31
- */
32
- stats(ctx: Context, next: () => Promise<void>): Promise<void>;
33
- /**
34
- * GET /clusterManagerAclCache:listKeys
35
- * Lists all cached ACL permission keys
36
- */
37
- listKeys(ctx: Context, next: () => Promise<void>): Promise<void>;
38
- /**
39
- * POST /clusterManagerAclCache:clear
40
- * Clear all ACL cache entries and reset stats
41
- */
42
- clear(ctx: Context, next: () => Promise<void>): Promise<void>;
43
- /**
44
- * POST /clusterManagerAclCache:resetStats
45
- * Reset the in-memory ACL stats counters
46
- */
47
- resetStats(ctx: Context, next: () => Promise<void>): Promise<void>;
48
- /**
49
- * POST /clusterManagerAclCache:clearRole
50
- * Clear ACL cache entries for a specific role
51
- */
52
- clearRole(ctx: Context, next: () => Promise<void>): Promise<void>;
53
- };
@@ -1,33 +0,0 @@
1
- import { Context } from '@nocobase/actions';
2
- export declare const cacheMonitorActions: {
3
- /**
4
- * GET /clusterManagerCacheMgr:stores
5
- * List all registered cache stores and their config
6
- */
7
- stores(ctx: Context, next: () => Promise<void>): Promise<void>;
8
- /**
9
- * GET /clusterManagerCacheMgr:caches
10
- * List all created named caches
11
- */
12
- caches(ctx: Context, next: () => Promise<void>): Promise<void>;
13
- /**
14
- * GET /clusterManagerCacheMgr:redisMemory
15
- * Get Redis memory usage for cache keys
16
- */
17
- redisMemory(ctx: Context, next: () => Promise<void>): Promise<void>;
18
- /**
19
- * POST /clusterManagerCacheMgr:flushAll
20
- * Flush all caches via CacheManager
21
- */
22
- flushAll(ctx: Context, next: () => Promise<void>): Promise<void>;
23
- /**
24
- * GET /clusterManagerCacheMgr:nginxCacheStatus
25
- * Detect if Nginx is installed, locate conf, and auto-load cache paths
26
- */
27
- nginxCacheStatus(ctx: Context, next: () => Promise<void>): Promise<void>;
28
- /**
29
- * POST /clusterManagerCacheMgr:clearNginxCache
30
- * Clear physical cache files or send an HTTP Purge request
31
- */
32
- clearNginxCache(ctx: Context, next: () => Promise<void>): Promise<void>;
33
- };