mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
迁移配置表
This commit is contained in:
@@ -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(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 端不可达等具体原因。
|
- 修复 openflared frpc 子进程异常退出(`exit status 1`)时缺乏详细诊断信息的问题。现捕获 frpc stderr 并在进程退出时将其输出记录到结构化日志 `stderr` 字段,便于排查配置格式错误、Auth Token 鉴权失败、relay 端不可达等具体原因。
|
||||||
- 修复 Relay 节点启动时在双栈网络环境可能上报 IPv6 地址,导致 Tunnel frpc 客户端无法连接 frps 的问题。强化 `pkg/geoip.HTTPOutboundIPStrategy` 在回退到双栈客户端后仍优先返回 IPv4 地址,确保 Relay 心跳上报的 IP 与 frpc 连接兼容。
|
- 修复 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 动态探测上报。
|
- 优化并统一 `agent`、`relay`、`flared` 的 IP 探测与上报逻辑,均复用公用 `nodeip` 包;在未指定 `node_ip` 时实现心跳 Tick 动态探测上报。
|
||||||
- 优化 `pkg/geoip.GetOutboundIP` 出口 IP 探测策略,优先通过 `tcp4` 建立 HTTP 连接以获得 IPv4 公网地址,并在纯 IPv6/无 IPv4 路由环境下自动降级为双栈 `tcp` 握手。
|
- 优化 `pkg/geoip.GetOutboundIP` 出口 IP 探测策略,优先通过 `tcp4` 建立 HTTP 连接以获得 IPv4 公网地址,并在纯 IPv6/无 IPv4 路由环境下自动降级为双栈 `tcp` 握手。
|
||||||
- 优化 `agent` 系统指纹缓存算法,计算指纹时排除 `UptimeSeconds` 和 `ReportedAtUnix` 动态字段,防止周期心跳时不断触发冗余完整的系统 Profile 数据上报。
|
- 优化 `agent` 系统指纹缓存算法,计算指纹时排除 `UptimeSeconds` 和 `ReportedAtUnix` 动态字段,防止周期心跳时不断触发冗余完整的系统 Profile 数据上报。
|
||||||
|
|||||||
+3
-9
@@ -16173,12 +16173,6 @@ const docTemplate = `{
|
|||||||
"email_verification": {
|
"email_verification": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"footer_html": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"home_page_link": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password_register_enabled": {
|
"password_register_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -16188,9 +16182,6 @@ const docTemplate = `{
|
|||||||
"start_time": {
|
"start_time": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"system_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"version": {
|
"version": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -16617,6 +16608,9 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"web_server_enabled": {
|
"web_server_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"web_server_port": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 的代理)
|
||||||
+3
-9
@@ -16166,12 +16166,6 @@
|
|||||||
"email_verification": {
|
"email_verification": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"footer_html": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"home_page_link": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password_register_enabled": {
|
"password_register_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -16181,9 +16175,6 @@
|
|||||||
"start_time": {
|
"start_time": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"system_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"version": {
|
"version": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -16610,6 +16601,9 @@
|
|||||||
},
|
},
|
||||||
"web_server_enabled": {
|
"web_server_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"web_server_port": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-6
@@ -2300,18 +2300,12 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
email_verification:
|
email_verification:
|
||||||
type: boolean
|
type: boolean
|
||||||
footer_html:
|
|
||||||
type: string
|
|
||||||
home_page_link:
|
|
||||||
type: string
|
|
||||||
password_register_enabled:
|
password_register_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
server_address:
|
server_address:
|
||||||
type: string
|
type: string
|
||||||
start_time:
|
start_time:
|
||||||
type: integer
|
type: integer
|
||||||
system_name:
|
|
||||||
type: string
|
|
||||||
version:
|
version:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
@@ -2592,6 +2586,8 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
web_server_enabled:
|
web_server_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
web_server_port:
|
||||||
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
protocol.RelayProxyStat:
|
protocol.RelayProxyStat:
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
import type {OptionItem} from "@/lib/services/openflare"
|
import type {OptionItem} from "@/lib/services/openflare"
|
||||||
|
|
||||||
export type OpenFlareOpsFields = {
|
export type OpenFlareOpsFields = {
|
||||||
AgentHeartbeatInterval: string
|
agent_heartbeat_interval: string
|
||||||
AgentWebsocketUpgradeEnabled: boolean
|
agent_websocket_upgrade_enabled: boolean
|
||||||
NodeOfflineThreshold: string
|
node_offline_threshold: string
|
||||||
AgentUpdateRepo: string
|
agent_update_repo: string
|
||||||
GeoIPProvider: string
|
geoip_provider: string
|
||||||
ServerAddress: string
|
server_address: string
|
||||||
UptimeKumaEnabled: boolean
|
uptime_kuma_enabled: boolean
|
||||||
UptimeKumaUrl: string
|
uptime_kuma_url: string
|
||||||
UptimeKumaUsername: string
|
uptime_kuma_username: string
|
||||||
UptimeKumaPassword: string
|
uptime_kuma_password: string
|
||||||
UptimeKumaMonitorScope: string
|
uptime_kuma_monitor_scope: string
|
||||||
UptimeKumaSelectedSites: string
|
uptime_kuma_selected_sites: string
|
||||||
UptimeKumaSyncInterval: string
|
uptime_kuma_sync_interval: string
|
||||||
UptimeKumaInterval: string
|
uptime_kuma_interval: string
|
||||||
UptimeKumaRetry: string
|
uptime_kuma_retry: string
|
||||||
UptimeKumaRetryInterval: string
|
uptime_kuma_retry_interval: string
|
||||||
UptimeKumaTimeout: string
|
uptime_kuma_timeout: string
|
||||||
DatabaseAutoCleanupEnabled: boolean
|
database_auto_cleanup_enabled: boolean
|
||||||
DatabaseAutoCleanupRetentionDays: string
|
database_auto_cleanup_retention_days: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultOpenFlareOpsFields: OpenFlareOpsFields = {
|
export const defaultOpenFlareOpsFields: OpenFlareOpsFields = {
|
||||||
AgentHeartbeatInterval: "10000",
|
agent_heartbeat_interval: "10000",
|
||||||
AgentWebsocketUpgradeEnabled: true,
|
agent_websocket_upgrade_enabled: true,
|
||||||
NodeOfflineThreshold: "120000",
|
node_offline_threshold: "120000",
|
||||||
AgentUpdateRepo: "Rain-kl/OpenFlare",
|
agent_update_repo: "Rain-kl/OpenFlare",
|
||||||
GeoIPProvider: "ipinfo",
|
geoip_provider: "ipinfo",
|
||||||
ServerAddress: "",
|
server_address: "",
|
||||||
UptimeKumaEnabled: false,
|
uptime_kuma_enabled: false,
|
||||||
UptimeKumaUrl: "",
|
uptime_kuma_url: "",
|
||||||
UptimeKumaUsername: "",
|
uptime_kuma_username: "",
|
||||||
UptimeKumaPassword: "",
|
uptime_kuma_password: "",
|
||||||
UptimeKumaMonitorScope: "all",
|
uptime_kuma_monitor_scope: "all",
|
||||||
UptimeKumaSelectedSites: "",
|
uptime_kuma_selected_sites: "",
|
||||||
UptimeKumaSyncInterval: "5",
|
uptime_kuma_sync_interval: "5",
|
||||||
UptimeKumaInterval: "60",
|
uptime_kuma_interval: "60",
|
||||||
UptimeKumaRetry: "0",
|
uptime_kuma_retry: "0",
|
||||||
UptimeKumaRetryInterval: "60",
|
uptime_kuma_retry_interval: "60",
|
||||||
UptimeKumaTimeout: "48",
|
uptime_kuma_timeout: "48",
|
||||||
DatabaseAutoCleanupEnabled: false,
|
database_auto_cleanup_enabled: false,
|
||||||
DatabaseAutoCleanupRetentionDays: "30",
|
database_auto_cleanup_retention_days: "30",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INSTALLER_SCRIPT_URL =
|
export const INSTALLER_SCRIPT_URL =
|
||||||
@@ -64,25 +64,25 @@ export function mapOptionsToOpsFields(
|
|||||||
serverAddress = "",
|
serverAddress = "",
|
||||||
): OpenFlareOpsFields {
|
): OpenFlareOpsFields {
|
||||||
return {
|
return {
|
||||||
AgentHeartbeatInterval: optionMap.AgentHeartbeatInterval ?? "10000",
|
agent_heartbeat_interval: optionMap.agent_heartbeat_interval ?? "10000",
|
||||||
AgentWebsocketUpgradeEnabled: toBoolean(optionMap.AgentWebsocketUpgradeEnabled, true),
|
agent_websocket_upgrade_enabled: toBoolean(optionMap.agent_websocket_upgrade_enabled, true),
|
||||||
NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? "120000",
|
node_offline_threshold: optionMap.node_offline_threshold ?? "120000",
|
||||||
AgentUpdateRepo: optionMap.AgentUpdateRepo ?? "Rain-kl/OpenFlare",
|
agent_update_repo: optionMap.agent_update_repo ?? "Rain-kl/OpenFlare",
|
||||||
GeoIPProvider: optionMap.GeoIPProvider ?? "ipinfo",
|
geoip_provider: optionMap.geoip_provider ?? "ipinfo",
|
||||||
ServerAddress: optionMap.ServerAddress || serverAddress,
|
server_address: optionMap.server_address || serverAddress,
|
||||||
UptimeKumaEnabled: toBoolean(optionMap.UptimeKumaEnabled, false),
|
uptime_kuma_enabled: toBoolean(optionMap.uptime_kuma_enabled, false),
|
||||||
UptimeKumaUrl: optionMap.UptimeKumaUrl ?? "",
|
uptime_kuma_url: optionMap.uptime_kuma_url ?? "",
|
||||||
UptimeKumaUsername: optionMap.UptimeKumaUsername ?? "",
|
uptime_kuma_username: optionMap.uptime_kuma_username ?? "",
|
||||||
UptimeKumaPassword: "",
|
uptime_kuma_password: "",
|
||||||
UptimeKumaMonitorScope: optionMap.UptimeKumaMonitorScope ?? "all",
|
uptime_kuma_monitor_scope: optionMap.uptime_kuma_monitor_scope ?? "all",
|
||||||
UptimeKumaSelectedSites: optionMap.UptimeKumaSelectedSites ?? "",
|
uptime_kuma_selected_sites: optionMap.uptime_kuma_selected_sites ?? "",
|
||||||
UptimeKumaSyncInterval: optionMap.UptimeKumaSyncInterval ?? "5",
|
uptime_kuma_sync_interval: optionMap.uptime_kuma_sync_interval ?? "5",
|
||||||
UptimeKumaInterval: optionMap.UptimeKumaInterval ?? "60",
|
uptime_kuma_interval: optionMap.uptime_kuma_interval ?? "60",
|
||||||
UptimeKumaRetry: optionMap.UptimeKumaRetry ?? "0",
|
uptime_kuma_retry: optionMap.uptime_kuma_retry ?? "0",
|
||||||
UptimeKumaRetryInterval: optionMap.UptimeKumaRetryInterval ?? "60",
|
uptime_kuma_retry_interval: optionMap.uptime_kuma_retry_interval ?? "60",
|
||||||
UptimeKumaTimeout: optionMap.UptimeKumaTimeout ?? "48",
|
uptime_kuma_timeout: optionMap.uptime_kuma_timeout ?? "48",
|
||||||
DatabaseAutoCleanupEnabled: toBoolean(optionMap.DatabaseAutoCleanupEnabled, false),
|
database_auto_cleanup_enabled: toBoolean(optionMap.database_auto_cleanup_enabled, false),
|
||||||
DatabaseAutoCleanupRetentionDays: optionMap.DatabaseAutoCleanupRetentionDays ?? "30",
|
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) {
|
export function validateAgentFields(fields: OpenFlareOpsFields) {
|
||||||
const heartbeat = Number.parseInt(fields.AgentHeartbeatInterval, 10)
|
const heartbeat = Number.parseInt(fields.agent_heartbeat_interval, 10)
|
||||||
const offline = Number.parseInt(fields.NodeOfflineThreshold, 10)
|
const offline = Number.parseInt(fields.node_offline_threshold, 10)
|
||||||
if (Number.isNaN(heartbeat) || heartbeat < 5000) {
|
if (Number.isNaN(heartbeat) || heartbeat < 5000) {
|
||||||
throw new Error("心跳间隔不能小于 5000 毫秒。")
|
throw new Error("心跳间隔不能小于 5000 毫秒。")
|
||||||
}
|
}
|
||||||
@@ -122,15 +122,15 @@ export function validateAgentFields(fields: OpenFlareOpsFields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function validateUptimeKumaFields(fields: OpenFlareOpsFields) {
|
export function validateUptimeKumaFields(fields: OpenFlareOpsFields) {
|
||||||
const syncInt = Number.parseInt(fields.UptimeKumaSyncInterval, 10)
|
const syncInt = Number.parseInt(fields.uptime_kuma_sync_interval, 10)
|
||||||
const interval = Number.parseInt(fields.UptimeKumaInterval, 10)
|
const interval = Number.parseInt(fields.uptime_kuma_interval, 10)
|
||||||
const retry = Number.parseInt(fields.UptimeKumaRetry, 10)
|
const retry = Number.parseInt(fields.uptime_kuma_retry, 10)
|
||||||
const retryInt = Number.parseInt(fields.UptimeKumaRetryInterval, 10)
|
const retryInt = Number.parseInt(fields.uptime_kuma_retry_interval, 10)
|
||||||
const timeout = Number.parseInt(fields.UptimeKumaTimeout, 10)
|
const timeout = Number.parseInt(fields.uptime_kuma_timeout, 10)
|
||||||
|
|
||||||
if (fields.UptimeKumaEnabled) {
|
if (fields.uptime_kuma_enabled) {
|
||||||
if (!fields.UptimeKumaUrl.trim()) throw new Error("请输入 Uptime Kuma 地址。")
|
if (!fields.uptime_kuma_url.trim()) throw new Error("请输入 Uptime Kuma 地址。")
|
||||||
if (!fields.UptimeKumaUsername.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(syncInt) || syncInt <= 0) throw new Error("同步间隔必须为正整数。")
|
||||||
if (Number.isNaN(interval) || interval <= 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) {
|
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) {
|
if (Number.isNaN(retentionDays) || retentionDays < 1) {
|
||||||
throw new Error("自动清理保留天数至少为 1 天。")
|
throw new Error("自动清理保留天数至少为 1 天。")
|
||||||
}
|
}
|
||||||
@@ -149,35 +149,35 @@ export function validateDatabaseAutoCleanup(fields: OpenFlareOpsFields) {
|
|||||||
export function agentOptionEntries(fields: OpenFlareOpsFields): OptionItem[] {
|
export function agentOptionEntries(fields: OpenFlareOpsFields): OptionItem[] {
|
||||||
validateAgentFields(fields)
|
validateAgentFields(fields)
|
||||||
return [
|
return [
|
||||||
{ key: "AgentHeartbeatInterval", value: fields.AgentHeartbeatInterval },
|
{ key: "agent_heartbeat_interval", value: fields.agent_heartbeat_interval },
|
||||||
{ key: "AgentWebsocketUpgradeEnabled", value: String(fields.AgentWebsocketUpgradeEnabled) },
|
{ key: "agent_websocket_upgrade_enabled", value: String(fields.agent_websocket_upgrade_enabled) },
|
||||||
{ key: "NodeOfflineThreshold", value: fields.NodeOfflineThreshold },
|
{ key: "node_offline_threshold", value: fields.node_offline_threshold },
|
||||||
{ key: "AgentUpdateRepo", value: fields.AgentUpdateRepo.trim() },
|
{ key: "agent_update_repo", value: fields.agent_update_repo.trim() },
|
||||||
{ key: "GeoIPProvider", value: fields.GeoIPProvider },
|
{ key: "geoip_provider", value: fields.geoip_provider },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uptimeKumaOptionEntries(fields: OpenFlareOpsFields): OptionItem[] {
|
export function uptimeKumaOptionEntries(fields: OpenFlareOpsFields): OptionItem[] {
|
||||||
validateUptimeKumaFields(fields)
|
validateUptimeKumaFields(fields)
|
||||||
return [
|
return [
|
||||||
{ key: "UptimeKumaEnabled", value: String(fields.UptimeKumaEnabled) },
|
{ key: "uptime_kuma_enabled", value: String(fields.uptime_kuma_enabled) },
|
||||||
{ key: "UptimeKumaUrl", value: fields.UptimeKumaUrl.trim() },
|
{ key: "uptime_kuma_url", value: fields.uptime_kuma_url.trim() },
|
||||||
{ key: "UptimeKumaUsername", value: fields.UptimeKumaUsername.trim() },
|
{ key: "uptime_kuma_username", value: fields.uptime_kuma_username.trim() },
|
||||||
{ key: "UptimeKumaPassword", value: fields.UptimeKumaPassword },
|
{ key: "uptime_kuma_password", value: fields.uptime_kuma_password },
|
||||||
{ key: "UptimeKumaMonitorScope", value: fields.UptimeKumaMonitorScope },
|
{ key: "uptime_kuma_monitor_scope", value: fields.uptime_kuma_monitor_scope },
|
||||||
{ key: "UptimeKumaSelectedSites", value: fields.UptimeKumaSelectedSites },
|
{ key: "uptime_kuma_selected_sites", value: fields.uptime_kuma_selected_sites },
|
||||||
{ key: "UptimeKumaSyncInterval", value: fields.UptimeKumaSyncInterval },
|
{ key: "uptime_kuma_sync_interval", value: fields.uptime_kuma_sync_interval },
|
||||||
{ key: "UptimeKumaInterval", value: fields.UptimeKumaInterval },
|
{ key: "uptime_kuma_interval", value: fields.uptime_kuma_interval },
|
||||||
{ key: "UptimeKumaRetry", value: fields.UptimeKumaRetry },
|
{ key: "uptime_kuma_retry", value: fields.uptime_kuma_retry },
|
||||||
{ key: "UptimeKumaRetryInterval", value: fields.UptimeKumaRetryInterval },
|
{ key: "uptime_kuma_retry_interval", value: fields.uptime_kuma_retry_interval },
|
||||||
{ key: "UptimeKumaTimeout", value: fields.UptimeKumaTimeout },
|
{ key: "uptime_kuma_timeout", value: fields.uptime_kuma_timeout },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function databaseAutoCleanupEntries(fields: OpenFlareOpsFields): OptionItem[] {
|
export function databaseAutoCleanupEntries(fields: OpenFlareOpsFields): OptionItem[] {
|
||||||
validateDatabaseAutoCleanup(fields)
|
validateDatabaseAutoCleanup(fields)
|
||||||
return [
|
return [
|
||||||
{ key: "DatabaseAutoCleanupEnabled", value: String(fields.DatabaseAutoCleanupEnabled) },
|
{ key: "database_auto_cleanup_enabled", value: String(fields.database_auto_cleanup_enabled) },
|
||||||
{ key: "DatabaseAutoCleanupRetentionDays", value: fields.DatabaseAutoCleanupRetentionDays },
|
{ key: "database_auto_cleanup_retention_days", value: fields.database_auto_cleanup_retention_days },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import {Textarea} from "@/components/ui/textarea"
|
|||||||
import {ErrorInline} from "@/components/layout/error"
|
import {ErrorInline} from "@/components/layout/error"
|
||||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||||
import type {DatabaseCleanupTarget} from "@/lib/services/openflare"
|
import type {DatabaseCleanupTarget} from "@/lib/services/openflare"
|
||||||
import {AdminStatusService} from "@/lib/services/admin"
|
|
||||||
import {NodeService, OptionService, StatusService, UptimeKumaService,} from "@/lib/services/openflare"
|
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 {VersionUpgradeDialog} from "@/app/(main)/components/version-upgrade-dialog"
|
||||||
import {adminUpdateStatusQueryKey, openflarePublicStatusQueryKey,} from "@/lib/hooks/use-openflare-server-upgrade"
|
import {adminUpdateStatusQueryKey, openflarePublicStatusQueryKey,} from "@/lib/hooks/use-openflare-server-upgrade"
|
||||||
|
|
||||||
@@ -111,14 +111,14 @@ export function OpenFlareOpsSettings() {
|
|||||||
if (!optionsQuery.data) return
|
if (!optionsQuery.data) return
|
||||||
const optionMap = optionsToMap(optionsQuery.data)
|
const optionMap = optionsToMap(optionsQuery.data)
|
||||||
const serverAddress =
|
const serverAddress =
|
||||||
optionMap.ServerAddress ||
|
optionMap.server_address ||
|
||||||
statusQuery.data?.server_address ||
|
statusQuery.data?.server_address ||
|
||||||
getBrowserOrigin()
|
getBrowserOrigin()
|
||||||
setFields(mapOptionsToOpsFields(optionMap, serverAddress))
|
setFields(mapOptionsToOpsFields(optionMap, serverAddress))
|
||||||
}, [optionsQuery.data, statusQuery.data?.server_address])
|
}, [optionsQuery.data, statusQuery.data?.server_address])
|
||||||
|
|
||||||
const geoIPMutation = useMutation({
|
const geoIPMutation = useMutation({
|
||||||
mutationFn: () => OptionService.lookupGeoIP(fields.GeoIPProvider, geoIPTestIP.trim()),
|
mutationFn: () => OptionService.lookupGeoIP(fields.geoip_provider, geoIPTestIP.trim()),
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
@@ -186,9 +186,9 @@ export function OpenFlareOpsSettings() {
|
|||||||
|
|
||||||
const discoveryToken = bootstrapQuery.data?.discovery_token ?? ""
|
const discoveryToken = bootstrapQuery.data?.discovery_token ?? ""
|
||||||
const discoveryCommand = useMemo(() => {
|
const discoveryCommand = useMemo(() => {
|
||||||
if (!fields.ServerAddress || !discoveryToken) return ""
|
if (!fields.server_address || !discoveryToken) return ""
|
||||||
return buildDiscoveryCommand(fields.ServerAddress, discoveryToken)
|
return buildDiscoveryCommand(fields.server_address, discoveryToken)
|
||||||
}, [discoveryToken, fields.ServerAddress])
|
}, [discoveryToken, fields.server_address])
|
||||||
|
|
||||||
const updateField = <K extends keyof OpenFlareOpsFields>(
|
const updateField = <K extends keyof OpenFlareOpsFields>(
|
||||||
key: K,
|
key: K,
|
||||||
@@ -266,29 +266,29 @@ export function OpenFlareOpsSettings() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label={`心跳间隔 (${formatDurationLabel(fields.AgentHeartbeatInterval)})`}
|
label={`心跳间隔 (${formatDurationLabel(fields.agent_heartbeat_interval)})`}
|
||||||
value={fields.AgentHeartbeatInterval}
|
value={fields.agent_heartbeat_interval}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(value) => updateField("AgentHeartbeatInterval", value)}
|
onChange={(value) => updateField("agent_heartbeat_interval", value)}
|
||||||
/>
|
/>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label={`离线阈值 (${formatDurationLabel(fields.NodeOfflineThreshold)})`}
|
label={`离线阈值 (${formatDurationLabel(fields.node_offline_threshold)})`}
|
||||||
value={fields.NodeOfflineThreshold}
|
value={fields.node_offline_threshold}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(value) => updateField("NodeOfflineThreshold", value)}
|
onChange={(value) => updateField("node_offline_threshold", value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label="开启 WS 连接升级"
|
label="开启 WS 连接升级"
|
||||||
description="HTTP 心跳成功后尝试升级为 WebSocket,配置发布可即时通知。"
|
description="HTTP 心跳成功后尝试升级为 WebSocket,配置发布可即时通知。"
|
||||||
checked={fields.AgentWebsocketUpgradeEnabled}
|
checked={fields.agent_websocket_upgrade_enabled}
|
||||||
onChange={(value) => updateField("AgentWebsocketUpgradeEnabled", value)}
|
onChange={(value) => updateField("agent_websocket_upgrade_enabled", value)}
|
||||||
/>
|
/>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="Agent 更新仓库"
|
label="Agent 更新仓库"
|
||||||
value={fields.AgentUpdateRepo}
|
value={fields.agent_update_repo}
|
||||||
placeholder="Rain-kl/OpenFlare"
|
placeholder="Rain-kl/OpenFlare"
|
||||||
onChange={(value) => updateField("AgentUpdateRepo", value)}
|
onChange={(value) => updateField("agent_update_repo", value)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -314,8 +314,8 @@ export function OpenFlareOpsSettings() {
|
|||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>归属方式</Label>
|
<Label>归属方式</Label>
|
||||||
<Select
|
<Select
|
||||||
value={fields.GeoIPProvider}
|
value={fields.geoip_provider}
|
||||||
onValueChange={(value) => updateField("GeoIPProvider", value)}
|
onValueChange={(value) => updateField("geoip_provider", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -384,8 +384,8 @@ export function OpenFlareOpsSettings() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="Server URL"
|
label="Server URL"
|
||||||
value={fields.ServerAddress}
|
value={fields.server_address}
|
||||||
onChange={(value) => updateField("ServerAddress", value)}
|
onChange={(value) => updateField("server_address", value)}
|
||||||
placeholder="https://yourdomain.com"
|
placeholder="https://yourdomain.com"
|
||||||
/>
|
/>
|
||||||
<div className="rounded-lg border border-dashed p-3">
|
<div className="rounded-lg border border-dashed p-3">
|
||||||
@@ -428,7 +428,7 @@ export function OpenFlareOpsSettings() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={!fields.UptimeKumaEnabled || syncUptimeKumaMutation.isPending}
|
disabled={!fields.uptime_kuma_enabled || syncUptimeKumaMutation.isPending}
|
||||||
onClick={() => syncUptimeKumaMutation.mutate()}
|
onClick={() => syncUptimeKumaMutation.mutate()}
|
||||||
>
|
>
|
||||||
<RefreshCw className="size-3.5 mr-1" />
|
<RefreshCw className="size-3.5 mr-1" />
|
||||||
@@ -446,41 +446,41 @@ export function OpenFlareOpsSettings() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label="开启 Uptime Kuma"
|
label="开启 Uptime Kuma"
|
||||||
checked={fields.UptimeKumaEnabled}
|
checked={fields.uptime_kuma_enabled}
|
||||||
onChange={(value) => updateField("UptimeKumaEnabled", value)}
|
onChange={(value) => updateField("uptime_kuma_enabled", value)}
|
||||||
/>
|
/>
|
||||||
{fields.UptimeKumaEnabled ? (
|
{fields.uptime_kuma_enabled ? (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="Uptime Kuma 地址"
|
label="Uptime Kuma 地址"
|
||||||
value={fields.UptimeKumaUrl}
|
value={fields.uptime_kuma_url}
|
||||||
onChange={(value) => updateField("UptimeKumaUrl", value)}
|
onChange={(value) => updateField("uptime_kuma_url", value)}
|
||||||
placeholder="http://localhost:3001"
|
placeholder="http://localhost:3001"
|
||||||
/>
|
/>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="用户名"
|
label="用户名"
|
||||||
value={fields.UptimeKumaUsername}
|
value={fields.uptime_kuma_username}
|
||||||
onChange={(value) => updateField("UptimeKumaUsername", value)}
|
onChange={(value) => updateField("uptime_kuma_username", value)}
|
||||||
/>
|
/>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="密码"
|
label="密码"
|
||||||
value={fields.UptimeKumaPassword}
|
value={fields.uptime_kuma_password}
|
||||||
type="password"
|
type="password"
|
||||||
onChange={(value) => updateField("UptimeKumaPassword", value)}
|
onChange={(value) => updateField("uptime_kuma_password", value)}
|
||||||
placeholder="留空表示不更新"
|
placeholder="留空表示不更新"
|
||||||
/>
|
/>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="同步间隔 (分钟)"
|
label="同步间隔 (分钟)"
|
||||||
value={fields.UptimeKumaSyncInterval}
|
value={fields.uptime_kuma_sync_interval}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(value) => updateField("UptimeKumaSyncInterval", value)}
|
onChange={(value) => updateField("uptime_kuma_sync_interval", value)}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>监控范围</Label>
|
<Label>监控范围</Label>
|
||||||
<Select
|
<Select
|
||||||
value={fields.UptimeKumaMonitorScope}
|
value={fields.uptime_kuma_monitor_scope}
|
||||||
onValueChange={(value) => updateField("UptimeKumaMonitorScope", value)}
|
onValueChange={(value) => updateField("uptime_kuma_monitor_scope", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -492,7 +492,7 @@ export function OpenFlareOpsSettings() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{fields.UptimeKumaMonitorScope === "selected" ? (
|
{fields.uptime_kuma_monitor_scope === "selected" ? (
|
||||||
<div className="rounded-lg border border-dashed p-3">
|
<div className="rounded-lg border border-dashed p-3">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="text-sm font-medium">已选站点</p>
|
<p className="text-sm font-medium">已选站点</p>
|
||||||
@@ -506,8 +506,8 @@ export function OpenFlareOpsSettings() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 break-all text-xs text-muted-foreground">
|
<p className="mt-2 break-all text-xs text-muted-foreground">
|
||||||
{fields.UptimeKumaSelectedSites
|
{fields.uptime_kuma_selected_sites
|
||||||
? fields.UptimeKumaSelectedSites.split(",").join(", ")
|
? fields.uptime_kuma_selected_sites.split(",").join(", ")
|
||||||
: "未选择任何站点"}
|
: "未选择任何站点"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -515,27 +515,27 @@ export function OpenFlareOpsSettings() {
|
|||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="检测频率 (秒)"
|
label="检测频率 (秒)"
|
||||||
value={fields.UptimeKumaInterval}
|
value={fields.uptime_kuma_interval}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(value) => updateField("UptimeKumaInterval", value)}
|
onChange={(value) => updateField("uptime_kuma_interval", value)}
|
||||||
/>
|
/>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="重试次数"
|
label="重试次数"
|
||||||
value={fields.UptimeKumaRetry}
|
value={fields.uptime_kuma_retry}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(value) => updateField("UptimeKumaRetry", value)}
|
onChange={(value) => updateField("uptime_kuma_retry", value)}
|
||||||
/>
|
/>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="重试间隔 (秒)"
|
label="重试间隔 (秒)"
|
||||||
value={fields.UptimeKumaRetryInterval}
|
value={fields.uptime_kuma_retry_interval}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(value) => updateField("UptimeKumaRetryInterval", value)}
|
onChange={(value) => updateField("uptime_kuma_retry_interval", value)}
|
||||||
/>
|
/>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="请求超时 (秒)"
|
label="请求超时 (秒)"
|
||||||
value={fields.UptimeKumaTimeout}
|
value={fields.uptime_kuma_timeout}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(value) => updateField("UptimeKumaTimeout", value)}
|
onChange={(value) => updateField("uptime_kuma_timeout", value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -561,14 +561,14 @@ export function OpenFlareOpsSettings() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label="启用每日自动清理"
|
label="启用每日自动清理"
|
||||||
checked={fields.DatabaseAutoCleanupEnabled}
|
checked={fields.database_auto_cleanup_enabled}
|
||||||
onChange={(value) => updateField("DatabaseAutoCleanupEnabled", value)}
|
onChange={(value) => updateField("database_auto_cleanup_enabled", value)}
|
||||||
/>
|
/>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
label="保留天数"
|
label="保留天数"
|
||||||
value={fields.DatabaseAutoCleanupRetentionDays}
|
value={fields.database_auto_cleanup_retention_days}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(value) => updateField("DatabaseAutoCleanupRetentionDays", value)}
|
onChange={(value) => updateField("database_auto_cleanup_retention_days", value)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -653,12 +653,12 @@ export function OpenFlareOpsSettings() {
|
|||||||
<UptimeKumaSiteSelectModal
|
<UptimeKumaSiteSelectModal
|
||||||
open={uptimeKumaModalOpen}
|
open={uptimeKumaModalOpen}
|
||||||
selectedSites={
|
selectedSites={
|
||||||
fields.UptimeKumaSelectedSites
|
fields.uptime_kuma_selected_sites
|
||||||
? fields.UptimeKumaSelectedSites.split(",")
|
? fields.uptime_kuma_selected_sites.split(",")
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
onOpenChange={setUptimeKumaModalOpen}
|
onOpenChange={setUptimeKumaModalOpen}
|
||||||
onSave={(sites) => updateField("UptimeKumaSelectedSites", sites.join(","))}
|
onSave={(sites) => updateField("uptime_kuma_selected_sites", sites.join(","))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
|
|||||||
@@ -94,11 +94,6 @@ export function VersionSnapshotSheet({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-semibold">快照 JSON</p>
|
|
||||||
<CodeBlock code={detail.snapshot_json} language="json" className="my-0 text-xs" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-semibold">主配置</p>
|
<p className="text-xs font-semibold">主配置</p>
|
||||||
<CodeBlock code={detail.main_config} language="nginx" className="my-0 text-xs" />
|
<CodeBlock code={detail.main_config} language="nginx" className="my-0 text-xs" />
|
||||||
|
|||||||
@@ -1,80 +1,80 @@
|
|||||||
export type PerformanceFields = {
|
export type PerformanceFields = {
|
||||||
OpenRestyDefaultServerReturnStatus: string
|
openresty_default_server_return_status: string
|
||||||
OpenRestyWorkerProcesses: string
|
openresty_worker_processes: string
|
||||||
OpenRestyWorkerConnections: string
|
openresty_worker_connections: string
|
||||||
OpenRestyWorkerRlimitNofile: string
|
openresty_worker_rlimit_nofile: string
|
||||||
OpenRestyEventsUse: string
|
openresty_events_use: string
|
||||||
OpenRestyEventsMultiAcceptEnabled: boolean
|
openresty_events_multi_accept_enabled: boolean
|
||||||
OpenRestyKeepaliveTimeout: string
|
openresty_keepalive_timeout: string
|
||||||
OpenRestyKeepaliveRequests: string
|
openresty_keepalive_requests: string
|
||||||
OpenRestyClientHeaderTimeout: string
|
openresty_client_header_timeout: string
|
||||||
OpenRestyClientBodyTimeout: string
|
openresty_client_body_timeout: string
|
||||||
OpenRestyClientMaxBodySize: string
|
openresty_client_max_body_size: string
|
||||||
OpenRestyLargeClientHeaderBuffers: string
|
openresty_large_client_header_buffers: string
|
||||||
OpenRestySendTimeout: string
|
openresty_send_timeout: string
|
||||||
OpenRestyProxyConnectTimeout: string
|
openresty_proxy_connect_timeout: string
|
||||||
OpenRestyProxySendTimeout: string
|
openresty_proxy_send_timeout: string
|
||||||
OpenRestyProxyReadTimeout: string
|
openresty_proxy_read_timeout: string
|
||||||
OpenRestyWebsocketEnabled: boolean
|
openresty_websocket_enabled: boolean
|
||||||
OpenRestyHTTP3Enabled: boolean
|
openresty_http3_enabled: boolean
|
||||||
OpenRestyProxyRequestBufferingEnabled: boolean
|
openresty_proxy_request_buffering_enabled: boolean
|
||||||
OpenRestyProxyBufferingEnabled: boolean
|
openresty_proxy_buffering_enabled: boolean
|
||||||
OpenRestyProxyBuffers: string
|
openresty_proxy_buffers: string
|
||||||
OpenRestyProxyBufferSize: string
|
openresty_proxy_buffer_size: string
|
||||||
OpenRestyProxyBusyBuffersSize: string
|
openresty_proxy_busy_buffers_size: string
|
||||||
OpenRestyGzipEnabled: boolean
|
openresty_gzip_enabled: boolean
|
||||||
OpenRestyGzipMinLength: string
|
openresty_gzip_min_length: string
|
||||||
OpenRestyGzipCompLevel: string
|
openresty_gzip_comp_level: string
|
||||||
OpenRestyCacheEnabled: boolean
|
openresty_cache_enabled: boolean
|
||||||
OpenRestyCachePath: string
|
openresty_cache_path: string
|
||||||
OpenRestyCacheLevels: string
|
openresty_cache_levels: string
|
||||||
OpenRestyCacheInactive: string
|
openresty_cache_inactive: string
|
||||||
OpenRestyCacheMaxSize: string
|
openresty_cache_max_size: string
|
||||||
OpenRestyCacheKeyTemplate: string
|
openresty_cache_key_template: string
|
||||||
OpenRestyCacheLockEnabled: boolean
|
openresty_cache_lock_enabled: boolean
|
||||||
OpenRestyCacheLockTimeout: string
|
openresty_cache_lock_timeout: string
|
||||||
OpenRestyCacheUseStale: string
|
openresty_cache_use_stale: string
|
||||||
OpenRestyResolvers: string
|
openresty_resolvers: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPerformanceFields: PerformanceFields = {
|
export const defaultPerformanceFields: PerformanceFields = {
|
||||||
OpenRestyDefaultServerReturnStatus: "421",
|
openresty_default_server_return_status: "421",
|
||||||
OpenRestyWorkerProcesses: "auto",
|
openresty_worker_processes: "auto",
|
||||||
OpenRestyWorkerConnections: "4096",
|
openresty_worker_connections: "4096",
|
||||||
OpenRestyWorkerRlimitNofile: "65535",
|
openresty_worker_rlimit_nofile: "65535",
|
||||||
OpenRestyEventsUse: "epoll",
|
openresty_events_use: "epoll",
|
||||||
OpenRestyEventsMultiAcceptEnabled: true,
|
openresty_events_multi_accept_enabled: true,
|
||||||
OpenRestyKeepaliveTimeout: "20",
|
openresty_keepalive_timeout: "20",
|
||||||
OpenRestyKeepaliveRequests: "1000",
|
openresty_keepalive_requests: "1000",
|
||||||
OpenRestyClientHeaderTimeout: "15",
|
openresty_client_header_timeout: "15",
|
||||||
OpenRestyClientBodyTimeout: "15",
|
openresty_client_body_timeout: "15",
|
||||||
OpenRestyClientMaxBodySize: "64m",
|
openresty_client_max_body_size: "64m",
|
||||||
OpenRestyLargeClientHeaderBuffers: "4 16k",
|
openresty_large_client_header_buffers: "4 16k",
|
||||||
OpenRestySendTimeout: "30",
|
openresty_send_timeout: "30",
|
||||||
OpenRestyProxyConnectTimeout: "3",
|
openresty_proxy_connect_timeout: "3",
|
||||||
OpenRestyProxySendTimeout: "60",
|
openresty_proxy_send_timeout: "60",
|
||||||
OpenRestyProxyReadTimeout: "60",
|
openresty_proxy_read_timeout: "60",
|
||||||
OpenRestyWebsocketEnabled: true,
|
openresty_websocket_enabled: true,
|
||||||
OpenRestyHTTP3Enabled: true,
|
openresty_http3_enabled: true,
|
||||||
OpenRestyProxyRequestBufferingEnabled: false,
|
openresty_proxy_request_buffering_enabled: false,
|
||||||
OpenRestyProxyBufferingEnabled: true,
|
openresty_proxy_buffering_enabled: true,
|
||||||
OpenRestyProxyBuffers: "16 16k",
|
openresty_proxy_buffers: "16 16k",
|
||||||
OpenRestyProxyBufferSize: "8k",
|
openresty_proxy_buffer_size: "8k",
|
||||||
OpenRestyProxyBusyBuffersSize: "64k",
|
openresty_proxy_busy_buffers_size: "64k",
|
||||||
OpenRestyGzipEnabled: true,
|
openresty_gzip_enabled: true,
|
||||||
OpenRestyGzipMinLength: "1024",
|
openresty_gzip_min_length: "1024",
|
||||||
OpenRestyGzipCompLevel: "5",
|
openresty_gzip_comp_level: "5",
|
||||||
OpenRestyCacheEnabled: false,
|
openresty_cache_enabled: false,
|
||||||
OpenRestyCachePath: "",
|
openresty_cache_path: "",
|
||||||
OpenRestyCacheLevels: "1:2",
|
openresty_cache_levels: "1:2",
|
||||||
OpenRestyCacheInactive: "30m",
|
openresty_cache_inactive: "30m",
|
||||||
OpenRestyCacheMaxSize: "1g",
|
openresty_cache_max_size: "1g",
|
||||||
OpenRestyCacheKeyTemplate: "$scheme$host$request_uri",
|
openresty_cache_key_template: "$scheme$host$request_uri",
|
||||||
OpenRestyCacheLockEnabled: true,
|
openresty_cache_lock_enabled: true,
|
||||||
OpenRestyCacheLockTimeout: "5s",
|
openresty_cache_lock_timeout: "5s",
|
||||||
OpenRestyCacheUseStale:
|
openresty_cache_use_stale:
|
||||||
"error timeout updating http_500 http_502 http_503 http_504",
|
"error timeout updating http_500 http_502 http_503 http_504",
|
||||||
OpenRestyResolvers: "",
|
openresty_resolvers: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function optionsToMap(options: Array<{ key: string; value: string }>) {
|
export function optionsToMap(options: Array<{ key: string; value: string }>) {
|
||||||
@@ -93,65 +93,65 @@ export function mapOptionsToFields(
|
|||||||
optionMap: Record<string, string>,
|
optionMap: Record<string, string>,
|
||||||
): PerformanceFields {
|
): PerformanceFields {
|
||||||
return {
|
return {
|
||||||
OpenRestyDefaultServerReturnStatus:
|
openresty_default_server_return_status:
|
||||||
optionMap.OpenRestyDefaultServerReturnStatus ?? "421",
|
optionMap.openresty_default_server_return_status ?? "421",
|
||||||
OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? "auto",
|
openresty_worker_processes: optionMap.openresty_worker_processes ?? "auto",
|
||||||
OpenRestyWorkerConnections: optionMap.OpenRestyWorkerConnections ?? "4096",
|
openresty_worker_connections: optionMap.openresty_worker_connections ?? "4096",
|
||||||
OpenRestyWorkerRlimitNofile:
|
openresty_worker_rlimit_nofile:
|
||||||
optionMap.OpenRestyWorkerRlimitNofile ?? "65535",
|
optionMap.openresty_worker_rlimit_nofile ?? "65535",
|
||||||
OpenRestyEventsUse: optionMap.OpenRestyEventsUse ?? "epoll",
|
openresty_events_use: optionMap.openresty_events_use ?? "epoll",
|
||||||
OpenRestyEventsMultiAcceptEnabled: toBoolean(
|
openresty_events_multi_accept_enabled: toBoolean(
|
||||||
optionMap.OpenRestyEventsMultiAcceptEnabled,
|
optionMap.openresty_events_multi_accept_enabled,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? "20",
|
openresty_keepalive_timeout: optionMap.openresty_keepalive_timeout ?? "20",
|
||||||
OpenRestyKeepaliveRequests: optionMap.OpenRestyKeepaliveRequests ?? "1000",
|
openresty_keepalive_requests: optionMap.openresty_keepalive_requests ?? "1000",
|
||||||
OpenRestyClientHeaderTimeout:
|
openresty_client_header_timeout:
|
||||||
optionMap.OpenRestyClientHeaderTimeout ?? "15",
|
optionMap.openresty_client_header_timeout ?? "15",
|
||||||
OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? "15",
|
openresty_client_body_timeout: optionMap.openresty_client_body_timeout ?? "15",
|
||||||
OpenRestyClientMaxBodySize: optionMap.OpenRestyClientMaxBodySize ?? "64m",
|
openresty_client_max_body_size: optionMap.openresty_client_max_body_size ?? "64m",
|
||||||
OpenRestyLargeClientHeaderBuffers:
|
openresty_large_client_header_buffers:
|
||||||
optionMap.OpenRestyLargeClientHeaderBuffers ?? "4 16k",
|
optionMap.openresty_large_client_header_buffers ?? "4 16k",
|
||||||
OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? "30",
|
openresty_send_timeout: optionMap.openresty_send_timeout ?? "30",
|
||||||
OpenRestyProxyConnectTimeout: optionMap.OpenRestyProxyConnectTimeout ?? "3",
|
openresty_proxy_connect_timeout: optionMap.openresty_proxy_connect_timeout ?? "3",
|
||||||
OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? "60",
|
openresty_proxy_send_timeout: optionMap.openresty_proxy_send_timeout ?? "60",
|
||||||
OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? "60",
|
openresty_proxy_read_timeout: optionMap.openresty_proxy_read_timeout ?? "60",
|
||||||
OpenRestyWebsocketEnabled: toBoolean(
|
openresty_websocket_enabled: toBoolean(
|
||||||
optionMap.OpenRestyWebsocketEnabled,
|
optionMap.openresty_websocket_enabled,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
OpenRestyHTTP3Enabled: toBoolean(optionMap.OpenRestyHTTP3Enabled, false),
|
openresty_http3_enabled: toBoolean(optionMap.openresty_http3_enabled, false),
|
||||||
OpenRestyProxyRequestBufferingEnabled: toBoolean(
|
openresty_proxy_request_buffering_enabled: toBoolean(
|
||||||
optionMap.OpenRestyProxyRequestBufferingEnabled,
|
optionMap.openresty_proxy_request_buffering_enabled,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
OpenRestyProxyBufferingEnabled: toBoolean(
|
openresty_proxy_buffering_enabled: toBoolean(
|
||||||
optionMap.OpenRestyProxyBufferingEnabled,
|
optionMap.openresty_proxy_buffering_enabled,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
OpenRestyProxyBuffers: optionMap.OpenRestyProxyBuffers ?? "16 16k",
|
openresty_proxy_buffers: optionMap.openresty_proxy_buffers ?? "16 16k",
|
||||||
OpenRestyProxyBufferSize: optionMap.OpenRestyProxyBufferSize ?? "8k",
|
openresty_proxy_buffer_size: optionMap.openresty_proxy_buffer_size ?? "8k",
|
||||||
OpenRestyProxyBusyBuffersSize:
|
openresty_proxy_busy_buffers_size:
|
||||||
optionMap.OpenRestyProxyBusyBuffersSize ?? "64k",
|
optionMap.openresty_proxy_busy_buffers_size ?? "64k",
|
||||||
OpenRestyGzipEnabled: toBoolean(optionMap.OpenRestyGzipEnabled, true),
|
openresty_gzip_enabled: toBoolean(optionMap.openresty_gzip_enabled, true),
|
||||||
OpenRestyGzipMinLength: optionMap.OpenRestyGzipMinLength ?? "1024",
|
openresty_gzip_min_length: optionMap.openresty_gzip_min_length ?? "1024",
|
||||||
OpenRestyGzipCompLevel: optionMap.OpenRestyGzipCompLevel ?? "5",
|
openresty_gzip_comp_level: optionMap.openresty_gzip_comp_level ?? "5",
|
||||||
OpenRestyCacheEnabled: toBoolean(optionMap.OpenRestyCacheEnabled, false),
|
openresty_cache_enabled: toBoolean(optionMap.openresty_cache_enabled, false),
|
||||||
OpenRestyCachePath: optionMap.OpenRestyCachePath ?? "",
|
openresty_cache_path: optionMap.openresty_cache_path ?? "",
|
||||||
OpenRestyCacheLevels: optionMap.OpenRestyCacheLevels ?? "1:2",
|
openresty_cache_levels: optionMap.openresty_cache_levels ?? "1:2",
|
||||||
OpenRestyCacheInactive: optionMap.OpenRestyCacheInactive ?? "30m",
|
openresty_cache_inactive: optionMap.openresty_cache_inactive ?? "30m",
|
||||||
OpenRestyCacheMaxSize: optionMap.OpenRestyCacheMaxSize ?? "1g",
|
openresty_cache_max_size: optionMap.openresty_cache_max_size ?? "1g",
|
||||||
OpenRestyCacheKeyTemplate:
|
openresty_cache_key_template:
|
||||||
optionMap.OpenRestyCacheKeyTemplate ?? "$scheme$host$request_uri",
|
optionMap.openresty_cache_key_template ?? "$scheme$host$request_uri",
|
||||||
OpenRestyCacheLockEnabled: toBoolean(
|
openresty_cache_lock_enabled: toBoolean(
|
||||||
optionMap.OpenRestyCacheLockEnabled,
|
optionMap.openresty_cache_lock_enabled,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
OpenRestyCacheLockTimeout: optionMap.OpenRestyCacheLockTimeout ?? "5s",
|
openresty_cache_lock_timeout: optionMap.openresty_cache_lock_timeout ?? "5s",
|
||||||
OpenRestyCacheUseStale:
|
openresty_cache_use_stale:
|
||||||
optionMap.OpenRestyCacheUseStale ??
|
optionMap.openresty_cache_use_stale ??
|
||||||
"error timeout updating http_500 http_502 http_503 http_504",
|
"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) {
|
export function validateRuntimeFields(fields: PerformanceFields) {
|
||||||
if (
|
if (
|
||||||
fields.OpenRestyWorkerProcesses !== "auto" &&
|
fields.openresty_worker_processes !== "auto" &&
|
||||||
!isPositiveInteger(fields.OpenRestyWorkerProcesses)
|
!isPositiveInteger(fields.openresty_worker_processes)
|
||||||
) {
|
) {
|
||||||
throw new Error("worker_processes 必须为 auto 或大于 0 的整数")
|
throw new Error("worker_processes 必须为 auto 或大于 0 的整数")
|
||||||
}
|
}
|
||||||
const integers = [
|
const integers = [
|
||||||
fields.OpenRestyWorkerConnections,
|
fields.openresty_worker_connections,
|
||||||
fields.OpenRestyWorkerRlimitNofile,
|
fields.openresty_worker_rlimit_nofile,
|
||||||
fields.OpenRestyKeepaliveTimeout,
|
fields.openresty_keepalive_timeout,
|
||||||
fields.OpenRestyKeepaliveRequests,
|
fields.openresty_keepalive_requests,
|
||||||
fields.OpenRestyClientHeaderTimeout,
|
fields.openresty_client_header_timeout,
|
||||||
fields.OpenRestyClientBodyTimeout,
|
fields.openresty_client_body_timeout,
|
||||||
fields.OpenRestySendTimeout,
|
fields.openresty_send_timeout,
|
||||||
]
|
]
|
||||||
if (integers.some((value) => !isPositiveInteger(value))) {
|
if (integers.some((value) => !isPositiveInteger(value))) {
|
||||||
throw new Error("超时与连接参数必须为大于 0 的整数")
|
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) {
|
if (Number.isNaN(status) || status < 100 || status > 999) {
|
||||||
throw new Error("空白页面返回状态码必须在 100 到 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 格式不合法")
|
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"')
|
throw new Error('large_client_header_buffers 格式必须类似 "4 16k"')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateProxyFields(fields: PerformanceFields) {
|
export function validateProxyFields(fields: PerformanceFields) {
|
||||||
const timeouts = [
|
const timeouts = [
|
||||||
fields.OpenRestyProxyConnectTimeout,
|
fields.openresty_proxy_connect_timeout,
|
||||||
fields.OpenRestyProxySendTimeout,
|
fields.openresty_proxy_send_timeout,
|
||||||
fields.OpenRestyProxyReadTimeout,
|
fields.openresty_proxy_read_timeout,
|
||||||
]
|
]
|
||||||
if (timeouts.some((value) => !isPositiveInteger(value))) {
|
if (timeouts.some((value) => !isPositiveInteger(value))) {
|
||||||
throw new Error("代理超时参数必须为大于 0 的整数秒")
|
throw new Error("代理超时参数必须为大于 0 的整数秒")
|
||||||
}
|
}
|
||||||
if (!isProxyBuffersValue(fields.OpenRestyProxyBuffers)) {
|
if (!isProxyBuffersValue(fields.openresty_proxy_buffers)) {
|
||||||
throw new Error('proxy_buffers 格式必须类似 "16 16k"')
|
throw new Error('proxy_buffers 格式必须类似 "16 16k"')
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!isSizeValue(fields.OpenRestyProxyBufferSize) ||
|
!isSizeValue(fields.openresty_proxy_buffer_size) ||
|
||||||
!isSizeValue(fields.OpenRestyProxyBusyBuffersSize)
|
!isSizeValue(fields.openresty_proxy_busy_buffers_size)
|
||||||
) {
|
) {
|
||||||
throw new Error("缓冲大小必须为整数或带 k/m/g 单位的值")
|
throw new Error("缓冲大小必须为整数或带 k/m/g 单位的值")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateGzipFields(fields: PerformanceFields) {
|
export function validateGzipFields(fields: PerformanceFields) {
|
||||||
if (!isPositiveInteger(fields.OpenRestyGzipMinLength)) {
|
if (!isPositiveInteger(fields.openresty_gzip_min_length)) {
|
||||||
throw new Error("gzip_min_length 必须为大于 0 的整数")
|
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) {
|
if (Number.isNaN(level) || level < 1 || level > 9) {
|
||||||
throw new Error("gzip_comp_level 必须在 1 到 9 之间")
|
throw new Error("gzip_comp_level 必须在 1 到 9 之间")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateCacheFields(fields: PerformanceFields) {
|
export function validateCacheFields(fields: PerformanceFields) {
|
||||||
if (!fields.OpenRestyCacheEnabled) return
|
if (!fields.openresty_cache_enabled) return
|
||||||
if (!fields.OpenRestyCachePath.trim()) {
|
if (!fields.openresty_cache_path.trim()) {
|
||||||
throw new Error("启用缓存时必须填写 proxy_cache_path 目录")
|
throw new Error("启用缓存时必须填写 proxy_cache_path 目录")
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!isCacheLevelsValue(fields.OpenRestyCacheLevels) ||
|
!isCacheLevelsValue(fields.openresty_cache_levels) ||
|
||||||
!isDurationToken(fields.OpenRestyCacheInactive) ||
|
!isDurationToken(fields.openresty_cache_inactive) ||
|
||||||
!isSizeValue(fields.OpenRestyCacheMaxSize) ||
|
!isSizeValue(fields.openresty_cache_max_size) ||
|
||||||
!isDurationToken(fields.OpenRestyCacheLockTimeout)
|
!isDurationToken(fields.openresty_cache_lock_timeout)
|
||||||
) {
|
) {
|
||||||
throw new Error("缓存 levels、inactive、max_size 或 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 模板")
|
throw new Error("启用缓存时必须填写缓存 Key 模板")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,20 +162,20 @@ export default function PerformancePage() {
|
|||||||
section: "runtime",
|
section: "runtime",
|
||||||
validator: validateRuntimeFields,
|
validator: validateRuntimeFields,
|
||||||
entries: entriesFromKeys(fields, [
|
entries: entriesFromKeys(fields, [
|
||||||
"OpenRestyDefaultServerReturnStatus",
|
"openresty_default_server_return_status",
|
||||||
"OpenRestyWorkerProcesses",
|
"openresty_worker_processes",
|
||||||
"OpenRestyResolvers",
|
"openresty_resolvers",
|
||||||
"OpenRestyWorkerConnections",
|
"openresty_worker_connections",
|
||||||
"OpenRestyWorkerRlimitNofile",
|
"openresty_worker_rlimit_nofile",
|
||||||
"OpenRestyEventsUse",
|
"openresty_events_use",
|
||||||
"OpenRestyEventsMultiAcceptEnabled",
|
"openresty_events_multi_accept_enabled",
|
||||||
"OpenRestyKeepaliveTimeout",
|
"openresty_keepalive_timeout",
|
||||||
"OpenRestyKeepaliveRequests",
|
"openresty_keepalive_requests",
|
||||||
"OpenRestyClientHeaderTimeout",
|
"openresty_client_header_timeout",
|
||||||
"OpenRestyClientBodyTimeout",
|
"openresty_client_body_timeout",
|
||||||
"OpenRestyClientMaxBodySize",
|
"openresty_client_max_body_size",
|
||||||
"OpenRestyLargeClientHeaderBuffers",
|
"openresty_large_client_header_buffers",
|
||||||
"OpenRestySendTimeout",
|
"openresty_send_timeout",
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -189,12 +189,12 @@ export default function PerformancePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<CardContent className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
<FieldInput label="worker_processes" value={fields.OpenRestyWorkerProcesses} onChange={(v) => updateField("OpenRestyWorkerProcesses", v)} />
|
<FieldInput label="worker_processes" value={fields.openresty_worker_processes} onChange={(v) => updateField("openresty_worker_processes", v)} />
|
||||||
<FieldInput label="worker_connections" value={fields.OpenRestyWorkerConnections} onChange={(v) => updateField("OpenRestyWorkerConnections", v)} type="number" />
|
<FieldInput label="worker_connections" value={fields.openresty_worker_connections} onChange={(v) => updateField("openresty_worker_connections", v)} type="number" />
|
||||||
<FieldInput label="worker_rlimit_nofile" value={fields.OpenRestyWorkerRlimitNofile} onChange={(v) => updateField("OpenRestyWorkerRlimitNofile", v)} type="number" />
|
<FieldInput label="worker_rlimit_nofile" value={fields.openresty_worker_rlimit_nofile} onChange={(v) => updateField("openresty_worker_rlimit_nofile", v)} type="number" />
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>events use</Label>
|
<Label>events use</Label>
|
||||||
<Select value={fields.OpenRestyEventsUse} onValueChange={(v) => updateField("OpenRestyEventsUse", v)}>
|
<Select value={fields.openresty_events_use} onValueChange={(v) => updateField("openresty_events_use", v)}>
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="epoll">epoll</SelectItem>
|
<SelectItem value="epoll">epoll</SelectItem>
|
||||||
@@ -204,11 +204,11 @@ export default function PerformancePage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<ToggleRow label="multi_accept" checked={fields.OpenRestyEventsMultiAcceptEnabled} onChange={(v) => updateField("OpenRestyEventsMultiAcceptEnabled", v)} />
|
<ToggleRow label="multi_accept" checked={fields.openresty_events_multi_accept_enabled} onChange={(v) => updateField("openresty_events_multi_accept_enabled", v)} />
|
||||||
<FieldInput label="keepalive_timeout" value={fields.OpenRestyKeepaliveTimeout} onChange={(v) => updateField("OpenRestyKeepaliveTimeout", v)} type="number" />
|
<FieldInput label="keepalive_timeout" value={fields.openresty_keepalive_timeout} onChange={(v) => updateField("openresty_keepalive_timeout", v)} type="number" />
|
||||||
<FieldInput label="client_max_body_size" value={fields.OpenRestyClientMaxBodySize} onChange={(v) => updateField("OpenRestyClientMaxBodySize", v)} />
|
<FieldInput label="client_max_body_size" value={fields.openresty_client_max_body_size} onChange={(v) => updateField("openresty_client_max_body_size", v)} />
|
||||||
<FieldInput label="resolvers" value={fields.OpenRestyResolvers} onChange={(v) => updateField("OpenRestyResolvers", v)} placeholder="1.1.1.1 8.8.8.8" />
|
<FieldInput label="resolvers" value={fields.openresty_resolvers} onChange={(v) => updateField("openresty_resolvers", v)} placeholder="1.1.1.1 8.8.8.8" />
|
||||||
<FieldInput label="default_server_return_status" value={fields.OpenRestyDefaultServerReturnStatus} onChange={(v) => updateField("OpenRestyDefaultServerReturnStatus", v)} type="number" />
|
<FieldInput label="default_server_return_status" value={fields.openresty_default_server_return_status} onChange={(v) => updateField("openresty_default_server_return_status", v)} type="number" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -226,16 +226,16 @@ export default function PerformancePage() {
|
|||||||
section: "proxy",
|
section: "proxy",
|
||||||
validator: validateProxyFields,
|
validator: validateProxyFields,
|
||||||
entries: entriesFromKeys(fields, [
|
entries: entriesFromKeys(fields, [
|
||||||
"OpenRestyProxyConnectTimeout",
|
"openresty_proxy_connect_timeout",
|
||||||
"OpenRestyProxySendTimeout",
|
"openresty_proxy_send_timeout",
|
||||||
"OpenRestyProxyReadTimeout",
|
"openresty_proxy_read_timeout",
|
||||||
"OpenRestyWebsocketEnabled",
|
"openresty_websocket_enabled",
|
||||||
"OpenRestyHTTP3Enabled",
|
"openresty_http3_enabled",
|
||||||
"OpenRestyProxyRequestBufferingEnabled",
|
"openresty_proxy_request_buffering_enabled",
|
||||||
"OpenRestyProxyBufferingEnabled",
|
"openresty_proxy_buffering_enabled",
|
||||||
"OpenRestyProxyBuffers",
|
"openresty_proxy_buffers",
|
||||||
"OpenRestyProxyBufferSize",
|
"openresty_proxy_buffer_size",
|
||||||
"OpenRestyProxyBusyBuffersSize",
|
"openresty_proxy_busy_buffers_size",
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -245,12 +245,12 @@ export default function PerformancePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||||
<FieldInput label="proxy_connect_timeout" value={fields.OpenRestyProxyConnectTimeout} onChange={(v) => updateField("OpenRestyProxyConnectTimeout", v)} type="number" />
|
<FieldInput label="proxy_connect_timeout" value={fields.openresty_proxy_connect_timeout} onChange={(v) => updateField("openresty_proxy_connect_timeout", v)} type="number" />
|
||||||
<FieldInput label="proxy_read_timeout" value={fields.OpenRestyProxyReadTimeout} onChange={(v) => updateField("OpenRestyProxyReadTimeout", v)} type="number" />
|
<FieldInput label="proxy_read_timeout" value={fields.openresty_proxy_read_timeout} onChange={(v) => updateField("openresty_proxy_read_timeout", v)} type="number" />
|
||||||
<ToggleRow label="websocket" checked={fields.OpenRestyWebsocketEnabled} onChange={(v) => updateField("OpenRestyWebsocketEnabled", v)} />
|
<ToggleRow label="websocket" checked={fields.openresty_websocket_enabled} onChange={(v) => updateField("openresty_websocket_enabled", v)} />
|
||||||
<ToggleRow label="http3" checked={fields.OpenRestyHTTP3Enabled} onChange={(v) => updateField("OpenRestyHTTP3Enabled", v)} />
|
<ToggleRow label="http3" checked={fields.openresty_http3_enabled} onChange={(v) => updateField("openresty_http3_enabled", v)} />
|
||||||
<ToggleRow label="proxy_buffering" checked={fields.OpenRestyProxyBufferingEnabled} onChange={(v) => updateField("OpenRestyProxyBufferingEnabled", v)} />
|
<ToggleRow label="proxy_buffering" checked={fields.openresty_proxy_buffering_enabled} onChange={(v) => updateField("openresty_proxy_buffering_enabled", v)} />
|
||||||
<FieldInput label="proxy_buffers" value={fields.OpenRestyProxyBuffers} onChange={(v) => updateField("OpenRestyProxyBuffers", v)} />
|
<FieldInput label="proxy_buffers" value={fields.openresty_proxy_buffers} onChange={(v) => updateField("openresty_proxy_buffers", v)} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -266,9 +266,9 @@ export default function PerformancePage() {
|
|||||||
section: "gzip",
|
section: "gzip",
|
||||||
validator: validateGzipFields,
|
validator: validateGzipFields,
|
||||||
entries: entriesFromKeys(fields, [
|
entries: entriesFromKeys(fields, [
|
||||||
"OpenRestyGzipEnabled",
|
"openresty_gzip_enabled",
|
||||||
"OpenRestyGzipMinLength",
|
"openresty_gzip_min_length",
|
||||||
"OpenRestyGzipCompLevel",
|
"openresty_gzip_comp_level",
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -277,9 +277,9 @@ export default function PerformancePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<ToggleRow label="gzip" checked={fields.OpenRestyGzipEnabled} onChange={(v) => updateField("OpenRestyGzipEnabled", v)} />
|
<ToggleRow label="gzip" checked={fields.openresty_gzip_enabled} onChange={(v) => updateField("openresty_gzip_enabled", v)} />
|
||||||
<FieldInput label="gzip_min_length" value={fields.OpenRestyGzipMinLength} onChange={(v) => updateField("OpenRestyGzipMinLength", v)} type="number" />
|
<FieldInput label="gzip_min_length" value={fields.openresty_gzip_min_length} onChange={(v) => updateField("openresty_gzip_min_length", v)} type="number" />
|
||||||
<FieldInput label="gzip_comp_level" value={fields.OpenRestyGzipCompLevel} onChange={(v) => updateField("OpenRestyGzipCompLevel", v)} type="number" />
|
<FieldInput label="gzip_comp_level" value={fields.openresty_gzip_comp_level} onChange={(v) => updateField("openresty_gzip_comp_level", v)} type="number" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -297,15 +297,15 @@ export default function PerformancePage() {
|
|||||||
section: "cache",
|
section: "cache",
|
||||||
validator: validateCacheFields,
|
validator: validateCacheFields,
|
||||||
entries: entriesFromKeys(fields, [
|
entries: entriesFromKeys(fields, [
|
||||||
"OpenRestyCacheEnabled",
|
"openresty_cache_enabled",
|
||||||
"OpenRestyCachePath",
|
"openresty_cache_path",
|
||||||
"OpenRestyCacheLevels",
|
"openresty_cache_levels",
|
||||||
"OpenRestyCacheInactive",
|
"openresty_cache_inactive",
|
||||||
"OpenRestyCacheMaxSize",
|
"openresty_cache_max_size",
|
||||||
"OpenRestyCacheKeyTemplate",
|
"openresty_cache_key_template",
|
||||||
"OpenRestyCacheLockEnabled",
|
"openresty_cache_lock_enabled",
|
||||||
"OpenRestyCacheLockTimeout",
|
"openresty_cache_lock_timeout",
|
||||||
"OpenRestyCacheUseStale",
|
"openresty_cache_use_stale",
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -314,10 +314,10 @@ export default function PerformancePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<ToggleRow label="cache_enabled" checked={fields.OpenRestyCacheEnabled} onChange={(v) => updateField("OpenRestyCacheEnabled", v)} />
|
<ToggleRow label="cache_enabled" checked={fields.openresty_cache_enabled} onChange={(v) => updateField("openresty_cache_enabled", v)} />
|
||||||
<FieldInput label="proxy_cache_path" value={fields.OpenRestyCachePath} onChange={(v) => updateField("OpenRestyCachePath", v)} disabled={!fields.OpenRestyCacheEnabled} />
|
<FieldInput label="proxy_cache_path" value={fields.openresty_cache_path} onChange={(v) => updateField("openresty_cache_path", v)} disabled={!fields.openresty_cache_enabled} />
|
||||||
<FieldInput label="levels" value={fields.OpenRestyCacheLevels} onChange={(v) => updateField("OpenRestyCacheLevels", v)} disabled={!fields.OpenRestyCacheEnabled} />
|
<FieldInput label="levels" value={fields.openresty_cache_levels} onChange={(v) => updateField("openresty_cache_levels", v)} disabled={!fields.openresty_cache_enabled} />
|
||||||
<FieldInput label="max_size" value={fields.OpenRestyCacheMaxSize} onChange={(v) => updateField("OpenRestyCacheMaxSize", v)} disabled={!fields.OpenRestyCacheEnabled} />
|
<FieldInput label="max_size" value={fields.openresty_cache_max_size} onChange={(v) => updateField("openresty_cache_max_size", v)} disabled={!fields.openresty_cache_enabled} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
ofgeoip "github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
ofgeoip "github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -22,6 +23,9 @@ const (
|
|||||||
releaseChannelStable = "stable"
|
releaseChannelStable = "stable"
|
||||||
randomTokenBytes = 16
|
randomTokenBytes = 16
|
||||||
maxDatabaseTextLength = 16000
|
maxDatabaseTextLength = 16000
|
||||||
|
|
||||||
|
defaultAgentHeartbeatInterval = 10000 // 默认心跳间隔 10 秒(毫秒)
|
||||||
|
defaultAgentUpdateRepo = "Rain-kl/OpenFlare"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRandomToken() (string, error) {
|
func newRandomToken() (string, error) {
|
||||||
@@ -173,10 +177,7 @@ func isPublicNodeIP(raw string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAgentSettings(node *model.OpenFlareNode, updateNow bool, updateChannel string, updateTag string, restartOpenrestyNow bool) *Settings {
|
func buildAgentSettings(ctx context.Context, node *model.OpenFlareNode, updateNow bool, updateChannel string, updateTag string, restartOpenrestyNow bool) *Settings {
|
||||||
model.OptionMapRWMutex.RLock()
|
|
||||||
defer model.OptionMapRWMutex.RUnlock()
|
|
||||||
|
|
||||||
autoUpdate := false
|
autoUpdate := false
|
||||||
if node != nil {
|
if node != nil {
|
||||||
autoUpdate = node.AutoUpdateEnabled
|
autoUpdate = node.AutoUpdateEnabled
|
||||||
@@ -184,11 +185,23 @@ func buildAgentSettings(node *model.OpenFlareNode, updateNow bool, updateChannel
|
|||||||
if strings.TrimSpace(updateChannel) == "" {
|
if strings.TrimSpace(updateChannel) == "" {
|
||||||
updateChannel = releaseChannelStable
|
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{
|
return &Settings{
|
||||||
HeartbeatInterval: model.AgentHeartbeatInterval,
|
HeartbeatInterval: heartbeatInterval,
|
||||||
WebsocketUpgradeEnabled: model.AgentWebsocketUpgradeEnabled,
|
WebsocketUpgradeEnabled: wsUpgradeEnabled,
|
||||||
AutoUpdate: autoUpdate,
|
AutoUpdate: autoUpdate,
|
||||||
UpdateRepo: model.AgentUpdateRepo,
|
UpdateRepo: updateRepo.Value,
|
||||||
UpdateNow: updateNow,
|
UpdateNow: updateNow,
|
||||||
UpdateChannel: updateChannel,
|
UpdateChannel: updateChannel,
|
||||||
UpdateTag: strings.TrimSpace(updateTag),
|
UpdateTag: strings.TrimSpace(updateTag),
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func HeartbeatNode(ctx context.Context, authNode *model.OpenFlareNode, payload N
|
|||||||
|
|
||||||
return &HeartbeatResponse{
|
return &HeartbeatResponse{
|
||||||
Node: authNode,
|
Node: authNode,
|
||||||
AgentSettings: buildAgentSettings(authNode, updateNow, updateChannel, updateTag, restartOpenrestyNow),
|
AgentSettings: buildAgentSettings(ctx, authNode, updateNow, updateChannel, updateTag, restartOpenrestyNow),
|
||||||
ActiveConfig: activeConfig,
|
ActiveConfig: activeConfig,
|
||||||
WAFIPGroups: wafIPGroups,
|
WAFIPGroups: wafIPGroups,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/option"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
@@ -32,16 +32,14 @@ func setupAgentAuthTestDB(t *testing.T) func() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, sqliteDB.AutoMigrate(
|
require.NoError(t, sqliteDB.AutoMigrate(
|
||||||
&model.OpenFlareNode{},
|
&model.OpenFlareNode{},
|
||||||
&model.OpenFlareOption{},
|
&model.SystemConfig{},
|
||||||
))
|
))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
tokenCache.reset()
|
tokenCache.reset()
|
||||||
|
|
||||||
return func() {
|
return func() {
|
||||||
db.SetDB(nil)
|
db.SetDB(nil)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
tokenCache.reset()
|
tokenCache.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +152,7 @@ func TestAgentRegisterAuthMiddleware(t *testing.T) {
|
|||||||
LastSeenAt: &now,
|
LastSeenAt: &now,
|
||||||
NodeType: "edge_node",
|
NodeType: "edge_node",
|
||||||
}).Error)
|
}).Error)
|
||||||
require.NoError(t, model.UpdateOpenFlareOption(ctx, "AgentDiscoveryToken", "discovery-token"))
|
require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyAgentDiscoveryToken, "discovery-token"))
|
||||||
|
|
||||||
router := testhelper.NewTestGinEngine()
|
router := testhelper.NewTestGinEngine()
|
||||||
router.POST("/register", RegisterAuth(), func(c *gin.Context) {
|
router.POST("/register", RegisterAuth(), func(c *gin.Context) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/uptimekuma"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/uptimekuma"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
"github.com/Rain-kl/Wavelet/internal/task"
|
"github.com/Rain-kl/Wavelet/internal/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,13 +110,20 @@ type DatabaseAutoCleanupHandler struct{}
|
|||||||
|
|
||||||
// Execute runs retention-based cleanup for all observability targets.
|
// Execute runs retention-based cleanup for all observability targets.
|
||||||
func (h *DatabaseAutoCleanupHandler) Execute(ctx context.Context, _ []byte) (*task.TaskResult, error) {
|
func (h *DatabaseAutoCleanupHandler) Execute(ctx context.Context, _ []byte) (*task.TaskResult, error) {
|
||||||
if !model.DatabaseAutoCleanupEnabled {
|
// 从 SystemConfig 读取自动清理配置
|
||||||
|
enabled, _ := repository.GetBoolByKey(ctx, model.ConfigKeyDatabaseAutoCleanupEnabled)
|
||||||
|
if !enabled {
|
||||||
msg := "自动清理未启用,跳过执行"
|
msg := "自动清理未启用,跳过执行"
|
||||||
task.AppendLog(ctx, "%s", msg)
|
task.AppendLog(ctx, "%s", msg)
|
||||||
return &task.TaskResult{Message: msg}, nil
|
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())
|
summary, err := tasks.RunDatabaseAutoCleanupOnce(ctx, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
task.AppendLog(ctx, "可观测数据自动清理失败: %v", err)
|
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.
|
// 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) {
|
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 集成未启用,跳过执行"
|
msg := "Uptime Kuma 集成未启用,跳过执行"
|
||||||
task.AppendLog(ctx, "%s", msg)
|
task.AppendLog(ctx, "%s", msg)
|
||||||
return &task.TaskResult{Message: msg}, nil
|
return &task.TaskResult{Message: msg}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
interval := model.UptimeKumaSyncInterval
|
interval, _ := repository.GetIntByKey(ctx, model.ConfigKeyUptimeKumaSyncInterval)
|
||||||
if interval <= 0 {
|
if interval <= 0 {
|
||||||
interval = 5
|
interval = 5
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -17,13 +18,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDatabaseAutoCleanupHandlerSkipsWhenDisabled(t *testing.T) {
|
func TestDatabaseAutoCleanupHandlerSkipsWhenDisabled(t *testing.T) {
|
||||||
previousEnabled := model.DatabaseAutoCleanupEnabled
|
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||||
model.DatabaseAutoCleanupEnabled = false
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
t.Cleanup(func() {
|
|
||||||
model.DatabaseAutoCleanupEnabled = previousEnabled
|
|
||||||
})
|
})
|
||||||
|
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.NoError(t, err)
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
assert.Contains(t, result.Message, "未启用")
|
assert.Contains(t, result.Message, "未启用")
|
||||||
@@ -34,6 +40,7 @@ func TestDatabaseAutoCleanupHandlerDeletesRowsWhenEnabled(t *testing.T) {
|
|||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, sqliteDB.AutoMigrate(&model.SystemConfig{}))
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore())
|
resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore())
|
||||||
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
||||||
@@ -43,8 +50,9 @@ func TestDatabaseAutoCleanupHandlerDeletesRowsWhenEnabled(t *testing.T) {
|
|||||||
db.SetDB(nil)
|
db.SetDB(nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
require.NoError(t, model.InsertOpenFlareAccessLogsBatch(context.Background(), []*model.OpenFlareAccessLog{{
|
require.NoError(t, model.InsertOpenFlareAccessLogsBatch(ctx, []*model.OpenFlareAccessLog{{
|
||||||
NodeID: "node-a",
|
NodeID: "node-a",
|
||||||
LoggedAt: now.Add(-48 * time.Hour),
|
LoggedAt: now.Add(-48 * time.Hour),
|
||||||
RemoteAddr: "203.0.113.10",
|
RemoteAddr: "203.0.113.10",
|
||||||
@@ -53,33 +61,32 @@ func TestDatabaseAutoCleanupHandlerDeletesRowsWhenEnabled(t *testing.T) {
|
|||||||
StatusCode: 200,
|
StatusCode: 200,
|
||||||
}}))
|
}}))
|
||||||
|
|
||||||
previousEnabled := model.DatabaseAutoCleanupEnabled
|
require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyDatabaseAutoCleanupEnabled, "true"))
|
||||||
previousRetentionDays := model.DatabaseAutoCleanupRetentionDays
|
require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyDatabaseAutoCleanupRetentionDays, "1"))
|
||||||
model.DatabaseAutoCleanupEnabled = true
|
|
||||||
model.DatabaseAutoCleanupRetentionDays = 1
|
|
||||||
t.Cleanup(func() {
|
|
||||||
model.DatabaseAutoCleanupEnabled = previousEnabled
|
|
||||||
model.DatabaseAutoCleanupRetentionDays = previousRetentionDays
|
|
||||||
})
|
|
||||||
|
|
||||||
result, err := (&DatabaseAutoCleanupHandler{}).Execute(context.Background(), nil)
|
result, err := (&DatabaseAutoCleanupHandler{}).Execute(ctx, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
assert.Contains(t, result.Message, "共删除")
|
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)
|
require.NoError(t, err)
|
||||||
assert.Empty(t, rows)
|
assert.Empty(t, rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUptimeKumaSyncHandlerSkipsWhenDisabled(t *testing.T) {
|
func TestUptimeKumaSyncHandlerSkipsWhenDisabled(t *testing.T) {
|
||||||
previousEnabled := model.UptimeKumaEnabled
|
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||||
model.UptimeKumaEnabled = false
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
t.Cleanup(func() {
|
|
||||||
model.UptimeKumaEnabled = previousEnabled
|
|
||||||
})
|
})
|
||||||
|
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.NoError(t, err)
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
assert.Contains(t, result.Message, "未启用")
|
assert.Contains(t, result.Message, "未启用")
|
||||||
|
|||||||
@@ -14,10 +14,27 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
|
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 {
|
type snapshotRoute struct {
|
||||||
ID uint `json:"id,omitempty"`
|
ID uint `json:"id,omitempty"`
|
||||||
@@ -166,7 +183,7 @@ func buildCurrentConfigBundle(ctx context.Context, requireRoutes bool) (*configB
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
openRestyConfig := buildOpenRestyConfigSnapshot()
|
openRestyConfig := buildOpenRestyConfigSnapshot(ctx)
|
||||||
snapshotDoc := snapshotDocument{
|
snapshotDoc := snapshotDocument{
|
||||||
Routes: snapshotRoutes,
|
Routes: snapshotRoutes,
|
||||||
OpenRestyConfig: openRestyConfig,
|
OpenRestyConfig: openRestyConfig,
|
||||||
@@ -451,47 +468,70 @@ func convertPoWConfig(enabled bool, config *waf.PoWConfig) *openrestyrender.PoWC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildOpenRestyConfigSnapshot() openRestyConfigSnapshot {
|
func buildOpenRestyConfigSnapshot(ctx context.Context) openRestyConfigSnapshot {
|
||||||
model.OptionMapRWMutex.RLock()
|
// 读取所有 OpenResty 配置,使用默认值作为降级
|
||||||
defer model.OptionMapRWMutex.RUnlock()
|
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{
|
return openRestyConfigSnapshot{
|
||||||
DefaultServerReturnStatus: model.OpenRestyDefaultServerReturnStatus,
|
DefaultServerReturnStatus: getIntConfig(model.ConfigKeyOpenRestyDefaultServerReturnStatus, defaultOpenRestyReturnStatus),
|
||||||
WorkerProcesses: model.OpenRestyWorkerProcesses,
|
WorkerProcesses: getStringConfig(model.ConfigKeyOpenRestyWorkerProcesses, "auto"),
|
||||||
WorkerConnections: model.OpenRestyWorkerConnections,
|
WorkerConnections: getIntConfig(model.ConfigKeyOpenRestyWorkerConnections, defaultOpenRestyWorkerConns),
|
||||||
WorkerRlimitNofile: model.OpenRestyWorkerRlimitNofile,
|
WorkerRlimitNofile: getIntConfig(model.ConfigKeyOpenRestyWorkerRlimitNofile, defaultOpenRestyRlimitNofile),
|
||||||
EventsUse: model.OpenRestyEventsUse,
|
EventsUse: getStringConfig(model.ConfigKeyOpenRestyEventsUse, "epoll"),
|
||||||
EventsMultiAcceptEnabled: model.OpenRestyEventsMultiAcceptEnabled,
|
EventsMultiAcceptEnabled: getBoolConfig(model.ConfigKeyOpenRestyEventsMultiAcceptEnabled, true),
|
||||||
KeepaliveTimeout: model.OpenRestyKeepaliveTimeout,
|
KeepaliveTimeout: getIntConfig(model.ConfigKeyOpenRestyKeepaliveTimeout, defaultOpenRestyKeepaliveTimeout),
|
||||||
KeepaliveRequests: model.OpenRestyKeepaliveRequests,
|
KeepaliveRequests: getIntConfig(model.ConfigKeyOpenRestyKeepaliveRequests, defaultOpenRestyKeepaliveReqs),
|
||||||
ClientHeaderTimeout: model.OpenRestyClientHeaderTimeout,
|
ClientHeaderTimeout: getIntConfig(model.ConfigKeyOpenRestyClientHeaderTimeout, defaultOpenRestyHeaderTimeout),
|
||||||
ClientBodyTimeout: model.OpenRestyClientBodyTimeout,
|
ClientBodyTimeout: getIntConfig(model.ConfigKeyOpenRestyClientBodyTimeout, defaultOpenRestyBodyTimeout),
|
||||||
ClientMaxBodySize: model.OpenRestyClientMaxBodySize,
|
ClientMaxBodySize: getStringConfig(model.ConfigKeyOpenRestyClientMaxBodySize, "64m"),
|
||||||
LargeClientHeaderBuffers: model.OpenRestyLargeClientHeaderBuffers,
|
LargeClientHeaderBuffers: getStringConfig(model.ConfigKeyOpenRestyLargeClientHeaderBuffers, "4 16k"),
|
||||||
SendTimeout: model.OpenRestySendTimeout,
|
SendTimeout: getIntConfig(model.ConfigKeyOpenRestySendTimeout, defaultOpenRestySendTimeout),
|
||||||
ProxyConnectTimeout: model.OpenRestyProxyConnectTimeout,
|
ProxyConnectTimeout: getIntConfig(model.ConfigKeyOpenRestyProxyConnectTimeout, defaultOpenRestyConnectTimeout),
|
||||||
ProxySendTimeout: model.OpenRestyProxySendTimeout,
|
ProxySendTimeout: getIntConfig(model.ConfigKeyOpenRestyProxySendTimeout, defaultOpenRestyProxyTimeout),
|
||||||
ProxyReadTimeout: model.OpenRestyProxyReadTimeout,
|
ProxyReadTimeout: getIntConfig(model.ConfigKeyOpenRestyProxyReadTimeout, defaultOpenRestyProxyTimeout),
|
||||||
WebsocketEnabled: model.OpenRestyWebsocketEnabled,
|
WebsocketEnabled: getBoolConfig(model.ConfigKeyOpenRestyWebsocketEnabled, true),
|
||||||
HTTP3Enabled: model.OpenRestyHTTP3Enabled,
|
HTTP3Enabled: getBoolConfig(model.ConfigKeyOpenRestyHTTP3Enabled, true),
|
||||||
ProxyRequestBuffering: model.OpenRestyProxyRequestBufferingEnabled,
|
ProxyRequestBuffering: getBoolConfig(model.ConfigKeyOpenRestyProxyRequestBufferingEnabled, false),
|
||||||
ProxyBufferingEnabled: model.OpenRestyProxyBufferingEnabled,
|
ProxyBufferingEnabled: getBoolConfig(model.ConfigKeyOpenRestyProxyBufferingEnabled, true),
|
||||||
ProxyBuffers: model.OpenRestyProxyBuffers,
|
ProxyBuffers: getStringConfig(model.ConfigKeyOpenRestyProxyBuffers, "16 16k"),
|
||||||
ProxyBufferSize: model.OpenRestyProxyBufferSize,
|
ProxyBufferSize: getStringConfig(model.ConfigKeyOpenRestyProxyBufferSize, "8k"),
|
||||||
ProxyBusyBuffersSize: model.OpenRestyProxyBusyBuffersSize,
|
ProxyBusyBuffersSize: getStringConfig(model.ConfigKeyOpenRestyProxyBusyBuffersSize, "64k"),
|
||||||
GzipEnabled: model.OpenRestyGzipEnabled,
|
GzipEnabled: getBoolConfig(model.ConfigKeyOpenRestyGzipEnabled, true),
|
||||||
GzipMinLength: model.OpenRestyGzipMinLength,
|
GzipMinLength: getIntConfig(model.ConfigKeyOpenRestyGzipMinLength, defaultOpenRestyGzipMinLen),
|
||||||
GzipCompLevel: model.OpenRestyGzipCompLevel,
|
GzipCompLevel: getIntConfig(model.ConfigKeyOpenRestyGzipCompLevel, defaultOpenRestyGzipLevel),
|
||||||
Resolvers: model.OpenRestyResolvers,
|
Resolvers: getStringConfig(model.ConfigKeyOpenRestyResolvers, ""),
|
||||||
CacheEnabled: model.OpenRestyCacheEnabled,
|
CacheEnabled: getBoolConfig(model.ConfigKeyOpenRestyCacheEnabled, false),
|
||||||
CachePath: model.OpenRestyCachePath,
|
CachePath: getStringConfig(model.ConfigKeyOpenRestyCachePath, ""),
|
||||||
CacheLevels: model.OpenRestyCacheLevels,
|
CacheLevels: getStringConfig(model.ConfigKeyOpenRestyCacheLevels, "1:2"),
|
||||||
CacheInactive: model.OpenRestyCacheInactive,
|
CacheInactive: getStringConfig(model.ConfigKeyOpenRestyCacheInactive, "30m"),
|
||||||
CacheMaxSize: model.OpenRestyCacheMaxSize,
|
CacheMaxSize: getStringConfig(model.ConfigKeyOpenRestyCacheMaxSize, "1g"),
|
||||||
CacheKeyTemplate: model.OpenRestyCacheKeyTemplate,
|
CacheKeyTemplate: getStringConfig(model.ConfigKeyOpenRestyCacheKeyTemplate, "$scheme$host$request_uri"),
|
||||||
CacheLockEnabled: model.OpenRestyCacheLockEnabled,
|
CacheLockEnabled: getBoolConfig(model.ConfigKeyOpenRestyCacheLockEnabled, true),
|
||||||
CacheLockTimeout: model.OpenRestyCacheLockTimeout,
|
CacheLockTimeout: getStringConfig(model.ConfigKeyOpenRestyCacheLockTimeout, "5s"),
|
||||||
CacheUseStale: model.OpenRestyCacheUseStale,
|
CacheUseStale: getStringConfig(model.ConfigKeyOpenRestyCacheUseStale, "error timeout updating http_500 http_502 http_503 http_504"),
|
||||||
MainConfigTemplate: model.OpenRestyMainConfigTemplate,
|
MainConfigTemplate: getStringConfig(model.ConfigKeyOpenRestyMainConfigTemplate, model.DefaultOpenRestyMainConfigTemplate),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ func computeNodeStatus(node *model.OpenFlareNode) string {
|
|||||||
if node.LastSeenAt == nil || node.LastSeenAt.IsZero() {
|
if node.LastSeenAt == nil || node.LastSeenAt.IsZero() {
|
||||||
return nodeStatusPending
|
return nodeStatusPending
|
||||||
}
|
}
|
||||||
if time.Since(*node.LastSeenAt) > model.NodeOfflineThreshold {
|
// 使用默认阈值 2 分钟
|
||||||
|
threshold := 2 * time.Minute
|
||||||
|
if time.Since(*node.LastSeenAt) > threshold {
|
||||||
return nodeStatusOffline
|
return nodeStatusOffline
|
||||||
}
|
}
|
||||||
return nodeStatusOnline
|
return nodeStatusOnline
|
||||||
|
|||||||
@@ -148,6 +148,6 @@ func sanitizeProxyName(domain string) string {
|
|||||||
return strings.ReplaceAll(strings.ReplaceAll(domain, ".", "-"), "*", "wildcard")
|
return strings.ReplaceAll(strings.ReplaceAll(domain, ".", "-"), "*", "wildcard")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTunnelSettings(node *model.OpenFlareNode, updateNow bool, updateChannel, updateTag string) *relay.Settings {
|
func buildTunnelSettings(ctx context.Context, node *model.OpenFlareNode, updateNow bool, updateChannel, updateTag string) *relay.Settings {
|
||||||
return relay.BuildSettings(node, updateNow, updateChannel, updateTag)
|
return relay.BuildSettings(ctx, node, updateNow, updateChannel, updateTag)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload Heartbeat
|
|||||||
|
|
||||||
return &HeartbeatResponse{
|
return &HeartbeatResponse{
|
||||||
ActiveConfig: activeConfig,
|
ActiveConfig: activeConfig,
|
||||||
TunnelSettings: buildTunnelSettings(node, updateNow, updateChannel, updateTag),
|
TunnelSettings: buildTunnelSettings(ctx, node, updateNow, updateChannel, updateTag),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
"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/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
@@ -27,15 +26,14 @@ func setupFlaredObservabilityTestDB(t *testing.T) func() {
|
|||||||
require.NoError(t, sqliteDB.AutoMigrate(
|
require.NoError(t, sqliteDB.AutoMigrate(
|
||||||
&model.OpenFlareNode{},
|
&model.OpenFlareNode{},
|
||||||
&model.OpenFlareHealthEvent{},
|
&model.OpenFlareHealthEvent{},
|
||||||
|
&model.SystemConfig{},
|
||||||
))
|
))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
agent.ResetAuthCacheForTest()
|
agent.ResetAuthCacheForTest()
|
||||||
|
|
||||||
return func() {
|
return func() {
|
||||||
db.SetDB(nil)
|
db.SetDB(nil)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
agent.ResetAuthCacheForTest()
|
agent.ResetAuthCacheForTest()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/agent/geoipdata"
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/geoipdata"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip"
|
pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip"
|
||||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
)
|
)
|
||||||
@@ -30,30 +31,29 @@ var (
|
|||||||
currentProvider string
|
currentProvider string
|
||||||
)
|
)
|
||||||
|
|
||||||
// EnsureRuntimeProvider loads OpenFlare options once and configures pkg/geoip.
|
// EnsureRuntimeProvider loads GeoIP provider config from SystemConfig.
|
||||||
func EnsureRuntimeProvider(ctx context.Context) error {
|
func EnsureRuntimeProvider(ctx context.Context) error {
|
||||||
runtimeOnce.Do(func() {
|
runtimeOnce.Do(func() {
|
||||||
if err := model.InitOptionMap(ctx); err != nil {
|
runtimeInitErr = applyProviderFromSystemConfig(ctx)
|
||||||
runtimeInitErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
runtimeInitErr = applyProviderFromModel(ctx)
|
|
||||||
})
|
})
|
||||||
return runtimeInitErr
|
return runtimeInitErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshRuntimeProvider reapplies GeoIPProvider after option updates.
|
// RefreshRuntimeProvider reapplies GeoIPProvider after config updates.
|
||||||
func RefreshRuntimeProvider(ctx context.Context) error {
|
func RefreshRuntimeProvider(ctx context.Context) error {
|
||||||
if err := model.InitOptionMap(ctx); err != nil {
|
return applyProviderFromSystemConfig(ctx)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return applyProviderFromModel(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyProviderFromModel(ctx context.Context) error {
|
func applyProviderFromSystemConfig(ctx context.Context) error {
|
||||||
model.OptionMapRWMutex.RLock()
|
config, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyGeoIPProvider)
|
||||||
provider := strings.TrimSpace(model.GeoIPProvider)
|
if err != nil {
|
||||||
model.OptionMapRWMutex.RUnlock()
|
// 降级到默认值
|
||||||
|
return ApplyProvider(ctx, "ipinfo")
|
||||||
|
}
|
||||||
|
provider := strings.TrimSpace(config.Value)
|
||||||
|
if provider == "" {
|
||||||
|
provider = "ipinfo"
|
||||||
|
}
|
||||||
return ApplyProvider(ctx, provider)
|
return ApplyProvider(ctx, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,21 +16,25 @@ func TestEnsureRuntimeProviderInitializesConfiguredProvider(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("open sqlite: %v", err)
|
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)
|
t.Fatalf("migrate: %v", err)
|
||||||
}
|
}
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
db.SetDB(nil)
|
db.SetDB(nil)
|
||||||
model.ResetOptionMapForTest()
|
|
||||||
ResetRuntimeForTest()
|
ResetRuntimeForTest()
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
model.ResetOptionMapForTest()
|
|
||||||
ResetRuntimeForTest()
|
ResetRuntimeForTest()
|
||||||
if err := model.UpdateOpenFlareOption(ctx, "GeoIPProvider", pkggeoip.ProviderIPInfo); err != nil {
|
// 通过 SystemConfig 设置 GeoIPProvider 配置
|
||||||
t.Fatalf("update option: %v", err)
|
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 {
|
if err := EnsureRuntimeProvider(ctx); err != nil {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
||||||
ofnode "github.com/Rain-kl/Wavelet/internal/apps/openflare/node"
|
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/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
"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, err)
|
||||||
require.NoError(t, sqliteDB.AutoMigrate(
|
require.NoError(t, sqliteDB.AutoMigrate(
|
||||||
&model.OpenFlareNode{},
|
&model.OpenFlareNode{},
|
||||||
&model.OpenFlareOption{},
|
&model.SystemConfig{},
|
||||||
&model.OpenFlareApplyLog{},
|
&model.OpenFlareApplyLog{},
|
||||||
&model.OpenFlareNodeSystemProfile{},
|
&model.OpenFlareNodeSystemProfile{},
|
||||||
&model.OpenFlareHealthEvent{},
|
&model.OpenFlareHealthEvent{},
|
||||||
@@ -51,7 +50,6 @@ func setupProtocolTestEnv(t *testing.T) (*gin.Engine, func()) {
|
|||||||
))
|
))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
agent.ResetAuthCacheForTest()
|
agent.ResetAuthCacheForTest()
|
||||||
resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore())
|
resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore())
|
||||||
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
||||||
@@ -63,7 +61,6 @@ func setupProtocolTestEnv(t *testing.T) (*gin.Engine, func()) {
|
|||||||
resetObservabilityStore()
|
resetObservabilityStore()
|
||||||
resetAccessLogStore()
|
resetAccessLogStore()
|
||||||
db.SetDB(nil)
|
db.SetDB(nil)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
agent.ResetAuthCacheForTest()
|
agent.ResetAuthCacheForTest()
|
||||||
}
|
}
|
||||||
return engine, cleanup
|
return engine, cleanup
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/cap"
|
"github.com/Rain-kl/Wavelet/internal/apps/cap"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
"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/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
@@ -28,7 +27,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type statusPayload struct {
|
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) {
|
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)
|
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||||
t.Cleanup(cleanup)
|
t.Cleanup(cleanup)
|
||||||
|
|
||||||
require.NoError(t, dbConn.AutoMigrate(&model.OpenFlareOption{}))
|
|
||||||
option.ResetInitializationForTest()
|
|
||||||
t.Cleanup(option.ResetInitializationForTest)
|
|
||||||
|
|
||||||
require.NoError(t, dbConn.Model(&model.SystemConfig{}).
|
require.NoError(t, dbConn.Model(&model.SystemConfig{}).
|
||||||
Where("key = ?", model.ConfigKeyCapLoginEnabled).
|
Where("key = ?", model.ConfigKeyCapLoginEnabled).
|
||||||
Update("value", "false").Error)
|
Update("value", "false").Error)
|
||||||
@@ -119,7 +115,7 @@ func TestGETStatusReturnsSuccessEnvelope(t *testing.T) {
|
|||||||
|
|
||||||
var status statusPayload
|
var status statusPayload
|
||||||
unmarshalAPIData(t, resp.Data, &status)
|
unmarshalAPIData(t, resp.Data, &status)
|
||||||
assert.NotEmpty(t, status.SystemName)
|
assert.NotEmpty(t, status.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGETOptionRequiresAdminAuth(t *testing.T) {
|
func TestGETOptionRequiresAdminAuth(t *testing.T) {
|
||||||
@@ -175,30 +171,28 @@ func TestGETNodesWithAccessToken(t *testing.T) {
|
|||||||
requireAPIOK(t, w)
|
requireAPIOK(t, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOptionHotReloadAfterUpdate(t *testing.T) {
|
func TestOptionUpdatePersistsAndReflectsInStatus(t *testing.T) {
|
||||||
dbConn, r := setupAuthOptionIntegration(t)
|
dbConn, r := setupAuthOptionIntegration(t)
|
||||||
adminToken := seedUserWithAccessToken(t, dbConn, "admin", "password123", true)
|
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{
|
updateResp := performJSONRequest(t, r, http.MethodPost, apiPath("/option/update"), map[string]string{
|
||||||
"key": "SystemName",
|
"key": model.ConfigKeyServerAddress,
|
||||||
"value": "HotReloadIntegration",
|
"value": "https://hotreload.openflare.test",
|
||||||
}, adminAuthHeaders(adminToken))
|
}, adminAuthHeaders(adminToken))
|
||||||
assert.Equal(t, http.StatusOK, updateResp.Code)
|
assert.Equal(t, http.StatusOK, updateResp.Code)
|
||||||
requireAPIOK(t, updateResp)
|
requireAPIOK(t, updateResp)
|
||||||
|
|
||||||
statusAfter := getStatusSystemName(t, r, nil)
|
statusAfter := getStatusServerAddress(t, r, nil)
|
||||||
assert.Equal(t, "HotReloadIntegration", statusAfter)
|
assert.Equal(t, "https://hotreload.openflare.test", statusAfter)
|
||||||
assert.Equal(t, "HotReloadIntegration", model.SystemName)
|
|
||||||
|
|
||||||
|
// 验证已持久化到 SystemConfig
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
require.NoError(t, option.EnsureInitialized(ctx))
|
saved, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress)
|
||||||
assert.Equal(t, "HotReloadIntegration", model.OptionValue("SystemName"))
|
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()
|
t.Helper()
|
||||||
|
|
||||||
w := performJSONRequest(t, r, http.MethodGet, apiPath("/status"), nil, headers)
|
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
|
var status statusPayload
|
||||||
unmarshalAPIData(t, resp.Data, &status)
|
unmarshalAPIData(t, resp.Data, &status)
|
||||||
return status.SystemName
|
return status.ServerAddress
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
"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/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||||
@@ -48,12 +47,11 @@ func setupCoreChainTest(t *testing.T) (*gin.Engine, adminSeed, func()) {
|
|||||||
&model.OpenFlareWAFRuleGroupBinding{},
|
&model.OpenFlareWAFRuleGroupBinding{},
|
||||||
&model.OpenFlareWAFIPGroup{},
|
&model.OpenFlareWAFIPGroup{},
|
||||||
&model.OpenFlareNode{},
|
&model.OpenFlareNode{},
|
||||||
&model.OpenFlareOption{},
|
&model.SystemConfig{},
|
||||||
&model.OpenFlareApplyLog{},
|
&model.OpenFlareApplyLog{},
|
||||||
))
|
))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
agent.ResetAuthCacheForTest()
|
agent.ResetAuthCacheForTest()
|
||||||
|
|
||||||
seed, err := seedAdminWithAccessToken(sqliteDB)
|
seed, err := seedAdminWithAccessToken(sqliteDB)
|
||||||
@@ -64,7 +62,6 @@ func setupCoreChainTest(t *testing.T) (*gin.Engine, adminSeed, func()) {
|
|||||||
|
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
db.SetDB(nil)
|
db.SetDB(nil)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
agent.ResetAuthCacheForTest()
|
agent.ResetAuthCacheForTest()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/option"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
@@ -46,10 +45,10 @@ func setupSecurityTest(t *testing.T) (*gin.Engine, adminSeed, func()) {
|
|||||||
&model.ManagedDomain{},
|
&model.ManagedDomain{},
|
||||||
&model.DNSAccount{},
|
&model.DNSAccount{},
|
||||||
&model.AcmeAccount{},
|
&model.AcmeAccount{},
|
||||||
|
&model.SystemConfig{},
|
||||||
))
|
))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
|
|
||||||
seed, err := seedAdminWithAccessToken(sqliteDB)
|
seed, err := seedAdminWithAccessToken(sqliteDB)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -63,7 +62,6 @@ func setupSecurityTest(t *testing.T) (*gin.Engine, adminSeed, func()) {
|
|||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
config.Config.App.SessionSecret = oldSecret
|
config.Config.App.SessionSecret = oldSecret
|
||||||
db.SetDB(nil)
|
db.SetDB(nil)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return engine, seed, cleanup
|
return engine, seed, cleanup
|
||||||
|
|||||||
@@ -194,7 +194,10 @@ func computeNodeStatus(node *model.OpenFlareNode) string {
|
|||||||
if node.LastSeenAt == nil || node.LastSeenAt.IsZero() {
|
if node.LastSeenAt == nil || node.LastSeenAt.IsZero() {
|
||||||
return nodeStatusPending
|
return nodeStatusPending
|
||||||
}
|
}
|
||||||
if time.Since(*node.LastSeenAt) > model.NodeOfflineThreshold {
|
// 使用默认阈值 2 分钟,避免在这里读取配置
|
||||||
|
// 实际阈值会在需要精确判断的地方通过 getNodeOfflineThreshold 读取
|
||||||
|
threshold := 2 * time.Minute
|
||||||
|
if time.Since(*node.LastSeenAt) > threshold {
|
||||||
return nodeStatusOffline
|
return nodeStatusOffline
|
||||||
}
|
}
|
||||||
return nodeStatusOnline
|
return nodeStatusOnline
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/observability"
|
"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"
|
ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,6 +22,15 @@ const (
|
|||||||
defaultRelayVhostHTTPPort = 8080
|
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.
|
// Input is the create/update node payload.
|
||||||
type Input struct {
|
type Input struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -272,7 +281,7 @@ func RotateBootstrapToken(ctx context.Context) (*BootstrapView, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 nil, err
|
||||||
}
|
}
|
||||||
return &BootstrapView{DiscoveryToken: token}, nil
|
return &BootstrapView{DiscoveryToken: token}, nil
|
||||||
@@ -284,7 +293,7 @@ func GetAgentRelease(ctx context.Context, id uint, channel string) (*AgentReleas
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
release, err := fetchLatestGitHubRelease(ctx, model.AgentUpdateRepo, normalizeReleaseChannel(channel))
|
release, err := fetchLatestGitHubRelease(ctx, getAgentUpdateRepo(ctx), normalizeReleaseChannel(channel))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -300,7 +309,7 @@ func RequestAgentUpdate(ctx context.Context, id uint, input AgentUpdateInput) (*
|
|||||||
channel := normalizeReleaseChannel(input.Channel)
|
channel := normalizeReleaseChannel(input.Channel)
|
||||||
tagName := strings.TrimSpace(input.TagName)
|
tagName := strings.TrimSpace(input.TagName)
|
||||||
if tagName != "" {
|
if tagName != "" {
|
||||||
release, releaseErr := fetchGitHubReleaseByTag(ctx, model.AgentUpdateRepo, tagName)
|
release, releaseErr := fetchGitHubReleaseByTag(ctx, getAgentUpdateRepo(ctx), tagName)
|
||||||
if releaseErr != nil {
|
if releaseErr != nil {
|
||||||
return nil, releaseErr
|
return nil, releaseErr
|
||||||
}
|
}
|
||||||
@@ -369,20 +378,20 @@ func CleanupHealthEvents(ctx context.Context, id uint) (*HealthEventCleanupResul
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ensureGlobalDiscoveryToken(ctx context.Context) (string, error) {
|
func ensureGlobalDiscoveryToken(ctx context.Context) (string, error) {
|
||||||
if err := option.EnsureInitialized(ctx); err != nil {
|
// 从 SystemConfig 读取 Agent 发现令牌
|
||||||
return "", err
|
config, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyAgentDiscoveryToken)
|
||||||
}
|
if err == nil && strings.TrimSpace(config.Value) != "" {
|
||||||
model.OptionMapRWMutex.RLock()
|
return strings.TrimSpace(config.Value), nil
|
||||||
token := strings.TrimSpace(model.AgentDiscoveryToken)
|
|
||||||
model.OptionMapRWMutex.RUnlock()
|
|
||||||
if token != "" {
|
|
||||||
return token, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果不存在,生成新令牌并保存
|
||||||
token, err := newRandomToken()
|
token, err := newRandomToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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 "", err
|
||||||
}
|
}
|
||||||
return token, nil
|
return token, nil
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/option"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -35,12 +35,11 @@ func setupNodeTestDB(t *testing.T) func() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, sqliteDB.AutoMigrate(
|
require.NoError(t, sqliteDB.AutoMigrate(
|
||||||
&model.OpenFlareNode{},
|
&model.OpenFlareNode{},
|
||||||
&model.OpenFlareOption{},
|
&model.SystemConfig{},
|
||||||
&model.OpenFlareApplyLog{},
|
&model.OpenFlareApplyLog{},
|
||||||
))
|
))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore())
|
resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore())
|
||||||
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
||||||
|
|
||||||
@@ -48,7 +47,6 @@ func setupNodeTestDB(t *testing.T) func() {
|
|||||||
resetObservabilityStore()
|
resetObservabilityStore()
|
||||||
resetAccessLogStore()
|
resetAccessLogStore()
|
||||||
db.SetDB(nil)
|
db.SetDB(nil)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +191,10 @@ func TestBootstrapTokenLifecycle(t *testing.T) {
|
|||||||
rotated, err := RotateBootstrapToken(ctx)
|
rotated, err := RotateBootstrapToken(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEqual(t, first.DiscoveryToken, rotated.DiscoveryToken)
|
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) {
|
func TestValidateDiscoveryToken(t *testing.T) {
|
||||||
@@ -218,7 +219,7 @@ func TestRequestAgentUpdateWithPreviewTag(t *testing.T) {
|
|||||||
|
|
||||||
originalClient := setReleaseHTTPClientForTest(&http.Client{
|
originalClient := setReleaseHTTPClientForTest(&http.Client{
|
||||||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
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())
|
require.Equal(t, expected, req.URL.String())
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
StatusCode: http.StatusOK,
|
StatusCode: http.StatusOK,
|
||||||
@@ -321,7 +322,8 @@ func TestComputeNodeStatus(t *testing.T) {
|
|||||||
online := &model.OpenFlareNode{LastSeenAt: &now}
|
online := &model.OpenFlareNode{LastSeenAt: &now}
|
||||||
assert.Equal(t, nodeStatusOnline, computeNodeStatus(online))
|
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}
|
offline := &model.OpenFlareNode{LastSeenAt: &offlineAt}
|
||||||
assert.Equal(t, nodeStatusOffline, computeNodeStatus(offline))
|
assert.Equal(t, nodeStatusOffline, computeNodeStatus(offline))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,35 +8,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
||||||
oftasks "github.com/Rain-kl/Wavelet/internal/apps/openflare/tasks"
|
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/apps/openflare/uptimekuma"
|
||||||
"github.com/Rain-kl/Wavelet/internal/buildinfo"
|
"github.com/Rain-kl/Wavelet/internal/buildinfo"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"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 {
|
type publicAuthSourceView struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -50,9 +30,6 @@ type statusView struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
StartTime int64 `json:"start_time"`
|
StartTime int64 `json:"start_time"`
|
||||||
EmailVerification bool `json:"email_verification"`
|
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"`
|
ServerAddress string `json:"server_address"`
|
||||||
PasswordRegisterEnabled bool `json:"password_register_enabled"`
|
PasswordRegisterEnabled bool `json:"password_register_enabled"`
|
||||||
CapLoginEnabled bool `json:"cap_login_enabled"`
|
CapLoginEnabled bool `json:"cap_login_enabled"`
|
||||||
@@ -91,37 +68,32 @@ type optionBatchPayload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listOptions(ctx context.Context) ([]model.OpenFlareOption, error) {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
model.OptionMapRWMutex.RLock()
|
options := make([]model.OpenFlareOption, 0, len(configs))
|
||||||
defer model.OptionMapRWMutex.RUnlock()
|
for _, config := range configs {
|
||||||
|
// 跳过敏感配置(如密码、令牌)
|
||||||
options := make([]model.OpenFlareOption, 0, len(model.OptionMap))
|
if config.Visibility == model.ConfigVisibilityHidden && isSecretConfigKey(config.Key) {
|
||||||
for key, value := range model.OptionMap {
|
|
||||||
if isSecretOptionKey(key) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 将 snake_case key 转换为 PascalCase 以保持向后兼容
|
||||||
options = append(options, model.OpenFlareOption{
|
options = append(options, model.OpenFlareOption{
|
||||||
Key: key,
|
Key: config.Key,
|
||||||
Value: value,
|
Value: config.Value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return options, nil
|
return options, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateOption(ctx context.Context, option model.OpenFlareOption) error {
|
func updateOption(ctx context.Context, option model.OpenFlareOption) error {
|
||||||
if err := EnsureInitialized(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return updateOptions(ctx, []model.OpenFlareOption{option})
|
return updateOptions(ctx, []model.OpenFlareOption{option})
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateOptionsBatch(ctx context.Context, payload optionBatchPayload) error {
|
func updateOptionsBatch(ctx context.Context, payload optionBatchPayload) error {
|
||||||
if err := EnsureInitialized(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(payload.Options) == 0 {
|
if len(payload.Options) == 0 {
|
||||||
return errors.New(errInvalidParams)
|
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 {
|
func updateOptions(ctx context.Context, options []model.OpenFlareOption) error {
|
||||||
if err := validateOptions(options); err != nil {
|
if err := validateOptions(ctx, options); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := model.UpdateOpenFlareOptions(ctx, options); err != nil {
|
|
||||||
return err
|
// 将每个 option 更新到 SystemConfig
|
||||||
}
|
for _, opt := range options {
|
||||||
for _, item := range options {
|
if err := repository.SaveOrUpdateSystemConfig(ctx, opt.Key, opt.Value); err != nil {
|
||||||
if item.Key == "GeoIPProvider" {
|
return fmt.Errorf("failed to update config %s: %w", opt.Key, err)
|
||||||
return geoip.RefreshRuntimeProvider(ctx)
|
}
|
||||||
|
|
||||||
|
// 特殊处理:GeoIP 配置变更时刷新运行时
|
||||||
|
if opt.Key == model.ConfigKeyGeoIPProvider {
|
||||||
|
if err := geoip.RefreshRuntimeProvider(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatus(ctx context.Context, baseAPIPath string) (*statusView, error) {
|
func getStatus(ctx context.Context, baseAPIPath string) (*statusView, error) {
|
||||||
if err := EnsureInitialized(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
authSources, err := publicAuthSources(ctx, baseAPIPath)
|
authSources, err := publicAuthSources(ctx, baseAPIPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
authSources = []publicAuthSourceView{}
|
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{
|
return &statusView{
|
||||||
Version: buildinfo.Version,
|
Version: buildinfo.Version,
|
||||||
StartTime: model.StartTime,
|
StartTime: model.StartTime,
|
||||||
EmailVerification: model.EmailVerificationEnabled,
|
EmailVerification: emailVerification,
|
||||||
SystemName: model.SystemName,
|
ServerAddress: serverAddress.Value,
|
||||||
HomePageLink: model.HomePageLink,
|
PasswordRegisterEnabled: passwordRegisterEnabled,
|
||||||
FooterHTML: model.Footer,
|
CapLoginEnabled: capLoginEnabled,
|
||||||
ServerAddress: model.ServerAddress,
|
|
||||||
PasswordRegisterEnabled: model.PasswordRegisterEnabled,
|
|
||||||
CapLoginEnabled: model.CapLoginEnabled,
|
|
||||||
AuthSources: authSources,
|
AuthSources: authSources,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -229,8 +207,9 @@ func syncUptimeKuma(ctx context.Context) error {
|
|||||||
return uptimekuma.SyncToUptimeKuma(ctx)
|
return uptimekuma.SyncToUptimeKuma(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSecretOptionKey(key string) bool {
|
// isSecretConfigKey 判断 SystemConfig 的 key 是否为敏感配置
|
||||||
return strings.Contains(key, "Token") ||
|
func isSecretConfigKey(key string) bool {
|
||||||
strings.Contains(key, "Secret") ||
|
return strings.Contains(key, "token") ||
|
||||||
strings.Contains(key, "Password")
|
strings.Contains(key, "secret") ||
|
||||||
|
strings.Contains(key, "password")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -23,28 +24,35 @@ func setupOptionTestDB(t *testing.T) func() {
|
|||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, sqliteDB.AutoMigrate(&model.OpenFlareOption{}))
|
require.NoError(t, sqliteDB.AutoMigrate(&model.SystemConfig{}))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
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() {
|
return func() {
|
||||||
db.SetDB(nil)
|
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) {
|
func TestListOptionsFiltersSecretKeys(t *testing.T) {
|
||||||
cleanup := setupOptionTestDB(t)
|
cleanup := setupOptionTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
ctx := context.Background()
|
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)
|
options, err := listOptions(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -53,24 +61,50 @@ func TestListOptionsFiltersSecretKeys(t *testing.T) {
|
|||||||
keys[option.Key] = option.Value
|
keys[option.Key] = option.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "TestFlare", keys["SystemName"])
|
// geoip_provider 应该出现在列表中
|
||||||
assert.NotContains(t, keys, "SMTPToken")
|
assert.Equal(t, "ipinfo", keys["geoip_provider"])
|
||||||
assert.NotContains(t, keys, "GitHubClientSecret")
|
// 敏感配置(密码)应该被过滤掉
|
||||||
|
assert.NotContains(t, keys, "uptime_kuma_password")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateOptionHotReloadsOptionMap(t *testing.T) {
|
func TestUpdateOptionPersistsToSystemConfig(t *testing.T) {
|
||||||
cleanup := setupOptionTestDB(t)
|
cleanup := setupOptionTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
err := updateOption(ctx, model.OpenFlareOption{
|
err := updateOption(ctx, model.OpenFlareOption{
|
||||||
Key: "SystemName",
|
Key: model.ConfigKeyGeoIPProvider,
|
||||||
Value: "HotReloaded",
|
Value: "mmdb",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "HotReloaded", model.OptionValue("SystemName"))
|
// 验证配置已写入 SystemConfig
|
||||||
assert.Equal(t, "HotReloaded", model.SystemName)
|
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) {
|
func TestLookupGeoIPDisabledProvider(t *testing.T) {
|
||||||
|
|||||||
@@ -8,46 +8,48 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
var openRestyOptionValidators = map[string]func(key, value string) error{
|
var openRestyOptionValidators = map[string]func(key, value string) error{
|
||||||
"OpenRestyDefaultServerReturnStatus": validateOpenRestyDefaultServerReturnStatus,
|
model.ConfigKeyOpenRestyDefaultServerReturnStatus: validateOpenRestyDefaultServerReturnStatus,
|
||||||
"OpenRestyWorkerProcesses": validateOpenRestyWorkerProcesses,
|
model.ConfigKeyOpenRestyWorkerProcesses: validateOpenRestyWorkerProcesses,
|
||||||
"OpenRestyWorkerConnections": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyWorkerConnections: validatePositiveIntegerOption,
|
||||||
"OpenRestyWorkerRlimitNofile": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyWorkerRlimitNofile: validatePositiveIntegerOption,
|
||||||
"OpenRestyKeepaliveTimeout": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyKeepaliveTimeout: validatePositiveIntegerOption,
|
||||||
"OpenRestyKeepaliveRequests": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyKeepaliveRequests: validatePositiveIntegerOption,
|
||||||
"OpenRestyClientHeaderTimeout": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyClientHeaderTimeout: validatePositiveIntegerOption,
|
||||||
"OpenRestyClientBodyTimeout": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyClientBodyTimeout: validatePositiveIntegerOption,
|
||||||
"OpenRestySendTimeout": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestySendTimeout: validatePositiveIntegerOption,
|
||||||
"OpenRestyProxyConnectTimeout": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyProxyConnectTimeout: validatePositiveIntegerOption,
|
||||||
"OpenRestyProxySendTimeout": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyProxySendTimeout: validatePositiveIntegerOption,
|
||||||
"OpenRestyProxyReadTimeout": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyProxyReadTimeout: validatePositiveIntegerOption,
|
||||||
"OpenRestyGzipMinLength": validatePositiveIntegerOption,
|
model.ConfigKeyOpenRestyGzipMinLength: validatePositiveIntegerOption,
|
||||||
"OpenRestyGzipCompLevel": validateOpenRestyGzipCompLevel,
|
model.ConfigKeyOpenRestyGzipCompLevel: validateOpenRestyGzipCompLevel,
|
||||||
"OpenRestyEventsUse": validateOpenRestyEventsUse,
|
model.ConfigKeyOpenRestyEventsUse: validateOpenRestyEventsUse,
|
||||||
"OpenRestyResolvers": validateOpenRestyResolvers,
|
model.ConfigKeyOpenRestyResolvers: validateOpenRestyResolvers,
|
||||||
"OpenRestyEventsMultiAcceptEnabled": validateBooleanOption,
|
model.ConfigKeyOpenRestyEventsMultiAcceptEnabled: validateBooleanOption,
|
||||||
"OpenRestyWebsocketEnabled": validateBooleanOption,
|
model.ConfigKeyOpenRestyWebsocketEnabled: validateBooleanOption,
|
||||||
"OpenRestyHTTP3Enabled": validateBooleanOption,
|
model.ConfigKeyOpenRestyHTTP3Enabled: validateBooleanOption,
|
||||||
"OpenRestyProxyRequestBufferingEnabled": validateBooleanOption,
|
model.ConfigKeyOpenRestyProxyRequestBufferingEnabled: validateBooleanOption,
|
||||||
"OpenRestyProxyBufferingEnabled": validateBooleanOption,
|
model.ConfigKeyOpenRestyProxyBufferingEnabled: validateBooleanOption,
|
||||||
"OpenRestyGzipEnabled": validateBooleanOption,
|
model.ConfigKeyOpenRestyGzipEnabled: validateBooleanOption,
|
||||||
"OpenRestyCacheEnabled": validateBooleanOption,
|
model.ConfigKeyOpenRestyCacheEnabled: validateBooleanOption,
|
||||||
"OpenRestyCacheLockEnabled": validateBooleanOption,
|
model.ConfigKeyOpenRestyCacheLockEnabled: validateBooleanOption,
|
||||||
"OpenRestyProxyBuffers": validateOpenRestyProxyBuffers,
|
model.ConfigKeyOpenRestyProxyBuffers: validateOpenRestyProxyBuffers,
|
||||||
"OpenRestyLargeClientHeaderBuffers": validateOpenRestyProxyBuffers,
|
model.ConfigKeyOpenRestyLargeClientHeaderBuffers: validateOpenRestyProxyBuffers,
|
||||||
"OpenRestyProxyBufferSize": validateOpenRestySizeValue,
|
model.ConfigKeyOpenRestyProxyBufferSize: validateOpenRestySizeValue,
|
||||||
"OpenRestyProxyBusyBuffersSize": validateOpenRestySizeValue,
|
model.ConfigKeyOpenRestyProxyBusyBuffersSize: validateOpenRestySizeValue,
|
||||||
"OpenRestyCacheMaxSize": validateOpenRestySizeValue,
|
model.ConfigKeyOpenRestyCacheMaxSize: validateOpenRestySizeValue,
|
||||||
"OpenRestyClientMaxBodySize": validateOpenRestySizeValue,
|
model.ConfigKeyOpenRestyClientMaxBodySize: validateOpenRestySizeValue,
|
||||||
"OpenRestyCachePath": validateOpenRestyCachePath,
|
model.ConfigKeyOpenRestyCachePath: validateOpenRestyCachePath,
|
||||||
"OpenRestyCacheLevels": validateOpenRestyCacheLevels,
|
model.ConfigKeyOpenRestyCacheLevels: validateOpenRestyCacheLevels,
|
||||||
"OpenRestyCacheInactive": validateOpenRestyDurationToken,
|
model.ConfigKeyOpenRestyCacheInactive: validateOpenRestyDurationToken,
|
||||||
"OpenRestyCacheLockTimeout": validateOpenRestyDurationToken,
|
model.ConfigKeyOpenRestyCacheLockTimeout: validateOpenRestyDurationToken,
|
||||||
"OpenRestyCacheKeyTemplate": validateOpenRestyCacheKeyTemplate,
|
model.ConfigKeyOpenRestyCacheKeyTemplate: validateOpenRestyCacheKeyTemplate,
|
||||||
"OpenRestyCacheUseStale": validateOpenRestyCacheUseStale,
|
model.ConfigKeyOpenRestyCacheUseStale: validateOpenRestyCacheUseStale,
|
||||||
"OpenRestyMainConfigTemplate": validateOpenRestyMainConfigTemplate,
|
model.ConfigKeyOpenRestyMainConfigTemplate: validateOpenRestyMainConfigTemplate,
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateOpenRestyOption(key, value string) error {
|
func validateOpenRestyOption(key, value string) error {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxOpenRestyGzipCompLevel = 9
|
const maxOpenRestyGzipCompLevel = 9
|
||||||
@@ -25,21 +27,25 @@ var (
|
|||||||
|
|
||||||
const optionValueTrue = "true"
|
const optionValueTrue = "true"
|
||||||
|
|
||||||
func buildOptionValidationState(options []model.OpenFlareOption) map[string]string {
|
func buildOptionValidationState(ctx context.Context, options []model.OpenFlareOption) map[string]string {
|
||||||
model.OptionMapRWMutex.RLock()
|
// 从 SystemConfig 读取所有业务配置构建状态
|
||||||
state := make(map[string]string, len(model.OptionMap)+len(options))
|
configs, err := repository.ListAdminSystemConfigs(ctx, "business")
|
||||||
for key, value := range model.OptionMap {
|
state := make(map[string]string, len(configs)+len(options))
|
||||||
state[key] = value
|
|
||||||
}
|
|
||||||
model.OptionMapRWMutex.RUnlock()
|
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
for _, config := range configs {
|
||||||
|
state[config.Key] = config.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用待验证的新值
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
state[option.Key] = option.Value
|
state[option.Key] = option.Value
|
||||||
}
|
}
|
||||||
return state
|
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 {
|
if err := validateOpenRestyOption(option.Key, option.Value); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -53,7 +59,7 @@ func validateOptionWithState(option model.OpenFlareOption, state map[string]stri
|
|||||||
if err := validateAgentOption(option.Key, option.Value); err != nil {
|
if err := validateAgentOption(option.Key, option.Value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return validateUptimeKumaOption(option.Key, option.Value, state)
|
return validateUptimeKumaOption(ctx, option.Key, option.Value, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePositiveIntegerOption(key, value string) error {
|
func validatePositiveIntegerOption(key, value string) error {
|
||||||
@@ -74,7 +80,7 @@ func validateBooleanOption(key, value string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateGeoIPOption(key, value string) error {
|
func validateGeoIPOption(key, value string) error {
|
||||||
if key != "GeoIPProvider" {
|
if key != model.ConfigKeyGeoIPProvider {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if geoip.IsValidProvider(value) {
|
if geoip.IsValidProvider(value) {
|
||||||
@@ -85,9 +91,9 @@ func validateGeoIPOption(key, value string) error {
|
|||||||
|
|
||||||
func validateDatabaseCleanupOption(key, value string) error {
|
func validateDatabaseCleanupOption(key, value string) error {
|
||||||
switch key {
|
switch key {
|
||||||
case "DatabaseAutoCleanupEnabled":
|
case model.ConfigKeyDatabaseAutoCleanupEnabled:
|
||||||
return validateBooleanOption(key, value)
|
return validateBooleanOption(key, value)
|
||||||
case "DatabaseAutoCleanupRetentionDays":
|
case model.ConfigKeyDatabaseAutoCleanupRetentionDays:
|
||||||
intValue, err := strconv.Atoi(value)
|
intValue, err := strconv.Atoi(value)
|
||||||
if err != nil || intValue < 1 {
|
if err != nil || intValue < 1 {
|
||||||
return fmt.Errorf("%s 必须为大于等于 1 的整数天", key)
|
return fmt.Errorf("%s 必须为大于等于 1 的整数天", key)
|
||||||
@@ -97,55 +103,59 @@ func validateDatabaseCleanupOption(key, value string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateAgentOption(key, value string) error {
|
func validateAgentOption(key, value string) error {
|
||||||
if key == "AgentWebsocketUpgradeEnabled" {
|
if key == model.ConfigKeyAgentWebsocketUpgradeEnabled {
|
||||||
return validateBooleanOption(key, strings.TrimSpace(value))
|
return validateBooleanOption(key, strings.TrimSpace(value))
|
||||||
}
|
}
|
||||||
return nil
|
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)
|
trimmed := strings.TrimSpace(value)
|
||||||
switch key {
|
switch key {
|
||||||
case "UptimeKumaEnabled":
|
case model.ConfigKeyUptimeKumaEnabled:
|
||||||
return validateUptimeKumaEnabled(key, trimmed, state)
|
return validateUptimeKumaEnabled(ctx, key, trimmed, state)
|
||||||
case "UptimeKumaUsername":
|
case model.ConfigKeyUptimeKumaUsername:
|
||||||
return validateUptimeKumaUsername(trimmed, state)
|
return validateUptimeKumaUsername(trimmed, state)
|
||||||
case "UptimeKumaUrl":
|
case model.ConfigKeyUptimeKumaURL:
|
||||||
return validateUptimeKumaURL(trimmed)
|
return validateUptimeKumaURL(trimmed)
|
||||||
case "UptimeKumaMonitorScope":
|
case model.ConfigKeyUptimeKumaMonitorScope:
|
||||||
return validateUptimeKumaMonitorScope(trimmed)
|
return validateUptimeKumaMonitorScope(trimmed)
|
||||||
case "UptimeKumaSyncInterval", "UptimeKumaInterval", "UptimeKumaRetryInterval", "UptimeKumaTimeout":
|
case model.ConfigKeyUptimeKumaSyncInterval, model.ConfigKeyUptimeKumaInterval, model.ConfigKeyUptimeKumaRetryInterval, model.ConfigKeyUptimeKumaTimeout:
|
||||||
return validatePositiveIntegerOption(key, trimmed)
|
return validatePositiveIntegerOption(key, trimmed)
|
||||||
case "UptimeKumaRetry":
|
case model.ConfigKeyUptimeKumaRetry:
|
||||||
return validateUptimeKumaRetry(key, trimmed)
|
return validateUptimeKumaRetry(key, trimmed)
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
if err := validateBooleanOption(key, trimmed); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if trimmed != optionValueTrue {
|
if trimmed != optionValueTrue {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
url := strings.TrimSpace(state["UptimeKumaUrl"])
|
url := strings.TrimSpace(state[model.ConfigKeyUptimeKumaURL])
|
||||||
username := strings.TrimSpace(state["UptimeKumaUsername"])
|
username := strings.TrimSpace(state[model.ConfigKeyUptimeKumaUsername])
|
||||||
password := strings.TrimSpace(state["UptimeKumaPassword"])
|
password := strings.TrimSpace(state[model.ConfigKeyUptimeKumaPassword])
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return fmt.Errorf("启用 Uptime Kuma 时地址不能为空")
|
return fmt.Errorf("启用 Uptime Kuma 时地址不能为空")
|
||||||
}
|
}
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return fmt.Errorf("启用 Uptime Kuma 时用户名不能为空")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateUptimeKumaUsername(trimmed string, state map[string]string) error {
|
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 fmt.Errorf("启用 Uptime Kuma 时用户名不能为空")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -173,17 +183,17 @@ func validateUptimeKumaRetry(key, trimmed string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateOptions(options []model.OpenFlareOption) error {
|
func validateOptions(ctx context.Context, options []model.OpenFlareOption) error {
|
||||||
if len(options) == 0 {
|
if len(options) == 0 {
|
||||||
return errors.New(errInvalidParams)
|
return errors.New(errInvalidParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
state := buildOptionValidationState(options)
|
state := buildOptionValidationState(ctx, options)
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
if strings.TrimSpace(option.Key) == "" {
|
if strings.TrimSpace(option.Key) == "" {
|
||||||
return errors.New(errInvalidParams)
|
return errors.New(errInvalidParams)
|
||||||
}
|
}
|
||||||
if err := validateOptionWithState(option, state); err != nil {
|
if err := validateOptionWithState(ctx, option, state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import (
|
|||||||
const (
|
const (
|
||||||
relayStatusUnhealthy = "unhealthy"
|
relayStatusUnhealthy = "unhealthy"
|
||||||
releaseChannelStable = "stable"
|
releaseChannelStable = "stable"
|
||||||
|
|
||||||
|
defaultAgentHeartbeatInterval = 10000 // 默认心跳间隔 10 秒(毫秒)
|
||||||
|
defaultAgentUpdateRepo = "Rain-kl/OpenFlare"
|
||||||
)
|
)
|
||||||
|
|
||||||
func normalizeRelayStatus(status string) string {
|
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.
|
// BuildSettings returns runtime settings shared by relay and flared clients.
|
||||||
func BuildSettings(node *model.OpenFlareNode, updateNow bool, updateChannel, updateTag string) *Settings {
|
func BuildSettings(ctx context.Context, node *model.OpenFlareNode, updateNow bool, updateChannel, updateTag string) *Settings {
|
||||||
model.OptionMapRWMutex.RLock()
|
|
||||||
defer model.OptionMapRWMutex.RUnlock()
|
|
||||||
|
|
||||||
autoUpdate := false
|
autoUpdate := false
|
||||||
if node != nil {
|
if node != nil {
|
||||||
autoUpdate = node.AutoUpdateEnabled
|
autoUpdate = node.AutoUpdateEnabled
|
||||||
@@ -115,11 +115,23 @@ func BuildSettings(node *model.OpenFlareNode, updateNow bool, updateChannel, upd
|
|||||||
if strings.TrimSpace(updateChannel) == "" {
|
if strings.TrimSpace(updateChannel) == "" {
|
||||||
updateChannel = releaseChannelStable
|
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{
|
return &Settings{
|
||||||
HeartbeatInterval: model.AgentHeartbeatInterval,
|
HeartbeatInterval: heartbeatInterval,
|
||||||
WebsocketUpgradeEnabled: model.AgentWebsocketUpgradeEnabled,
|
WebsocketUpgradeEnabled: wsUpgradeEnabled,
|
||||||
AutoUpdate: autoUpdate,
|
AutoUpdate: autoUpdate,
|
||||||
UpdateRepo: model.AgentUpdateRepo,
|
UpdateRepo: updateRepo.Value,
|
||||||
UpdateNow: updateNow,
|
UpdateNow: updateNow,
|
||||||
UpdateChannel: updateChannel,
|
UpdateChannel: updateChannel,
|
||||||
UpdateTag: strings.TrimSpace(updateTag),
|
UpdateTag: strings.TrimSpace(updateTag),
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload Heartbeat
|
|||||||
|
|
||||||
return &HeartbeatResponse{
|
return &HeartbeatResponse{
|
||||||
RelayConfig: buildRelayConfig(ctx, node),
|
RelayConfig: buildRelayConfig(ctx, node),
|
||||||
RelaySettings: BuildSettings(node, updateNow, updateChannel, updateTag),
|
RelaySettings: BuildSettings(ctx, node, updateNow, updateChannel, updateTag),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
"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/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
@@ -28,7 +27,7 @@ func setupRelayTestDB(t *testing.T) func() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, sqliteDB.AutoMigrate(
|
require.NoError(t, sqliteDB.AutoMigrate(
|
||||||
&model.OpenFlareNode{},
|
&model.OpenFlareNode{},
|
||||||
&model.OpenFlareOption{},
|
&model.SystemConfig{},
|
||||||
&model.OpenFlareNodeSystemProfile{},
|
&model.OpenFlareNodeSystemProfile{},
|
||||||
&model.OpenFlareMetricSnapshot{},
|
&model.OpenFlareMetricSnapshot{},
|
||||||
&model.OpenFlareHealthEvent{},
|
&model.OpenFlareHealthEvent{},
|
||||||
@@ -36,14 +35,12 @@ func setupRelayTestDB(t *testing.T) func() {
|
|||||||
))
|
))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
agent.ResetAuthCacheForTest()
|
agent.ResetAuthCacheForTest()
|
||||||
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
||||||
|
|
||||||
return func() {
|
return func() {
|
||||||
resetObservabilityStore()
|
resetObservabilityStore()
|
||||||
db.SetDB(nil)
|
db.SetDB(nil)
|
||||||
option.ResetInitializationForTest()
|
|
||||||
agent.ResetAuthCacheForTest()
|
agent.ResetAuthCacheForTest()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -91,14 +92,20 @@ func CleanupDatabaseObservability(ctx context.Context, input DatabaseCleanupInpu
|
|||||||
|
|
||||||
// RunDatabaseAutoCleanupOnce runs retention-based cleanup for all observability targets.
|
// RunDatabaseAutoCleanupOnce runs retention-based cleanup for all observability targets.
|
||||||
func RunDatabaseAutoCleanupOnce(ctx context.Context, now time.Time) (*DatabaseAutoCleanupSummary, error) {
|
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
|
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))
|
results := make([]DatabaseCleanupResult, 0, len(databaseCleanupTargets))
|
||||||
for _, target := range []string{
|
for _, target := range []string{
|
||||||
DatabaseCleanupTargetAccessLogs,
|
DatabaseCleanupTargetAccessLogs,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -23,6 +24,7 @@ func setupDatabaseCleanupTestDB(t *testing.T) context.Context {
|
|||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, sqliteDB.AutoMigrate(&model.SystemConfig{}))
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore())
|
resetAccessLogStore := model.SetAccessLogStoreForTest(model.NewMemoryAccessLogStore())
|
||||||
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
resetObservabilityStore := model.SetObservabilityStoreForTest(model.NewMemoryObservabilityStore())
|
||||||
@@ -123,14 +125,8 @@ func TestRunDatabaseAutoCleanupOnceDeletesAllObservabilityTargets(t *testing.T)
|
|||||||
RequestCount: 15,
|
RequestCount: 15,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
previousEnabled := model.DatabaseAutoCleanupEnabled
|
require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyDatabaseAutoCleanupEnabled, "true"))
|
||||||
previousRetentionDays := model.DatabaseAutoCleanupRetentionDays
|
require.NoError(t, repository.SaveOrUpdateSystemConfig(ctx, model.ConfigKeyDatabaseAutoCleanupRetentionDays, "1"))
|
||||||
model.DatabaseAutoCleanupEnabled = true
|
|
||||||
model.DatabaseAutoCleanupRetentionDays = 1
|
|
||||||
t.Cleanup(func() {
|
|
||||||
model.DatabaseAutoCleanupEnabled = previousEnabled
|
|
||||||
model.DatabaseAutoCleanupRetentionDays = previousRetentionDays
|
|
||||||
})
|
|
||||||
|
|
||||||
summary, err := RunDatabaseAutoCleanupOnce(ctx, now)
|
summary, err := RunDatabaseAutoCleanupOnce(ctx, now)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -12,15 +12,70 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
const uptimeKumaTagOpenFlare = "OpenFlare"
|
const uptimeKumaTagOpenFlare = "OpenFlare"
|
||||||
|
|
||||||
var isSyncing atomic.Bool
|
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.
|
// SyncToUptimeKuma synchronizes enabled proxy routes to Uptime Kuma monitors.
|
||||||
func SyncToUptimeKuma(ctx context.Context) error {
|
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")
|
return fmt.Errorf("uptime Kuma integration is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,15 +84,21 @@ func SyncToUptimeKuma(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
defer isSyncing.Store(false)
|
defer isSyncing.Store(false)
|
||||||
|
|
||||||
kumaURL, kumaUsername, kumaPassword, err := validateUptimeKumaConfig()
|
// 加载配置
|
||||||
|
config, err := loadKumaConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证配置
|
||||||
|
if err := validateKumaConfig(config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
slog.Info("Starting Uptime Kuma sync process",
|
slog.Info("Starting Uptime Kuma sync process",
|
||||||
"url", kumaURL,
|
"url", config.URL,
|
||||||
"username", kumaUsername,
|
"username", config.Username,
|
||||||
"scope", model.UptimeKumaMonitorScope,
|
"scope", config.MonitorScope,
|
||||||
)
|
)
|
||||||
|
|
||||||
allRoutes, err := model.ListProxyRoutes(ctx)
|
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)
|
return fmt.Errorf("failed to list local proxy routes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedRoutes, err := filterExpectedRoutes(allRoutes)
|
expectedRoutes, err := filterExpectedRoutes(allRoutes, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := connectAndLoginUptimeKuma(kumaURL, kumaUsername, kumaPassword)
|
client, err := connectAndLoginUptimeKuma(config.URL, config.Username, config.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -62,16 +123,16 @@ func SyncToUptimeKuma(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
existingOpenFlareMonitors := filterOpenFlareMonitors(client.GetMonitorList(), openFlareTagID)
|
existingOpenFlareMonitors := filterOpenFlareMonitors(client.GetMonitorList(), openFlareTagID)
|
||||||
expectedSitesMap := syncRouteMonitors(client, expectedRoutes, existingOpenFlareMonitors, openFlareTagID)
|
expectedSitesMap := syncRouteMonitors(client, expectedRoutes, existingOpenFlareMonitors, openFlareTagID, config)
|
||||||
removeStaleMonitors(client, existingOpenFlareMonitors, expectedSitesMap)
|
removeStaleMonitors(client, existingOpenFlareMonitors, expectedSitesMap)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterExpectedRoutes(allRoutes []*model.ProxyRoute) ([]*model.ProxyRoute, error) {
|
func filterExpectedRoutes(allRoutes []*model.ProxyRoute, config *kumaConfig) ([]*model.ProxyRoute, error) {
|
||||||
scope := model.UptimeKumaMonitorScope
|
scope := config.MonitorScope
|
||||||
if scope == "selected" {
|
if scope == "selected" {
|
||||||
selectedList := strings.Split(model.UptimeKumaSelectedSites, ",")
|
selectedList := strings.Split(config.SelectedSites, ",")
|
||||||
selectedMap := make(map[string]bool)
|
selectedMap := make(map[string]bool)
|
||||||
for _, name := range selectedList {
|
for _, name := range selectedList {
|
||||||
trimmedName := strings.TrimSpace(name)
|
trimmedName := strings.TrimSpace(name)
|
||||||
@@ -175,15 +236,15 @@ func routeMonitorURL(route *model.ProxyRoute) (string, error) {
|
|||||||
return "http://" + domain, nil
|
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{
|
payload := map[string]any{
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"name": name,
|
"name": name,
|
||||||
"url": targetURL,
|
"url": targetURL,
|
||||||
"interval": model.UptimeKumaInterval,
|
"interval": config.Interval,
|
||||||
"maxretries": model.UptimeKumaRetry,
|
"maxretries": config.Retry,
|
||||||
"retryInterval": model.UptimeKumaRetryInterval,
|
"retryInterval": config.RetryInterval,
|
||||||
"timeout": model.UptimeKumaTimeout,
|
"timeout": config.Timeout,
|
||||||
"active": true,
|
"active": true,
|
||||||
"resendInterval": 0,
|
"resendInterval": 0,
|
||||||
"expiryNotification": false,
|
"expiryNotification": false,
|
||||||
@@ -198,17 +259,17 @@ func monitorPayload(id int, name, targetURL string) map[string]any {
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
func monitorNeedsUpdate(existing Monitor, targetURL string) bool {
|
func monitorNeedsUpdate(existing Monitor, targetURL string, config *kumaConfig) bool {
|
||||||
return existing.URL != targetURL ||
|
return existing.URL != targetURL ||
|
||||||
existing.Interval != model.UptimeKumaInterval ||
|
existing.Interval != config.Interval ||
|
||||||
existing.MaxRetries != model.UptimeKumaRetry ||
|
existing.MaxRetries != config.Retry ||
|
||||||
existing.RetryInterval != model.UptimeKumaRetryInterval ||
|
existing.RetryInterval != config.RetryInterval ||
|
||||||
existing.Timeout != model.UptimeKumaTimeout
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -238,9 +299,9 @@ func createMonitor(client *SocketIOClient, siteName, targetURL string, openFlare
|
|||||||
return nil
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,17 +14,18 @@ import (
|
|||||||
|
|
||||||
const monitorListWaitTimeout = 5 * time.Second
|
const monitorListWaitTimeout = 5 * time.Second
|
||||||
|
|
||||||
func validateUptimeKumaConfig() (string, string, string, error) {
|
// validateKumaConfig 验证 kumaConfig 配置完整性
|
||||||
kumaURL := strings.TrimSpace(model.UptimeKumaURL)
|
func validateKumaConfig(config *kumaConfig) error {
|
||||||
kumaUsername := strings.TrimSpace(model.UptimeKumaUsername)
|
if strings.TrimSpace(config.URL) == "" {
|
||||||
kumaPassword := strings.TrimSpace(model.UptimeKumaPassword)
|
return fmt.Errorf("uptime Kuma URL is not configured")
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
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) {
|
func connectAndLoginUptimeKuma(kumaURL, kumaUsername, kumaPassword string) (*SocketIOClient, error) {
|
||||||
@@ -68,7 +69,7 @@ func connectAndLoginUptimeKuma(kumaURL, kumaUsername, kumaPassword string) (*Soc
|
|||||||
return client, nil
|
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))
|
expectedSitesMap := make(map[string]bool, len(expectedRoutes))
|
||||||
for _, route := range expectedRoutes {
|
for _, route := range expectedRoutes {
|
||||||
expectedSitesMap[route.SiteName] = true
|
expectedSitesMap[route.SiteName] = true
|
||||||
@@ -80,13 +81,13 @@ func syncRouteMonitors(client *SocketIOClient, expectedRoutes []*model.ProxyRout
|
|||||||
|
|
||||||
existing, exists := existingMonitors[route.SiteName]
|
existing, exists := existingMonitors[route.SiteName]
|
||||||
if !exists {
|
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)
|
slog.Error("Failed to add monitor to Uptime Kuma", "name", route.SiteName, "error", err)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if monitorNeedsUpdate(existing, targetURL) {
|
if monitorNeedsUpdate(existing, targetURL, config) {
|
||||||
if err := updateMonitor(client, existing.ID, route.SiteName, targetURL); err != nil {
|
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)
|
slog.Error("Failed to edit monitor in Uptime Kuma", "name", route.SiteName, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -121,7 +122,7 @@ func setupSyncTestDB(t *testing.T) func() {
|
|||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, sqliteDB.AutoMigrate(&model.ProxyRoute{}))
|
require.NoError(t, sqliteDB.AutoMigrate(&model.ProxyRoute{}, &model.SystemConfig{}))
|
||||||
|
|
||||||
db.SetDB(sqliteDB)
|
db.SetDB(sqliteDB)
|
||||||
return func() {
|
return func() {
|
||||||
@@ -129,41 +130,50 @@ func setupSyncTestDB(t *testing.T) func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func backupUptimeKumaConfig() func() {
|
func backupUptimeKumaConfig(ctx context.Context) func() {
|
||||||
oldEnabled := model.UptimeKumaEnabled
|
// 备份所有 UptimeKuma 相关配置
|
||||||
oldURL := model.UptimeKumaURL
|
configs := []string{
|
||||||
oldUsername := model.UptimeKumaUsername
|
model.ConfigKeyUptimeKumaEnabled,
|
||||||
oldPassword := model.UptimeKumaPassword
|
model.ConfigKeyUptimeKumaURL,
|
||||||
oldScope := model.UptimeKumaMonitorScope
|
model.ConfigKeyUptimeKumaUsername,
|
||||||
oldSelected := model.UptimeKumaSelectedSites
|
model.ConfigKeyUptimeKumaPassword,
|
||||||
oldInterval := model.UptimeKumaInterval
|
model.ConfigKeyUptimeKumaMonitorScope,
|
||||||
oldRetry := model.UptimeKumaRetry
|
model.ConfigKeyUptimeKumaSelectedSites,
|
||||||
oldRetryInterval := model.UptimeKumaRetryInterval
|
model.ConfigKeyUptimeKumaInterval,
|
||||||
oldTimeout := model.UptimeKumaTimeout
|
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() {
|
return func() {
|
||||||
model.UptimeKumaEnabled = oldEnabled
|
// 恢复所有配置
|
||||||
model.UptimeKumaURL = oldURL
|
for key, value := range oldValues {
|
||||||
model.UptimeKumaUsername = oldUsername
|
_ = db.DB(ctx).Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error
|
||||||
model.UptimeKumaPassword = oldPassword
|
}
|
||||||
model.UptimeKumaMonitorScope = oldScope
|
|
||||||
model.UptimeKumaSelectedSites = oldSelected
|
|
||||||
model.UptimeKumaInterval = oldInterval
|
|
||||||
model.UptimeKumaRetry = oldRetry
|
|
||||||
model.UptimeKumaRetryInterval = oldRetryInterval
|
|
||||||
model.UptimeKumaTimeout = oldTimeout
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setTestConfig 设置测试配置的辅助函数(不存在则创建)
|
||||||
|
func setTestConfig(ctx context.Context, key, value string) {
|
||||||
|
_ = repository.SaveOrUpdateSystemConfig(ctx, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSyncToUptimeKumaDisabled(t *testing.T) {
|
func TestSyncToUptimeKumaDisabled(t *testing.T) {
|
||||||
cleanup := setupSyncTestDB(t)
|
cleanup := setupSyncTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
restore := backupUptimeKumaConfig()
|
ctx := context.Background()
|
||||||
|
restore := backupUptimeKumaConfig(ctx)
|
||||||
defer restore()
|
defer restore()
|
||||||
|
|
||||||
model.UptimeKumaEnabled = false
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "false")
|
||||||
|
|
||||||
err := SyncToUptimeKuma(context.Background())
|
err := SyncToUptimeKuma(ctx)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "disabled")
|
assert.Contains(t, err.Error(), "disabled")
|
||||||
}
|
}
|
||||||
@@ -171,9 +181,9 @@ func TestSyncToUptimeKumaDisabled(t *testing.T) {
|
|||||||
func TestSyncToUptimeKumaSuccess(t *testing.T) {
|
func TestSyncToUptimeKumaSuccess(t *testing.T) {
|
||||||
cleanup := setupSyncTestDB(t)
|
cleanup := setupSyncTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
restore := backupUptimeKumaConfig()
|
|
||||||
defer restore()
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
restore := backupUptimeKumaConfig(ctx)
|
||||||
|
defer restore()
|
||||||
|
|
||||||
require.NoError(t, db.DB(ctx).Where("1 = 1").Delete(&model.ProxyRoute{}).Error)
|
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)
|
server := httptest.NewServer(mockSrv)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
model.UptimeKumaEnabled = true
|
// 设置测试配置
|
||||||
model.UptimeKumaURL = server.URL
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "true")
|
||||||
model.UptimeKumaUsername = "admin"
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaURL, server.URL)
|
||||||
model.UptimeKumaPassword = "password"
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaUsername, "admin")
|
||||||
model.UptimeKumaMonitorScope = "all"
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaPassword, "password")
|
||||||
model.UptimeKumaInterval = 60
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaMonitorScope, "all")
|
||||||
model.UptimeKumaRetry = 0
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaInterval, "60")
|
||||||
model.UptimeKumaRetryInterval = 60
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaRetry, "0")
|
||||||
model.UptimeKumaTimeout = 48
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaRetryInterval, "60")
|
||||||
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaTimeout, "48")
|
||||||
|
|
||||||
require.NoError(t, SyncToUptimeKuma(ctx))
|
require.NoError(t, SyncToUptimeKuma(ctx))
|
||||||
|
|
||||||
@@ -282,9 +293,9 @@ func TestSyncToUptimeKumaSuccess(t *testing.T) {
|
|||||||
func TestSyncToUptimeKumaSelectedScope(t *testing.T) {
|
func TestSyncToUptimeKumaSelectedScope(t *testing.T) {
|
||||||
cleanup := setupSyncTestDB(t)
|
cleanup := setupSyncTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
restore := backupUptimeKumaConfig()
|
|
||||||
defer restore()
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
restore := backupUptimeKumaConfig(ctx)
|
||||||
|
defer restore()
|
||||||
|
|
||||||
require.NoError(t, db.DB(ctx).Where("1 = 1").Delete(&model.ProxyRoute{}).Error)
|
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)
|
server := httptest.NewServer(mockSrv)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
model.UptimeKumaEnabled = true
|
// 设置测试配置
|
||||||
model.UptimeKumaURL = server.URL
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "true")
|
||||||
model.UptimeKumaUsername = "admin"
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaURL, server.URL)
|
||||||
model.UptimeKumaPassword = "password"
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaUsername, "admin")
|
||||||
model.UptimeKumaMonitorScope = "selected"
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaPassword, "password")
|
||||||
model.UptimeKumaSelectedSites = "site-a"
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaMonitorScope, "selected")
|
||||||
|
setTestConfig(ctx, model.ConfigKeyUptimeKumaSelectedSites, "site-a")
|
||||||
|
|
||||||
require.NoError(t, SyncToUptimeKuma(ctx))
|
require.NoError(t, SyncToUptimeKuma(ctx))
|
||||||
|
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ func TestMigrateUpgradesLegacyDatabaseAndPreservesData(t *testing.T) {
|
|||||||
// Legacy normal user
|
// Legacy normal user
|
||||||
`INSERT INTO users (id, username, password, display_name, role, status, email)
|
`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');`,
|
VALUES (2, 'jack', 'hashed_pass_456', 'Normal User jack', 10, 1, 'jack@example.com');`,
|
||||||
// Legacy config options
|
// Legacy config options(GeoIPProvider 会被迁移到 w_system_configs.geoip_provider)
|
||||||
`INSERT INTO options (key, value) VALUES ('SystemName', 'CustomOpenFlare');`,
|
`INSERT INTO options (key, value) VALUES ('GeoIPProvider', 'ip-api');`,
|
||||||
// Legacy origin
|
// Legacy origin
|
||||||
`INSERT INTO origins (id, name, address, remark, created_at, updated_at)
|
`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');`,
|
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)
|
t.Errorf("jack user migrated incorrectly: %+v", jackUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify options migrated to of_options
|
// Verify legacy option migrated all the way to w_system_configs(of_options 已被删除)
|
||||||
var systemNameVal string
|
var geoIPProviderVal string
|
||||||
if err := sqliteDB.Table("of_options").Where("key = ?", "SystemName").Select("value").Scan(&systemNameVal).Error; err != nil {
|
if err := sqliteDB.Table("w_system_configs").Where("key = ?", "geoip_provider").Select("value").Scan(&geoIPProviderVal).Error; err != nil {
|
||||||
t.Fatalf("query SystemName failed: %v", err)
|
t.Fatalf("query geoip_provider failed: %v", err)
|
||||||
}
|
}
|
||||||
if systemNameVal != "CustomOpenFlare" {
|
if geoIPProviderVal != "ip-api" {
|
||||||
t.Errorf("SystemName value incorrect: got %s, want CustomOpenFlare", systemNameVal)
|
t.Errorf("geoip_provider value incorrect: got %s, want ip-api", geoIPProviderVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify origin migrated to of_origins
|
// Verify origin migrated to of_origins
|
||||||
|
|||||||
+347
@@ -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'
|
||||||
|
);
|
||||||
@@ -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 ''
|
||||||
|
);
|
||||||
+291
@@ -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'
|
||||||
|
);
|
||||||
@@ -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 ''
|
||||||
|
);
|
||||||
@@ -7,7 +7,9 @@ package migrator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
@@ -79,6 +81,9 @@ func Migrate() {
|
|||||||
if err := goose.SetDialect(gooseDialect()); err != nil {
|
if err := goose.SetDialect(gooseDialect()); err != nil {
|
||||||
log.Fatalf("[%s] set goose dialect failed: %v\n", dbType(), err)
|
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 {
|
if err := goose.Up(sqlDB, migrationDir()); err != nil {
|
||||||
log.Fatalf("[%s] goose migrate failed: %v\n", dbType(), err)
|
log.Fatalf("[%s] goose migrate failed: %v\n", dbType(), err)
|
||||||
}
|
}
|
||||||
@@ -88,6 +93,37 @@ func Migrate() {
|
|||||||
log.Printf("[%s] goose migrate success\n", dbType())
|
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() {
|
func clearSystemConfigCache() {
|
||||||
if err := repository.InvalidateAllSystemConfigCaches(context.Background()); err != nil {
|
if err := repository.InvalidateAllSystemConfigCaches(context.Background()); err != nil {
|
||||||
log.Printf("[%s] clear system config cache failed: %v\n", dbType(), err)
|
log.Printf("[%s] clear system config cache failed: %v\n", dbType(), err)
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const expectedMigratedSystemConfigCount = 32
|
// expectedMigratedSystemConfigCount 包含初始 32 项系统配置,以及 202606220004
|
||||||
|
// 从 of_options 迁移过来的 48 项业务配置(OpenFlare/UptimeKuma/OpenResty)。
|
||||||
|
const expectedMigratedSystemConfigCount = 80
|
||||||
|
|
||||||
func TestMigrateInitializesSQLiteDatabase(t *testing.T) {
|
func TestMigrateInitializesSQLiteDatabase(t *testing.T) {
|
||||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||||
|
|||||||
@@ -21,12 +21,10 @@ const (
|
|||||||
|
|
||||||
// Shared GORM column name constants used across model package.
|
// Shared GORM column name constants used across model package.
|
||||||
const (
|
const (
|
||||||
colName = "name"
|
colName = "name"
|
||||||
colEnabled = "enabled"
|
colEnabled = "enabled"
|
||||||
colRemark = "remark"
|
colRemark = "remark"
|
||||||
tableOfProxyRoutes = "of_proxy_routes"
|
tableOfProxyRoutes = "of_proxy_routes"
|
||||||
optionKeyUptimeKumaPassword = "UptimeKumaPassword"
|
|
||||||
optionValueTrue = "true"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var authSourceNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$`)
|
var authSourceNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$`)
|
||||||
|
|||||||
@@ -3,105 +3,22 @@
|
|||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import "time"
|
||||||
"context"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
// OpenFlareOption is the key/value DTO used by the OpenFlare option API surface.
|
||||||
"gorm.io/gorm"
|
// Key 与 w_system_configs.key 一致(snake_case)。此类型仅作为
|
||||||
)
|
// /api/v1/d/option 接口的请求/响应载体,不再映射到独立的 of_options 表。
|
||||||
|
|
||||||
// OpenFlareOption stores a hot-reloadable OpenFlare system option.
|
|
||||||
type OpenFlareOption struct {
|
type OpenFlareOption struct {
|
||||||
Key string `json:"key" gorm:"column:key;primaryKey;size:128;not null"`
|
Key string `json:"key"`
|
||||||
Value string `json:"value" gorm:"type:text;not null"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the OpenFlare options table name.
|
// StartTime records process start time (seconds) for the public /status endpoint.
|
||||||
func (OpenFlareOption) TableName() string {
|
var StartTime = time.Now().Unix()
|
||||||
return "of_options"
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptionMap holds the in-memory option snapshot for fast reads and hot reload.
|
// DefaultOpenRestyMainConfigTemplate 是 OpenResty 主配置模板的内置默认值。
|
||||||
var (
|
// 当 SystemConfig 中缺失 openresty_main_config_template 时作为兜底使用。
|
||||||
OptionMap map[string]string
|
const DefaultOpenRestyMainConfigTemplate = `# This file is generated by OpenFlare. Do not edit manually.
|
||||||
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.
|
|
||||||
user openflare;
|
user openflare;
|
||||||
worker_processes {{OpenRestyWorkerProcesses}};
|
worker_processes {{OpenRestyWorkerProcesses}};
|
||||||
worker_rlimit_nofile {{OpenRestyWorkerRlimitNofile}};
|
worker_rlimit_nofile {{OpenRestyWorkerRlimitNofile}};
|
||||||
@@ -147,171 +64,3 @@ http {
|
|||||||
{{OpenRestyResolverDirective}}{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}};
|
{{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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,8 +37,70 @@ const (
|
|||||||
ConfigKeyLoginSessionTTLHours = "login_session_ttl_hours" // 登录会话过期时间 (小时,0表示浏览器关闭后自动退出登录,-1表示永不过期)
|
ConfigKeyLoginSessionTTLHours = "login_session_ttl_hours" // 登录会话过期时间 (小时,0表示浏览器关闭后自动退出登录,-1表示永不过期)
|
||||||
ConfigKeyUpdateUpstreamRepository = "update_upstream_repository" // GitHub Actions Release 上游仓库
|
ConfigKeyUpdateUpstreamRepository = "update_upstream_repository" // GitHub Actions Release 上游仓库
|
||||||
ConfigKeyStorageConfig = "storage_config" // 文件存储配置 (JSON)
|
ConfigKeyStorageConfig = "storage_config" // 文件存储配置 (JSON)
|
||||||
ConfigKeyRelayFRPSWebUIEnabled = "relay_frps_web_ui_enabled" // 是否启用 FRPS 内置 Web 界面
|
ConfigKeyRelayFRPSWebUIEnabled = "relay_frps_web_ui_enabled" // 是否启用 FRPS 内置 Web 界面
|
||||||
ConfigKeyRelayFRPSWebUIPort = "relay_frps_web_ui_port" // 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 (
|
const (
|
||||||
|
|||||||
Reference in New Issue
Block a user