mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 07:36:37 +08:00
[优化] 自动发现接入
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { apiRequest } from '@/lib/api/client';
|
import { apiRequest } from '@/lib/api/client';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
BootstrapTokenPayload,
|
||||||
OptionItem,
|
OptionItem,
|
||||||
SettingsProfile,
|
SettingsProfile,
|
||||||
UpdateSelfPayload,
|
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() {
|
export function getSettingsProfile() {
|
||||||
return apiRequest<SettingsProfile>('/user/self');
|
return apiRequest<SettingsProfile>('/user/self');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,18 @@ import {
|
|||||||
bindEmail,
|
bindEmail,
|
||||||
bindWeChat,
|
bindWeChat,
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
|
getBootstrapToken,
|
||||||
getOptions,
|
getOptions,
|
||||||
getSettingsProfile,
|
getSettingsProfile,
|
||||||
|
rotateBootstrapToken,
|
||||||
updateOption,
|
updateOption,
|
||||||
updateSelf,
|
updateSelf,
|
||||||
} from '@/features/settings/api/settings';
|
} from '@/features/settings/api/settings';
|
||||||
import type { OptionItem, UpdateSelfPayload } from '@/features/settings/types';
|
import type {
|
||||||
|
BootstrapTokenPayload,
|
||||||
|
OptionItem,
|
||||||
|
UpdateSelfPayload,
|
||||||
|
} from '@/features/settings/types';
|
||||||
import {
|
import {
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
@@ -36,6 +42,8 @@ import {
|
|||||||
import { formatDateTime } from '@/lib/utils/date';
|
import { formatDateTime } from '@/lib/utils/date';
|
||||||
|
|
||||||
const settingsQueryKey = ['settings', 'options'] as const;
|
const settingsQueryKey = ['settings', 'options'] as const;
|
||||||
|
const installerScriptUrl =
|
||||||
|
'https://raw.githubusercontent.com/Rain-kl/ATSFlare/main/scripts/install-agent.sh';
|
||||||
|
|
||||||
const defaultSystemFields = {
|
const defaultSystemFields = {
|
||||||
ServerAddress: '',
|
ServerAddress: '',
|
||||||
@@ -124,6 +132,14 @@ function normalizeServerUrl(value: string) {
|
|||||||
return value.trim().replace(/\/+$/, '');
|
return value.trim().replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBrowserOrigin() {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeServerUrl(window.location.origin);
|
||||||
|
}
|
||||||
|
|
||||||
function formatDurationLabel(value: string) {
|
function formatDurationLabel(value: string) {
|
||||||
const milliseconds = Number.parseInt(value, 10);
|
const milliseconds = Number.parseInt(value, 10);
|
||||||
if (Number.isNaN(milliseconds)) {
|
if (Number.isNaN(milliseconds)) {
|
||||||
@@ -154,6 +170,14 @@ function formatSecondsLabel(value: string) {
|
|||||||
return `${seconds} 秒`;
|
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) {
|
async function copyToClipboard(value: string) {
|
||||||
await navigator.clipboard.writeText(value);
|
await navigator.clipboard.writeText(value);
|
||||||
}
|
}
|
||||||
@@ -194,6 +218,12 @@ export function SettingsPage() {
|
|||||||
enabled: isRoot,
|
enabled: isRoot,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bootstrapQuery = useQuery({
|
||||||
|
queryKey: ['settings', 'bootstrap-token'],
|
||||||
|
queryFn: getBootstrapToken,
|
||||||
|
enabled: isRoot,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profileQuery.data) {
|
if (profileQuery.data) {
|
||||||
setProfileFields({
|
setProfileFields({
|
||||||
@@ -211,9 +241,12 @@ export function SettingsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedServerAddress =
|
||||||
|
publicStatus.server_address || getBrowserOrigin();
|
||||||
|
|
||||||
setSystemFields((previous) => ({
|
setSystemFields((previous) => ({
|
||||||
...previous,
|
...previous,
|
||||||
ServerAddress: publicStatus.server_address || previous.ServerAddress,
|
ServerAddress: resolvedServerAddress || previous.ServerAddress,
|
||||||
GitHubClientId: publicStatus.github_client_id || previous.GitHubClientId,
|
GitHubClientId: publicStatus.github_client_id || previous.GitHubClientId,
|
||||||
WeChatAccountQRCodeImageURL:
|
WeChatAccountQRCodeImageURL:
|
||||||
publicStatus.wechat_qrcode || previous.WeChatAccountQRCodeImageURL,
|
publicStatus.wechat_qrcode || previous.WeChatAccountQRCodeImageURL,
|
||||||
@@ -228,7 +261,7 @@ export function SettingsPage() {
|
|||||||
}));
|
}));
|
||||||
setOperationFields((previous) => ({
|
setOperationFields((previous) => ({
|
||||||
...previous,
|
...previous,
|
||||||
ServerAddress: publicStatus.server_address || previous.ServerAddress,
|
ServerAddress: resolvedServerAddress || previous.ServerAddress,
|
||||||
}));
|
}));
|
||||||
}, [publicStatusQuery.data]);
|
}, [publicStatusQuery.data]);
|
||||||
|
|
||||||
@@ -238,9 +271,13 @@ export function SettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const optionMap = optionsToMap(optionsQuery.data);
|
const optionMap = optionsToMap(optionsQuery.data);
|
||||||
|
const resolvedServerAddress =
|
||||||
|
optionMap.ServerAddress ||
|
||||||
|
publicStatusQuery.data?.server_address ||
|
||||||
|
getBrowserOrigin();
|
||||||
|
|
||||||
setSystemFields({
|
setSystemFields({
|
||||||
ServerAddress: optionMap.ServerAddress ?? '',
|
ServerAddress: resolvedServerAddress,
|
||||||
PasswordLoginEnabled: toBoolean(optionMap.PasswordLoginEnabled, true),
|
PasswordLoginEnabled: toBoolean(optionMap.PasswordLoginEnabled, true),
|
||||||
PasswordRegisterEnabled: toBoolean(
|
PasswordRegisterEnabled: toBoolean(
|
||||||
optionMap.PasswordRegisterEnabled,
|
optionMap.PasswordRegisterEnabled,
|
||||||
@@ -282,8 +319,7 @@ export function SettingsPage() {
|
|||||||
DownloadRateLimitDuration: optionMap.DownloadRateLimitDuration ?? '60',
|
DownloadRateLimitDuration: optionMap.DownloadRateLimitDuration ?? '60',
|
||||||
CriticalRateLimitNum: optionMap.CriticalRateLimitNum ?? '100',
|
CriticalRateLimitNum: optionMap.CriticalRateLimitNum ?? '100',
|
||||||
CriticalRateLimitDuration: optionMap.CriticalRateLimitDuration ?? '1200',
|
CriticalRateLimitDuration: optionMap.CriticalRateLimitDuration ?? '1200',
|
||||||
ServerAddress:
|
ServerAddress: resolvedServerAddress,
|
||||||
optionMap.ServerAddress ?? publicStatusQuery.data?.server_address ?? '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setOtherFields({
|
setOtherFields({
|
||||||
@@ -295,6 +331,26 @@ export function SettingsPage() {
|
|||||||
});
|
});
|
||||||
}, [optionsQuery.data, publicStatusQuery.data?.server_address]);
|
}, [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({
|
const accessTokenMutation = useMutation({
|
||||||
mutationFn: generateAccessToken,
|
mutationFn: generateAccessToken,
|
||||||
onSuccess: (token) => {
|
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(
|
const tabs = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -321,7 +383,7 @@ export function SettingsPage() {
|
|||||||
{
|
{
|
||||||
key: 'operation' as const,
|
key: 'operation' as const,
|
||||||
label: '运维设置',
|
label: '运维设置',
|
||||||
description: 'Agent 参数、更新仓库与接口限流设置。',
|
description: 'Agent 参数、Discovery Token 与部署命令。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system' as const,
|
key: 'system' as const,
|
||||||
@@ -925,6 +987,73 @@ export function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</ResourceField>
|
</ResourceField>
|
||||||
</AppCard>
|
</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
|
<AppCard
|
||||||
title="版本与构建信息"
|
title="版本与构建信息"
|
||||||
description="版本检查与升级入口已移动到顶栏“版本”。这里保留构建模式和当前运行状态说明。"
|
description="版本检查与升级入口已移动到顶栏“版本”。这里保留构建模式和当前运行状态说明。"
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ export interface OptionItem {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BootstrapTokenPayload {
|
||||||
|
discovery_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateSelfPayload {
|
export interface UpdateSelfPayload {
|
||||||
username: string;
|
username: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user