[优化] 添加服务器升级日志的WebSocket流

This commit is contained in:
ryan
2026-03-15 11:57:52 +08:00
parent ae70ba1cce
commit eb9a2a8814
9 changed files with 255 additions and 5 deletions
@@ -8,14 +8,18 @@ import { useAuth } from '@/components/providers/auth-provider';
import { ThemeToggle } from '@/components/ui/theme-toggle';
import { getPublicStatus } from '@/features/auth/api/public';
import {
createUpgradeLogsWebSocket,
confirmManualServerUpgrade,
getLatestRelease,
parseUpgradeStreamSnapshot,
upgradeServer,
uploadServerBinary,
} from '@/features/update/api/update';
import { VersionUpgradeModal } from '@/features/update/components/version-upgrade-modal';
import type {
LatestReleaseInfo,
ReleaseChannel,
UpgradeStreamSnapshot,
UploadedServerBinaryInfo,
} from '@/features/update/types';
import { publicEnv } from '@/lib/env/public-env';
@@ -46,6 +50,8 @@ export function DashboardTopbar() {
const [uploadedBinary, setUploadedBinary] =
useState<UploadedServerBinaryInfo | null>(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [upgradeStream, setUpgradeStream] =
useState<UpgradeStreamSnapshot | null>(null);
const menuRef = useRef<HTMLDivElement | null>(null);
const isRoot = (user?.role ?? 0) >= 100;
const upgradeStatusPollInterval = 3000;
@@ -146,6 +152,51 @@ export function DashboardTopbar() {
},
});
useEffect(() => {
if (!isVersionModalOpen || !isRoot) {
setUpgradeStream(null);
return;
}
let closed = false;
let reconnectTimer: number | null = null;
let socket: WebSocket | null = null;
const connect = () => {
if (closed) {
return;
}
socket = createUpgradeLogsWebSocket();
if (!socket) {
return;
}
socket.onmessage = (event) => {
const snapshot = parseUpgradeStreamSnapshot(String(event.data));
if (snapshot) {
setUpgradeStream(snapshot);
}
};
socket.onclose = () => {
if (!closed) {
reconnectTimer = window.setTimeout(connect, 1500);
}
};
};
connect();
return () => {
closed = true;
if (reconnectTimer !== null) {
window.clearTimeout(reconnectTimer);
}
socket?.close();
};
}, [isRoot, isVersionModalOpen]);
useEffect(() => {
if (!isUserMenuOpen) {
return;
@@ -245,6 +296,10 @@ export function DashboardTopbar() {
selectedReleaseChannel === 'preview'
? previewReleaseQuery.data
: stableReleaseQuery.data;
const releaseWithStream = mergeReleaseWithUpgradeStream(
selectedRelease,
upgradeStream,
);
const selectedReleaseError =
selectedReleaseChannel === 'preview'
? previewReleaseQuery.error
@@ -345,7 +400,7 @@ export function DashboardTopbar() {
isOpen={isVersionModalOpen}
onClose={() => setIsVersionModalOpen(false)}
currentVersion={currentVersion}
release={selectedRelease}
release={releaseWithStream}
selectedChannel={selectedReleaseChannel}
uploadedBinary={uploadedBinary}
isLoading={
@@ -376,3 +431,19 @@ export function DashboardTopbar() {
</>
);
}
function mergeReleaseWithUpgradeStream(
release: LatestReleaseInfo | null | undefined,
stream: UpgradeStreamSnapshot | null,
) {
if (!release || !stream) {
return release;
}
return {
...release,
in_progress: stream.in_progress,
upgrade_status: stream.upgrade_status,
upgrade_logs: stream.upgrade_logs,
};
}
+41 -1
View File
@@ -4,6 +4,7 @@ import type { ApiEnvelope } from '@/types/api';
import type {
LatestReleaseInfo,
ReleaseChannel,
UpgradeStreamSnapshot,
UploadedServerBinaryInfo,
} from '@/features/update/types';
@@ -48,7 +49,9 @@ export function uploadServerBinary(
xhr.addEventListener('load', () => {
let payload: ApiEnvelope<UploadedServerBinaryInfo> | null = null;
try {
payload = JSON.parse(xhr.responseText) as ApiEnvelope<UploadedServerBinaryInfo>;
payload = JSON.parse(
xhr.responseText,
) as ApiEnvelope<UploadedServerBinaryInfo>;
} catch {
payload = null;
}
@@ -86,3 +89,40 @@ export function confirmManualServerUpgrade(uploadToken: string) {
body: JSON.stringify({ upload_token: uploadToken }),
});
}
export function createUpgradeLogsWebSocket() {
if (typeof window === 'undefined') {
return null;
}
const apiUrl = getApiUrl('/update/logs/ws');
const resolvedUrl = apiUrl.startsWith('http://')
? `ws://${apiUrl.slice('http://'.length)}`
: apiUrl.startsWith('https://')
? `wss://${apiUrl.slice('https://'.length)}`
: `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${apiUrl}`;
return new WebSocket(resolvedUrl);
}
export function parseUpgradeStreamSnapshot(
rawMessage: string,
): UpgradeStreamSnapshot | null {
try {
const parsed = JSON.parse(rawMessage) as Partial<UpgradeStreamSnapshot>;
if (
typeof parsed.in_progress !== 'boolean' ||
typeof parsed.upgrade_status !== 'string' ||
!Array.isArray(parsed.upgrade_logs)
) {
return null;
}
return {
in_progress: parsed.in_progress,
upgrade_status: parsed.upgrade_status,
upgrade_logs: parsed.upgrade_logs,
};
} catch {
return null;
}
}
+6
View File
@@ -21,6 +21,12 @@ export interface LatestReleaseInfo {
upgrade_logs: UpgradeLogItem[];
}
export interface UpgradeStreamSnapshot {
in_progress: boolean;
upgrade_status: 'idle' | 'running' | 'succeeded' | 'failed' | string;
upgrade_logs: UpgradeLogItem[];
}
export interface UploadedServerBinaryInfo {
upload_token: string;
file_name: string;