plugin-cluster-manager 1.1.10 → 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 (119) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/client.js +1 -0
  4. package/dist/client/index.js +1 -1
  5. package/dist/client-v2/914.5dc1105cf3ada6a6.js +10 -0
  6. package/dist/client-v2/index.js +10 -0
  7. package/dist/externalVersion.js +6 -5
  8. package/dist/locale/en-US.json +138 -28
  9. package/dist/locale/vi-VN.json +139 -28
  10. package/dist/locale/zh-CN.json +140 -28
  11. package/dist/server/actions/cache-monitor.js +301 -0
  12. package/dist/server/actions/cluster-nodes.js +391 -11
  13. package/dist/server/actions/doctor.js +1246 -0
  14. package/dist/server/actions/orchestrator.js +37 -0
  15. package/dist/server/actions/queue-mappings.js +107 -0
  16. package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
  17. package/dist/server/collections/cluster-manager-doctor.js +44 -0
  18. package/dist/server/collections/worker-queue-mappings.js +106 -0
  19. package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
  20. package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
  21. package/dist/server/orchestrator/PackageManager.js +21 -24
  22. package/dist/server/orchestrator/docker-adapter.js +49 -27
  23. package/dist/server/plugin.js +71 -16
  24. package/dist/server/queue-scanner.js +141 -0
  25. package/dist/server/utils/node.js +30 -2
  26. package/dist/server/utils/versionManager.js +91 -0
  27. package/package.json +9 -5
  28. package/server.js +1 -0
  29. package/src/client/AclCacheManager.tsx +292 -287
  30. package/src/client/CacheMonitor.tsx +166 -179
  31. package/src/client/ClusterManagerLayout.tsx +54 -42
  32. package/src/client/ClusterNodes.tsx +698 -418
  33. package/src/client/ContainerOrchestrator.tsx +184 -102
  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/QueueAssignment.tsx +355 -0
  38. package/src/client/TaskManager.tsx +194 -187
  39. package/src/client/WorkflowExecutions.tsx +243 -238
  40. package/src/client/index.tsx +22 -14
  41. package/src/client/utils/clientSafeCache.ts +41 -0
  42. package/src/client/utils/requestDedupInterceptor.ts +213 -0
  43. package/src/client-v2/plugin.tsx +24 -0
  44. package/src/locale/en-US.json +138 -28
  45. package/src/locale/vi-VN.json +139 -28
  46. package/src/locale/zh-CN.json +140 -28
  47. package/src/server/__tests__/doctor.test.ts +53 -0
  48. package/src/server/actions/acl-cache.ts +272 -272
  49. package/src/server/actions/cache-monitor.ts +453 -116
  50. package/src/server/actions/cluster-nodes.ts +878 -378
  51. package/src/server/actions/doctor.ts +1536 -0
  52. package/src/server/actions/orchestrator.ts +54 -2
  53. package/src/server/actions/queue-mappings.ts +94 -0
  54. package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
  55. package/src/server/collections/cluster-manager-doctor.ts +19 -0
  56. package/src/server/collections/worker-queue-mappings.ts +85 -0
  57. package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
  58. package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
  59. package/src/server/orchestrator/PackageManager.ts +20 -24
  60. package/src/server/orchestrator/docker-adapter.ts +74 -37
  61. package/src/server/plugin.ts +347 -270
  62. package/src/server/queue-scanner.ts +154 -0
  63. package/src/server/utils/node.ts +48 -0
  64. package/src/server/utils/versionManager.ts +69 -0
  65. package/dist/client/AclCacheManager.d.ts +0 -2
  66. package/dist/client/CacheMonitor.d.ts +0 -2
  67. package/dist/client/ClusterManagerLayout.d.ts +0 -2
  68. package/dist/client/ClusterNodes.d.ts +0 -2
  69. package/dist/client/ContainerOrchestrator.d.ts +0 -2
  70. package/dist/client/EventQueueMonitor.d.ts +0 -2
  71. package/dist/client/LockMonitor.d.ts +0 -2
  72. package/dist/client/PackageInstaller.d.ts +0 -2
  73. package/dist/client/PluginOperations.d.ts +0 -2
  74. package/dist/client/RedisMonitor.d.ts +0 -2
  75. package/dist/client/TaskManager.d.ts +0 -2
  76. package/dist/client/WorkflowExecutions.d.ts +0 -2
  77. package/dist/client/index.d.ts +0 -5
  78. package/dist/client/utils.d.ts +0 -12
  79. package/dist/index.d.ts +0 -2
  80. package/dist/server/actions/acl-cache.d.ts +0 -53
  81. package/dist/server/actions/cache-monitor.d.ts +0 -23
  82. package/dist/server/actions/cluster-nodes.d.ts +0 -49
  83. package/dist/server/actions/event-queue-monitor.d.ts +0 -13
  84. package/dist/server/actions/lock-monitor.d.ts +0 -19
  85. package/dist/server/actions/orchestrator.d.ts +0 -58
  86. package/dist/server/actions/package-manager.d.ts +0 -6
  87. package/dist/server/actions/plugin-operations.d.ts +0 -6
  88. package/dist/server/actions/redis-monitor.d.ts +0 -12
  89. package/dist/server/actions/tasks.d.ts +0 -7
  90. package/dist/server/actions/workflow-executions.d.ts +0 -7
  91. package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
  92. package/dist/server/adapters/redis-node-registry.d.ts +0 -12
  93. package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
  94. package/dist/server/collections/app.d.ts +0 -8
  95. package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
  96. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
  97. package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
  98. package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
  99. package/dist/server/collections/cluster-manager-plugins.d.ts +0 -18
  100. package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
  101. package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
  102. package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
  103. package/dist/server/collections/cluster-manager.d.ts +0 -22
  104. package/dist/server/collections/orchestrator-settings.d.ts +0 -59
  105. package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
  106. package/dist/server/collections/worker-orchestrator.d.ts +0 -22
  107. package/dist/server/collections/worker-packages-configs.d.ts +0 -3
  108. package/dist/server/collections/worker-packages.d.ts +0 -22
  109. package/dist/server/orchestrator/PackageManager.d.ts +0 -39
  110. package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
  111. package/dist/server/orchestrator/index.d.ts +0 -4
  112. package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
  113. package/dist/server/orchestrator/leader-election.d.ts +0 -48
  114. package/dist/server/orchestrator/types.d.ts +0 -84
  115. package/dist/server/plugin.d.ts +0 -26
  116. package/dist/server/utils/node.d.ts +0 -6
  117. package/dist/server/utils/redis.d.ts +0 -29
  118. package/dist/shared/packages.d.ts +0 -23
  119. /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
