mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
[优化] 自动发现接入
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { apiRequest } from '@/lib/api/client';
|
||||
|
||||
import type {
|
||||
BootstrapTokenPayload,
|
||||
OptionItem,
|
||||
SettingsProfile,
|
||||
UpdateSelfPayload,
|
||||
@@ -17,6 +18,16 @@ export function updateOption(key: string, value: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getBootstrapToken() {
|
||||
return apiRequest<BootstrapTokenPayload>('/nodes/bootstrap-token');
|
||||
}
|
||||
|
||||
export function rotateBootstrapToken() {
|
||||
return apiRequest<BootstrapTokenPayload>('/nodes/bootstrap-token/rotate', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export function getSettingsProfile() {
|
||||
return apiRequest<SettingsProfile>('/user/self');
|
||||
}
|
||||
|
||||
@@ -18,12 +18,18 @@ import {
|
||||
bindEmail,
|
||||
bindWeChat,
|
||||
generateAccessToken,
|
||||
getBootstrapToken,
|
||||
getOptions,
|
||||
getSettingsProfile,
|
||||
rotateBootstrapToken,
|
||||
updateOption,
|
||||
updateSelf,
|
||||
} from '@/features/settings/api/settings';
|
||||
import type { OptionItem, UpdateSelfPayload } from '@/features/settings/types';
|
||||
import type {
|
||||
BootstrapTokenPayload,
|
||||
OptionItem,
|
||||
UpdateSelfPayload,
|
||||
} from '@/features/settings/types';
|
||||
import {
|
||||
CodeBlock,
|
||||
PrimaryButton,
|
||||
@@ -36,6 +42,8 @@ import {
|
||||
import { formatDateTime } from '@/lib/utils/date';
|
||||
|
||||
const settingsQueryKey = ['settings', 'options'] as const;
|
||||
const installerScriptUrl =
|
||||
'https://raw.githubusercontent.com/Rain-kl/ATSFlare/main/scripts/install-agent.sh';
|
||||
|
||||
const defaultSystemFields = {
|
||||
ServerAddress: '',
|
||||
@@ -124,6 +132,14 @@ function normalizeServerUrl(value: string) {
|
||||
return value.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function getBrowserOrigin() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return normalizeServerUrl(window.location.origin);
|
||||
}
|
||||
|
||||
function formatDurationLabel(value: string) {
|
||||
const milliseconds = Number.parseInt(value, 10);
|
||||
if (Number.isNaN(milliseconds)) {
|
||||
@@ -154,6 +170,14 @@ function formatSecondsLabel(value: string) {
|
||||
return `${seconds} 秒`;
|
||||
}
|
||||
|
||||
function buildDiscoveryCommand(serverUrl: string, discoveryToken: string) {
|
||||
return [
|
||||
`curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
|
||||
` --server-url ${normalizeServerUrl(serverUrl)} \\`,
|
||||
` --discovery-token ${discoveryToken}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function copyToClipboard(value: string) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
}
|
||||
@@ -194,6 +218,12 @@ export function SettingsPage() {
|
||||
enabled: isRoot,
|
||||
});
|
||||
|
||||
const bootstrapQuery = useQuery({
|
||||
queryKey: ['settings', 'bootstrap-token'],
|
||||
queryFn: getBootstrapToken,
|
||||
enabled: isRoot,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (profileQuery.data) {
|
||||
setProfileFields({
|
||||
@@ -211,9 +241,12 @@ export function SettingsPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedServerAddress =
|
||||
publicStatus.server_address || getBrowserOrigin();
|
||||
|
||||
setSystemFields((previous) => ({
|
||||
...previous,
|
||||
ServerAddress: publicStatus.server_address || previous.ServerAddress,
|
||||
ServerAddress: resolvedServerAddress || previous.ServerAddress,
|
||||
GitHubClientId: publicStatus.github_client_id || previous.GitHubClientId,
|
||||
WeChatAccountQRCodeImageURL:
|
||||
publicStatus.wechat_qrcode || previous.WeChatAccountQRCodeImageURL,
|
||||
@@ -228,7 +261,7 @@ export function SettingsPage() {
|
||||
}));
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
ServerAddress: publicStatus.server_address || previous.ServerAddress,
|
||||
ServerAddress: resolvedServerAddress || previous.ServerAddress,
|
||||
}));
|
||||
}, [publicStatusQuery.data]);
|
||||
|
||||
@@ -238,9 +271,13 @@ export function SettingsPage() {
|
||||
}
|
||||
|
||||
const optionMap = optionsToMap(optionsQuery.data);
|
||||
const resolvedServerAddress =
|
||||
optionMap.ServerAddress ||
|
||||
publicStatusQuery.data?.server_address ||
|
||||
getBrowserOrigin();
|
||||
|
||||
setSystemFields({
|
||||
ServerAddress: optionMap.ServerAddress ?? '',
|
||||
ServerAddress: resolvedServerAddress,
|
||||
PasswordLoginEnabled: toBoolean(optionMap.PasswordLoginEnabled, true),
|
||||
PasswordRegisterEnabled: toBoolean(
|
||||
optionMap.PasswordRegisterEnabled,
|
||||
@@ -282,8 +319,7 @@ export function SettingsPage() {
|
||||
DownloadRateLimitDuration: optionMap.DownloadRateLimitDuration ?? '60',
|
||||
CriticalRateLimitNum: optionMap.CriticalRateLimitNum ?? '100',
|
||||
CriticalRateLimitDuration: optionMap.CriticalRateLimitDuration ?? '1200',
|
||||
ServerAddress:
|
||||
optionMap.ServerAddress ?? publicStatusQuery.data?.server_address ?? '',
|
||||
ServerAddress: resolvedServerAddress,
|
||||
});
|
||||
|
||||
setOtherFields({
|
||||
@@ -295,6 +331,26 @@ export function SettingsPage() {
|
||||
});
|
||||
}, [optionsQuery.data, publicStatusQuery.data?.server_address]);
|
||||
|
||||
const rotateTokenMutation = useMutation({
|
||||
mutationFn: rotateBootstrapToken,
|
||||
onSuccess: async (data: BootstrapTokenPayload) => {
|
||||
setFeedback({ tone: 'success', message: 'Discovery Token 已重新生成。' });
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['settings', 'bootstrap-token'],
|
||||
});
|
||||
if (data.discovery_token) {
|
||||
try {
|
||||
await copyToClipboard(data.discovery_token);
|
||||
} catch {
|
||||
// ignore clipboard errors
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setFeedback({ tone: 'danger', message: getErrorMessage(error) });
|
||||
},
|
||||
});
|
||||
|
||||
const accessTokenMutation = useMutation({
|
||||
mutationFn: generateAccessToken,
|
||||
onSuccess: (token) => {
|
||||
@@ -309,6 +365,12 @@ export function SettingsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const discoveryToken = bootstrapQuery.data?.discovery_token ?? '';
|
||||
const discoveryCommand =
|
||||
isRoot && operationFields.ServerAddress && discoveryToken
|
||||
? buildDiscoveryCommand(operationFields.ServerAddress, discoveryToken)
|
||||
: '';
|
||||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -321,7 +383,7 @@ export function SettingsPage() {
|
||||
{
|
||||
key: 'operation' as const,
|
||||
label: '运维设置',
|
||||
description: 'Agent 参数、更新仓库与接口限流设置。',
|
||||
description: 'Agent 参数、Discovery Token 与部署命令。',
|
||||
},
|
||||
{
|
||||
key: 'system' as const,
|
||||
@@ -925,6 +987,73 @@ export function SettingsPage() {
|
||||
/>
|
||||
</ResourceField>
|
||||
</AppCard>
|
||||
<AppCard
|
||||
title="Discovery Token 与部署命令"
|
||||
description="适用于新节点首次接入。可直接复制一键安装命令。"
|
||||
action={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<SecondaryButton
|
||||
type="button"
|
||||
onClick={() => rotateTokenMutation.mutate()}
|
||||
disabled={rotateTokenMutation.isPending}
|
||||
>
|
||||
{rotateTokenMutation.isPending
|
||||
? '生成中...'
|
||||
: '重新生成 Token'}
|
||||
</SecondaryButton>
|
||||
{discoveryCommand ? (
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
onClick={() => void copyToClipboard(discoveryCommand)}
|
||||
>
|
||||
复制命令
|
||||
</PrimaryButton>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{bootstrapQuery.isLoading ? (
|
||||
<LoadingState />
|
||||
) : bootstrapQuery.isError ? (
|
||||
<ErrorState
|
||||
title="Discovery Token 加载失败"
|
||||
description={getErrorMessage(bootstrapQuery.error)}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<ResourceField
|
||||
label="Server URL"
|
||||
hint="默认使用当前 ServerAddress,可按需改为外部访问地址。"
|
||||
>
|
||||
<ResourceInput
|
||||
value={operationFields.ServerAddress}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
ServerAddress: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-[var(--foreground-muted)]">
|
||||
Discovery Token
|
||||
</p>
|
||||
<p className="mt-2 break-all text-sm text-[var(--foreground-primary)]">
|
||||
{discoveryToken || '未生成'}
|
||||
</p>
|
||||
</div>
|
||||
<ResourceField
|
||||
label="一键部署命令"
|
||||
hint="命令会使用安装脚本自动注册 Agent,并在首次注册后换发专属 agent_token。"
|
||||
>
|
||||
<CodeBlock className="break-all whitespace-pre-wrap">
|
||||
{discoveryCommand || '请先填写可访问的 Server URL。'}
|
||||
</CodeBlock>
|
||||
</ResourceField>
|
||||
</div>
|
||||
)}
|
||||
</AppCard>
|
||||
<AppCard
|
||||
title="版本与构建信息"
|
||||
description="版本检查与升级入口已移动到顶栏“版本”。这里保留构建模式和当前运行状态说明。"
|
||||
|
||||
@@ -5,6 +5,10 @@ export interface OptionItem {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface BootstrapTokenPayload {
|
||||
discovery_token: string;
|
||||
}
|
||||
|
||||
export interface UpdateSelfPayload {
|
||||
username: string;
|
||||
display_name: string;
|
||||
|
||||
Reference in New Issue
Block a user