mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 06:56:36 +08:00
[优化] 格式化
This commit is contained in:
@@ -64,9 +64,9 @@ export function requestNodeAgentUpdate(
|
||||
}
|
||||
|
||||
export function requestNodeForceSync(id: number) {
|
||||
return apiRequest<NodeItem>(`/nodes/${id}/force-sync`, {
|
||||
method: 'POST',
|
||||
});
|
||||
return apiRequest<NodeItem>(`/nodes/${id}/force-sync`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export function requestNodeOpenrestyRestart(id: number) {
|
||||
|
||||
@@ -404,7 +404,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
});
|
||||
setHealthEventCleanupModalOpen(false);
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['node-observability', nodeId] }),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['node-observability', nodeId],
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard', 'overview'] }),
|
||||
]);
|
||||
},
|
||||
@@ -869,9 +871,7 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
)}
|
||||
</AppCard>
|
||||
|
||||
<AppCard
|
||||
title="实时资源"
|
||||
>
|
||||
<AppCard title="实时资源">
|
||||
{observabilityQuery.isLoading ? (
|
||||
<LoadingState />
|
||||
) : observabilityQuery.isError ? (
|
||||
@@ -930,9 +930,7 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
)}
|
||||
</AppCard>
|
||||
|
||||
<AppCard
|
||||
title="网络流量"
|
||||
>
|
||||
<AppCard title="网络流量">
|
||||
{observabilityQuery.isLoading ? (
|
||||
<LoadingState />
|
||||
) : observabilityQuery.isError ? (
|
||||
@@ -1628,7 +1626,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
|
||||
{nodeInstallCommand ? (
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">脚本部署 (Linux / macOS)</p>
|
||||
<p className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">
|
||||
脚本部署 (Linux / macOS)
|
||||
</p>
|
||||
<CodeBlock className="whitespace-pre-wrap">
|
||||
{nodeInstallCommand}
|
||||
</CodeBlock>
|
||||
@@ -1637,7 +1637,9 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
|
||||
{nodeDockerInstallCommand ? (
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">Docker 部署</p>
|
||||
<p className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">
|
||||
Docker 部署
|
||||
</p>
|
||||
<CodeBlock className="whitespace-pre-wrap">
|
||||
{nodeDockerInstallCommand}
|
||||
</CodeBlock>
|
||||
@@ -1835,8 +1837,12 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm text-[var(--foreground-secondary)]">
|
||||
<p>该操作会删除当前节点已记录的全部健康事件,包括活动中与已恢复事件。</p>
|
||||
<p>这不会影响节点后续继续上报新的健康事件,但现有时间线与相关摘要会立即刷新。</p>
|
||||
<p>
|
||||
该操作会删除当前节点已记录的全部健康事件,包括活动中与已恢复事件。
|
||||
</p>
|
||||
<p>
|
||||
这不会影响节点后续继续上报新的健康事件,但现有时间线与相关摘要会立即刷新。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</AppModal>
|
||||
|
||||
@@ -151,8 +151,8 @@ function getRegionCenter(feature: GeoJsonFeature) {
|
||||
}
|
||||
|
||||
function buildRegionOptions() {
|
||||
const features = ((worldGeoJson as { features?: GeoJsonFeature[] }).features ??
|
||||
[]) as GeoJsonFeature[];
|
||||
const features = ((worldGeoJson as { features?: GeoJsonFeature[] })
|
||||
.features ?? []) as GeoJsonFeature[];
|
||||
const options = new Map<string, RegionOption>();
|
||||
|
||||
for (const feature of features) {
|
||||
@@ -186,7 +186,7 @@ function buildFormValues(node?: Partial<NodeItem> | null): NodeEditorValues {
|
||||
ip: node.ip ?? '',
|
||||
auto_update_enabled: node.auto_update_enabled ?? false,
|
||||
geo_manual_override: node.geo_manual_override ?? false,
|
||||
geo_region: node.geo_manual_override ? node.geo_name ?? '' : '',
|
||||
geo_region: node.geo_manual_override ? (node.geo_name ?? '') : '',
|
||||
geo_name: node.geo_name ?? '',
|
||||
geo_latitude:
|
||||
node.geo_latitude === undefined || node.geo_latitude === null
|
||||
|
||||
@@ -354,10 +354,10 @@ export function NodesPage() {
|
||||
{isWSConnectedLastSeen(node.last_seen_at)
|
||||
? 'WS 已连接'
|
||||
: isMeaningfulTime(node.last_seen_at)
|
||||
? `${formatRelativeTime(
|
||||
node.last_seen_at,
|
||||
)} · ${formatDateTime(node.last_seen_at)}`
|
||||
: '暂无'}
|
||||
? `${formatRelativeTime(
|
||||
node.last_seen_at,
|
||||
)} · ${formatDateTime(node.last_seen_at)}`
|
||||
: '暂无'}
|
||||
</td>
|
||||
<td className="px-3 py-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -1,188 +1,188 @@
|
||||
import type { ReleaseChannel } from '@/features/update/types';
|
||||
|
||||
export interface NodeItem {
|
||||
id: number;
|
||||
node_id: string;
|
||||
name: string;
|
||||
ip: string;
|
||||
geo_name: string;
|
||||
geo_latitude?: number | null;
|
||||
geo_longitude?: number | null;
|
||||
geo_manual_override: boolean;
|
||||
agent_token: string;
|
||||
auto_update_enabled: boolean;
|
||||
update_requested: boolean;
|
||||
update_channel: ReleaseChannel;
|
||||
update_tag: string;
|
||||
restart_openresty_requested: boolean;
|
||||
agent_version: string;
|
||||
nginx_version: string;
|
||||
openresty_status: 'healthy' | 'unhealthy' | 'unknown';
|
||||
openresty_message: string;
|
||||
status: 'online' | 'offline' | 'pending';
|
||||
current_version: string;
|
||||
last_seen_at: string;
|
||||
last_error: string;
|
||||
latest_apply_result: 'success' | 'warning' | 'failed' | '';
|
||||
latest_apply_message: string;
|
||||
latest_apply_checksum: string;
|
||||
latest_main_config_checksum: string;
|
||||
latest_route_config_checksum: string;
|
||||
latest_support_file_count: number;
|
||||
latest_apply_at?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface NodeBootstrapToken {
|
||||
discovery_token: string;
|
||||
}
|
||||
|
||||
export interface NodeMutationPayload {
|
||||
name: string;
|
||||
ip: string;
|
||||
auto_update_enabled: boolean;
|
||||
geo_name: string;
|
||||
geo_latitude?: number | null;
|
||||
geo_longitude?: number | null;
|
||||
geo_manual_override: boolean;
|
||||
}
|
||||
|
||||
export interface NodeAgentReleaseInfo {
|
||||
tag_name: string;
|
||||
body: string;
|
||||
html_url: string;
|
||||
published_at: string;
|
||||
current_version: string;
|
||||
has_update: boolean;
|
||||
channel: ReleaseChannel;
|
||||
prerelease: boolean;
|
||||
update_requested: boolean;
|
||||
requested_channel: ReleaseChannel;
|
||||
requested_tag: string;
|
||||
}
|
||||
|
||||
export interface NodeAgentUpdatePayload {
|
||||
channel?: ReleaseChannel;
|
||||
tag_name?: string;
|
||||
}
|
||||
|
||||
export interface NodeSystemProfile {
|
||||
hostname: string;
|
||||
os_name: string;
|
||||
os_version: string;
|
||||
kernel_version: string;
|
||||
architecture: string;
|
||||
cpu_model: string;
|
||||
cpu_cores: number;
|
||||
total_memory_bytes: number;
|
||||
total_disk_bytes: number;
|
||||
uptime_seconds: number;
|
||||
reported_at: string;
|
||||
}
|
||||
|
||||
export interface NodeMetricSnapshot {
|
||||
captured_at: string;
|
||||
cpu_usage_percent: number;
|
||||
memory_used_bytes: number;
|
||||
memory_total_bytes: number;
|
||||
storage_used_bytes: number;
|
||||
storage_total_bytes: number;
|
||||
disk_read_bytes: number;
|
||||
disk_write_bytes: number;
|
||||
network_rx_bytes: number;
|
||||
network_tx_bytes: number;
|
||||
openresty_rx_bytes: number;
|
||||
openresty_tx_bytes: number;
|
||||
openresty_connections: number;
|
||||
}
|
||||
|
||||
export interface NodeTrafficReport {
|
||||
window_started_at: string;
|
||||
window_ended_at: string;
|
||||
request_count: number;
|
||||
error_count: number;
|
||||
unique_visitor_count: number;
|
||||
status_codes_json: string;
|
||||
top_domains_json: string;
|
||||
source_countries_json: string;
|
||||
}
|
||||
|
||||
export interface NodeTrafficTrendPoint {
|
||||
bucket_started_at: string;
|
||||
request_count: number;
|
||||
error_count: number;
|
||||
unique_visitor_count: number;
|
||||
}
|
||||
|
||||
export interface NodeCapacityTrendPoint {
|
||||
bucket_started_at: string;
|
||||
average_cpu_usage_percent: number;
|
||||
average_memory_usage_percent: number;
|
||||
reported_nodes: number;
|
||||
}
|
||||
|
||||
export interface NodeNetworkTrendPoint {
|
||||
bucket_started_at: string;
|
||||
network_rx_bytes: number;
|
||||
network_tx_bytes: number;
|
||||
openresty_rx_bytes: number;
|
||||
openresty_tx_bytes: number;
|
||||
reported_nodes: number;
|
||||
}
|
||||
|
||||
export interface NodeDiskIOTrendPoint {
|
||||
bucket_started_at: string;
|
||||
disk_read_bytes: number;
|
||||
disk_write_bytes: number;
|
||||
reported_nodes: number;
|
||||
}
|
||||
|
||||
export interface NodeDistributionItem {
|
||||
key: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface NodeTrafficDistributions {
|
||||
status_codes: NodeDistributionItem[];
|
||||
top_domains: NodeDistributionItem[];
|
||||
source_countries: NodeDistributionItem[];
|
||||
}
|
||||
|
||||
export interface NodeTrafficSummary {
|
||||
window_started_at: string;
|
||||
window_ended_at: string;
|
||||
request_count: number;
|
||||
unique_visitor_count: number;
|
||||
error_count: number;
|
||||
estimated_qps: number;
|
||||
error_rate_percent: number;
|
||||
}
|
||||
|
||||
export interface NodeHealthSummary {
|
||||
active_alerts: number;
|
||||
critical_alerts: number;
|
||||
warning_alerts: number;
|
||||
info_alerts: number;
|
||||
resolved_alerts: number;
|
||||
has_capacity_risk: boolean;
|
||||
has_traffic_risk: boolean;
|
||||
has_runtime_risk: boolean;
|
||||
}
|
||||
|
||||
export interface NodeObservabilityAnalytics {
|
||||
traffic: NodeTrafficSummary;
|
||||
distributions: NodeTrafficDistributions;
|
||||
health: NodeHealthSummary;
|
||||
}
|
||||
|
||||
export interface NodeObservabilityTrends {
|
||||
traffic_24h: NodeTrafficTrendPoint[];
|
||||
capacity_24h: NodeCapacityTrendPoint[];
|
||||
network_24h: NodeNetworkTrendPoint[];
|
||||
disk_io_24h: NodeDiskIOTrendPoint[];
|
||||
}
|
||||
|
||||
import type { ReleaseChannel } from '@/features/update/types';
|
||||
|
||||
export interface NodeItem {
|
||||
id: number;
|
||||
node_id: string;
|
||||
name: string;
|
||||
ip: string;
|
||||
geo_name: string;
|
||||
geo_latitude?: number | null;
|
||||
geo_longitude?: number | null;
|
||||
geo_manual_override: boolean;
|
||||
agent_token: string;
|
||||
auto_update_enabled: boolean;
|
||||
update_requested: boolean;
|
||||
update_channel: ReleaseChannel;
|
||||
update_tag: string;
|
||||
restart_openresty_requested: boolean;
|
||||
agent_version: string;
|
||||
nginx_version: string;
|
||||
openresty_status: 'healthy' | 'unhealthy' | 'unknown';
|
||||
openresty_message: string;
|
||||
status: 'online' | 'offline' | 'pending';
|
||||
current_version: string;
|
||||
last_seen_at: string;
|
||||
last_error: string;
|
||||
latest_apply_result: 'success' | 'warning' | 'failed' | '';
|
||||
latest_apply_message: string;
|
||||
latest_apply_checksum: string;
|
||||
latest_main_config_checksum: string;
|
||||
latest_route_config_checksum: string;
|
||||
latest_support_file_count: number;
|
||||
latest_apply_at?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface NodeBootstrapToken {
|
||||
discovery_token: string;
|
||||
}
|
||||
|
||||
export interface NodeMutationPayload {
|
||||
name: string;
|
||||
ip: string;
|
||||
auto_update_enabled: boolean;
|
||||
geo_name: string;
|
||||
geo_latitude?: number | null;
|
||||
geo_longitude?: number | null;
|
||||
geo_manual_override: boolean;
|
||||
}
|
||||
|
||||
export interface NodeAgentReleaseInfo {
|
||||
tag_name: string;
|
||||
body: string;
|
||||
html_url: string;
|
||||
published_at: string;
|
||||
current_version: string;
|
||||
has_update: boolean;
|
||||
channel: ReleaseChannel;
|
||||
prerelease: boolean;
|
||||
update_requested: boolean;
|
||||
requested_channel: ReleaseChannel;
|
||||
requested_tag: string;
|
||||
}
|
||||
|
||||
export interface NodeAgentUpdatePayload {
|
||||
channel?: ReleaseChannel;
|
||||
tag_name?: string;
|
||||
}
|
||||
|
||||
export interface NodeSystemProfile {
|
||||
hostname: string;
|
||||
os_name: string;
|
||||
os_version: string;
|
||||
kernel_version: string;
|
||||
architecture: string;
|
||||
cpu_model: string;
|
||||
cpu_cores: number;
|
||||
total_memory_bytes: number;
|
||||
total_disk_bytes: number;
|
||||
uptime_seconds: number;
|
||||
reported_at: string;
|
||||
}
|
||||
|
||||
export interface NodeMetricSnapshot {
|
||||
captured_at: string;
|
||||
cpu_usage_percent: number;
|
||||
memory_used_bytes: number;
|
||||
memory_total_bytes: number;
|
||||
storage_used_bytes: number;
|
||||
storage_total_bytes: number;
|
||||
disk_read_bytes: number;
|
||||
disk_write_bytes: number;
|
||||
network_rx_bytes: number;
|
||||
network_tx_bytes: number;
|
||||
openresty_rx_bytes: number;
|
||||
openresty_tx_bytes: number;
|
||||
openresty_connections: number;
|
||||
}
|
||||
|
||||
export interface NodeTrafficReport {
|
||||
window_started_at: string;
|
||||
window_ended_at: string;
|
||||
request_count: number;
|
||||
error_count: number;
|
||||
unique_visitor_count: number;
|
||||
status_codes_json: string;
|
||||
top_domains_json: string;
|
||||
source_countries_json: string;
|
||||
}
|
||||
|
||||
export interface NodeTrafficTrendPoint {
|
||||
bucket_started_at: string;
|
||||
request_count: number;
|
||||
error_count: number;
|
||||
unique_visitor_count: number;
|
||||
}
|
||||
|
||||
export interface NodeCapacityTrendPoint {
|
||||
bucket_started_at: string;
|
||||
average_cpu_usage_percent: number;
|
||||
average_memory_usage_percent: number;
|
||||
reported_nodes: number;
|
||||
}
|
||||
|
||||
export interface NodeNetworkTrendPoint {
|
||||
bucket_started_at: string;
|
||||
network_rx_bytes: number;
|
||||
network_tx_bytes: number;
|
||||
openresty_rx_bytes: number;
|
||||
openresty_tx_bytes: number;
|
||||
reported_nodes: number;
|
||||
}
|
||||
|
||||
export interface NodeDiskIOTrendPoint {
|
||||
bucket_started_at: string;
|
||||
disk_read_bytes: number;
|
||||
disk_write_bytes: number;
|
||||
reported_nodes: number;
|
||||
}
|
||||
|
||||
export interface NodeDistributionItem {
|
||||
key: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface NodeTrafficDistributions {
|
||||
status_codes: NodeDistributionItem[];
|
||||
top_domains: NodeDistributionItem[];
|
||||
source_countries: NodeDistributionItem[];
|
||||
}
|
||||
|
||||
export interface NodeTrafficSummary {
|
||||
window_started_at: string;
|
||||
window_ended_at: string;
|
||||
request_count: number;
|
||||
unique_visitor_count: number;
|
||||
error_count: number;
|
||||
estimated_qps: number;
|
||||
error_rate_percent: number;
|
||||
}
|
||||
|
||||
export interface NodeHealthSummary {
|
||||
active_alerts: number;
|
||||
critical_alerts: number;
|
||||
warning_alerts: number;
|
||||
info_alerts: number;
|
||||
resolved_alerts: number;
|
||||
has_capacity_risk: boolean;
|
||||
has_traffic_risk: boolean;
|
||||
has_runtime_risk: boolean;
|
||||
}
|
||||
|
||||
export interface NodeObservabilityAnalytics {
|
||||
traffic: NodeTrafficSummary;
|
||||
distributions: NodeTrafficDistributions;
|
||||
health: NodeHealthSummary;
|
||||
}
|
||||
|
||||
export interface NodeObservabilityTrends {
|
||||
traffic_24h: NodeTrafficTrendPoint[];
|
||||
capacity_24h: NodeCapacityTrendPoint[];
|
||||
network_24h: NodeNetworkTrendPoint[];
|
||||
disk_io_24h: NodeDiskIOTrendPoint[];
|
||||
}
|
||||
|
||||
export interface NodeHealthEvent {
|
||||
event_type: string;
|
||||
severity: string;
|
||||
@@ -194,13 +194,13 @@ export interface NodeHealthEvent {
|
||||
reported_at: string;
|
||||
resolved_at?: string | null;
|
||||
}
|
||||
|
||||
export interface NodeObservability {
|
||||
node_id: string;
|
||||
profile: NodeSystemProfile | null;
|
||||
metric_snapshots: NodeMetricSnapshot[];
|
||||
traffic_reports: NodeTrafficReport[];
|
||||
health_events: NodeHealthEvent[];
|
||||
analytics: NodeObservabilityAnalytics;
|
||||
trends: NodeObservabilityTrends;
|
||||
}
|
||||
|
||||
export interface NodeObservability {
|
||||
node_id: string;
|
||||
profile: NodeSystemProfile | null;
|
||||
metric_snapshots: NodeMetricSnapshot[];
|
||||
traffic_reports: NodeTrafficReport[];
|
||||
health_events: NodeHealthEvent[];
|
||||
analytics: NodeObservabilityAnalytics;
|
||||
trends: NodeObservabilityTrends;
|
||||
}
|
||||
|
||||
@@ -1,189 +1,194 @@
|
||||
import type { NodeItem } from '@/features/nodes/types';
|
||||
|
||||
export const WS_CONNECTED_LAST_SEEN = '__OPENFLARE_WS_CONNECTED__';
|
||||
|
||||
export function isWSConnectedLastSeen(value: string | null | undefined) {
|
||||
return value === WS_CONNECTED_LAST_SEEN;
|
||||
}
|
||||
|
||||
export function isMeaningfulTime(value: string | null | undefined) {
|
||||
return (
|
||||
Boolean(value) &&
|
||||
!isWSConnectedLastSeen(value) &&
|
||||
!String(value).startsWith('0001-01-01')
|
||||
);
|
||||
}
|
||||
|
||||
export function getNodeStatusVariant(status: NodeItem['status']) {
|
||||
if (status === 'online') {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (status === 'pending') {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
export function getNodeStatusLabel(status: NodeItem['status']) {
|
||||
if (status === 'online') {
|
||||
return '在线';
|
||||
}
|
||||
|
||||
if (status === 'pending') {
|
||||
return '待接入';
|
||||
}
|
||||
|
||||
return '离线';
|
||||
}
|
||||
|
||||
export function getApplyVariant(result: NodeItem['latest_apply_result']) {
|
||||
if (result === 'success') {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (result === 'warning') {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
if (result === 'failed') {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
export function getApplyLabel(result: NodeItem['latest_apply_result']) {
|
||||
if (result === 'success') {
|
||||
return '成功';
|
||||
}
|
||||
|
||||
if (result === 'warning') {
|
||||
return '警告';
|
||||
}
|
||||
|
||||
if (result === 'failed') {
|
||||
return '失败';
|
||||
}
|
||||
|
||||
return '暂无';
|
||||
}
|
||||
|
||||
export function getUpdateMode(node: NodeItem) {
|
||||
if (node.update_requested) {
|
||||
if (node.update_channel === 'preview') {
|
||||
return { label: '等待预览更新', variant: 'warning' as const };
|
||||
}
|
||||
|
||||
return { label: '等待更新', variant: 'warning' as const };
|
||||
}
|
||||
|
||||
if (node.auto_update_enabled) {
|
||||
return { label: '自动', variant: 'success' as const };
|
||||
}
|
||||
|
||||
return { label: '手动', variant: 'info' as const };
|
||||
}
|
||||
|
||||
export function getOpenrestyStatusVariant(status: NodeItem['openresty_status']) {
|
||||
if (status === 'healthy') {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (status === 'unhealthy') {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
export function getOpenrestyStatusLabel(status: NodeItem['openresty_status']) {
|
||||
if (status === 'healthy') {
|
||||
return '健康';
|
||||
}
|
||||
|
||||
if (status === 'unhealthy') {
|
||||
return '异常';
|
||||
}
|
||||
|
||||
return '未知';
|
||||
}
|
||||
|
||||
function parseVersionParts(version: string) {
|
||||
const normalized = version.trim().replace(/^v/i, '');
|
||||
if (!normalized || normalized.toLowerCase() === 'unknown') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized.split('.').map((segment) => {
|
||||
const matched = segment.trim().match(/^\d+/);
|
||||
return matched ? Number.parseInt(matched[0], 10) : 0;
|
||||
});
|
||||
}
|
||||
|
||||
function isOlderVersion(current: string, target: string) {
|
||||
const currentParts = parseVersionParts(current);
|
||||
const targetParts = parseVersionParts(target);
|
||||
if (!currentParts || !targetParts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxLength = Math.max(currentParts.length, targetParts.length);
|
||||
for (let index = 0; index < maxLength; index += 1) {
|
||||
const currentPart = currentParts[index] ?? 0;
|
||||
const targetPart = targetParts[index] ?? 0;
|
||||
if (currentPart < targetPart) {
|
||||
return true;
|
||||
}
|
||||
if (currentPart > targetPart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldShowManualUpdate(
|
||||
agentVersion: string,
|
||||
serverVersion: string,
|
||||
) {
|
||||
const normalizedServerVersion = serverVersion.trim();
|
||||
const normalizedAgentVersion = agentVersion.trim();
|
||||
|
||||
if (
|
||||
!normalizedServerVersion ||
|
||||
normalizedServerVersion.toLowerCase() === 'dev' ||
|
||||
!normalizedAgentVersion ||
|
||||
normalizedAgentVersion.toLowerCase() === 'unknown'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isOlderVersion(normalizedAgentVersion, normalizedServerVersion);
|
||||
}
|
||||
|
||||
export function getServerUrl(value: string) {
|
||||
return value.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
const installerScriptUrl =
|
||||
'https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh';
|
||||
|
||||
export function buildNodeInstallCommand(serverUrl: string, agentToken: string) {
|
||||
return [
|
||||
`curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
|
||||
` --server-url ${serverUrl} \\`,
|
||||
` --agent-token ${agentToken}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function buildNodeDockerInstallCommand(serverUrl: string, agentToken: string) {
|
||||
return [
|
||||
`docker run -d --name openflare-agent --restart unless-stopped \\`,
|
||||
` -p 80:80 -p 443:443 \\`,
|
||||
` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
|
||||
` -e OPENFLARE_AGENT_TOKEN=${agentToken} \\`,
|
||||
` ghcr.io/rain-kl/openflare-agent:latest`
|
||||
].join('\n');
|
||||
}
|
||||
import type { NodeItem } from '@/features/nodes/types';
|
||||
|
||||
export const WS_CONNECTED_LAST_SEEN = '__OPENFLARE_WS_CONNECTED__';
|
||||
|
||||
export function isWSConnectedLastSeen(value: string | null | undefined) {
|
||||
return value === WS_CONNECTED_LAST_SEEN;
|
||||
}
|
||||
|
||||
export function isMeaningfulTime(value: string | null | undefined) {
|
||||
return (
|
||||
Boolean(value) &&
|
||||
!isWSConnectedLastSeen(value) &&
|
||||
!String(value).startsWith('0001-01-01')
|
||||
);
|
||||
}
|
||||
|
||||
export function getNodeStatusVariant(status: NodeItem['status']) {
|
||||
if (status === 'online') {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (status === 'pending') {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
export function getNodeStatusLabel(status: NodeItem['status']) {
|
||||
if (status === 'online') {
|
||||
return '在线';
|
||||
}
|
||||
|
||||
if (status === 'pending') {
|
||||
return '待接入';
|
||||
}
|
||||
|
||||
return '离线';
|
||||
}
|
||||
|
||||
export function getApplyVariant(result: NodeItem['latest_apply_result']) {
|
||||
if (result === 'success') {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (result === 'warning') {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
if (result === 'failed') {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
export function getApplyLabel(result: NodeItem['latest_apply_result']) {
|
||||
if (result === 'success') {
|
||||
return '成功';
|
||||
}
|
||||
|
||||
if (result === 'warning') {
|
||||
return '警告';
|
||||
}
|
||||
|
||||
if (result === 'failed') {
|
||||
return '失败';
|
||||
}
|
||||
|
||||
return '暂无';
|
||||
}
|
||||
|
||||
export function getUpdateMode(node: NodeItem) {
|
||||
if (node.update_requested) {
|
||||
if (node.update_channel === 'preview') {
|
||||
return { label: '等待预览更新', variant: 'warning' as const };
|
||||
}
|
||||
|
||||
return { label: '等待更新', variant: 'warning' as const };
|
||||
}
|
||||
|
||||
if (node.auto_update_enabled) {
|
||||
return { label: '自动', variant: 'success' as const };
|
||||
}
|
||||
|
||||
return { label: '手动', variant: 'info' as const };
|
||||
}
|
||||
|
||||
export function getOpenrestyStatusVariant(
|
||||
status: NodeItem['openresty_status'],
|
||||
) {
|
||||
if (status === 'healthy') {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (status === 'unhealthy') {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
export function getOpenrestyStatusLabel(status: NodeItem['openresty_status']) {
|
||||
if (status === 'healthy') {
|
||||
return '健康';
|
||||
}
|
||||
|
||||
if (status === 'unhealthy') {
|
||||
return '异常';
|
||||
}
|
||||
|
||||
return '未知';
|
||||
}
|
||||
|
||||
function parseVersionParts(version: string) {
|
||||
const normalized = version.trim().replace(/^v/i, '');
|
||||
if (!normalized || normalized.toLowerCase() === 'unknown') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized.split('.').map((segment) => {
|
||||
const matched = segment.trim().match(/^\d+/);
|
||||
return matched ? Number.parseInt(matched[0], 10) : 0;
|
||||
});
|
||||
}
|
||||
|
||||
function isOlderVersion(current: string, target: string) {
|
||||
const currentParts = parseVersionParts(current);
|
||||
const targetParts = parseVersionParts(target);
|
||||
if (!currentParts || !targetParts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxLength = Math.max(currentParts.length, targetParts.length);
|
||||
for (let index = 0; index < maxLength; index += 1) {
|
||||
const currentPart = currentParts[index] ?? 0;
|
||||
const targetPart = targetParts[index] ?? 0;
|
||||
if (currentPart < targetPart) {
|
||||
return true;
|
||||
}
|
||||
if (currentPart > targetPart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldShowManualUpdate(
|
||||
agentVersion: string,
|
||||
serverVersion: string,
|
||||
) {
|
||||
const normalizedServerVersion = serverVersion.trim();
|
||||
const normalizedAgentVersion = agentVersion.trim();
|
||||
|
||||
if (
|
||||
!normalizedServerVersion ||
|
||||
normalizedServerVersion.toLowerCase() === 'dev' ||
|
||||
!normalizedAgentVersion ||
|
||||
normalizedAgentVersion.toLowerCase() === 'unknown'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isOlderVersion(normalizedAgentVersion, normalizedServerVersion);
|
||||
}
|
||||
|
||||
export function getServerUrl(value: string) {
|
||||
return value.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
const installerScriptUrl =
|
||||
'https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh';
|
||||
|
||||
export function buildNodeInstallCommand(serverUrl: string, agentToken: string) {
|
||||
return [
|
||||
`curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
|
||||
` --server-url ${serverUrl} \\`,
|
||||
` --agent-token ${agentToken}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function buildNodeDockerInstallCommand(
|
||||
serverUrl: string,
|
||||
agentToken: string,
|
||||
) {
|
||||
return [
|
||||
`docker run -d --name openflare-agent --restart unless-stopped \\`,
|
||||
` -p 80:80 -p 443:443 \\`,
|
||||
` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
|
||||
` -e OPENFLARE_AGENT_TOKEN=${agentToken} \\`,
|
||||
` ghcr.io/rain-kl/openflare-agent:latest`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user