@@ -192,7 +192,7 @@ type PlaybackRateBlockRecord = {
192192 id: string
193193 server_id: string
194194 server_name: string
195- action: ' block_ip' | ' disable_user'
195+ action: ' block_ip' | ' disable_user' | ' mixed '
196196 ip: string
197197 user_name: string
198198 blocked_until: string
@@ -203,6 +203,9 @@ type PlaybackRateBlockRecord = {
203203
204204type PlaybackRateWindowStatus = {
205205 server_id: string
206+ block_id? : string
207+ block_action? : string
208+ user_name: string
206209 ip: string
207210 current_count: number
208211 threshold: number
@@ -227,7 +230,7 @@ type ClientControlConfig = {
227230 playback_rate_limit_window_seconds: number
228231 playback_rate_limit_max_requests: number
229232 playback_rate_limit_block_seconds: number
230- playback_rate_limit_action: ' block_ip' | ' disable_user'
233+ playback_rate_limit_action: ' block_ip' | ' disable_user' | ' mixed '
231234 rate_limit_blocks: PlaybackRateBlockRecord []
232235 webhook? : WebhookNotifyConfig
233236 webhooks: WebhookNotifyConfig []
@@ -459,6 +462,7 @@ const realIpModeOptions: Array<{ value: RealIpMode; label: string }> = [
459462const playbackLimitActionOptions: Array <{ value: ClientControlConfig [' playback_rate_limit_action' ]; label: string ; description: string }> = [
460463 { value: ' block_ip' , label: ' 屏蔽 IP' , description: ' 屏蔽频繁播放的 IP' },
461464 { value: ' disable_user' , label: ' 禁用用户' , description: ' 通过 API 禁用该用户' },
465+ { value: ' mixed' , label: ' 混合模式' , description: ' 同时禁用用户并屏蔽频繁播放的 IP' },
462466]
463467
464468const defaultCdnHeaders = [
@@ -596,7 +600,7 @@ const requestStatsTotals = computed(() =>
596600 ),
597601)
598602const rateLimitOverview = computed (() => ({
599- active_windows: rateLimitWindows .value .length ,
603+ active_windows: rateLimitWindows .value .filter (( row ) => row . current_count > 0 ). length ,
600604 blocked_windows: rateLimitWindows .value .filter ((row ) => row .blocked ).length ,
601605 highest_count: rateLimitWindows .value .reduce ((max , row ) => Math .max (max , row .current_count ), 0 ),
602606}))
@@ -987,6 +991,28 @@ async function restartProxyServer(server: EmbyServerConfig) {
987991 }
988992}
989993
994+ async function toggleProxyServer(server : EmbyServerConfig ) {
995+ const nextEnabled = ! server .enabled
996+ restartingServerId .value = server .id
997+ notice .value = ' '
998+ error .value = ' '
999+ try {
1000+ const response = await api <Settings >(' /api/settings/toggle-proxy' , {
1001+ method: ' POST' ,
1002+ body: JSON .stringify ({ server_id: server .id , enabled: nextEnabled }),
1003+ })
1004+ Object .assign (settings , response )
1005+ normalizeSettingsServers ()
1006+ notice .value = ` ${server .name || ' 服务器' } 已${nextEnabled ? ' 开启' : ' 关闭' } `
1007+ await refreshOperationalData ()
1008+ await refreshDashboard ()
1009+ } catch (err ) {
1010+ error .value = err instanceof Error ? err .message : String (err )
1011+ } finally {
1012+ restartingServerId .value = ' '
1013+ }
1014+ }
1015+
9901016function buildSettingsPayload() {
9911017 const servers = settings .servers .map ((server ) => ({
9921018 ... server ,
@@ -1274,7 +1300,23 @@ async function unblockRateLimit(record: PlaybackRateBlockRecord) {
12741300 body: JSON .stringify (await encryptPayload (' rate_limit_block' , { id: record .id })),
12751301 })
12761302 applyClientControlConfig (response )
1277- notice .value = record .action === ' disable_user' ? ' 用户封禁已解除' : ' IP 屏蔽已解除'
1303+ notice .value = playbackRateUnblockNotice (record .action )
1304+ } catch (err ) {
1305+ clientControlError .value = err instanceof Error ? err .message : String (err )
1306+ }
1307+ }
1308+
1309+ async function unblockRateLimitWindow(row : PlaybackRateWindowStatus ) {
1310+ if (! row .block_id ) return
1311+ clientControlError .value = ' '
1312+ try {
1313+ const response = await api <ClientControlConfig >(' /api/client-control/rate-blocks/unblock' , {
1314+ method: ' POST' ,
1315+ body: JSON .stringify (await encryptPayload (' rate_limit_block' , { id: row .block_id })),
1316+ })
1317+ applyClientControlConfig (response )
1318+ await refreshRateLimitStatus ()
1319+ notice .value = playbackRateUnblockNotice (row .block_action )
12781320 } catch (err ) {
12791321 clientControlError .value = err instanceof Error ? err .message : String (err )
12801322 }
@@ -1344,6 +1386,19 @@ function rateLimitBlockIp(record: PlaybackRateBlockRecord) {
13441386 return record .note .match (/ (?:IP| ip)\s + ([0-9a-fA-F :. ] + )/ )?.[1 ] ?? ' --'
13451387}
13461388
1389+ function playbackRateActionLabel(action ? : string ) {
1390+ if (action === ' disable_user' ) return ' 禁用用户'
1391+ if (action === ' mixed' ) return ' 混合模式'
1392+ if (action === ' block_ip' ) return ' 屏蔽 IP'
1393+ return ' --'
1394+ }
1395+
1396+ function playbackRateUnblockNotice(action ? : string ) {
1397+ if (action === ' disable_user' ) return ' 用户封禁已解除'
1398+ if (action === ' mixed' ) return ' 混合封禁已解除'
1399+ return ' IP 屏蔽已解除'
1400+ }
1401+
13471402function logout() {
13481403 token .value = ' '
13491404 page .value = ' home'
@@ -2119,16 +2174,46 @@ onBeforeUnmount(stopDashboardPolling)
21192174 <small >当前窗口最大次数</small >
21202175 </div >
21212176 </div >
2122- <div v-if =" rateLimitWindows.length" class =" rate-window-mini-list" >
2123- <div v-for =" row in rateLimitWindows.slice(0, 5)" :key =" `home-${row.server_id}-${row.ip}`" class =" rate-window-mini-row" >
2124- <strong >{{ formatServerName(row.server_id) }}</strong >
2125- <span >{{ row.ip }}</span >
2126- <span >{{ row.current_count }}/{{ row.threshold }}</span >
2127- <span >{{ row.window_seconds }}s</span >
2128- <span :class =" ['client-badge', row.blocked ? 'blocked' : 'allowed']" >
2129- {{ row.blocked ? '已封禁' : '观察中' }}
2130- </span >
2131- </div >
2177+ <div v-if =" rateLimitWindows.length" class =" rate-block-table-wrap home-rate-table" >
2178+ <table class =" rate-block-table" >
2179+ <thead >
2180+ <tr >
2181+ <th >封禁方式</th >
2182+ <th >服务器</th >
2183+ <th >IP</th >
2184+ <th >用户</th >
2185+ <th >命中</th >
2186+ <th >窗口</th >
2187+ <th >状态</th >
2188+ <th >{{ rateLimitWindows.length }} 条</th >
2189+ </tr >
2190+ </thead >
2191+ <tbody >
2192+ <tr v-for =" row in rateLimitWindows.slice(0, 5)" :key =" `home-${row.server_id}-${row.ip}`" >
2193+ <td >{{ playbackRateActionLabel(row.block_action) }}</td >
2194+ <td >{{ formatServerName(row.server_id) }}</td >
2195+ <td ><strong >{{ row.ip }}</strong ></td >
2196+ <td >{{ row.user_name || '--' }}</td >
2197+ <td >{{ row.current_count }}/{{ row.threshold }}</td >
2198+ <td >{{ row.window_seconds }}s</td >
2199+ <td >
2200+ <span :class =" ['client-badge', row.blocked ? 'blocked' : 'allowed']" >
2201+ {{ row.blocked ? '已封禁' : '观察中' }}
2202+ </span >
2203+ </td >
2204+ <td >
2205+ <button
2206+ v-if =" row.blocked && row.block_id"
2207+ type =" button"
2208+ class =" secondary"
2209+ @click =" unblockRateLimitWindow(row)"
2210+ >
2211+ 解除封禁
2212+ </button >
2213+ </td >
2214+ </tr >
2215+ </tbody >
2216+ </table >
21322217 </div >
21332218 <div v-else class =" empty-state compact" >当前没有播放频率窗口数据。</div >
21342219 </section >
@@ -2186,17 +2271,22 @@ onBeforeUnmount(stopDashboardPolling)
21862271 <div class =" server-card-head" >
21872272 <strong >{{ server.name || `服务器 ${index + 1}` }}</strong >
21882273 <div class =" server-actions" >
2189- <label class =" check compact-check" >
2190- <input v-model =" server.enabled" type =" checkbox" />
2191- <span >启用</span >
2192- </label >
2274+ <button
2275+ type =" button"
2276+ :class =" ['server-toggle-button', { disabled: !server.enabled }]"
2277+ :disabled =" saving || restartingServerId === server.id"
2278+ :aria-pressed =" server.enabled"
2279+ @click =" toggleProxyServer(server)"
2280+ >
2281+ {{ server.enabled ? '关闭服务器' : '开启服务器' }}
2282+ </button >
21932283 <button
21942284 type =" button"
21952285 class =" secondary restart-button"
21962286 :disabled =" saving || restartingServerId === server.id || !server.enabled"
21972287 @click =" restartProxyServer(server)"
21982288 >
2199- {{ restartingServerId === server.id ? '重启中' : '重启 ' }}
2289+ {{ restartingServerId === server.id ? '重启中' : '重启服务器 ' }}
22002290 </button >
22012291 <button class =" danger-button" :disabled =" settings.servers.length <= 1" @click =" removeServer(server.id)" >
22022292 删除
@@ -2414,11 +2504,6 @@ onBeforeUnmount(stopDashboardPolling)
24142504 {{ option.label }}
24152505 </option >
24162506 </select >
2417- <small class =" field-help" >
2418- {{
2419- playbackLimitActionOptions.find((option) => option.value === clientControl.playback_rate_limit_action)?.description
2420- }}
2421- </small >
24222507 </label >
24232508 <label >
24242509 <span >检测时间窗口(秒)</span >
@@ -2429,33 +2514,29 @@ onBeforeUnmount(stopDashboardPolling)
24292514 <input v-model.number =" clientControl.playback_rate_limit_max_requests" type =" number" min =" 1" />
24302515 </label >
24312516 <label >
2432- <span >{{ clientControl.playback_rate_limit_action === 'block_ip ' ? '屏蔽时长 (秒)' : '重复拦截冷却 (秒)' }}</span >
2517+ <span >{{ clientControl.playback_rate_limit_action === 'disable_user ' ? '重复拦截冷却 (秒)' : '封禁时长 (秒)' }}</span >
24332518 <input v-model.number =" clientControl.playback_rate_limit_block_seconds" type =" number" min =" 1" />
24342519 </label >
24352520 </div >
24362521 <p class =" muted rate-limit-help" >
2437- 同一 IP 在窗口内超过次数后,按选择的方式处理:屏蔽 IP 为临时封禁;禁用用户会调用 Emby API 禁用账号。
2522+ 同一 IP 在窗口内超过次数后,按选择的方式处理:屏蔽 IP 为临时封禁;禁用用户会调用 Emby API 禁用账号;混合模式会同时处理用户和 IP 。
24382523 </p >
24392524 <div class =" rate-block-list" >
2440- <div class =" rate-block-head" >
2441- <strong >当前封禁</strong >
2442- <span >{{ activeRateLimitBlocks.length }} 条</span >
2443- </div >
24442525 <div v-if =" activeRateLimitBlocks.length" class =" rate-block-table-wrap" >
24452526 <table class =" rate-block-table" >
24462527 <thead >
24472528 <tr >
2448- <th >方式 </th >
2529+ <th >封禁方式 </th >
24492530 <th >服务器</th >
24502531 <th >IP</th >
24512532 <th >用户</th >
24522533 <th >到期时间</th >
2453- <th >操作 </th >
2534+ <th >{{ activeRateLimitBlocks.length }} 条 </th >
24542535 </tr >
24552536 </thead >
24562537 <tbody >
24572538 <tr v-for =" record in activeRateLimitBlocks" :key =" record.id" >
2458- <td >{{ record.action === 'disable_user' ? '禁用用户' : '屏蔽 IP' }}</td >
2539+ <td >{{ playbackRateActionLabel( record.action) }}</td >
24592540 <td >{{ record.server_name }}</td >
24602541 <td >
24612542 <strong >{{ rateLimitBlockIp(record) }}</strong >
@@ -2519,6 +2600,7 @@ onBeforeUnmount(stopDashboardPolling)
25192600 <tr >
25202601 <th >服务器</th >
25212602 <th >IP</th >
2603+ <th >用户</th >
25222604 <th >当前次数</th >
25232605 <th >阈值</th >
25242606 <th >剩余</th >
@@ -2531,6 +2613,7 @@ onBeforeUnmount(stopDashboardPolling)
25312613 <tr v-for =" row in rateLimitWindows" :key =" `${row.server_id}-${row.ip}`" >
25322614 <td >{{ formatServerName(row.server_id) }}</td >
25332615 <td >{{ row.ip }}</td >
2616+ <td >{{ row.user_name || '--' }}</td >
25342617 <td >{{ row.current_count }}</td >
25352618 <td >{{ row.threshold }}</td >
25362619 <td >{{ row.remaining }}</td >
@@ -2760,7 +2843,7 @@ onBeforeUnmount(stopDashboardPolling)
27602843 </div >
27612844 </div >
27622845 <div v-if =" logsError" class =" notice error" >{{ logsError }}</div >
2763- <div class =" log-toolbar compact" >
2846+ <div : class =" [' log-toolbar', ' compact', { proxy: selectedLogView === 'proxy' }] " >
27642847 <label >
27652848 <span >日志类型</span >
27662849 <select v-model =" selectedLogView" @change =" handleLogViewChange" >
0 commit comments