diff --git a/docs/changelog/index.md b/docs/changelog/index.md index e67aa033..e102fb4a 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -18,6 +18,7 @@ sidebar: false ### 新增 +- 新增可配置的 FRPS 内置 WebUI 开关和监听端口。在数据库 w_system_configs 中新增 `relay_frps_web_ui_enabled` 与 `relay_frps_web_ui_port`,支持通过后台系统设置页进行图形化管理与动态同步至 Relay 节点。 - 将 TLS 证书续签逻辑接入 Asynq 异步任务框架。新增单证书续期任务 `of_ssl_single_renew`(`openflare:ssl_single_renew`),支持在管理后台查看每步的申请状态和详细日志,并提供失败重试能力。 ### 变更 @@ -32,6 +33,8 @@ sidebar: false ### 修复 +- 修复由于生成的 Docker 安装/部署命令硬编码拉取 `:latest` 镜像,导致使用 v3 新版协议的控制端与 v2 旧版协议的 Relay 节点无法通信的问题。前端改用动态获取当前控制端版本(serverVersion)并自动拉取与当前控制端匹配的 `:beta` 或具体版本镜像。 + - 修复点击「打开 FRPS WebUI」跳转到 `about:blank#blocked`:在 Relay 节点详情页的 WebUI 磁贴卡片中增加展示具体 URL 地址,并提供一键复制按钮。解决因 Chrome 浏览器限制从公网安全源(HTTPS)跨域直接访问本地/私有网络 HTTP 端口(Private Network Access 限制)而导致新页面打开被拦截的问题。 - 修复 Docker 部署模式下 Agent 无法自更新:修改 Docker 启动脚本 `agent-entrypoint.sh`,在降权前将二进制文件所在目录 `/usr/local/bin` 及二进制文件自身的属主赋予 `openflare`,解决容器内自更新写入时报 `permission denied` 的问题。 diff --git a/frontend/app/(main)/nodes/components/relay-node-detail.tsx b/frontend/app/(main)/nodes/components/relay-node-detail.tsx index 400356fb..8775bfe2 100644 --- a/frontend/app/(main)/nodes/components/relay-node-detail.tsx +++ b/frontend/app/(main)/nodes/components/relay-node-detail.tsx @@ -2,14 +2,15 @@ import Link from 'next/link'; import {useRouter} from 'next/navigation'; -import {useMutation, useQueryClient} from '@tanstack/react-query'; -import {useState} from 'react'; +import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; +import {useEffect, useMemo, useState} from 'react'; import { Activity, Copy, ExternalLink, FileText, Fingerprint, + Loader2, Network, RefreshCw, RotateCcw, @@ -30,11 +31,14 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog'; import {Button} from '@/components/ui/button'; +import {Input} from '@/components/ui/input'; import {Label} from '@/components/ui/label'; import {Switch} from '@/components/ui/switch'; import {formatDateTime} from '@/lib/utils'; import type {NodeAgentReleaseInfo, NodeItem, ReleaseChannel} from '@/lib/services/openflare'; import {NodeService} from '@/lib/services/openflare'; +import services from '@/lib/services'; +import type {SystemConfig} from '@/lib/services/admin'; import {AgentUpdateDialog} from './agent-update-dialog'; import {InstallCommand} from './install-command'; @@ -58,6 +62,13 @@ import { const nodesQueryKey = ['openflare', 'nodes']; +function systemConfigMap(configs: SystemConfig[]) { + return configs.reduce>((accumulator, config) => { + accumulator[config.key] = config; + return accumulator; + }, {}); +} + export function RelayNodeDetail({ node }: { node: NodeItem }) { const router = useRouter(); const queryClient = useQueryClient(); @@ -65,6 +76,43 @@ export function RelayNodeDetail({ node }: { node: NodeItem }) { const [upgradeOpen, setUpgradeOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); + const [webServerPort, setWebServerPort] = useState('17500'); + + const systemConfigsQuery = useQuery({ + queryKey: ['admin', 'system-configs'], + queryFn: () => services.adminSystemConfig.listSystemConfigs(), + }); + + const configs = useMemo( + () => systemConfigMap(systemConfigsQuery.data ?? []), + [systemConfigsQuery.data] + ); + + useEffect(() => { + if (systemConfigsQuery.data) { + setWebServerPort(configs['relay_frps_web_ui_port']?.value || '17500'); + } + }, [systemConfigsQuery.data, configs]); + + const savePortMutation = useMutation({ + mutationFn: async (port: string) => { + const portNum = parseInt(port, 10); + if (isNaN(portNum) || portNum < 1 || portNum > 65535) { + throw new Error('端口号必须在 1 ~ 65535 范围内'); + } + const portCfg = configs['relay_frps_web_ui_port']; + await services.adminSystemConfig.updateSystemConfig('relay_frps_web_ui_port', { + value: String(portNum), + description: portCfg?.description || 'FRPS 内置 Web 管理界面端口', + }); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['admin', 'system-configs'] }); + toast.success('FRPS WebUI 端口配置已更新'); + }, + onError: (error) => toast.error(error instanceof Error ? error.message : '更新端口失败'), + }); + const saveMutation = useMutation({ mutationFn: (payload: Parameters[1]) => NodeService.updateNode(node.id, payload), @@ -147,9 +195,14 @@ export function RelayNodeDetail({ node }: { node: NodeItem }) { ]); }; + const webServerPortNum = parseInt(configs['relay_frps_web_ui_port']?.value || '', 10); + const resolvedPort = !isNaN(webServerPortNum) && webServerPortNum > 0 + ? webServerPortNum + : (node.relay_bind_port || 7000) + 500; + const webUiUrl = node.relay_web_server_enabled && node.relay_bind_port - ? `http://${node.ip || '127.0.0.1'}:${node.relay_bind_port + 500}` + ? `http://${node.ip || '127.0.0.1'}:${resolvedPort}` : null; const headerActions = ( @@ -257,14 +310,13 @@ export function RelayNodeDetail({ node }: { node: NodeItem }) { const manageTab = (
- +

- 默认监听绑定端口 + 500,例如 {node.relay_bind_port || 7000} →{' '} - {(node.relay_bind_port || 7000) + 500} + 开启后,节点将启动 frps 的内置管理服务。

+
+ +
+ setWebServerPort(e.target.value)} + placeholder="例如: 17500" + className="bg-card text-xs h-9" + disabled={savePortMutation.isPending || systemConfigsQuery.isLoading} + /> + +
+

+ 当前实际监听端口:{resolvedPort} (修改后在下次节点心跳同步时生效,默认为绑定端口 + 500)。 +

+
+ {webUiUrl ? (