feat(channel-monitor): add feature switch settings + fix extra_models save
Settings:
- New "功能开关" tab between 通用设置 and 安全与认证
- ChannelMonitorEnabled toggle: runner skips scheduling when false,
user-facing list returns empty
- ChannelMonitorDefaultIntervalSeconds (15-3600): pre-fills interval
when creating a new monitor; each monitor can still override
Bug fix:
- ModelTagInput now commits pending input on blur, not just Enter/Tab.
Previously clicking "save" with an un-Enter'd extra model would drop
the value (DB stored extra_models=[] even when user typed entries).
Backend:
- domain_constants: SettingKeyChannelMonitor{Enabled,DefaultIntervalSeconds}
- SettingService.GetChannelMonitorRuntime: lightweight getter used by
runner tick + user handler per-request (fail-open on DB error)
- Runner tickDueChecks: bails early when feature disabled
- ChannelMonitorUserHandler: checks feature flag before serving
- Comment on runner doc: scheduler state is implicit (every tick re-reads
ListEnabled from DB), so CRUD ops on monitors self-maintain the schedule
Bump VERSION to 0.1.114.25
This commit is contained in:
@@ -217,8 +217,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
channelMonitorRepository := repository.NewChannelMonitorRepository(client, sqlDB)
|
||||
channelMonitorService := service.ProvideChannelMonitorService(channelMonitorRepository, secretEncryptor)
|
||||
channelMonitorHandler := admin.NewChannelMonitorHandler(channelMonitorService)
|
||||
channelMonitorUserHandler := handler.NewChannelMonitorUserHandler(channelMonitorService)
|
||||
channelMonitorRunner := service.ProvideChannelMonitorRunner(channelMonitorService)
|
||||
channelMonitorUserHandler := handler.NewChannelMonitorUserHandler(channelMonitorService, settingService)
|
||||
channelMonitorRunner := service.ProvideChannelMonitorRunner(channelMonitorService, settingService)
|
||||
_ = channelMonitorRunner
|
||||
registry := payment.ProvideRegistry()
|
||||
encryptionKey, err := payment.ProvideEncryptionKey(configConfig)
|
||||
|
||||
@@ -235,6 +235,9 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
PaymentCancelRateLimitWindow: paymentCfg.CancelRateLimitWindow,
|
||||
PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit,
|
||||
PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode,
|
||||
|
||||
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
||||
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
|
||||
}
|
||||
response.Success(c, systemSettingsResponseData(payload, authSourceDefaults))
|
||||
}
|
||||
@@ -425,6 +428,10 @@ type UpdateSettingsRequest struct {
|
||||
PaymentCancelRateLimitWindow *int `json:"payment_cancel_rate_limit_window"`
|
||||
PaymentCancelRateLimitUnit *string `json:"payment_cancel_rate_limit_unit"`
|
||||
PaymentCancelRateLimitMode *string `json:"payment_cancel_rate_limit_window_mode"`
|
||||
|
||||
// Channel Monitor feature switch
|
||||
ChannelMonitorEnabled *bool `json:"channel_monitor_enabled"`
|
||||
ChannelMonitorDefaultIntervalSeconds *int `json:"channel_monitor_default_interval_seconds"`
|
||||
}
|
||||
|
||||
// UpdateSettings 更新系统设置
|
||||
@@ -1219,6 +1226,18 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
return previousSettings.AccountQuotaNotifyEmails
|
||||
}(),
|
||||
ChannelMonitorEnabled: func() bool {
|
||||
if req.ChannelMonitorEnabled != nil {
|
||||
return *req.ChannelMonitorEnabled
|
||||
}
|
||||
return previousSettings.ChannelMonitorEnabled
|
||||
}(),
|
||||
ChannelMonitorDefaultIntervalSeconds: func() int {
|
||||
if req.ChannelMonitorDefaultIntervalSeconds != nil {
|
||||
return *req.ChannelMonitorDefaultIntervalSeconds
|
||||
}
|
||||
return previousSettings.ChannelMonitorDefaultIntervalSeconds
|
||||
}(),
|
||||
}
|
||||
|
||||
authSourceDefaults := &service.AuthSourceDefaultSettings{
|
||||
@@ -1449,6 +1468,9 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
PaymentCancelRateLimitWindow: updatedPaymentCfg.CancelRateLimitWindow,
|
||||
PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit,
|
||||
PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode,
|
||||
|
||||
ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled,
|
||||
ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds,
|
||||
}
|
||||
response.Success(c, systemSettingsResponseData(payload, updatedAuthSourceDefaults))
|
||||
}
|
||||
@@ -1805,6 +1827,12 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if !equalNotifyEmailEntries(before.AccountQuotaNotifyEmails, after.AccountQuotaNotifyEmails) {
|
||||
changed = append(changed, "account_quota_notify_emails")
|
||||
}
|
||||
if before.ChannelMonitorEnabled != after.ChannelMonitorEnabled {
|
||||
changed = append(changed, "channel_monitor_enabled")
|
||||
}
|
||||
if before.ChannelMonitorDefaultIntervalSeconds != after.ChannelMonitorDefaultIntervalSeconds {
|
||||
changed = append(changed, "channel_monitor_default_interval_seconds")
|
||||
}
|
||||
changed = appendAuthSourceDefaultChanges(changed, beforeAuthSourceDefaults, afterAuthSourceDefaults)
|
||||
return changed
|
||||
}
|
||||
|
||||
@@ -14,11 +14,28 @@ import (
|
||||
// ChannelMonitorUserHandler 渠道监控用户只读 handler。
|
||||
type ChannelMonitorUserHandler struct {
|
||||
monitorService *service.ChannelMonitorService
|
||||
settingService *service.SettingService
|
||||
}
|
||||
|
||||
// NewChannelMonitorUserHandler 创建 handler。
|
||||
func NewChannelMonitorUserHandler(monitorService *service.ChannelMonitorService) *ChannelMonitorUserHandler {
|
||||
return &ChannelMonitorUserHandler{monitorService: monitorService}
|
||||
// settingService 用于每次请求前读取功能开关;关闭时 List/GetStatus 直接返回空/404。
|
||||
func NewChannelMonitorUserHandler(
|
||||
monitorService *service.ChannelMonitorService,
|
||||
settingService *service.SettingService,
|
||||
) *ChannelMonitorUserHandler {
|
||||
return &ChannelMonitorUserHandler{
|
||||
monitorService: monitorService,
|
||||
settingService: settingService,
|
||||
}
|
||||
}
|
||||
|
||||
// featureEnabled 返回当前渠道监控功能是否开启。
|
||||
// settingService 为 nil(测试场景)视为启用。
|
||||
func (h *ChannelMonitorUserHandler) featureEnabled(c *gin.Context) bool {
|
||||
if h.settingService == nil {
|
||||
return true
|
||||
}
|
||||
return h.settingService.GetChannelMonitorRuntime(c.Request.Context()).Enabled
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
@@ -123,6 +140,10 @@ func userMonitorDetailToResponse(d *service.UserMonitorDetail) *channelMonitorUs
|
||||
|
||||
// List GET /api/v1/channel-monitors
|
||||
func (h *ChannelMonitorUserHandler) List(c *gin.Context) {
|
||||
if !h.featureEnabled(c) {
|
||||
response.Success(c, gin.H{"items": []channelMonitorUserListItem{}})
|
||||
return
|
||||
}
|
||||
views, err := h.monitorService.ListUserView(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
@@ -137,6 +158,10 @@ func (h *ChannelMonitorUserHandler) List(c *gin.Context) {
|
||||
|
||||
// GetStatus GET /api/v1/channel-monitors/:id/status
|
||||
func (h *ChannelMonitorUserHandler) GetStatus(c *gin.Context) {
|
||||
if !h.featureEnabled(c) {
|
||||
response.ErrorFrom(c, service.ErrChannelMonitorNotFound)
|
||||
return
|
||||
}
|
||||
// 复用 admin.ParseChannelMonitorID 保持错误码与日志一致。
|
||||
id, ok := admin.ParseChannelMonitorID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -183,6 +183,10 @@ type SystemSettings struct {
|
||||
BalanceLowNotifyRechargeURL string `json:"balance_low_notify_recharge_url"`
|
||||
AccountQuotaNotifyEnabled bool `json:"account_quota_notify_enabled"`
|
||||
AccountQuotaNotifyEmails []NotifyEmailEntry `json:"account_quota_notify_emails"`
|
||||
|
||||
// Channel Monitor feature switch
|
||||
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
||||
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
||||
}
|
||||
|
||||
type DefaultSubscriptionSetting struct {
|
||||
@@ -230,6 +234,9 @@ type PublicSettings struct {
|
||||
AccountQuotaNotifyEnabled bool `json:"account_quota_notify_enabled"`
|
||||
BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"`
|
||||
BalanceLowNotifyRechargeURL string `json:"balance_low_notify_recharge_url"`
|
||||
|
||||
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
||||
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
||||
}
|
||||
|
||||
// OverloadCooldownSettings 529过载冷却配置 DTO
|
||||
|
||||
@@ -70,5 +70,8 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
||||
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
|
||||
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
|
||||
BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL,
|
||||
|
||||
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
||||
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,8 +18,13 @@ import (
|
||||
// - Stop 时优雅关闭:池 drain + ticker.Stop + wg.Wait
|
||||
//
|
||||
// 不引入 cron 库;清理调度通过"每小时检查时间"实现,足够 MVP。
|
||||
//
|
||||
// 定时任务维护:删除/创建/编辑 monitor 无需显式 reload,每个 tick 都会重新查 DB
|
||||
// (ListEnabled + listDueForCheck),新 monitor 的 LastCheckedAt 为 nil 天然立即到期,
|
||||
// 被删除的 monitor 自然不再返回,interval 变化下次 tick 自动按新值判定。
|
||||
type ChannelMonitorRunner struct {
|
||||
svc *ChannelMonitorService
|
||||
svc *ChannelMonitorService
|
||||
settingService *SettingService
|
||||
|
||||
pool pond.Pool
|
||||
stopCh chan struct{}
|
||||
@@ -37,11 +42,13 @@ type ChannelMonitorRunner struct {
|
||||
}
|
||||
|
||||
// NewChannelMonitorRunner 构造调度器。Start 在 wire 中调用。
|
||||
func NewChannelMonitorRunner(svc *ChannelMonitorService) *ChannelMonitorRunner {
|
||||
// settingService 用于在每次 tick 前读取功能开关;传 nil 时视为总是启用(兼容测试)。
|
||||
func NewChannelMonitorRunner(svc *ChannelMonitorService, settingService *SettingService) *ChannelMonitorRunner {
|
||||
return &ChannelMonitorRunner{
|
||||
svc: svc,
|
||||
stopCh: make(chan struct{}),
|
||||
inFlight: make(map[int64]struct{}),
|
||||
svc: svc,
|
||||
settingService: settingService,
|
||||
stopCh: make(chan struct{}),
|
||||
inFlight: make(map[int64]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,10 +100,15 @@ func (r *ChannelMonitorRunner) dueCheckLoop() {
|
||||
// tickDueChecks 一次扫描:查询到期监控并逐个提交到池。
|
||||
// 已在执行的 monitor 会被跳过(防止单次检测耗时 > interval 时重复调度)。
|
||||
// 池满时使用 TrySubmit 跳过(不能阻塞 ticker),同时立即释放已占用的 inFlight 槽。
|
||||
// 当功能开关关闭时直接返回——管理员可以动态禁用模块,runner 不会拉取 DB。
|
||||
func (r *ChannelMonitorRunner) tickDueChecks() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), monitorListDueTimeout)
|
||||
defer cancel()
|
||||
|
||||
if r.settingService != nil && !r.settingService.GetChannelMonitorRuntime(ctx).Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
due, err := r.svc.listDueForCheck(ctx)
|
||||
if err != nil {
|
||||
slog.Warn("channel_monitor: list due failed", "error", err)
|
||||
|
||||
@@ -242,6 +242,18 @@ const (
|
||||
// SettingKeyOpsRuntimeLogConfig stores JSON config for runtime log settings.
|
||||
SettingKeyOpsRuntimeLogConfig = "ops_runtime_log_config"
|
||||
|
||||
// =========================
|
||||
// Channel Monitor (渠道监控)
|
||||
// =========================
|
||||
|
||||
// SettingKeyChannelMonitorEnabled is a DB-backed soft switch for the channel monitor feature.
|
||||
// When false: runner skips scheduling and user-facing endpoints return an empty list.
|
||||
SettingKeyChannelMonitorEnabled = "channel_monitor_enabled"
|
||||
|
||||
// SettingKeyChannelMonitorDefaultIntervalSeconds controls the default interval (seconds)
|
||||
// pre-filled when creating a new channel monitor from the admin UI. Range: [15, 3600].
|
||||
SettingKeyChannelMonitorDefaultIntervalSeconds = "channel_monitor_default_interval_seconds"
|
||||
|
||||
// =========================
|
||||
// Overload Cooldown (529)
|
||||
// =========================
|
||||
|
||||
@@ -450,6 +450,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
||||
SettingKeyBalanceLowNotifyThreshold,
|
||||
SettingKeyBalanceLowNotifyRechargeURL,
|
||||
SettingKeyAccountQuotaNotifyEnabled,
|
||||
SettingKeyChannelMonitorEnabled,
|
||||
SettingKeyChannelMonitorDefaultIntervalSeconds,
|
||||
}
|
||||
|
||||
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
||||
@@ -532,9 +534,67 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
||||
AccountQuotaNotifyEnabled: settings[SettingKeyAccountQuotaNotifyEnabled] == "true",
|
||||
BalanceLowNotifyThreshold: balanceLowNotifyThreshold,
|
||||
BalanceLowNotifyRechargeURL: settings[SettingKeyBalanceLowNotifyRechargeURL],
|
||||
|
||||
ChannelMonitorEnabled: !isFalseSettingValue(settings[SettingKeyChannelMonitorEnabled]),
|
||||
ChannelMonitorDefaultIntervalSeconds: parseChannelMonitorInterval(settings[SettingKeyChannelMonitorDefaultIntervalSeconds]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// channelMonitorIntervalMin / channelMonitorIntervalMax bound the default interval
|
||||
// (mirrors the monitor-level constraint but lives here so setting_service stays decoupled).
|
||||
const (
|
||||
channelMonitorIntervalMin = 15
|
||||
channelMonitorIntervalMax = 3600
|
||||
channelMonitorIntervalFallback = 60
|
||||
)
|
||||
|
||||
// parseChannelMonitorInterval parses the stored string and clamps to [15, 3600].
|
||||
// Empty / invalid input falls back to channelMonitorIntervalFallback.
|
||||
func parseChannelMonitorInterval(raw string) int {
|
||||
v, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return channelMonitorIntervalFallback
|
||||
}
|
||||
return clampChannelMonitorInterval(v)
|
||||
}
|
||||
|
||||
// clampChannelMonitorInterval clamps v to the allowed range. 0 means "not provided".
|
||||
func clampChannelMonitorInterval(v int) int {
|
||||
if v <= 0 {
|
||||
return 0
|
||||
}
|
||||
if v < channelMonitorIntervalMin {
|
||||
return channelMonitorIntervalMin
|
||||
}
|
||||
if v > channelMonitorIntervalMax {
|
||||
return channelMonitorIntervalMax
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ChannelMonitorRuntime is the lightweight view of the channel monitor feature
|
||||
// consumed by the runner and user-facing handlers.
|
||||
type ChannelMonitorRuntime struct {
|
||||
Enabled bool
|
||||
DefaultIntervalSeconds int
|
||||
}
|
||||
|
||||
// GetChannelMonitorRuntime reads the channel monitor feature flags directly from
|
||||
// the settings store. Fail-open: on error returns Enabled=true with the default interval.
|
||||
func (s *SettingService) GetChannelMonitorRuntime(ctx context.Context) ChannelMonitorRuntime {
|
||||
vals, err := s.settingRepo.GetMultiple(ctx, []string{
|
||||
SettingKeyChannelMonitorEnabled,
|
||||
SettingKeyChannelMonitorDefaultIntervalSeconds,
|
||||
})
|
||||
if err != nil {
|
||||
return ChannelMonitorRuntime{Enabled: true, DefaultIntervalSeconds: channelMonitorIntervalFallback}
|
||||
}
|
||||
return ChannelMonitorRuntime{
|
||||
Enabled: !isFalseSettingValue(vals[SettingKeyChannelMonitorEnabled]),
|
||||
DefaultIntervalSeconds: parseChannelMonitorInterval(vals[SettingKeyChannelMonitorDefaultIntervalSeconds]),
|
||||
}
|
||||
}
|
||||
|
||||
// SetOnUpdateCallback sets a callback function to be called when settings are updated
|
||||
// This is used for cache invalidation (e.g., HTML cache in frontend server)
|
||||
func (s *SettingService) SetOnUpdateCallback(callback func()) {
|
||||
@@ -1085,6 +1145,12 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
||||
updates[SettingKeyOpsMetricsIntervalSeconds] = strconv.Itoa(settings.OpsMetricsIntervalSeconds)
|
||||
}
|
||||
|
||||
// Channel monitor feature switch
|
||||
updates[SettingKeyChannelMonitorEnabled] = strconv.FormatBool(settings.ChannelMonitorEnabled)
|
||||
if v := clampChannelMonitorInterval(settings.ChannelMonitorDefaultIntervalSeconds); v > 0 {
|
||||
updates[SettingKeyChannelMonitorDefaultIntervalSeconds] = strconv.Itoa(v)
|
||||
}
|
||||
|
||||
// Claude Code version check
|
||||
updates[SettingKeyMinClaudeCodeVersion] = settings.MinClaudeCodeVersion
|
||||
updates[SettingKeyMaxClaudeCodeVersion] = settings.MaxClaudeCodeVersion
|
||||
@@ -1630,6 +1696,10 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
||||
SettingKeyOpsQueryModeDefault: "auto",
|
||||
SettingKeyOpsMetricsIntervalSeconds: "60",
|
||||
|
||||
// Channel monitor defaults (enabled, 60s)
|
||||
SettingKeyChannelMonitorEnabled: "true",
|
||||
SettingKeyChannelMonitorDefaultIntervalSeconds: "60",
|
||||
|
||||
// Claude Code version check (default: empty = disabled)
|
||||
SettingKeyMinClaudeCodeVersion: "",
|
||||
SettingKeyMaxClaudeCodeVersion: "",
|
||||
@@ -1932,6 +2002,12 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
}
|
||||
}
|
||||
|
||||
// Channel monitor feature (default: enabled, 60s)
|
||||
result.ChannelMonitorEnabled = !isFalseSettingValue(settings[SettingKeyChannelMonitorEnabled])
|
||||
result.ChannelMonitorDefaultIntervalSeconds = parseChannelMonitorInterval(
|
||||
settings[SettingKeyChannelMonitorDefaultIntervalSeconds],
|
||||
)
|
||||
|
||||
// Claude Code version check
|
||||
result.MinClaudeCodeVersion = settings[SettingKeyMinClaudeCodeVersion]
|
||||
result.MaxClaudeCodeVersion = settings[SettingKeyMaxClaudeCodeVersion]
|
||||
|
||||
@@ -125,6 +125,10 @@ type SystemSettings struct {
|
||||
OpsQueryModeDefault string
|
||||
OpsMetricsIntervalSeconds int
|
||||
|
||||
// Channel Monitor feature
|
||||
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
||||
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
||||
|
||||
// Claude Code version check
|
||||
MinClaudeCodeVersion string
|
||||
MaxClaudeCodeVersion string
|
||||
@@ -209,6 +213,10 @@ type PublicSettings struct {
|
||||
AccountQuotaNotifyEnabled bool
|
||||
BalanceLowNotifyThreshold float64
|
||||
BalanceLowNotifyRechargeURL string
|
||||
|
||||
// Channel Monitor feature
|
||||
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
||||
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
||||
}
|
||||
|
||||
type WeChatConnectOAuthConfig struct {
|
||||
|
||||
@@ -500,8 +500,9 @@ func ProvideChannelMonitorService(
|
||||
|
||||
// ProvideChannelMonitorRunner 创建并启动渠道监控调度器。
|
||||
// Runner.Stop 由 cleanup function 调用。
|
||||
func ProvideChannelMonitorRunner(svc *ChannelMonitorService) *ChannelMonitorRunner {
|
||||
r := NewChannelMonitorRunner(svc)
|
||||
// settingService 用于 runner 每个 tick 读取功能开关。
|
||||
func ProvideChannelMonitorRunner(svc *ChannelMonitorService, settingService *SettingService) *ChannelMonitorRunner {
|
||||
r := NewChannelMonitorRunner(svc, settingService)
|
||||
r.Start()
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -469,6 +469,10 @@ export interface SystemSettings {
|
||||
balance_low_notify_recharge_url: string;
|
||||
account_quota_notify_enabled: boolean;
|
||||
account_quota_notify_emails: NotifyEmailEntry[];
|
||||
|
||||
// Channel Monitor feature switch
|
||||
channel_monitor_enabled: boolean;
|
||||
channel_monitor_default_interval_seconds: number;
|
||||
}
|
||||
|
||||
export interface UpdateSettingsRequest {
|
||||
@@ -618,6 +622,10 @@ export interface UpdateSettingsRequest {
|
||||
balance_low_notify_recharge_url?: string;
|
||||
account_quota_notify_enabled?: boolean;
|
||||
account_quota_notify_emails?: NotifyEmailEntry[];
|
||||
|
||||
// Channel Monitor feature switch
|
||||
channel_monitor_enabled?: boolean;
|
||||
channel_monitor_default_interval_seconds?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
@keydown.tab.prevent="addModel"
|
||||
@keydown.delete="handleBackspace"
|
||||
@paste="handlePaste"
|
||||
@blur="addModel"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-400">
|
||||
|
||||
@@ -143,6 +143,13 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// System-configured default interval for new monitors. Falls back to the static
|
||||
// constant when public settings haven't loaded yet or store the legacy 0 value.
|
||||
const systemDefaultInterval = computed<number>(() => {
|
||||
const configured = appStore.cachedPublicSettings?.channel_monitor_default_interval_seconds
|
||||
return configured && configured > 0 ? configured : DEFAULT_INTERVAL_SECONDS
|
||||
})
|
||||
|
||||
// editing is true when we have an existing monitor
|
||||
const editing = computed<ChannelMonitor | null>(() => props.monitor)
|
||||
|
||||
@@ -173,7 +180,7 @@ const form = reactive<MonitorForm>({
|
||||
primary_model: '',
|
||||
extra_models: [],
|
||||
group_name: '',
|
||||
interval_seconds: DEFAULT_INTERVAL_SECONDS,
|
||||
interval_seconds: systemDefaultInterval.value,
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
@@ -191,7 +198,7 @@ function resetForm() {
|
||||
form.primary_model = ''
|
||||
form.extra_models = []
|
||||
form.group_name = ''
|
||||
form.interval_seconds = DEFAULT_INTERVAL_SECONDS
|
||||
form.interval_seconds = systemDefaultInterval.value
|
||||
form.enabled = true
|
||||
}
|
||||
|
||||
@@ -203,7 +210,7 @@ function loadFromMonitor(m: ChannelMonitor) {
|
||||
form.primary_model = m.primary_model
|
||||
form.extra_models = [...(m.extra_models || [])]
|
||||
form.group_name = m.group_name || ''
|
||||
form.interval_seconds = m.interval_seconds || DEFAULT_INTERVAL_SECONDS
|
||||
form.interval_seconds = m.interval_seconds || systemDefaultInterval.value
|
||||
form.enabled = m.enabled
|
||||
}
|
||||
|
||||
|
||||
@@ -4530,6 +4530,7 @@ export default {
|
||||
description: 'Manage registration, email verification, default values, and SMTP settings',
|
||||
tabs: {
|
||||
general: 'General',
|
||||
features: 'Feature Switches',
|
||||
security: 'Security',
|
||||
users: 'Users',
|
||||
gateway: 'Gateway',
|
||||
@@ -4537,6 +4538,16 @@ export default {
|
||||
backup: 'Backup',
|
||||
payment: 'Payment',
|
||||
},
|
||||
features: {
|
||||
channelMonitor: {
|
||||
title: 'Channel Monitor',
|
||||
description: 'Periodically probe configured channels and surface availability / latency to users. Turning it off stops the scheduler and returns an empty list on the user page.',
|
||||
enabled: 'Enable Channel Monitor',
|
||||
enabledHint: 'Disabling stops background checks; existing history is preserved.',
|
||||
defaultInterval: 'Default check interval (seconds)',
|
||||
defaultIntervalHint: 'Pre-fills the interval when creating a new monitor; each monitor can override it. Range 15 – 3600.',
|
||||
},
|
||||
},
|
||||
emailTabDisabledTitle: 'Email Verification Not Enabled',
|
||||
emailTabDisabledHint: 'Enable email verification in the Security tab to configure SMTP settings.',
|
||||
registration: {
|
||||
|
||||
@@ -4695,6 +4695,7 @@ export default {
|
||||
description: '管理注册、邮箱验证、默认值和 SMTP 设置',
|
||||
tabs: {
|
||||
general: '通用设置',
|
||||
features: '功能开关',
|
||||
security: '安全与认证',
|
||||
users: '用户默认值',
|
||||
gateway: '网关服务',
|
||||
@@ -4702,6 +4703,16 @@ export default {
|
||||
backup: '数据备份',
|
||||
payment: '支付设置',
|
||||
},
|
||||
features: {
|
||||
channelMonitor: {
|
||||
title: '渠道监控',
|
||||
description: '定期对配置的渠道发起健康检查,向用户展示可用性与延迟。关闭后调度器停止扫描,用户端列表为空。',
|
||||
enabled: '启用渠道监控',
|
||||
enabledHint: '关闭后后台不再执行定时检测,已有数据保留。',
|
||||
defaultInterval: '默认检测间隔(秒)',
|
||||
defaultIntervalHint: '新建渠道监控时表单的默认值,可被单个渠道覆盖。范围 15 – 3600 秒。',
|
||||
},
|
||||
},
|
||||
emailTabDisabledTitle: '邮箱验证未启用',
|
||||
emailTabDisabledHint: '请在「安全与认证」选项卡中启用邮箱验证后,再配置 SMTP 设置。',
|
||||
registration: {
|
||||
|
||||
@@ -352,6 +352,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
balance_low_notify_enabled: false,
|
||||
account_quota_notify_enabled: false,
|
||||
balance_low_notify_threshold: 0,
|
||||
channel_monitor_enabled: true,
|
||||
channel_monitor_default_interval_seconds: 60,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,8 @@ export interface PublicSettings {
|
||||
balance_low_notify_enabled: boolean
|
||||
account_quota_notify_enabled: boolean
|
||||
balance_low_notify_threshold: number
|
||||
channel_monitor_enabled: boolean
|
||||
channel_monitor_default_interval_seconds: number
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
|
||||
@@ -3749,6 +3749,52 @@
|
||||
</div>
|
||||
<!-- /Tab: General -->
|
||||
|
||||
<!-- Tab: Features (功能开关) -->
|
||||
<div v-show="activeTab === 'features'" class="space-y-6">
|
||||
|
||||
<div class="card">
|
||||
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.settings.features.channelMonitor.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.features.channelMonitor.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-5 p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.features.channelMonitor.enabled') }}
|
||||
</label>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.features.channelMonitor.enabledHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.channel_monitor_enabled" />
|
||||
</div>
|
||||
|
||||
<div v-if="form.channel_monitor_enabled">
|
||||
<label class="input-label">
|
||||
{{ t('admin.settings.features.channelMonitor.defaultInterval') }}
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
v-model.number="form.channel_monitor_default_interval_seconds"
|
||||
type="number"
|
||||
min="15"
|
||||
max="3600"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-400">
|
||||
{{ t('admin.settings.features.channelMonitor.defaultIntervalHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /Tab: Features -->
|
||||
|
||||
<!-- Tab: Email -->
|
||||
<!-- Tab: Payment -->
|
||||
<div v-show="activeTab === 'payment'" class="space-y-6">
|
||||
@@ -4737,6 +4783,7 @@ const paymentMethodsHref = computed(() =>
|
||||
|
||||
type SettingsTab =
|
||||
| "general"
|
||||
| "features"
|
||||
| "security"
|
||||
| "users"
|
||||
| "gateway"
|
||||
@@ -4746,6 +4793,7 @@ type SettingsTab =
|
||||
const activeTab = ref<SettingsTab>("general");
|
||||
const settingsTabs = [
|
||||
{ key: "general" as SettingsTab, icon: "home" as const },
|
||||
{ key: "features" as SettingsTab, icon: "bolt" as const },
|
||||
{ key: "security" as SettingsTab, icon: "shield" as const },
|
||||
{ key: "users" as SettingsTab, icon: "user" as const },
|
||||
{ key: "gateway" as SettingsTab, icon: "server" as const },
|
||||
@@ -5005,6 +5053,9 @@ const form = reactive<SettingsForm>({
|
||||
balance_low_notify_recharge_url: "",
|
||||
account_quota_notify_enabled: false,
|
||||
account_quota_notify_emails: [] as NotifyEmailEntry[],
|
||||
// Channel Monitor feature switch
|
||||
channel_monitor_enabled: true,
|
||||
channel_monitor_default_interval_seconds: 60,
|
||||
});
|
||||
|
||||
const authSourceDefaults = reactive<AuthSourceDefaultsState>(
|
||||
@@ -5912,6 +5963,10 @@ async function saveSettings() {
|
||||
account_quota_notify_emails: (
|
||||
form.account_quota_notify_emails || []
|
||||
).filter((e) => e.email.trim() !== ""),
|
||||
// Channel Monitor feature switch
|
||||
channel_monitor_enabled: form.channel_monitor_enabled,
|
||||
channel_monitor_default_interval_seconds:
|
||||
Number(form.channel_monitor_default_interval_seconds) || 60,
|
||||
};
|
||||
|
||||
appendAuthSourceDefaultsToUpdateRequest(payload, authSourceDefaults);
|
||||
|
||||
Reference in New Issue
Block a user