mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
5a8722ff07
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
341 lines
9.1 KiB
TypeScript
341 lines
9.1 KiB
TypeScript
import type {
|
|
ApplyResult,
|
|
NodeItem,
|
|
NodeStatus,
|
|
OpenrestyStatus,
|
|
} from '@/lib/services/openflare';
|
|
|
|
export const WS_CONNECTED_LAST_SEEN = '__OPENFLARE_WS_CONNECTED__';
|
|
export const FLARED_WS_CONNECTED_LAST_SEEN =
|
|
'__OPENFLARE_FLARED_WS_CONNECTED__';
|
|
|
|
export type StatusTone = 'success' | 'warning' | 'danger' | 'info';
|
|
|
|
export type NodeMessageT = (
|
|
key: string,
|
|
values?: Record<string, string | number | Date>,
|
|
) => string;
|
|
|
|
export function isWSConnectedLastSeen(value: string | null | undefined) {
|
|
return (
|
|
value === WS_CONNECTED_LAST_SEEN || value === FLARED_WS_CONNECTED_LAST_SEEN
|
|
);
|
|
}
|
|
|
|
export function isMeaningfulTime(
|
|
value: string | null | undefined,
|
|
): value is string {
|
|
return (
|
|
Boolean(value) &&
|
|
!isWSConnectedLastSeen(value) &&
|
|
!String(value).startsWith('0001-01-01')
|
|
);
|
|
}
|
|
|
|
export function formatRelativeTime(value: string, t: NodeMessageT) {
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return value;
|
|
}
|
|
|
|
const diffMs = Date.now() - date.getTime();
|
|
const diffMinutes = Math.floor(diffMs / 60_000);
|
|
if (diffMinutes < 1) return t('relative.justNow');
|
|
if (diffMinutes < 60) return t('relative.minutesAgo', { count: diffMinutes });
|
|
|
|
const diffHours = Math.floor(diffMinutes / 60);
|
|
if (diffHours < 24) return t('relative.hoursAgo', { count: diffHours });
|
|
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
if (diffDays < 30) return t('relative.daysAgo', { count: diffDays });
|
|
|
|
return t('relative.monthsAgo', { count: Math.floor(diffDays / 30) });
|
|
}
|
|
|
|
export function getNodeStatusTone(status: NodeStatus): StatusTone {
|
|
if (status === 'online') return 'success';
|
|
if (status === 'pending') return 'warning';
|
|
return 'danger';
|
|
}
|
|
|
|
export function getNodeStatusLabel(status: NodeStatus, t: NodeMessageT) {
|
|
if (status === 'online') return t('status.online');
|
|
if (status === 'pending') return t('status.pending');
|
|
return t('status.offline');
|
|
}
|
|
|
|
export function getApplyTone(result: ApplyResult): StatusTone {
|
|
if (result === 'success') return 'success';
|
|
if (result === 'warning') return 'warning';
|
|
if (result === 'failed') return 'danger';
|
|
return 'warning';
|
|
}
|
|
|
|
export function getApplyLabel(result: ApplyResult, t: NodeMessageT) {
|
|
if (result === 'success') return t('apply.success');
|
|
if (result === 'warning') return t('apply.warning');
|
|
if (result === 'failed') return t('apply.failed');
|
|
return t('apply.none');
|
|
}
|
|
|
|
export function getOpenrestyStatusTone(status: OpenrestyStatus): StatusTone {
|
|
if (status === 'healthy') return 'success';
|
|
if (status === 'unhealthy') return 'danger';
|
|
return 'warning';
|
|
}
|
|
|
|
export function getOpenrestyStatusLabel(
|
|
status: OpenrestyStatus,
|
|
t: NodeMessageT,
|
|
) {
|
|
if (status === 'healthy') return t('health.healthy');
|
|
if (status === 'unhealthy') return t('health.unhealthy');
|
|
return t('health.unknown');
|
|
}
|
|
|
|
export function getRelayStatusTone(
|
|
status: string | null | undefined,
|
|
): StatusTone {
|
|
if (status === 'healthy') return 'success';
|
|
if (status === 'unhealthy') return 'danger';
|
|
return 'warning';
|
|
}
|
|
|
|
export function getRelayStatusLabel(
|
|
status: string | null | undefined,
|
|
t: NodeMessageT,
|
|
) {
|
|
if (status === 'healthy') return t('health.healthy');
|
|
if (status === 'unhealthy') return t('health.unhealthy');
|
|
return t('health.unknown');
|
|
}
|
|
|
|
export function getNodeTypeLabel(nodeType: NodeItem['node_type']) {
|
|
if (nodeType === 'tunnel_relay') return 'Relay';
|
|
if (nodeType === 'tunnel_client') return 'Tunnel';
|
|
return 'Edge';
|
|
}
|
|
|
|
export function getErrorMessage(error: unknown, fallback: string) {
|
|
return error instanceof Error ? error.message : fallback;
|
|
}
|
|
|
|
export function getServerUrl(value: string) {
|
|
return value.trim().replace(/\/+$/, '');
|
|
}
|
|
|
|
const relayInstallerScriptUrl =
|
|
'https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-relay.sh';
|
|
|
|
const flaredInstallerScriptUrl =
|
|
'https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-flared.sh';
|
|
|
|
export function getImageTag(version?: string): string {
|
|
if (!version) {
|
|
return 'latest';
|
|
}
|
|
const v = version.toLowerCase();
|
|
if (
|
|
v === 'dev' ||
|
|
v.includes('alpha') ||
|
|
v.includes('beta') ||
|
|
v.includes('rc')
|
|
) {
|
|
return 'beta';
|
|
}
|
|
if (v.startsWith('v')) {
|
|
return version;
|
|
}
|
|
return 'latest';
|
|
}
|
|
|
|
export function buildEdgeDockerInstallCommand(
|
|
serverUrl: string,
|
|
agentToken: string,
|
|
version?: string,
|
|
) {
|
|
const tag = getImageTag(version);
|
|
const image = `ghcr.io/rain-kl/openflare-agent:${tag}`;
|
|
|
|
return [
|
|
`docker pull ${image}`,
|
|
`docker rm -f openflare-agent 2>/dev/null || true`,
|
|
`docker run -d --name openflare-agent --restart unless-stopped \\`,
|
|
` -p 80:80 -p 443:443/tcp -p 443:443/udp \\`,
|
|
` -v openflare-agent-pages:/data/var/lib/openflare/pages \\`,
|
|
` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
|
|
` -e OPENFLARE_AGENT_TOKEN=${agentToken} \\`,
|
|
` ${image}`,
|
|
].join('\n');
|
|
}
|
|
|
|
export function buildRelayInstallCommand(
|
|
serverUrl: string,
|
|
discoveryToken: string,
|
|
) {
|
|
return [
|
|
`curl -fsSL ${relayInstallerScriptUrl} | bash -s -- \\`,
|
|
` --server-url ${serverUrl} \\`,
|
|
` --discovery-token ${discoveryToken}`,
|
|
].join('\n');
|
|
}
|
|
|
|
export function buildRelayDockerInstallCommand(
|
|
serverUrl: string,
|
|
discoveryToken: string,
|
|
version?: string,
|
|
) {
|
|
const tag = getImageTag(version);
|
|
const image = `ghcr.io/rain-kl/openflare-relay:${tag}`;
|
|
|
|
return [
|
|
`docker pull ${image}`,
|
|
`docker rm -f openflare-relay 2>/dev/null || true`,
|
|
`docker run -d --name openflare-relay --net host --restart unless-stopped \\`,
|
|
` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
|
|
` -e OPENFLARE_DISCOVERY_TOKEN=${discoveryToken} \\`,
|
|
` ${image}`,
|
|
].join('\n');
|
|
}
|
|
|
|
export function buildTunnelInstallCommand(
|
|
serverUrl: string,
|
|
tunnelToken: string,
|
|
) {
|
|
return [
|
|
`curl -fsSL ${flaredInstallerScriptUrl} | bash -s -- \\`,
|
|
` --server-url ${serverUrl} \\`,
|
|
` --tunnel-token ${tunnelToken}`,
|
|
].join('\n');
|
|
}
|
|
|
|
export function buildTunnelDockerInstallCommand(
|
|
serverUrl: string,
|
|
tunnelToken: string,
|
|
version?: string,
|
|
) {
|
|
const tag = getImageTag(version);
|
|
const image = `ghcr.io/rain-kl/openflared:${tag}`;
|
|
|
|
return [
|
|
`docker pull ${image}`,
|
|
`docker rm -f openflared 2>/dev/null || true`,
|
|
`docker run -d --name openflared --restart unless-stopped \\`,
|
|
` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
|
|
` -e OPENFLARE_TUNNEL_TOKEN=${tunnelToken} \\`,
|
|
` ${image}`,
|
|
].join('\n');
|
|
}
|
|
|
|
export function formatBytes(bytes?: number | null, decimals = 1) {
|
|
if (bytes === undefined || bytes === null || !Number.isFinite(bytes)) {
|
|
return '—';
|
|
}
|
|
if (bytes <= 0) {
|
|
return '0 B';
|
|
}
|
|
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const index = Math.min(
|
|
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
units.length - 1,
|
|
);
|
|
const value = bytes / 1024 ** index;
|
|
return `${value.toFixed(decimals)} ${units[index]}`;
|
|
}
|
|
|
|
export function formatPercent(value?: number | null) {
|
|
if (value === undefined || value === null || !Number.isFinite(value)) {
|
|
return '—';
|
|
}
|
|
return `${value.toFixed(1)}%`;
|
|
}
|
|
|
|
export function formatMetricCount(value?: number | null) {
|
|
if (value === undefined || value === null || !Number.isFinite(value)) {
|
|
return '—';
|
|
}
|
|
return value.toLocaleString('zh-CN');
|
|
}
|
|
|
|
export function formatBytesPerSecond(value?: number | null, windowSeconds = 1) {
|
|
if (value === undefined || value === null || !Number.isFinite(value)) {
|
|
return '—';
|
|
}
|
|
if (windowSeconds <= 0) {
|
|
return '—';
|
|
}
|
|
return `${formatBytes(value / windowSeconds)}/s`;
|
|
}
|
|
|
|
export function formatUsageRatio(used?: number | null, total?: number | null) {
|
|
if (!used || !total || total <= 0) {
|
|
return null;
|
|
}
|
|
return Math.max(0, Math.min(100, (used / total) * 100));
|
|
}
|
|
|
|
export function formatUptime(
|
|
seconds: number | null | undefined,
|
|
t: NodeMessageT,
|
|
) {
|
|
if (!seconds || seconds <= 0) {
|
|
return '—';
|
|
}
|
|
|
|
const days = Math.floor(seconds / 86400);
|
|
const hours = Math.floor((seconds % 86400) / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
|
|
if (days > 0) {
|
|
return t('uptime.daysHours', { days, hours });
|
|
}
|
|
if (hours > 0) {
|
|
return t('uptime.hoursMinutes', { hours, minutes });
|
|
}
|
|
return t('uptime.minutes', { minutes });
|
|
}
|
|
|
|
export function getHealthEventTone(event: {
|
|
status: string;
|
|
severity: string;
|
|
}): StatusTone {
|
|
if (event.status === 'resolved') {
|
|
return 'success';
|
|
}
|
|
if (event.severity === 'critical') {
|
|
return 'danger';
|
|
}
|
|
if (event.severity === 'warning') {
|
|
return 'warning';
|
|
}
|
|
return 'info';
|
|
}
|
|
|
|
export function getHealthEventLabel(event: { event_type: string }) {
|
|
return event.event_type.replaceAll('_', ' ');
|
|
}
|
|
|
|
export function getFlaredStatusLabel(node: NodeItem, t: NodeMessageT) {
|
|
if (isWSConnectedLastSeen(node.last_seen_at)) {
|
|
return t('status.wsConnected');
|
|
}
|
|
if (node.status === 'online') {
|
|
return t('status.running');
|
|
}
|
|
if (node.status === 'pending') {
|
|
return t('status.pending');
|
|
}
|
|
return t('status.offline');
|
|
}
|
|
|
|
export function getFlaredStatusTone(node: NodeItem): StatusTone {
|
|
if (isWSConnectedLastSeen(node.last_seen_at) || node.status === 'online') {
|
|
return 'success';
|
|
}
|
|
if (node.status === 'pending') {
|
|
return 'warning';
|
|
}
|
|
return 'danger';
|
|
}
|