plugin-cluster-manager 1.1.0 → 1.1.6
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.
- package/README.md +17 -5
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +5 -5
- package/dist/locale/en-US.json +28 -4
- package/dist/locale/vi-VN.json +28 -4
- package/dist/locale/zh-CN.json +28 -4
- package/dist/server/actions/event-queue-monitor.js +123 -1
- package/dist/server/actions/orchestrator.js +20 -1
- package/dist/server/actions/plugin-operations.js +171 -0
- package/dist/server/collections/app.js +38 -0
- package/dist/server/collections/cluster-manager-acl-cache.js +44 -0
- package/dist/server/collections/cluster-manager-cache-mgr.js +44 -0
- package/dist/server/collections/cluster-manager-cluster.js +44 -0
- package/dist/server/collections/cluster-manager-lock.js +44 -0
- package/dist/server/collections/cluster-manager-plugins.js +44 -0
- package/dist/server/collections/cluster-manager-queue.js +44 -0
- package/dist/server/collections/cluster-manager-redis.js +44 -0
- package/dist/server/collections/cluster-manager-workflow.js +44 -0
- package/dist/server/collections/cluster-manager.js +44 -0
- package/dist/server/collections/worker-orchestrator.js +44 -0
- package/dist/server/collections/worker-packages.js +44 -0
- package/dist/server/orchestrator/docker-adapter.js +19 -6
- package/dist/server/plugin.js +9 -3
- package/package.json +9 -4
- package/src/client/ClusterManagerLayout.tsx +16 -10
- package/src/client/ContainerOrchestrator.tsx +38 -3
- package/src/client/EventQueueMonitor.tsx +349 -202
- package/src/client/PluginOperations.tsx +226 -0
- package/src/locale/en-US.json +28 -4
- package/src/locale/vi-VN.json +28 -4
- package/src/locale/zh-CN.json +28 -4
- package/src/server/actions/event-queue-monitor.ts +234 -95
- package/src/server/actions/orchestrator.ts +21 -1
- package/src/server/actions/plugin-operations.ts +151 -0
- package/src/server/collections/app.ts +7 -0
- package/src/server/collections/cluster-manager-acl-cache.ts +23 -0
- package/src/server/collections/cluster-manager-cache-mgr.ts +23 -0
- package/src/server/collections/cluster-manager-cluster.ts +23 -0
- package/src/server/collections/cluster-manager-lock.ts +23 -0
- package/src/server/collections/cluster-manager-plugins.ts +19 -0
- package/src/server/collections/cluster-manager-queue.ts +23 -0
- package/src/server/collections/cluster-manager-redis.ts +23 -0
- package/src/server/collections/cluster-manager-workflow.ts +23 -0
- package/src/server/collections/cluster-manager.ts +23 -0
- package/src/server/collections/worker-orchestrator.ts +23 -0
- package/src/server/collections/worker-packages.ts +23 -0
- package/src/server/orchestrator/docker-adapter.ts +25 -9
- package/src/server/orchestrator/types.ts +3 -0
- package/src/server/plugin.ts +29 -21
- package/dist/client/AclCacheManager.d.ts +0 -2
- package/dist/client/CacheMonitor.d.ts +0 -2
- package/dist/client/ClusterManagerLayout.d.ts +0 -2
- package/dist/client/ClusterNodes.d.ts +0 -2
- package/dist/client/ContainerOrchestrator.d.ts +0 -2
- package/dist/client/EventQueueMonitor.d.ts +0 -2
- package/dist/client/LockMonitor.d.ts +0 -2
- package/dist/client/PackageInstaller.d.ts +0 -2
- package/dist/client/RedisMonitor.d.ts +0 -2
- package/dist/client/TaskManager.d.ts +0 -2
- package/dist/client/WorkflowExecutions.d.ts +0 -2
- package/dist/client/index.d.ts +0 -5
- package/dist/client/utils.d.ts +0 -12
- package/dist/index.d.ts +0 -2
- package/dist/server/actions/acl-cache.d.ts +0 -53
- package/dist/server/actions/cache-monitor.d.ts +0 -23
- package/dist/server/actions/cluster-nodes.d.ts +0 -49
- package/dist/server/actions/event-queue-monitor.d.ts +0 -13
- package/dist/server/actions/lock-monitor.d.ts +0 -19
- package/dist/server/actions/orchestrator.d.ts +0 -53
- package/dist/server/actions/package-manager.d.ts +0 -6
- package/dist/server/actions/redis-monitor.d.ts +0 -12
- package/dist/server/actions/tasks.d.ts +0 -7
- package/dist/server/actions/workflow-executions.d.ts +0 -7
- package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
- package/dist/server/adapters/redis-node-registry.d.ts +0 -12
- package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
- package/dist/server/collections/orchestrator-settings.d.ts +0 -59
- package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
- package/dist/server/collections/worker-packages-configs.d.ts +0 -3
- package/dist/server/index.d.ts +0 -1
- package/dist/server/orchestrator/PackageManager.d.ts +0 -37
- package/dist/server/orchestrator/docker-adapter.d.ts +0 -37
- package/dist/server/orchestrator/index.d.ts +0 -4
- package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
- package/dist/server/orchestrator/leader-election.d.ts +0 -48
- package/dist/server/orchestrator/types.d.ts +0 -79
- package/dist/server/plugin.d.ts +0 -26
- package/dist/server/utils/node.d.ts +0 -6
- package/dist/server/utils/redis.d.ts +0 -29
- package/dist/shared/packages.d.ts +0 -23
|
@@ -1,95 +1,234 @@
|
|
|
1
|
-
import { Context } from '@nocobase/actions';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
1
|
+
import { Context } from '@nocobase/actions';
|
|
2
|
+
import { scanKeys } from '../utils/redis';
|
|
3
|
+
|
|
4
|
+
const REDIS_QUEUE_CONNECTION = 'cluster-manager:queue-monitor';
|
|
5
|
+
const REDIS_QUEUE_PATTERNS = ['*:plugin-git-manager:review:queue', '*:plugin-build-guide-block:build:queue'];
|
|
6
|
+
|
|
7
|
+
function getQueueRedisUrl() {
|
|
8
|
+
return process.env.QUEUE_ADAPTER_REDIS_URL || process.env.REDIS_URL;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function getQueueRedis(ctx: Context) {
|
|
12
|
+
const url = getQueueRedisUrl();
|
|
13
|
+
const manager = (ctx.app as any).redisConnectionManager;
|
|
14
|
+
if (!url || !manager?.getConnectionSync) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return manager.getConnectionSync(REDIS_QUEUE_CONNECTION, { connectionString: url });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function knownRedisQueueKeys(ctx: Context) {
|
|
21
|
+
const appName = (ctx.app as any).name || process.env.APP_NAME || 'main';
|
|
22
|
+
return [`${appName}:plugin-git-manager:review:queue`, `${appName}:plugin-build-guide-block:build:queue`];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isKnownRedisQueueKey(key: string) {
|
|
26
|
+
return REDIS_QUEUE_PATTERNS.some((pattern) => {
|
|
27
|
+
const suffix = pattern.replace('*:', '');
|
|
28
|
+
return key === suffix || key.endsWith(`:${suffix}`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function describeRedisQueueKey(key: string) {
|
|
33
|
+
const parts = String(key).split(':');
|
|
34
|
+
const queue = parts[parts.length - 2] || key;
|
|
35
|
+
const plugin = parts[parts.length - 3] || 'unknown';
|
|
36
|
+
const appName = parts.slice(0, Math.max(1, parts.length - 3)).join(':') || 'main';
|
|
37
|
+
return {
|
|
38
|
+
appName,
|
|
39
|
+
plugin,
|
|
40
|
+
queue,
|
|
41
|
+
channel: `${plugin}.${queue}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getRedisQueues(ctx: Context) {
|
|
46
|
+
const redis = await getQueueRedis(ctx);
|
|
47
|
+
if (!redis) {
|
|
48
|
+
return {
|
|
49
|
+
connected: false,
|
|
50
|
+
urlConfigured: Boolean(getQueueRedisUrl()),
|
|
51
|
+
queues: [],
|
|
52
|
+
note: 'Redis queue connection is not configured',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const keys = new Set<string>(knownRedisQueueKeys(ctx));
|
|
57
|
+
for (const pattern of REDIS_QUEUE_PATTERNS) {
|
|
58
|
+
try {
|
|
59
|
+
const scanned = await scanKeys(redis, pattern, 200);
|
|
60
|
+
scanned.forEach((key) => keys.add(key));
|
|
61
|
+
} catch {
|
|
62
|
+
// Keep known keys even if SCAN is not available.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const queues = [];
|
|
67
|
+
for (const key of keys) {
|
|
68
|
+
if (!isKnownRedisQueueKey(key)) continue;
|
|
69
|
+
let pending = 0;
|
|
70
|
+
try {
|
|
71
|
+
pending = Number(await redis.sendCommand(['LLEN', key])) || 0;
|
|
72
|
+
} catch {
|
|
73
|
+
pending = 0;
|
|
74
|
+
}
|
|
75
|
+
queues.push({
|
|
76
|
+
source: 'redis',
|
|
77
|
+
key,
|
|
78
|
+
type: 'list',
|
|
79
|
+
pending,
|
|
80
|
+
...describeRedisQueueKey(key),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
connected: true,
|
|
86
|
+
urlConfigured: true,
|
|
87
|
+
queues,
|
|
88
|
+
totalPending: queues.reduce((sum, queue) => sum + (queue.pending || 0), 0),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseRedisQueueMessage(raw: string, key: string, index: number) {
|
|
93
|
+
let content: any = raw;
|
|
94
|
+
try {
|
|
95
|
+
content = JSON.parse(raw);
|
|
96
|
+
} catch {
|
|
97
|
+
// Keep the raw string for non-JSON messages.
|
|
98
|
+
}
|
|
99
|
+
const queuedAt = content?.queuedAt ? Date.parse(content.queuedAt) : null;
|
|
100
|
+
return {
|
|
101
|
+
id: `${key}:${index}`,
|
|
102
|
+
index,
|
|
103
|
+
content,
|
|
104
|
+
raw,
|
|
105
|
+
timestamp: Number.isFinite(queuedAt) ? queuedAt : null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const eventQueueActions = {
|
|
110
|
+
/**
|
|
111
|
+
* GET /clusterManagerQueue:stats
|
|
112
|
+
* Returns event queue statistics
|
|
113
|
+
*/
|
|
114
|
+
async stats(ctx: Context, next: () => Promise<void>) {
|
|
115
|
+
const eq = ctx.app.eventQueue;
|
|
116
|
+
if (!eq) {
|
|
117
|
+
ctx.throw(503, 'Event queue is not available');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const adapter = (eq as any).adapter;
|
|
121
|
+
const events = (eq as any).events as Map<string, any>;
|
|
122
|
+
const adapterName = adapter?.constructor?.name || 'unknown';
|
|
123
|
+
const connected = eq.isConnected();
|
|
124
|
+
|
|
125
|
+
const channels: any[] = [];
|
|
126
|
+
if (events) {
|
|
127
|
+
for (const [channel, opts] of events.entries()) {
|
|
128
|
+
const queueData: any = { channel };
|
|
129
|
+
queueData.concurrency = opts.concurrency || 1;
|
|
130
|
+
queueData.interval = opts.interval || 250;
|
|
131
|
+
|
|
132
|
+
// For MemoryEventQueueAdapter, we can peek at queue depth
|
|
133
|
+
if (adapter?.queues) {
|
|
134
|
+
const fullChannel = eq.getFullChannel(channel, opts.shared);
|
|
135
|
+
const queue = adapter.queues.get(fullChannel);
|
|
136
|
+
queueData.pending = queue?.length || 0;
|
|
137
|
+
} else {
|
|
138
|
+
queueData.pending = null; // Unknown for external adapters
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
channels.push(queueData);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const totalPending = channels.reduce((sum, c) => sum + (c.pending || 0), 0);
|
|
146
|
+
const redisQueues = await getRedisQueues(ctx);
|
|
147
|
+
|
|
148
|
+
ctx.body = {
|
|
149
|
+
adapter: adapterName,
|
|
150
|
+
connected,
|
|
151
|
+
totalChannels: channels.length,
|
|
152
|
+
totalPending,
|
|
153
|
+
redisQueues,
|
|
154
|
+
channels,
|
|
155
|
+
};
|
|
156
|
+
await next();
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* GET /clusterManagerQueue:messages
|
|
161
|
+
* List pending messages in a specific channel (memory adapter only)
|
|
162
|
+
*/
|
|
163
|
+
async messages(ctx: Context, next: () => Promise<void>) {
|
|
164
|
+
const eq = ctx.app.eventQueue;
|
|
165
|
+
if (!eq) {
|
|
166
|
+
ctx.throw(503, 'Event queue is not available');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { channel, key, source, page = 1, pageSize = 20 } = ctx.action.params;
|
|
170
|
+
if (source === 'redis') {
|
|
171
|
+
const redisKey = String(key || channel || '');
|
|
172
|
+
if (!redisKey || !isKnownRedisQueueKey(redisKey)) {
|
|
173
|
+
ctx.throw(400, 'Invalid Redis queue key');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const redis = await getQueueRedis(ctx);
|
|
177
|
+
if (!redis) {
|
|
178
|
+
ctx.body = {
|
|
179
|
+
data: [],
|
|
180
|
+
meta: { count: 0, page: 1, pageSize: 20, note: 'Redis queue connection is not configured' },
|
|
181
|
+
};
|
|
182
|
+
await next();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const currentPage = Number(page);
|
|
187
|
+
const currentPageSize = Number(pageSize);
|
|
188
|
+
const start = (currentPage - 1) * currentPageSize;
|
|
189
|
+
const end = start + currentPageSize - 1;
|
|
190
|
+
const count = Number(await redis.sendCommand(['LLEN', redisKey])) || 0;
|
|
191
|
+
const rows = (await redis.sendCommand(['LRANGE', redisKey, String(start), String(end)])) as string[];
|
|
192
|
+
|
|
193
|
+
ctx.body = {
|
|
194
|
+
data: rows.map((raw, offset) => parseRedisQueueMessage(raw, redisKey, start + offset)),
|
|
195
|
+
meta: { count, page: currentPage, pageSize: currentPageSize, source: 'redis', key: redisKey },
|
|
196
|
+
};
|
|
197
|
+
await next();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const adapter = (eq as any).adapter;
|
|
202
|
+
|
|
203
|
+
if (!adapter?.queues) {
|
|
204
|
+
ctx.body = {
|
|
205
|
+
data: [],
|
|
206
|
+
meta: { count: 0, page: 1, pageSize: 20, note: 'Message inspection not available for this adapter' },
|
|
207
|
+
};
|
|
208
|
+
await next();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const events = (eq as any).events as Map<string, any>;
|
|
213
|
+
let fullChannel = channel;
|
|
214
|
+
if (events?.has(channel)) {
|
|
215
|
+
fullChannel = eq.getFullChannel(channel, events.get(channel)?.shared);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const queue = adapter.queues.get(fullChannel) || [];
|
|
219
|
+
const start = (Number(page) - 1) * Number(pageSize);
|
|
220
|
+
const slice = queue.slice(start, start + Number(pageSize));
|
|
221
|
+
|
|
222
|
+
ctx.body = {
|
|
223
|
+
data: slice.map((msg: any) => ({
|
|
224
|
+
id: msg.id,
|
|
225
|
+
content: msg.content,
|
|
226
|
+
timestamp: msg.options?.timestamp,
|
|
227
|
+
retried: msg.options?.retried || 0,
|
|
228
|
+
maxRetries: msg.options?.maxRetries || 0,
|
|
229
|
+
})),
|
|
230
|
+
meta: { count: queue.length, page: Number(page), pageSize: Number(pageSize) },
|
|
231
|
+
};
|
|
232
|
+
await next();
|
|
233
|
+
},
|
|
234
|
+
};
|
|
@@ -13,7 +13,7 @@ import type { IOrchestratorAdapter, StackConfig } from '../orchestrator/types';
|
|
|
13
13
|
function getAdapter(ctx: Context): IOrchestratorAdapter {
|
|
14
14
|
const plugin = ctx.app.pm.get('plugin-cluster-manager') as any;
|
|
15
15
|
if (!plugin?.orchestrator) {
|
|
16
|
-
ctx.throw(503, 'Orchestrator adapter not configured. Configure it in
|
|
16
|
+
ctx.throw(503, 'Orchestrator adapter not configured. Configure it in Cluster Manager settings.');
|
|
17
17
|
}
|
|
18
18
|
return plugin.orchestrator;
|
|
19
19
|
}
|
|
@@ -251,4 +251,24 @@ export const orchestratorActions = {
|
|
|
251
251
|
}
|
|
252
252
|
await next();
|
|
253
253
|
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* GET /workerOrchestrator:networks
|
|
257
|
+
* List available networks (if supported by adapter)
|
|
258
|
+
*/
|
|
259
|
+
async networks(ctx: Context, next: () => Promise<void>) {
|
|
260
|
+
const adapter = getAdapter(ctx);
|
|
261
|
+
if (!adapter.listNetworks) {
|
|
262
|
+
ctx.body = { data: [] };
|
|
263
|
+
} else {
|
|
264
|
+
try {
|
|
265
|
+
const networks = await adapter.listNetworks();
|
|
266
|
+
ctx.body = { data: networks };
|
|
267
|
+
} catch (err: any) {
|
|
268
|
+
ctx.app.logger.warn(`Failed to list networks: ${err.message}`);
|
|
269
|
+
ctx.body = { data: [] };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
await next();
|
|
273
|
+
},
|
|
254
274
|
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Context } from '@nocobase/actions';
|
|
2
|
+
|
|
3
|
+
const PROTECTED_PLUGIN_NAMES = new Set(['nocobase', 'plugin-cluster-manager']);
|
|
4
|
+
const PROTECTED_PACKAGE_NAMES = new Set(['nocobase', '@nocobase/preset-nocobase', 'plugin-cluster-manager']);
|
|
5
|
+
|
|
6
|
+
function getPayload(ctx: Context) {
|
|
7
|
+
return (ctx.action.params.values || (ctx as any).request?.body?.values || (ctx as any).request?.body || {}) as any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function pluginDisplayName(plugin: any) {
|
|
11
|
+
return plugin.displayName || plugin.name || plugin.packageName;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isProtectedPlugin(plugin: any) {
|
|
15
|
+
return PROTECTED_PLUGIN_NAMES.has(plugin.name) || PROTECTED_PACKAGE_NAMES.has(plugin.packageName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function getApplicationPlugins(ctx: Context) {
|
|
19
|
+
const repo = ctx.db.getRepository('applicationPlugins');
|
|
20
|
+
const rows = await repo.find({ sort: ['name'] });
|
|
21
|
+
return rows.map((row: any) => row.toJSON());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function findPlugin(ctx: Context, requestedName?: string) {
|
|
25
|
+
const name = String(requestedName || '').trim();
|
|
26
|
+
if (!name) {
|
|
27
|
+
ctx.throw(400, 'Plugin name is required.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const plugins = await getApplicationPlugins(ctx);
|
|
31
|
+
const plugin = plugins.find((item: any) => item.name === name || item.packageName === name);
|
|
32
|
+
if (!plugin) {
|
|
33
|
+
ctx.throw(404, `Plugin "${name}" was not found in applicationPlugins.`);
|
|
34
|
+
}
|
|
35
|
+
return plugin;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getLoadedPluginInfo(ctx: Context, plugin: any, locale: string): Promise<any> {
|
|
39
|
+
const pm = (ctx.app as any).pm;
|
|
40
|
+
const instance = pm?.get?.(plugin.name) || pm?.get?.(plugin.packageName);
|
|
41
|
+
if (!instance) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return await instance.toJSON({ locale, withOutOpenFile: true });
|
|
47
|
+
} catch {
|
|
48
|
+
return {
|
|
49
|
+
name: instance.name,
|
|
50
|
+
packageName: instance.options?.packageName,
|
|
51
|
+
displayName: instance.options?.packageJson?.displayName,
|
|
52
|
+
description: instance.options?.packageJson?.description,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getPackageInfo(ctx: Context, plugin: any, locale: string): Promise<any> {
|
|
58
|
+
const loadedInfo = await getLoadedPluginInfo(ctx, plugin, locale);
|
|
59
|
+
if (loadedInfo.displayName || loadedInfo.description) {
|
|
60
|
+
return loadedInfo;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const pmCtor = (ctx.app as any).pm?.constructor as any;
|
|
65
|
+
const pkgJson = await pmCtor.getPackageJson(plugin.packageName);
|
|
66
|
+
return {
|
|
67
|
+
displayName: pkgJson?.[`displayName.${locale}`] || pkgJson?.displayName || plugin.name,
|
|
68
|
+
description: pkgJson?.[`description.${locale}`] || pkgJson?.description,
|
|
69
|
+
keywords: pkgJson?.keywords,
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const pluginOperationsActions = {
|
|
77
|
+
async list(ctx: Context, next: () => Promise<void>) {
|
|
78
|
+
const locale = (ctx as any).getCurrentLocale?.() || 'en-US';
|
|
79
|
+
const plugins = await getApplicationPlugins(ctx);
|
|
80
|
+
const data = await Promise.all(
|
|
81
|
+
plugins.map(async (plugin: any) => {
|
|
82
|
+
const info = await getPackageInfo(ctx, plugin, locale);
|
|
83
|
+
const loaded = Boolean((ctx.app as any).pm?.get?.(plugin.name) || (ctx.app as any).pm?.get?.(plugin.packageName));
|
|
84
|
+
return {
|
|
85
|
+
...plugin,
|
|
86
|
+
displayName: info.displayName || plugin.name,
|
|
87
|
+
description: info.description || '',
|
|
88
|
+
keywords: info.keywords || [],
|
|
89
|
+
loaded,
|
|
90
|
+
protected: isProtectedPlugin(plugin),
|
|
91
|
+
};
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
ctx.body = {
|
|
96
|
+
data,
|
|
97
|
+
meta: { count: data.length },
|
|
98
|
+
};
|
|
99
|
+
await next();
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async forceDisable(ctx: Context, next: () => Promise<void>) {
|
|
103
|
+
const payload = getPayload(ctx);
|
|
104
|
+
const plugin = await findPlugin(ctx, payload.name || payload.packageName || ctx.action.params.filterByTk);
|
|
105
|
+
if (isProtectedPlugin(plugin)) {
|
|
106
|
+
ctx.throw(400, `Plugin "${pluginDisplayName(plugin)}" is protected and cannot be disabled from Cluster Manager.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const repo = ctx.db.getRepository('applicationPlugins');
|
|
110
|
+
await repo.update({
|
|
111
|
+
filter: { name: plugin.name },
|
|
112
|
+
values: { enabled: false },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const instance = (ctx.app as any).pm?.get?.(plugin.name) || (ctx.app as any).pm?.get?.(plugin.packageName);
|
|
116
|
+
if (instance) {
|
|
117
|
+
instance.enabled = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
ctx.body = {
|
|
121
|
+
success: true,
|
|
122
|
+
name: plugin.name,
|
|
123
|
+
packageName: plugin.packageName,
|
|
124
|
+
restartRequired: true,
|
|
125
|
+
message: `Plugin "${pluginDisplayName(plugin)}" was force disabled. Restart or reload the app to fully unload it.`,
|
|
126
|
+
};
|
|
127
|
+
await next();
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async forceRemove(ctx: Context, next: () => Promise<void>) {
|
|
131
|
+
const payload = getPayload(ctx);
|
|
132
|
+
const plugin = await findPlugin(ctx, payload.name || payload.packageName || ctx.action.params.filterByTk);
|
|
133
|
+
if (isProtectedPlugin(plugin)) {
|
|
134
|
+
ctx.throw(400, `Plugin "${pluginDisplayName(plugin)}" is protected and cannot be removed from Cluster Manager.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const repo = ctx.db.getRepository('applicationPlugins');
|
|
138
|
+
await repo.destroy({
|
|
139
|
+
filter: { name: plugin.name },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
ctx.body = {
|
|
143
|
+
success: true,
|
|
144
|
+
name: plugin.name,
|
|
145
|
+
packageName: plugin.packageName,
|
|
146
|
+
restartRequired: true,
|
|
147
|
+
message: `Plugin "${pluginDisplayName(plugin)}" was force removed from the application registry. Package files were not deleted.`,
|
|
148
|
+
};
|
|
149
|
+
await next();
|
|
150
|
+
},
|
|
151
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* This collection is created to prevent NocoBase workflow/ACL from throwing the error:
|
|
4
|
+
* '[Workflow pre-action]: collection "clusterManagerAclCache" not found'
|
|
5
|
+
*
|
|
6
|
+
* Since 'clusterManagerAclCache' is registered as a resourcer in plugin.ts, NocoBase
|
|
7
|
+
* implicitly looks for a collection with the same name.
|
|
8
|
+
* We set dumpRules: 'skip' and avoid timestamps to ensure this collection
|
|
9
|
+
* is completely ignored during backups/migrations and doesn't pollute the actual DB.
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
name: 'clusterManagerAclCache',
|
|
13
|
+
dumpRules: 'skip',
|
|
14
|
+
autoGenId: true,
|
|
15
|
+
createdAt: false,
|
|
16
|
+
updatedAt: false,
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: 'name',
|
|
20
|
+
type: 'string',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* This collection is created to prevent NocoBase workflow/ACL from throwing the error:
|
|
4
|
+
* '[Workflow pre-action]: collection "clusterManagerCacheMgr" not found'
|
|
5
|
+
*
|
|
6
|
+
* Since 'clusterManagerCacheMgr' is registered as a resourcer in plugin.ts, NocoBase
|
|
7
|
+
* implicitly looks for a collection with the same name.
|
|
8
|
+
* We set dumpRules: 'skip' and avoid timestamps to ensure this collection
|
|
9
|
+
* is completely ignored during backups/migrations and doesn't pollute the actual DB.
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
name: 'clusterManagerCacheMgr',
|
|
13
|
+
dumpRules: 'skip',
|
|
14
|
+
autoGenId: true,
|
|
15
|
+
createdAt: false,
|
|
16
|
+
updatedAt: false,
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: 'name',
|
|
20
|
+
type: 'string',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* This collection is created to prevent NocoBase workflow/ACL from throwing the error:
|
|
4
|
+
* '[Workflow pre-action]: collection "clusterManagerCluster" not found'
|
|
5
|
+
*
|
|
6
|
+
* Since 'clusterManagerCluster' is registered as a resourcer in plugin.ts, NocoBase
|
|
7
|
+
* implicitly looks for a collection with the same name.
|
|
8
|
+
* We set dumpRules: 'skip' and avoid timestamps to ensure this collection
|
|
9
|
+
* is completely ignored during backups/migrations and doesn't pollute the actual DB.
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
name: 'clusterManagerCluster',
|
|
13
|
+
dumpRules: 'skip',
|
|
14
|
+
autoGenId: true,
|
|
15
|
+
createdAt: false,
|
|
16
|
+
updatedAt: false,
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: 'name',
|
|
20
|
+
type: 'string',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* This collection is created to prevent NocoBase workflow/ACL from throwing the error:
|
|
4
|
+
* '[Workflow pre-action]: collection "clusterManagerLock" not found'
|
|
5
|
+
*
|
|
6
|
+
* Since 'clusterManagerLock' is registered as a resourcer in plugin.ts, NocoBase
|
|
7
|
+
* implicitly looks for a collection with the same name.
|
|
8
|
+
* We set dumpRules: 'skip' and avoid timestamps to ensure this collection
|
|
9
|
+
* is completely ignored during backups/migrations and doesn't pollute the actual DB.
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
name: 'clusterManagerLock',
|
|
13
|
+
dumpRules: 'skip',
|
|
14
|
+
autoGenId: true,
|
|
15
|
+
createdAt: false,
|
|
16
|
+
updatedAt: false,
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: 'name',
|
|
20
|
+
type: 'string',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* Keeps the `clusterManagerPlugins` resourcer compatible with NocoBase
|
|
4
|
+
* workflow/ACL collection lookups. The actual plugin records live in
|
|
5
|
+
* the core `applicationPlugins` collection.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
name: 'clusterManagerPlugins',
|
|
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,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* This collection is created to prevent NocoBase workflow/ACL from throwing the error:
|
|
4
|
+
* '[Workflow pre-action]: collection "clusterManagerQueue" not found'
|
|
5
|
+
*
|
|
6
|
+
* Since 'clusterManagerQueue' is registered as a resourcer in plugin.ts, NocoBase
|
|
7
|
+
* implicitly looks for a collection with the same name.
|
|
8
|
+
* We set dumpRules: 'skip' and avoid timestamps to ensure this collection
|
|
9
|
+
* is completely ignored during backups/migrations and doesn't pollute the actual DB.
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
name: 'clusterManagerQueue',
|
|
13
|
+
dumpRules: 'skip',
|
|
14
|
+
autoGenId: true,
|
|
15
|
+
createdAt: false,
|
|
16
|
+
updatedAt: false,
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: 'name',
|
|
20
|
+
type: 'string',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|