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.
- package/dist/client/index.js +1 -1
- package/dist/client-v2/914.c0bce51908fd81d7.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +1 -1
- package/package.json +1 -1
- package/src/client/AclCacheManager.tsx +2 -2
- package/src/client/CacheMonitor.tsx +2 -2
- package/src/client/ClusterNodes.tsx +3 -3
- package/src/client/ContainerOrchestrator.tsx +2 -2
- package/src/client/Doctor.tsx +2 -2
- package/src/client/EventQueueMonitor.tsx +2 -2
- package/src/client/LockMonitor.tsx +2 -2
- package/src/client/NginxCacheManager.tsx +2 -2
- package/src/client/PackageInstaller.tsx +2 -2
- package/src/client/PluginOperations.tsx +2 -2
- package/src/client/QueueAssignment.tsx +2 -2
- package/src/client/RedisMonitor.tsx +3 -3
- package/src/client/TaskManager.tsx +2 -2
- package/src/client/WorkflowExecutions.tsx +2 -2
- package/src/client/utils.ts +1 -1
- package/src/server/actions/cluster-nodes.ts +2 -1
- package/src/server/actions/doctor.ts +10 -4
- package/src/server/adapters/redis-node-registry.ts +126 -131
- package/src/server/plugin.ts +3 -2
- package/dist/client-v2/914.5dc1105cf3ada6a6.js +0 -10
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
PlusOutlined,
|
|
37
37
|
EditOutlined,
|
|
38
38
|
} from '@ant-design/icons';
|
|
39
|
-
import {
|
|
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 =
|
|
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[]>>({});
|
package/src/client/Doctor.tsx
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
StopOutlined,
|
|
24
24
|
WarningOutlined,
|
|
25
25
|
} from '@ant-design/icons';
|
|
26
|
-
import {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 });
|
package/src/client/utils.ts
CHANGED
|
@@ -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: {
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
package/src/server/plugin.ts
CHANGED
|
@@ -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 {
|