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:
erio
2026-04-21 00:21:29 +08:00
parent a1425b457d
commit 7da5124067
18 changed files with 283 additions and 14 deletions

View File

@@ -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;
}
/**

View File

@@ -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">

View File

@@ -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
}

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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,
}
}

View File

@@ -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 {

View File

@@ -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);