plugin-cluster-manager 1.1.13 → 1.1.15

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.
@@ -36,7 +36,7 @@ import {
36
36
  PlusOutlined,
37
37
  EditOutlined,
38
38
  } from '@ant-design/icons';
39
- import { useAPIClient } from '@nocobase/client';
39
+ import { useApp } from '@nocobase/client-v2';
40
40
  import { useT } from './utils';
41
41
 
42
42
  const { Text, Title } = Typography;
@@ -212,7 +212,7 @@ function formValuesToStack(values: any) {
212
212
 
213
213
  export function ContainerOrchestrator() {
214
214
  const t = useT();
215
- const api = useAPIClient();
215
+ const api = useApp().apiClient;
216
216
  const [loading, setLoading] = useState(false);
217
217
  const [stacks, setStacks] = useState<StackInfo[]>([]);
218
218
  const [containers, setContainers] = useState<Record<number, ContainerInfo[]>>({});
@@ -23,7 +23,7 @@ import {
23
23
  StopOutlined,
24
24
  WarningOutlined,
25
25
  } from '@ant-design/icons';
26
- import { useAPIClient } from '@nocobase/client';
26
+ import { useApp } from '@nocobase/client-v2';
27
27
  import { useT } from './utils';
28
28
 
29
29
  const { Text } = Typography;
@@ -147,7 +147,7 @@ function countMissingPackages(packages?: { apt?: string[]; npm?: string[]; pytho
147
147
 
148
148
  export function Doctor() {
149
149
  const t = useT();
150
- const api = useAPIClient();
150
+ const api = useApp().apiClient;
151
151
  const [loading, setLoading] = useState(false);
152
152
  const [starting, setStarting] = useState(false);
153
153
  const [stopping, setStopping] = useState(false);
@@ -8,12 +8,12 @@ import {
8
8
  ApiOutlined,
9
9
  DatabaseOutlined,
10
10
  } from '@ant-design/icons';
11
- import { useAPIClient } from '@nocobase/client';
11
+ import { useApp } from '@nocobase/client-v2';
12
12
  import { useT } from './utils';
13
13
 
14
14
  export function EventQueueMonitor() {
15
15
  const t = useT();
16
- const api = useAPIClient();
16
+ const api = useApp().apiClient;
17
17
  const [loading, setLoading] = useState(false);
18
18
  const [stats, setStats] = useState<any>(null);
19
19
  const [autoRefresh, setAutoRefresh] = useState<number | null>(null);
@@ -5,12 +5,12 @@ import {
5
5
  LockOutlined,
6
6
  UnlockOutlined,
7
7
  } from '@ant-design/icons';
8
- import { useAPIClient } from '@nocobase/client';
8
+ import { useApp } from '@nocobase/client-v2';
9
9
  import { useT } from './utils';
10
10
 
11
11
  export function LockMonitor() {
12
12
  const t = useT();
13
- const api = useAPIClient();
13
+ const api = useApp().apiClient;
14
14
  const [loading, setLoading] = useState(false);
15
15
  const [info, setInfo] = useState<any>(null);
16
16
  const [locks, setLocks] = useState<any[]>([]);
@@ -22,7 +22,7 @@ import {
22
22
  CloseCircleOutlined,
23
23
  SendOutlined,
24
24
  } from '@ant-design/icons';
25
- import { useAPIClient } from '@nocobase/client';
25
+ import { useApp } from '@nocobase/client-v2';
26
26
  import { useT } from './utils';
27
27
 
28
28
  const { TextArea } = Input;
@@ -30,7 +30,7 @@ const { Title, Text } = Typography;
30
30
 
31
31
  export function NginxCacheManager() {
32
32
  const t = useT();
33
- const api = useAPIClient();
33
+ const api = useApp().apiClient;
34
34
  const [loading, setLoading] = useState(false);
35
35
  const [clearing, setClearing] = useState(false);
36
36
  const [status, setStatus] = useState<any>(null);
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } from 'react';
2
2
  import { Card, Form, Input, Button, Alert, Progress, Tag, Typography, Space, Divider, message, Select } from 'antd';
3
3
  import { CloudServerOutlined, SafetyOutlined, ReloadOutlined } from '@ant-design/icons';
4
- import { useAPIClient } from '@nocobase/client';
4
+ import { useApp } from '@nocobase/client-v2';
5
5
  import { useT } from './utils';
6
6
  import {
7
7
  DEFAULT_WORKER_PACKAGES,
@@ -37,7 +37,7 @@ function renderPackageTags(packages: string[], color?: string) {
37
37
 
38
38
  export const PackageInstaller: React.FC = () => {
39
39
  const t = useT();
40
- const api = useAPIClient();
40
+ const api = useApp().apiClient;
41
41
  const [form] = Form.useForm();
42
42
  const [loading, setLoading] = useState(false);
43
43
  const [saving, setSaving] = useState(false);
@@ -1,7 +1,7 @@
1
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { Alert, Button, Input, Popconfirm, Space, Spin, Table, Tag, Typography, message } from 'antd';
3
3
  import { DeleteOutlined, ExclamationCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons';
4
- import { useAPIClient } from '@nocobase/client';
4
+ import { useApp } from '@nocobase/client-v2';
5
5
  import { useT } from './utils';
6
6
 
7
7
  interface PluginRecord {
@@ -22,7 +22,7 @@ function getErrorMessage(err: any, fallback: string) {
22
22
 
23
23
  export function PluginOperations() {
24
24
  const t = useT();
25
- const api = useAPIClient();
25
+ const api = useApp().apiClient;
26
26
  const [loading, setLoading] = useState(false);
27
27
  const [actionKey, setActionKey] = useState<string | null>(null);
28
28
  const [search, setSearch] = useState('');
@@ -24,7 +24,7 @@ import {
24
24
  CloseCircleOutlined,
25
25
  MinusCircleOutlined,
26
26
  } from '@ant-design/icons';
27
- import { useAPIClient } from '@nocobase/client';
27
+ import { useApp } from '@nocobase/client-v2';
28
28
  import { useT } from './utils';
29
29
 
30
30
  const { Text } = Typography;
@@ -54,7 +54,7 @@ interface StackInfo {
54
54
 
55
55
  export function QueueAssignment() {
56
56
  const t = useT();
57
- const api = useAPIClient();
57
+ const api = useApp().apiClient;
58
58
  const [loading, setLoading] = useState(false);
59
59
  const [scanning, setScanning] = useState(false);
60
60
  const [discovered, setDiscovered] = useState<DiscoveredQueue[]>([]);
@@ -7,7 +7,7 @@ import {
7
7
  ThunderboltOutlined,
8
8
  FieldTimeOutlined,
9
9
  } from '@ant-design/icons';
10
- import { useAPIClient } from '@nocobase/client';
10
+ import { useApp } from '@nocobase/client-v2';
11
11
  import { formatBytes, formatUptime } from './utils';
12
12
 
13
13
  const { Text } = Typography;
@@ -40,7 +40,7 @@ interface RedisInfo {
40
40
 
41
41
 
42
42
  export function RedisMonitor() {
43
- const api = useAPIClient();
43
+ const api = useApp().apiClient;
44
44
  const [info, setInfo] = useState<RedisInfo | null>(null);
45
45
  const [channels, setChannels] = useState<Record<string, number>>({});
46
46
  const [totalChannels, setTotalChannels] = useState(0);
@@ -306,7 +306,7 @@ export function RedisMonitor() {
306
306
  }
307
307
 
308
308
  function SyncMessagesSection() {
309
- const api = useAPIClient();
309
+ const api = useApp().apiClient;
310
310
  const [data, setData] = useState<any>(null);
311
311
 
312
312
  useEffect(() => {
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useState, useCallback } from 'react';
2
2
  import { Table, Tag, Button, Progress, Space, Popconfirm, message, Select, Dropdown } from 'antd';
3
3
  import { ReloadOutlined, StopOutlined, RedoOutlined } from '@ant-design/icons';
4
- import { useAPIClient } from '@nocobase/client';
4
+ import { useApp } from '@nocobase/client-v2';
5
5
  import dayjs from 'dayjs';
6
6
 
7
7
  const STATUS_MAP: Record<number | string, { label: string; color: string }> = {
@@ -13,7 +13,7 @@ const STATUS_MAP: Record<number | string, { label: string; color: string }> = {
13
13
  };
14
14
 
15
15
  export function TaskManager() {
16
- const api = useAPIClient();
16
+ const api = useApp().apiClient;
17
17
  const [data, setData] = useState<any[]>([]);
18
18
  const [loading, setLoading] = useState(false);
19
19
  const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 });
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useState, useCallback } from 'react';
2
2
  import { Table, Tag, Button, Space, Popconfirm, message, Select, Dropdown } from 'antd';
3
3
  import { ReloadOutlined, StopOutlined, UnorderedListOutlined } from '@ant-design/icons';
4
- import { useAPIClient } from '@nocobase/client';
4
+ import { useApp } from '@nocobase/client-v2';
5
5
  import dayjs from 'dayjs';
6
6
 
7
7
  const EXEC_STATUS: Record<string, { label: string; color: string }> = {
@@ -26,7 +26,7 @@ const JOB_STATUS: Record<string, { label: string; color: string }> = {
26
26
  };
27
27
 
28
28
  export function WorkflowExecutions() {
29
- const api = useAPIClient();
29
+ const api = useApp().apiClient;
30
30
  const [data, setData] = useState<any[]>([]);
31
31
  const [loading, setLoading] = useState(false);
32
32
  const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 });
@@ -1,5 +1,5 @@
1
1
  import { useCallback } from 'react';
2
- import { useApp } from '@nocobase/client';
2
+ import { useApp } from '@nocobase/client-v2';
3
3
 
4
4
  const namespace = 'cluster-manager';
5
5
 
@@ -17,6 +17,7 @@ interface ClusterNodeRecord {
17
17
  hostname?: string;
18
18
  appVersion?: string;
19
19
  workerMode?: string;
20
+ appRole?: string;
20
21
  isSandbox?: boolean;
21
22
  status?: string;
22
23
  url?: string | null;
@@ -162,7 +163,7 @@ function getErrorMessage(error: unknown) {
162
163
  }
163
164
 
164
165
  function getNodeRole(node: ClusterNodeRecord): 'app' | 'worker' | 'sandbox' {
165
- return getNodeRoleFrom({ workerMode: node.workerMode, isSandbox: node.isSandbox });
166
+ return getNodeRoleFrom({ workerMode: node.workerMode, appRole: node.appRole, isSandbox: node.isSandbox });
166
167
  }
167
168
 
168
169
  function getReferenceVersion(nodes: ClusterNodeRecord[]) {
@@ -83,6 +83,7 @@ interface DoctorNodeRecord {
83
83
  hostname?: string;
84
84
  appVersion?: string;
85
85
  workerMode?: string;
86
+ appRole?: string;
86
87
  isSandbox?: boolean;
87
88
  status?: string;
88
89
  lastHeartbeatAt?: number;
@@ -281,8 +282,12 @@ function countPackages(packages: NormalizedPackages) {
281
282
  return packages.apt.length + packages.npm.length + packages.python.length;
282
283
  }
283
284
 
284
- function getNodeRole(node: { workerMode?: string; isSandbox?: boolean }): 'app' | 'worker' | 'sandbox' {
285
- return getNodeRoleFrom({ workerMode: node.workerMode, isSandbox: node.isSandbox });
285
+ function getNodeRole(node: {
286
+ workerMode?: string;
287
+ appRole?: string;
288
+ isSandbox?: boolean;
289
+ }): 'app' | 'worker' | 'sandbox' {
290
+ return getNodeRoleFrom({ workerMode: node.workerMode, appRole: node.appRole, isSandbox: node.isSandbox });
286
291
  }
287
292
 
288
293
  function getSafeEnv() {
@@ -535,11 +540,12 @@ export async function collectLocalDoctorSnapshot(
535
540
  options: DoctorSnapshotOptions = {},
536
541
  ): Promise<DoctorNodeSnapshot> {
537
542
  const workerMode = process.env.WORKER_MODE || 'main';
543
+ const appRole = process.env.APP_ROLE;
538
544
  const node = {
539
545
  hostname: os.hostname(),
540
546
  pid: process.pid,
541
547
  workerMode,
542
- role: getNodeRole({ workerMode, isSandbox: process.env.SKILL_HUB_SANDBOX === 'true' }),
548
+ role: getNodeRole({ workerMode, appRole, isSandbox: process.env.SKILL_HUB_SANDBOX === 'true' }),
543
549
  appVersion: process.env.NOCOBASE_VERSION || process.version,
544
550
  nodeVersion: process.version,
545
551
  platform: process.platform,
@@ -769,7 +775,7 @@ async function collectNodeSnapshots(
769
775
  hostname: node.hostname || 'unknown',
770
776
  pid: Number(node.pid || 0),
771
777
  workerMode,
772
- role: getNodeRole({ workerMode, isSandbox: node.isSandbox }),
778
+ role: getNodeRole({ workerMode, appRole: node.appRole, isSandbox: node.isSandbox }),
773
779
  appVersion: node.appVersion || '',
774
780
  nodeVersion: '',
775
781
  platform: '',
@@ -1,131 +1,126 @@
1
- import os from 'os';
2
- import { scanKeys, getRedisClient } from '../utils/redis';
3
- import { getLocalNodeId } from '../utils/node';
4
-
5
- export class RedisNodeRegistry {
6
- private timer: NodeJS.Timeout | null = null;
7
- private readonly ttlSecs = 30; // 30 seconds TTL
8
- private readonly intervalMs = 10000; // Heartbeat every 10 seconds
9
- private readonly keyPrefix = 'cluster-manager:nodes:';
10
-
11
- constructor(private app: any) {}
12
-
13
- public start() {
14
- if (this.timer) {
15
- clearInterval(this.timer);
16
- }
17
-
18
- // Initial heartbeat
19
- this.heartbeat();
20
-
21
- // Loop
22
- this.timer = setInterval(() => {
23
- this.heartbeat();
24
- }, this.intervalMs);
25
- }
26
-
27
- public stop() {
28
- if (this.timer) {
29
- clearInterval(this.timer);
30
- this.timer = null;
31
- }
32
- }
33
-
34
- private async heartbeat() {
35
- const redis = getRedisClient(this.app);
36
- if (!redis) return;
37
-
38
- // Unique identifier combining hostname, port, pid, mode, and appName to handle multiple workers on the same host
39
- const port = process.env.APP_PORT || 'unknown';
40
- const mode = process.env.WORKER_MODE || 'main';
41
- const appName = process.env.APP_NAME || this.app.name || 'main';
42
- const nodeId = getLocalNodeId(this.app);
43
- const key = `${this.keyPrefix}${nodeId}`;
44
-
45
- // Collect process-level metrics so any node can read another node's full info from Redis
46
- const mem = process.memoryUsage();
47
-
48
- const metadata = {
49
- id: nodeId,
50
- name: `${appName} (${os.hostname()})`,
51
- hostname: os.hostname(),
52
- appVersion: process.env.NOCOBASE_VERSION || process.version,
53
- workerMode: mode,
54
- isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
55
- pid: process.pid,
56
- url: process.env.APP_PUBLIC_URL || null,
57
- available: true,
58
- lastHeartbeatAt: Date.now(),
59
- status: 'online', // Implicitly online since it just reported
60
- // Full node details (replicated from the `current` action shape)
61
- // so that any node can serve the "current" endpoint for the APP node
62
- nodeDetails: {
63
- node: {
64
- hostname: os.hostname(),
65
- pid: process.pid,
66
- nodeVersion: process.version,
67
- platform: process.platform,
68
- arch: process.arch,
69
- uptime: process.uptime(),
70
- workerMode: mode,
71
- appPort: port,
72
- clusterMode: process.env.CLUSTER_MODE || '',
73
- },
74
- memory: {
75
- rss: mem.rss,
76
- heapUsed: mem.heapUsed,
77
- heapTotal: mem.heapTotal,
78
- external: mem.external,
79
- arrayBuffers: mem.arrayBuffers || 0,
80
- },
81
- os: {
82
- totalMemory: os.totalmem(),
83
- freeMemory: os.freemem(),
84
- cpuCount: os.cpus().length,
85
- loadAvg: os.loadavg(),
86
- },
87
- },
88
- };
89
-
90
- try {
91
- await redis.sendCommand([
92
- 'SET',
93
- key,
94
- JSON.stringify(metadata),
95
- 'EX',
96
- this.ttlSecs.toString(),
97
- ]);
98
- } catch (err: any) {
99
- this.app.logger.error(`[RedisNodeRegistry] Heartbeat failed: ${err.message}`);
100
- }
101
- }
102
-
103
- public async getNodes(): Promise<any[]> {
104
- const redis = getRedisClient(this.app);
105
- if (!redis) return [];
106
-
107
- try {
108
- const rawKeys = await scanKeys(redis, `${this.keyPrefix}*`);
109
- if (rawKeys.length === 0) return [];
110
-
111
- const values = await redis.sendCommand(['MGET', ...rawKeys]);
112
-
113
- const nodes: any[] = [];
114
- if (Array.isArray(values)) {
115
- for (const val of values) {
116
- if (val) {
117
- try {
118
- nodes.push(JSON.parse(val));
119
- } catch (e) {
120
- // bad JSON, ignore
121
- }
122
- }
123
- }
124
- }
125
- return nodes;
126
- } catch (err: any) {
127
- this.app.logger.error(`[RedisNodeRegistry] Error fetching nodes: ${err.message}`);
128
- return [];
129
- }
130
- }
131
- }
1
+ import os from 'os';
2
+ import { scanKeys, getRedisClient } from '../utils/redis';
3
+ import { getLocalNodeId } from '../utils/node';
4
+
5
+ export class RedisNodeRegistry {
6
+ private timer: NodeJS.Timeout | null = null;
7
+ private readonly ttlSecs = 30; // 30 seconds TTL
8
+ private readonly intervalMs = 10000; // Heartbeat every 10 seconds
9
+ private readonly keyPrefix = 'cluster-manager:nodes:';
10
+
11
+ constructor(private app: any) {}
12
+
13
+ public start() {
14
+ if (this.timer) {
15
+ clearInterval(this.timer);
16
+ }
17
+
18
+ // Initial heartbeat
19
+ this.heartbeat();
20
+
21
+ // Loop
22
+ this.timer = setInterval(() => {
23
+ this.heartbeat();
24
+ }, this.intervalMs);
25
+ }
26
+
27
+ public stop() {
28
+ if (this.timer) {
29
+ clearInterval(this.timer);
30
+ this.timer = null;
31
+ }
32
+ }
33
+
34
+ private async heartbeat() {
35
+ const redis = getRedisClient(this.app);
36
+ if (!redis) return;
37
+
38
+ // Unique identifier combining hostname, port, pid, mode, and appName to handle multiple workers on the same host
39
+ const port = process.env.APP_PORT || 'unknown';
40
+ const mode = process.env.WORKER_MODE || 'main';
41
+ const appName = process.env.APP_NAME || this.app.name || 'main';
42
+ const nodeId = getLocalNodeId(this.app);
43
+ const key = `${this.keyPrefix}${nodeId}`;
44
+
45
+ // Collect process-level metrics so any node can read another node's full info from Redis
46
+ const mem = process.memoryUsage();
47
+
48
+ const metadata = {
49
+ id: nodeId,
50
+ name: `${appName} (${os.hostname()})`,
51
+ hostname: os.hostname(),
52
+ appVersion: process.env.NOCOBASE_VERSION || process.version,
53
+ workerMode: mode,
54
+ appRole: process.env.APP_ROLE,
55
+ isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
56
+ pid: process.pid,
57
+ url: process.env.APP_PUBLIC_URL || null,
58
+ available: true,
59
+ lastHeartbeatAt: Date.now(),
60
+ status: 'online', // Implicitly online since it just reported
61
+ // Full node details (replicated from the `current` action shape)
62
+ // so that any node can serve the "current" endpoint for the APP node
63
+ nodeDetails: {
64
+ node: {
65
+ hostname: os.hostname(),
66
+ pid: process.pid,
67
+ nodeVersion: process.version,
68
+ platform: process.platform,
69
+ arch: process.arch,
70
+ uptime: process.uptime(),
71
+ workerMode: mode,
72
+ appPort: port,
73
+ clusterMode: process.env.CLUSTER_MODE || '',
74
+ },
75
+ memory: {
76
+ rss: mem.rss,
77
+ heapUsed: mem.heapUsed,
78
+ heapTotal: mem.heapTotal,
79
+ external: mem.external,
80
+ arrayBuffers: mem.arrayBuffers || 0,
81
+ },
82
+ os: {
83
+ totalMemory: os.totalmem(),
84
+ freeMemory: os.freemem(),
85
+ cpuCount: os.cpus().length,
86
+ loadAvg: os.loadavg(),
87
+ },
88
+ },
89
+ };
90
+
91
+ try {
92
+ await redis.sendCommand(['SET', key, JSON.stringify(metadata), 'EX', this.ttlSecs.toString()]);
93
+ } catch (err: any) {
94
+ this.app.logger.error(`[RedisNodeRegistry] Heartbeat failed: ${err.message}`);
95
+ }
96
+ }
97
+
98
+ public async getNodes(): Promise<any[]> {
99
+ const redis = getRedisClient(this.app);
100
+ if (!redis) return [];
101
+
102
+ try {
103
+ const rawKeys = await scanKeys(redis, `${this.keyPrefix}*`);
104
+ if (rawKeys.length === 0) return [];
105
+
106
+ const values = await redis.sendCommand(['MGET', ...rawKeys]);
107
+
108
+ const nodes: any[] = [];
109
+ if (Array.isArray(values)) {
110
+ for (const val of values) {
111
+ if (val) {
112
+ try {
113
+ nodes.push(JSON.parse(val));
114
+ } catch (e) {
115
+ // bad JSON, ignore
116
+ }
117
+ }
118
+ }
119
+ }
120
+ return nodes;
121
+ } catch (err: any) {
122
+ this.app.logger.error(`[RedisNodeRegistry] Error fetching nodes: ${err.message}`);
123
+ return [];
124
+ }
125
+ }
126
+ }
@@ -57,11 +57,12 @@ export class PluginClusterManagerServer extends Plugin {
57
57
  (this.app as any).on('afterStart', () => {
58
58
  this.nodeRegistry?.start();
59
59
 
60
- // Automatically install packages on boot for worker nodes
60
+ // Automatically install packages on boot for worker / sandbox nodes
61
61
  const isWorker =
62
62
  isWorkerMode(process.env.WORKER_MODE) ||
63
63
  process.env.APP_ROLE === 'worker' ||
64
- process.env.APP_ROLE === 'sandbox';
64
+ process.env.APP_ROLE === 'sandbox' ||
65
+ process.env.SKILL_HUB_SANDBOX === 'true';
65
66
  if (isWorker) {
66
67
  setTimeout(async () => {
67
68
  try {