Merge pull request #249 from IanShaw027/feat/stream-timeout-handling
feat(gateway): 添加流超时处理机制
This commit is contained in:
@@ -201,6 +201,41 @@ export async function deleteAdminApiKey(): Promise<{ message: string }> {
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream timeout settings interface
|
||||
*/
|
||||
export interface StreamTimeoutSettings {
|
||||
enabled: boolean
|
||||
action: 'temp_unsched' | 'error' | 'none'
|
||||
temp_unsched_minutes: number
|
||||
threshold_count: number
|
||||
threshold_window_minutes: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stream timeout settings
|
||||
* @returns Stream timeout settings
|
||||
*/
|
||||
export async function getStreamTimeoutSettings(): Promise<StreamTimeoutSettings> {
|
||||
const { data } = await apiClient.get<StreamTimeoutSettings>('/admin/settings/stream-timeout')
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Update stream timeout settings
|
||||
* @param settings - Stream timeout settings to update
|
||||
* @returns Updated settings
|
||||
*/
|
||||
export async function updateStreamTimeoutSettings(
|
||||
settings: StreamTimeoutSettings
|
||||
): Promise<StreamTimeoutSettings> {
|
||||
const { data } = await apiClient.put<StreamTimeoutSettings>(
|
||||
'/admin/settings/stream-timeout',
|
||||
settings
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
export const settingsAPI = {
|
||||
getSettings,
|
||||
updateSettings,
|
||||
@@ -208,7 +243,9 @@ export const settingsAPI = {
|
||||
sendTestEmail,
|
||||
getAdminApiKey,
|
||||
regenerateAdminApiKey,
|
||||
deleteAdminApiKey
|
||||
deleteAdminApiKey,
|
||||
getStreamTimeoutSettings,
|
||||
updateStreamTimeoutSettings
|
||||
}
|
||||
|
||||
export default settingsAPI
|
||||
|
||||
@@ -2512,6 +2512,27 @@ export default {
|
||||
securityWarning: 'Warning: This key provides full admin access. Keep it secure.',
|
||||
usage: 'Usage: Add to request header - x-api-key: <your-admin-api-key>'
|
||||
},
|
||||
streamTimeout: {
|
||||
title: 'Stream Timeout Handling',
|
||||
description: 'Configure account handling strategy when upstream response times out',
|
||||
enabled: 'Enable Stream Timeout Handling',
|
||||
enabledHint: 'Automatically handle problematic accounts when upstream times out',
|
||||
timeoutSeconds: 'Timeout Threshold (seconds)',
|
||||
timeoutSecondsHint: 'Stream data interval exceeding this time is considered timeout (30-300s)',
|
||||
action: 'Action',
|
||||
actionTempUnsched: 'Temporarily Unschedulable',
|
||||
actionError: 'Mark as Error',
|
||||
actionNone: 'No Action',
|
||||
actionHint: 'Action to take on the account after timeout',
|
||||
tempUnschedMinutes: 'Pause Duration (minutes)',
|
||||
tempUnschedMinutesHint: 'Duration of temporary unschedulable state (1-60 minutes)',
|
||||
thresholdCount: 'Trigger Threshold (count)',
|
||||
thresholdCountHint: 'Number of timeouts before triggering action (1-10)',
|
||||
thresholdWindowMinutes: 'Threshold Window (minutes)',
|
||||
thresholdWindowMinutesHint: 'Time window for counting timeouts (1-60 minutes)',
|
||||
saved: 'Stream timeout settings saved',
|
||||
saveFailed: 'Failed to save stream timeout settings'
|
||||
},
|
||||
saveSettings: 'Save Settings',
|
||||
saving: 'Saving...',
|
||||
settingsSaved: 'Settings saved successfully',
|
||||
|
||||
@@ -2696,6 +2696,27 @@ export default {
|
||||
securityWarning: '警告:此密钥拥有完整的管理员权限,请妥善保管。',
|
||||
usage: '使用方法:在请求头中添加 x-api-key: <your-admin-api-key>'
|
||||
},
|
||||
streamTimeout: {
|
||||
title: '流超时处理',
|
||||
description: '配置上游响应超时时的账户处理策略,避免问题账户持续被选中',
|
||||
enabled: '启用流超时处理',
|
||||
enabledHint: '当上游响应超时时,自动处理问题账户',
|
||||
timeoutSeconds: '超时阈值(秒)',
|
||||
timeoutSecondsHint: '流数据间隔超过此时间视为超时(30-300秒)',
|
||||
action: '处理方式',
|
||||
actionTempUnsched: '临时不可调度',
|
||||
actionError: '标记为错误状态',
|
||||
actionNone: '不处理',
|
||||
actionHint: '超时后对账户执行的操作',
|
||||
tempUnschedMinutes: '暂停时长(分钟)',
|
||||
tempUnschedMinutesHint: '临时不可调度的持续时间(1-60分钟)',
|
||||
thresholdCount: '触发阈值(次数)',
|
||||
thresholdCountHint: '累计超时多少次后触发处理(1-10次)',
|
||||
thresholdWindowMinutes: '阈值窗口(分钟)',
|
||||
thresholdWindowMinutesHint: '超时计数的时间窗口(1-60分钟)',
|
||||
saved: '流超时设置保存成功',
|
||||
saveFailed: '保存流超时设置失败'
|
||||
},
|
||||
saveSettings: '保存设置',
|
||||
saving: '保存中...',
|
||||
settingsSaved: '设置保存成功',
|
||||
|
||||
@@ -147,6 +147,161 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stream Timeout Settings -->
|
||||
<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.streamTimeout.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.streamTimeout.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-5 p-6">
|
||||
<!-- Loading State -->
|
||||
<div v-if="streamTimeoutLoading" class="flex items-center gap-2 text-gray-500">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"></div>
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Enable Stream Timeout -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t('admin.settings.streamTimeout.enabled')
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.streamTimeout.enabledHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="streamTimeoutForm.enabled" />
|
||||
</div>
|
||||
|
||||
<!-- Settings - Only show when enabled -->
|
||||
<div
|
||||
v-if="streamTimeoutForm.enabled"
|
||||
class="space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
>
|
||||
<!-- Timeout Seconds -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.streamTimeout.timeoutSeconds') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="streamTimeoutForm.timeout_seconds"
|
||||
type="number"
|
||||
min="30"
|
||||
max="300"
|
||||
class="input w-32"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.streamTimeout.timeoutSecondsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.streamTimeout.action') }}
|
||||
</label>
|
||||
<select v-model="streamTimeoutForm.action" class="input w-64">
|
||||
<option value="temp_unsched">{{ t('admin.settings.streamTimeout.actionTempUnsched') }}</option>
|
||||
<option value="error">{{ t('admin.settings.streamTimeout.actionError') }}</option>
|
||||
<option value="none">{{ t('admin.settings.streamTimeout.actionNone') }}</option>
|
||||
</select>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.streamTimeout.actionHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Temp Unsched Minutes (only show when action is temp_unsched) -->
|
||||
<div v-if="streamTimeoutForm.action === 'temp_unsched'">
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.streamTimeout.tempUnschedMinutes') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="streamTimeoutForm.temp_unsched_minutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
class="input w-32"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.streamTimeout.tempUnschedMinutesHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Threshold Count -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.streamTimeout.thresholdCount') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="streamTimeoutForm.threshold_count"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
class="input w-32"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.streamTimeout.thresholdCountHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Threshold Window Minutes -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.streamTimeout.thresholdWindowMinutes') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="streamTimeoutForm.threshold_window_minutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
class="input w-32"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.streamTimeout.thresholdWindowMinutesHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="flex justify-end border-t border-gray-100 pt-4 dark:border-dark-700">
|
||||
<button
|
||||
type="button"
|
||||
@click="saveStreamTimeoutSettings"
|
||||
:disabled="streamTimeoutSaving"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
<svg
|
||||
v-if="streamTimeoutSaving"
|
||||
class="mr-1 h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ streamTimeoutSaving ? t('common.saving') : t('common.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Registration Settings -->
|
||||
<div class="card">
|
||||
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||
@@ -840,6 +995,18 @@ const adminApiKeyMasked = ref('')
|
||||
const adminApiKeyOperating = ref(false)
|
||||
const newAdminApiKey = ref('')
|
||||
|
||||
// Stream Timeout 状态
|
||||
const streamTimeoutLoading = ref(true)
|
||||
const streamTimeoutSaving = ref(false)
|
||||
const streamTimeoutForm = reactive({
|
||||
enabled: true,
|
||||
timeout_seconds: 60,
|
||||
action: 'temp_unsched' as 'temp_unsched' | 'error' | 'none',
|
||||
temp_unsched_minutes: 5,
|
||||
threshold_count: 3,
|
||||
threshold_window_minutes: 10
|
||||
})
|
||||
|
||||
type SettingsForm = SystemSettings & {
|
||||
smtp_password: string
|
||||
turnstile_secret_key: string
|
||||
@@ -1129,8 +1296,44 @@ function copyNewKey() {
|
||||
})
|
||||
}
|
||||
|
||||
// Stream Timeout 方法
|
||||
async function loadStreamTimeoutSettings() {
|
||||
streamTimeoutLoading.value = true
|
||||
try {
|
||||
const settings = await adminAPI.settings.getStreamTimeoutSettings()
|
||||
Object.assign(streamTimeoutForm, settings)
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load stream timeout settings:', error)
|
||||
} finally {
|
||||
streamTimeoutLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStreamTimeoutSettings() {
|
||||
streamTimeoutSaving.value = true
|
||||
try {
|
||||
const updated = await adminAPI.settings.updateStreamTimeoutSettings({
|
||||
enabled: streamTimeoutForm.enabled,
|
||||
timeout_seconds: streamTimeoutForm.timeout_seconds,
|
||||
action: streamTimeoutForm.action,
|
||||
temp_unsched_minutes: streamTimeoutForm.temp_unsched_minutes,
|
||||
threshold_count: streamTimeoutForm.threshold_count,
|
||||
threshold_window_minutes: streamTimeoutForm.threshold_window_minutes
|
||||
})
|
||||
Object.assign(streamTimeoutForm, updated)
|
||||
appStore.showSuccess(t('admin.settings.streamTimeout.saved'))
|
||||
} catch (error: any) {
|
||||
appStore.showError(
|
||||
t('admin.settings.streamTimeout.saveFailed') + ': ' + (error.message || t('common.unknownError'))
|
||||
)
|
||||
} finally {
|
||||
streamTimeoutSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSettings()
|
||||
loadAdminApiKey()
|
||||
loadStreamTimeoutSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user