plugin-cluster-manager 1.1.15 → 1.1.17

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 (49) hide show
  1. package/dist/client/index.js +1 -1
  2. package/dist/client-v2/376.cd1d86e85a50088e.js +10 -0
  3. package/dist/client-v2/index.js +1 -1
  4. package/dist/externalVersion.js +6 -6
  5. package/dist/locale/en-US.json +16 -6
  6. package/dist/locale/vi-VN.json +16 -6
  7. package/dist/locale/zh-CN.json +16 -6
  8. package/dist/server/actions/cluster-nodes.js +44 -11
  9. package/dist/server/actions/doctor.js +73 -7
  10. package/dist/server/actions/event-queue-monitor.js +33 -3
  11. package/dist/server/actions/orchestrator.js +48 -32
  12. package/dist/server/actions/queue-mappings.js +1 -0
  13. package/dist/server/actions/tasks.js +8 -8
  14. package/dist/server/adapters/redis-event-queue-adapter.js +188 -0
  15. package/dist/server/adapters/redis-node-registry.js +44 -10
  16. package/dist/server/collections/orchestrator-stacks.js +6 -0
  17. package/dist/server/collections/worker-queue-mappings.js +1 -1
  18. package/dist/server/orchestrator/PackageManager.js +47 -12
  19. package/dist/server/plugin.js +37 -6
  20. package/dist/server/queue-scanner.js +54 -34
  21. package/dist/server/utils/node.js +3 -7
  22. package/dist/server/utils/redis.js +37 -9
  23. package/dist/shared/worker-processes.js +233 -0
  24. package/package.json +1 -1
  25. package/src/client/ClusterNodes.tsx +76 -10
  26. package/src/client/ContainerOrchestrator.tsx +146 -8
  27. package/src/client/QueueAssignment.tsx +10 -2
  28. package/src/locale/en-US.json +16 -6
  29. package/src/locale/vi-VN.json +16 -6
  30. package/src/locale/zh-CN.json +16 -6
  31. package/src/server/__tests__/worker-processes.test.ts +42 -0
  32. package/src/server/actions/cluster-nodes.ts +43 -8
  33. package/src/server/actions/doctor.ts +77 -0
  34. package/src/server/actions/event-queue-monitor.ts +34 -3
  35. package/src/server/actions/orchestrator.ts +58 -38
  36. package/src/server/actions/queue-mappings.ts +1 -0
  37. package/src/server/actions/tasks.ts +142 -142
  38. package/src/server/adapters/redis-event-queue-adapter.ts +189 -0
  39. package/src/server/adapters/redis-node-registry.ts +44 -4
  40. package/src/server/collections/orchestrator-stacks.ts +6 -0
  41. package/src/server/collections/worker-queue-mappings.ts +3 -3
  42. package/src/server/orchestrator/PackageManager.ts +48 -11
  43. package/src/server/orchestrator/types.ts +5 -4
  44. package/src/server/plugin.ts +40 -6
  45. package/src/server/queue-scanner.ts +65 -51
  46. package/src/server/utils/node.ts +3 -10
  47. package/src/server/utils/redis.ts +39 -4
  48. package/src/shared/worker-processes.ts +216 -0
  49. package/dist/client-v2/914.c0bce51908fd81d7.js +0 -10
@@ -1,14 +1,15 @@
1
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.
2
+ * QueueScanner discovers process keys and queue aliases that can be assigned
3
+ * to worker stacks. The worker deployment should receive process keys in
4
+ * WORKER_MODE; physical EventQueue/Redis names are exposed as aliases only.
9
5
  */
10
6
 
11
7
  import type { Application } from '@nocobase/server';
8
+ import {
9
+ getWorkerProcessDefinition,
10
+ resolveWorkerProcessName,
11
+ WORKER_PROCESS_DEFINITIONS,
12
+ } from '../shared/worker-processes';
12
13
  import { getRedisClient } from './utils/redis';
