merge: remove legacy openflare update API

This commit is contained in:
ryan
2026-06-19 11:18:15 +08:00
16 changed files with 3601 additions and 2147 deletions
@@ -3,36 +3,13 @@
import {useMutation, useQuery} from '@tanstack/react-query';
import {useCallback, useEffect, useRef, useState} from 'react';
import type {
LatestReleaseInfo,
ReleaseChannel,
UpgradeStreamSnapshot,
UploadedServerBinaryInfo,
} from '@/lib/services/openflare';
import {StatusService, UpdateService} from '@/lib/services/openflare';
const UPGRADE_STATUS_POLL_INTERVAL = 3000;
import {AdminStatusService} from '@/lib/services/admin';
import type {AppUpdateStatus} from '@/lib/services/admin/types';
import {StatusService} from '@/lib/services/openflare';
export const openflarePublicStatusQueryKey = ['openflare', 'public-status'] as const;
export const openflareLatestReleaseQueryKey = (channel: ReleaseChannel = 'stable') =>
['openflare', 'latest-release', channel] as const;
export 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,
};
}
export const adminUpdateStatusQueryKey = ['admin', 'update'] as const;
export function useOpenFlareServerUpgrade({
open,
@@ -41,15 +18,8 @@ export function useOpenFlareServerUpgrade({
open: boolean;
canUpgrade: boolean;
}) {
const [selectedChannel, setSelectedChannel] = useState<ReleaseChannel>('stable');
const [feedback, setFeedback] = useState<string | null>(null);
const [manualStatusMessage, setManualStatusMessage] = useState<string | null>(null);
const [manualErrorMessage, setManualErrorMessage] = useState<string | null>(null);
const [uploadedBinary, setUploadedBinary] = useState<UploadedServerBinaryInfo | null>(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [upgradeStream, setUpgradeStream] = useState<UpgradeStreamSnapshot | null>(null);
const upgradeRefreshPendingRef = useRef(false);
const upgradeReloadStartedRef = useRef(false);
const upgradeReloadTimerRef = useRef<number | null>(null);
@@ -59,30 +29,11 @@ export function useOpenFlareServerUpgrade({
enabled: open,
});
const stableReleaseQuery = useQuery({
queryKey: openflareLatestReleaseQueryKey('stable'),
queryFn: () => UpdateService.getLatestRelease('stable'),
const updateQuery = useQuery({
queryKey: adminUpdateStatusQueryKey,
queryFn: () => AdminStatusService.getUpdateStatus(),
enabled: open && canUpgrade,
refetchInterval: (query) => {
const release = query.state.data;
if (open && release?.in_progress) {
return UPGRADE_STATUS_POLL_INTERVAL;
}
return 60 * 60 * 1000;
},
});
const previewReleaseQuery = useQuery({
queryKey: openflareLatestReleaseQueryKey('preview'),
queryFn: () => UpdateService.getLatestRelease('preview'),
enabled: false,
refetchInterval: (query) => {
const release = query.state.data;
if (open && release?.in_progress) {
return UPGRADE_STATUS_POLL_INTERVAL;
}
return false;
},
staleTime: 5 * 60 * 1000,
});
const scheduleUpgradePageReload = useCallback(() => {
@@ -113,236 +64,64 @@ export function useOpenFlareServerUpgrade({
};
}, []);
useEffect(() => {
if (!open || !canUpgrade) {
setUpgradeStream(null);
return;
}
let closed = false;
let reconnectTimer: number | null = null;
let socket: WebSocket | null = null;
const connect = () => {
if (closed) {
return;
}
socket = UpdateService.createUpgradeLogsWebSocket();
if (!socket) {
return;
}
socket.onmessage = (event) => {
const snapshot = UpdateService.parseUpgradeStreamSnapshot(String(event.data));
if (snapshot) {
if (snapshot.in_progress || snapshot.upgrade_status === 'succeeded') {
upgradeRefreshPendingRef.current = true;
}
if (snapshot.upgrade_status === 'failed') {
upgradeRefreshPendingRef.current = false;
}
setUpgradeStream(snapshot);
}
};
socket.onclose = () => {
if (closed) {
return;
}
if (upgradeRefreshPendingRef.current) {
scheduleUpgradePageReload();
return;
}
reconnectTimer = window.setTimeout(connect, 1500);
};
};
connect();
return () => {
closed = true;
if (reconnectTimer !== null) {
window.clearTimeout(reconnectTimer);
}
socket?.close();
};
}, [canUpgrade, open, scheduleUpgradePageReload]);
const upgradeMutation = useMutation({
mutationFn: (channel: ReleaseChannel) => UpdateService.upgradeServer(channel),
onSuccess: (release) => {
upgradeRefreshPendingRef.current = true;
setUploadedBinary(null);
setManualStatusMessage(null);
setManualErrorMessage(null);
setFeedback(
`服务升级任务已启动,目标版本 ${release.tag_name}(${release.channel === 'preview' ? '预览版' : '正式版'})。页面可能短暂不可用。`,
);
void stableReleaseQuery.refetch();
if (release.channel === 'preview') {
void previewReleaseQuery.refetch();
}
mutationFn: () => AdminStatusService.applyUpdate(),
onSuccess: () => {
scheduleUpgradePageReload();
setFeedback('升级包已校验完成,服务正在重启。');
},
onError: (error) => {
setFeedback(error instanceof Error ? error.message : '升级失败,请稍后重试。');
},
});
const uploadBinaryMutation = useMutation({
mutationFn: (binary: File) =>
UpdateService.uploadServerBinary(binary, (progress) => {
setUploadProgress(progress);
}),
onSuccess: (candidate) => {
setUploadProgress(0);
setFeedback(null);
setManualErrorMessage(null);
setUploadedBinary(candidate);
setManualStatusMessage(candidate.comparison_message);
},
onError: (error) => {
setUploadProgress(0);
setUploadedBinary(null);
setManualStatusMessage(null);
setManualErrorMessage(
error instanceof Error ? error.message : '上传升级包失败,请稍后重试。',
);
},
});
const confirmManualUpgradeMutation = useMutation({
mutationFn: (uploadToken: string) => UpdateService.confirmManualServerUpgrade(uploadToken),
onSuccess: (candidate) => {
upgradeRefreshPendingRef.current = true;
setFeedback(null);
setManualErrorMessage(null);
setUploadedBinary(candidate);
setManualStatusMessage(
`手动升级任务已启动,目标版本 ${candidate.detected_version}。页面可能短暂不可用。`,
);
void stableReleaseQuery.refetch();
void previewReleaseQuery.refetch();
},
onError: (error) => {
setManualStatusMessage(null);
setManualErrorMessage(
error instanceof Error ? error.message : '确认手动升级失败,请稍后重试。',
);
},
});
const resetTransientState = useCallback(() => {
setSelectedChannel('stable');
setFeedback(null);
setManualStatusMessage(null);
setManualErrorMessage(null);
setUploadedBinary(null);
setUploadProgress(0);
setUpgradeStream(null);
upgradeRefreshPendingRef.current = false;
upgradeReloadStartedRef.current = false;
}, []);
const handleOpen = useCallback(() => {
resetTransientState();
if (canUpgrade) {
void stableReleaseQuery.refetch();
void updateQuery.refetch();
}
}, [canUpgrade, resetTransientState, stableReleaseQuery]);
}, [canUpgrade, resetTransientState, updateQuery]);
const handleCheckRelease = useCallback(() => {
setFeedback(null);
if (!canUpgrade) {
return;
}
if (selectedChannel === 'preview') {
void previewReleaseQuery.refetch();
return;
}
void stableReleaseQuery.refetch();
}, [canUpgrade, previewReleaseQuery, selectedChannel, stableReleaseQuery]);
const handleChannelChange = useCallback((channel: ReleaseChannel) => {
setSelectedChannel(channel);
setFeedback(null);
}, []);
void updateQuery.refetch();
}, [canUpgrade, updateQuery]);
const handleUpgrade = useCallback(() => {
setFeedback(null);
setManualStatusMessage(null);
setManualErrorMessage(null);
upgradeMutation.mutate(selectedChannel);
}, [selectedChannel, upgradeMutation]);
const handleUploadBinary = useCallback(
(binary: File) => {
setUploadProgress(0);
setManualStatusMessage(null);
setManualErrorMessage(null);
uploadBinaryMutation.mutate(binary);
},
[uploadBinaryMutation],
);
const handleConfirmManualUpgrade = useCallback(() => {
if (!uploadedBinary?.upload_token) {
setManualStatusMessage(null);
setManualErrorMessage('请先上传并检查升级包。');
return;
}
setFeedback(null);
setManualStatusMessage(null);
setManualErrorMessage(null);
confirmManualUpgradeMutation.mutate(uploadedBinary.upload_token);
}, [confirmManualUpgradeMutation, uploadedBinary?.upload_token]);
const selectedRelease =
selectedChannel === 'preview' ? previewReleaseQuery.data : stableReleaseQuery.data;
const releaseWithStream = mergeReleaseWithUpgradeStream(selectedRelease, upgradeStream);
const selectedReleaseError =
selectedChannel === 'preview' ? previewReleaseQuery.error : stableReleaseQuery.error;
const isSelectedReleaseError =
selectedChannel === 'preview' ? previewReleaseQuery.isError : stableReleaseQuery.isError;
const isChecking =
selectedChannel === 'preview'
? previewReleaseQuery.isFetching
: stableReleaseQuery.isFetching;
const isInitialLoading =
(selectedChannel === 'preview'
? previewReleaseQuery.isLoading && !previewReleaseQuery.data
: stableReleaseQuery.isLoading && !stableReleaseQuery.data) && canUpgrade;
upgradeMutation.mutate();
}, [upgradeMutation]);
const update = updateQuery.data;
const releaseErrorMessage =
feedback ||
(isSelectedReleaseError
? selectedReleaseError instanceof Error
? selectedReleaseError.message
(updateQuery.isError
? updateQuery.error instanceof Error
? updateQuery.error.message
: '版本检查失败,请稍后重试。'
: undefined);
const currentVersion = statusQuery.data?.version || releaseWithStream?.current_version || 'unknown';
const currentVersion = statusQuery.data?.version || update?.current_version || 'unknown';
return {
currentVersion,
selectedChannel,
release: releaseWithStream,
uploadedBinary,
update,
releaseErrorMessage,
manualStatusMessage,
manualErrorMessage,
isInitialLoading,
isChecking,
isInitialLoading: updateQuery.isLoading && !updateQuery.data && canUpgrade,
isChecking: updateQuery.isFetching,
isUpgrading: upgradeMutation.isPending,
isUploadingBinary: uploadBinaryMutation.isPending,
uploadProgress,
isConfirmingManualUpgrade: confirmManualUpgradeMutation.isPending,
stableRelease: stableReleaseQuery.data,
handleOpen,
handleCheckRelease,
handleChannelChange,
handleUpgrade,
handleUploadBinary,
handleConfirmManualUpgrade,
};
}
}
export type {AppUpdateStatus};
-4
View File
@@ -40,7 +40,6 @@ import {
ProxyRouteService,
StatusService,
TlsCertificateService,
UpdateService,
UptimeKumaService,
WafService,
WebsiteService,
@@ -77,7 +76,6 @@ const services = {
openflareAccessLog: AccessLogService,
openflareOption: OptionService,
openflareUptimeKuma: UptimeKumaService,
openflareUpdate: UpdateService,
openflareStatus: StatusService,
} as const;
@@ -199,7 +197,6 @@ export {
AccessLogService,
OptionService,
StatusService,
UpdateService,
UptimeKumaService,
} from './openflare';
@@ -227,6 +224,5 @@ export type {
OptionItem,
GeoIPLookupResult,
DatabaseCleanupResult,
LatestReleaseInfo,
OpenFlarePublicStatus,
} from './openflare';
@@ -15,7 +15,6 @@ export { OriginService } from './origin.service';
export { AccessLogService } from './access-log.service';
export { OptionService } from './option.service';
export { UptimeKumaService } from './uptimekuma.service';
export { UpdateService } from './update.service';
export { StatusService } from './status.service';
export type {
@@ -93,11 +92,7 @@ export type {
DatabaseCleanupPayload,
DatabaseCleanupResult,
DatabaseCleanupTarget,
LatestReleaseInfo,
OpenFlarePublicStatus,
UpgradeLogItem,
UpgradeStreamSnapshot,
UploadedServerBinaryInfo,
OriginDetail,
OriginItem,
OriginMutationPayload,
@@ -128,7 +123,6 @@ import {DnsAccountService} from './dns-account.service';
import {NodeService} from './node.service';
import {OptionService} from './option.service';
import {StatusService} from './status.service';
import {UpdateService} from './update.service';
import {UptimeKumaService} from './uptimekuma.service';
import {OriginService} from './origin.service';
import {PagesService} from './pages.service';
@@ -153,6 +147,5 @@ export const openflareServices = {
accessLog: AccessLogService,
option: OptionService,
uptimeKuma: UptimeKumaService,
update: UpdateService,
status: StatusService,
} as const;
@@ -696,45 +696,6 @@ export interface OpenFlarePublicStatus {
system_name: string;
}
export interface UpgradeLogItem {
level: string;
message: string;
created_at: string;
}
export interface LatestReleaseInfo {
tag_name: string;
body: string;
html_url: string;
published_at: string;
channel: ReleaseChannel;
prerelease: boolean;
current_version: string;
has_update: boolean;
upgrade_supported: boolean;
in_progress: boolean;
upgrade_status: 'idle' | 'running' | 'succeeded' | 'failed' | string;
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;
detected_version: string;
current_version: string;
has_update: boolean;
upgrade_supported: boolean;
ready_to_upgrade: boolean;
comparison_message: string;
uploaded_at: string;
}
export interface WAFRuleGroup {
id: number;
name: string;
@@ -1,131 +0,0 @@
import type {InternalAxiosRequestConfig} from 'axios';
import {getApiBaseUrl} from '@/lib/services/core/config';
import {ApiErrorBase} from '@/lib/services/core/errors';
import type {ApiResponse} from '@/lib/services/core';
import {OpenFlareBaseService} from './base.service';
import type {LatestReleaseInfo, ReleaseChannel, UpgradeStreamSnapshot, UploadedServerBinaryInfo} from './types';
/**
* OpenFlare 服务端升级 API(`/api/v1/d/update/*`)。
*/
export class UpdateService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/update';
private static getApiUrl(path: string): string {
const baseURL = getApiBaseUrl();
const fullPath = this.getFullPath(path);
if (!baseURL) {
return fullPath;
}
return `${baseURL.replace(/\/$/, '')}${fullPath}`;
}
static getLatestRelease(channel: ReleaseChannel = 'stable'): Promise<LatestReleaseInfo> {
return this.get<LatestReleaseInfo>('/latest-release', { channel });
}
static upgradeServer(channel: ReleaseChannel = 'stable'): Promise<LatestReleaseInfo> {
return this.post<LatestReleaseInfo>('/upgrade', { channel });
}
static uploadServerBinary(
binary: File,
onProgress?: (progress: number) => void,
): Promise<UploadedServerBinaryInfo> {
const formData = new FormData();
formData.append('binary', binary);
if (!onProgress) {
return this.post<UploadedServerBinaryInfo>('/manual-upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
} as InternalAxiosRequestConfig);
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', this.getApiUrl('/manual-upload'));
xhr.withCredentials = true;
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
onProgress(Math.round((event.loaded / event.total) * 100));
}
});
xhr.addEventListener('load', () => {
let payload: ApiResponse<UploadedServerBinaryInfo> | null = null;
try {
payload = JSON.parse(
xhr.responseText,
) as ApiResponse<UploadedServerBinaryInfo>;
} catch {
payload = null;
}
if (xhr.status < 200 || xhr.status >= 300) {
reject(
new ApiErrorBase(
payload?.message || `请求失败(${xhr.status})`,
undefined,
xhr.status,
),
);
return;
}
if (!payload) {
reject(new ApiErrorBase('响应格式无效', undefined, xhr.status));
return;
}
resolve(payload.data);
});
xhr.addEventListener('error', () => {
reject(new ApiErrorBase('上传过程中网络连接中断,请检查网络后重试', undefined, 0));
});
xhr.send(formData);
});
}
static confirmManualServerUpgrade(uploadToken: string): Promise<UploadedServerBinaryInfo> {
return this.post<UploadedServerBinaryInfo>('/manual-upgrade', {
upload_token: uploadToken,
});
}
static createUpgradeLogsWebSocket(): WebSocket | null {
if (typeof window === 'undefined') {
return null;
}
const apiUrl = this.getApiUrl('/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);
}
static 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;
}
}
}