@@ -41,7 +41,12 @@ async function getStack(ctx: Context, stackId?: number | string): Promise<StackC
41
41
  return stack.toJSON() as StackConfig;
42
42
  }
43
43
 
44
- async function assertManagedContainer(ctx: Context, adapter: IOrchestratorAdapter, stack: StackConfig, containerId: string) {
44
+ async function assertManagedContainer(
45
+ ctx: Context,
46
+ adapter: IOrchestratorAdapter,
47
+ stack: StackConfig,
48
+ containerId: string,
49
+ ) {
45
50
  try {
46
51
  await adapter.assertManagedByStack(stack, containerId);
47
52
  } catch (err: any) {
@@ -87,7 +92,9 @@ export const orchestratorActions = {
87
92
  if (rawStatus && typeof rawStatus === 'string') {
88
93
  (c as any).packageStatus = JSON.parse(rawStatus);
89
94
  }
90
- } catch {}
95
+ } catch {
96
+ // Redis may not be configured — skip package status
97
+ }
91
98
  }
92
99
  }
93
100
 
@@ -114,6 +121,10 @@ export const orchestratorActions = {
114
121
  * POST /workerOrchestrator:scale
115
122
  * Body: { stackId: 1, replicas: 3 }
116
123
  * Leader-only
124
+ *
125
+ * Before scaling, resolves queue-to-stack mappings and injects
126
+ * WORKER_MODE into the stack's envVars so new containers only
127
+ * process assigned queues.
117
128
  */
118
129
  async scale(ctx: Context, next: () => Promise<void>) {
119
130
  assertLeader(ctx);
@@ -124,6 +135,47 @@ export const orchestratorActions = {
124
135
  if (replicas < 0 || replicas > 20) ctx.throw(400, 'replicas must be between 0 and 20');
125
136
 
126
137
  const stack = await getStack(ctx, stackId);
138
+
139
+ // ── Resolve queue assignments for this stack ──
140
+ try {
141
+ const mappingsRepo = ctx.db.getRepository('workerQueueMappings');
142
+ if (mappingsRepo) {
143
+ const assigned = await mappingsRepo.find({
144
+ filter: {
145
+ stackId: stack.id,
146
+ enabled: true,
147
+ },
148
+ });
149
+
150
+ const queueNames = assigned.map((m: any) => m.get('queueName') as string).filter(Boolean);
151
+
152
+ if (queueNames.length > 0) {
153
+ const workerMode = queueNames.join(',');
154
+ ctx.app.logger.info(
155
+ `[Orchestrator] Injecting WORKER_MODE=${workerMode} for stack "${stack.name}" (${queueNames.length} queue(s) assigned)`,
156
+ );
157
+ // Merge into envVars; adapter code merges envVars over inherited env
158
+ stack.envVars = {
159
+ ...(stack.envVars || {}),
160
+ WORKER_MODE: workerMode,
161
+ };
162
+ } else {
163
+ // No specific assignment → default to all queues (backwards compatible)
164
+ stack.envVars = {
165
+ ...(stack.envVars || {}),
166
+ WORKER_MODE: '*',
167
+ };
168
+ }
169
+ }
170
+ } catch (err: any) {
171
+ // If workerQueueMappings table doesn't exist yet, fall back gracefully
172
+ ctx.app.logger.debug(`[Orchestrator] Queue mappings not available: ${err.message}`);
173
+ stack.envVars = {
174
+ ...(stack.envVars || {}),
175
+ WORKER_MODE: '*',
176
+ };
177
+ }
178
+
127
179
  const result = await adapter.scale(stack, Number(replicas));
128
180
 
129
181
  // Update DB
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Queue Mappings Actions
3
+ *
4
+ * CRUD for workerQueueMappings + scanQueues action that discovers
5
+ * all registered queues via QueueScanner.
6
+ */
7
+
8
+ import { Context } from '@nocobase/actions';
9
+ import { scanQueues } from '../queue-scanner';
10
+
11
+ export const queueMappingsActions = {
12
+ /**
13
+ * GET /workerQueueMappings:scanQueues
14
+ * Scans all registered queues (EventQueue + Redis) and merges with existing mappings.
15
+ */
16
+ async scanQueues(ctx: Context, next: () => Promise<void>) {
17
+ const result = await scanQueues(ctx.app);
18
+
19
+ // Load existing mappings from DB
20
+ const repo = ctx.db.getRepository('workerQueueMappings');
21
+ let existingMappings: any[] = [];
22
+ try {
23
+ existingMappings = await repo.find();
24
+ } catch {
25
+ // Table may not exist yet
26
+ }
27
+
28
+ const mappedNames = new Set(existingMappings.map((m) => m.get('queueName') as string));
29
+
30
+ ctx.body = {
31
+ discovered: result.queues,
32
+ total: result.total,
33
+ registered: existingMappings.map((m) => ({
34
+ id: m.get('id'),
35
+ queueName: m.get('queueName'),
36
+ label: m.get('label'),
37
+ stackId: m.get('stackId'),
38
+ enabled: m.get('enabled'),
39
+ type: m.get('type'),
40
+ })),
41
+ unmapped: result.queues
42
+ .filter((q) => !mappedNames.has(q.name))
43
+ .map((q) => ({
44
+ name: q.name,
45
+ type: q.type,
46
+ label: q.label,
47
+ description: q.description,
48
+ })),
49
+ };
50
+ await next();
51
+ },
52
+
53
+ /**
54
+ * POST /workerQueueMappings:autoMap
55
+ * Auto-create mappings for any discovered queues that don't have one yet.
56
+ * Body: { stackId?: number } — optional default stack for new mappings
57
+ */
58
+ async autoMap(ctx: Context, next: () => Promise<void>) {
59
+ const { stackId } = ctx.action.params.values || {};
60
+ const result = await scanQueues(ctx.app);
61
+ const repo = ctx.db.getRepository('workerQueueMappings');
62
+
63
+ let existingMappings: any[] = [];
64
+ try {
65
+ existingMappings = await repo.find();
66
+ } catch {
67
+ // Table may not exist yet
68
+ }
69
+
70
+ const mappedNames = new Set(existingMappings.map((m) => m.get('queueName') as string));
71
+ const created: string[] = [];
72
+
73
+ for (const q of result.queues) {
74
+ if (mappedNames.has(q.name)) continue;
75
+ await repo.create({
76
+ values: {
77
+ queueName: q.name,
78
+ label: q.label,
79
+ description: q.description,
80
+ type: q.type,
81
+ stackId: stackId || null,
82
+ enabled: true,
83
+ },
84
+ });
85
+ created.push(q.name);
86
+ }
87
+
88
+ ctx.body = {
89
+ created,
90
+ count: created.length,
91
+ };
92
+ await next();
93
+ },
94
+ };
@@ -0,0 +1,23 @@
1
+ import { CollectionOptions } from '@nocobase/database';
2
+
3
+ export default {
4
+ name: 'clusterManagerDoctorRuns',
5
+ title: 'Cluster Manager Doctor Runs',
6
+ fields: [
7
+ { name: 'id', type: 'bigInt', autoIncrement: true, primaryKey: true },
8
+ { name: 'runId', type: 'string', length: 64, unique: true, allowNull: false },
9
+ { name: 'status', type: 'string', length: 20, defaultValue: 'running', allowNull: false },
10
+ { name: 'durationMs', type: 'integer', defaultValue: 120000, allowNull: false },
11
+ { name: 'progress', type: 'integer', defaultValue: 0, allowNull: false },
12
+ { name: 'startedAt', type: 'date', allowNull: false },
13
+ { name: 'deadlineAt', type: 'date', allowNull: false },
14
+ { name: 'finishedAt', type: 'date', allowNull: true },
15
+ { name: 'finishReason', type: 'string', length: 40, allowNull: true },
16
+ { name: 'startedBy', type: 'string', length: 200, allowNull: true },
17
+ { name: 'summary', type: 'json', allowNull: true },
18
+ { name: 'report', type: 'json', allowNull: true },
19
+ { name: 'error', type: 'text', allowNull: true },
20
+ { name: 'createdAt', type: 'date' },
21
+ { name: 'updatedAt', type: 'date' },
22
+ ],
23
+ } as CollectionOptions;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * DUMMY COLLECTION
3
+ * Keeps the `clusterManagerDoctor` resourcer compatible with NocoBase
4
+ * workflow/ACL collection lookups. Diagnostic run data lives in
5
+ * `clusterManagerDoctorRuns`.
6
+ */
7
+ export default {
8
+ name: 'clusterManagerDoctor',
9
+ dumpRules: 'skip',
10
+ autoGenId: true,
11
+ createdAt: false,
12
+ updatedAt: false,
13
+ fields: [
14
+ {
15
+ name: 'name',
16
+ type: 'string',
17
+ },
18
+ ],
19
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Collection: workerQueueMappings
3
+ *
4
+ * Maps queue names to worker stacks (orchestratorStacks).
5
+ * When a stack has assigned queues, the orchestrator adapter sets
6
+ * WORKER_MODE=<comma-separated-queue-names> on new containers.
7
+ *
8
+ * If no mappings exist for a stack, WORKER_MODE=* is preserved
9
+ * (backwards compatibility).
10
+ */
11
+ export default {
12
+ name: 'workerQueueMappings',
13
+ autoGenId: true,
14
+ createdAt: true,
15
+ updatedAt: true,
16
+ fields: [
17
+ {
18
+ name: 'queueName',
19
+ type: 'string',
20
+ unique: true,
21
+ interface: 'input',
22
+ uiSchema: {
23
+ title: 'Queue Name',
24
+ 'x-component': 'Input',
25
+ required: true,
26
+ },
27
+ },
28
+ {
29
+ name: 'label',
30
+ type: 'string',
31
+ interface: 'input',
32
+ uiSchema: {
33
+ title: 'Label',
34
+ 'x-component': 'Input',
35
+ description: 'Human-readable display name',
36
+ },
37
+ },
38
+ {
39
+ name: 'description',
40
+ type: 'text',
41
+ interface: 'textarea',
42
+ uiSchema: {
43
+ title: 'Description',
44
+ 'x-component': 'Input.TextArea',
45
+ },
46
+ },
47
+ {
48
+ name: 'type',
49
+ type: 'string',
50
+ interface: 'select',
51
+ defaultValue: 'event-queue',
52
+ uiSchema: {
53
+ title: 'Source Type',
54
+ 'x-component': 'Select',
55
+ enum: [
56
+ { value: 'event-queue', label: 'EventQueue' },
57
+ { value: 'redis-list', label: 'Redis List' },
58
+ ],
59
+ },
60
+ },
61
+ {
62
+ name: 'stackId',
63
+ type: 'integer',
64
+ interface: 'select',
65
+ uiSchema: {
66
+ title: 'Assigned Stack',
67
+ 'x-component': 'Select',
68
+ 'x-component-props': {
69
+ allowClear: true,
70
+ placeholder: 'Unassigned (worker runs all queues)',
71
+ },
72
+ },
73
+ },
74
+ {
75
+ name: 'enabled',
76
+ type: 'boolean',
77
+ defaultValue: true,
78
+ interface: 'checkbox',
79
+ uiSchema: {
80
+ title: 'Enabled',
81
+ 'x-component': 'Checkbox',
82
+ },
83
+ },
84
+ ],
85
+ };
@@ -0,0 +1,58 @@
1
+ import { cacheVersionManager } from '../utils/versionManager';
2
+
3
+ export function registerCacheHooks(app: any) {
4
+ const db = app.db;
5
+
6
+ // 1. Collections & Fields changes (Metadata)
7
+ db.on('collections.afterSave', async () => {
8
+ await cacheVersionManager.incrementCollectionVersion(app);
9
+ });
10
+ db.on('collections.afterDestroy', async () => {
11
+ await cacheVersionManager.incrementCollectionVersion(app);
12
+ });
13
+ db.on('fields.afterSave', async () => {
14
+ await cacheVersionManager.incrementCollectionVersion(app);
15
+ });
16
+ db.on('fields.afterDestroy', async () => {
17
+ await cacheVersionManager.incrementCollectionVersion(app);
18
+ });
19
+
20
+ // 2. UI Schemas changes (Dynamic Layouts)
21
+ db.on('uiSchemas.afterSave', async () => {
22
+ await cacheVersionManager.incrementSchemaVersion(app);
23
+ });
24
+ db.on('uiSchemas.afterDestroy', async () => {
25
+ await cacheVersionManager.incrementSchemaVersion(app);
26
+ });
27
+
28
+ // 3. ACL, Roles & Scopes changes
29
+ const invalidateRole = async (model: any) => {
30
+ const roleName = model.get?.('roleName') || model.get?.('name');
31
+ if (roleName) {
32
+ await cacheVersionManager.incrementAclVersion(app, roleName);
33
+ } else {
34
+ await cacheVersionManager.incrementAllAclVersions(app);
35
+ }
36
+ };
37
+
38
+ db.on('roles.afterSave', invalidateRole);
39
+ db.on('roles.afterDestroy', invalidateRole);
40
+
41
+ db.on('rolesResources.afterSave', invalidateRole);
42
+ db.on('rolesResources.afterDestroy', invalidateRole);
43
+
44
+ db.on('rolesResourcesActions.afterSave', invalidateRole);
45
+ db.on('rolesResourcesActions.afterDestroy', invalidateRole);
46
+
47
+ db.on('rolesUsers.afterSave', invalidateRole);
48
+ db.on('rolesUsers.afterDestroy', invalidateRole);
49
+
50
+ db.on('scopes.afterSave', async () => {
51
+ await cacheVersionManager.incrementAllAclVersions(app);
52
+ });
53
+ db.on('scopes.afterDestroy', async () => {
54
+ await cacheVersionManager.incrementAllAclVersions(app);
55
+ });
56
+
57
+ app.logger.info('[ClusterManager] Cache invalidation hooks registered successfully');
58
+ }
@@ -0,0 +1,55 @@
1
+ import { Context } from '@nocobase/actions';
2
+ import { cacheVersionManager } from '../utils/versionManager';
3
+
4
+ const LIST_META_CACHE_TTL = 1000 * 60 * 10; // 10 minutes cache duration
5
+
6
+ function getErrorMessage(error: unknown) {
7
+ return error instanceof Error ? error.message : String(error);
8
+ }
9
+
10
+ export function createListMetaCacheMiddleware(app: any) {
11
+ return async function listMetaCacheMiddleware(ctx: Context, next: () => Promise<void>) {
12
+ const cache = app.cache;
13
+ // Skip caching if cache manager is not initialized or this is not collections:listMeta
14
+ if (!cache || ctx.action?.resourceName !== 'collections' || ctx.action?.actionName !== 'listMeta') {
15
+ return next();
16
+ }
17
+
18
+ const currentRole = ctx.state?.currentRole || 'anonymous';
19
+ const appName = ctx.headers['x-app'] || 'main';
20
+ const dataSource = ctx.headers['x-data-source'] || 'main';
21
+ const locale = ctx.headers['x-locale'] || ctx.headers['accept-language'] || 'en-US';
22
+ let cacheKey = '';
23
+ let version = 0;
24
+
25
+ try {
26
+ version = await cacheVersionManager.getCollectionVersion(app);
27
+ cacheKey = `nb:cache:${appName}:meta:v${version}:ds:${dataSource}:role:${currentRole}:lang:${locale}`;
28
+
29
+ // Try reading from NocoBase shared cache manager (Redis or Memory)
30
+ const cached = await cache.get(cacheKey);
31
+ if (cached !== undefined && cached !== null) {
32
+ ctx.body = typeof cached === 'string' ? JSON.parse(cached) : cached;
33
+ ctx.set?.('X-Cache', 'HIT');
34
+ ctx.set?.('X-Collection-Version', String(version));
35
+ return;
36
+ }
37
+ } catch (err) {
38
+ app.logger.warn(`[ClusterManager] listMeta cache read skipped: ${getErrorMessage(err)}`);
39
+ }
40
+
41
+ await next();
42
+
43
+ // If the response is valid, write to cache manager. This must not call next() again.
44
+ if (ctx.status === 200 && ctx.body && cacheKey) {
45
+ try {
46
+ const valueToCache = typeof ctx.body === 'string' ? ctx.body : JSON.stringify(ctx.body);
47
+ await cache.set(cacheKey, valueToCache, LIST_META_CACHE_TTL);
48
+ ctx.set?.('X-Cache', 'MISS');
49
+ ctx.set?.('X-Collection-Version', String(version));
50
+ } catch (err) {
51
+ app.logger.warn(`[ClusterManager] listMeta cache write skipped: ${getErrorMessage(err)}`);
52
+ }
53
+ }
54
+ };
55
+ }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { getRedisClient } from '../utils/redis';
3
+ import { getLocalNodeId, getLocalRole } from '../utils/node';
3
4
  import { promises as fsp } from 'fs';
4
5
  import path from 'path';
5
6
  import Application from '@nocobase/server';
@@ -66,15 +67,7 @@ function redactUrl(value: string): string {
66
67
  }
67
68
 
68
69
  function getCurrentRole(): Exclude<TargetRole, 'all'> {
69
- if (process.env.APP_ROLE === 'app' || process.env.APP_ROLE === 'worker' || process.env.APP_ROLE === 'sandbox') {
70
- return process.env.APP_ROLE;
71
- }
72
- if (process.env.SKILL_HUB_SANDBOX === 'true') {
73
- return 'sandbox';
74
- }
75
-
76
- const workerMode = process.env.WORKER_MODE || 'main';
77
- return workerMode === 'worker' || workerMode === 'task' || workerMode === '*' ? 'worker' : 'app';
70
+ return getLocalRole();
78
71
  }
79
72
 
80
73
  function formatCommand(command: string, args: string[]): string {
@@ -451,21 +444,24 @@ export class PackageManager {
451
444
  const redisClient = getRedisClient(this.app);
452
445
  const podName = process.env.POD_NAME || require('os').hostname();
453
446
  if (redisClient) {
454
- const key = `orchestrator:pkg-status:${podName}`;
455
- await redisClient.sendCommand([
456
- 'SET',
457
- key,
458
- JSON.stringify({
459
- initStatus,
460
- initProgressPercent,
461
- initProgressLog,
462
- lastInitAt: new Date(),
463
- lastInitLog: logs.join('\n'),
464
- ...extraValues,
465
- }),
466
- 'EX',
467
- '86400', // expire after 1 day
468
- ]);
447
+ const statusPayload = JSON.stringify({
448
+ initStatus,
449
+ initProgressPercent,
450
+ initProgressLog,
451
+ lastInitAt: new Date(),
452
+ lastInitLog: logs.join('\n'),
453
+ ...extraValues,
454
+ });
455
+ const keys = [`orchestrator:pkg-status:${podName}`, `cluster-manager:pkg-status:${getLocalNodeId(this.app)}`];
456
+ for (const key of keys) {
457
+ await redisClient.sendCommand([
458
+ 'SET',
459
+ key,
460
+ statusPayload,
461
+ 'EX',
462
+ '86400', // expire after 1 day
463
+ ]);
464
+ }
469
465
  }
470
466
 
471
467
  // We can also keep the global DB record as a fallback/historical record