From d40fc1314fb5def2da29e892a00447dbb0900bac Mon Sep 17 00:00:00 2001 From: dushixiang Date: Tue, 10 Feb 2026 19:56:46 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E4=BF=AE=E6=94=B9=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E6=8E=A2=E9=92=88=E7=9A=84=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=E4=B8=BA=E6=89=8B=E5=8A=A8=E5=A1=AB=E5=86=99=202.=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20Linux=20=E7=B3=BB=E7=BB=9F=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E6=9D=BF=203.=20=E4=BC=98=E5=8C=96=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E6=B5=81=E9=87=8F=E5=B1=95=E7=A4=BA=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/agent_install_handler.go | 29 ++-- internal/models/property.go | 5 + internal/service/property_service.go | 22 +++ pkg/agent/service/service.go | 19 ++- .../admin/pages/Agents/AgentInstallManual.tsx | 12 +- .../pages/Agents/AgentInstallOneClick.tsx | 161 +++++++++++++++--- .../admin/pages/Agents/AgentInstallShared.tsx | 135 +++------------ web/src/api/property.ts | 19 +++ web/src/portal/components/ServerCard.tsx | 45 +++++ web/src/portal/pages/ServerList.tsx | 127 ++++++-------- 10 files changed, 336 insertions(+), 238 deletions(-) diff --git a/internal/handler/agent_install_handler.go b/internal/handler/agent_install_handler.go index f83a55b..668da8d 100644 --- a/internal/handler/agent_install_handler.go +++ b/internal/handler/agent_install_handler.go @@ -57,21 +57,15 @@ func (h *AgentHandler) DownloadAgent(c echo.Context) error { return c.Stream(http.StatusOK, "application/octet-stream", agentFile) } -// getServerURL 获取服务器地址(支持反向代理) -func getServerURL(c echo.Context) string { - // 优先读取 X-Forwarded-Proto 和 X-Forwarded-Host - scheme := c.Request().Header.Get("X-Forwarded-Proto") - host := c.Request().Header.Get("X-Forwarded-Host") - - // 如果没有反向代理头部,使用默认值 - if scheme == "" { - scheme = c.Scheme() - } - if host == "" { - host = c.Request().Host +// getServerURL 获取服务器地址(仅从配置读取) +func (h *AgentHandler) getServerURL(c echo.Context) string { + // 优先读取配置的服务端地址 + config, err := h.propertyService.GetAgentInstallConfig(c.Request().Context()) + if err == nil && config.ServerURL != "" { + return config.ServerURL } - return scheme + "://" + host + return "" } func bashSingleQuote(value string) string { @@ -82,7 +76,7 @@ func bashSingleQuote(value string) string { } func (h *AgentHandler) GetServerUrl(c echo.Context) error { - serverUrl := getServerURL(c) + serverUrl := h.getServerURL(c) return orz.Ok(c, orz.Map{ "serverUrl": serverUrl, }) @@ -95,8 +89,11 @@ func (h *AgentHandler) GetInstallScript(c echo.Context) error { return orz.NewError(400, "token不能为空") } - // 使用统一的 getServerURL 函数获取服务器地址(支持反向代理) - serverUrl := getServerURL(c) + // 使用统一的 getServerURL 函数获取服务器地址(仅从配置读取) + serverUrl := strings.TrimSpace(h.getServerURL(c)) + if serverUrl == "" { + return orz.NewError(400, "请先配置服务端地址") + } customName := strings.TrimSpace(c.QueryParam("name")) customNameLiteral := bashSingleQuote(customName) diff --git a/internal/models/property.go b/internal/models/property.go index 0010d30..992318c 100644 --- a/internal/models/property.go +++ b/internal/models/property.go @@ -156,3 +156,8 @@ type AlertNotifications struct { SSHLoginSuccessEnabled bool `json:"sshLoginSuccessEnabled"` // SSH 登录成功通知 TamperEventEnabled bool `json:"tamperEventEnabled"` // 防篡改事件通知 } + +// AgentInstallConfig 探针安装配置 +type AgentInstallConfig struct { + ServerURL string `json:"serverUrl"` // 服务端地址 +} diff --git a/internal/service/property_service.go b/internal/service/property_service.go index 5ceeb04..038ef2e 100644 --- a/internal/service/property_service.go +++ b/internal/service/property_service.go @@ -28,6 +28,8 @@ const ( PropertyIDAlertConfig = "alert_config" // PropertyIDDNSProviders DNS 服务商配置的固定 ID PropertyIDDNSProviders = "dns_providers" + // PropertyIDAgentInstallConfig 探针安装配置的固定 ID + PropertyIDAgentInstallConfig = "agent_install_config" ) var defaultPublicIPv4APIs = []string{ @@ -313,6 +315,21 @@ func (s *PropertyService) DeleteDNSProvider(ctx context.Context, providerType st return s.SetDNSProviderConfigs(ctx, newProviders) } +// GetAgentInstallConfig 获取探针安装配置 +func (s *PropertyService) GetAgentInstallConfig(ctx context.Context) (*models.AgentInstallConfig, error) { + var config models.AgentInstallConfig + err := s.GetValue(ctx, PropertyIDAgentInstallConfig, &config) + if err != nil { + return nil, fmt.Errorf("获取探针安装配置失败: %w", err) + } + return &config, nil +} + +// SetAgentInstallConfig 设置探针安装配置 +func (s *PropertyService) SetAgentInstallConfig(ctx context.Context, config models.AgentInstallConfig) error { + return s.Set(ctx, PropertyIDAgentInstallConfig, "探针安装配置", config) +} + // defaultPropertyConfig 默认配置项定义 type defaultPropertyConfig struct { ID string @@ -393,6 +410,11 @@ func (s *PropertyService) InitializeDefaultConfigs(ctx context.Context) error { Name: "DNS 服务商配置", Value: []models.DNSProviderConfig{}, // 默认为空数组 }, + { + ID: PropertyIDAgentInstallConfig, + Name: "探针安装配置", + Value: models.AgentInstallConfig{ServerURL: ""}, // 默认空字符串,使用自动检测 + }, } // 遍历并初始化每个配置 diff --git a/pkg/agent/service/service.go b/pkg/agent/service/service.go index 7ff1d94..163bc84 100644 --- a/pkg/agent/service/service.go +++ b/pkg/agent/service/service.go @@ -111,7 +111,8 @@ type ServiceManager struct { const systemdScript = `[Unit] Description={{.Description}} ConditionFileIsExecutable={{.Path|cmdEscape}} -{{range .Dependencies}} {{.}} {{end}} +{{range $i, $dep := .Dependencies}} +{{$dep}} {{end}} [Service] StartLimitInterval=5 @@ -122,14 +123,20 @@ ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}} {{if .UserName}}User={{.UserName}}{{end}} {{if .ReloadSignal}}ExecReload=/bin/kill -{{.ReloadSignal}} "$MAINPID"{{end}} {{if .PIDFile}}PIDFile={{.PIDFile|cmd}}{{end}} -{{if .LogOutput}}StandardOutput={{.LogOutput}} -StandardError={{.LogOutput}}{{end}} -{{if .LogDirectory}}LogsDirectory={{.LogDirectory}}{{end}} -Restart=always +{{if and .LogOutput .HasOutputFileSupport -}} +StandardOutput=file:{{.LogDirectory}}/{{.Name}}.out +StandardError=file:{{.LogDirectory}}/{{.Name}}.err +{{- end}} +{{if gt .LimitNOFILE -1 }}LimitNOFILE={{.LimitNOFILE}}{{end}} +{{if .Restart}}Restart={{.Restart}}{{end}} +{{if .SuccessExitStatus}}SuccessExitStatus={{.SuccessExitStatus}}{{end}} RestartSec=5 -KillMode=process EnvironmentFile=-/etc/sysconfig/{{.Name}} +{{range $k, $v := .EnvVars -}} +Environment={{$k}}={{$v}} +{{end -}} + [Install] WantedBy=multi-user.target ` diff --git a/web/src/admin/pages/Agents/AgentInstallManual.tsx b/web/src/admin/pages/Agents/AgentInstallManual.tsx index 44baf45..0d9bc50 100644 --- a/web/src/admin/pages/Agents/AgentInstallManual.tsx +++ b/web/src/admin/pages/Agents/AgentInstallManual.tsx @@ -9,7 +9,6 @@ import { AgentInstallLayout, ApiChooser, ConfigHelper, - ServerUrlChecker, ServiceHelper, AGENT_NAME, AGENT_NAME_EXE, @@ -36,14 +35,11 @@ const DEFAULT_OS: OSType = 'linux-amd64'; const AgentInstallManual = () => { const { message } = App.useApp(); - const frontendUrl = useMemo(() => window.location.origin, []); const [selectedOS, setSelectedOS] = useState(DEFAULT_OS); const { apiKeys, selectedApiKey, setSelectedApiKey, - customAgentName, - setCustomAgentName, loading, backendServerUrl, apiKeyOptions, @@ -94,7 +90,6 @@ const AgentInstallManual = () => { const getManualInstallSteps = (os: OSType): InstallStep[] => { const config = osConfigs[os]; - const trimmedName = customAgentName.trim(); if (os.startsWith('windows')) { return [ @@ -108,7 +103,7 @@ Invoke-WebRequest -Uri "${backendServerUrl}${config.downloadUrl}?key=${selectedA }, { title: '2. 注册探针', - command: `.\\${AGENT_NAME_EXE} register --endpoint "${backendServerUrl}" --token "${selectedApiKey}"${trimmedName ? ` --name "${trimmedName}"` : ''}` + command: `.\\${AGENT_NAME_EXE} register --endpoint "${backendServerUrl}" --token "${selectedApiKey}"` }, { title: '3. 验证安装', @@ -136,7 +131,7 @@ curl -L "${backendServerUrl}${config.downloadUrl}?key=${selectedApiKey}" -o ${AG }, { title: '4. 注册探针', - command: `sudo ${AGENT_NAME} register --endpoint "${backendServerUrl}" --token "${selectedApiKey}"${trimmedName ? ` --name "${trimmedName}"` : ''}` + command: `sudo ${AGENT_NAME} register --endpoint "${backendServerUrl}" --token "${selectedApiKey}"` }, { title: '5. 验证安装', @@ -148,15 +143,12 @@ curl -L "${backendServerUrl}${config.downloadUrl}?key=${selectedApiKey}" -o ${AG return ( - { const { message } = App.useApp(); - const frontendUrl = useMemo(() => window.location.origin, []); const { apiKeys, selectedApiKey, @@ -28,14 +26,64 @@ const AgentInstallOneClick = () => { apiKeyOptions, } = useAgentInstallConfig(); + const [serverUrl, setServerUrl] = useState(''); + const [serverUrlError, setServerUrlError] = useState(''); + const [serverUrlLoading, setServerUrlLoading] = useState(true); + const effectiveServerUrl = serverUrl || backendServerUrl; + + // 加载服务端地址配置 + useEffect(() => { + const fetchConfig = async () => { + try { + const config = await getAgentInstallConfig(); + const configuredUrl = (config.serverUrl || '').trim(); + setServerUrl(configuredUrl); + setServerUrlError(configuredUrl ? '' : '请先配置服务端地址'); + } catch (error) { + console.error('加载服务端地址配置失败:', error); + } finally { + setServerUrlLoading(false); + } + }; + + void fetchConfig(); + }, []); + + // 保存服务端地址配置 + const handleServerUrlBlur = async (value: string) => { + const trimmed = value.trim(); + setServerUrl(trimmed); + if (!trimmed) { + setServerUrlError('请先配置服务端地址'); + message.error('请先配置服务端地址'); + return; + } + setServerUrlError(''); + + try { + await saveAgentInstallConfig({ serverUrl: trimmed }); + message.success('服务端地址已保存'); + } catch (error) { + console.error('保存服务端地址失败:', error); + message.error('保存服务端地址失败'); + } + }; + + const handleFillCurrentUrl = () => { + const currentUrl = window.location.origin; + setServerUrl(currentUrl); + setServerUrlError(''); + void handleServerUrlBlur(currentUrl); + }; + const installCommand = useMemo(() => { - if (!backendServerUrl || !selectedApiKey) { + if (!effectiveServerUrl || !selectedApiKey) { return ''; } const trimmedName = customAgentName.trim(); const nameParam = trimmedName ? `&name=${encodeURIComponent(trimmedName)}` : ''; - return `curl -fsSL "${backendServerUrl}/api/agent/install.sh?token=${selectedApiKey}${nameParam}" | sudo bash`; - }, [backendServerUrl, selectedApiKey, customAgentName]); + return `curl -fsSL "${effectiveServerUrl}/api/agent/install.sh?token=${selectedApiKey}${nameParam}" | sudo bash`; + }, [backendServerUrl, selectedApiKey, customAgentName, serverUrl]); const copyToClipboard = (text: string) => { copy(text); @@ -45,22 +93,97 @@ const AgentInstallOneClick = () => { return ( - - + + +
+
+ 服务端地址 (必填) +
+ + { + void handleServerUrlBlur(e.currentTarget.value); + }} + className="w-full" + /> + +
+ +
+ {serverUrlError ? ( +
{serverUrlError}
+ ) : ( +
+ 请配置可访问的服务端地址,否则无法生成安装命令 +
+ )} +
+ +
+
选择 API Token
+ {apiKeys.length === 0 ? ( + + 请先前往 API密钥管理 页面生成一个 API Token + + } + type="warning" + showIcon + className="mt-2" + /> + ) : ( + { + const trimmed = e.currentTarget.value.trim(); + setCustomAgentName(trimmed); + e.currentTarget.value = trimmed; + }} + className="w-full" + allowClear + /> +
+
+
+ + {!effectiveServerUrl && ( + + )} 脚本会自动检测系统架构并下载对应版本的探针,然后完成注册和安装。 @@ -74,7 +197,7 @@ const AgentInstallOneClick = () => { onClick={() => void copyToClipboard(installCommand)} icon={} style={{ margin: 0, padding: 0 }} - disabled={!selectedApiKey} + disabled={!selectedApiKey || !effectiveServerUrl} > 复制命令 diff --git a/web/src/admin/pages/Agents/AgentInstallShared.tsx b/web/src/admin/pages/Agents/AgentInstallShared.tsx index 83370d1..7ec0ad4 100644 --- a/web/src/admin/pages/Agents/AgentInstallShared.tsx +++ b/web/src/admin/pages/Agents/AgentInstallShared.tsx @@ -21,9 +21,7 @@ export type ApiChooserProps = { selectedApiKey: string; apiKeyOptions: ApiKeyOption[]; loading: boolean; - customAgentName: string; onSelectApiKey: (value: string) => void; - onCustomNameBlur: (value: string) => void; }; export const AgentInstallLayout = ({ activeKey, children }: { activeKey: InstallNavKey; children: ReactNode }) => { @@ -64,124 +62,39 @@ export const AgentInstallLayout = ({ activeKey, children }: { activeKey: Install ); }; -export const ServerUrlChecker = ({ backendServerUrl, frontendUrl }: { backendServerUrl: string; frontendUrl: string }) => { - const hasAddressMismatch = backendServerUrl && backendServerUrl !== frontendUrl; - - if (!hasAddressMismatch) { - return null; - } - - return ( - -
- 当前访问地址: {frontendUrl} -
- 后端检测地址: {backendServerUrl} -
-
- 这通常是因为您使用了反向代理,但未正确配置转发头部。 -
-
- 请在反向代理配置中添加以下头部: -
-
- Nginx 配置示例: -
-                            {`location / {
-    proxy_pass http://backend;
-    proxy_set_header X-Forwarded-Proto $scheme;
-    proxy_set_header X-Forwarded-Host $host;
-    proxy_set_header X-Real-IP $remote_addr;
-    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-}`}
-                        
-
-
- Caddy 配置示例: -
-                            {`reverse_proxy backend:8080 {
-    header_up X-Forwarded-Proto {scheme}
-    header_up X-Forwarded-Host {host}
-}`}
-                        
-
-
- Traefik 配置说明: -
-                            {`# Traefik 默认会自动添加 X-Forwarded-* 头部
-# 无需额外配置`}
-                        
-
-
- 配置完成后,刷新页面即可生效。 -
-
- } - type="warning" - showIcon - closable - /> - ); -}; - export const ApiChooser = ({ apiKeys, selectedApiKey, apiKeyOptions, loading, - customAgentName, onSelectApiKey, - onCustomNameBlur, }: ApiChooserProps) => ( - -
-
选择 API Token
- {apiKeys.length === 0 ? ( - - 请先前往 API密钥管理 页面生成一个 API Token - - } - type="warning" - showIcon - className="mt-2" - /> - ) : ( - { - const trimmed = e.currentTarget.value.trim(); - onCustomNameBlur(trimmed); - e.currentTarget.value = trimmed; - }} - className="w-full" - allowClear +
+
选择 API Token
+ {apiKeys.length === 0 ? ( + + 请先前往 API密钥管理 页面生成一个 API Token + + } + type="warning" + showIcon + className="mt-2" /> -
- + ) : ( +