13
14
 
14
15
  export type QueueItem = {
@@ -17,39 +18,27 @@ export type QueueItem = {
17
18
  description: string;
18
19
  type: 'event-queue' | 'redis-list';
19
20
  pending: number | null;
21
+ workerProcessName?: string;
20
22
  };
21
23
 
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'];
24
+ const REDIS_QUEUE_PATTERNS = [
25
+ '*:plugin-git-manager:review:queue',
26
+ '*:plugin-build-guide-block:build:queue',
27
+ '*:plugin-build-visualization-block:build:queue',
28
+ 'file-preview-auth.ocr.queue',
29
+ ];
30
+
31
+ function describeRedisQueueKey(key: string): { label: string; description: string; workerProcessName?: string } {
32
+ const workerProcessName = resolveWorkerProcessName(key);
33
+ const definition = getWorkerProcessDefinition(workerProcessName);
34
+ if (definition) {
35
+ return {
36
+ label: definition.label,
37
+ description: definition.description,
38
+ workerProcessName: definition.name,
39
+ };
40
+ }
51
41
 
52
- function describeRedisQueueKey(key: string): { label: string; description: string } {
53
42
  const parts = String(key).split(':');
54
43
  const plugin = parts[parts.length - 3] || 'unknown';
55
44
  const queue = parts[parts.length - 2] || key;
@@ -59,33 +48,54 @@ function describeRedisQueueKey(key: string): { label: string; description: strin
59
48
  };
60
49
  }
61
50
 
62
- /**
63
- * Discover all queues from EventQueue subscribers.
64
- */
65
51
  function scanEventQueue(app: Application): QueueItem[] {
66
52
  const eq = (app as any).eventQueue;
67
- if (!eq || !eq.events) return [];
53
+ if (!eq?.events) return [];
68
54
 
69
55
  const events: Map<string, { concurrency?: number; interval?: number; shared?: boolean }> = eq.events;
70
56
  const items: QueueItem[] = [];
71
57
 
72
58
  for (const [channel] of events.entries()) {
73
- const known = KNOWN_QUEUE_LABELS[channel];
59
+ const workerProcessName = resolveWorkerProcessName(channel);
60
+ const known = getWorkerProcessDefinition(workerProcessName);
74
61
  items.push({
75
62
  name: channel,
76
63
  label: known?.label ?? channel,
77
64
  description: known?.description ?? `EventQueue channel: ${channel}`,
78
65
  type: 'event-queue',
79
66
  pending: null,
67
+ workerProcessName: known?.name,
80
68
  });
81
69
  }
82
70
 
83
71
  return items;
84
72
  }
85
73
 
86
- /**
87
- * Discover Redis List-based queues via SCAN.
88
- */
74
+ function scanKnownWorkerModes(app: Application): QueueItem[] {
75
+ const pluginManager = (app as unknown as { pm?: { get?: (name: string) => unknown } }).pm;
76
+ if (!pluginManager?.get) return [];
77
+
78
+ const hasPlugin = (name: string) => {
79
+ try {
80
+ return Boolean(pluginManager.get?.(name));
81
+ } catch {
82
+ return false;
83
+ }
84
+ };
85
+
86
+ return WORKER_PROCESS_DEFINITIONS.filter(
87
+ (definition) =>
88
+ definition.common && !definition.sandbox && (!definition.pluginName || hasPlugin(definition.pluginName)),
89
+ ).map((definition) => ({
90
+ name: definition.name,
91
+ label: definition.label,
92
+ description: definition.description,
93
+ type: definition.kind === 'redis-list' ? 'redis-list' : ('event-queue' as const),
94
+ pending: null,
95
+ workerProcessName: definition.name,
96
+ }));
97
+ }
98
+
89
99
  async function scanRedisQueues(app: Application): Promise<QueueItem[]> {
90
100
  const redis = getRedisClient(app);
91
101
  if (!redis) {
@@ -97,8 +107,8 @@ async function scanRedisQueues(app: Application): Promise<QueueItem[]> {
97
107
 
98
108
  for (const pattern of REDIS_QUEUE_PATTERNS) {
99
109
  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] : [];
110
+ const result = await redis.sendCommand(['SCAN', '0', 'MATCH', pattern, 'COUNT', '200']);
111
+ const keyList: string[] = Array.isArray(result?.[1]) ? result[1] : [];
102
112
 
103
113
  for (const key of keyList) {
104
114
  if (seen.has(key)) continue;
@@ -118,6 +128,7 @@ async function scanRedisQueues(app: Application): Promise<QueueItem[]> {
118
128
  description: desc.description,
119
129
  type: 'redis-list',
120
130
  pending,
131
+ workerProcessName: desc.workerProcessName,
121
132
  });
122
133
  }
123
134
  } catch {
@@ -128,14 +139,11 @@ async function scanRedisQueues(app: Application): Promise<QueueItem[]> {
128
139
  return items;
129
140
  }
130
141
 
131
- /**
132
- * Full queue scan — merges EventQueue + Redis results.
133
- */
134
142
  export async function scanQueues(app: Application): Promise<{ queues: QueueItem[]; total: number }> {
135
143
  const eventQueues = scanEventQueue(app);
144
+ const knownWorkerModes = scanKnownWorkerModes(app);
136
145
  const redisQueues = await scanRedisQueues(app);
137
146
 
138
- // Deduplicate: if a queue name appears in both sources, prefer EventQueue
139
147
  const seenNames = new Set<string>();
140
148
  const merged: QueueItem[] = [];
141
149
 
@@ -143,6 +151,12 @@ export async function scanQueues(app: Application): Promise<{ queues: QueueItem[
143
151
  merged.push(q);
144
152
  seenNames.add(q.name);
145
153
  }
154
+ for (const q of knownWorkerModes) {
155
+ if (!seenNames.has(q.name)) {
156
+ merged.push(q);
157
+ seenNames.add(q.name);
158
+ }
159
+ }
146
160
  for (const q of redisQueues) {
147
161
  if (!seenNames.has(q.name)) {
148
162
  merged.push(q);
@@ -1,4 +1,5 @@
1
1
  import os from 'os';
2
+ import { isWorkerOnlyMode, normalizeWorkerMode } from '../../shared/worker-processes';
2
3
 
3
4
  export type NodeRole = 'app' | 'worker' | 'sandbox';
4
5
 
@@ -15,15 +16,7 @@ export type NodeRole = 'app' | 'worker' | 'sandbox';
15
16
  * HTTP, so it is treated as an app node.
16
17
  */
17
18
  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;
19
+ return isWorkerOnlyMode(workerMode ?? process.env.WORKER_MODE);
27
20
  }
28
21
 
29
22
  /**
@@ -55,7 +48,7 @@ export function getLocalRole(): NodeRole {
55
48
  */
56
49
  export function getLocalNodeId(app: any): string {
57
50
  const port = process.env.APP_PORT || 'unknown';
58
- const mode = process.env.WORKER_MODE || 'main';
51
+ const mode = normalizeWorkerMode(process.env.WORKER_MODE) || 'main';
59
52
  const appName = process.env.APP_NAME || app?.name || 'main';
60
53
  return `${appName}_${mode}_${os.hostname()}_${port}_${process.pid}`;
61
54
  }
@@ -3,15 +3,52 @@ import { createClient } from 'redis';
3
3
 
4
4
  let globalRedisClient: any = null;
5
5
 
6
+ const CLUSTER_REDIS_CONNECTION = 'cluster-manager:registry';
7
+
8
+ const CLUSTER_REDIS_ENV_CANDIDATES = [
9
+ 'NODE_REGISTRY_REDIS_URL',
10
+ 'CLUSTER_MANAGER_REDIS_URL',
11
+ 'REDIS_URL',
12
+ 'CACHE_REDIS_URL',
13
+ 'PUBSUB_ADAPTER_REDIS_URL',
14
+ 'QUEUE_ADAPTER_REDIS_URL',
15
+ 'LOCK_ADAPTER_REDIS_URL',
16
+ ];
17
+
18
+ export function getClusterRedisUrl() {
19
+ for (const key of CLUSTER_REDIS_ENV_CANDIDATES) {
20
+ const value = process.env[key];
21
+ if (value?.trim()) {
22
+ return value.trim();
23
+ }
24
+ }
25
+ return '';
26
+ }
27
+
28
+ export function isClusterRedisConfigured(app?: any) {
29
+ if (getClusterRedisUrl()) {
30
+ return true;
31
+ }
32
+
33
+ const manager = app?.redisConnectionManager;
34
+ return Boolean(manager?.getConnection?.());
35
+ }
36
+
6
37
  export function getRedisClient(app?: any) {
7
38
  if (globalRedisClient) return globalRedisClient;
8
39
 
40
+ const url = getClusterRedisUrl();
41
+
9
42
  if (app?.redisConnectionManager) {
43
+ if (url) {
44
+ const conn = app.redisConnectionManager.getConnection(CLUSTER_REDIS_CONNECTION, { connectionString: url });
45
+ if (conn) return conn;
46
+ }
47
+
10
48
  const conn = app.redisConnectionManager.getConnection();
11
49
  if (conn) return conn;
12
50
  }
13
51
 
14
- const url = process.env.REDIS_URL || process.env.CACHE_REDIS_URL || process.env.PUBSUB_ADAPTER_REDIS_URL;
15
52
  if (!url) return null;
16
53
 
17
54
  globalRedisClient = createClient({ url });
@@ -53,9 +90,7 @@ export async function scanKeys(redis: any, pattern: string, batchSize = 200): Pr
53
90
  let cursor = '0';
54
91
 
55
92
  do {
56
- const result = await redis.sendCommand([
57
- 'SCAN', cursor, 'MATCH', pattern, 'COUNT', String(batchSize),
58
- ]);
93
+ const result = await redis.sendCommand(['SCAN', cursor, 'MATCH', pattern, 'COUNT', String(batchSize)]);
59
94
 
60
95
  // result is [nextCursor, [...keys]]
61
96
  cursor = String(result[0]);
@@ -0,0 +1,216 @@
1
+ export type WorkerProcessKind = 'event-queue' | 'redis-list' | 'pubsub';
2
+
3
+ export type WorkerProcessDefinition = {
4
+ name: string;
5
+ label: string;
6
+ description: string;
7
+ kind: WorkerProcessKind;
8
+ aliases?: string[];
9
+ redisKeySuffixes?: string[];
10
+ pluginName?: string;
11
+ sandbox?: boolean;
12
+ common?: boolean;
13
+ };
14
+
15
+ export const WORKER_PROCESS_DEFINITIONS: WorkerProcessDefinition[] = [
16
+ {
17
+ name: 'workflow:process',
18
+ label: 'Workflow',
19
+ description: 'Process workflow executions',
20
+ kind: 'event-queue',
21
+ aliases: ['workflow.pendingExecution', '@nocobase/plugin-workflow.pendingExecution'],
22
+ common: true,
23
+ },
24
+ {
25
+ name: 'async-task:process',
26
+ label: 'Async Tasks',
27
+ description: 'Execute async tasks',
28
+ kind: 'event-queue',
29
+ aliases: ['async-task-manager.task', '@nocobase/plugin-async-task-manager.task'],
30
+ common: true,
31
+ },
32
+ {
33
+ name: 'notification:send',
34
+ label: 'Notifications',
35
+ description: 'Send notification messages',
36
+ kind: 'event-queue',
37
+ aliases: ['notification-manager.send', '@nocobase/plugin-notification-manager.send'],
38
+ common: true,
39
+ },
40
+ {
41
+ name: 'knowledge-base:document-vectorize',
42
+ label: 'Document Vectorization',
43
+ description: 'Vectorize knowledge base documents',
44
+ kind: 'event-queue',
45
+ aliases: ['knowledge-base:document-vectorize'],
46
+ pluginName: 'plugin-knowledge-base',
47
+ common: true,
48
+ },
49
+ {
50
+ name: 'git-review:process',
51
+ label: 'Git Review',
52
+ description: 'AI code review jobs',
53
+ kind: 'redis-list',
54
+ aliases: ['plugin-git-manager.review'],
55
+ redisKeySuffixes: [':plugin-git-manager:review:queue'],
56
+ pluginName: 'plugin-git-manager',
57
+ common: true,
58
+ },
59
+ {
60
+ name: 'build-guide:process',
61
+ label: 'Build Guide',
62
+ description: 'Build user guide pages',
63
+ kind: 'redis-list',
64
+ aliases: ['plugin-build-guide-block.build'],
65
+ redisKeySuffixes: [':plugin-build-guide-block:build:queue'],
66
+ pluginName: 'plugin-build-guide-block',
67
+ common: true,
68
+ },
69
+ {
70
+ name: 'build-visualization:process',
71
+ label: 'Build Visualization',
72
+ description: 'Build visualization blocks',
73
+ kind: 'redis-list',
74
+ aliases: ['plugin-build-visualization-block.build'],
75
+ redisKeySuffixes: [':plugin-build-visualization-block:build:queue'],
76
+ pluginName: 'plugin-build-visualization-block',
77
+ common: true,
78
+ },
79
+ {
80
+ name: 'build-ui-template:process',
81
+ label: 'Build UI Template',
82
+ description: 'Build UI template pages',
83
+ kind: 'event-queue',
84
+ aliases: ['plugin-build-ui-template.build'],
85
+ pluginName: 'plugin-build-ui-template',
86
+ common: true,
87
+ },
88
+ {
89
+ name: 'file-preview-auth:ocr',
90
+ label: 'File Preview OCR',
91
+ description: 'Run OCR extraction for authenticated file previews',
92
+ kind: 'redis-list',
93
+ aliases: ['file-preview-auth.ocr.queue'],
94
+ redisKeySuffixes: ['file-preview-auth.ocr.queue'],
95
+ pluginName: 'plugin-file-preview-auth',
96
+ common: true,
97
+ },
98
+ {
99
+ name: 'skill-hub:sandbox',
100
+ label: 'Skill Sandbox',
101
+ description: 'Execute Skill Hub sandbox tasks',
102
+ kind: 'pubsub',
103
+ aliases: ['skill-hub.task'],
104
+ pluginName: 'plugin-agent-orchestrator',
105
+ sandbox: true,
106
+ },
107
+ ];
108
+
109
+ const PROCESS_BY_NAME = new Map(WORKER_PROCESS_DEFINITIONS.map((definition) => [definition.name, definition]));
110
+
111
+ const ALIAS_TO_PROCESS = new Map<string, string>();
112
+ for (const definition of WORKER_PROCESS_DEFINITIONS) {
113
+ ALIAS_TO_PROCESS.set(definition.name, definition.name);
114
+ for (const alias of definition.aliases || []) {
115
+ ALIAS_TO_PROCESS.set(alias, definition.name);
116
+ }
117
+ }
118
+
119
+ function splitWorkerModeTokens(value?: string | string[]): string[] {
120
+ const rawTokens = Array.isArray(value) ? value : String(value || '').split(',');
121
+ return rawTokens.map((token) => String(token).trim()).filter(Boolean);
122
+ }
123
+
124
+ function stripKnownPrefixes(token: string): string[] {
125
+ const candidates = [token];
126
+
127
+ const dotIndex = token.indexOf('.');
128
+ if (dotIndex > 0) {
129
+ candidates.push(token.slice(dotIndex + 1));
130
+ }
131
+
132
+ const colonIndex = token.indexOf(':');
133
+ if (colonIndex > 0) {
134
+ candidates.push(token.slice(colonIndex + 1));
135
+ }
136
+
137
+ return Array.from(new Set(candidates));
138
+ }
139
+
140
+ export function resolveWorkerProcessName(token: string): string {
141
+ const value = String(token || '').trim();
142
+ if (!value || value === '*' || value === '!' || value === '-') {
143
+ return value;
144
+ }
145
+
146
+ const directProcessName = ALIAS_TO_PROCESS.get(value);
147
+ if (directProcessName) {
148
+ return directProcessName;
149
+ }
150
+
151
+ for (const candidate of stripKnownPrefixes(value)) {
152
+ const processName = ALIAS_TO_PROCESS.get(candidate);
153
+ if (processName) {
154
+ return processName;
155
+ }
156
+ }
157
+
158
+ for (const definition of WORKER_PROCESS_DEFINITIONS) {
159
+ if ((definition.redisKeySuffixes || []).some((suffix) => value.endsWith(suffix))) {
160
+ return definition.name;
161
+ }
162
+ }
163
+
164
+ return value;
165
+ }
166
+
167
+ export function normalizeWorkerMode(value?: string | string[]): string | undefined {
168
+ const resolved = splitWorkerModeTokens(value).map(resolveWorkerProcessName);
169
+ if (resolved.includes('*')) {
170
+ return '*';
171
+ }
172
+
173
+ const unique = Array.from(new Set(resolved.filter(Boolean)));
174
+ return unique.length ? unique.join(',') : undefined;
175
+ }
176
+
177
+ export function workerModeTokens(value?: string | string[]): string[] {
178
+ const normalized = normalizeWorkerMode(value);
179
+ return normalized ? normalized.split(',') : [];
180
+ }
181
+
182
+ export function workerModeServesProcess(workerMode: string | undefined, processNameOrAlias: string): boolean {
183
+ const mode = normalizeWorkerMode(workerMode);
184
+ if (!mode) return true;
185
+ if (mode === 'main' || mode === 'app') return true;
186
+ if (mode === '-') return false;
187
+ if (mode === '*') return true;
188
+
189
+ const processName = resolveWorkerProcessName(processNameOrAlias);
190
+ return mode.split(',').includes(processName);
191
+ }
192
+
193
+ export function isWorkerOnlyMode(workerMode?: string): boolean {
194
+ const mode = normalizeWorkerMode(workerMode);
195
+ if (!mode || mode === 'main' || mode === 'app' || mode === '-') return false;
196
+ return !mode.split(',').includes('!');
197
+ }
198
+
199
+ export function isAppServingMode(workerMode?: string): boolean {
200
+ const mode = normalizeWorkerMode(workerMode);
201
+ if (!mode || mode === 'main' || mode === 'app') return true;
202
+ return mode.split(',').includes('!');
203
+ }
204
+
205
+ export function getWorkerProcessDefinition(nameOrAlias: string): WorkerProcessDefinition | undefined {
206
+ return PROCESS_BY_NAME.get(resolveWorkerProcessName(nameOrAlias));
207
+ }
208
+
209
+ export function getCommonWorkerProcesses(): WorkerProcessDefinition[] {
210
+ return WORKER_PROCESS_DEFINITIONS.filter((definition) => definition.common && !definition.sandbox);
211
+ }
212
+
213
+ export function getWorkerProcessByChannel(channel: string): WorkerProcessDefinition | undefined {
214
+ const processName = resolveWorkerProcessName(channel);
215
+ return PROCESS_BY_NAME.get(processName);
216
+ }