Skip to content

Commit 11df670

Browse files
soso
authored andcommitted
Release v0.1.0
1 parent d746577 commit 11df670

8 files changed

Lines changed: 493 additions & 139 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "emby302gateway-rs"
3-
version = "0.0.9"
3+
version = "0.1.0"
44
edition = "2024"
55
license = "MIT"
66

docs/changelog.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
11
# 版本更新日志
22

3+
## v0.1.0
4+
5+
发布时间:2026-05-25
6+
7+
### 新增
8+
9+
- 服务器配置支持单个服务器即时开启和关闭,关闭后会立即停止对应反代监听,开启后只启动对应服务器反代。
10+
- 播放频率限制新增“混合模式”,触发后同时屏蔽 IP,并在识别到用户名时调用 Emby API 禁用用户。
11+
- 首页播放频率限制表格支持直接显示当前封禁、命中窗口、用户名和解除封禁按钮。
12+
- 播放频率窗口和封禁记录补充用户名显示,封禁窗口过期但封禁仍有效时仍会在首页保留展示。
13+
14+
### 优化
15+
16+
- 日志页筛选栏按日志类型适配宽度,反代请求明细增加请求类型筛选后不再挤出换行。
17+
- 日志页只保留单一滚动区域,筛选控件和表格在窄宽度下避免横向溢出。
18+
- 文件日志减少普通代理请求刷屏,只记录播放直链、缓存命中、拦截、错误、异常状态和慢请求等有排查价值的信息。
19+
- 代理异常和慢请求日志会脱敏 `api_key``token``access_token` 等敏感查询参数。
20+
- 客户端当前封禁表格和首页封禁表格统一展示样式,封禁方式、服务器、IP、用户和操作列更清晰。
21+
- 服务器卡片按钮文案调整为“开启服务器 / 关闭服务器 / 重启服务器”。
22+
23+
### 修复
24+
25+
- 修复播放频率限制中封禁用户后,窗口过期导致首页已封禁数量归零的问题。
26+
- 修复播放频率限制窗口用户名在部分场景不显示的问题。
27+
- 解除播放频率封禁的审计日志补充服务器、封禁方式、用户、IP 和原到期时间。
28+
29+
### 验证
30+
31+
- `cargo fmt --check`
32+
- `cargo check`
33+
- `cd frontend && npx vue-tsc -b --noEmit`
34+
335
## v0.0.9
436

537
发布时间:2026-05-24

frontend/src/App.vue

Lines changed: 117 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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
204204
type 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 }> = [
459462
const 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
464468
const defaultCdnHeaders = [
@@ -596,7 +600,7 @@ const requestStatsTotals = computed(() =>
596600
),
597601
)
598602
const 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+
9901016
function 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+
13471402
function 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

Comments
 (0)