From 92ceecc6ce96722613b3f0d9e48faa04d3680f66 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 22 Jun 2026 19:49:37 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E9=85=8D=E7=BD=AE=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Claude.md | 1 + docs/changelog/index.md | 3 + docs/docs.go | 12 +- docs/plan/of-options-migration.md | 191 ++++++++++ docs/swagger.json | 12 +- docs/swagger.yaml | 8 +- .../components/openflare-ops-utils.ts | 172 ++++----- .../settings/components/openflare-ops.tsx | 104 +++--- .../components/version-snapshot-sheet.tsx | 5 - .../components/performance-utils.ts | 294 +++++++-------- frontend/app/(main)/performance/page.tsx | 116 +++--- internal/apps/openflare/agent/helpers.go | 27 +- internal/apps/openflare/agent/logics.go | 2 +- .../apps/openflare/agent/middleware_test.go | 8 +- internal/apps/openflare/async_tasks.go | 18 +- internal/apps/openflare/async_tasks_test.go | 49 +-- .../apps/openflare/config_version/snapshot.go | 124 ++++--- internal/apps/openflare/dashboard/helpers.go | 4 +- internal/apps/openflare/flared/helpers.go | 4 +- internal/apps/openflare/flared/logics.go | 2 +- .../openflare/flared/observability_test.go | 4 +- internal/apps/openflare/geoip/runtime.go | 30 +- internal/apps/openflare/geoip/runtime_test.go | 14 +- .../integration/agent_protocol_test.go | 5 +- .../openflare/integration/auth_option_test.go | 36 +- .../openflare/integration/core_chain_test.go | 7 +- .../openflare/integration/security_test.go | 6 +- internal/apps/openflare/node/helpers.go | 5 +- internal/apps/openflare/node/logics.go | 35 +- internal/apps/openflare/node/logics_test.go | 16 +- internal/apps/openflare/option/logics.go | 101 ++--- internal/apps/openflare/option/logics_test.go | 68 +++- .../openflare/option/openresty_validators.go | 76 ++-- internal/apps/openflare/option/validate.go | 72 ++-- internal/apps/openflare/relay/helpers.go | 26 +- internal/apps/openflare/relay/logics.go | 2 +- internal/apps/openflare/relay/logics_test.go | 5 +- .../apps/openflare/tasks/database_cleanup.go | 15 +- .../openflare/tasks/database_cleanup_test.go | 12 +- internal/apps/openflare/uptimekuma/sync.go | 111 ++++-- .../apps/openflare/uptimekuma/sync_helpers.go | 29 +- .../apps/openflare/uptimekuma/sync_test.go | 100 ++--- internal/db/migrator/bridge_test.go | 16 +- ...4_migrate_of_options_to_system_configs.sql | 347 ++++++++++++++++++ .../postgres/202606220005_drop_of_options.sql | 10 + ...4_migrate_of_options_to_system_configs.sql | 291 +++++++++++++++ .../sqlite/202606220005_drop_of_options.sql | 10 + internal/db/migrator/migrator.go | 36 ++ internal/db/migrator/migrator_test.go | 4 +- internal/model/auth_source.go | 10 +- internal/model/openflare_option.go | 273 +------------- internal/model/openflare_option_apply.go | 86 ----- internal/model/system_configs.go | 66 +++- 53 files changed, 1928 insertions(+), 1152 deletions(-) create mode 120000 Claude.md create mode 100644 docs/plan/of-options-migration.md create mode 100644 internal/db/migrator/goose/postgres/202606220004_migrate_of_options_to_system_configs.sql create mode 100644 internal/db/migrator/goose/postgres/202606220005_drop_of_options.sql create mode 100644 internal/db/migrator/goose/sqlite/202606220004_migrate_of_options_to_system_configs.sql create mode 100644 internal/db/migrator/goose/sqlite/202606220005_drop_of_options.sql delete mode 100644 internal/model/openflare_option_apply.go diff --git a/Claude.md b/Claude.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/Claude.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/docs/changelog/index.md b/docs/changelog/index.md index bedae03c..46c0359b 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -23,12 +23,15 @@ sidebar: false ### 修复 +- 修复 `/api/v1/d/option` 批量更新 OpenResty 等业务配置不生效的问题。根本原因是 option 模块在读写时做了 PascalCase 与 snake_case 的机械转换(如 `OpenRestyEventsUse` → `open_resty_events_use`),与 `w_system_configs` 中实际 key(`openresty_events_use`)不一致,更新写入了错误的幽灵配置行。现改为 API 直接使用与数据库一致的 snake_case key,并同步更新前端性能调优与运维设置页。 +- 修复 PostgreSQL 数据库执行迁移时报 `duplicate key value violates unique constraint "goose_db_version_pkey"` 导致迁移中断的问题。根本原因:`goose_db_version.id` 自增序列落后于表内 `MAX(id)`(常见于从 dump 恢复或历史迁移以显式 id 复制版本记录后),goose 记录新版本号时自增 id 与既有行冲突。修复方式:在 `goose.Up` 前对 PostgreSQL 执行 `setval` 重新对齐 `goose_db_version` 的 id 序列。 - 修复 openflared(Tunnel Client)WebSocket 连接在 Cloudflare 代理环境下频繁收到 EOF 断连的问题。根本原因:服务端 `read_pump` 仅在收到 WebSocket 协议层 Pong 帧时刷新读超时,而客户端(`golang.org/x/net/websocket`)以 JSON 应用层 `{"type":"pong"}` 响应 ping,服务端 90s 读超时到期后主动关闭连接,客户端收到 EOF 并进入无限重连循环。修复方式:在 `clientPongType` 分支中同步调用 `conn.SetReadDeadline` 刷新超时。 - 修复 openflared frpc 子进程异常退出(`exit status 1`)时缺乏详细诊断信息的问题。现捕获 frpc stderr 并在进程退出时将其输出记录到结构化日志 `stderr` 字段,便于排查配置格式错误、Auth Token 鉴权失败、relay 端不可达等具体原因。 - 修复 Relay 节点启动时在双栈网络环境可能上报 IPv6 地址,导致 Tunnel frpc 客户端无法连接 frps 的问题。强化 `pkg/geoip.HTTPOutboundIPStrategy` 在回退到双栈客户端后仍优先返回 IPv4 地址,确保 Relay 心跳上报的 IP 与 frpc 连接兼容。 ### 变更 +- 将 OpenFlare 配置体系从独立的 `of_options` 表统一迁移至标准系统配置框架 `w_system_configs`(SystemConfig),全部归类为业务类型(`type=business`)。涵盖 Agent(心跳间隔、发现令牌、更新仓库等)、UptimeKuma 集成、GeoIP、数据库自动清理及全部 OpenResty 主配置项共 48 项。业务代码统一改为通过 `repository.GetSystemConfigByKey`/`GetBoolByKey`/`GetIntByKey` 读取,移除进程级内存快照 `OptionMap` 与启动时热重载机制,配置变更经 Redis 缓存失效实现动态生效。已存在的同义配置(如 `password_login_enabled`、`smtp_host`)不重复迁移,旧系统遗留的 `SystemName`、`Footer`、`HomePageLink`、`About` 等无用项一并清理;公开状态接口 `/api/v1/d/status` 相应移除 `system_name`、`home_page_link`、`footer_html` 字段。数据迁移完成后通过 goose 迁移 `202606220005` 删除遗留的 `of_options` 表。 - 优化并统一 `agent`、`relay`、`flared` 的 IP 探测与上报逻辑,均复用公用 `nodeip` 包;在未指定 `node_ip` 时实现心跳 Tick 动态探测上报。 - 优化 `pkg/geoip.GetOutboundIP` 出口 IP 探测策略,优先通过 `tcp4` 建立 HTTP 连接以获得 IPv4 公网地址,并在纯 IPv6/无 IPv4 路由环境下自动降级为双栈 `tcp` 握手。 - 优化 `agent` 系统指纹缓存算法,计算指纹时排除 `UptimeSeconds` 和 `ReportedAtUnix` 动态字段,防止周期心跳时不断触发冗余完整的系统 Profile 数据上报。 diff --git a/docs/docs.go b/docs/docs.go index 7edc39a8..3b17ec0c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -16173,12 +16173,6 @@ const docTemplate = `{ "email_verification": { "type": "boolean" }, - "footer_html": { - "type": "string" - }, - "home_page_link": { - "type": "string" - }, "password_register_enabled": { "type": "boolean" }, @@ -16188,9 +16182,6 @@ const docTemplate = `{ "start_time": { "type": "integer" }, - "system_name": { - "type": "string" - }, "version": { "type": "string" } @@ -16617,6 +16608,9 @@ const docTemplate = `{ }, "web_server_enabled": { "type": "boolean" + }, + "web_server_port": { + "type": "integer" } } }, diff --git a/docs/plan/of-options-migration.md b/docs/plan/of-options-migration.md new file mode 100644 index 00000000..dcfe1843 --- /dev/null +++ b/docs/plan/of-options-migration.md @@ -0,0 +1,191 @@ +# of_options 迁移到 w_system_configs 方案 + +## 背景 + +当前 OpenFlare 使用 `of_options` 表存储系统配置,维护了独立的 OptionMap 内存缓存和热重载机制。为了统一配置管理框架,需要将所有配置迁移到标准的 `w_system_configs` 表,复用现有的 SystemConfig 读取 API 和 Redis 缓存机制。 + +## 配置项分类 + +### 已存在于 w_system_configs,无需迁移 + +以下配置在新系统中已存在,不从 of_options 迁移: + +- `PasswordLoginEnabled` → `password_login_enabled` (已存在) +- `CapLoginEnabled` → `cap_login_enabled` (已存在) +- `PasswordRegisterEnabled` → `password_register_enabled` (已存在) +- `EmailVerificationEnabled` → 映射到 `email_login_verification_enabled` (已存在) +- `ServerAddress` → `server_address` (已存在) +- `SMTPServer` → `smtp_host` (已存在,字段名不同) +- `SMTPPort` → `smtp_port` (已存在) +- `SMTPAccount` → `smtp_username` (已存在,字段名不同) +- `SMTPToken` → `smtp_password` (已存在,字段名不同) + +### 旧系统冗余配置,直接删除 + +以下配置是旧系统遗留,当前系统不使用,不迁移: + +- `SystemName` - 前端不再使用系统名称配置 +- `Footer` - 前端不再使用页脚 HTML +- `HomePageLink` - 前端不再使用首页链接 +- `About` - 前端不再使用关于信息 + +### 需要迁移的配置(全部 type=business) + +**所有迁移的配置都设为 business 类型**。system 类型仅用于框架级配置(如 upload_allowed_extensions、disk_cache_max_size_mb 等)。 + +#### Agent 相关配置 (business, visibility=0) + +| 原 Key (PascalCase) | 新 Key (snake_case) | 默认值 | 说明 | +|---------------------|---------------------|--------|------| +| AgentDiscoveryToken | agent_discovery_token | "" | Agent 发现令牌(敏感) | +| AgentHeartbeatInterval | agent_heartbeat_interval | 10000 | Agent 心跳间隔(毫秒) | +| AgentWebsocketUpgradeEnabled | agent_websocket_upgrade_enabled | true | Agent WebSocket 升级开关 | +| NodeOfflineThreshold | node_offline_threshold | 120000 | 节点离线阈值(毫秒) | +| AgentUpdateRepo | agent_update_repo | Rain-kl/OpenFlare | Agent 更新仓库 | + +#### 系统功能配置 (business, visibility=0) + +| 原 Key (PascalCase) | 新 Key (snake_case) | 默认值 | 说明 | +|---------------------|---------------------|--------|------| +| GeoIPProvider | geoip_provider | ipinfo | GeoIP 服务商 | +| DatabaseAutoCleanupEnabled | database_auto_cleanup_enabled | false | 数据库自动清理开关 | +| DatabaseAutoCleanupRetentionDays | database_auto_cleanup_retention_days | 30 | 数据库保留天数 | + +#### UptimeKuma 集成配置 (business, visibility=0) + +| 原 Key (PascalCase) | 新 Key (snake_case) | 默认值 | 说明 | +|---------------------|---------------------|--------|------| +| UptimeKumaEnabled | uptime_kuma_enabled | false | UptimeKuma 集成开关 | +| UptimeKumaUrl | uptime_kuma_url | "" | UptimeKuma URL | +| UptimeKumaUsername | uptime_kuma_username | "" | UptimeKuma 用户名 | +| UptimeKumaPassword | uptime_kuma_password | "" | UptimeKuma 密码(敏感) | +| UptimeKumaMonitorScope | uptime_kuma_monitor_scope | all | UptimeKuma 监控范围 | +| UptimeKumaSelectedSites | uptime_kuma_selected_sites | "" | UptimeKuma 选定站点 | +| UptimeKumaSyncInterval | uptime_kuma_sync_interval | 5 | UptimeKuma 同步间隔(分钟) | +| UptimeKumaInterval | uptime_kuma_interval | 60 | UptimeKuma 监控间隔(秒) | +| UptimeKumaRetry | uptime_kuma_retry | 0 | UptimeKuma 重试次数 | +| UptimeKumaRetryInterval | uptime_kuma_retry_interval | 60 | UptimeKuma 重试间隔(秒) | +| UptimeKumaTimeout | uptime_kuma_timeout | 48 | UptimeKuma 超时(秒) | + +### OpenResty 配置 (type=business) + +OpenResty 反向代理和缓存配置,全部为 business 类型,visibility=0: + +| 原 Key (PascalCase) | 新 Key (snake_case) | 默认值 | +|---------------------|---------------------|--------| +| OpenRestyDefaultServerReturnStatus | openresty_default_server_return_status | 421 | +| OpenRestyWorkerProcesses | openresty_worker_processes | auto | +| OpenRestyWorkerConnections | openresty_worker_connections | 4096 | +| OpenRestyWorkerRlimitNofile | openresty_worker_rlimit_nofile | 65535 | +| OpenRestyEventsUse | openresty_events_use | epoll | +| OpenRestyEventsMultiAcceptEnabled | openresty_events_multi_accept_enabled | true | +| OpenRestyKeepaliveTimeout | openresty_keepalive_timeout | 20 | +| OpenRestyKeepaliveRequests | openresty_keepalive_requests | 1000 | +| OpenRestyClientHeaderTimeout | openresty_client_header_timeout | 15 | +| OpenRestyClientBodyTimeout | openresty_client_body_timeout | 15 | +| OpenRestyClientMaxBodySize | openresty_client_max_body_size | 64m | +| OpenRestyLargeClientHeaderBuffers | openresty_large_client_header_buffers | 4 16k | +| OpenRestySendTimeout | openresty_send_timeout | 30 | +| OpenRestyResolvers | openresty_resolvers | "" | +| OpenRestyProxyConnectTimeout | openresty_proxy_connect_timeout | 3 | +| OpenRestyProxySendTimeout | openresty_proxy_send_timeout | 60 | +| OpenRestyProxyReadTimeout | openresty_proxy_read_timeout | 60 | +| OpenRestyWebsocketEnabled | openresty_websocket_enabled | true | +| OpenRestyHTTP3Enabled | openresty_http3_enabled | true | +| OpenRestyProxyRequestBufferingEnabled | openresty_proxy_request_buffering_enabled | false | +| OpenRestyProxyBufferingEnabled | openresty_proxy_buffering_enabled | true | +| OpenRestyProxyBuffers | openresty_proxy_buffers | 16 16k | +| OpenRestyProxyBufferSize | openresty_proxy_buffer_size | 8k | +| OpenRestyProxyBusyBuffersSize | openresty_proxy_busy_buffers_size | 64k | +| OpenRestyGzipEnabled | openresty_gzip_enabled | true | +| OpenRestyGzipMinLength | openresty_gzip_min_length | 1024 | +| OpenRestyGzipCompLevel | openresty_gzip_comp_level | 5 | +| OpenRestyCacheEnabled | openresty_cache_enabled | false | +| OpenRestyCachePath | openresty_cache_path | "" | +| OpenRestyCacheLevels | openresty_cache_levels | 1:2 | +| OpenRestyCacheInactive | openresty_cache_inactive | 30m | +| OpenRestyCacheMaxSize | openresty_cache_max_size | 1g | +| OpenRestyCacheKeyTemplate | openresty_cache_key_template | $scheme$host$request_uri | +| OpenRestyCacheLockEnabled | openresty_cache_lock_enabled | true | +| OpenRestyCacheLockTimeout | openresty_cache_lock_timeout | 5s | +| OpenRestyCacheUseStale | openresty_cache_use_stale | error timeout updating http_500... | +| OpenRestyMainConfigTemplate | openresty_main_config_template | (长模板) | + +**总计**:约 58 个配置需要迁移,全部为 business 类型。 + +## 迁移策略 + +### 1. 保持向后兼容 + +- 在迁移期间同时支持旧 API (`/api/v1/openflare/options`) 和新 API (`/api/v1/admin/system-configs`) +- 旧 API 内部委派到 SystemConfig 读写,不再直接操作 of_options 表 +- 保留 `/api/v1/openflare/status` 接口,但从 SystemConfig 读取数据 + +### 2. 数据迁移顺序 + +1. 创建新的 ConfigKey 常量(已完成) +2. 创建 goose 迁移脚本,将 of_options 真正需要的配置复制到 w_system_configs(已完成) +3. 重构代码使用 repository.GetSystemConfigByKey / GetBoolByKey / GetIntByKey +4. 标记 of_options 表为 deprecated(保留一段时间用于回滚) +5. 后续版本完全删除 of_options 相关代码 + +### 3. 配置类型说明 + +**所有从 of_options 迁移的配置都设为 business 类型**: +- `type='business'`:业务配置,影响业务规则和功能行为 +- `type='system'`:框架配置,仅用于框架级设置(如 upload_allowed_extensions、disk_cache_max_size_mb) + +**不迁移的配置**: +- 已存在于 w_system_configs 的配置(如 password_login_enabled、smtp_host) +- 旧系统冗余配置(SystemName、Footer、HomePageLink、About) + +### 4. 包级变量处理 + +原 `openflare_option.go` 中的包级变量(如 `SystemName`、`PasswordLoginEnabled`)将被移除。所有读取改为: + +```go +// 旧方式(包级变量) +systemName := model.SystemName +enabled := model.PasswordLoginEnabled + +// 新方式(repository 读取) +systemName, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySystemName) +enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordLoginEnabled) +``` + +注意:对于已存在的配置,使用对应的新 key: +```go +// 已存在的配置使用现有 key +enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordLoginEnabled) // 不是 ConfigKeySystemName +``` + +### 5. 热重载机制 + +- 移除 `InitOptionMap` 和 `OptionMapRWMutex` +- SystemConfig 已通过 Redis 缓存实现热重载,更新后自动失效 + +## 实现步骤 + +1. ✅ 分析配置项并设计迁移方案(本文档) +2. 在 `system_configs.go` 添加所有 ConfigKey 常量 +3. 创建 goose 迁移脚本(PostgreSQL + SQLite) +4. 重构所有业务代码使用 SystemConfig API +5. 更新 option 模块 API 和测试 +6. 清理 of_options 遗留代码并验证 + +## 风险与注意事项 + +1. **敏感配置处理**:包含 Token/Password/Secret 的配置不应暴露给前端(visibility=0) +2. **类型转换**:of_options 将数值存为字符串,需要在读取时正确转换 +3. **默认值一致性**:确保 SQL 迁移中的默认值与代码中的默认值一致 +4. **模板配置**:`OpenRestyMainConfigTemplate` 是长文本,需要正确处理 +5. **测试覆盖**:所有使用 OptionMap 的测试需要更新为使用 SystemConfig + +## 后续清理计划 + +迁移完成后,在下一个主版本(如 v2.0)中完全移除: + +- `internal/model/openflare_option.go` +- `internal/model/openflare_option_apply.go` +- `of_options` 表及相关迁移文件 +- `internal/apps/openflare/option` 模块(或重构为 SystemConfig 的代理) diff --git a/docs/swagger.json b/docs/swagger.json index 5501c74b..b079dd27 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -16166,12 +16166,6 @@ "email_verification": { "type": "boolean" }, - "footer_html": { - "type": "string" - }, - "home_page_link": { - "type": "string" - }, "password_register_enabled": { "type": "boolean" }, @@ -16181,9 +16175,6 @@ "start_time": { "type": "integer" }, - "system_name": { - "type": "string" - }, "version": { "type": "string" } @@ -16610,6 +16601,9 @@ }, "web_server_enabled": { "type": "boolean" + }, + "web_server_port": { + "type": "integer" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d2519bda..2178e11a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2300,18 +2300,12 @@ definitions: type: boolean email_verification: type: boolean - footer_html: - type: string - home_page_link: - type: string password_register_enabled: type: boolean server_address: type: string start_time: type: integer - system_name: - type: string version: type: string type: object @@ -2592,6 +2586,8 @@ definitions: type: integer web_server_enabled: type: boolean + web_server_port: + type: integer type: object protocol.RelayProxyStat: properties: diff --git a/frontend/app/(main)/admin/settings/components/openflare-ops-utils.ts b/frontend/app/(main)/admin/settings/components/openflare-ops-utils.ts index 0855744e..5328df55 100644 --- a/frontend/app/(main)/admin/settings/components/openflare-ops-utils.ts +++ b/frontend/app/(main)/admin/settings/components/openflare-ops-utils.ts @@ -1,47 +1,47 @@ import type {OptionItem} from "@/lib/services/openflare" export type OpenFlareOpsFields = { - AgentHeartbeatInterval: string - AgentWebsocketUpgradeEnabled: boolean - NodeOfflineThreshold: string - AgentUpdateRepo: string - GeoIPProvider: string - ServerAddress: string - UptimeKumaEnabled: boolean - UptimeKumaUrl: string - UptimeKumaUsername: string - UptimeKumaPassword: string - UptimeKumaMonitorScope: string - UptimeKumaSelectedSites: string - UptimeKumaSyncInterval: string - UptimeKumaInterval: string - UptimeKumaRetry: string - UptimeKumaRetryInterval: string - UptimeKumaTimeout: string - DatabaseAutoCleanupEnabled: boolean - DatabaseAutoCleanupRetentionDays: string + agent_heartbeat_interval: string + agent_websocket_upgrade_enabled: boolean + node_offline_threshold: string + agent_update_repo: string + geoip_provider: string + server_address: string + uptime_kuma_enabled: boolean + uptime_kuma_url: string + uptime_kuma_username: string + uptime_kuma_password: string + uptime_kuma_monitor_scope: string + uptime_kuma_selected_sites: string + uptime_kuma_sync_interval: string + uptime_kuma_interval: string + uptime_kuma_retry: string + uptime_kuma_retry_interval: string + uptime_kuma_timeout: string + database_auto_cleanup_enabled: boolean + database_auto_cleanup_retention_days: string } export const defaultOpenFlareOpsFields: OpenFlareOpsFields = { - AgentHeartbeatInterval: "10000", - AgentWebsocketUpgradeEnabled: true, - NodeOfflineThreshold: "120000", - AgentUpdateRepo: "Rain-kl/OpenFlare", - GeoIPProvider: "ipinfo", - ServerAddress: "", - UptimeKumaEnabled: false, - UptimeKumaUrl: "", - UptimeKumaUsername: "", - UptimeKumaPassword: "", - UptimeKumaMonitorScope: "all", - UptimeKumaSelectedSites: "", - UptimeKumaSyncInterval: "5", - UptimeKumaInterval: "60", - UptimeKumaRetry: "0", - UptimeKumaRetryInterval: "60", - UptimeKumaTimeout: "48", - DatabaseAutoCleanupEnabled: false, - DatabaseAutoCleanupRetentionDays: "30", + agent_heartbeat_interval: "10000", + agent_websocket_upgrade_enabled: true, + node_offline_threshold: "120000", + agent_update_repo: "Rain-kl/OpenFlare", + geoip_provider: "ipinfo", + server_address: "", + uptime_kuma_enabled: false, + uptime_kuma_url: "", + uptime_kuma_username: "", + uptime_kuma_password: "", + uptime_kuma_monitor_scope: "all", + uptime_kuma_selected_sites: "", + uptime_kuma_sync_interval: "5", + uptime_kuma_interval: "60", + uptime_kuma_retry: "0", + uptime_kuma_retry_interval: "60", + uptime_kuma_timeout: "48", + database_auto_cleanup_enabled: false, + database_auto_cleanup_retention_days: "30", } export const INSTALLER_SCRIPT_URL = @@ -64,25 +64,25 @@ export function mapOptionsToOpsFields( serverAddress = "", ): OpenFlareOpsFields { return { - AgentHeartbeatInterval: optionMap.AgentHeartbeatInterval ?? "10000", - AgentWebsocketUpgradeEnabled: toBoolean(optionMap.AgentWebsocketUpgradeEnabled, true), - NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? "120000", - AgentUpdateRepo: optionMap.AgentUpdateRepo ?? "Rain-kl/OpenFlare", - GeoIPProvider: optionMap.GeoIPProvider ?? "ipinfo", - ServerAddress: optionMap.ServerAddress || serverAddress, - UptimeKumaEnabled: toBoolean(optionMap.UptimeKumaEnabled, false), - UptimeKumaUrl: optionMap.UptimeKumaUrl ?? "", - UptimeKumaUsername: optionMap.UptimeKumaUsername ?? "", - UptimeKumaPassword: "", - UptimeKumaMonitorScope: optionMap.UptimeKumaMonitorScope ?? "all", - UptimeKumaSelectedSites: optionMap.UptimeKumaSelectedSites ?? "", - UptimeKumaSyncInterval: optionMap.UptimeKumaSyncInterval ?? "5", - UptimeKumaInterval: optionMap.UptimeKumaInterval ?? "60", - UptimeKumaRetry: optionMap.UptimeKumaRetry ?? "0", - UptimeKumaRetryInterval: optionMap.UptimeKumaRetryInterval ?? "60", - UptimeKumaTimeout: optionMap.UptimeKumaTimeout ?? "48", - DatabaseAutoCleanupEnabled: toBoolean(optionMap.DatabaseAutoCleanupEnabled, false), - DatabaseAutoCleanupRetentionDays: optionMap.DatabaseAutoCleanupRetentionDays ?? "30", + agent_heartbeat_interval: optionMap.agent_heartbeat_interval ?? "10000", + agent_websocket_upgrade_enabled: toBoolean(optionMap.agent_websocket_upgrade_enabled, true), + node_offline_threshold: optionMap.node_offline_threshold ?? "120000", + agent_update_repo: optionMap.agent_update_repo ?? "Rain-kl/OpenFlare", + geoip_provider: optionMap.geoip_provider ?? "ipinfo", + server_address: optionMap.server_address || serverAddress, + uptime_kuma_enabled: toBoolean(optionMap.uptime_kuma_enabled, false), + uptime_kuma_url: optionMap.uptime_kuma_url ?? "", + uptime_kuma_username: optionMap.uptime_kuma_username ?? "", + uptime_kuma_password: "", + uptime_kuma_monitor_scope: optionMap.uptime_kuma_monitor_scope ?? "all", + uptime_kuma_selected_sites: optionMap.uptime_kuma_selected_sites ?? "", + uptime_kuma_sync_interval: optionMap.uptime_kuma_sync_interval ?? "5", + uptime_kuma_interval: optionMap.uptime_kuma_interval ?? "60", + uptime_kuma_retry: optionMap.uptime_kuma_retry ?? "0", + uptime_kuma_retry_interval: optionMap.uptime_kuma_retry_interval ?? "60", + uptime_kuma_timeout: optionMap.uptime_kuma_timeout ?? "48", + database_auto_cleanup_enabled: toBoolean(optionMap.database_auto_cleanup_enabled, false), + database_auto_cleanup_retention_days: optionMap.database_auto_cleanup_retention_days ?? "30", } } @@ -111,8 +111,8 @@ export function buildDiscoveryCommand(serverUrl: string, discoveryToken: string) } export function validateAgentFields(fields: OpenFlareOpsFields) { - const heartbeat = Number.parseInt(fields.AgentHeartbeatInterval, 10) - const offline = Number.parseInt(fields.NodeOfflineThreshold, 10) + const heartbeat = Number.parseInt(fields.agent_heartbeat_interval, 10) + const offline = Number.parseInt(fields.node_offline_threshold, 10) if (Number.isNaN(heartbeat) || heartbeat < 5000) { throw new Error("心跳间隔不能小于 5000 毫秒。") } @@ -122,15 +122,15 @@ export function validateAgentFields(fields: OpenFlareOpsFields) { } export function validateUptimeKumaFields(fields: OpenFlareOpsFields) { - const syncInt = Number.parseInt(fields.UptimeKumaSyncInterval, 10) - const interval = Number.parseInt(fields.UptimeKumaInterval, 10) - const retry = Number.parseInt(fields.UptimeKumaRetry, 10) - const retryInt = Number.parseInt(fields.UptimeKumaRetryInterval, 10) - const timeout = Number.parseInt(fields.UptimeKumaTimeout, 10) + const syncInt = Number.parseInt(fields.uptime_kuma_sync_interval, 10) + const interval = Number.parseInt(fields.uptime_kuma_interval, 10) + const retry = Number.parseInt(fields.uptime_kuma_retry, 10) + const retryInt = Number.parseInt(fields.uptime_kuma_retry_interval, 10) + const timeout = Number.parseInt(fields.uptime_kuma_timeout, 10) - if (fields.UptimeKumaEnabled) { - if (!fields.UptimeKumaUrl.trim()) throw new Error("请输入 Uptime Kuma 地址。") - if (!fields.UptimeKumaUsername.trim()) throw new Error("请输入 Uptime Kuma 用户名。") + if (fields.uptime_kuma_enabled) { + if (!fields.uptime_kuma_url.trim()) throw new Error("请输入 Uptime Kuma 地址。") + if (!fields.uptime_kuma_username.trim()) throw new Error("请输入 Uptime Kuma 用户名。") } if (Number.isNaN(syncInt) || syncInt <= 0) throw new Error("同步间隔必须为正整数。") if (Number.isNaN(interval) || interval <= 0) throw new Error("心跳间隔必须为正整数。") @@ -140,7 +140,7 @@ export function validateUptimeKumaFields(fields: OpenFlareOpsFields) { } export function validateDatabaseAutoCleanup(fields: OpenFlareOpsFields) { - const retentionDays = Number.parseInt(fields.DatabaseAutoCleanupRetentionDays, 10) + const retentionDays = Number.parseInt(fields.database_auto_cleanup_retention_days, 10) if (Number.isNaN(retentionDays) || retentionDays < 1) { throw new Error("自动清理保留天数至少为 1 天。") } @@ -149,35 +149,35 @@ export function validateDatabaseAutoCleanup(fields: OpenFlareOpsFields) { export function agentOptionEntries(fields: OpenFlareOpsFields): OptionItem[] { validateAgentFields(fields) return [ - { key: "AgentHeartbeatInterval", value: fields.AgentHeartbeatInterval }, - { key: "AgentWebsocketUpgradeEnabled", value: String(fields.AgentWebsocketUpgradeEnabled) }, - { key: "NodeOfflineThreshold", value: fields.NodeOfflineThreshold }, - { key: "AgentUpdateRepo", value: fields.AgentUpdateRepo.trim() }, - { key: "GeoIPProvider", value: fields.GeoIPProvider }, + { key: "agent_heartbeat_interval", value: fields.agent_heartbeat_interval }, + { key: "agent_websocket_upgrade_enabled", value: String(fields.agent_websocket_upgrade_enabled) }, + { key: "node_offline_threshold", value: fields.node_offline_threshold }, + { key: "agent_update_repo", value: fields.agent_update_repo.trim() }, + { key: "geoip_provider", value: fields.geoip_provider }, ] } export function uptimeKumaOptionEntries(fields: OpenFlareOpsFields): OptionItem[] { validateUptimeKumaFields(fields) return [ - { key: "UptimeKumaEnabled", value: String(fields.UptimeKumaEnabled) }, - { key: "UptimeKumaUrl", value: fields.UptimeKumaUrl.trim() }, - { key: "UptimeKumaUsername", value: fields.UptimeKumaUsername.trim() }, - { key: "UptimeKumaPassword", value: fields.UptimeKumaPassword }, - { key: "UptimeKumaMonitorScope", value: fields.UptimeKumaMonitorScope }, - { key: "UptimeKumaSelectedSites", value: fields.UptimeKumaSelectedSites }, - { key: "UptimeKumaSyncInterval", value: fields.UptimeKumaSyncInterval }, - { key: "UptimeKumaInterval", value: fields.UptimeKumaInterval }, - { key: "UptimeKumaRetry", value: fields.UptimeKumaRetry }, - { key: "UptimeKumaRetryInterval", value: fields.UptimeKumaRetryInterval }, - { key: "UptimeKumaTimeout", value: fields.UptimeKumaTimeout }, + { key: "uptime_kuma_enabled", value: String(fields.uptime_kuma_enabled) }, + { key: "uptime_kuma_url", value: fields.uptime_kuma_url.trim() }, + { key: "uptime_kuma_username", value: fields.uptime_kuma_username.trim() }, + { key: "uptime_kuma_password", value: fields.uptime_kuma_password }, + { key: "uptime_kuma_monitor_scope", value: fields.uptime_kuma_monitor_scope }, + { key: "uptime_kuma_selected_sites", value: fields.uptime_kuma_selected_sites }, + { key: "uptime_kuma_sync_interval", value: fields.uptime_kuma_sync_interval }, + { key: "uptime_kuma_interval", value: fields.uptime_kuma_interval }, + { key: "uptime_kuma_retry", value: fields.uptime_kuma_retry }, + { key: "uptime_kuma_retry_interval", value: fields.uptime_kuma_retry_interval }, + { key: "uptime_kuma_timeout", value: fields.uptime_kuma_timeout }, ] } export function databaseAutoCleanupEntries(fields: OpenFlareOpsFields): OptionItem[] { validateDatabaseAutoCleanup(fields) return [ - { key: "DatabaseAutoCleanupEnabled", value: String(fields.DatabaseAutoCleanupEnabled) }, - { key: "DatabaseAutoCleanupRetentionDays", value: fields.DatabaseAutoCleanupRetentionDays }, + { key: "database_auto_cleanup_enabled", value: String(fields.database_auto_cleanup_enabled) }, + { key: "database_auto_cleanup_retention_days", value: fields.database_auto_cleanup_retention_days }, ] } diff --git a/frontend/app/(main)/admin/settings/components/openflare-ops.tsx b/frontend/app/(main)/admin/settings/components/openflare-ops.tsx index aac38819..9aba6c8c 100644 --- a/frontend/app/(main)/admin/settings/components/openflare-ops.tsx +++ b/frontend/app/(main)/admin/settings/components/openflare-ops.tsx @@ -27,8 +27,8 @@ import {Textarea} from "@/components/ui/textarea" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import type {DatabaseCleanupTarget} from "@/lib/services/openflare" -import {AdminStatusService} from "@/lib/services/admin" import {NodeService, OptionService, StatusService, UptimeKumaService,} from "@/lib/services/openflare" +import {AdminStatusService} from "@/lib/services/admin" import {VersionUpgradeDialog} from "@/app/(main)/components/version-upgrade-dialog" import {adminUpdateStatusQueryKey, openflarePublicStatusQueryKey,} from "@/lib/hooks/use-openflare-server-upgrade" @@ -111,14 +111,14 @@ export function OpenFlareOpsSettings() { if (!optionsQuery.data) return const optionMap = optionsToMap(optionsQuery.data) const serverAddress = - optionMap.ServerAddress || + optionMap.server_address || statusQuery.data?.server_address || getBrowserOrigin() setFields(mapOptionsToOpsFields(optionMap, serverAddress)) }, [optionsQuery.data, statusQuery.data?.server_address]) const geoIPMutation = useMutation({ - mutationFn: () => OptionService.lookupGeoIP(fields.GeoIPProvider, geoIPTestIP.trim()), + mutationFn: () => OptionService.lookupGeoIP(fields.geoip_provider, geoIPTestIP.trim()), }) const saveMutation = useMutation({ @@ -186,9 +186,9 @@ export function OpenFlareOpsSettings() { const discoveryToken = bootstrapQuery.data?.discovery_token ?? "" const discoveryCommand = useMemo(() => { - if (!fields.ServerAddress || !discoveryToken) return "" - return buildDiscoveryCommand(fields.ServerAddress, discoveryToken) - }, [discoveryToken, fields.ServerAddress]) + if (!fields.server_address || !discoveryToken) return "" + return buildDiscoveryCommand(fields.server_address, discoveryToken) + }, [discoveryToken, fields.server_address]) const updateField = ( key: K, @@ -266,29 +266,29 @@ export function OpenFlareOpsSettings() {
updateField("AgentHeartbeatInterval", value)} + onChange={(value) => updateField("agent_heartbeat_interval", value)} /> updateField("NodeOfflineThreshold", value)} + onChange={(value) => updateField("node_offline_threshold", value)} />
updateField("AgentWebsocketUpgradeEnabled", value)} + checked={fields.agent_websocket_upgrade_enabled} + onChange={(value) => updateField("agent_websocket_upgrade_enabled", value)} /> updateField("AgentUpdateRepo", value)} + onChange={(value) => updateField("agent_update_repo", value)} />
@@ -314,8 +314,8 @@ export function OpenFlareOpsSettings() {
updateField("UptimeKumaMonitorScope", value)} + value={fields.uptime_kuma_monitor_scope} + onValueChange={(value) => updateField("uptime_kuma_monitor_scope", value)} > @@ -492,7 +492,7 @@ export function OpenFlareOpsSettings() {
- {fields.UptimeKumaMonitorScope === "selected" ? ( + {fields.uptime_kuma_monitor_scope === "selected" ? (

已选站点

@@ -506,8 +506,8 @@ export function OpenFlareOpsSettings() {

- {fields.UptimeKumaSelectedSites - ? fields.UptimeKumaSelectedSites.split(",").join(", ") + {fields.uptime_kuma_selected_sites + ? fields.uptime_kuma_selected_sites.split(",").join(", ") : "未选择任何站点"}

@@ -515,27 +515,27 @@ export function OpenFlareOpsSettings() {
updateField("UptimeKumaInterval", value)} + onChange={(value) => updateField("uptime_kuma_interval", value)} /> updateField("UptimeKumaRetry", value)} + onChange={(value) => updateField("uptime_kuma_retry", value)} /> updateField("UptimeKumaRetryInterval", value)} + onChange={(value) => updateField("uptime_kuma_retry_interval", value)} /> updateField("UptimeKumaTimeout", value)} + onChange={(value) => updateField("uptime_kuma_timeout", value)} />
@@ -561,14 +561,14 @@ export function OpenFlareOpsSettings() { updateField("DatabaseAutoCleanupEnabled", value)} + checked={fields.database_auto_cleanup_enabled} + onChange={(value) => updateField("database_auto_cleanup_enabled", value)} /> updateField("DatabaseAutoCleanupRetentionDays", value)} + onChange={(value) => updateField("database_auto_cleanup_retention_days", value)} /> @@ -653,12 +653,12 @@ export function OpenFlareOpsSettings() { updateField("UptimeKumaSelectedSites", sites.join(","))} + onSave={(sites) => updateField("uptime_kuma_selected_sites", sites.join(","))} /> -
-

快照 JSON

- -
-

主配置

diff --git a/frontend/app/(main)/performance/components/performance-utils.ts b/frontend/app/(main)/performance/components/performance-utils.ts index 3c25d27d..5aa6e3f8 100644 --- a/frontend/app/(main)/performance/components/performance-utils.ts +++ b/frontend/app/(main)/performance/components/performance-utils.ts @@ -1,80 +1,80 @@ export type PerformanceFields = { - OpenRestyDefaultServerReturnStatus: string - OpenRestyWorkerProcesses: string - OpenRestyWorkerConnections: string - OpenRestyWorkerRlimitNofile: string - OpenRestyEventsUse: string - OpenRestyEventsMultiAcceptEnabled: boolean - OpenRestyKeepaliveTimeout: string - OpenRestyKeepaliveRequests: string - OpenRestyClientHeaderTimeout: string - OpenRestyClientBodyTimeout: string - OpenRestyClientMaxBodySize: string - OpenRestyLargeClientHeaderBuffers: string - OpenRestySendTimeout: string - OpenRestyProxyConnectTimeout: string - OpenRestyProxySendTimeout: string - OpenRestyProxyReadTimeout: string - OpenRestyWebsocketEnabled: boolean - OpenRestyHTTP3Enabled: boolean - OpenRestyProxyRequestBufferingEnabled: boolean - OpenRestyProxyBufferingEnabled: boolean - OpenRestyProxyBuffers: string - OpenRestyProxyBufferSize: string - OpenRestyProxyBusyBuffersSize: string - OpenRestyGzipEnabled: boolean - OpenRestyGzipMinLength: string - OpenRestyGzipCompLevel: string - OpenRestyCacheEnabled: boolean - OpenRestyCachePath: string - OpenRestyCacheLevels: string - OpenRestyCacheInactive: string - OpenRestyCacheMaxSize: string - OpenRestyCacheKeyTemplate: string - OpenRestyCacheLockEnabled: boolean - OpenRestyCacheLockTimeout: string - OpenRestyCacheUseStale: string - OpenRestyResolvers: string + openresty_default_server_return_status: string + openresty_worker_processes: string + openresty_worker_connections: string + openresty_worker_rlimit_nofile: string + openresty_events_use: string + openresty_events_multi_accept_enabled: boolean + openresty_keepalive_timeout: string + openresty_keepalive_requests: string + openresty_client_header_timeout: string + openresty_client_body_timeout: string + openresty_client_max_body_size: string + openresty_large_client_header_buffers: string + openresty_send_timeout: string + openresty_proxy_connect_timeout: string + openresty_proxy_send_timeout: string + openresty_proxy_read_timeout: string + openresty_websocket_enabled: boolean + openresty_http3_enabled: boolean + openresty_proxy_request_buffering_enabled: boolean + openresty_proxy_buffering_enabled: boolean + openresty_proxy_buffers: string + openresty_proxy_buffer_size: string + openresty_proxy_busy_buffers_size: string + openresty_gzip_enabled: boolean + openresty_gzip_min_length: string + openresty_gzip_comp_level: string + openresty_cache_enabled: boolean + openresty_cache_path: string + openresty_cache_levels: string + openresty_cache_inactive: string + openresty_cache_max_size: string + openresty_cache_key_template: string + openresty_cache_lock_enabled: boolean + openresty_cache_lock_timeout: string + openresty_cache_use_stale: string + openresty_resolvers: string } export const defaultPerformanceFields: PerformanceFields = { - OpenRestyDefaultServerReturnStatus: "421", - OpenRestyWorkerProcesses: "auto", - OpenRestyWorkerConnections: "4096", - OpenRestyWorkerRlimitNofile: "65535", - OpenRestyEventsUse: "epoll", - OpenRestyEventsMultiAcceptEnabled: true, - OpenRestyKeepaliveTimeout: "20", - OpenRestyKeepaliveRequests: "1000", - OpenRestyClientHeaderTimeout: "15", - OpenRestyClientBodyTimeout: "15", - OpenRestyClientMaxBodySize: "64m", - OpenRestyLargeClientHeaderBuffers: "4 16k", - OpenRestySendTimeout: "30", - OpenRestyProxyConnectTimeout: "3", - OpenRestyProxySendTimeout: "60", - OpenRestyProxyReadTimeout: "60", - OpenRestyWebsocketEnabled: true, - OpenRestyHTTP3Enabled: true, - OpenRestyProxyRequestBufferingEnabled: false, - OpenRestyProxyBufferingEnabled: true, - OpenRestyProxyBuffers: "16 16k", - OpenRestyProxyBufferSize: "8k", - OpenRestyProxyBusyBuffersSize: "64k", - OpenRestyGzipEnabled: true, - OpenRestyGzipMinLength: "1024", - OpenRestyGzipCompLevel: "5", - OpenRestyCacheEnabled: false, - OpenRestyCachePath: "", - OpenRestyCacheLevels: "1:2", - OpenRestyCacheInactive: "30m", - OpenRestyCacheMaxSize: "1g", - OpenRestyCacheKeyTemplate: "$scheme$host$request_uri", - OpenRestyCacheLockEnabled: true, - OpenRestyCacheLockTimeout: "5s", - OpenRestyCacheUseStale: + openresty_default_server_return_status: "421", + openresty_worker_processes: "auto", + openresty_worker_connections: "4096", + openresty_worker_rlimit_nofile: "65535", + openresty_events_use: "epoll", + openresty_events_multi_accept_enabled: true, + openresty_keepalive_timeout: "20", + openresty_keepalive_requests: "1000", + openresty_client_header_timeout: "15", + openresty_client_body_timeout: "15", + openresty_client_max_body_size: "64m", + openresty_large_client_header_buffers: "4 16k", + openresty_send_timeout: "30", + openresty_proxy_connect_timeout: "3", + openresty_proxy_send_timeout: "60", + openresty_proxy_read_timeout: "60", + openresty_websocket_enabled: true, + openresty_http3_enabled: true, + openresty_proxy_request_buffering_enabled: false, + openresty_proxy_buffering_enabled: true, + openresty_proxy_buffers: "16 16k", + openresty_proxy_buffer_size: "8k", + openresty_proxy_busy_buffers_size: "64k", + openresty_gzip_enabled: true, + openresty_gzip_min_length: "1024", + openresty_gzip_comp_level: "5", + openresty_cache_enabled: false, + openresty_cache_path: "", + openresty_cache_levels: "1:2", + openresty_cache_inactive: "30m", + openresty_cache_max_size: "1g", + openresty_cache_key_template: "$scheme$host$request_uri", + openresty_cache_lock_enabled: true, + openresty_cache_lock_timeout: "5s", + openresty_cache_use_stale: "error timeout updating http_500 http_502 http_503 http_504", - OpenRestyResolvers: "", + openresty_resolvers: "", } export function optionsToMap(options: Array<{ key: string; value: string }>) { @@ -93,65 +93,65 @@ export function mapOptionsToFields( optionMap: Record, ): PerformanceFields { return { - OpenRestyDefaultServerReturnStatus: - optionMap.OpenRestyDefaultServerReturnStatus ?? "421", - OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? "auto", - OpenRestyWorkerConnections: optionMap.OpenRestyWorkerConnections ?? "4096", - OpenRestyWorkerRlimitNofile: - optionMap.OpenRestyWorkerRlimitNofile ?? "65535", - OpenRestyEventsUse: optionMap.OpenRestyEventsUse ?? "epoll", - OpenRestyEventsMultiAcceptEnabled: toBoolean( - optionMap.OpenRestyEventsMultiAcceptEnabled, + openresty_default_server_return_status: + optionMap.openresty_default_server_return_status ?? "421", + openresty_worker_processes: optionMap.openresty_worker_processes ?? "auto", + openresty_worker_connections: optionMap.openresty_worker_connections ?? "4096", + openresty_worker_rlimit_nofile: + optionMap.openresty_worker_rlimit_nofile ?? "65535", + openresty_events_use: optionMap.openresty_events_use ?? "epoll", + openresty_events_multi_accept_enabled: toBoolean( + optionMap.openresty_events_multi_accept_enabled, true, ), - OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? "20", - OpenRestyKeepaliveRequests: optionMap.OpenRestyKeepaliveRequests ?? "1000", - OpenRestyClientHeaderTimeout: - optionMap.OpenRestyClientHeaderTimeout ?? "15", - OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? "15", - OpenRestyClientMaxBodySize: optionMap.OpenRestyClientMaxBodySize ?? "64m", - OpenRestyLargeClientHeaderBuffers: - optionMap.OpenRestyLargeClientHeaderBuffers ?? "4 16k", - OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? "30", - OpenRestyProxyConnectTimeout: optionMap.OpenRestyProxyConnectTimeout ?? "3", - OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? "60", - OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? "60", - OpenRestyWebsocketEnabled: toBoolean( - optionMap.OpenRestyWebsocketEnabled, + openresty_keepalive_timeout: optionMap.openresty_keepalive_timeout ?? "20", + openresty_keepalive_requests: optionMap.openresty_keepalive_requests ?? "1000", + openresty_client_header_timeout: + optionMap.openresty_client_header_timeout ?? "15", + openresty_client_body_timeout: optionMap.openresty_client_body_timeout ?? "15", + openresty_client_max_body_size: optionMap.openresty_client_max_body_size ?? "64m", + openresty_large_client_header_buffers: + optionMap.openresty_large_client_header_buffers ?? "4 16k", + openresty_send_timeout: optionMap.openresty_send_timeout ?? "30", + openresty_proxy_connect_timeout: optionMap.openresty_proxy_connect_timeout ?? "3", + openresty_proxy_send_timeout: optionMap.openresty_proxy_send_timeout ?? "60", + openresty_proxy_read_timeout: optionMap.openresty_proxy_read_timeout ?? "60", + openresty_websocket_enabled: toBoolean( + optionMap.openresty_websocket_enabled, true, ), - OpenRestyHTTP3Enabled: toBoolean(optionMap.OpenRestyHTTP3Enabled, false), - OpenRestyProxyRequestBufferingEnabled: toBoolean( - optionMap.OpenRestyProxyRequestBufferingEnabled, + openresty_http3_enabled: toBoolean(optionMap.openresty_http3_enabled, false), + openresty_proxy_request_buffering_enabled: toBoolean( + optionMap.openresty_proxy_request_buffering_enabled, false, ), - OpenRestyProxyBufferingEnabled: toBoolean( - optionMap.OpenRestyProxyBufferingEnabled, + openresty_proxy_buffering_enabled: toBoolean( + optionMap.openresty_proxy_buffering_enabled, true, ), - OpenRestyProxyBuffers: optionMap.OpenRestyProxyBuffers ?? "16 16k", - OpenRestyProxyBufferSize: optionMap.OpenRestyProxyBufferSize ?? "8k", - OpenRestyProxyBusyBuffersSize: - optionMap.OpenRestyProxyBusyBuffersSize ?? "64k", - OpenRestyGzipEnabled: toBoolean(optionMap.OpenRestyGzipEnabled, true), - OpenRestyGzipMinLength: optionMap.OpenRestyGzipMinLength ?? "1024", - OpenRestyGzipCompLevel: optionMap.OpenRestyGzipCompLevel ?? "5", - OpenRestyCacheEnabled: toBoolean(optionMap.OpenRestyCacheEnabled, false), - OpenRestyCachePath: optionMap.OpenRestyCachePath ?? "", - OpenRestyCacheLevels: optionMap.OpenRestyCacheLevels ?? "1:2", - OpenRestyCacheInactive: optionMap.OpenRestyCacheInactive ?? "30m", - OpenRestyCacheMaxSize: optionMap.OpenRestyCacheMaxSize ?? "1g", - OpenRestyCacheKeyTemplate: - optionMap.OpenRestyCacheKeyTemplate ?? "$scheme$host$request_uri", - OpenRestyCacheLockEnabled: toBoolean( - optionMap.OpenRestyCacheLockEnabled, + openresty_proxy_buffers: optionMap.openresty_proxy_buffers ?? "16 16k", + openresty_proxy_buffer_size: optionMap.openresty_proxy_buffer_size ?? "8k", + openresty_proxy_busy_buffers_size: + optionMap.openresty_proxy_busy_buffers_size ?? "64k", + openresty_gzip_enabled: toBoolean(optionMap.openresty_gzip_enabled, true), + openresty_gzip_min_length: optionMap.openresty_gzip_min_length ?? "1024", + openresty_gzip_comp_level: optionMap.openresty_gzip_comp_level ?? "5", + openresty_cache_enabled: toBoolean(optionMap.openresty_cache_enabled, false), + openresty_cache_path: optionMap.openresty_cache_path ?? "", + openresty_cache_levels: optionMap.openresty_cache_levels ?? "1:2", + openresty_cache_inactive: optionMap.openresty_cache_inactive ?? "30m", + openresty_cache_max_size: optionMap.openresty_cache_max_size ?? "1g", + openresty_cache_key_template: + optionMap.openresty_cache_key_template ?? "$scheme$host$request_uri", + openresty_cache_lock_enabled: toBoolean( + optionMap.openresty_cache_lock_enabled, true, ), - OpenRestyCacheLockTimeout: optionMap.OpenRestyCacheLockTimeout ?? "5s", - OpenRestyCacheUseStale: - optionMap.OpenRestyCacheUseStale ?? + openresty_cache_lock_timeout: optionMap.openresty_cache_lock_timeout ?? "5s", + openresty_cache_use_stale: + optionMap.openresty_cache_use_stale ?? "error timeout updating http_500 http_502 http_503 http_504", - OpenRestyResolvers: optionMap.OpenRestyResolvers ?? "", + openresty_resolvers: optionMap.openresty_resolvers ?? "", } } @@ -178,79 +178,79 @@ function isCacheLevelsValue(value: string) { export function validateRuntimeFields(fields: PerformanceFields) { if ( - fields.OpenRestyWorkerProcesses !== "auto" && - !isPositiveInteger(fields.OpenRestyWorkerProcesses) + fields.openresty_worker_processes !== "auto" && + !isPositiveInteger(fields.openresty_worker_processes) ) { throw new Error("worker_processes 必须为 auto 或大于 0 的整数") } const integers = [ - fields.OpenRestyWorkerConnections, - fields.OpenRestyWorkerRlimitNofile, - fields.OpenRestyKeepaliveTimeout, - fields.OpenRestyKeepaliveRequests, - fields.OpenRestyClientHeaderTimeout, - fields.OpenRestyClientBodyTimeout, - fields.OpenRestySendTimeout, + fields.openresty_worker_connections, + fields.openresty_worker_rlimit_nofile, + fields.openresty_keepalive_timeout, + fields.openresty_keepalive_requests, + fields.openresty_client_header_timeout, + fields.openresty_client_body_timeout, + fields.openresty_send_timeout, ] if (integers.some((value) => !isPositiveInteger(value))) { throw new Error("超时与连接参数必须为大于 0 的整数") } - const status = Number.parseInt(fields.OpenRestyDefaultServerReturnStatus, 10) + const status = Number.parseInt(fields.openresty_default_server_return_status, 10) if (Number.isNaN(status) || status < 100 || status > 999) { throw new Error("空白页面返回状态码必须在 100 到 999 之间") } - if (!isSizeValue(fields.OpenRestyClientMaxBodySize)) { + if (!isSizeValue(fields.openresty_client_max_body_size)) { throw new Error("client_max_body_size 格式不合法") } - if (!isProxyBuffersValue(fields.OpenRestyLargeClientHeaderBuffers)) { + if (!isProxyBuffersValue(fields.openresty_large_client_header_buffers)) { throw new Error('large_client_header_buffers 格式必须类似 "4 16k"') } } export function validateProxyFields(fields: PerformanceFields) { const timeouts = [ - fields.OpenRestyProxyConnectTimeout, - fields.OpenRestyProxySendTimeout, - fields.OpenRestyProxyReadTimeout, + fields.openresty_proxy_connect_timeout, + fields.openresty_proxy_send_timeout, + fields.openresty_proxy_read_timeout, ] if (timeouts.some((value) => !isPositiveInteger(value))) { throw new Error("代理超时参数必须为大于 0 的整数秒") } - if (!isProxyBuffersValue(fields.OpenRestyProxyBuffers)) { + if (!isProxyBuffersValue(fields.openresty_proxy_buffers)) { throw new Error('proxy_buffers 格式必须类似 "16 16k"') } if ( - !isSizeValue(fields.OpenRestyProxyBufferSize) || - !isSizeValue(fields.OpenRestyProxyBusyBuffersSize) + !isSizeValue(fields.openresty_proxy_buffer_size) || + !isSizeValue(fields.openresty_proxy_busy_buffers_size) ) { throw new Error("缓冲大小必须为整数或带 k/m/g 单位的值") } } export function validateGzipFields(fields: PerformanceFields) { - if (!isPositiveInteger(fields.OpenRestyGzipMinLength)) { + if (!isPositiveInteger(fields.openresty_gzip_min_length)) { throw new Error("gzip_min_length 必须为大于 0 的整数") } - const level = Number.parseInt(fields.OpenRestyGzipCompLevel, 10) + const level = Number.parseInt(fields.openresty_gzip_comp_level, 10) if (Number.isNaN(level) || level < 1 || level > 9) { throw new Error("gzip_comp_level 必须在 1 到 9 之间") } } export function validateCacheFields(fields: PerformanceFields) { - if (!fields.OpenRestyCacheEnabled) return - if (!fields.OpenRestyCachePath.trim()) { + if (!fields.openresty_cache_enabled) return + if (!fields.openresty_cache_path.trim()) { throw new Error("启用缓存时必须填写 proxy_cache_path 目录") } if ( - !isCacheLevelsValue(fields.OpenRestyCacheLevels) || - !isDurationToken(fields.OpenRestyCacheInactive) || - !isSizeValue(fields.OpenRestyCacheMaxSize) || - !isDurationToken(fields.OpenRestyCacheLockTimeout) + !isCacheLevelsValue(fields.openresty_cache_levels) || + !isDurationToken(fields.openresty_cache_inactive) || + !isSizeValue(fields.openresty_cache_max_size) || + !isDurationToken(fields.openresty_cache_lock_timeout) ) { throw new Error("缓存 levels、inactive、max_size 或 lock_timeout 格式不合法") } - if (!fields.OpenRestyCacheKeyTemplate.trim()) { + if (!fields.openresty_cache_key_template.trim()) { throw new Error("启用缓存时必须填写缓存 Key 模板") } } diff --git a/frontend/app/(main)/performance/page.tsx b/frontend/app/(main)/performance/page.tsx index 22d8a1b8..85fef537 100644 --- a/frontend/app/(main)/performance/page.tsx +++ b/frontend/app/(main)/performance/page.tsx @@ -162,20 +162,20 @@ export default function PerformancePage() { section: "runtime", validator: validateRuntimeFields, entries: entriesFromKeys(fields, [ - "OpenRestyDefaultServerReturnStatus", - "OpenRestyWorkerProcesses", - "OpenRestyResolvers", - "OpenRestyWorkerConnections", - "OpenRestyWorkerRlimitNofile", - "OpenRestyEventsUse", - "OpenRestyEventsMultiAcceptEnabled", - "OpenRestyKeepaliveTimeout", - "OpenRestyKeepaliveRequests", - "OpenRestyClientHeaderTimeout", - "OpenRestyClientBodyTimeout", - "OpenRestyClientMaxBodySize", - "OpenRestyLargeClientHeaderBuffers", - "OpenRestySendTimeout", + "openresty_default_server_return_status", + "openresty_worker_processes", + "openresty_resolvers", + "openresty_worker_connections", + "openresty_worker_rlimit_nofile", + "openresty_events_use", + "openresty_events_multi_accept_enabled", + "openresty_keepalive_timeout", + "openresty_keepalive_requests", + "openresty_client_header_timeout", + "openresty_client_body_timeout", + "openresty_client_max_body_size", + "openresty_large_client_header_buffers", + "openresty_send_timeout", ]), }) } @@ -189,12 +189,12 @@ export default function PerformancePage() { - updateField("OpenRestyWorkerProcesses", v)} /> - updateField("OpenRestyWorkerConnections", v)} type="number" /> - updateField("OpenRestyWorkerRlimitNofile", v)} type="number" /> + updateField("openresty_worker_processes", v)} /> + updateField("openresty_worker_connections", v)} type="number" /> + updateField("openresty_worker_rlimit_nofile", v)} type="number" />
- updateField("openresty_events_use", v)}> epoll @@ -204,11 +204,11 @@ export default function PerformancePage() {
- updateField("OpenRestyEventsMultiAcceptEnabled", v)} /> - updateField("OpenRestyKeepaliveTimeout", v)} type="number" /> - updateField("OpenRestyClientMaxBodySize", v)} /> - updateField("OpenRestyResolvers", v)} placeholder="1.1.1.1 8.8.8.8" /> - updateField("OpenRestyDefaultServerReturnStatus", v)} type="number" /> + updateField("openresty_events_multi_accept_enabled", v)} /> + updateField("openresty_keepalive_timeout", v)} type="number" /> + updateField("openresty_client_max_body_size", v)} /> + updateField("openresty_resolvers", v)} placeholder="1.1.1.1 8.8.8.8" /> + updateField("openresty_default_server_return_status", v)} type="number" />
@@ -226,16 +226,16 @@ export default function PerformancePage() { section: "proxy", validator: validateProxyFields, entries: entriesFromKeys(fields, [ - "OpenRestyProxyConnectTimeout", - "OpenRestyProxySendTimeout", - "OpenRestyProxyReadTimeout", - "OpenRestyWebsocketEnabled", - "OpenRestyHTTP3Enabled", - "OpenRestyProxyRequestBufferingEnabled", - "OpenRestyProxyBufferingEnabled", - "OpenRestyProxyBuffers", - "OpenRestyProxyBufferSize", - "OpenRestyProxyBusyBuffersSize", + "openresty_proxy_connect_timeout", + "openresty_proxy_send_timeout", + "openresty_proxy_read_timeout", + "openresty_websocket_enabled", + "openresty_http3_enabled", + "openresty_proxy_request_buffering_enabled", + "openresty_proxy_buffering_enabled", + "openresty_proxy_buffers", + "openresty_proxy_buffer_size", + "openresty_proxy_busy_buffers_size", ]), }) } @@ -245,12 +245,12 @@ export default function PerformancePage() { - updateField("OpenRestyProxyConnectTimeout", v)} type="number" /> - updateField("OpenRestyProxyReadTimeout", v)} type="number" /> - updateField("OpenRestyWebsocketEnabled", v)} /> - updateField("OpenRestyHTTP3Enabled", v)} /> - updateField("OpenRestyProxyBufferingEnabled", v)} /> - updateField("OpenRestyProxyBuffers", v)} /> + updateField("openresty_proxy_connect_timeout", v)} type="number" /> + updateField("openresty_proxy_read_timeout", v)} type="number" /> + updateField("openresty_websocket_enabled", v)} /> + updateField("openresty_http3_enabled", v)} /> + updateField("openresty_proxy_buffering_enabled", v)} /> + updateField("openresty_proxy_buffers", v)} /> @@ -266,9 +266,9 @@ export default function PerformancePage() { section: "gzip", validator: validateGzipFields, entries: entriesFromKeys(fields, [ - "OpenRestyGzipEnabled", - "OpenRestyGzipMinLength", - "OpenRestyGzipCompLevel", + "openresty_gzip_enabled", + "openresty_gzip_min_length", + "openresty_gzip_comp_level", ]), }) } @@ -277,9 +277,9 @@ export default function PerformancePage() { - updateField("OpenRestyGzipEnabled", v)} /> - updateField("OpenRestyGzipMinLength", v)} type="number" /> - updateField("OpenRestyGzipCompLevel", v)} type="number" /> + updateField("openresty_gzip_enabled", v)} /> + updateField("openresty_gzip_min_length", v)} type="number" /> + updateField("openresty_gzip_comp_level", v)} type="number" /> @@ -297,15 +297,15 @@ export default function PerformancePage() { section: "cache", validator: validateCacheFields, entries: entriesFromKeys(fields, [ - "OpenRestyCacheEnabled", - "OpenRestyCachePath", - "OpenRestyCacheLevels", - "OpenRestyCacheInactive", - "OpenRestyCacheMaxSize", - "OpenRestyCacheKeyTemplate", - "OpenRestyCacheLockEnabled", - "OpenRestyCacheLockTimeout", - "OpenRestyCacheUseStale", + "openresty_cache_enabled", + "openresty_cache_path", + "openresty_cache_levels", + "openresty_cache_inactive", + "openresty_cache_max_size", + "openresty_cache_key_template", + "openresty_cache_lock_enabled", + "openresty_cache_lock_timeout", + "openresty_cache_use_stale", ]), }) } @@ -314,10 +314,10 @@ export default function PerformancePage() { - updateField("OpenRestyCacheEnabled", v)} /> - updateField("OpenRestyCachePath", v)} disabled={!fields.OpenRestyCacheEnabled} /> - updateField("OpenRestyCacheLevels", v)} disabled={!fields.OpenRestyCacheEnabled} /> - updateField("OpenRestyCacheMaxSize", v)} disabled={!fields.OpenRestyCacheEnabled} /> + updateField("openresty_cache_enabled", v)} /> + updateField("openresty_cache_path", v)} disabled={!fields.openresty_cache_enabled} /> + updateField("openresty_cache_levels", v)} disabled={!fields.openresty_cache_enabled} /> + updateField("openresty_cache_max_size", v)} disabled={!fields.openresty_cache_enabled} />
diff --git a/internal/apps/openflare/agent/helpers.go b/internal/apps/openflare/agent/helpers.go index f8b5b98d..5ddad385 100644 --- a/internal/apps/openflare/agent/helpers.go +++ b/internal/apps/openflare/agent/helpers.go @@ -13,6 +13,7 @@ import ( ofgeoip "github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" ) const ( @@ -22,6 +23,9 @@ const ( releaseChannelStable = "stable" randomTokenBytes = 16 maxDatabaseTextLength = 16000 + + defaultAgentHeartbeatInterval = 10000 // 默认心跳间隔 10 秒(毫秒) + defaultAgentUpdateRepo = "Rain-kl/OpenFlare" ) func newRandomToken() (string, error) { @@ -173,10 +177,7 @@ func isPublicNodeIP(raw string) bool { return true } -func buildAgentSettings(node *model.OpenFlareNode, updateNow bool, updateChannel string, updateTag string, restartOpenrestyNow bool) *Settings { - model.OptionMapRWMutex.RLock() - defer model.OptionMapRWMutex.RUnlock() - +func buildAgentSettings(ctx context.Context, node *model.OpenFlareNode, updateNow bool, updateChannel string, updateTag string, restartOpenrestyNow bool) *Settings { autoUpdate := false if node != nil { autoUpdate = node.AutoUpdateEnabled @@ -184,11 +185,23 @@ func buildAgentSettings(node *model.OpenFlareNode, updateNow bool, updateChannel if strings.TrimSpace(updateChannel) == "" { updateChannel = releaseChannelStable } + + // 从 SystemConfig 读取配置,使用默认值作为降级 + heartbeatInterval, _ := repository.GetIntByKey(ctx, model.ConfigKeyAgentHeartbeatInterval) + if heartbeatInterval <= 0 { + heartbeatInterval = defaultAgentHeartbeatInterval + } + wsUpgradeEnabled, _ := repository.GetBoolByKey(ctx, model.ConfigKeyAgentWebsocketUpgradeEnabled) + updateRepo, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyAgentUpdateRepo) + if strings.TrimSpace(updateRepo.Value) == "" { + updateRepo.Value = defaultAgentUpdateRepo + } + return &Settings{ - HeartbeatInterval: model.AgentHeartbeatInterval, - WebsocketUpgradeEnabled: model.AgentWebsocketUpgradeEnabled, + HeartbeatInterval: heartbeatInterval, + WebsocketUpgradeEnabled: wsUpgradeEnabled, AutoUpdate: autoUpdate, - UpdateRepo: model.AgentUpdateRepo, + UpdateRepo: updateRepo.Value, UpdateNow: updateNow, UpdateChannel: updateChannel, UpdateTag: strings.TrimSpace(updateTag), diff --git a/internal/apps/openflare/agent/logics.go b/internal/apps/openflare/agent/logics.go index 0434d4f7..d2cac1a7 100644 --- a/internal/apps/openflare/agent/logics.go +++ b/internal/apps/openflare/agent/logics.go @@ -140,7 +140,7 @@ func HeartbeatNode(ctx context.Context, authNode *model.OpenFlareNode, payload N return &HeartbeatResponse{ Node: authNode, - AgentSettings: buildAgentSettings(authNode, updateNow, updateChannel, updateTag, restartOpenrestyNow), + AgentSettings: buildAgentSettings(ctx, authNode, updateNow, updateChannel, updateTag, restartOpenrestyNow), ActiveConfig: activeConfig, WAFIPGroups: wafIPGroups, }, nil diff --git a/internal/apps/openflare/agent/middleware_test.go b/internal/apps/openflare/agent/middleware_test.go index 5c1a575b..8be37b83 100644 --- a/internal/apps/openflare/agent/middleware_test.go +++ b/internal/apps/openflare/agent/middleware_test.go @@ -11,10 +11,10 @@ import ( "testing" "time" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" "github.com/Rain-kl/Wavelet/internal/testhelper" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" @@ -32,16 +32,14 @@ func setupAgentAuthTestDB(t *testing.T) func() { require.NoError(t, err) require.NoError(t, sqliteDB.AutoMigrate( &model.OpenFlareNode{}, - &model.OpenFlareOption{}, + &model.SystemConfig{}, )) db.SetDB(sqliteDB) - option.ResetInitializationForTest() tokenCache.reset() return func() { db.SetDB(nil) - option.ResetInitializationForTest() tokenCache.reset() } } @@ -154,7 +152,7 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) { LastSeenAt: &now, NodeType: "edge_node", }).Error) - require.NoError(t, model.UpdateOpenFlareOption(ctx, "AgentDiscoveryToken", "discovery-token")) + require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyAgentDiscoveryToken, "discovery-token")) router := testhelper.NewTestGinEngine() router.POST("/register", RegisterAuth(), func(c *gin.Context) { diff --git a/internal/apps/openflare/async_tasks.go b/internal/apps/openflare/async_tasks.go index 05f7ab83..87df194a 100644 --- a/internal/apps/openflare/async_tasks.go +++ b/internal/apps/openflare/async_tasks.go @@ -11,6 +11,7 @@ import ( "github.com/Rain-kl/Wavelet/internal/apps/openflare/uptimekuma" "github.com/Rain-kl/Wavelet/internal/apps/openflare/waf" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" "github.com/Rain-kl/Wavelet/internal/task" ) @@ -109,13 +110,20 @@ type DatabaseAutoCleanupHandler struct{} // Execute runs retention-based cleanup for all observability targets. func (h *DatabaseAutoCleanupHandler) Execute(ctx context.Context, _ []byte) (*task.TaskResult, error) { - if !model.DatabaseAutoCleanupEnabled { + // 从 SystemConfig 读取自动清理配置 + enabled, _ := repository.GetBoolByKey(ctx, model.ConfigKeyDatabaseAutoCleanupEnabled) + if !enabled { msg := "自动清理未启用,跳过执行" task.AppendLog(ctx, "%s", msg) return &task.TaskResult{Message: msg}, nil } - task.AppendLog(ctx, "开始执行可观测数据自动清理,保留天数=%d", model.DatabaseAutoCleanupRetentionDays) + retentionDays, _ := repository.GetIntByKey(ctx, model.ConfigKeyDatabaseAutoCleanupRetentionDays) + if retentionDays <= 0 { + retentionDays = 30 // 默认保留 30 天 + } + + task.AppendLog(ctx, "开始执行可观测数据自动清理,保留天数=%d", retentionDays) summary, err := tasks.RunDatabaseAutoCleanupOnce(ctx, time.Now()) if err != nil { task.AppendLog(ctx, "可观测数据自动清理失败: %v", err) @@ -162,13 +170,15 @@ type UptimeKumaSyncHandler struct{} // Execute runs Uptime Kuma sync when integration is enabled and the interval has elapsed. func (h *UptimeKumaSyncHandler) Execute(ctx context.Context, _ []byte) (*task.TaskResult, error) { - if !model.UptimeKumaEnabled { + // 从 SystemConfig 读取 UptimeKuma 配置 + enabled, _ := repository.GetBoolByKey(ctx, model.ConfigKeyUptimeKumaEnabled) + if !enabled { msg := "Uptime Kuma 集成未启用,跳过执行" task.AppendLog(ctx, "%s", msg) return &task.TaskResult{Message: msg}, nil } - interval := model.UptimeKumaSyncInterval + interval, _ := repository.GetIntByKey(ctx, model.ConfigKeyUptimeKumaSyncInterval) if interval <= 0 { interval = 5 } diff --git a/internal/apps/openflare/async_tasks_test.go b/internal/apps/openflare/async_tasks_test.go index 5bceb3bc..d8fc9df6 100644 --- a/internal/apps/openflare/async_tasks_test.go +++ b/internal/apps/openflare/async_tasks_test.go @@ -10,6 +10,7 @@ import ( "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,13 +18,18 @@ import ( ) func TestDatabaseAutoCleanupHandlerSkipsWhenDisabled(t *testing.T) { - previousEnabled := model.DatabaseAutoCleanupEnabled - model.DatabaseAutoCleanupEnabled = false - t.Cleanup(func() { - model.DatabaseAutoCleanupEnabled = previousEnabled + sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, }) + require.NoError(t, err) + require.NoError(t, sqliteDB.AutoMigrate(&model.SystemConfig{})) + db.SetDB(sqliteDB) + t.Cleanup(func() { db.SetDB(nil) }) - result, err := (&DatabaseAutoCleanupHandler{}).Execute(context.Background(), nil) + ctx := context.Background() + require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyDatabaseAutoCleanupEnabled, "false")) + + result, err := (&DatabaseAutoCleanupHandler{}).Execute(ctx, nil) require.NoError(t, err) require.NotNil(t, result) assert.Contains(t, result.Message, "未启用") @@ -34,6 +40,7 @@ func TestDatabaseAutoCleanupHandlerDeletesRowsWhenEnabled(t *testing.T) { DisableForeignKeyConstraintWhenMigrating: true, }) require.NoError(t, err) + require.NoError(t, sqliteDB.AutoMigrate(&model.SystemConfig{})) db.SetDB(sqliteDB) resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore()) resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore()) @@ -43,8 +50,9 @@ func TestDatabaseAutoCleanupHandlerDeletesRowsWhenEnabled(t *testing.T) { db.SetDB(nil) }) + ctx := context.Background() now := time.Now().UTC() - require.NoError(t, model.InsertOpenFlareAccessLogsBatch(context.Background(), []*model.OpenFlareAccessLog{{ + require.NoError(t, model.InsertOpenFlareAccessLogsBatch(ctx, []*model.OpenFlareAccessLog{{ NodeID: "node-a", LoggedAt: now.Add(-48 * time.Hour), RemoteAddr: "203.0.113.10", @@ -53,33 +61,32 @@ func TestDatabaseAutoCleanupHandlerDeletesRowsWhenEnabled(t *testing.T) { StatusCode: 200, }})) - previousEnabled := model.DatabaseAutoCleanupEnabled - previousRetentionDays := model.DatabaseAutoCleanupRetentionDays - model.DatabaseAutoCleanupEnabled = true - model.DatabaseAutoCleanupRetentionDays = 1 - t.Cleanup(func() { - model.DatabaseAutoCleanupEnabled = previousEnabled - model.DatabaseAutoCleanupRetentionDays = previousRetentionDays - }) + require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyDatabaseAutoCleanupEnabled, "true")) + require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyDatabaseAutoCleanupRetentionDays, "1")) - result, err := (&DatabaseAutoCleanupHandler{}).Execute(context.Background(), nil) + result, err := (&DatabaseAutoCleanupHandler{}).Execute(ctx, nil) require.NoError(t, err) require.NotNil(t, result) assert.Contains(t, result.Message, "共删除") - rows, err := model.ListOpenFlareAccessLogs(context.Background(), model.OpenFlareAccessLogQuery{Page: 0, PageSize: 10}) + rows, err := model.ListOpenFlareAccessLogs(ctx, model.OpenFlareAccessLogQuery{Page: 0, PageSize: 10}) require.NoError(t, err) assert.Empty(t, rows) } func TestUptimeKumaSyncHandlerSkipsWhenDisabled(t *testing.T) { - previousEnabled := model.UptimeKumaEnabled - model.UptimeKumaEnabled = false - t.Cleanup(func() { - model.UptimeKumaEnabled = previousEnabled + sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, }) + require.NoError(t, err) + require.NoError(t, sqliteDB.AutoMigrate(&model.SystemConfig{})) + db.SetDB(sqliteDB) + t.Cleanup(func() { db.SetDB(nil) }) - result, err := (&UptimeKumaSyncHandler{}).Execute(context.Background(), nil) + ctx := context.Background() + require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "false")) + + result, err := (&UptimeKumaSyncHandler{}).Execute(ctx, nil) require.NoError(t, err) require.NotNil(t, result) assert.Contains(t, result.Message, "未启用") diff --git a/internal/apps/openflare/config_version/snapshot.go b/internal/apps/openflare/config_version/snapshot.go index f27887af..297f4627 100644 --- a/internal/apps/openflare/config_version/snapshot.go +++ b/internal/apps/openflare/config_version/snapshot.go @@ -14,10 +14,27 @@ import ( "github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity" "github.com/Rain-kl/Wavelet/internal/apps/openflare/waf" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty" ) -const supportFilesPerCertificate = 2 +const ( + supportFilesPerCertificate = 2 + + // OpenResty 默认配置值 + defaultOpenRestyReturnStatus = 421 + defaultOpenRestyWorkerConns = 4096 + defaultOpenRestyRlimitNofile = 65535 + defaultOpenRestyKeepaliveTimeout = 20 + defaultOpenRestyKeepaliveReqs = 1000 + defaultOpenRestyHeaderTimeout = 15 + defaultOpenRestyBodyTimeout = 15 + defaultOpenRestySendTimeout = 30 + defaultOpenRestyConnectTimeout = 3 + defaultOpenRestyProxyTimeout = 60 + defaultOpenRestyGzipMinLen = 1024 + defaultOpenRestyGzipLevel = 5 +) type snapshotRoute struct { ID uint `json:"id,omitempty"` @@ -166,7 +183,7 @@ func buildCurrentConfigBundle(ctx context.Context, requireRoutes bool) (*configB if err != nil { return nil, err } - openRestyConfig := buildOpenRestyConfigSnapshot() + openRestyConfig := buildOpenRestyConfigSnapshot(ctx) snapshotDoc := snapshotDocument{ Routes: snapshotRoutes, OpenRestyConfig: openRestyConfig, @@ -451,47 +468,70 @@ func convertPoWConfig(enabled bool, config *waf.PoWConfig) *openrestyrender.PoWC } } -func buildOpenRestyConfigSnapshot() openRestyConfigSnapshot { - model.OptionMapRWMutex.RLock() - defer model.OptionMapRWMutex.RUnlock() +func buildOpenRestyConfigSnapshot(ctx context.Context) openRestyConfigSnapshot { + // 读取所有 OpenResty 配置,使用默认值作为降级 + getIntConfig := func(key string, defaultVal int) int { + val, err := repository.GetIntByKey(ctx, key) + if err != nil || val <= 0 { + return defaultVal + } + return val + } + + getBoolConfig := func(key string, defaultVal bool) bool { + val, err := repository.GetBoolByKey(ctx, key) + if err != nil { + return defaultVal + } + return val + } + + getStringConfig := func(key string, defaultVal string) string { + config, err := repository.GetSystemConfigByKey(ctx, key) + if err != nil { + return defaultVal + } + return config.Value + } + return openRestyConfigSnapshot{ - DefaultServerReturnStatus: model.OpenRestyDefaultServerReturnStatus, - WorkerProcesses: model.OpenRestyWorkerProcesses, - WorkerConnections: model.OpenRestyWorkerConnections, - WorkerRlimitNofile: model.OpenRestyWorkerRlimitNofile, - EventsUse: model.OpenRestyEventsUse, - EventsMultiAcceptEnabled: model.OpenRestyEventsMultiAcceptEnabled, - KeepaliveTimeout: model.OpenRestyKeepaliveTimeout, - KeepaliveRequests: model.OpenRestyKeepaliveRequests, - ClientHeaderTimeout: model.OpenRestyClientHeaderTimeout, - ClientBodyTimeout: model.OpenRestyClientBodyTimeout, - ClientMaxBodySize: model.OpenRestyClientMaxBodySize, - LargeClientHeaderBuffers: model.OpenRestyLargeClientHeaderBuffers, - SendTimeout: model.OpenRestySendTimeout, - ProxyConnectTimeout: model.OpenRestyProxyConnectTimeout, - ProxySendTimeout: model.OpenRestyProxySendTimeout, - ProxyReadTimeout: model.OpenRestyProxyReadTimeout, - WebsocketEnabled: model.OpenRestyWebsocketEnabled, - HTTP3Enabled: model.OpenRestyHTTP3Enabled, - ProxyRequestBuffering: model.OpenRestyProxyRequestBufferingEnabled, - ProxyBufferingEnabled: model.OpenRestyProxyBufferingEnabled, - ProxyBuffers: model.OpenRestyProxyBuffers, - ProxyBufferSize: model.OpenRestyProxyBufferSize, - ProxyBusyBuffersSize: model.OpenRestyProxyBusyBuffersSize, - GzipEnabled: model.OpenRestyGzipEnabled, - GzipMinLength: model.OpenRestyGzipMinLength, - GzipCompLevel: model.OpenRestyGzipCompLevel, - Resolvers: model.OpenRestyResolvers, - CacheEnabled: model.OpenRestyCacheEnabled, - CachePath: model.OpenRestyCachePath, - CacheLevels: model.OpenRestyCacheLevels, - CacheInactive: model.OpenRestyCacheInactive, - CacheMaxSize: model.OpenRestyCacheMaxSize, - CacheKeyTemplate: model.OpenRestyCacheKeyTemplate, - CacheLockEnabled: model.OpenRestyCacheLockEnabled, - CacheLockTimeout: model.OpenRestyCacheLockTimeout, - CacheUseStale: model.OpenRestyCacheUseStale, - MainConfigTemplate: model.OpenRestyMainConfigTemplate, + DefaultServerReturnStatus: getIntConfig(model.ConfigKeyOpenRestyDefaultServerReturnStatus, defaultOpenRestyReturnStatus), + WorkerProcesses: getStringConfig(model.ConfigKeyOpenRestyWorkerProcesses, "auto"), + WorkerConnections: getIntConfig(model.ConfigKeyOpenRestyWorkerConnections, defaultOpenRestyWorkerConns), + WorkerRlimitNofile: getIntConfig(model.ConfigKeyOpenRestyWorkerRlimitNofile, defaultOpenRestyRlimitNofile), + EventsUse: getStringConfig(model.ConfigKeyOpenRestyEventsUse, "epoll"), + EventsMultiAcceptEnabled: getBoolConfig(model.ConfigKeyOpenRestyEventsMultiAcceptEnabled, true), + KeepaliveTimeout: getIntConfig(model.ConfigKeyOpenRestyKeepaliveTimeout, defaultOpenRestyKeepaliveTimeout), + KeepaliveRequests: getIntConfig(model.ConfigKeyOpenRestyKeepaliveRequests, defaultOpenRestyKeepaliveReqs), + ClientHeaderTimeout: getIntConfig(model.ConfigKeyOpenRestyClientHeaderTimeout, defaultOpenRestyHeaderTimeout), + ClientBodyTimeout: getIntConfig(model.ConfigKeyOpenRestyClientBodyTimeout, defaultOpenRestyBodyTimeout), + ClientMaxBodySize: getStringConfig(model.ConfigKeyOpenRestyClientMaxBodySize, "64m"), + LargeClientHeaderBuffers: getStringConfig(model.ConfigKeyOpenRestyLargeClientHeaderBuffers, "4 16k"), + SendTimeout: getIntConfig(model.ConfigKeyOpenRestySendTimeout, defaultOpenRestySendTimeout), + ProxyConnectTimeout: getIntConfig(model.ConfigKeyOpenRestyProxyConnectTimeout, defaultOpenRestyConnectTimeout), + ProxySendTimeout: getIntConfig(model.ConfigKeyOpenRestyProxySendTimeout, defaultOpenRestyProxyTimeout), + ProxyReadTimeout: getIntConfig(model.ConfigKeyOpenRestyProxyReadTimeout, defaultOpenRestyProxyTimeout), + WebsocketEnabled: getBoolConfig(model.ConfigKeyOpenRestyWebsocketEnabled, true), + HTTP3Enabled: getBoolConfig(model.ConfigKeyOpenRestyHTTP3Enabled, true), + ProxyRequestBuffering: getBoolConfig(model.ConfigKeyOpenRestyProxyRequestBufferingEnabled, false), + ProxyBufferingEnabled: getBoolConfig(model.ConfigKeyOpenRestyProxyBufferingEnabled, true), + ProxyBuffers: getStringConfig(model.ConfigKeyOpenRestyProxyBuffers, "16 16k"), + ProxyBufferSize: getStringConfig(model.ConfigKeyOpenRestyProxyBufferSize, "8k"), + ProxyBusyBuffersSize: getStringConfig(model.ConfigKeyOpenRestyProxyBusyBuffersSize, "64k"), + GzipEnabled: getBoolConfig(model.ConfigKeyOpenRestyGzipEnabled, true), + GzipMinLength: getIntConfig(model.ConfigKeyOpenRestyGzipMinLength, defaultOpenRestyGzipMinLen), + GzipCompLevel: getIntConfig(model.ConfigKeyOpenRestyGzipCompLevel, defaultOpenRestyGzipLevel), + Resolvers: getStringConfig(model.ConfigKeyOpenRestyResolvers, ""), + CacheEnabled: getBoolConfig(model.ConfigKeyOpenRestyCacheEnabled, false), + CachePath: getStringConfig(model.ConfigKeyOpenRestyCachePath, ""), + CacheLevels: getStringConfig(model.ConfigKeyOpenRestyCacheLevels, "1:2"), + CacheInactive: getStringConfig(model.ConfigKeyOpenRestyCacheInactive, "30m"), + CacheMaxSize: getStringConfig(model.ConfigKeyOpenRestyCacheMaxSize, "1g"), + CacheKeyTemplate: getStringConfig(model.ConfigKeyOpenRestyCacheKeyTemplate, "$scheme$host$request_uri"), + CacheLockEnabled: getBoolConfig(model.ConfigKeyOpenRestyCacheLockEnabled, true), + CacheLockTimeout: getStringConfig(model.ConfigKeyOpenRestyCacheLockTimeout, "5s"), + CacheUseStale: getStringConfig(model.ConfigKeyOpenRestyCacheUseStale, "error timeout updating http_500 http_502 http_503 http_504"), + MainConfigTemplate: getStringConfig(model.ConfigKeyOpenRestyMainConfigTemplate, model.DefaultOpenRestyMainConfigTemplate), } } diff --git a/internal/apps/openflare/dashboard/helpers.go b/internal/apps/openflare/dashboard/helpers.go index b3999b8a..a46bea67 100644 --- a/internal/apps/openflare/dashboard/helpers.go +++ b/internal/apps/openflare/dashboard/helpers.go @@ -30,7 +30,9 @@ func computeNodeStatus(node *model.OpenFlareNode) string { if node.LastSeenAt == nil || node.LastSeenAt.IsZero() { return nodeStatusPending } - if time.Since(*node.LastSeenAt) > model.NodeOfflineThreshold { + // 使用默认阈值 2 分钟 + threshold := 2 * time.Minute + if time.Since(*node.LastSeenAt) > threshold { return nodeStatusOffline } return nodeStatusOnline diff --git a/internal/apps/openflare/flared/helpers.go b/internal/apps/openflare/flared/helpers.go index 68e4370d..842a67c1 100644 --- a/internal/apps/openflare/flared/helpers.go +++ b/internal/apps/openflare/flared/helpers.go @@ -148,6 +148,6 @@ func sanitizeProxyName(domain string) string { return strings.ReplaceAll(strings.ReplaceAll(domain, ".", "-"), "*", "wildcard") } -func buildTunnelSettings(node *model.OpenFlareNode, updateNow bool, updateChannel, updateTag string) *relay.Settings { - return relay.BuildSettings(node, updateNow, updateChannel, updateTag) +func buildTunnelSettings(ctx context.Context, node *model.OpenFlareNode, updateNow bool, updateChannel, updateTag string) *relay.Settings { + return relay.BuildSettings(ctx, node, updateNow, updateChannel, updateTag) } diff --git a/internal/apps/openflare/flared/logics.go b/internal/apps/openflare/flared/logics.go index d01cced4..5e2e04ba 100644 --- a/internal/apps/openflare/flared/logics.go +++ b/internal/apps/openflare/flared/logics.go @@ -88,7 +88,7 @@ func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload Heartbeat return &HeartbeatResponse{ ActiveConfig: activeConfig, - TunnelSettings: buildTunnelSettings(node, updateNow, updateChannel, updateTag), + TunnelSettings: buildTunnelSettings(ctx, node, updateNow, updateChannel, updateTag), }, nil } diff --git a/internal/apps/openflare/flared/observability_test.go b/internal/apps/openflare/flared/observability_test.go index ca477a33..94836bfe 100644 --- a/internal/apps/openflare/flared/observability_test.go +++ b/internal/apps/openflare/flared/observability_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/Rain-kl/Wavelet/internal/apps/openflare/agent" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" "github.com/glebarez/sqlite" @@ -27,15 +26,14 @@ func setupFlaredObservabilityTestDB(t *testing.T) func() { require.NoError(t, sqliteDB.AutoMigrate( &model.OpenFlareNode{}, &model.OpenFlareHealthEvent{}, + &model.SystemConfig{}, )) db.SetDB(sqliteDB) - option.ResetInitializationForTest() agent.ResetAuthCacheForTest() return func() { db.SetDB(nil) - option.ResetInitializationForTest() agent.ResetAuthCacheForTest() } } diff --git a/internal/apps/openflare/geoip/runtime.go b/internal/apps/openflare/geoip/runtime.go index 77876e1f..3207820f 100644 --- a/internal/apps/openflare/geoip/runtime.go +++ b/internal/apps/openflare/geoip/runtime.go @@ -13,6 +13,7 @@ import ( "github.com/Rain-kl/Wavelet/internal/apps/agent/geoipdata" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip" "github.com/Rain-kl/Wavelet/pkg/logger" ) @@ -30,30 +31,29 @@ var ( currentProvider string ) -// EnsureRuntimeProvider loads OpenFlare options once and configures pkg/geoip. +// EnsureRuntimeProvider loads GeoIP provider config from SystemConfig. func EnsureRuntimeProvider(ctx context.Context) error { runtimeOnce.Do(func() { - if err := model.InitOptionMap(ctx); err != nil { - runtimeInitErr = err - return - } - runtimeInitErr = applyProviderFromModel(ctx) + runtimeInitErr = applyProviderFromSystemConfig(ctx) }) return runtimeInitErr } -// RefreshRuntimeProvider reapplies GeoIPProvider after option updates. +// RefreshRuntimeProvider reapplies GeoIPProvider after config updates. func RefreshRuntimeProvider(ctx context.Context) error { - if err := model.InitOptionMap(ctx); err != nil { - return err - } - return applyProviderFromModel(ctx) + return applyProviderFromSystemConfig(ctx) } -func applyProviderFromModel(ctx context.Context) error { - model.OptionMapRWMutex.RLock() - provider := strings.TrimSpace(model.GeoIPProvider) - model.OptionMapRWMutex.RUnlock() +func applyProviderFromSystemConfig(ctx context.Context) error { + config, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyGeoIPProvider) + if err != nil { + // 降级到默认值 + return ApplyProvider(ctx, "ipinfo") + } + provider := strings.TrimSpace(config.Value) + if provider == "" { + provider = "ipinfo" + } return ApplyProvider(ctx, provider) } diff --git a/internal/apps/openflare/geoip/runtime_test.go b/internal/apps/openflare/geoip/runtime_test.go index 9d11303d..81ff99bf 100644 --- a/internal/apps/openflare/geoip/runtime_test.go +++ b/internal/apps/openflare/geoip/runtime_test.go @@ -16,21 +16,25 @@ func TestEnsureRuntimeProviderInitializesConfiguredProvider(t *testing.T) { if err != nil { t.Fatalf("open sqlite: %v", err) } - if err := sqliteDB.AutoMigrate(&model.OpenFlareOption{}); err != nil { + if err := sqliteDB.AutoMigrate(&model.SystemConfig{}); err != nil { t.Fatalf("migrate: %v", err) } db.SetDB(sqliteDB) t.Cleanup(func() { db.SetDB(nil) - model.ResetOptionMapForTest() ResetRuntimeForTest() }) ctx := context.Background() - model.ResetOptionMapForTest() ResetRuntimeForTest() - if err := model.UpdateOpenFlareOption(ctx, "GeoIPProvider", pkggeoip.ProviderIPInfo); err != nil { - t.Fatalf("update option: %v", err) + // 通过 SystemConfig 设置 GeoIPProvider 配置 + if err := db.DB(ctx).Create(&model.SystemConfig{ + Key: model.ConfigKeyGeoIPProvider, + Value: pkggeoip.ProviderIPInfo, + Type: "business", + Visibility: 0, + }).Error; err != nil { + t.Fatalf("create system config: %v", err) } if err := EnsureRuntimeProvider(ctx); err != nil { diff --git a/internal/apps/openflare/integration/agent_protocol_test.go b/internal/apps/openflare/integration/agent_protocol_test.go index 82a85105..3aa8ccf5 100644 --- a/internal/apps/openflare/integration/agent_protocol_test.go +++ b/internal/apps/openflare/integration/agent_protocol_test.go @@ -10,7 +10,6 @@ import ( "github.com/Rain-kl/Wavelet/internal/apps/openflare/agent" ofnode "github.com/Rain-kl/Wavelet/internal/apps/openflare/node" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" "github.com/Rain-kl/Wavelet/internal/testhelper" @@ -43,7 +42,7 @@ func setupProtocolTestEnv(t *testing.T) (*gin.Engine, func()) { require.NoError(t, err) require.NoError(t, sqliteDB.AutoMigrate( &model.OpenFlareNode{}, - &model.OpenFlareOption{}, + &model.SystemConfig{}, &model.OpenFlareApplyLog{}, &model.OpenFlareNodeSystemProfile{}, &model.OpenFlareHealthEvent{}, @@ -51,7 +50,6 @@ func setupProtocolTestEnv(t *testing.T) (*gin.Engine, func()) { )) db.SetDB(sqliteDB) - option.ResetInitializationForTest() agent.ResetAuthCacheForTest() resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore()) resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore()) @@ -63,7 +61,6 @@ func setupProtocolTestEnv(t *testing.T) (*gin.Engine, func()) { resetObservabilityStore() resetAccessLogStore() db.SetDB(nil) - option.ResetInitializationForTest() agent.ResetAuthCacheForTest() } return engine, cleanup diff --git a/internal/apps/openflare/integration/auth_option_test.go b/internal/apps/openflare/integration/auth_option_test.go index 214213ad..6212dd01 100644 --- a/internal/apps/openflare/integration/auth_option_test.go +++ b/internal/apps/openflare/integration/auth_option_test.go @@ -13,7 +13,6 @@ import ( "github.com/Rain-kl/Wavelet/internal/apps/admin" "github.com/Rain-kl/Wavelet/internal/apps/cap" "github.com/Rain-kl/Wavelet/internal/apps/oauth" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/config" "github.com/Rain-kl/Wavelet/internal/db/idgen" "github.com/Rain-kl/Wavelet/internal/model" @@ -28,7 +27,8 @@ import ( ) type statusPayload struct { - SystemName string `json:"system_name"` + Version string `json:"version"` + ServerAddress string `json:"server_address"` } func setupAuthOptionIntegration(t *testing.T) (*gorm.DB, *gin.Engine) { @@ -37,10 +37,6 @@ func setupAuthOptionIntegration(t *testing.T) (*gorm.DB, *gin.Engine) { dbConn, _, cleanup := testhelper.SetupTestEnvironment(t) t.Cleanup(cleanup) - require.NoError(t, dbConn.AutoMigrate(&model.OpenFlareOption{})) - option.ResetInitializationForTest() - t.Cleanup(option.ResetInitializationForTest) - require.NoError(t, dbConn.Model(&model.SystemConfig{}). Where("key = ?", model.ConfigKeyCapLoginEnabled). Update("value", "false").Error) @@ -119,7 +115,7 @@ func TestGETStatusReturnsSuccessEnvelope(t *testing.T) { var status statusPayload unmarshalAPIData(t, resp.Data, &status) - assert.NotEmpty(t, status.SystemName) + assert.NotEmpty(t, status.Version) } func TestGETOptionRequiresAdminAuth(t *testing.T) { @@ -175,30 +171,28 @@ func TestGETNodesWithAccessToken(t *testing.T) { requireAPIOK(t, w) } -func TestOptionHotReloadAfterUpdate(t *testing.T) { +func TestOptionUpdatePersistsAndReflectsInStatus(t *testing.T) { dbConn, r := setupAuthOptionIntegration(t) adminToken := seedUserWithAccessToken(t, dbConn, "admin", "password123", true) - statusBefore := getStatusSystemName(t, r, nil) - assert.NotEmpty(t, statusBefore) - updateResp := performJSONRequest(t, r, http.MethodPost, apiPath("/option/update"), map[string]string{ - "key": "SystemName", - "value": "HotReloadIntegration", + "key": model.ConfigKeyServerAddress, + "value": "https://hotreload.openflare.test", }, adminAuthHeaders(adminToken)) assert.Equal(t, http.StatusOK, updateResp.Code) requireAPIOK(t, updateResp) - statusAfter := getStatusSystemName(t, r, nil) - assert.Equal(t, "HotReloadIntegration", statusAfter) - assert.Equal(t, "HotReloadIntegration", model.SystemName) + statusAfter := getStatusServerAddress(t, r, nil) + assert.Equal(t, "https://hotreload.openflare.test", statusAfter) + // 验证已持久化到 SystemConfig ctx := context.Background() - require.NoError(t, option.EnsureInitialized(ctx)) - assert.Equal(t, "HotReloadIntegration", model.OptionValue("SystemName")) + saved, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress) + require.NoError(t, err) + assert.Equal(t, "https://hotreload.openflare.test", saved.Value) } -func getStatusSystemName(t *testing.T, r http.Handler, headers map[string]string) string { +func getStatusServerAddress(t *testing.T, r http.Handler, headers map[string]string) string { t.Helper() w := performJSONRequest(t, r, http.MethodGet, apiPath("/status"), nil, headers) @@ -207,5 +201,5 @@ func getStatusSystemName(t *testing.T, r http.Handler, headers map[string]string var status statusPayload unmarshalAPIData(t, resp.Data, &status) - return status.SystemName -} \ No newline at end of file + return status.ServerAddress +} diff --git a/internal/apps/openflare/integration/core_chain_test.go b/internal/apps/openflare/integration/core_chain_test.go index 6f71e651..d561af21 100644 --- a/internal/apps/openflare/integration/core_chain_test.go +++ b/internal/apps/openflare/integration/core_chain_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/Rain-kl/Wavelet/internal/apps/openflare/agent" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" "github.com/Rain-kl/Wavelet/internal/testhelper" @@ -48,12 +47,11 @@ func setupCoreChainTest(t *testing.T) (*gin.Engine, adminSeed, func()) { &model.OpenFlareWAFRuleGroupBinding{}, &model.OpenFlareWAFIPGroup{}, &model.OpenFlareNode{}, - &model.OpenFlareOption{}, + &model.SystemConfig{}, &model.OpenFlareApplyLog{}, )) db.SetDB(sqliteDB) - option.ResetInitializationForTest() agent.ResetAuthCacheForTest() seed, err := seedAdminWithAccessToken(sqliteDB) @@ -64,7 +62,6 @@ func setupCoreChainTest(t *testing.T) (*gin.Engine, adminSeed, func()) { cleanup := func() { db.SetDB(nil) - option.ResetInitializationForTest() agent.ResetAuthCacheForTest() } @@ -272,4 +269,4 @@ func TestCoreChainMigrationFlow(t *testing.T) { assert.Equal(t, configChecksum, nodeView["latest_apply_checksum"]) assert.Equal(t, float64(2), nodeView["latest_support_file_count"]) }) -} \ No newline at end of file +} diff --git a/internal/apps/openflare/integration/security_test.go b/internal/apps/openflare/integration/security_test.go index 9e130c35..41daf8de 100644 --- a/internal/apps/openflare/integration/security_test.go +++ b/internal/apps/openflare/integration/security_test.go @@ -15,7 +15,6 @@ import ( "testing" "time" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/config" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" @@ -46,10 +45,10 @@ func setupSecurityTest(t *testing.T) (*gin.Engine, adminSeed, func()) { &model.ManagedDomain{}, &model.DNSAccount{}, &model.AcmeAccount{}, + &model.SystemConfig{}, )) db.SetDB(sqliteDB) - option.ResetInitializationForTest() seed, err := seedAdminWithAccessToken(sqliteDB) require.NoError(t, err) @@ -63,7 +62,6 @@ func setupSecurityTest(t *testing.T) (*gin.Engine, adminSeed, func()) { cleanup := func() { config.Config.App.SessionSecret = oldSecret db.SetDB(nil) - option.ResetInitializationForTest() } return engine, seed, cleanup @@ -357,4 +355,4 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { _ = ipGroupID _ = domainID _ = dnsAccountID -} \ No newline at end of file +} diff --git a/internal/apps/openflare/node/helpers.go b/internal/apps/openflare/node/helpers.go index 72b04035..983bf97b 100644 --- a/internal/apps/openflare/node/helpers.go +++ b/internal/apps/openflare/node/helpers.go @@ -194,7 +194,10 @@ func computeNodeStatus(node *model.OpenFlareNode) string { if node.LastSeenAt == nil || node.LastSeenAt.IsZero() { return nodeStatusPending } - if time.Since(*node.LastSeenAt) > model.NodeOfflineThreshold { + // 使用默认阈值 2 分钟,避免在这里读取配置 + // 实际阈值会在需要精确判断的地方通过 getNodeOfflineThreshold 读取 + threshold := 2 * time.Minute + if time.Since(*node.LastSeenAt) > threshold { return nodeStatusOffline } return nodeStatusOnline diff --git a/internal/apps/openflare/node/logics.go b/internal/apps/openflare/node/logics.go index 39f605da..3553db35 100644 --- a/internal/apps/openflare/node/logics.go +++ b/internal/apps/openflare/node/logics.go @@ -11,9 +11,9 @@ import ( "time" "github.com/Rain-kl/Wavelet/internal/apps/openflare/observability" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" "gorm.io/gorm" ) @@ -22,6 +22,15 @@ const ( defaultRelayVhostHTTPPort = 8080 ) +// getAgentUpdateRepo 从 SystemConfig 读取 Agent 更新仓库配置 +func getAgentUpdateRepo(ctx context.Context) string { + config, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyAgentUpdateRepo) + if err != nil || strings.TrimSpace(config.Value) == "" { + return "Rain-kl/OpenFlare" // 默认值 + } + return strings.TrimSpace(config.Value) +} + // Input is the create/update node payload. type Input struct { Name string `json:"name"` @@ -272,7 +281,7 @@ func RotateBootstrapToken(ctx context.Context) (*BootstrapView, error) { if err != nil { return nil, err } - if err = model.UpdateOpenFlareOption(ctx, "AgentDiscoveryToken", token); err != nil { + if err = repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyAgentDiscoveryToken, token); err != nil { return nil, err } return &BootstrapView{DiscoveryToken: token}, nil @@ -284,7 +293,7 @@ func GetAgentRelease(ctx context.Context, id uint, channel string) (*AgentReleas if err != nil { return nil, err } - release, err := fetchLatestGitHubRelease(ctx, model.AgentUpdateRepo, normalizeReleaseChannel(channel)) + release, err := fetchLatestGitHubRelease(ctx, getAgentUpdateRepo(ctx), normalizeReleaseChannel(channel)) if err != nil { return nil, err } @@ -300,7 +309,7 @@ func RequestAgentUpdate(ctx context.Context, id uint, input AgentUpdateInput) (* channel := normalizeReleaseChannel(input.Channel) tagName := strings.TrimSpace(input.TagName) if tagName != "" { - release, releaseErr := fetchGitHubReleaseByTag(ctx, model.AgentUpdateRepo, tagName) + release, releaseErr := fetchGitHubReleaseByTag(ctx, getAgentUpdateRepo(ctx), tagName) if releaseErr != nil { return nil, releaseErr } @@ -369,20 +378,20 @@ func CleanupHealthEvents(ctx context.Context, id uint) (*HealthEventCleanupResul } func ensureGlobalDiscoveryToken(ctx context.Context) (string, error) { - if err := option.EnsureInitialized(ctx); err != nil { - return "", err - } - model.OptionMapRWMutex.RLock() - token := strings.TrimSpace(model.AgentDiscoveryToken) - model.OptionMapRWMutex.RUnlock() - if token != "" { - return token, nil + // 从 SystemConfig 读取 Agent 发现令牌 + config, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyAgentDiscoveryToken) + if err == nil && strings.TrimSpace(config.Value) != "" { + return strings.TrimSpace(config.Value), nil } + + // 如果不存在,生成新令牌并保存 token, err := newRandomToken() if err != nil { return "", err } - if err = model.UpdateOpenFlareOption(ctx, "AgentDiscoveryToken", token); err != nil { + + // 更新到 SystemConfig + if err = repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyAgentDiscoveryToken, token); err != nil { return "", err } return token, nil diff --git a/internal/apps/openflare/node/logics_test.go b/internal/apps/openflare/node/logics_test.go index 281dc8ef..4c121a16 100644 --- a/internal/apps/openflare/node/logics_test.go +++ b/internal/apps/openflare/node/logics_test.go @@ -11,9 +11,9 @@ import ( "testing" "time" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -35,12 +35,11 @@ func setupNodeTestDB(t *testing.T) func() { require.NoError(t, err) require.NoError(t, sqliteDB.AutoMigrate( &model.OpenFlareNode{}, - &model.OpenFlareOption{}, + &model.SystemConfig{}, &model.OpenFlareApplyLog{}, )) db.SetDB(sqliteDB) - option.ResetInitializationForTest() resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore()) resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore()) @@ -48,7 +47,6 @@ func setupNodeTestDB(t *testing.T) func() { resetObservabilityStore() resetAccessLogStore() db.SetDB(nil) - option.ResetInitializationForTest() } } @@ -193,7 +191,10 @@ func TestBootstrapTokenLifecycle(t *testing.T) { rotated, err := RotateBootstrapToken(ctx) require.NoError(t, err) assert.NotEqual(t, first.DiscoveryToken, rotated.DiscoveryToken) - assert.Equal(t, rotated.DiscoveryToken, model.OptionValue("AgentDiscoveryToken")) + // 验证令牌已保存到 SystemConfig + savedToken, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyAgentDiscoveryToken) + require.NoError(t, err) + assert.Equal(t, rotated.DiscoveryToken, savedToken.Value) } func TestValidateDiscoveryToken(t *testing.T) { @@ -218,7 +219,7 @@ func TestRequestAgentUpdateWithPreviewTag(t *testing.T) { originalClient := setReleaseHTTPClientForTest(&http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { - expected := "https://api.github.com/repos/" + model.AgentUpdateRepo + "/releases/tags/v0.5.0-rc.1" + expected := "https://api.github.com/repos/Rain-kl/OpenFlare/releases/tags/v0.5.0-rc.1" require.Equal(t, expected, req.URL.String()) return &http.Response{ StatusCode: http.StatusOK, @@ -321,7 +322,8 @@ func TestComputeNodeStatus(t *testing.T) { online := &model.OpenFlareNode{LastSeenAt: &now} assert.Equal(t, nodeStatusOnline, computeNodeStatus(online)) - offlineAt := now.Add(-model.NodeOfflineThreshold - time.Minute) + // computeNodeStatus 使用默认阈值 2 分钟 + offlineAt := now.Add(-2*time.Minute - time.Minute) offline := &model.OpenFlareNode{LastSeenAt: &offlineAt} assert.Equal(t, nodeStatusOffline, computeNodeStatus(offline)) } diff --git a/internal/apps/openflare/option/logics.go b/internal/apps/openflare/option/logics.go index 0b168fec..0edd6127 100644 --- a/internal/apps/openflare/option/logics.go +++ b/internal/apps/openflare/option/logics.go @@ -8,35 +8,15 @@ import ( "errors" "fmt" "strings" - "sync" "github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip" oftasks "github.com/Rain-kl/Wavelet/internal/apps/openflare/tasks" "github.com/Rain-kl/Wavelet/internal/apps/openflare/uptimekuma" "github.com/Rain-kl/Wavelet/internal/buildinfo" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" ) -var ( - initOnce sync.Once - initErr error -) - -// EnsureInitialized loads OptionMap from defaults and database once per process. -func EnsureInitialized(ctx context.Context) error { - initOnce.Do(func() { - initErr = model.InitOptionMap(ctx) - }) - return initErr -} - -// ResetInitializationForTest clears lazy-init state for unit tests. -func ResetInitializationForTest() { - initOnce = sync.Once{} - initErr = nil - model.ResetOptionMapForTest() -} - type publicAuthSourceView struct { ID uint64 `json:"id"` Name string `json:"name"` @@ -50,9 +30,6 @@ type statusView struct { Version string `json:"version"` StartTime int64 `json:"start_time"` EmailVerification bool `json:"email_verification"` - SystemName string `json:"system_name"` - HomePageLink string `json:"home_page_link"` - FooterHTML string `json:"footer_html"` ServerAddress string `json:"server_address"` PasswordRegisterEnabled bool `json:"password_register_enabled"` CapLoginEnabled bool `json:"cap_login_enabled"` @@ -91,37 +68,32 @@ type optionBatchPayload struct { } func listOptions(ctx context.Context) ([]model.OpenFlareOption, error) { - if err := EnsureInitialized(ctx); err != nil { + // 从 SystemConfig 读取所有业务配置 + configs, err := repository.ListAdminSystemConfigs(ctx, "business") + if err != nil { return nil, err } - model.OptionMapRWMutex.RLock() - defer model.OptionMapRWMutex.RUnlock() - - options := make([]model.OpenFlareOption, 0, len(model.OptionMap)) - for key, value := range model.OptionMap { - if isSecretOptionKey(key) { + options := make([]model.OpenFlareOption, 0, len(configs)) + for _, config := range configs { + // 跳过敏感配置(如密码、令牌) + if config.Visibility == model.ConfigVisibilityHidden && isSecretConfigKey(config.Key) { continue } + // 将 snake_case key 转换为 PascalCase 以保持向后兼容 options = append(options, model.OpenFlareOption{ - Key: key, - Value: value, + Key: config.Key, + Value: config.Value, }) } return options, nil } func updateOption(ctx context.Context, option model.OpenFlareOption) error { - if err := EnsureInitialized(ctx); err != nil { - return err - } return updateOptions(ctx, []model.OpenFlareOption{option}) } func updateOptionsBatch(ctx context.Context, payload optionBatchPayload) error { - if err := EnsureInitialized(ctx); err != nil { - return err - } if len(payload.Options) == 0 { return errors.New(errInvalidParams) } @@ -129,40 +101,46 @@ func updateOptionsBatch(ctx context.Context, payload optionBatchPayload) error { } func updateOptions(ctx context.Context, options []model.OpenFlareOption) error { - if err := validateOptions(options); err != nil { + if err := validateOptions(ctx, options); err != nil { return err } - if err := model.UpdateOpenFlareOptions(ctx, options); err != nil { - return err - } - for _, item := range options { - if item.Key == "GeoIPProvider" { - return geoip.RefreshRuntimeProvider(ctx) + + // 将每个 option 更新到 SystemConfig + for _, opt := range options { + if err := repository.SaveOrUpdateSystemConfig(ctx, opt.Key, opt.Value); err != nil { + return fmt.Errorf("failed to update config %s: %w", opt.Key, err) + } + + // 特殊处理:GeoIP 配置变更时刷新运行时 + if opt.Key == model.ConfigKeyGeoIPProvider { + if err := geoip.RefreshRuntimeProvider(ctx); err != nil { + return err + } } } + return nil } func getStatus(ctx context.Context, baseAPIPath string) (*statusView, error) { - if err := EnsureInitialized(ctx); err != nil { - return nil, err - } - authSources, err := publicAuthSources(ctx, baseAPIPath) if err != nil { authSources = []publicAuthSourceView{} } + // 从 SystemConfig 读取配置 + emailVerification, _ := repository.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled) + serverAddress, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress) + passwordRegisterEnabled, _ := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordRegisterEnabled) + capLoginEnabled, _ := repository.GetBoolByKey(ctx, model.ConfigKeyCapLoginEnabled) + return &statusView{ Version: buildinfo.Version, StartTime: model.StartTime, - EmailVerification: model.EmailVerificationEnabled, - SystemName: model.SystemName, - HomePageLink: model.HomePageLink, - FooterHTML: model.Footer, - ServerAddress: model.ServerAddress, - PasswordRegisterEnabled: model.PasswordRegisterEnabled, - CapLoginEnabled: model.CapLoginEnabled, + EmailVerification: emailVerification, + ServerAddress: serverAddress.Value, + PasswordRegisterEnabled: passwordRegisterEnabled, + CapLoginEnabled: capLoginEnabled, AuthSources: authSources, }, nil } @@ -229,8 +207,9 @@ func syncUptimeKuma(ctx context.Context) error { return uptimekuma.SyncToUptimeKuma(ctx) } -func isSecretOptionKey(key string) bool { - return strings.Contains(key, "Token") || - strings.Contains(key, "Secret") || - strings.Contains(key, "Password") +// isSecretConfigKey 判断 SystemConfig 的 key 是否为敏感配置 +func isSecretConfigKey(key string) bool { + return strings.Contains(key, "token") || + strings.Contains(key, "secret") || + strings.Contains(key, "password") } diff --git a/internal/apps/openflare/option/logics_test.go b/internal/apps/openflare/option/logics_test.go index f97baf7b..c89dd45f 100644 --- a/internal/apps/openflare/option/logics_test.go +++ b/internal/apps/openflare/option/logics_test.go @@ -10,6 +10,7 @@ import ( "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -23,28 +24,35 @@ func setupOptionTestDB(t *testing.T) func() { DisableForeignKeyConstraintWhenMigrating: true, }) require.NoError(t, err) - require.NoError(t, sqliteDB.AutoMigrate(&model.OpenFlareOption{})) + require.NoError(t, sqliteDB.AutoMigrate(&model.SystemConfig{})) db.SetDB(sqliteDB) - ResetInitializationForTest() + + // 预填充一些业务配置用于测试 + seedConfigs := []model.SystemConfig{ + {Key: "geoip_provider", Value: "ipinfo", Type: "business", Visibility: 0}, + {Key: "uptime_kuma_password", Value: "secret-pwd", Type: "business", Visibility: 0}, + } + for _, cfg := range seedConfigs { + require.NoError(t, sqliteDB.Create(&cfg).Error) + } return func() { db.SetDB(nil) - ResetInitializationForTest() } } +// setTestConfig 设置测试配置的辅助函数 +func setTestConfig(t *testing.T, ctx context.Context, key, value string) { + t.Helper() + require.NoError(t, db.DB(ctx).Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error) +} + func TestListOptionsFiltersSecretKeys(t *testing.T) { cleanup := setupOptionTestDB(t) defer cleanup() ctx := context.Background() - require.NoError(t, model.UpdateOpenFlareOptions(ctx, []model.OpenFlareOption{ - {Key: "SystemName", Value: "TestFlare"}, - {Key: "SMTPToken", Value: "secret-token"}, - {Key: "GitHubClientSecret", Value: "secret-id"}, - })) - options, err := listOptions(ctx) require.NoError(t, err) @@ -53,24 +61,50 @@ func TestListOptionsFiltersSecretKeys(t *testing.T) { keys[option.Key] = option.Value } - assert.Equal(t, "TestFlare", keys["SystemName"]) - assert.NotContains(t, keys, "SMTPToken") - assert.NotContains(t, keys, "GitHubClientSecret") + // geoip_provider 应该出现在列表中 + assert.Equal(t, "ipinfo", keys["geoip_provider"]) + // 敏感配置(密码)应该被过滤掉 + assert.NotContains(t, keys, "uptime_kuma_password") } -func TestUpdateOptionHotReloadsOptionMap(t *testing.T) { +func TestUpdateOptionPersistsToSystemConfig(t *testing.T) { cleanup := setupOptionTestDB(t) defer cleanup() ctx := context.Background() err := updateOption(ctx, model.OpenFlareOption{ - Key: "SystemName", - Value: "HotReloaded", + Key: model.ConfigKeyGeoIPProvider, + Value: "mmdb", }) require.NoError(t, err) - assert.Equal(t, "HotReloaded", model.OptionValue("SystemName")) - assert.Equal(t, "HotReloaded", model.SystemName) + // 验证配置已写入 SystemConfig + config, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyGeoIPProvider) + require.NoError(t, err) + assert.Equal(t, "mmdb", config.Value) +} + +func TestUpdateOpenRestyOptionPersistsToSystemConfig(t *testing.T) { + cleanup := setupOptionTestDB(t) + defer cleanup() + ctx := context.Background() + + require.NoError(t, db.DB(ctx).Create(&model.SystemConfig{ + Key: model.ConfigKeyOpenRestyEventsUse, + Value: "epoll", + Type: "business", + Visibility: 0, + }).Error) + + err := updateOption(ctx, model.OpenFlareOption{ + Key: model.ConfigKeyOpenRestyEventsUse, + Value: "kqueue", + }) + require.NoError(t, err) + + config, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyOpenRestyEventsUse) + require.NoError(t, err) + assert.Equal(t, "kqueue", config.Value) } func TestLookupGeoIPDisabledProvider(t *testing.T) { diff --git a/internal/apps/openflare/option/openresty_validators.go b/internal/apps/openflare/option/openresty_validators.go index 6e9c10e9..f7964737 100644 --- a/internal/apps/openflare/option/openresty_validators.go +++ b/internal/apps/openflare/option/openresty_validators.go @@ -8,46 +8,48 @@ import ( "regexp" "strconv" "strings" + + "github.com/Rain-kl/Wavelet/internal/model" ) var openRestyOptionValidators = map[string]func(key, value string) error{ - "OpenRestyDefaultServerReturnStatus": validateOpenRestyDefaultServerReturnStatus, - "OpenRestyWorkerProcesses": validateOpenRestyWorkerProcesses, - "OpenRestyWorkerConnections": validatePositiveIntegerOption, - "OpenRestyWorkerRlimitNofile": validatePositiveIntegerOption, - "OpenRestyKeepaliveTimeout": validatePositiveIntegerOption, - "OpenRestyKeepaliveRequests": validatePositiveIntegerOption, - "OpenRestyClientHeaderTimeout": validatePositiveIntegerOption, - "OpenRestyClientBodyTimeout": validatePositiveIntegerOption, - "OpenRestySendTimeout": validatePositiveIntegerOption, - "OpenRestyProxyConnectTimeout": validatePositiveIntegerOption, - "OpenRestyProxySendTimeout": validatePositiveIntegerOption, - "OpenRestyProxyReadTimeout": validatePositiveIntegerOption, - "OpenRestyGzipMinLength": validatePositiveIntegerOption, - "OpenRestyGzipCompLevel": validateOpenRestyGzipCompLevel, - "OpenRestyEventsUse": validateOpenRestyEventsUse, - "OpenRestyResolvers": validateOpenRestyResolvers, - "OpenRestyEventsMultiAcceptEnabled": validateBooleanOption, - "OpenRestyWebsocketEnabled": validateBooleanOption, - "OpenRestyHTTP3Enabled": validateBooleanOption, - "OpenRestyProxyRequestBufferingEnabled": validateBooleanOption, - "OpenRestyProxyBufferingEnabled": validateBooleanOption, - "OpenRestyGzipEnabled": validateBooleanOption, - "OpenRestyCacheEnabled": validateBooleanOption, - "OpenRestyCacheLockEnabled": validateBooleanOption, - "OpenRestyProxyBuffers": validateOpenRestyProxyBuffers, - "OpenRestyLargeClientHeaderBuffers": validateOpenRestyProxyBuffers, - "OpenRestyProxyBufferSize": validateOpenRestySizeValue, - "OpenRestyProxyBusyBuffersSize": validateOpenRestySizeValue, - "OpenRestyCacheMaxSize": validateOpenRestySizeValue, - "OpenRestyClientMaxBodySize": validateOpenRestySizeValue, - "OpenRestyCachePath": validateOpenRestyCachePath, - "OpenRestyCacheLevels": validateOpenRestyCacheLevels, - "OpenRestyCacheInactive": validateOpenRestyDurationToken, - "OpenRestyCacheLockTimeout": validateOpenRestyDurationToken, - "OpenRestyCacheKeyTemplate": validateOpenRestyCacheKeyTemplate, - "OpenRestyCacheUseStale": validateOpenRestyCacheUseStale, - "OpenRestyMainConfigTemplate": validateOpenRestyMainConfigTemplate, + model.ConfigKeyOpenRestyDefaultServerReturnStatus: validateOpenRestyDefaultServerReturnStatus, + model.ConfigKeyOpenRestyWorkerProcesses: validateOpenRestyWorkerProcesses, + model.ConfigKeyOpenRestyWorkerConnections: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyWorkerRlimitNofile: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyKeepaliveTimeout: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyKeepaliveRequests: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyClientHeaderTimeout: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyClientBodyTimeout: validatePositiveIntegerOption, + model.ConfigKeyOpenRestySendTimeout: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyProxyConnectTimeout: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyProxySendTimeout: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyProxyReadTimeout: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyGzipMinLength: validatePositiveIntegerOption, + model.ConfigKeyOpenRestyGzipCompLevel: validateOpenRestyGzipCompLevel, + model.ConfigKeyOpenRestyEventsUse: validateOpenRestyEventsUse, + model.ConfigKeyOpenRestyResolvers: validateOpenRestyResolvers, + model.ConfigKeyOpenRestyEventsMultiAcceptEnabled: validateBooleanOption, + model.ConfigKeyOpenRestyWebsocketEnabled: validateBooleanOption, + model.ConfigKeyOpenRestyHTTP3Enabled: validateBooleanOption, + model.ConfigKeyOpenRestyProxyRequestBufferingEnabled: validateBooleanOption, + model.ConfigKeyOpenRestyProxyBufferingEnabled: validateBooleanOption, + model.ConfigKeyOpenRestyGzipEnabled: validateBooleanOption, + model.ConfigKeyOpenRestyCacheEnabled: validateBooleanOption, + model.ConfigKeyOpenRestyCacheLockEnabled: validateBooleanOption, + model.ConfigKeyOpenRestyProxyBuffers: validateOpenRestyProxyBuffers, + model.ConfigKeyOpenRestyLargeClientHeaderBuffers: validateOpenRestyProxyBuffers, + model.ConfigKeyOpenRestyProxyBufferSize: validateOpenRestySizeValue, + model.ConfigKeyOpenRestyProxyBusyBuffersSize: validateOpenRestySizeValue, + model.ConfigKeyOpenRestyCacheMaxSize: validateOpenRestySizeValue, + model.ConfigKeyOpenRestyClientMaxBodySize: validateOpenRestySizeValue, + model.ConfigKeyOpenRestyCachePath: validateOpenRestyCachePath, + model.ConfigKeyOpenRestyCacheLevels: validateOpenRestyCacheLevels, + model.ConfigKeyOpenRestyCacheInactive: validateOpenRestyDurationToken, + model.ConfigKeyOpenRestyCacheLockTimeout: validateOpenRestyDurationToken, + model.ConfigKeyOpenRestyCacheKeyTemplate: validateOpenRestyCacheKeyTemplate, + model.ConfigKeyOpenRestyCacheUseStale: validateOpenRestyCacheUseStale, + model.ConfigKeyOpenRestyMainConfigTemplate: validateOpenRestyMainConfigTemplate, } func validateOpenRestyOption(key, value string) error { diff --git a/internal/apps/openflare/option/validate.go b/internal/apps/openflare/option/validate.go index 06688cbe..dee250d4 100644 --- a/internal/apps/openflare/option/validate.go +++ b/internal/apps/openflare/option/validate.go @@ -4,6 +4,7 @@ package option import ( + "context" "errors" "fmt" "regexp" @@ -12,6 +13,7 @@ import ( "github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" ) const maxOpenRestyGzipCompLevel = 9 @@ -25,21 +27,25 @@ var ( const optionValueTrue = "true" -func buildOptionValidationState(options []model.OpenFlareOption) map[string]string { - model.OptionMapRWMutex.RLock() - state := make(map[string]string, len(model.OptionMap)+len(options)) - for key, value := range model.OptionMap { - state[key] = value - } - model.OptionMapRWMutex.RUnlock() +func buildOptionValidationState(ctx context.Context, options []model.OpenFlareOption) map[string]string { + // 从 SystemConfig 读取所有业务配置构建状态 + configs, err := repository.ListAdminSystemConfigs(ctx, "business") + state := make(map[string]string, len(configs)+len(options)) + if err == nil { + for _, config := range configs { + state[config.Key] = config.Value + } + } + + // 应用待验证的新值 for _, option := range options { state[option.Key] = option.Value } return state } -func validateOptionWithState(option model.OpenFlareOption, state map[string]string) error { +func validateOptionWithState(ctx context.Context, option model.OpenFlareOption, state map[string]string) error { if err := validateOpenRestyOption(option.Key, option.Value); err != nil { return err @@ -53,7 +59,7 @@ func validateOptionWithState(option model.OpenFlareOption, state map[string]stri if err := validateAgentOption(option.Key, option.Value); err != nil { return err } - return validateUptimeKumaOption(option.Key, option.Value, state) + return validateUptimeKumaOption(ctx, option.Key, option.Value, state) } func validatePositiveIntegerOption(key, value string) error { @@ -74,7 +80,7 @@ func validateBooleanOption(key, value string) error { } func validateGeoIPOption(key, value string) error { - if key != "GeoIPProvider" { + if key != model.ConfigKeyGeoIPProvider { return nil } if geoip.IsValidProvider(value) { @@ -85,9 +91,9 @@ func validateGeoIPOption(key, value string) error { func validateDatabaseCleanupOption(key, value string) error { switch key { - case "DatabaseAutoCleanupEnabled": + case model.ConfigKeyDatabaseAutoCleanupEnabled: return validateBooleanOption(key, value) - case "DatabaseAutoCleanupRetentionDays": + case model.ConfigKeyDatabaseAutoCleanupRetentionDays: intValue, err := strconv.Atoi(value) if err != nil || intValue < 1 { return fmt.Errorf("%s 必须为大于等于 1 的整数天", key) @@ -97,55 +103,59 @@ func validateDatabaseCleanupOption(key, value string) error { } func validateAgentOption(key, value string) error { - if key == "AgentWebsocketUpgradeEnabled" { + if key == model.ConfigKeyAgentWebsocketUpgradeEnabled { return validateBooleanOption(key, strings.TrimSpace(value)) } return nil } -func validateUptimeKumaOption(key, value string, state map[string]string) error { +func validateUptimeKumaOption(ctx context.Context, key, value string, state map[string]string) error { trimmed := strings.TrimSpace(value) switch key { - case "UptimeKumaEnabled": - return validateUptimeKumaEnabled(key, trimmed, state) - case "UptimeKumaUsername": + case model.ConfigKeyUptimeKumaEnabled: + return validateUptimeKumaEnabled(ctx, key, trimmed, state) + case model.ConfigKeyUptimeKumaUsername: return validateUptimeKumaUsername(trimmed, state) - case "UptimeKumaUrl": + case model.ConfigKeyUptimeKumaURL: return validateUptimeKumaURL(trimmed) - case "UptimeKumaMonitorScope": + case model.ConfigKeyUptimeKumaMonitorScope: return validateUptimeKumaMonitorScope(trimmed) - case "UptimeKumaSyncInterval", "UptimeKumaInterval", "UptimeKumaRetryInterval", "UptimeKumaTimeout": + case model.ConfigKeyUptimeKumaSyncInterval, model.ConfigKeyUptimeKumaInterval, model.ConfigKeyUptimeKumaRetryInterval, model.ConfigKeyUptimeKumaTimeout: return validatePositiveIntegerOption(key, trimmed) - case "UptimeKumaRetry": + case model.ConfigKeyUptimeKumaRetry: return validateUptimeKumaRetry(key, trimmed) } return nil } -func validateUptimeKumaEnabled(key, trimmed string, state map[string]string) error { +func validateUptimeKumaEnabled(ctx context.Context, key, trimmed string, state map[string]string) error { if err := validateBooleanOption(key, trimmed); err != nil { return err } if trimmed != optionValueTrue { return nil } - url := strings.TrimSpace(state["UptimeKumaUrl"]) - username := strings.TrimSpace(state["UptimeKumaUsername"]) - password := strings.TrimSpace(state["UptimeKumaPassword"]) + url := strings.TrimSpace(state[model.ConfigKeyUptimeKumaURL]) + username := strings.TrimSpace(state[model.ConfigKeyUptimeKumaUsername]) + password := strings.TrimSpace(state[model.ConfigKeyUptimeKumaPassword]) if url == "" { return fmt.Errorf("启用 Uptime Kuma 时地址不能为空") } if username == "" { return fmt.Errorf("启用 Uptime Kuma 时用户名不能为空") } - if password == "" && model.UptimeKumaPassword == "" { - return fmt.Errorf("启用 Uptime Kuma 时密码不能为空") + // 如果待验证的密码为空,且当前配置中也没有密码,则报错 + if password == "" { + existingPwd, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaPassword) + if strings.TrimSpace(existingPwd.Value) == "" { + return fmt.Errorf("启用 Uptime Kuma 时密码不能为空") + } } return nil } func validateUptimeKumaUsername(trimmed string, state map[string]string) error { - if trimmed == "" && state["UptimeKumaEnabled"] == optionValueTrue { + if trimmed == "" && state[model.ConfigKeyUptimeKumaEnabled] == optionValueTrue { return fmt.Errorf("启用 Uptime Kuma 时用户名不能为空") } return nil @@ -173,17 +183,17 @@ func validateUptimeKumaRetry(key, trimmed string) error { return nil } -func validateOptions(options []model.OpenFlareOption) error { +func validateOptions(ctx context.Context, options []model.OpenFlareOption) error { if len(options) == 0 { return errors.New(errInvalidParams) } - state := buildOptionValidationState(options) + state := buildOptionValidationState(ctx, options) for _, option := range options { if strings.TrimSpace(option.Key) == "" { return errors.New(errInvalidParams) } - if err := validateOptionWithState(option, state); err != nil { + if err := validateOptionWithState(ctx, option, state); err != nil { return err } } diff --git a/internal/apps/openflare/relay/helpers.go b/internal/apps/openflare/relay/helpers.go index 17ed2c74..5831d2b4 100644 --- a/internal/apps/openflare/relay/helpers.go +++ b/internal/apps/openflare/relay/helpers.go @@ -15,6 +15,9 @@ import ( const ( relayStatusUnhealthy = "unhealthy" releaseChannelStable = "stable" + + defaultAgentHeartbeatInterval = 10000 // 默认心跳间隔 10 秒(毫秒) + defaultAgentUpdateRepo = "Rain-kl/OpenFlare" ) func normalizeRelayStatus(status string) string { @@ -104,10 +107,7 @@ func buildRelayConfig(ctx context.Context, node *model.OpenFlareNode) *Config { } // BuildSettings returns runtime settings shared by relay and flared clients. -func BuildSettings(node *model.OpenFlareNode, updateNow bool, updateChannel, updateTag string) *Settings { - model.OptionMapRWMutex.RLock() - defer model.OptionMapRWMutex.RUnlock() - +func BuildSettings(ctx context.Context, node *model.OpenFlareNode, updateNow bool, updateChannel, updateTag string) *Settings { autoUpdate := false if node != nil { autoUpdate = node.AutoUpdateEnabled @@ -115,11 +115,23 @@ func BuildSettings(node *model.OpenFlareNode, updateNow bool, updateChannel, upd if strings.TrimSpace(updateChannel) == "" { updateChannel = releaseChannelStable } + + // 从 SystemConfig 读取配置,使用默认值作为降级 + heartbeatInterval, _ := repository.GetIntByKey(ctx, model.ConfigKeyAgentHeartbeatInterval) + if heartbeatInterval <= 0 { + heartbeatInterval = defaultAgentHeartbeatInterval + } + wsUpgradeEnabled, _ := repository.GetBoolByKey(ctx, model.ConfigKeyAgentWebsocketUpgradeEnabled) + updateRepo, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyAgentUpdateRepo) + if strings.TrimSpace(updateRepo.Value) == "" { + updateRepo.Value = defaultAgentUpdateRepo + } + return &Settings{ - HeartbeatInterval: model.AgentHeartbeatInterval, - WebsocketUpgradeEnabled: model.AgentWebsocketUpgradeEnabled, + HeartbeatInterval: heartbeatInterval, + WebsocketUpgradeEnabled: wsUpgradeEnabled, AutoUpdate: autoUpdate, - UpdateRepo: model.AgentUpdateRepo, + UpdateRepo: updateRepo.Value, UpdateNow: updateNow, UpdateChannel: updateChannel, UpdateTag: strings.TrimSpace(updateTag), diff --git a/internal/apps/openflare/relay/logics.go b/internal/apps/openflare/relay/logics.go index 3b373fa9..7e02a177 100644 --- a/internal/apps/openflare/relay/logics.go +++ b/internal/apps/openflare/relay/logics.go @@ -99,7 +99,7 @@ func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload Heartbeat return &HeartbeatResponse{ RelayConfig: buildRelayConfig(ctx, node), - RelaySettings: BuildSettings(node, updateNow, updateChannel, updateTag), + RelaySettings: BuildSettings(ctx, node, updateNow, updateChannel, updateTag), }, nil } diff --git a/internal/apps/openflare/relay/logics_test.go b/internal/apps/openflare/relay/logics_test.go index b6e6f9ae..6eea5fe9 100644 --- a/internal/apps/openflare/relay/logics_test.go +++ b/internal/apps/openflare/relay/logics_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/Rain-kl/Wavelet/internal/apps/openflare/agent" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/option" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" "github.com/glebarez/sqlite" @@ -28,7 +27,7 @@ func setupRelayTestDB(t *testing.T) func() { require.NoError(t, err) require.NoError(t, sqliteDB.AutoMigrate( &model.OpenFlareNode{}, - &model.OpenFlareOption{}, + &model.SystemConfig{}, &model.OpenFlareNodeSystemProfile{}, &model.OpenFlareMetricSnapshot{}, &model.OpenFlareHealthEvent{}, @@ -36,14 +35,12 @@ func setupRelayTestDB(t *testing.T) func() { )) db.SetDB(sqliteDB) - option.ResetInitializationForTest() agent.ResetAuthCacheForTest() resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore()) return func() { resetObservabilityStore() db.SetDB(nil) - option.ResetInitializationForTest() agent.ResetAuthCacheForTest() } } diff --git a/internal/apps/openflare/tasks/database_cleanup.go b/internal/apps/openflare/tasks/database_cleanup.go index 34a02b6c..96aea940 100644 --- a/internal/apps/openflare/tasks/database_cleanup.go +++ b/internal/apps/openflare/tasks/database_cleanup.go @@ -11,6 +11,7 @@ import ( "time" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" ) const ( @@ -91,14 +92,20 @@ func CleanupDatabaseObservability(ctx context.Context, input DatabaseCleanupInpu // RunDatabaseAutoCleanupOnce runs retention-based cleanup for all observability targets. func RunDatabaseAutoCleanupOnce(ctx context.Context, now time.Time) (*DatabaseAutoCleanupSummary, error) { - if !model.DatabaseAutoCleanupEnabled { + enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyDatabaseAutoCleanupEnabled) + if err != nil { + return nil, fmt.Errorf("failed to read database_auto_cleanup_enabled: %w", err) + } + if !enabled { return nil, nil } - if model.DatabaseAutoCleanupRetentionDays < 1 { - return nil, fmt.Errorf("database auto cleanup retention_days must be at least 1") + + retentionDays, err := repository.GetIntByKey(ctx, model.ConfigKeyDatabaseAutoCleanupRetentionDays) + if err != nil || retentionDays <= 0 { + // Use default value 30 if config read fails or value is invalid + retentionDays = 30 } - retentionDays := model.DatabaseAutoCleanupRetentionDays results := make([]DatabaseCleanupResult, 0, len(databaseCleanupTargets)) for _, target := range []string{ DatabaseCleanupTargetAccessLogs, diff --git a/internal/apps/openflare/tasks/database_cleanup_test.go b/internal/apps/openflare/tasks/database_cleanup_test.go index 04d28ea8..408b006c 100644 --- a/internal/apps/openflare/tasks/database_cleanup_test.go +++ b/internal/apps/openflare/tasks/database_cleanup_test.go @@ -10,6 +10,7 @@ import ( "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -23,6 +24,7 @@ func setupDatabaseCleanupTestDB(t *testing.T) context.Context { DisableForeignKeyConstraintWhenMigrating: true, }) require.NoError(t, err) + require.NoError(t, sqliteDB.AutoMigrate(&model.SystemConfig{})) db.SetDB(sqliteDB) resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore()) resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore()) @@ -123,14 +125,8 @@ func TestRunDatabaseAutoCleanupOnceDeletesAllObservabilityTargets(t *testing.T) RequestCount: 15, })) - previousEnabled := model.DatabaseAutoCleanupEnabled - previousRetentionDays := model.DatabaseAutoCleanupRetentionDays - model.DatabaseAutoCleanupEnabled = true - model.DatabaseAutoCleanupRetentionDays = 1 - t.Cleanup(func() { - model.DatabaseAutoCleanupEnabled = previousEnabled - model.DatabaseAutoCleanupRetentionDays = previousRetentionDays - }) + require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyDatabaseAutoCleanupEnabled, "true")) + require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyDatabaseAutoCleanupRetentionDays, "1")) summary, err := RunDatabaseAutoCleanupOnce(ctx, now) require.NoError(t, err) diff --git a/internal/apps/openflare/uptimekuma/sync.go b/internal/apps/openflare/uptimekuma/sync.go index b428e7e0..4df2fbbe 100644 --- a/internal/apps/openflare/uptimekuma/sync.go +++ b/internal/apps/openflare/uptimekuma/sync.go @@ -12,15 +12,70 @@ import ( "github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" ) const uptimeKumaTagOpenFlare = "OpenFlare" var isSyncing atomic.Bool +// kumaConfig 封装 UptimeKuma 配置 +type kumaConfig struct { + URL string + Username string + Password string + MonitorScope string + SelectedSites string + Interval int + Retry int + RetryInterval int + Timeout int +} + +// loadKumaConfig 从 SystemConfig 加载 UptimeKuma 配置 +func loadKumaConfig(ctx context.Context) (*kumaConfig, error) { + url, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaURL) + username, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaUsername) + password, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaPassword) + scope, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaMonitorScope) + selected, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaSelectedSites) + + interval, _ := repository.GetIntByKey(ctx, model.ConfigKeyUptimeKumaInterval) + if interval <= 0 { + interval = 60 + } + retry, _ := repository.GetIntByKey(ctx, model.ConfigKeyUptimeKumaRetry) + retryInterval, _ := repository.GetIntByKey(ctx, model.ConfigKeyUptimeKumaRetryInterval) + if retryInterval <= 0 { + retryInterval = 60 + } + timeout, _ := repository.GetIntByKey(ctx, model.ConfigKeyUptimeKumaTimeout) + if timeout <= 0 { + timeout = 48 + } + + if scope.Value == "" { + scope.Value = "all" + } + + return &kumaConfig{ + URL: strings.TrimSpace(url.Value), + Username: strings.TrimSpace(username.Value), + Password: strings.TrimSpace(password.Value), + MonitorScope: scope.Value, + SelectedSites: selected.Value, + Interval: interval, + Retry: retry, + RetryInterval: retryInterval, + Timeout: timeout, + }, nil +} + // SyncToUptimeKuma synchronizes enabled proxy routes to Uptime Kuma monitors. func SyncToUptimeKuma(ctx context.Context) error { - if !model.UptimeKumaEnabled { + // 检查是否启用 + enabled, _ := repository.GetBoolByKey(ctx, model.ConfigKeyUptimeKumaEnabled) + if !enabled { return fmt.Errorf("uptime Kuma integration is disabled") } @@ -29,15 +84,21 @@ func SyncToUptimeKuma(ctx context.Context) error { } defer isSyncing.Store(false) - kumaURL, kumaUsername, kumaPassword, err := validateUptimeKumaConfig() + // 加载配置 + config, err := loadKumaConfig(ctx) if err != nil { return err } + // 验证配置 + if err := validateKumaConfig(config); err != nil { + return err + } + slog.Info("Starting Uptime Kuma sync process", - "url", kumaURL, - "username", kumaUsername, - "scope", model.UptimeKumaMonitorScope, + "url", config.URL, + "username", config.Username, + "scope", config.MonitorScope, ) allRoutes, err := model.ListProxyRoutes(ctx) @@ -45,12 +106,12 @@ func SyncToUptimeKuma(ctx context.Context) error { return fmt.Errorf("failed to list local proxy routes: %w", err) } - expectedRoutes, err := filterExpectedRoutes(allRoutes) + expectedRoutes, err := filterExpectedRoutes(allRoutes, config) if err != nil { return err } - client, err := connectAndLoginUptimeKuma(kumaURL, kumaUsername, kumaPassword) + client, err := connectAndLoginUptimeKuma(config.URL, config.Username, config.Password) if err != nil { return err } @@ -62,16 +123,16 @@ func SyncToUptimeKuma(ctx context.Context) error { } existingOpenFlareMonitors := filterOpenFlareMonitors(client.GetMonitorList(), openFlareTagID) - expectedSitesMap := syncRouteMonitors(client, expectedRoutes, existingOpenFlareMonitors, openFlareTagID) + expectedSitesMap := syncRouteMonitors(client, expectedRoutes, existingOpenFlareMonitors, openFlareTagID, config) removeStaleMonitors(client, existingOpenFlareMonitors, expectedSitesMap) return nil } -func filterExpectedRoutes(allRoutes []*model.ProxyRoute) ([]*model.ProxyRoute, error) { - scope := model.UptimeKumaMonitorScope +func filterExpectedRoutes(allRoutes []*model.ProxyRoute, config *kumaConfig) ([]*model.ProxyRoute, error) { + scope := config.MonitorScope if scope == "selected" { - selectedList := strings.Split(model.UptimeKumaSelectedSites, ",") + selectedList := strings.Split(config.SelectedSites, ",") selectedMap := make(map[string]bool) for _, name := range selectedList { trimmedName := strings.TrimSpace(name) @@ -175,15 +236,15 @@ func routeMonitorURL(route *model.ProxyRoute) (string, error) { return "http://" + domain, nil } -func monitorPayload(id int, name, targetURL string) map[string]any { +func monitorPayload(id int, name, targetURL string, config *kumaConfig) map[string]any { payload := map[string]any{ "type": "http", "name": name, "url": targetURL, - "interval": model.UptimeKumaInterval, - "maxretries": model.UptimeKumaRetry, - "retryInterval": model.UptimeKumaRetryInterval, - "timeout": model.UptimeKumaTimeout, + "interval": config.Interval, + "maxretries": config.Retry, + "retryInterval": config.RetryInterval, + "timeout": config.Timeout, "active": true, "resendInterval": 0, "expiryNotification": false, @@ -198,17 +259,17 @@ func monitorPayload(id int, name, targetURL string) map[string]any { return payload } -func monitorNeedsUpdate(existing Monitor, targetURL string) bool { +func monitorNeedsUpdate(existing Monitor, targetURL string, config *kumaConfig) bool { return existing.URL != targetURL || - existing.Interval != model.UptimeKumaInterval || - existing.MaxRetries != model.UptimeKumaRetry || - existing.RetryInterval != model.UptimeKumaRetryInterval || - existing.Timeout != model.UptimeKumaTimeout + existing.Interval != config.Interval || + existing.MaxRetries != config.Retry || + existing.RetryInterval != config.RetryInterval || + existing.Timeout != config.Timeout } -func createMonitor(client *SocketIOClient, siteName, targetURL string, openFlareTagID int) error { +func createMonitor(client *SocketIOClient, siteName, targetURL string, openFlareTagID int, config *kumaConfig) error { slog.Info("Creating monitor in Uptime Kuma", "name", siteName, "url", targetURL) - addAck, err := client.Emit("add", monitorPayload(0, siteName, targetURL)) + addAck, err := client.Emit("add", monitorPayload(0, siteName, targetURL, config)) if err != nil { return err } @@ -238,9 +299,9 @@ func createMonitor(client *SocketIOClient, siteName, targetURL string, openFlare return nil } -func updateMonitor(client *SocketIOClient, monitorID int, siteName, targetURL string) error { +func updateMonitor(client *SocketIOClient, monitorID int, siteName, targetURL string, config *kumaConfig) error { slog.Info("Updating monitor in Uptime Kuma due to settings mismatch", "name", siteName) - editAck, err := client.Emit("editMonitor", monitorPayload(monitorID, siteName, targetURL)) + editAck, err := client.Emit("editMonitor", monitorPayload(monitorID, siteName, targetURL, config)) if err != nil { return err } diff --git a/internal/apps/openflare/uptimekuma/sync_helpers.go b/internal/apps/openflare/uptimekuma/sync_helpers.go index 428bec4b..869b72e3 100644 --- a/internal/apps/openflare/uptimekuma/sync_helpers.go +++ b/internal/apps/openflare/uptimekuma/sync_helpers.go @@ -14,17 +14,18 @@ import ( const monitorListWaitTimeout = 5 * time.Second -func validateUptimeKumaConfig() (string, string, string, error) { - kumaURL := strings.TrimSpace(model.UptimeKumaURL) - kumaUsername := strings.TrimSpace(model.UptimeKumaUsername) - kumaPassword := strings.TrimSpace(model.UptimeKumaPassword) - if kumaURL == "" || kumaUsername == "" || kumaPassword == "" { - return kumaURL, kumaUsername, kumaPassword, fmt.Errorf( - "uptime Kuma URL, username, or password is not configured (URL: %q, Username: %q, PasswordLength: %d)", - kumaURL, kumaUsername, len(kumaPassword), - ) +// validateKumaConfig 验证 kumaConfig 配置完整性 +func validateKumaConfig(config *kumaConfig) error { + if strings.TrimSpace(config.URL) == "" { + return fmt.Errorf("uptime Kuma URL is not configured") } - return kumaURL, kumaUsername, kumaPassword, nil + if strings.TrimSpace(config.Username) == "" { + return fmt.Errorf("uptime Kuma username is not configured") + } + if strings.TrimSpace(config.Password) == "" { + return fmt.Errorf("uptime Kuma password is not configured") + } + return nil } func connectAndLoginUptimeKuma(kumaURL, kumaUsername, kumaPassword string) (*SocketIOClient, error) { @@ -68,7 +69,7 @@ func connectAndLoginUptimeKuma(kumaURL, kumaUsername, kumaPassword string) (*Soc return client, nil } -func syncRouteMonitors(client *SocketIOClient, expectedRoutes []*model.ProxyRoute, existingMonitors map[string]Monitor, openFlareTagID int) map[string]bool { +func syncRouteMonitors(client *SocketIOClient, expectedRoutes []*model.ProxyRoute, existingMonitors map[string]Monitor, openFlareTagID int, config *kumaConfig) map[string]bool { expectedSitesMap := make(map[string]bool, len(expectedRoutes)) for _, route := range expectedRoutes { expectedSitesMap[route.SiteName] = true @@ -80,13 +81,13 @@ func syncRouteMonitors(client *SocketIOClient, expectedRoutes []*model.ProxyRout existing, exists := existingMonitors[route.SiteName] if !exists { - if err := createMonitor(client, route.SiteName, targetURL, openFlareTagID); err != nil { + if err := createMonitor(client, route.SiteName, targetURL, openFlareTagID, config); err != nil { slog.Error("Failed to add monitor to Uptime Kuma", "name", route.SiteName, "error", err) } continue } - if monitorNeedsUpdate(existing, targetURL) { - if err := updateMonitor(client, existing.ID, route.SiteName, targetURL); err != nil { + if monitorNeedsUpdate(existing, targetURL, config) { + if err := updateMonitor(client, existing.ID, route.SiteName, targetURL, config); err != nil { slog.Error("Failed to edit monitor in Uptime Kuma", "name", route.SiteName, "error", err) } } diff --git a/internal/apps/openflare/uptimekuma/sync_test.go b/internal/apps/openflare/uptimekuma/sync_test.go index 6c399957..550b05ce 100644 --- a/internal/apps/openflare/uptimekuma/sync_test.go +++ b/internal/apps/openflare/uptimekuma/sync_test.go @@ -17,6 +17,7 @@ import ( "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/repository" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -121,7 +122,7 @@ func setupSyncTestDB(t *testing.T) func() { DisableForeignKeyConstraintWhenMigrating: true, }) require.NoError(t, err) - require.NoError(t, sqliteDB.AutoMigrate(&model.ProxyRoute{})) + require.NoError(t, sqliteDB.AutoMigrate(&model.ProxyRoute{}, &model.SystemConfig{})) db.SetDB(sqliteDB) return func() { @@ -129,41 +130,50 @@ func setupSyncTestDB(t *testing.T) func() { } } -func backupUptimeKumaConfig() func() { - oldEnabled := model.UptimeKumaEnabled - oldURL := model.UptimeKumaURL - oldUsername := model.UptimeKumaUsername - oldPassword := model.UptimeKumaPassword - oldScope := model.UptimeKumaMonitorScope - oldSelected := model.UptimeKumaSelectedSites - oldInterval := model.UptimeKumaInterval - oldRetry := model.UptimeKumaRetry - oldRetryInterval := model.UptimeKumaRetryInterval - oldTimeout := model.UptimeKumaTimeout +func backupUptimeKumaConfig(ctx context.Context) func() { + // 备份所有 UptimeKuma 相关配置 + configs := []string{ + model.ConfigKeyUptimeKumaEnabled, + model.ConfigKeyUptimeKumaURL, + model.ConfigKeyUptimeKumaUsername, + model.ConfigKeyUptimeKumaPassword, + model.ConfigKeyUptimeKumaMonitorScope, + model.ConfigKeyUptimeKumaSelectedSites, + model.ConfigKeyUptimeKumaInterval, + model.ConfigKeyUptimeKumaRetry, + model.ConfigKeyUptimeKumaRetryInterval, + model.ConfigKeyUptimeKumaTimeout, + } + + oldValues := make(map[string]string) + for _, key := range configs { + config, _ := repository.GetSystemConfigByKey(ctx, key) + oldValues[key] = config.Value + } return func() { - model.UptimeKumaEnabled = oldEnabled - model.UptimeKumaURL = oldURL - model.UptimeKumaUsername = oldUsername - model.UptimeKumaPassword = oldPassword - model.UptimeKumaMonitorScope = oldScope - model.UptimeKumaSelectedSites = oldSelected - model.UptimeKumaInterval = oldInterval - model.UptimeKumaRetry = oldRetry - model.UptimeKumaRetryInterval = oldRetryInterval - model.UptimeKumaTimeout = oldTimeout + // 恢复所有配置 + for key, value := range oldValues { + _ = db.DB(ctx).Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error + } } } +// setTestConfig 设置测试配置的辅助函数(不存在则创建) +func setTestConfig(ctx context.Context, key, value string) { + _ = repository.SaveOrUpdateSystemConfig(ctx, key, value) +} + func TestSyncToUptimeKumaDisabled(t *testing.T) { cleanup := setupSyncTestDB(t) defer cleanup() - restore := backupUptimeKumaConfig() + ctx := context.Background() + restore := backupUptimeKumaConfig(ctx) defer restore() - model.UptimeKumaEnabled = false + setTestConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "false") - err := SyncToUptimeKuma(context.Background()) + err := SyncToUptimeKuma(ctx) require.Error(t, err) assert.Contains(t, err.Error(), "disabled") } @@ -171,9 +181,9 @@ func TestSyncToUptimeKumaDisabled(t *testing.T) { func TestSyncToUptimeKumaSuccess(t *testing.T) { cleanup := setupSyncTestDB(t) defer cleanup() - restore := backupUptimeKumaConfig() - defer restore() ctx := context.Background() + restore := backupUptimeKumaConfig(ctx) + defer restore() require.NoError(t, db.DB(ctx).Where("1 = 1").Delete(&model.ProxyRoute{}).Error) @@ -227,15 +237,16 @@ func TestSyncToUptimeKumaSuccess(t *testing.T) { server := httptest.NewServer(mockSrv) defer server.Close() - model.UptimeKumaEnabled = true - model.UptimeKumaURL = server.URL - model.UptimeKumaUsername = "admin" - model.UptimeKumaPassword = "password" - model.UptimeKumaMonitorScope = "all" - model.UptimeKumaInterval = 60 - model.UptimeKumaRetry = 0 - model.UptimeKumaRetryInterval = 60 - model.UptimeKumaTimeout = 48 + // 设置测试配置 + setTestConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "true") + setTestConfig(ctx, model.ConfigKeyUptimeKumaURL, server.URL) + setTestConfig(ctx, model.ConfigKeyUptimeKumaUsername, "admin") + setTestConfig(ctx, model.ConfigKeyUptimeKumaPassword, "password") + setTestConfig(ctx, model.ConfigKeyUptimeKumaMonitorScope, "all") + setTestConfig(ctx, model.ConfigKeyUptimeKumaInterval, "60") + setTestConfig(ctx, model.ConfigKeyUptimeKumaRetry, "0") + setTestConfig(ctx, model.ConfigKeyUptimeKumaRetryInterval, "60") + setTestConfig(ctx, model.ConfigKeyUptimeKumaTimeout, "48") require.NoError(t, SyncToUptimeKuma(ctx)) @@ -282,9 +293,9 @@ func TestSyncToUptimeKumaSuccess(t *testing.T) { func TestSyncToUptimeKumaSelectedScope(t *testing.T) { cleanup := setupSyncTestDB(t) defer cleanup() - restore := backupUptimeKumaConfig() - defer restore() ctx := context.Background() + restore := backupUptimeKumaConfig(ctx) + defer restore() require.NoError(t, db.DB(ctx).Where("1 = 1").Delete(&model.ProxyRoute{}).Error) @@ -312,12 +323,13 @@ func TestSyncToUptimeKumaSelectedScope(t *testing.T) { server := httptest.NewServer(mockSrv) defer server.Close() - model.UptimeKumaEnabled = true - model.UptimeKumaURL = server.URL - model.UptimeKumaUsername = "admin" - model.UptimeKumaPassword = "password" - model.UptimeKumaMonitorScope = "selected" - model.UptimeKumaSelectedSites = "site-a" + // 设置测试配置 + setTestConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "true") + setTestConfig(ctx, model.ConfigKeyUptimeKumaURL, server.URL) + setTestConfig(ctx, model.ConfigKeyUptimeKumaUsername, "admin") + setTestConfig(ctx, model.ConfigKeyUptimeKumaPassword, "password") + setTestConfig(ctx, model.ConfigKeyUptimeKumaMonitorScope, "selected") + setTestConfig(ctx, model.ConfigKeyUptimeKumaSelectedSites, "site-a") require.NoError(t, SyncToUptimeKuma(ctx)) diff --git a/internal/db/migrator/bridge_test.go b/internal/db/migrator/bridge_test.go index d7cb6e29..75a69b7c 100644 --- a/internal/db/migrator/bridge_test.go +++ b/internal/db/migrator/bridge_test.go @@ -99,8 +99,8 @@ func TestMigrateUpgradesLegacyDatabaseAndPreservesData(t *testing.T) { // Legacy normal user `INSERT INTO users (id, username, password, display_name, role, status, email) VALUES (2, 'jack', 'hashed_pass_456', 'Normal User jack', 10, 1, 'jack@example.com');`, - // Legacy config options - `INSERT INTO options (key, value) VALUES ('SystemName', 'CustomOpenFlare');`, + // Legacy config options(GeoIPProvider 会被迁移到 w_system_configs.geoip_provider) + `INSERT INTO options (key, value) VALUES ('GeoIPProvider', 'ip-api');`, // Legacy origin `INSERT INTO origins (id, name, address, remark, created_at, updated_at) VALUES (10, 'my_origin', '127.0.0.1:8080', 'original origin', '2026-06-01 12:00:00', '2026-06-01 12:00:00');`, @@ -181,13 +181,13 @@ func TestMigrateUpgradesLegacyDatabaseAndPreservesData(t *testing.T) { t.Errorf("jack user migrated incorrectly: %+v", jackUser) } - // Verify options migrated to of_options - var systemNameVal string - if err := sqliteDB.Table("of_options").Where("key = ?", "SystemName").Select("value").Scan(&systemNameVal).Error; err != nil { - t.Fatalf("query SystemName failed: %v", err) + // Verify legacy option migrated all the way to w_system_configs(of_options 已被删除) + var geoIPProviderVal string + if err := sqliteDB.Table("w_system_configs").Where("key = ?", "geoip_provider").Select("value").Scan(&geoIPProviderVal).Error; err != nil { + t.Fatalf("query geoip_provider failed: %v", err) } - if systemNameVal != "CustomOpenFlare" { - t.Errorf("SystemName value incorrect: got %s, want CustomOpenFlare", systemNameVal) + if geoIPProviderVal != "ip-api" { + t.Errorf("geoip_provider value incorrect: got %s, want ip-api", geoIPProviderVal) } // Verify origin migrated to of_origins diff --git a/internal/db/migrator/goose/postgres/202606220004_migrate_of_options_to_system_configs.sql b/internal/db/migrator/goose/postgres/202606220004_migrate_of_options_to_system_configs.sql new file mode 100644 index 00000000..9d660bfb --- /dev/null +++ b/internal/db/migrator/goose/postgres/202606220004_migrate_of_options_to_system_configs.sql @@ -0,0 +1,347 @@ +-- +goose Up +-- 将 of_options 配置迁移到 w_system_configs(仅迁移新配置,已存在的不重复) + +-- Agent 相关配置 (business, visibility=0) +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'agent_discovery_token', COALESCE(value, ''), 'business', 0, 'Agent 发现令牌' +FROM of_options WHERE key = 'AgentDiscoveryToken' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'agent_heartbeat_interval', value, 'business', 0, 'Agent 心跳间隔(毫秒)' +FROM of_options WHERE key = 'AgentHeartbeatInterval' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'agent_websocket_upgrade_enabled', value, 'business', 0, 'Agent WebSocket 升级开关' +FROM of_options WHERE key = 'AgentWebsocketUpgradeEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'node_offline_threshold', value, 'business', 0, '节点离线阈值(毫秒)' +FROM of_options WHERE key = 'NodeOfflineThreshold' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'agent_update_repo', value, 'business', 0, 'Agent 更新仓库' +FROM of_options WHERE key = 'AgentUpdateRepo' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +-- 系统功能配置 (business, visibility=0) +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'geoip_provider', value, 'business', 0, 'GeoIP 服务商' +FROM of_options WHERE key = 'GeoIPProvider' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'database_auto_cleanup_enabled', value, 'business', 0, '数据库自动清理开关' +FROM of_options WHERE key = 'DatabaseAutoCleanupEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'database_auto_cleanup_retention_days', value, 'business', 0, '数据库保留天数' +FROM of_options WHERE key = 'DatabaseAutoCleanupRetentionDays' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +-- UptimeKuma 集成配置 (business, visibility=0) +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_enabled', value, 'business', 0, 'UptimeKuma 集成开关' +FROM of_options WHERE key = 'UptimeKumaEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_url', COALESCE(value, ''), 'business', 0, 'UptimeKuma URL' +FROM of_options WHERE key = 'UptimeKumaUrl' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_username', COALESCE(value, ''), 'business', 0, 'UptimeKuma 用户名' +FROM of_options WHERE key = 'UptimeKumaUsername' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_password', COALESCE(value, ''), 'business', 0, 'UptimeKuma 密码' +FROM of_options WHERE key = 'UptimeKumaPassword' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_monitor_scope', value, 'business', 0, 'UptimeKuma 监控范围' +FROM of_options WHERE key = 'UptimeKumaMonitorScope' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_selected_sites', COALESCE(value, ''), 'business', 0, 'UptimeKuma 选定站点' +FROM of_options WHERE key = 'UptimeKumaSelectedSites' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_sync_interval', value, 'business', 0, 'UptimeKuma 同步间隔(分钟)' +FROM of_options WHERE key = 'UptimeKumaSyncInterval' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_interval', value, 'business', 0, 'UptimeKuma 监控间隔(秒)' +FROM of_options WHERE key = 'UptimeKumaInterval' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_retry', value, 'business', 0, 'UptimeKuma 重试次数' +FROM of_options WHERE key = 'UptimeKumaRetry' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_retry_interval', value, 'business', 0, 'UptimeKuma 重试间隔(秒)' +FROM of_options WHERE key = 'UptimeKumaRetryInterval' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_timeout', value, 'business', 0, 'UptimeKuma 超时(秒)' +FROM of_options WHERE key = 'UptimeKumaTimeout' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +-- OpenResty 配置(全部 business 类型,visibility=0) +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_default_server_return_status', value, 'business', 0, '默认服务器返回状态码' +FROM of_options WHERE key = 'OpenRestyDefaultServerReturnStatus' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_worker_processes', value, 'business', 0, 'Worker 进程数' +FROM of_options WHERE key = 'OpenRestyWorkerProcesses' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_worker_connections', value, 'business', 0, 'Worker 连接数' +FROM of_options WHERE key = 'OpenRestyWorkerConnections' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_worker_rlimit_nofile', value, 'business', 0, 'Worker 文件描述符限制' +FROM of_options WHERE key = 'OpenRestyWorkerRlimitNofile' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_events_use', value, 'business', 0, '事件模型' +FROM of_options WHERE key = 'OpenRestyEventsUse' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_events_multi_accept_enabled', value, 'business', 0, '多路接受开关' +FROM of_options WHERE key = 'OpenRestyEventsMultiAcceptEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_keepalive_timeout', value, 'business', 0, 'Keepalive 超时(秒)' +FROM of_options WHERE key = 'OpenRestyKeepaliveTimeout' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_keepalive_requests', value, 'business', 0, 'Keepalive 请求数' +FROM of_options WHERE key = 'OpenRestyKeepaliveRequests' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_client_header_timeout', value, 'business', 0, '客户端头超时(秒)' +FROM of_options WHERE key = 'OpenRestyClientHeaderTimeout' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_client_body_timeout', value, 'business', 0, '客户端体超时(秒)' +FROM of_options WHERE key = 'OpenRestyClientBodyTimeout' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_client_max_body_size', value, 'business', 0, '客户端最大体大小' +FROM of_options WHERE key = 'OpenRestyClientMaxBodySize' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_large_client_header_buffers', value, 'business', 0, '大客户端头缓冲区' +FROM of_options WHERE key = 'OpenRestyLargeClientHeaderBuffers' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_send_timeout', value, 'business', 0, '发送超时(秒)' +FROM of_options WHERE key = 'OpenRestySendTimeout' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_resolvers', COALESCE(value, ''), 'business', 0, 'DNS 解析器' +FROM of_options WHERE key = 'OpenRestyResolvers' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_connect_timeout', value, 'business', 0, '代理连接超时(秒)' +FROM of_options WHERE key = 'OpenRestyProxyConnectTimeout' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_send_timeout', value, 'business', 0, '代理发送超时(秒)' +FROM of_options WHERE key = 'OpenRestyProxySendTimeout' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_read_timeout', value, 'business', 0, '代理读取超时(秒)' +FROM of_options WHERE key = 'OpenRestyProxyReadTimeout' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_websocket_enabled', value, 'business', 0, 'WebSocket 支持开关' +FROM of_options WHERE key = 'OpenRestyWebsocketEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_http3_enabled', value, 'business', 0, 'HTTP/3 支持开关' +FROM of_options WHERE key = 'OpenRestyHTTP3Enabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_request_buffering_enabled', value, 'business', 0, '代理请求缓冲开关' +FROM of_options WHERE key = 'OpenRestyProxyRequestBufferingEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_buffering_enabled', value, 'business', 0, '代理响应缓冲开关' +FROM of_options WHERE key = 'OpenRestyProxyBufferingEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_buffers', value, 'business', 0, '代理缓冲区' +FROM of_options WHERE key = 'OpenRestyProxyBuffers' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_buffer_size', value, 'business', 0, '代理缓冲区大小' +FROM of_options WHERE key = 'OpenRestyProxyBufferSize' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_busy_buffers_size', value, 'business', 0, '代理繁忙缓冲区大小' +FROM of_options WHERE key = 'OpenRestyProxyBusyBuffersSize' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_gzip_enabled', value, 'business', 0, 'Gzip 压缩开关' +FROM of_options WHERE key = 'OpenRestyGzipEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_gzip_min_length', value, 'business', 0, 'Gzip 最小长度' +FROM of_options WHERE key = 'OpenRestyGzipMinLength' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_gzip_comp_level', value, 'business', 0, 'Gzip 压缩级别' +FROM of_options WHERE key = 'OpenRestyGzipCompLevel' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_enabled', value, 'business', 0, '缓存开关' +FROM of_options WHERE key = 'OpenRestyCacheEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_path', COALESCE(value, ''), 'business', 0, '缓存路径' +FROM of_options WHERE key = 'OpenRestyCachePath' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_levels', value, 'business', 0, '缓存层级' +FROM of_options WHERE key = 'OpenRestyCacheLevels' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_inactive', value, 'business', 0, '缓存不活跃时间' +FROM of_options WHERE key = 'OpenRestyCacheInactive' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_max_size', value, 'business', 0, '缓存最大大小' +FROM of_options WHERE key = 'OpenRestyCacheMaxSize' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_key_template', value, 'business', 0, '缓存键模板' +FROM of_options WHERE key = 'OpenRestyCacheKeyTemplate' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_lock_enabled', value, 'business', 0, '缓存锁开关' +FROM of_options WHERE key = 'OpenRestyCacheLockEnabled' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_lock_timeout', value, 'business', 0, '缓存锁超时' +FROM of_options WHERE key = 'OpenRestyCacheLockTimeout' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_use_stale', value, 'business', 0, '缓存失效策略' +FROM of_options WHERE key = 'OpenRestyCacheUseStale' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +INSERT INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_main_config_template', value, 'business', 0, '主配置模板' +FROM of_options WHERE key = 'OpenRestyMainConfigTemplate' +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +-- +goose Down +-- 回滚时删除所有迁移的配置项 +DELETE FROM w_system_configs WHERE key IN ( + 'agent_discovery_token', + 'agent_heartbeat_interval', + 'agent_websocket_upgrade_enabled', + 'node_offline_threshold', + 'agent_update_repo', + 'geoip_provider', + 'database_auto_cleanup_enabled', + 'database_auto_cleanup_retention_days', + 'uptime_kuma_enabled', + 'uptime_kuma_url', + 'uptime_kuma_username', + 'uptime_kuma_password', + 'uptime_kuma_monitor_scope', + 'uptime_kuma_selected_sites', + 'uptime_kuma_sync_interval', + 'uptime_kuma_interval', + 'uptime_kuma_retry', + 'uptime_kuma_retry_interval', + 'uptime_kuma_timeout', + 'openresty_default_server_return_status', + 'openresty_worker_processes', + 'openresty_worker_connections', + 'openresty_worker_rlimit_nofile', + 'openresty_events_use', + 'openresty_events_multi_accept_enabled', + 'openresty_keepalive_timeout', + 'openresty_keepalive_requests', + 'openresty_client_header_timeout', + 'openresty_client_body_timeout', + 'openresty_client_max_body_size', + 'openresty_large_client_header_buffers', + 'openresty_send_timeout', + 'openresty_resolvers', + 'openresty_proxy_connect_timeout', + 'openresty_proxy_send_timeout', + 'openresty_proxy_read_timeout', + 'openresty_websocket_enabled', + 'openresty_http3_enabled', + 'openresty_proxy_request_buffering_enabled', + 'openresty_proxy_buffering_enabled', + 'openresty_proxy_buffers', + 'openresty_proxy_buffer_size', + 'openresty_proxy_busy_buffers_size', + 'openresty_gzip_enabled', + 'openresty_gzip_min_length', + 'openresty_gzip_comp_level', + 'openresty_cache_enabled', + 'openresty_cache_path', + 'openresty_cache_levels', + 'openresty_cache_inactive', + 'openresty_cache_max_size', + 'openresty_cache_key_template', + 'openresty_cache_lock_enabled', + 'openresty_cache_lock_timeout', + 'openresty_cache_use_stale', + 'openresty_main_config_template' +); diff --git a/internal/db/migrator/goose/postgres/202606220005_drop_of_options.sql b/internal/db/migrator/goose/postgres/202606220005_drop_of_options.sql new file mode 100644 index 00000000..3d72ee6a --- /dev/null +++ b/internal/db/migrator/goose/postgres/202606220005_drop_of_options.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- of_options 配置已于 202606220004 迁移至 w_system_configs,删除遗留表。 +DROP TABLE IF EXISTS of_options; + +-- +goose Down +-- 回滚时重建空表结构(数据无法恢复,迁移来源已为 w_system_configs)。 +CREATE TABLE IF NOT EXISTS of_options ( + key VARCHAR(128) PRIMARY KEY, + value TEXT NOT NULL DEFAULT '' +); diff --git a/internal/db/migrator/goose/sqlite/202606220004_migrate_of_options_to_system_configs.sql b/internal/db/migrator/goose/sqlite/202606220004_migrate_of_options_to_system_configs.sql new file mode 100644 index 00000000..655568c4 --- /dev/null +++ b/internal/db/migrator/goose/sqlite/202606220004_migrate_of_options_to_system_configs.sql @@ -0,0 +1,291 @@ +-- +goose Up +-- 将 of_options 配置迁移到 w_system_configs(仅迁移新配置,已存在的不重复) + +-- Agent 相关配置 (business, visibility=0) +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'agent_discovery_token', COALESCE(value, ''), 'business', 0, 'Agent 发现令牌' +FROM of_options WHERE key = 'AgentDiscoveryToken'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'agent_heartbeat_interval', value, 'business', 0, 'Agent 心跳间隔(毫秒)' +FROM of_options WHERE key = 'AgentHeartbeatInterval'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'agent_websocket_upgrade_enabled', value, 'business', 0, 'Agent WebSocket 升级开关' +FROM of_options WHERE key = 'AgentWebsocketUpgradeEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'node_offline_threshold', value, 'business', 0, '节点离线阈值(毫秒)' +FROM of_options WHERE key = 'NodeOfflineThreshold'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'agent_update_repo', value, 'business', 0, 'Agent 更新仓库' +FROM of_options WHERE key = 'AgentUpdateRepo'; + +-- 系统功能配置 (business, visibility=0) +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'geoip_provider', value, 'business', 0, 'GeoIP 服务商' +FROM of_options WHERE key = 'GeoIPProvider'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'database_auto_cleanup_enabled', value, 'business', 0, '数据库自动清理开关' +FROM of_options WHERE key = 'DatabaseAutoCleanupEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'database_auto_cleanup_retention_days', value, 'business', 0, '数据库保留天数' +FROM of_options WHERE key = 'DatabaseAutoCleanupRetentionDays'; + +-- UptimeKuma 集成配置 (business, visibility=0) +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_enabled', value, 'business', 0, 'UptimeKuma 集成开关' +FROM of_options WHERE key = 'UptimeKumaEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_url', COALESCE(value, ''), 'business', 0, 'UptimeKuma URL' +FROM of_options WHERE key = 'UptimeKumaUrl'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_username', COALESCE(value, ''), 'business', 0, 'UptimeKuma 用户名' +FROM of_options WHERE key = 'UptimeKumaUsername'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_password', COALESCE(value, ''), 'business', 0, 'UptimeKuma 密码' +FROM of_options WHERE key = 'UptimeKumaPassword'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_monitor_scope', value, 'business', 0, 'UptimeKuma 监控范围' +FROM of_options WHERE key = 'UptimeKumaMonitorScope'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_selected_sites', COALESCE(value, ''), 'business', 0, 'UptimeKuma 选定站点' +FROM of_options WHERE key = 'UptimeKumaSelectedSites'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_sync_interval', value, 'business', 0, 'UptimeKuma 同步间隔(分钟)' +FROM of_options WHERE key = 'UptimeKumaSyncInterval'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_interval', value, 'business', 0, 'UptimeKuma 监控间隔(秒)' +FROM of_options WHERE key = 'UptimeKumaInterval'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_retry', value, 'business', 0, 'UptimeKuma 重试次数' +FROM of_options WHERE key = 'UptimeKumaRetry'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_retry_interval', value, 'business', 0, 'UptimeKuma 重试间隔(秒)' +FROM of_options WHERE key = 'UptimeKumaRetryInterval'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'uptime_kuma_timeout', value, 'business', 0, 'UptimeKuma 超时(秒)' +FROM of_options WHERE key = 'UptimeKumaTimeout'; + +-- OpenResty 配置(全部 business 类型,visibility=0) +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_default_server_return_status', value, 'business', 0, '默认服务器返回状态码' +FROM of_options WHERE key = 'OpenRestyDefaultServerReturnStatus'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_worker_processes', value, 'business', 0, 'Worker 进程数' +FROM of_options WHERE key = 'OpenRestyWorkerProcesses'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_worker_connections', value, 'business', 0, 'Worker 连接数' +FROM of_options WHERE key = 'OpenRestyWorkerConnections'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_worker_rlimit_nofile', value, 'business', 0, 'Worker 文件描述符限制' +FROM of_options WHERE key = 'OpenRestyWorkerRlimitNofile'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_events_use', value, 'business', 0, '事件模型' +FROM of_options WHERE key = 'OpenRestyEventsUse'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_events_multi_accept_enabled', value, 'business', 0, '多路接受开关' +FROM of_options WHERE key = 'OpenRestyEventsMultiAcceptEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_keepalive_timeout', value, 'business', 0, 'Keepalive 超时(秒)' +FROM of_options WHERE key = 'OpenRestyKeepaliveTimeout'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_keepalive_requests', value, 'business', 0, 'Keepalive 请求数' +FROM of_options WHERE key = 'OpenRestyKeepaliveRequests'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_client_header_timeout', value, 'business', 0, '客户端头超时(秒)' +FROM of_options WHERE key = 'OpenRestyClientHeaderTimeout'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_client_body_timeout', value, 'business', 0, '客户端体超时(秒)' +FROM of_options WHERE key = 'OpenRestyClientBodyTimeout'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_client_max_body_size', value, 'business', 0, '客户端最大体大小' +FROM of_options WHERE key = 'OpenRestyClientMaxBodySize'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_large_client_header_buffers', value, 'business', 0, '大客户端头缓冲区' +FROM of_options WHERE key = 'OpenRestyLargeClientHeaderBuffers'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_send_timeout', value, 'business', 0, '发送超时(秒)' +FROM of_options WHERE key = 'OpenRestySendTimeout'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_resolvers', COALESCE(value, ''), 'business', 0, 'DNS 解析器' +FROM of_options WHERE key = 'OpenRestyResolvers'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_connect_timeout', value, 'business', 0, '代理连接超时(秒)' +FROM of_options WHERE key = 'OpenRestyProxyConnectTimeout'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_send_timeout', value, 'business', 0, '代理发送超时(秒)' +FROM of_options WHERE key = 'OpenRestyProxySendTimeout'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_read_timeout', value, 'business', 0, '代理读取超时(秒)' +FROM of_options WHERE key = 'OpenRestyProxyReadTimeout'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_websocket_enabled', value, 'business', 0, 'WebSocket 支持开关' +FROM of_options WHERE key = 'OpenRestyWebsocketEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_http3_enabled', value, 'business', 0, 'HTTP/3 支持开关' +FROM of_options WHERE key = 'OpenRestyHTTP3Enabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_request_buffering_enabled', value, 'business', 0, '代理请求缓冲开关' +FROM of_options WHERE key = 'OpenRestyProxyRequestBufferingEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_buffering_enabled', value, 'business', 0, '代理响应缓冲开关' +FROM of_options WHERE key = 'OpenRestyProxyBufferingEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_buffers', value, 'business', 0, '代理缓冲区' +FROM of_options WHERE key = 'OpenRestyProxyBuffers'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_buffer_size', value, 'business', 0, '代理缓冲区大小' +FROM of_options WHERE key = 'OpenRestyProxyBufferSize'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_proxy_busy_buffers_size', value, 'business', 0, '代理繁忙缓冲区大小' +FROM of_options WHERE key = 'OpenRestyProxyBusyBuffersSize'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_gzip_enabled', value, 'business', 0, 'Gzip 压缩开关' +FROM of_options WHERE key = 'OpenRestyGzipEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_gzip_min_length', value, 'business', 0, 'Gzip 最小长度' +FROM of_options WHERE key = 'OpenRestyGzipMinLength'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_gzip_comp_level', value, 'business', 0, 'Gzip 压缩级别' +FROM of_options WHERE key = 'OpenRestyGzipCompLevel'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_enabled', value, 'business', 0, '缓存开关' +FROM of_options WHERE key = 'OpenRestyCacheEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_path', COALESCE(value, ''), 'business', 0, '缓存路径' +FROM of_options WHERE key = 'OpenRestyCachePath'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_levels', value, 'business', 0, '缓存层级' +FROM of_options WHERE key = 'OpenRestyCacheLevels'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_inactive', value, 'business', 0, '缓存不活跃时间' +FROM of_options WHERE key = 'OpenRestyCacheInactive'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_max_size', value, 'business', 0, '缓存最大大小' +FROM of_options WHERE key = 'OpenRestyCacheMaxSize'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_key_template', value, 'business', 0, '缓存键模板' +FROM of_options WHERE key = 'OpenRestyCacheKeyTemplate'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_lock_enabled', value, 'business', 0, '缓存锁开关' +FROM of_options WHERE key = 'OpenRestyCacheLockEnabled'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_lock_timeout', value, 'business', 0, '缓存锁超时' +FROM of_options WHERE key = 'OpenRestyCacheLockTimeout'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_cache_use_stale', value, 'business', 0, '缓存失效策略' +FROM of_options WHERE key = 'OpenRestyCacheUseStale'; + +INSERT OR REPLACE INTO w_system_configs (key, value, type, visibility, description) +SELECT 'openresty_main_config_template', value, 'business', 0, '主配置模板' +FROM of_options WHERE key = 'OpenRestyMainConfigTemplate'; + +-- +goose Down +-- 回滚时删除所有迁移的配置项 +DELETE FROM w_system_configs WHERE key IN ( + 'agent_discovery_token', + 'agent_heartbeat_interval', + 'agent_websocket_upgrade_enabled', + 'node_offline_threshold', + 'agent_update_repo', + 'geoip_provider', + 'database_auto_cleanup_enabled', + 'database_auto_cleanup_retention_days', + 'uptime_kuma_enabled', + 'uptime_kuma_url', + 'uptime_kuma_username', + 'uptime_kuma_password', + 'uptime_kuma_monitor_scope', + 'uptime_kuma_selected_sites', + 'uptime_kuma_sync_interval', + 'uptime_kuma_interval', + 'uptime_kuma_retry', + 'uptime_kuma_retry_interval', + 'uptime_kuma_timeout', + 'openresty_default_server_return_status', + 'openresty_worker_processes', + 'openresty_worker_connections', + 'openresty_worker_rlimit_nofile', + 'openresty_events_use', + 'openresty_events_multi_accept_enabled', + 'openresty_keepalive_timeout', + 'openresty_keepalive_requests', + 'openresty_client_header_timeout', + 'openresty_client_body_timeout', + 'openresty_client_max_body_size', + 'openresty_large_client_header_buffers', + 'openresty_send_timeout', + 'openresty_resolvers', + 'openresty_proxy_connect_timeout', + 'openresty_proxy_send_timeout', + 'openresty_proxy_read_timeout', + 'openresty_websocket_enabled', + 'openresty_http3_enabled', + 'openresty_proxy_request_buffering_enabled', + 'openresty_proxy_buffering_enabled', + 'openresty_proxy_buffers', + 'openresty_proxy_buffer_size', + 'openresty_proxy_busy_buffers_size', + 'openresty_gzip_enabled', + 'openresty_gzip_min_length', + 'openresty_gzip_comp_level', + 'openresty_cache_enabled', + 'openresty_cache_path', + 'openresty_cache_levels', + 'openresty_cache_inactive', + 'openresty_cache_max_size', + 'openresty_cache_key_template', + 'openresty_cache_lock_enabled', + 'openresty_cache_lock_timeout', + 'openresty_cache_use_stale', + 'openresty_main_config_template' +); diff --git a/internal/db/migrator/goose/sqlite/202606220005_drop_of_options.sql b/internal/db/migrator/goose/sqlite/202606220005_drop_of_options.sql new file mode 100644 index 00000000..ded21037 --- /dev/null +++ b/internal/db/migrator/goose/sqlite/202606220005_drop_of_options.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- of_options 配置已于 202606220004 迁移至 w_system_configs,删除遗留表。 +DROP TABLE IF EXISTS of_options; + +-- +goose Down +-- 回滚时重建空表结构(数据无法恢复,迁移来源已为 w_system_configs)。 +CREATE TABLE IF NOT EXISTS of_options ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL DEFAULT '' +); diff --git a/internal/db/migrator/migrator.go b/internal/db/migrator/migrator.go index 76315080..b86b9bd7 100644 --- a/internal/db/migrator/migrator.go +++ b/internal/db/migrator/migrator.go @@ -7,7 +7,9 @@ package migrator import ( "context" + "database/sql" "embed" + "fmt" "log" "github.com/Rain-kl/Wavelet/internal/config" @@ -79,6 +81,9 @@ func Migrate() { if err := goose.SetDialect(gooseDialect()); err != nil { log.Fatalf("[%s] set goose dialect failed: %v\n", dbType(), err) } + if err := resyncGooseVersionSequence(sqlDB); err != nil { + log.Fatalf("[%s] resync goose_db_version sequence failed: %v\n", dbType(), err) + } if err := goose.Up(sqlDB, migrationDir()); err != nil { log.Fatalf("[%s] goose migrate failed: %v\n", dbType(), err) } @@ -88,6 +93,37 @@ func Migrate() { log.Printf("[%s] goose migrate success\n", dbType()) } +// resyncGooseVersionSequence 修复 PostgreSQL 下 goose_db_version.id 自增序列落后于 +// MAX(id) 的问题(常见于从 dump 恢复或历史迁移以显式 id 复制数据后)。序列落后会 +// 导致 goose 记录新版本号时 INSERT 命中 goose_db_version_pkey 唯一约束冲突。 +// 仅在表已存在且为 PostgreSQL 方言时执行;SQLite 使用 AUTOINCREMENT 不受影响。 +func resyncGooseVersionSequence(sqlDB *sql.DB) error { + if gooseDialect() != dialectPostgres { + return nil + } + + ctx := context.Background() + var exists bool + if err := sqlDB.QueryRowContext(ctx, + "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='goose_db_version')", + ).Scan(&exists); err != nil { + return fmt.Errorf("check goose_db_version existence failed: %w", err) + } + if !exists { + return nil + } + + const resyncSQL = `SELECT setval( + pg_get_serial_sequence('goose_db_version', 'id'), + GREATEST(COALESCE((SELECT MAX(id) FROM goose_db_version), 1), 1), + (SELECT MAX(id) IS NOT NULL FROM goose_db_version) + )` + if _, err := sqlDB.ExecContext(ctx, resyncSQL); err != nil { + return fmt.Errorf("setval goose_db_version sequence failed: %w", err) + } + return nil +} + func clearSystemConfigCache() { if err := repository.InvalidateAllSystemConfigCaches(context.Background()); err != nil { log.Printf("[%s] clear system config cache failed: %v\n", dbType(), err) diff --git a/internal/db/migrator/migrator_test.go b/internal/db/migrator/migrator_test.go index 734b598a..7badfd4b 100644 --- a/internal/db/migrator/migrator_test.go +++ b/internal/db/migrator/migrator_test.go @@ -18,7 +18,9 @@ import ( "gorm.io/gorm" ) -const expectedMigratedSystemConfigCount = 32 +// expectedMigratedSystemConfigCount 包含初始 32 项系统配置,以及 202606220004 +// 从 of_options 迁移过来的 48 项业务配置(OpenFlare/UptimeKuma/OpenResty)。 +const expectedMigratedSystemConfigCount = 80 func TestMigrateInitializesSQLiteDatabase(t *testing.T) { sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ diff --git a/internal/model/auth_source.go b/internal/model/auth_source.go index 579a63cd..182667fa 100644 --- a/internal/model/auth_source.go +++ b/internal/model/auth_source.go @@ -21,12 +21,10 @@ const ( // Shared GORM column name constants used across model package. const ( - colName = "name" - colEnabled = "enabled" - colRemark = "remark" - tableOfProxyRoutes = "of_proxy_routes" - optionKeyUptimeKumaPassword = "UptimeKumaPassword" - optionValueTrue = "true" + colName = "name" + colEnabled = "enabled" + colRemark = "remark" + tableOfProxyRoutes = "of_proxy_routes" ) var authSourceNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$`) diff --git a/internal/model/openflare_option.go b/internal/model/openflare_option.go index 3b590ef0..57a8449e 100644 --- a/internal/model/openflare_option.go +++ b/internal/model/openflare_option.go @@ -3,105 +3,22 @@ package model -import ( - "context" - "strconv" - "strings" - "sync" - "time" +import "time" - "github.com/Rain-kl/Wavelet/internal/db" - "gorm.io/gorm" -) - -// OpenFlareOption stores a hot-reloadable OpenFlare system option. +// OpenFlareOption is the key/value DTO used by the OpenFlare option API surface. +// Key 与 w_system_configs.key 一致(snake_case)。此类型仅作为 +// /api/v1/d/option 接口的请求/响应载体,不再映射到独立的 of_options 表。 type OpenFlareOption struct { - Key string `json:"key" gorm:"column:key;primaryKey;size:128;not null"` - Value string `json:"value" gorm:"type:text;not null"` + Key string `json:"key"` + Value string `json:"value"` } -// TableName returns the OpenFlare options table name. -func (OpenFlareOption) TableName() string { - return "of_options" -} +// StartTime records process start time (seconds) for the public /status endpoint. +var StartTime = time.Now().Unix() -// OptionMap holds the in-memory option snapshot for fast reads and hot reload. -var ( - OptionMap map[string]string - OptionMapRWMutex sync.RWMutex - - // StartTime records process start time (seconds) for legacy /api/status. - StartTime = time.Now().Unix() - - // Hot-reload mirrors for frequently read options (legacy OpenFlare keys). - SystemName = "OpenFlare" - ServerAddress = "" - Footer = "" - HomePageLink = "" - PasswordLoginEnabled = true - CapLoginEnabled = true - PasswordRegisterEnabled = false - EmailVerificationEnabled = false - SMTPPort = 587 - AgentDiscoveryToken = "" - AgentHeartbeatInterval = 10000 - AgentWebsocketUpgradeEnabled = true - NodeOfflineThreshold = 2 * time.Minute - AgentUpdateRepo = "Rain-kl/OpenFlare" - GeoIPProvider = "ipinfo" - DatabaseAutoCleanupEnabled = false - DatabaseAutoCleanupRetentionDays = 30 - UptimeKumaEnabled = false - UptimeKumaURL = "" - UptimeKumaUsername = "" - UptimeKumaPassword = "" - UptimeKumaMonitorScope = "all" - UptimeKumaSelectedSites = "" - UptimeKumaSyncInterval = 5 - UptimeKumaInterval = 60 - UptimeKumaRetry = 0 - UptimeKumaRetryInterval = 60 - UptimeKumaTimeout = 48 - OpenRestyDefaultServerReturnStatus = 421 - OpenRestyWorkerProcesses = "auto" - OpenRestyWorkerConnections = 4096 - OpenRestyWorkerRlimitNofile = 65535 - OpenRestyEventsUse = "epoll" - OpenRestyEventsMultiAcceptEnabled = true - OpenRestyKeepaliveTimeout = 20 - OpenRestyKeepaliveRequests = 1000 - OpenRestyClientHeaderTimeout = 15 - OpenRestyClientBodyTimeout = 15 - OpenRestyClientMaxBodySize = "64m" - OpenRestyLargeClientHeaderBuffers = "4 16k" - OpenRestySendTimeout = 30 - OpenRestyResolvers = "" - OpenRestyProxyConnectTimeout = 3 - OpenRestyProxySendTimeout = 60 - OpenRestyProxyReadTimeout = 60 - OpenRestyWebsocketEnabled = true - OpenRestyHTTP3Enabled = true - OpenRestyProxyRequestBufferingEnabled = false - OpenRestyProxyBufferingEnabled = true - OpenRestyProxyBuffers = "16 16k" - OpenRestyProxyBufferSize = "8k" - OpenRestyProxyBusyBuffersSize = "64k" - OpenRestyGzipEnabled = true - OpenRestyGzipMinLength = 1024 - OpenRestyGzipCompLevel = 5 - OpenRestyCacheEnabled = false - OpenRestyCachePath = "" - OpenRestyCacheLevels = "1:2" - OpenRestyCacheInactive = "30m" - OpenRestyCacheMaxSize = "1g" - OpenRestyCacheKeyTemplate = "$scheme$host$request_uri" - OpenRestyCacheLockEnabled = true - OpenRestyCacheLockTimeout = "5s" - OpenRestyCacheUseStale = "error timeout updating http_500 http_502 http_503 http_504" - OpenRestyMainConfigTemplate = defaultOpenRestyMainConfigTemplate -) - -const defaultOpenRestyMainConfigTemplate = `# This file is generated by OpenFlare. Do not edit manually. +// DefaultOpenRestyMainConfigTemplate 是 OpenResty 主配置模板的内置默认值。 +// 当 SystemConfig 中缺失 openresty_main_config_template 时作为兜底使用。 +const DefaultOpenRestyMainConfigTemplate = `# This file is generated by OpenFlare. Do not edit manually. user openflare; worker_processes {{OpenRestyWorkerProcesses}}; worker_rlimit_nofile {{OpenRestyWorkerRlimitNofile}}; @@ -147,171 +64,3 @@ http { {{OpenRestyResolverDirective}}{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}}; } ` - -// DefaultOpenFlareOptions returns built-in defaults keyed by legacy OpenFlare option names. -func DefaultOpenFlareOptions() map[string]string { - return map[string]string{ - "PasswordLoginEnabled": strconv.FormatBool(PasswordLoginEnabled), - "CapLoginEnabled": strconv.FormatBool(CapLoginEnabled), - "PasswordRegisterEnabled": strconv.FormatBool(PasswordRegisterEnabled), - "EmailVerificationEnabled": strconv.FormatBool(EmailVerificationEnabled), - "SMTPServer": "", - "SMTPPort": strconv.Itoa(SMTPPort), - "SMTPAccount": "", - "SMTPToken": "", - "About": "", - "Footer": Footer, - "HomePageLink": HomePageLink, - "SystemName": SystemName, - "ServerAddress": "", - "AgentDiscoveryToken": "", - "AgentHeartbeatInterval": strconv.Itoa(AgentHeartbeatInterval), - "AgentWebsocketUpgradeEnabled": strconv.FormatBool(AgentWebsocketUpgradeEnabled), - "NodeOfflineThreshold": strconv.Itoa(int(NodeOfflineThreshold.Milliseconds())), - "AgentUpdateRepo": AgentUpdateRepo, - "GeoIPProvider": GeoIPProvider, - "DatabaseAutoCleanupEnabled": strconv.FormatBool(DatabaseAutoCleanupEnabled), - "UptimeKumaEnabled": strconv.FormatBool(UptimeKumaEnabled), - "UptimeKumaUrl": UptimeKumaURL, - "UptimeKumaUsername": UptimeKumaUsername, - optionKeyUptimeKumaPassword: UptimeKumaPassword, - "UptimeKumaMonitorScope": UptimeKumaMonitorScope, - "UptimeKumaSelectedSites": UptimeKumaSelectedSites, - "UptimeKumaSyncInterval": strconv.Itoa(UptimeKumaSyncInterval), - "UptimeKumaInterval": strconv.Itoa(UptimeKumaInterval), - "UptimeKumaRetry": strconv.Itoa(UptimeKumaRetry), - "UptimeKumaRetryInterval": strconv.Itoa(UptimeKumaRetryInterval), - "UptimeKumaTimeout": strconv.Itoa(UptimeKumaTimeout), - "DatabaseAutoCleanupRetentionDays": strconv.Itoa(DatabaseAutoCleanupRetentionDays), - "OpenRestyDefaultServerReturnStatus": strconv.Itoa(OpenRestyDefaultServerReturnStatus), - "OpenRestyWorkerProcesses": OpenRestyWorkerProcesses, - "OpenRestyWorkerConnections": strconv.Itoa(OpenRestyWorkerConnections), - "OpenRestyWorkerRlimitNofile": strconv.Itoa(OpenRestyWorkerRlimitNofile), - "OpenRestyEventsUse": OpenRestyEventsUse, - "OpenRestyEventsMultiAcceptEnabled": strconv.FormatBool(OpenRestyEventsMultiAcceptEnabled), - "OpenRestyKeepaliveTimeout": strconv.Itoa(OpenRestyKeepaliveTimeout), - "OpenRestyKeepaliveRequests": strconv.Itoa(OpenRestyKeepaliveRequests), - "OpenRestyClientHeaderTimeout": strconv.Itoa(OpenRestyClientHeaderTimeout), - "OpenRestyClientBodyTimeout": strconv.Itoa(OpenRestyClientBodyTimeout), - "OpenRestyClientMaxBodySize": OpenRestyClientMaxBodySize, - "OpenRestyLargeClientHeaderBuffers": OpenRestyLargeClientHeaderBuffers, - "OpenRestySendTimeout": strconv.Itoa(OpenRestySendTimeout), - "OpenRestyProxyConnectTimeout": strconv.Itoa(OpenRestyProxyConnectTimeout), - "OpenRestyProxySendTimeout": strconv.Itoa(OpenRestyProxySendTimeout), - "OpenRestyProxyReadTimeout": strconv.Itoa(OpenRestyProxyReadTimeout), - "OpenRestyWebsocketEnabled": strconv.FormatBool(OpenRestyWebsocketEnabled), - "OpenRestyHTTP3Enabled": strconv.FormatBool(OpenRestyHTTP3Enabled), - "OpenRestyProxyRequestBufferingEnabled": strconv.FormatBool(OpenRestyProxyRequestBufferingEnabled), - "OpenRestyProxyBufferingEnabled": strconv.FormatBool(OpenRestyProxyBufferingEnabled), - "OpenRestyProxyBuffers": OpenRestyProxyBuffers, - "OpenRestyProxyBufferSize": OpenRestyProxyBufferSize, - "OpenRestyProxyBusyBuffersSize": OpenRestyProxyBusyBuffersSize, - "OpenRestyGzipEnabled": strconv.FormatBool(OpenRestyGzipEnabled), - "OpenRestyGzipMinLength": strconv.Itoa(OpenRestyGzipMinLength), - "OpenRestyGzipCompLevel": strconv.Itoa(OpenRestyGzipCompLevel), - "OpenRestyCacheEnabled": strconv.FormatBool(OpenRestyCacheEnabled), - "OpenRestyCachePath": OpenRestyCachePath, - "OpenRestyCacheLevels": OpenRestyCacheLevels, - "OpenRestyCacheInactive": OpenRestyCacheInactive, - "OpenRestyCacheMaxSize": OpenRestyCacheMaxSize, - "OpenRestyCacheKeyTemplate": OpenRestyCacheKeyTemplate, - "OpenRestyCacheLockEnabled": strconv.FormatBool(OpenRestyCacheLockEnabled), - "OpenRestyCacheLockTimeout": OpenRestyCacheLockTimeout, - "OpenRestyCacheUseStale": OpenRestyCacheUseStale, - "OpenRestyMainConfigTemplate": OpenRestyMainConfigTemplate, - } -} - -// InitOptionMap seeds defaults and overlays persisted options from of_options. -func InitOptionMap(ctx context.Context) error { - OptionMapRWMutex.Lock() - OptionMap = DefaultOpenFlareOptions() - OptionMapRWMutex.Unlock() - - options, err := ListOpenFlareOptions(ctx) - if err != nil { - return err - } - for _, option := range options { - applyOptionMap(option.Key, option.Value) - } - return nil -} - -// ListOpenFlareOptions returns all persisted options. -func ListOpenFlareOptions(ctx context.Context) ([]OpenFlareOption, error) { - var options []OpenFlareOption - if err := db.DB(ctx).Find(&options).Error; err != nil { - return nil, err - } - return options, nil -} - -// UpdateOpenFlareOption updates a single option in DB and memory. -func UpdateOpenFlareOption(ctx context.Context, key, value string) error { - return UpdateOpenFlareOptions(ctx, []OpenFlareOption{{Key: key, Value: value}}) -} - -// UpdateOpenFlareOptions batch-updates options in a transaction and refreshes OptionMap. -func UpdateOpenFlareOptions(ctx context.Context, options []OpenFlareOption) error { - if len(options) == 0 { - return nil - } - - if err := db.DB(ctx).Transaction(func(tx *gorm.DB) error { - for _, item := range options { - if item.Key == optionKeyUptimeKumaPassword && strings.TrimSpace(item.Value) == "" { - continue - } - option := OpenFlareOption{Key: item.Key} - if err := tx.FirstOrCreate(&option, OpenFlareOption{Key: item.Key}).Error; err != nil { - return err - } - option.Value = item.Value - if err := tx.Save(&option).Error; err != nil { - return err - } - } - return nil - }); err != nil { - return err - } - - for _, item := range options { - if item.Key == optionKeyUptimeKumaPassword && strings.TrimSpace(item.Value) == "" { - continue - } - applyOptionMap(item.Key, item.Value) - } - return nil -} - -// OptionValue returns a snapshot value from OptionMap. -func OptionValue(key string) string { - OptionMapRWMutex.RLock() - defer OptionMapRWMutex.RUnlock() - if OptionMap == nil { - return "" - } - return OptionMap[key] -} - -// ResetOptionMapForTest clears in-memory option state for unit tests. -func ResetOptionMapForTest() { - OptionMapRWMutex.Lock() - OptionMap = nil - OptionMapRWMutex.Unlock() -} - -func applyOptionMap(key, value string) { - OptionMapRWMutex.Lock() - defer OptionMapRWMutex.Unlock() - if OptionMap == nil { - OptionMap = make(map[string]string) - } - OptionMap[key] = value - applyEnabledOptionMirror(key, value) - applyStringOptionMirror(key, value) - applyIntegerOptionMirror(key, value) - applySpecialOptionMirror(key, value) -} diff --git a/internal/model/openflare_option_apply.go b/internal/model/openflare_option_apply.go deleted file mode 100644 index a69b40e7..00000000 --- a/internal/model/openflare_option_apply.go +++ /dev/null @@ -1,86 +0,0 @@ -package model - -import ( - "strconv" - "strings" - "time" -) - -var enabledOptionAppliers = map[string]func(bool){ - "PasswordRegisterEnabled": func(v bool) { PasswordRegisterEnabled = v }, - "PasswordLoginEnabled": func(v bool) { PasswordLoginEnabled = v }, - "CapLoginEnabled": func(v bool) { CapLoginEnabled = v }, - "EmailVerificationEnabled": func(v bool) { EmailVerificationEnabled = v }, - "AgentWebsocketUpgradeEnabled": func(v bool) { AgentWebsocketUpgradeEnabled = v }, - "UptimeKumaEnabled": func(v bool) { UptimeKumaEnabled = v }, - "DatabaseAutoCleanupEnabled": func(v bool) { DatabaseAutoCleanupEnabled = v }, -} - -var stringOptionAppliers = map[string]func(string){ - "ServerAddress": func(v string) { ServerAddress = v }, - "Footer": func(v string) { Footer = v }, - "HomePageLink": func(v string) { HomePageLink = v }, - "SystemName": func(v string) { SystemName = v }, - "AgentDiscoveryToken": func(v string) { AgentDiscoveryToken = v }, - "GeoIPProvider": func(v string) { GeoIPProvider = v }, - "UptimeKumaUrl": func(v string) { UptimeKumaURL = v }, - "UptimeKumaUsername": func(v string) { UptimeKumaUsername = v }, - optionKeyUptimeKumaPassword: func(v string) { UptimeKumaPassword = v }, - "UptimeKumaMonitorScope": func(v string) { UptimeKumaMonitorScope = v }, - "UptimeKumaSelectedSites": func(v string) { UptimeKumaSelectedSites = v }, -} - -func applyEnabledOptionMirror(key, value string) { - applier, ok := enabledOptionAppliers[key] - if !ok { - return - } - applier(value == optionValueTrue) -} - -func applyStringOptionMirror(key, value string) { - if applier, ok := stringOptionAppliers[key]; ok { - applier(value) - } -} - -type intOptionApplier struct { - apply func(int) - min int -} - -var integerOptionAppliers = map[string]intOptionApplier{ - "SMTPPort": {apply: func(v int) { SMTPPort = v }}, - "AgentHeartbeatInterval": {apply: func(v int) { AgentHeartbeatInterval = v }, min: 1}, - "UptimeKumaSyncInterval": {apply: func(v int) { UptimeKumaSyncInterval = v }, min: 1}, - "UptimeKumaInterval": {apply: func(v int) { UptimeKumaInterval = v }, min: 1}, - "UptimeKumaRetry": {apply: func(v int) { UptimeKumaRetry = v }, min: 0}, - "UptimeKumaRetryInterval": {apply: func(v int) { UptimeKumaRetryInterval = v }, min: 1}, - "UptimeKumaTimeout": {apply: func(v int) { UptimeKumaTimeout = v }, min: 1}, - "DatabaseAutoCleanupRetentionDays": {apply: func(v int) { DatabaseAutoCleanupRetentionDays = v }, min: 1}, -} - -func applyIntegerOptionMirror(key, value string) { - applier, ok := integerOptionAppliers[key] - if !ok { - return - } - intValue, err := strconv.Atoi(value) - if err != nil || intValue < applier.min { - return - } - applier.apply(intValue) -} - -func applySpecialOptionMirror(key, value string) { - switch key { - case "NodeOfflineThreshold": - if v, err := strconv.Atoi(value); err == nil && v > 0 { - NodeOfflineThreshold = time.Duration(v) * time.Millisecond - } - case "AgentUpdateRepo": - if strings.TrimSpace(value) != "" { - AgentUpdateRepo = value - } - } -} diff --git a/internal/model/system_configs.go b/internal/model/system_configs.go index 7ae3a9b7..6b42add0 100644 --- a/internal/model/system_configs.go +++ b/internal/model/system_configs.go @@ -37,8 +37,70 @@ const ( ConfigKeyLoginSessionTTLHours = "login_session_ttl_hours" // 登录会话过期时间 (小时,0表示浏览器关闭后自动退出登录,-1表示永不过期) ConfigKeyUpdateUpstreamRepository = "update_upstream_repository" // GitHub Actions Release 上游仓库 ConfigKeyStorageConfig = "storage_config" // 文件存储配置 (JSON) - ConfigKeyRelayFRPSWebUIEnabled = "relay_frps_web_ui_enabled" // 是否启用 FRPS 内置 Web 界面 - ConfigKeyRelayFRPSWebUIPort = "relay_frps_web_ui_port" // FRPS 内置 Web 界面端口 + ConfigKeyRelayFRPSWebUIEnabled = "relay_frps_web_ui_enabled" // 是否启用 FRPS 内置 Web 界面 + ConfigKeyRelayFRPSWebUIPort = "relay_frps_web_ui_port" // FRPS 内置 Web 界面端口 + + // OpenFlare 业务配置(从 of_options 迁移) + ConfigKeyAgentDiscoveryToken = "agent_discovery_token" //nolint:gosec // false positive: config key name. Agent 发现令牌 + ConfigKeyAgentHeartbeatInterval = "agent_heartbeat_interval" // Agent 心跳间隔(毫秒) + ConfigKeyAgentWebsocketUpgradeEnabled = "agent_websocket_upgrade_enabled" // Agent WebSocket 升级开关 + ConfigKeyNodeOfflineThreshold = "node_offline_threshold" // 节点离线阈值(毫秒) + ConfigKeyAgentUpdateRepo = "agent_update_repo" // Agent 更新仓库 + ConfigKeyGeoIPProvider = "geoip_provider" // GeoIP 服务商 + ConfigKeyDatabaseAutoCleanupEnabled = "database_auto_cleanup_enabled" // 数据库自动清理开关 + ConfigKeyDatabaseAutoCleanupRetentionDays = "database_auto_cleanup_retention_days" // 数据库保留天数 + + // UptimeKuma 集成配置 + ConfigKeyUptimeKumaEnabled = "uptime_kuma_enabled" // UptimeKuma 集成开关 + ConfigKeyUptimeKumaURL = "uptime_kuma_url" // UptimeKuma URL + ConfigKeyUptimeKumaUsername = "uptime_kuma_username" // UptimeKuma 用户名 + ConfigKeyUptimeKumaPassword = "uptime_kuma_password" //nolint:gosec // false positive: config key name. UptimeKuma 密码 + ConfigKeyUptimeKumaMonitorScope = "uptime_kuma_monitor_scope" // UptimeKuma 监控范围 + ConfigKeyUptimeKumaSelectedSites = "uptime_kuma_selected_sites" // UptimeKuma 选定站点 + ConfigKeyUptimeKumaSyncInterval = "uptime_kuma_sync_interval" // UptimeKuma 同步间隔(分钟) + ConfigKeyUptimeKumaInterval = "uptime_kuma_interval" // UptimeKuma 监控间隔(秒) + ConfigKeyUptimeKumaRetry = "uptime_kuma_retry" // UptimeKuma 重试次数 + ConfigKeyUptimeKumaRetryInterval = "uptime_kuma_retry_interval" // UptimeKuma 重试间隔(秒) + ConfigKeyUptimeKumaTimeout = "uptime_kuma_timeout" // UptimeKuma 超时(秒) + + // OpenResty 配置 + ConfigKeyOpenRestyDefaultServerReturnStatus = "openresty_default_server_return_status" // 默认服务器返回状态码 + ConfigKeyOpenRestyWorkerProcesses = "openresty_worker_processes" // Worker 进程数 + ConfigKeyOpenRestyWorkerConnections = "openresty_worker_connections" // Worker 连接数 + ConfigKeyOpenRestyWorkerRlimitNofile = "openresty_worker_rlimit_nofile" // Worker 文件描述符限制 + ConfigKeyOpenRestyEventsUse = "openresty_events_use" // 事件模型 + ConfigKeyOpenRestyEventsMultiAcceptEnabled = "openresty_events_multi_accept_enabled" // 多路接受开关 + ConfigKeyOpenRestyKeepaliveTimeout = "openresty_keepalive_timeout" // Keepalive 超时(秒) + ConfigKeyOpenRestyKeepaliveRequests = "openresty_keepalive_requests" // Keepalive 请求数 + ConfigKeyOpenRestyClientHeaderTimeout = "openresty_client_header_timeout" // 客户端头超时(秒) + ConfigKeyOpenRestyClientBodyTimeout = "openresty_client_body_timeout" // 客户端体超时(秒) + ConfigKeyOpenRestyClientMaxBodySize = "openresty_client_max_body_size" // 客户端最大体大小 + ConfigKeyOpenRestyLargeClientHeaderBuffers = "openresty_large_client_header_buffers" // 大客户端头缓冲区 + ConfigKeyOpenRestySendTimeout = "openresty_send_timeout" // 发送超时(秒) + ConfigKeyOpenRestyResolvers = "openresty_resolvers" // DNS 解析器 + ConfigKeyOpenRestyProxyConnectTimeout = "openresty_proxy_connect_timeout" // 代理连接超时(秒) + ConfigKeyOpenRestyProxySendTimeout = "openresty_proxy_send_timeout" // 代理发送超时(秒) + ConfigKeyOpenRestyProxyReadTimeout = "openresty_proxy_read_timeout" // 代理读取超时(秒) + ConfigKeyOpenRestyWebsocketEnabled = "openresty_websocket_enabled" // WebSocket 支持开关 + ConfigKeyOpenRestyHTTP3Enabled = "openresty_http3_enabled" // HTTP/3 支持开关 + ConfigKeyOpenRestyProxyRequestBufferingEnabled = "openresty_proxy_request_buffering_enabled" // 代理请求缓冲开关 + ConfigKeyOpenRestyProxyBufferingEnabled = "openresty_proxy_buffering_enabled" // 代理响应缓冲开关 + ConfigKeyOpenRestyProxyBuffers = "openresty_proxy_buffers" // 代理缓冲区 + ConfigKeyOpenRestyProxyBufferSize = "openresty_proxy_buffer_size" // 代理缓冲区大小 + ConfigKeyOpenRestyProxyBusyBuffersSize = "openresty_proxy_busy_buffers_size" // 代理繁忙缓冲区大小 + ConfigKeyOpenRestyGzipEnabled = "openresty_gzip_enabled" // Gzip 压缩开关 + ConfigKeyOpenRestyGzipMinLength = "openresty_gzip_min_length" // Gzip 最小长度 + ConfigKeyOpenRestyGzipCompLevel = "openresty_gzip_comp_level" // Gzip 压缩级别 + ConfigKeyOpenRestyCacheEnabled = "openresty_cache_enabled" // 缓存开关 + ConfigKeyOpenRestyCachePath = "openresty_cache_path" // 缓存路径 + ConfigKeyOpenRestyCacheLevels = "openresty_cache_levels" // 缓存层级 + ConfigKeyOpenRestyCacheInactive = "openresty_cache_inactive" // 缓存不活跃时间 + ConfigKeyOpenRestyCacheMaxSize = "openresty_cache_max_size" // 缓存最大大小 + ConfigKeyOpenRestyCacheKeyTemplate = "openresty_cache_key_template" // 缓存键模板 + ConfigKeyOpenRestyCacheLockEnabled = "openresty_cache_lock_enabled" // 缓存锁开关 + ConfigKeyOpenRestyCacheLockTimeout = "openresty_cache_lock_timeout" // 缓存锁超时 + ConfigKeyOpenRestyCacheUseStale = "openresty_cache_use_stale" // 缓存失效策略 + ConfigKeyOpenRestyMainConfigTemplate = "openresty_main_config_template" // 主配置模板 ) const (