feat(media-server): implement runtime runtime management system#204
feat(media-server): implement runtime runtime management system#204
Conversation
- add UV bootstrapper with cross-platform install, caching, progress, validation - manage Python venv lifecycle, dependency installs, cleanup, and phased progress - introduce MediaServerService with lifecycle control, health checks, and port management - extend FFmpeg service to handle FFprobe with unified downloads and version updates - build settings UI sections for media server and plugins with real-time status - wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control - update build configs, add ffprobe download script, auto-start server, and bump backend ref
Walkthrough新增 FFprobe 下载脚本与服务、UV 引导器、Python 虚拟环境与媒体服务器服务;将 FFmpeg 下载服务泛化以支持 ffprobe;扩展 IPC 与 preload API;更新打包/复制规则、SessionService 动态端口及播放器播放列表获取;新增设置页与大量单元测试。 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant App as 主进程
participant Venv as PythonVenvService
participant MS as MediaServerService
participant UV as UvBootstrapperService
App->>Venv: checkVenvInfo()
alt venv 存在
App->>MS: start(config?)
MS->>UV: checkUvInstallation()
UV-->>MS: {exists, path, version}
MS->>MS: 分配端口 / 构建 env / spawn uvicorn
MS-->>App: 启动结果(成功/失败)
else venv 不存在
App-->>App: 跳过媒体服务器启动
end
sequenceDiagram
autonumber
participant UI as Renderer(设置页)
participant Pre as Preload(ffprobe)
participant IPC as Main IPC
participant Svc as FFmpegDownloadService
UI->>Pre: ffprobe.download.download(platform, arch)
Pre->>IPC: IpcChannel.FfprobeDownload_Download
IPC->>Svc: downloadFFprobe(platform, arch)
Svc-->>IPC: 进度/结果事件
IPC-->>Pre: boolean
loop 轮询
UI->>Pre: ffprobe.download.getProgress()
Pre->>IPC: IpcChannel.FfprobeDownload_GetProgress
IPC-->>UI: {percent,speed,remainingTime,status}
end
sequenceDiagram
autonumber
participant Player as Renderer(PlayerPage)
participant SS as SessionService
participant Pre as Preload(mediaServer)
participant IPC as Main IPC
participant MS as MediaServerService
Player->>SS: getPlaylistUrl(sessionId)
alt port 未缓存
SS->>Pre: mediaServer.getPort()
Pre->>IPC: IpcChannel.MediaServer_GetPort
IPC->>MS: getPort()
MS-->>IPC: port|null
IPC-->>Pre: port|null
Pre-->>SS: port|null
SS->>Player: 构建并返回 playlist URL
else 已缓存
SS-->>Player: playlist URL
end
note over SS,Player: 收到 MediaServer_PortChanged 时调用 resetPort()
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
src/main/ipc.ts (1)
508-515: 修复 FFmpeg 下载进度/取消的参数顺序
FFmpegDownloadService现在要求第一个参数是'ffmpeg' | 'ffprobe'来区分二进制类型(测试文件也已经改为getDownloadProgress('ffmpeg', ...)/cancelDownload('ffmpeg', ...))。这里仍然用旧签名,把platform传成首参,实际会查不到'win32'这种 key,导致进度始终为null、取消操作无效。请同步补上 binaryType。建议修改如下:
- return ffmpegDownloadService.getDownloadProgress(platform as any, arch as any) + return ffmpegDownloadService.getDownloadProgress('ffmpeg', platform as any, arch as any) ... - return ffmpegDownloadService.cancelDownload(platform as any, arch as any) + return ffmpegDownloadService.cancelDownload('ffmpeg', platform as any, arch as any)src/main/services/FFmpegDownloadService.ts (6)
12-15: 类型重复定义,建议复用单一来源Platform/Arch 在 UvBootstrapperService 已导出,这里重复定义会造成漂移风险。建议直接 import type 统一来源。
-// 支持的平台类型 -export type Platform = 'win32' | 'darwin' | 'linux' -export type Arch = 'x64' | 'arm64' +// 复用统一类型定义 +import type { Platform, Arch } from './UvBootstrapperService'
879-885: 仅处理 301/302 重定向,遗漏 303/307/308实际下载源(GitHub、第三方镜像)可能返回 303/307/308。建议一并处理。
- if (response.statusCode === 301 || response.statusCode === 302) { + if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0)) { const redirectUrl = response.headers.location if (redirectUrl) { download(redirectUrl, redirectCount + 1) return } }
939-942: 请求超时处理更稳妥的写法https.get 的 options.timeout 兼容性不如 req.setTimeout 明确。建议改为 setTimeout。
- request.on('timeout', () => { - request.destroy() - reject(new Error('下载超时')) - }) + request.setTimeout(30_000, () => { + request.destroy(new Error('下载超时')) + reject(new Error('下载超时')) + })
964-992: ZIP 解压依赖系统工具,Linux/某些发行版可能缺少 unzip当 unzip 不存在会 ENOENT 失败。建议增加 JS 解压回退(如 yauzl/adm-zip),或在失败时提示安装依赖。
可在捕获 ENOENT 时回退到 JS 解压:
- 优先使用系统工具(当前逻辑)
- 捕获 error.code === 'ENOENT' → 动态 import 解压库并执行
303-319: 地区检测失败默认切到中国镜像,需确认是否符合产品预期检测失败时 useChinaMirror = true,可能导致全球用户先走中国镜像再回退,增加失败/时延。请确认策略;若面向全球,建议默认 global。
候选策略:
- 默认 global;仅在 country ∈ {cn,hk,mo,tw} 时启用 china
- 或增加用户可见设置,允许覆盖
618-722: 下载/解压失败后的残留及幂等性失败时可能残留部分下载文件或不完整目录。可考虑:
- 失败后清理临时目录与目标文件(若已创建)
- 写入时先到临时文件,再原子替换到 finalPath
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (24)
.gitignore(1 hunks)backend(1 hunks)electron-builder.yml(1 hunks)electron.vite.config.ts(2 hunks)packages/shared/IpcChannel.ts(1 hunks)scripts/download-ffmpeg.ts(1 hunks)scripts/download-ffprobe.ts(1 hunks)src/main/index.ts(2 hunks)src/main/ipc.ts(2 hunks)src/main/services/FFmpegDownloadService.ts(22 hunks)src/main/services/MediaServerService.ts(1 hunks)src/main/services/PythonVenvService.ts(1 hunks)src/main/services/UvBootstrapperService.ts(1 hunks)src/main/services/__tests__/FFmpegDownloadService.test.ts(3 hunks)src/main/services/__tests__/UvBootstrapperService.test.ts(1 hunks)src/preload/index.ts(2 hunks)src/renderer/src/i18n/locales/zh-cn.json(1 hunks)src/renderer/src/pages/player/PlayerPage.tsx(1 hunks)src/renderer/src/pages/settings/FFmpegSection.tsx(1 hunks)src/renderer/src/pages/settings/FFprobeSection.tsx(1 hunks)src/renderer/src/pages/settings/MediaServerSection.tsx(1 hunks)src/renderer/src/pages/settings/PluginsSettings.tsx(1 hunks)src/renderer/src/pages/settings/SettingsPage.tsx(2 hunks)src/renderer/src/services/SessionService.ts(5 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
src/renderer/src/**/*.{ts,tsx,scss,css}
📄 CodeRabbit inference engine (CLAUDE.md)
优先使用 CSS 变量,避免硬编码样式值(颜色等)
Files:
src/renderer/src/pages/player/PlayerPage.tsxsrc/renderer/src/pages/settings/FFprobeSection.tsxsrc/renderer/src/services/SessionService.tssrc/renderer/src/pages/settings/PluginsSettings.tsxsrc/renderer/src/pages/settings/SettingsPage.tsxsrc/renderer/src/pages/settings/FFmpegSection.tsxsrc/renderer/src/pages/settings/MediaServerSection.tsx
src/renderer/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
尺寸与时长等不要硬编码,优先使用 useTheme() 的 token 或集中样式变量(如 motionDurationMid、borderRadiusSM/MD)
Files:
src/renderer/src/pages/player/PlayerPage.tsxsrc/renderer/src/pages/settings/FFprobeSection.tsxsrc/renderer/src/services/SessionService.tssrc/renderer/src/pages/settings/PluginsSettings.tsxsrc/renderer/src/pages/settings/SettingsPage.tsxsrc/renderer/src/pages/settings/FFmpegSection.tsxsrc/renderer/src/pages/settings/MediaServerSection.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 定制 antd 组件样式优先使用 styled-components 包装 styled(Component),避免全局 classNames
项目的图标统一使用 lucide-react,不使用 emoji 作为图标
组件/Hook 顶层必须通过 useStore(selector) 使用 Zustand,禁止在 useMemo/useEffect 内部调用 store Hook
避免使用返回对象的 Zustand 选择器(如 useStore(s => ({ a: s.a, b: s.b })));应使用单字段选择器或配合 shallow 比较器
遵循 React「副作用与状态更新」规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定等
统一使用 loggerService 记录日志而不是 console
logger 使用示例中第二个参数必须为对象字面量(如 logger.error('msg', { error }))
任何组件或页面都不要写入 media 元素的 currentTime,播放器控制由编排器统一负责
在 styled-components 中:主题相关属性使用 AntD CSS 变量(如 var(--ant-color-bg-elevated));
在 styled-components 中:设计系统常量(尺寸、动画、层级、字体、毛玻璃等)使用 JS 常量(如 SPACING、BORDER_RADIUS、Z_INDEX、FONT_SIZES 等)
Files:
src/renderer/src/pages/player/PlayerPage.tsxscripts/download-ffmpeg.tssrc/main/services/__tests__/FFmpegDownloadService.test.tssrc/renderer/src/pages/settings/FFprobeSection.tsxsrc/renderer/src/services/SessionService.tssrc/renderer/src/pages/settings/PluginsSettings.tsxsrc/renderer/src/pages/settings/SettingsPage.tsxsrc/main/services/PythonVenvService.tssrc/renderer/src/pages/settings/FFmpegSection.tsxsrc/main/services/MediaServerService.tsscripts/download-ffprobe.tssrc/main/services/UvBootstrapperService.tssrc/main/ipc.tssrc/preload/index.tspackages/shared/IpcChannel.tssrc/main/services/__tests__/UvBootstrapperService.test.tssrc/renderer/src/pages/settings/MediaServerSection.tsxelectron.vite.config.tssrc/main/index.tssrc/main/services/FFmpegDownloadService.ts
**/player/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Player 页面:统一在组件顶层使用 Zustand selector,禁止在 useMemo/useEffect 内调用 store Hook;useSubtitleEngine 通过参数传入 subtitles 等防御处理
Files:
src/renderer/src/pages/player/PlayerPage.tsx
**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
使用 Vitest 作为测试框架
Files:
src/main/services/__tests__/FFmpegDownloadService.test.tssrc/main/services/__tests__/UvBootstrapperService.test.ts
🧬 Code graph analysis (14)
src/renderer/src/pages/player/PlayerPage.tsx (1)
src/renderer/src/services/SessionService.ts (1)
SessionService(123-533)
src/renderer/src/pages/settings/FFprobeSection.tsx (4)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)src/renderer/src/pages/settings/AboutSettings.tsx (1)
SettingRowTitle(412-424)src/renderer/src/infrastructure/styles/theme.ts (6)
SPACING(43-58)BORDER_RADIUS(61-72)ANIMATION_DURATION(91-100)EASING(103-114)FONT_WEIGHTS(11-22)FONT_SIZES(25-40)
src/renderer/src/services/SessionService.ts (1)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)
src/renderer/src/pages/settings/PluginsSettings.tsx (1)
src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)
src/main/services/PythonVenvService.ts (2)
src/renderer/src/services/Logger.ts (3)
loggerService(817-817)error(422-424)info(436-438)src/main/services/UvBootstrapperService.ts (1)
uvBootstrapperService(1188-1188)
src/renderer/src/pages/settings/FFmpegSection.tsx (4)
src/renderer/src/services/Logger.ts (1)
loggerService(817-817)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)src/renderer/src/pages/settings/AboutSettings.tsx (1)
SettingRowTitle(412-424)src/renderer/src/infrastructure/styles/theme.ts (6)
SPACING(43-58)BORDER_RADIUS(61-72)ANIMATION_DURATION(91-100)EASING(103-114)FONT_WEIGHTS(11-22)FONT_SIZES(25-40)
src/main/services/MediaServerService.ts (4)
src/main/services/FFmpegDownloadService.ts (1)
FFmpegDownloadService(280-1050)src/main/utils/index.ts (1)
getDataPath(11-17)src/main/services/UvBootstrapperService.ts (1)
uvBootstrapperService(1188-1188)src/main/services/PythonVenvService.ts (1)
pythonVenvService(518-518)
src/main/services/UvBootstrapperService.ts (2)
src/renderer/src/services/Logger.ts (3)
loggerService(817-817)error(422-424)info(436-438)src/main/services/FFmpegDownloadService.ts (3)
Platform(13-13)Arch(14-14)DownloadProgress(28-35)
src/main/ipc.ts (4)
src/main/services/FFmpegDownloadService.ts (1)
ffmpegDownloadService(1053-1053)src/main/services/UvBootstrapperService.ts (1)
uvBootstrapperService(1188-1188)src/main/services/PythonVenvService.ts (1)
pythonVenvService(518-518)src/main/services/MediaServerService.ts (1)
mediaServerService(616-616)
src/main/services/__tests__/UvBootstrapperService.test.ts (2)
src/main/services/UvBootstrapperService.ts (2)
UvBootstrapperService(138-1185)DownloadProgress(26-33)src/main/services/FFmpegDownloadService.ts (1)
DownloadProgress(28-35)
src/renderer/src/pages/settings/MediaServerSection.tsx (6)
src/renderer/src/services/Logger.ts (3)
loggerService(817-817)info(436-438)error(422-424)src/main/services/MediaServerService.ts (1)
MediaServerInfo(29-36)src/main/services/PythonVenvService.ts (1)
InstallProgress(25-29)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)src/renderer/src/pages/settings/AboutSettings.tsx (1)
SettingRowTitle(412-424)src/renderer/src/infrastructure/styles/theme.ts (6)
SPACING(43-58)BORDER_RADIUS(61-72)ANIMATION_DURATION(91-100)EASING(103-114)FONT_WEIGHTS(11-22)FONT_SIZES(25-40)
electron.vite.config.ts (1)
scripts/upload-assets.js (1)
fs(4-4)
src/main/index.ts (2)
src/main/services/PythonVenvService.ts (1)
pythonVenvService(518-518)src/main/services/MediaServerService.ts (1)
mediaServerService(616-616)
src/main/services/FFmpegDownloadService.ts (1)
src/main/services/UvBootstrapperService.ts (3)
Platform(11-11)Arch(12-12)DownloadProgress(26-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test (macos-latest, 20)
- GitHub Check: test (windows-latest, 20)
- GitHub Check: test (ubuntu-latest, 20)
🔇 Additional comments (3)
src/renderer/src/pages/player/PlayerPage.tsx (1)
179-180: 动态 URL 构建:集中化维护
- 将硬编码播放列表 URL 替换为
SessionService.getPlaylistUrl()动态获取,✅ 异步调用和错误处理(try-catch)已覆盖- 建议确认媒体服务器自动启动逻辑在播放器加载前已完成,以避免
getPort()返回 nullbackend (1)
1-1: 请确认子模块更新内容已通过审查此处仅更新了子模块指针,我在当前 PR 中无法看到新提交的代码,请确认指向的
d747012bbd0a394f644939e9c15212ea769274ab已经过审核和验证,避免引入未预期的改动。src/main/services/PythonVenvService.ts (1)
5-6: 无需修改 LoggerService 引入:src/main/services/LoggerService.ts存在且已实现error、warn、info、debug方法。
| // FFprobe 配置(与 FFmpeg 使用相同的包,只是提取不同的可执行文件) | ||
| const FFPROBE_VERSIONS: Record<Platform, Record<Arch, FFmpegVersion>> = { | ||
| win32: { | ||
| x64: { | ||
| version: 'latest', | ||
| platform: 'win32', | ||
| arch: 'x64', | ||
| url: 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip', | ||
| size: 89 * 1024 * 1024, | ||
| extractPath: 'ffmpeg-master-latest-win64-gpl/bin/ffprobe.exe' | ||
| }, | ||
| arm64: { | ||
| version: 'latest', | ||
| platform: 'win32', | ||
| arch: 'arm64', | ||
| url: 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-winarm64-gpl.zip', | ||
| size: 85 * 1024 * 1024, | ||
| extractPath: 'ffmpeg-master-latest-winarm64-gpl/bin/ffprobe.exe' | ||
| } | ||
| }, | ||
| darwin: { | ||
| x64: { | ||
| version: 'latest', | ||
| platform: 'darwin', | ||
| arch: 'x64', | ||
| url: 'https://evermeet.cx/ffprobe/ffprobe-8.0.zip', | ||
| size: 65 * 1024 * 1024, | ||
| extractPath: 'ffprobe' | ||
| }, | ||
| arm64: { | ||
| version: 'latest', | ||
| platform: 'darwin', | ||
| arch: 'arm64', | ||
| url: 'https://evermeet.cx/ffprobe/ffprobe-8.0.zip', | ||
| size: 65 * 1024 * 1024, | ||
| extractPath: 'ffprobe' | ||
| } | ||
| }, | ||
| linux: { | ||
| x64: { | ||
| version: 'latest', | ||
| platform: 'linux', | ||
| arch: 'x64', | ||
| url: 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz', | ||
| size: 35 * 1024 * 1024, | ||
| extractPath: 'ffmpeg-*-amd64-static/ffprobe' | ||
| }, | ||
| arm64: { | ||
| version: 'latest', | ||
| platform: 'linux', | ||
| arch: 'arm64', | ||
| url: 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz', | ||
| size: 33 * 1024 * 1024, | ||
| extractPath: 'ffmpeg-*-arm64-static/ffprobe' | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
版本与下载源未做校验,建议补充 SHA256 校验或固定版本
当前使用 latest 与第三方源,无哈希校验。存在供应链与漂移风险。
- 为各版本配置 sha256,并在下载后计算校验(已留 TODO,可实现)
- 生产环境尽量固定具体版本而非 latest,必要时定期更新
| if (venvExists) { | ||
| try { | ||
| // 使用 uv 获取 Python 版本 | ||
| const uvPath = await uvBootstrapperService.getAvailableUvPath() | ||
| if (uvPath) { | ||
| const result = await this.executeCommand( | ||
| uvPath, | ||
| ['run', 'python', '--version'], | ||
| mediaServerPath | ||
| ) | ||
| const versionMatch = result.match(/Python (\S+)/) | ||
| pythonVersion = versionMatch ? versionMatch[1] : undefined | ||
| } | ||
| } catch (error) { |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
优先直接调用 venv 内的 python 获取版本,避免依赖 uv
当前通过 uv run python --version 获取版本。当 uv 遇到异常或未安装时会失败。既然已检测到 venvExists,也可直接调用 venv 的解释器更可靠。
- const uvPath = await uvBootstrapperService.getAvailableUvPath()
- if (uvPath) {
- const result = await this.executeCommand(
- uvPath,
- ['run', 'python', '--version'],
- mediaServerPath
- )
- const versionMatch = result.match(/Python (\S+)/)
- pythonVersion = versionMatch ? versionMatch[1] : undefined
- }
+ const result = await this.executeCommand(
+ pythonPath,
+ ['--version'],
+ mediaServerPath
+ )
+ const versionMatch = result.match(/Python (\S+)/)
+ pythonVersion = versionMatch ? versionMatch[1] : undefined📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (venvExists) { | |
| try { | |
| // 使用 uv 获取 Python 版本 | |
| const uvPath = await uvBootstrapperService.getAvailableUvPath() | |
| if (uvPath) { | |
| const result = await this.executeCommand( | |
| uvPath, | |
| ['run', 'python', '--version'], | |
| mediaServerPath | |
| ) | |
| const versionMatch = result.match(/Python (\S+)/) | |
| pythonVersion = versionMatch ? versionMatch[1] : undefined | |
| } | |
| } catch (error) { | |
| if (venvExists) { | |
| try { | |
| // 使用 uv 获取 Python 版本 | |
| const result = await this.executeCommand( | |
| pythonPath, | |
| ['--version'], | |
| mediaServerPath | |
| ) | |
| const versionMatch = result.match(/Python (\S+)/) | |
| pythonVersion = versionMatch ? versionMatch[1] : undefined | |
| } catch (error) { |
🤖 Prompt for AI Agents
In src/main/services/PythonVenvService.ts around lines 176-189, the code
currently always tries to get the Python version via uv run, which can fail if
uv is absent; since venvExists is true, first invoke the venv's interpreter
directly to determine the version (resolve the venv Python path using
platform-specific locations: <venv>/bin/python on Unix and
<venv>/Scripts/python.exe on Windows), check that the file exists/executable,
run it with --version and parse the output into pythonVersion; only if the venv
interpreter is missing or calling it fails, fall back to using
uvBootstrapperService.getAvailableUvPath() and the existing uv run approach, and
ensure all calls are wrapped in try/catch so errors fall back cleanly without
throwing.
| if (pythonVersion) { | ||
| args.push('--python', pythonVersion) | ||
| } | ||
|
|
There was a problem hiding this comment.
校验 pythonVersion,阻止非法入参并提升健壮性
即便禁用 shell,仍建议对白名单格式进行校验,避免异常参数导致 uv 行为不可预期。
- if (pythonVersion) {
- args.push('--python', pythonVersion)
- }
+ if (pythonVersion) {
+ // 仅允许形如 3、3.11、3.11.6 或 3.11.* 的版本标记
+ const isSafe = /^[0-9]+(\.[0-9]+){0,2}(\.\*)?$/.test(pythonVersion)
+ if (!isSafe) {
+ throw new Error(`非法的 Python 版本号: ${pythonVersion}`)
+ }
+ args.push('--python', pythonVersion)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (pythonVersion) { | |
| args.push('--python', pythonVersion) | |
| } | |
| if (pythonVersion) { | |
| // 仅允许形如 3、3.11、3.11.6 或 3.11.* 的版本标记 | |
| const isSafe = /^[0-9]+(\.[0-9]+){0,2}(\.\*)?$/.test(pythonVersion) | |
| if (!isSafe) { | |
| throw new Error(`非法的 Python 版本号: ${pythonVersion}`) | |
| } | |
| args.push('--python', pythonVersion) | |
| } |
🤖 Prompt for AI Agents
In src/main/services/PythonVenvService.ts around lines 331 to 334, validate the
incoming pythonVersion before pushing it into args: ensure it's a non-empty
string and matches an allowed whitelist pattern (e.g.
/^python(\d+(\.\d+)?)?$|^\/[A-Za-z0-9_\/\-.]+$/ to permit named interpreters
like "python3.8" or absolute paths), and if it fails validation either throw a
clear Error or skip adding the flag; this prevents illegal inputs from being
passed to the command and improves robustness.
| private async executeCommand(command: string, args: string[], cwd?: string): Promise<string> { | ||
| const { spawn } = await import('child_process') | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| const child = spawn(command, args, { | ||
| cwd, | ||
| stdio: 'pipe', | ||
| shell: process.platform === 'win32' | ||
| }) | ||
|
|
There was a problem hiding this comment.
严重:Windows 下使用 shell 执行带入参命令存在注入风险
executeCommand 在 win32 下启用了 shell:true,且 pythonVersion 源自外部参数。组合后经 shell 传递,存在命令注入风险。建议禁用 shell 并仅以可执行文件+参数方式调用。
应用此修复:
- const child = spawn(command, args, {
- cwd,
- stdio: 'pipe',
- shell: process.platform === 'win32'
- })
+ const child = spawn(command, args, {
+ cwd,
+ stdio: 'pipe',
+ shell: false,
+ windowsHide: true
+ })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private async executeCommand(command: string, args: string[], cwd?: string): Promise<string> { | |
| const { spawn } = await import('child_process') | |
| return new Promise((resolve, reject) => { | |
| const child = spawn(command, args, { | |
| cwd, | |
| stdio: 'pipe', | |
| shell: process.platform === 'win32' | |
| }) | |
| private async executeCommand(command: string, args: string[], cwd?: string): Promise<string> { | |
| const { spawn } = await import('child_process') | |
| return new Promise((resolve, reject) => { | |
| const child = spawn(command, args, { | |
| cwd, | |
| stdio: 'pipe', | |
| shell: false, | |
| windowsHide: true | |
| }) |
🤖 Prompt for AI Agents
In src/main/services/PythonVenvService.ts around lines 459 to 468, the current
executeCommand enables shell:true on Windows which allows untrusted
pythonVersion/other inputs to be interpreted by the shell and creates a
command-injection risk; change the spawn call to avoid using the shell (remove
or set shell:false) and invoke the Python executable directly with the args
array (ensure the command is the executable path and all user-controlled data
goes only into the args array), and optionally validate/sanitize pythonVersion
or resolve it to a safe absolute path before calling spawn.
| "download": { | ||
| "cancel": "取消下载", | ||
| "cancelled": "下载已取消", | ||
| "downloading": "下载中", | ||
| "failed": "下载失败,请重试", | ||
| "success": "下载完成" | ||
| }, | ||
| "path": { | ||
| "browse": "浏览", | ||
| "browse_title": "选择 FFprobe 可执行文件", | ||
| "invalid": "路径无效或文件不存在", | ||
| "label": "路径", | ||
| "placeholder": "FFprobe 路径将在下载后自动填入,也可手动指定", | ||
| "valid": "路径验证成功", | ||
| "validation_failed": "路径验证失败" | ||
| }, | ||
| "status": { | ||
| "downloading": "下载中...", | ||
| "installed": "已安装", | ||
| "label": "状态", | ||
| "loading": "检测中...", | ||
| "not_installed": "未安装", | ||
| "system_version": "系统版本", | ||
| "unknown": "状态未知" | ||
| }, | ||
| "title": "媒体分析组件 (FFprobe)", | ||
| "uninstall": { | ||
| "button": "卸载", | ||
| "confirm": "确认卸载", | ||
| "confirm_description": "此操作将删除已下载的 FFprobe 文件,但不会影响系统安装的版本。", | ||
| "confirm_title": "确认卸载 FFprobe?", | ||
| "failed": "卸载失败,请重试", | ||
| "success": "FFprobe 卸载成功" | ||
| }, | ||
| "version": "版本" | ||
| }, |
There was a problem hiding this comment.
补充“安装”文案的翻译键
前端组件会改为读取 settings.plugins.ffprobe.download.install,这里需要同步新增对应键值(例如 "install": "安装 FFprobe")。否则调用会返回空字符串。
🤖 Prompt for AI Agents
In src/renderer/src/i18n/locales/zh-cn.json around lines 380 to 415, the
translations for the FFprobe download object are missing the "install" key
referenced by settings.plugins.ffprobe.download.install; add an "install" entry
(e.g., "install": "安装 FFprobe") under the "download" object so the frontend
returns the correct localized string instead of an empty value.
d013f89 to
ecd57ee
Compare
ecd57ee to
448a647
Compare
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 4
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
src/renderer/src/pages/settings/FFmpegSection.tsx(1 hunks)src/renderer/src/pages/settings/FFprobeSection.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/renderer/src/**/*.{ts,tsx,scss,css}
📄 CodeRabbit inference engine (CLAUDE.md)
优先使用 CSS 变量,避免硬编码样式值(颜色等)
Files:
src/renderer/src/pages/settings/FFprobeSection.tsxsrc/renderer/src/pages/settings/FFmpegSection.tsx
src/renderer/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
尺寸与时长等不要硬编码,优先使用 useTheme() 的 token 或集中样式变量(如 motionDurationMid、borderRadiusSM/MD)
Files:
src/renderer/src/pages/settings/FFprobeSection.tsxsrc/renderer/src/pages/settings/FFmpegSection.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 定制 antd 组件样式优先使用 styled-components 包装 styled(Component),避免全局 classNames
项目的图标统一使用 lucide-react,不使用 emoji 作为图标
组件/Hook 顶层必须通过 useStore(selector) 使用 Zustand,禁止在 useMemo/useEffect 内部调用 store Hook
避免使用返回对象的 Zustand 选择器(如 useStore(s => ({ a: s.a, b: s.b })));应使用单字段选择器或配合 shallow 比较器
遵循 React「副作用与状态更新」规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定等
统一使用 loggerService 记录日志而不是 console
logger 使用示例中第二个参数必须为对象字面量(如 logger.error('msg', { error }))
任何组件或页面都不要写入 media 元素的 currentTime,播放器控制由编排器统一负责
在 styled-components 中:主题相关属性使用 AntD CSS 变量(如 var(--ant-color-bg-elevated));
在 styled-components 中:设计系统常量(尺寸、动画、层级、字体、毛玻璃等)使用 JS 常量(如 SPACING、BORDER_RADIUS、Z_INDEX、FONT_SIZES 等)
Files:
src/renderer/src/pages/settings/FFprobeSection.tsxsrc/renderer/src/pages/settings/FFmpegSection.tsx
🧬 Code graph analysis (2)
src/renderer/src/pages/settings/FFprobeSection.tsx (4)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)src/renderer/src/pages/settings/AboutSettings.tsx (1)
SettingRowTitle(412-424)src/renderer/src/infrastructure/styles/theme.ts (6)
SPACING(43-58)BORDER_RADIUS(61-72)ANIMATION_DURATION(91-100)EASING(103-114)FONT_WEIGHTS(11-22)FONT_SIZES(25-40)
src/renderer/src/pages/settings/FFmpegSection.tsx (3)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)src/renderer/src/infrastructure/styles/theme.ts (6)
SPACING(43-58)BORDER_RADIUS(61-72)ANIMATION_DURATION(91-100)EASING(103-114)FONT_WEIGHTS(11-22)FONT_SIZES(25-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test (windows-latest, 20)
- GitHub Check: test (ubuntu-latest, 20)
- GitHub Check: test (macos-latest, 20)
| setTimeout(() => { | ||
| setIsDownloading(false) | ||
| setShowSuccessState(false) | ||
| setFFmpegStatus(currentStatus) | ||
| // 更新 FFmpeg 路径为下载后的路径 | ||
| setFFmpegPath(currentStatus.path) | ||
| // 自动开始预热 | ||
| handleWarmup() | ||
| }, 2000) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
清理成功状态定时器防止卸载后 setState
下载完成后用 setTimeout 延迟复位,但没有保存并清理句柄。若组件在 2 秒内卸载,会对已卸载组件执行 setState。建议与 FFprobeSection 类似,使用 useRef 保存定时器,并在新下载开始或组件卸载时 clearTimeout。
- setTimeout(() => {
+ if (successTimeoutRef.current) {
+ clearTimeout(successTimeoutRef.current)
+ }
+ successTimeoutRef.current = setTimeout(() => {
setIsDownloading(false)
setShowSuccessState(false)
setFFmpegStatus(currentStatus)
// 更新 FFmpeg 路径为下载后的路径
setFFmpegPath(currentStatus.path)
// 自动开始预热
handleWarmup()
- }, 2000)
+ }, 2000)顶部补充:
const isCancellingRef = useRef(false)
const isCompletionHandledRef = useRef(false)
+const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)并新增清理逻辑:
useEffect(() => {
fetchFFmpegStatus()
}, [fetchFFmpegStatus, refreshKey])
+
+useEffect(() => {
+ return () => {
+ if (successTimeoutRef.current) {
+ clearTimeout(successTimeoutRef.current)
+ }
+ }
+}, [])🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/FFmpegSection.tsx around lines 152-160, the
setTimeout used to reset success state isn't stored or cleared, allowing
setState on an unmounted component; store the timeout id in a useRef, assign the
timer to that ref when scheduling the 2s reset, call clearTimeout on that ref
before starting a new download and in a useEffect cleanup (component unmount),
and null out the ref after clearing so the delayed setState won't run after
unmount or when a new download begins.
| title: t('settings.plugins.ffmpeg.path.browse_title'), | ||
| properties: ['openFile'], | ||
| filters: [{ name: 'FFmpeg 可执行文件', extensions: ['exe', 'app', '*'] }] | ||
| }) |
There was a problem hiding this comment.
复用翻译键避免硬编码中文
文件选择对话框名称写死为“FFmpeg 可执行文件”,会导致非中文语言界面出现中文。请改用翻译函数,并在语言包中新增相应键值,例如 settings.plugins.ffmpeg.path.file_filter。
- filters: [{ name: 'FFmpeg 可执行文件', extensions: ['exe', 'app', '*'] }]
+ filters: [
+ {
+ name: t('settings.plugins.ffmpeg.path.file_filter'),
+ extensions: ['exe', 'app', '*']
+ }
+ ](As per coding guidelines)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| title: t('settings.plugins.ffmpeg.path.browse_title'), | |
| properties: ['openFile'], | |
| filters: [{ name: 'FFmpeg 可执行文件', extensions: ['exe', 'app', '*'] }] | |
| }) | |
| title: t('settings.plugins.ffmpeg.path.browse_title'), | |
| properties: ['openFile'], | |
| filters: [ | |
| { | |
| name: t('settings.plugins.ffmpeg.path.file_filter'), | |
| extensions: ['exe', 'app', '*'] | |
| } | |
| ] | |
| }) |
🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/FFmpegSection.tsx around lines 237 to 240,
the file dialog filter name is hardcoded as "FFmpeg 可执行文件"; replace that literal
with the translation call (e.g. t('settings.plugins.ffmpeg.path.file_filter'))
so the dialog label is localized, update the language JSON files to add the new
key settings.plugins.ffmpeg.path.file_filter with appropriate translations, and
ensure the filters array uses that translated string instead of the Chinese
literal.
| setTimeout(() => { | ||
| setIsDownloading(false) | ||
| setShowSuccessState(false) | ||
| setFFprobeStatus(currentStatus) | ||
| // 更新 FFprobe 路径为下载后的路径 | ||
| setFFprobePath(currentStatus.path) | ||
| }, 2000) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
为成功状态的定时器添加清理逻辑
setTimeout 创建后未保存句柄并在组件卸载时清理,下载流程结束前如果页面跳转,后续的 setState 会对已卸载组件执行。建议用 useRef 记录定时器并在 useEffect 清理,下载流程重新开始前也应 clearTimeout。
- setTimeout(() => {
+ if (successTimeoutRef.current) {
+ clearTimeout(successTimeoutRef.current)
+ }
+ successTimeoutRef.current = setTimeout(() => {
setIsDownloading(false)
setShowSuccessState(false)
setFFprobeStatus(currentStatus)
// 更新 FFprobe 路径为下载后的路径
setFFprobePath(currentStatus.path)
- }, 2000)
+ }, 2000)并在顶部新增:
const isCancellingRef = useRef(false)
const isCompletionHandledRef = useRef(false)
+const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)最后在组件末尾补充:
useEffect(() => {
fetchFFprobeStatus()
}, [fetchFFprobeStatus, refreshKey])
+
+useEffect(() => {
+ return () => {
+ if (successTimeoutRef.current) {
+ clearTimeout(successTimeoutRef.current)
+ }
+ }
+}, [])📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setTimeout(() => { | |
| setIsDownloading(false) | |
| setShowSuccessState(false) | |
| setFFprobeStatus(currentStatus) | |
| // 更新 FFprobe 路径为下载后的路径 | |
| setFFprobePath(currentStatus.path) | |
| }, 2000) | |
| if (successTimeoutRef.current) { | |
| clearTimeout(successTimeoutRef.current) | |
| } | |
| successTimeoutRef.current = setTimeout(() => { | |
| setIsDownloading(false) | |
| setShowSuccessState(false) | |
| setFFprobeStatus(currentStatus) | |
| // 更新 FFprobe 路径为下载后的路径 | |
| setFFprobePath(currentStatus.path) | |
| }, 2000) |
🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/FFprobeSection.tsx around lines 120-126, the
setTimeout used to reset download state is not tracked or cleared, which can
call setState after the component unmounts; create a timerRef using
useRef<number | null>(null) at the top of the component, store the timeout id in
timerRef when calling setTimeout, call clearTimeout(timerRef.current) before
starting any new timeout (e.g., when a new download begins or status changes),
and add a useEffect cleanup at the component end that clears the timeout and
sets timerRef.current = null on unmount; ensure types align with
window.setTimeout return type for the project.
| title: t('settings.plugins.ffprobe.path.browse_title'), | ||
| properties: ['openFile'], | ||
| filters: [{ name: 'FFprobe 可执行文件', extensions: ['exe', 'app', '*'] }] | ||
| }) |
There was a problem hiding this comment.
替换文件对话框标题为翻译键
这里直接写死了“FFprobe 可执行文件”,绕过了现有 i18n 体系,非中文环境会看到中文文案。请改为使用 t(...) 并在语言包中补足键值,例如 settings.plugins.ffprobe.path.file_filter。
- filters: [{ name: 'FFprobe 可执行文件', extensions: ['exe', 'app', '*'] }]
+ filters: [
+ {
+ name: t('settings.plugins.ffprobe.path.file_filter'),
+ extensions: ['exe', 'app', '*']
+ }
+ ](As per coding guidelines)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| title: t('settings.plugins.ffprobe.path.browse_title'), | |
| properties: ['openFile'], | |
| filters: [{ name: 'FFprobe 可执行文件', extensions: ['exe', 'app', '*'] }] | |
| }) | |
| title: t('settings.plugins.ffprobe.path.browse_title'), | |
| properties: ['openFile'], | |
| filters: [ | |
| { | |
| name: t('settings.plugins.ffprobe.path.file_filter'), | |
| extensions: ['exe', 'app', '*'] | |
| } | |
| ] | |
| }) |
🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/FFprobeSection.tsx around lines 203 to 206,
the file dialog filter label is hard-coded as "FFprobe 可执行文件"; replace this
literal with the i18n call t('settings.plugins.ffprobe.path.file_filter') (or
the agreed key) so the UI uses translations, and then add the corresponding
key/value to all language resource files (e.g. en, zh, etc.) with appropriate
translated strings; keep the rest of the filters array unchanged and ensure the
t(...) import/context is available in this file.
…kage - Create packages/shared/types/system.ts with Platform and Arch type definitions - Remove duplicate type definitions from FFmpegDownloadService - Remove duplicate type definitions from UvBootstrapperService - Import shared types from @shared/types/system in both services This change improves code maintainability by centralizing common system type definitions.
Ensure media server port is properly cleared and frontend is notified when the process stops or crashes to prevent stale port usage. Changes: - Update notifyPortChanged signature to accept number | null - Clear this.port to null in both normal stop and crash exit handlers - Notify frontend with null port when server stops - Update SessionService listener to handle null port notifications - Add appropriate logging for port clearing events This prevents the frontend from continuing to use expired ports after the media server has stopped, requiring manual app restart.
Fix potential memory leak and unmounted component state update in MediaServerSection by properly managing setTimeout lifecycle: - Add successTimeoutRef to store timeout handle - Clear timeout in effect cleanup on component unmount - Clear timeout before creating new one in handleInstall - Clear timeout in error handler to prevent orphaned timers - Set ref to null after clearing to maintain clean state This prevents "Can't perform a React state update on an unmounted component" warnings and ensures proper cleanup of async operations. Co-Authored-By: Claude <noreply@anthropic.com>
…rtup - Add 'starting' status immediately after installation completes - Move fetchServerInfo() to finally block to ensure status refresh - Prevent confusing 'stopped' status display during startup process
Remove finally block that immediately clears installProgress, preventing callers from reading final "completed" or "error" state. Instead: - Clear installProgress at start of new installation - Preserve final state after completion/error for caller consumption - Remove unnecessary finally block cleanup This ensures MediaServerSection and other callers can reliably read the final installation state. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/services/FFmpegDownloadService.ts (1)
49-159: 请固定版本并补齐哈希校验,避免供应链风险这里依旧使用
latest与第三方源且未做 SHA256 校验,新加入的 FFprobe 表也沿用了同样模式。任何上游变更都会直接影响用户,甚至可能被投毒。建议:1)改为显式写死版本号与下载地址;2)为每个版本填入官方公布的 SHA256;3)在performDownload中下载完成后进入verifying阶段对比哈希(crypto.createHash('sha256'))。只有校验通过再继续解压,否则失败并清理临时文件。这两步缺一不可。Also applies to: 219-275
♻️ Duplicate comments (1)
src/main/services/PythonVenvService.ts (1)
460-468: 仍需移除 Windows shell=true 的命令注入风险这里依旧在 Windows 下以
shell: true运行spawn。initializeEnvironment()/createVenv()会把 UI 传入的pythonVersion等参数直接塞进args,经过cmd.exe时可以被构造为恶意命令片段,导致任意代码执行。这一问题之前已提示过,请务必修正。同样的修复方式:
- const child = spawn(command, args, { - cwd, - stdio: 'pipe', - shell: process.platform === 'win32' - }) + const child = spawn(command, args, { + cwd, + stdio: 'pipe', + shell: false, + windowsHide: true + })结合
uvBootstrapperService的调整即可统一规避注入风险。
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (7)
packages/shared/types/system.ts(1 hunks)src/main/services/FFmpegDownloadService.ts(23 hunks)src/main/services/MediaServerService.ts(1 hunks)src/main/services/PythonVenvService.ts(1 hunks)src/main/services/UvBootstrapperService.ts(1 hunks)src/renderer/src/pages/settings/MediaServerSection.tsx(1 hunks)src/renderer/src/services/SessionService.ts(5 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/renderer/src/**/*.{ts,tsx,scss,css}
📄 CodeRabbit inference engine (CLAUDE.md)
优先使用 CSS 变量,避免硬编码样式值(颜色等)
Files:
src/renderer/src/pages/settings/MediaServerSection.tsxsrc/renderer/src/services/SessionService.ts
src/renderer/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
尺寸与时长等不要硬编码,优先使用 useTheme() 的 token 或集中样式变量(如 motionDurationMid、borderRadiusSM/MD)
Files:
src/renderer/src/pages/settings/MediaServerSection.tsxsrc/renderer/src/services/SessionService.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 定制 antd 组件样式优先使用 styled-components 包装 styled(Component),避免全局 classNames
项目的图标统一使用 lucide-react,不使用 emoji 作为图标
组件/Hook 顶层必须通过 useStore(selector) 使用 Zustand,禁止在 useMemo/useEffect 内部调用 store Hook
避免使用返回对象的 Zustand 选择器(如 useStore(s => ({ a: s.a, b: s.b })));应使用单字段选择器或配合 shallow 比较器
遵循 React「副作用与状态更新」规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定等
统一使用 loggerService 记录日志而不是 console
logger 使用示例中第二个参数必须为对象字面量(如 logger.error('msg', { error }))
任何组件或页面都不要写入 media 元素的 currentTime,播放器控制由编排器统一负责
在 styled-components 中:主题相关属性使用 AntD CSS 变量(如 var(--ant-color-bg-elevated));
在 styled-components 中:设计系统常量(尺寸、动画、层级、字体、毛玻璃等)使用 JS 常量(如 SPACING、BORDER_RADIUS、Z_INDEX、FONT_SIZES 等)
Files:
src/renderer/src/pages/settings/MediaServerSection.tsxsrc/main/services/UvBootstrapperService.tssrc/main/services/MediaServerService.tssrc/main/services/PythonVenvService.tssrc/main/services/FFmpegDownloadService.tspackages/shared/types/system.tssrc/renderer/src/services/SessionService.ts
🧬 Code graph analysis (6)
src/renderer/src/pages/settings/MediaServerSection.tsx (6)
src/renderer/src/services/Logger.ts (3)
loggerService(817-817)info(436-438)error(422-424)src/main/services/MediaServerService.ts (1)
MediaServerInfo(29-36)src/main/services/PythonVenvService.ts (1)
InstallProgress(25-29)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)src/renderer/src/pages/settings/AboutSettings.tsx (1)
SettingRowTitle(412-424)src/renderer/src/infrastructure/styles/theme.ts (6)
SPACING(43-58)BORDER_RADIUS(61-72)ANIMATION_DURATION(91-100)EASING(103-114)FONT_WEIGHTS(11-22)FONT_SIZES(25-40)
src/main/services/UvBootstrapperService.ts (3)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/system.ts (2)
Platform(2-2)Arch(3-3)src/main/services/FFmpegDownloadService.ts (1)
DownloadProgress(25-32)
src/main/services/MediaServerService.ts (5)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)info(436-438)src/main/services/FFmpegDownloadService.ts (1)
FFmpegDownloadService(277-1047)src/main/utils/index.ts (1)
getDataPath(11-17)src/main/services/UvBootstrapperService.ts (1)
uvBootstrapperService(1185-1185)src/main/services/PythonVenvService.ts (1)
pythonVenvService(519-519)
src/main/services/PythonVenvService.ts (2)
src/renderer/src/services/Logger.ts (1)
loggerService(817-817)src/main/services/UvBootstrapperService.ts (1)
uvBootstrapperService(1185-1185)
src/main/services/FFmpegDownloadService.ts (2)
packages/shared/types/system.ts (2)
Platform(2-2)Arch(3-3)src/main/services/UvBootstrapperService.ts (1)
DownloadProgress(23-30)
src/renderer/src/services/SessionService.ts (1)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test (macos-latest, 20)
- GitHub Check: test (windows-latest, 20)
- GitHub Check: test (ubuntu-latest, 20)
🔇 Additional comments (2)
src/renderer/src/pages/settings/MediaServerSection.tsx (2)
127-183: 定时器清理已正确实现轮询安装进度的 Effect 现已正确清理所有定时器:
- 旧的
setTimeout在创建新定时器前被清除(153-157 行)- cleanup 函数清理 interval 和 timeout(174-182 行)
- 所有相关清理点(handleInstall、错误处理)都正确处理了 timeout
这解决了之前审查中提到的"组件卸载后状态更新"问题。
505-607: 样式组件遵循设计规范样式组件正确使用了设计系统:
- 使用 CSS 变量处理主题相关属性(如
var(--ant-color-success)、var(--ant-color-primary))- 使用 JS 常量处理设计系统常量(
SPACING、BORDER_RADIUS、FONT_SIZES、ANIMATION_DURATION、EASING)- 通过
styled(Button)包装 antd 组件以实现定制完全符合编码规范。
基于编码规范
| this.process.on('exit', (code, signal) => { | ||
| logger.warn('Media Server 进程退出', { code, signal }) | ||
|
|
||
| const previousPort = this.port | ||
|
|
||
| this.status = 'stopped' | ||
| this.process = null | ||
| this.startTime = null | ||
| this.port = null | ||
|
|
||
| // 通知前端端口已失效 | ||
| if (previousPort !== null) { | ||
| this.notifyPortChanged(null) | ||
| } | ||
|
|
||
| // 如果是异常退出,尝试重启 | ||
| if (code !== 0 && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) { | ||
| this.restartAttempts++ | ||
| logger.info('尝试重启 Media Server', { | ||
| attempt: this.restartAttempts, | ||
| maxAttempts: this.MAX_RESTART_ATTEMPTS | ||
| }) | ||
|
|
||
| setTimeout(() => { | ||
| this.start(finalConfig).catch((error) => { | ||
| logger.error('重启 Media Server 失败', { error }) | ||
| }) | ||
| }, 2000 * this.restartAttempts) // 递增延迟 | ||
| } else if (this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) { | ||
| logger.error('Media Server 重启次数过多,停止重启') | ||
| this.lastError = '服务异常退出次数过多' | ||
| this.status = 'error' | ||
| } | ||
| }) |
There was a problem hiding this comment.
阻止 stop() 误触发自重启
调用 stop() 时子进程通常以 code = null、signal = 'SIGTERM' 退出,但这里直接判断 code !== 0 会为 true,从而排队 this.start(finalConfig)。结果是点击“停止”后服务立刻又被拉起,主进程永远停不下来。这是严重的行为缺陷。
建议在 exit 回调一开始记录 const wasStopping = this.status === 'stopping',并改用 const exitCode = typeof code === 'number' ? code : 0、const shouldRestart = !wasStopping && signal === null && exitCode !== 0。只有在 shouldRestart 为真时才递增 restartAttempts 并排队自重启,其他分支(包含人工 stop、SIGTERM、SIGKILL 强杀等)需立刻清零 restartAttempts。示例修复:
- this.process.on('exit', (code, signal) => {
- logger.warn('Media Server 进程退出', { code, signal })
+ this.process.on('exit', (code, signal) => {
+ const wasStopping = this.status === 'stopping'
+ logger.warn('Media Server 进程退出', { code, signal })
...
- if (code !== 0 && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) {
+ const exitCode = typeof code === 'number' ? code : 0
+ const shouldRestart = !wasStopping && signal === null && exitCode !== 0
+
+ if (shouldRestart && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) {
...
- } else if (this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) {
+ } else if (shouldRestart && this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) {
...
- }
+ } else {
+ this.restartAttempts = 0
+ }这样手动 stop、不正常崩溃与强制 kill 才会被正确区分。
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| this.process.on('exit', (code, signal) => { | |
| logger.warn('Media Server 进程退出', { code, signal }) | |
| const previousPort = this.port | |
| this.status = 'stopped' | |
| this.process = null | |
| this.startTime = null | |
| this.port = null | |
| // 通知前端端口已失效 | |
| if (previousPort !== null) { | |
| this.notifyPortChanged(null) | |
| } | |
| // 如果是异常退出,尝试重启 | |
| if (code !== 0 && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) { | |
| this.restartAttempts++ | |
| logger.info('尝试重启 Media Server', { | |
| attempt: this.restartAttempts, | |
| maxAttempts: this.MAX_RESTART_ATTEMPTS | |
| }) | |
| setTimeout(() => { | |
| this.start(finalConfig).catch((error) => { | |
| logger.error('重启 Media Server 失败', { error }) | |
| }) | |
| }, 2000 * this.restartAttempts) // 递增延迟 | |
| } else if (this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) { | |
| logger.error('Media Server 重启次数过多,停止重启') | |
| this.lastError = '服务异常退出次数过多' | |
| this.status = 'error' | |
| } | |
| }) | |
| this.process.on('exit', (code, signal) => { | |
| const wasStopping = this.status === 'stopping' | |
| logger.warn('Media Server 进程退出', { code, signal }) | |
| const previousPort = this.port | |
| this.status = 'stopped' | |
| this.process = null | |
| this.startTime = null | |
| this.port = null | |
| // 通知前端端口已失效 | |
| if (previousPort !== null) { | |
| this.notifyPortChanged(null) | |
| } | |
| // 如果是异常退出,尝试重启 | |
| const exitCode = typeof code === 'number' ? code : 0 | |
| const shouldRestart = !wasStopping && signal === null && exitCode !== 0 | |
| if (shouldRestart && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) { | |
| this.restartAttempts++ | |
| logger.info('尝试重启 Media Server', { | |
| attempt: this.restartAttempts, | |
| maxAttempts: this.MAX_RESTART_ATTEMPTS | |
| }) | |
| setTimeout(() => { | |
| this.start(finalConfig).catch((error) => { | |
| logger.error('重启 Media Server 失败', { error }) | |
| }) | |
| }, 2000 * this.restartAttempts) // 递增延迟 | |
| } else if (shouldRestart && this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) { | |
| logger.error('Media Server 重启次数过多,停止重启') | |
| this.lastError = '服务异常退出次数过多' | |
| this.status = 'error' | |
| } else { | |
| // 手动 stop 或强杀等场景,不累计重启次数 | |
| this.restartAttempts = 0 | |
| } | |
| }) |
🤖 Prompt for AI Agents
In src/main/services/MediaServerService.ts around lines 339-372, the exit
handler treats any non-zero code as a crash which causes stop() (which yields
code=null and signal='SIGTERM') to trigger an unwanted restart; change the logic
to first capture const wasStopping = this.status === 'stopping', then normalize
the exit code with const exitCode = typeof code === 'number' ? code : 0, compute
const shouldRestart = !wasStopping && signal === null && exitCode !== 0, and
only when shouldRestart is true increment restartAttempts and schedule
this.start(finalConfig); in all other cases (including manual stop,
SIGTERM/SIGKILL) reset this.restartAttempts = 0 and avoid scheduling a restart;
keep the existing status/port clearing and notifyPortChanged behavior untouched
and set status='error' only when restartAttempts exceeds MAX_RESTART_ATTEMPTS.
| private async executeCommand(command: string, args: string[], cwd?: string): Promise<string> { | ||
| return new Promise((resolve, reject) => { | ||
| const child = spawn(command, args, { | ||
| cwd, | ||
| stdio: 'pipe', | ||
| shell: process.platform === 'win32' | ||
| }) | ||
|
|
||
| let stdout = '' | ||
| let stderr = '' | ||
|
|
||
| child.stdout?.on('data', (data) => { | ||
| stdout += data.toString() | ||
| }) | ||
|
|
||
| child.stderr?.on('data', (data) => { | ||
| stderr += data.toString() | ||
| }) | ||
|
|
||
| child.on('close', (code) => { | ||
| if (code === 0) { | ||
| resolve(stdout.trim()) | ||
| } else { | ||
| reject( | ||
| new Error( | ||
| `命令执行失败: ${command} ${args.join(' ')}\n退出代码: ${code}\n错误输出: ${stderr}` | ||
| ) | ||
| ) | ||
| } | ||
| }) | ||
|
|
||
| child.on('error', (error) => { | ||
| reject(error) | ||
| }) | ||
| }) | ||
| } |
There was a problem hiding this comment.
关闭 Windows shell=true 的注入面
executeCommand 在 Windows 下仍使用 shell: true。当后续调用诸如 installPackage() 时,packageName、version 等来自用户/配置的字符串会直接经 cmd.exe 解析,一旦字符串里包含诸如 "; calc.exe; " 等片段,就能突破引号约束把兼容命令注入进来,导致任意命令执行。这是严重安全漏洞。
请改为直接调用二进制,禁用 shell,并顺带隐藏窗口。例如:
- const child = spawn(command, args, {
- cwd,
- stdio: 'pipe',
- shell: process.platform === 'win32'
- })
+ const child = spawn(command, args, {
+ cwd,
+ stdio: 'pipe',
+ shell: false,
+ windowsHide: true
+ })如需 PATH 解析,可保持 command 为 'uv'/'where' 等,Node 会处理 PATHEXT。这样可彻底消除此注入面。
🤖 Prompt for AI Agents
In src/main/services/UvBootstrapperService.ts around lines 343-378, the current
executeCommand enables shell on Windows which permits command injection via
untrusted args; change the spawn call to invoke the binary directly (no shell)
and add windowsHide: true to avoid showing a window on Windows: remove or set
shell: false, ensure args are passed only via the args array (do not interpolate
user/config strings into a single command string), keep stdio: 'pipe', and
preserve the same stdout/stderr handling and exit-code rejection so callers
receive the same error details.
| const handleInstall = useCallback(async () => { | ||
| try { | ||
| // 清理之前的 timeout(如果存在) | ||
| if (successTimeoutRef.current) { | ||
| clearTimeout(successTimeoutRef.current) | ||
| successTimeoutRef.current = null | ||
| } | ||
|
|
||
| isCompletionHandledRef.current = false | ||
| setIsInstalling(true) | ||
| updateInstallProgress({ stage: 'init', message: '正在检查依赖...', percent: 0 }) | ||
|
|
||
| const ensureDependency = async ( | ||
| type: DependencyType, | ||
| label: 'FFprobe' | 'FFmpeg', | ||
| startPercent: number, | ||
| completionPercent: number | ||
| ) => { | ||
| const checkExists = | ||
| type === 'ffprobe' | ||
| ? () => window.api.ffprobe.checkExists() | ||
| : () => window.api.ffmpeg.checkExists() | ||
| const download = | ||
| type === 'ffprobe' | ||
| ? () => window.api.ffprobe.download.download() | ||
| : () => window.api.ffmpeg.download.download() | ||
|
|
||
| logger.info(`检查 ${label} 安装状态`) | ||
| const exists = await checkExists() | ||
|
|
||
| if (exists) { | ||
| logger.info(`${label} 已安装,跳过下载`) | ||
| updateInstallProgress({ | ||
| stage: 'deps', | ||
| message: `${label} 已就绪`, | ||
| percent: completionPercent | ||
| }) | ||
| onDependencyReady?.(type) | ||
| return | ||
| } | ||
|
|
||
| logger.info(`${label} 未安装,开始下载`) | ||
| updateInstallProgress({ | ||
| stage: 'deps', | ||
| message: `正在安装 ${label}...`, | ||
| percent: startPercent | ||
| }) | ||
| message.info(`正在安装 ${label},请稍候...`) | ||
|
|
||
| const result = await download() | ||
| if (!result) { | ||
| const installedAfterAttempt = await checkExists() | ||
| if (!installedAfterAttempt) { | ||
| throw new Error(`${label} 安装失败,请先完成 ${label} 安装后重试`) | ||
| } | ||
| } | ||
|
|
||
| updateInstallProgress({ | ||
| stage: 'deps', | ||
| message: `${label} 安装完成`, | ||
| percent: completionPercent | ||
| }) | ||
| message.success(`${label} 安装完成`) | ||
| onDependencyReady?.(type) | ||
| } | ||
|
|
||
| // 依赖安装:先安装 FFprobe,再安装 FFmpeg | ||
| await ensureDependency('ffprobe', 'FFprobe', 5, 10) | ||
| await ensureDependency('ffmpeg', 'FFmpeg', 15, 20) | ||
|
|
||
| updateInstallProgress({ stage: 'init', message: '正在检查环境...', percent: 25 }) | ||
|
|
||
| // 步骤 1: 检查并安装 UV | ||
| logger.info('检查 UV 安装状态') | ||
| const uvInfo = await window.api.uv.checkInstallation() | ||
|
|
||
| if (!uvInfo.exists) { | ||
| logger.info('UV 未安装,开始下载...') | ||
| updateInstallProgress({ stage: 'init', message: '正在下载 UV 包管理器...', percent: 35 }) | ||
|
|
||
| // 下载 UV | ||
| const uvDownloaded = await window.api.uv.download() | ||
| if (!uvDownloaded) { | ||
| throw new Error('UV 下载失败') | ||
| } | ||
|
|
||
| logger.info('UV 下载完成') | ||
| updateInstallProgress({ stage: 'init', message: 'UV 安装完成', percent: 45 }) | ||
| } else { | ||
| logger.info('UV 已存在,跳过下载', { uvInfo }) | ||
| updateInstallProgress({ stage: 'init', message: 'UV 已就绪', percent: 45 }) | ||
| } | ||
|
|
||
| // 步骤 2: 初始化 Python 环境(uv 会自动下载匹配的 Python 解释器) | ||
| logger.info('开始初始化 Python 环境') | ||
| updateInstallProgress({ stage: 'init', message: '正在初始化 Python 环境...', percent: 55 }) | ||
|
|
||
| const result = await window.api.pythonVenv.initialize() | ||
| if (!result) { | ||
| throw new Error('Python 环境初始化失败') | ||
| } | ||
|
|
||
| logger.info('Media Server 环境安装完成') | ||
|
|
||
| // 步骤 3: 安装完成后自动启动 Media Server | ||
| logger.info('准备启动 Media Server') | ||
| updateInstallProgress({ | ||
| stage: 'completed', | ||
| message: '正在启动 Media Server...', | ||
| percent: 95 | ||
| }) | ||
|
|
||
| // 先设置状态为"启动中",避免用户看到"已停止"状态 | ||
| setServerInfo({ status: 'starting' }) | ||
|
|
||
| try { | ||
| const startResult = await window.api.mediaServer.start() | ||
| if (startResult) { | ||
| logger.info('Media Server 启动成功') | ||
| } else { | ||
| logger.warn('Media Server 启动失败,但环境安装成功') | ||
| } | ||
| } catch (startError) { | ||
| logger.error('启动 Media Server 失败,但环境安装成功:', { error: startError }) | ||
| // 不抛出错误,因为安装已成功 | ||
| } finally { | ||
| // 无论启动成功与否,都刷新服务器状态以显示最新状态 | ||
| await fetchServerInfo() | ||
| } | ||
| } catch (error) { | ||
| // 清理 timeout | ||
| if (successTimeoutRef.current) { | ||
| clearTimeout(successTimeoutRef.current) | ||
| successTimeoutRef.current = null | ||
| } | ||
|
|
||
| setIsInstalling(false) | ||
| updateInstallProgress(null) | ||
| message.error( | ||
| error instanceof Error ? error.message : 'Media Server 环境安装失败,请稍后重试' | ||
| ) | ||
| logger.error('安装 Media Server 环境失败:', { error }) | ||
| } | ||
| }, [fetchServerInfo, onDependencyReady, updateInstallProgress]) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
考虑提取辅助函数以降低复杂度
handleInstall 函数较长(144 行),编排了多个安装步骤(依赖检查、下载、UV 安装、虚拟环境初始化、服务器启动)。虽然当前实现逻辑清晰且正确,但可以考虑将 ensureDependency 等辅助逻辑提取为组件外部或自定义 Hook 中的独立函数,以提升可测试性和可维护性。
这是一个可选的代码组织改进,不影响当前功能的正确性。
🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/MediaServerSection.tsx around lines 186 to
329, the handleInstall function is large and should be refactored: extract the
ensureDependency logic into a standalone helper (or hook) that accepts
parameters (type, label, startPercent, completionPercent, onDependencyReady,
updateInstallProgress, message, logger, window.api) and returns a promise;
extract the UV install/check block, the Python venv initialization, and the
media server start/refresh logic into separate functions (or methods in the
hook) that accept the minimal state setters and utilities they need
(setIsInstalling, setServerInfo, fetchServerInfo, updateInstallProgress,
message, logger, window.api); update handleInstall to call these helpers in
sequence and handle the try/catch and timeout cleanup as before; ensure refs
(successTimeoutRef, isCompletionHandledRef) and callbacks (onDependencyReady,
fetchServerInfo) are passed in or referenced from the hook, and add small unit
tests for each extracted function to validate success/failure flows.
- Create shared types module for Media Server and Python Venv - Add media-server.ts with MediaServerInfo, MediaServerStatus, PythonVenvInfo, and InstallProgress types - Remove duplicate type definitions from MediaServerService, PythonVenvService, and MediaServerSection - Update imports to use @shared/types across main and renderer processes - Maintain backward compatibility by re-exporting types from service files - All type checks passing
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/main/services/MediaServerService.ts (1)
318-350: 修复 stop() 后被误判为崩溃重启的问题
stop()会让子进程以code = null、signal = 'SIGTERM'退出,当前逻辑把它当成code !== 0的异常重启,导致手动停止后服务立刻又被拉起。请在退出回调中识别this.status === 'stopping'并只在真正崩溃时重试。- this.process.on('exit', (code, signal) => { - logger.warn('Media Server 进程退出', { code, signal }) + this.process.on('exit', (code, signal) => { + const wasStopping = this.status === 'stopping' + logger.warn('Media Server 进程退出', { code, signal, wasStopping }) ... - if (code !== 0 && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) { + const exitCode = typeof code === 'number' ? code : 0 + const shouldRestart = !wasStopping && signal === null && exitCode !== 0 + + if (shouldRestart && this.restartAttempts < this.MAX_RESTART_ATTEMPTS) { ... - } else if (this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) { + } else if (shouldRestart && this.restartAttempts >= this.MAX_RESTART_ATTEMPTS) { logger.error('Media Server 重启次数过多,停止重启') this.lastError = '服务异常退出次数过多' this.status = 'error' + } else { + this.restartAttempts = 0 }src/main/services/PythonVenvService.ts (2)
413-429: 避免在主进程中同步删除大型目录
fs.rmSync会阻塞事件循环,删除几十/上百 MB 的 venv 时主进程会卡顿。改用异步 Promise 版本并await,保持主线程响应。- // 递归删除目录 - fs.rmSync(venvPath, { recursive: true, force: true }) + // 递归删除目录(异步,避免阻塞主进程) + await fs.promises.rm(venvPath, { recursive: true, force: true })
440-448: 关闭 Windows shell 执行以消除命令注入风险在 Windows 下启用
shell: true会让用户输入(如pythonVersion)被当作命令拼接,存在注入隐患。请强制shell: false并隐藏窗口。- const child = spawn(command, args, { - cwd, - stdio: 'pipe', - shell: process.platform === 'win32' - }) + const child = spawn(command, args, { + cwd, + stdio: 'pipe', + shell: false, + windowsHide: true + })
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
packages/shared/types/index.ts(1 hunks)packages/shared/types/media-server.ts(1 hunks)src/main/services/MediaServerService.ts(1 hunks)src/main/services/PythonVenvService.ts(1 hunks)src/renderer/src/pages/settings/MediaServerSection.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 定制 antd 组件样式优先使用 styled-components 包装 styled(Component),避免全局 classNames
项目的图标统一使用 lucide-react,不使用 emoji 作为图标
组件/Hook 顶层必须通过 useStore(selector) 使用 Zustand,禁止在 useMemo/useEffect 内部调用 store Hook
避免使用返回对象的 Zustand 选择器(如 useStore(s => ({ a: s.a, b: s.b })));应使用单字段选择器或配合 shallow 比较器
遵循 React「副作用与状态更新」规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定等
统一使用 loggerService 记录日志而不是 console
logger 使用示例中第二个参数必须为对象字面量(如 logger.error('msg', { error }))
任何组件或页面都不要写入 media 元素的 currentTime,播放器控制由编排器统一负责
在 styled-components 中:主题相关属性使用 AntD CSS 变量(如 var(--ant-color-bg-elevated));
在 styled-components 中:设计系统常量(尺寸、动画、层级、字体、毛玻璃等)使用 JS 常量(如 SPACING、BORDER_RADIUS、Z_INDEX、FONT_SIZES 等)
Files:
packages/shared/types/media-server.tspackages/shared/types/index.tssrc/main/services/MediaServerService.tssrc/main/services/PythonVenvService.tssrc/renderer/src/pages/settings/MediaServerSection.tsx
src/renderer/src/**/*.{ts,tsx,scss,css}
📄 CodeRabbit inference engine (CLAUDE.md)
优先使用 CSS 变量,避免硬编码样式值(颜色等)
Files:
src/renderer/src/pages/settings/MediaServerSection.tsx
src/renderer/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
尺寸与时长等不要硬编码,优先使用 useTheme() 的 token 或集中样式变量(如 motionDurationMid、borderRadiusSM/MD)
Files:
src/renderer/src/pages/settings/MediaServerSection.tsx
🧬 Code graph analysis (3)
src/main/services/MediaServerService.ts (6)
src/renderer/src/services/Logger.ts (3)
loggerService(817-817)error(422-424)info(436-438)packages/shared/types/media-server.ts (2)
MediaServerStatus(8-13)MediaServerInfo(18-25)src/main/services/FFmpegDownloadService.ts (1)
FFmpegDownloadService(277-1047)src/main/utils/index.ts (1)
getDataPath(11-17)src/main/services/UvBootstrapperService.ts (1)
uvBootstrapperService(1185-1185)src/main/services/PythonVenvService.ts (1)
pythonVenvService(499-499)
src/main/services/PythonVenvService.ts (3)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/media-server.ts (2)
InstallProgress(42-46)PythonVenvInfo(30-37)src/main/services/UvBootstrapperService.ts (1)
uvBootstrapperService(1185-1185)
src/renderer/src/pages/settings/MediaServerSection.tsx (5)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)packages/shared/types/media-server.ts (3)
MediaServerInfo(18-25)PythonVenvInfo(30-37)InstallProgress(42-46)src/renderer/src/pages/settings/AboutSettings.tsx (1)
SettingRowTitle(412-424)src/renderer/src/infrastructure/styles/theme.ts (6)
SPACING(43-58)BORDER_RADIUS(61-72)ANIMATION_DURATION(91-100)EASING(103-114)FONT_WEIGHTS(11-22)FONT_SIZES(25-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test (ubuntu-latest, 20)
- GitHub Check: test (macos-latest, 20)
- GitHub Check: test (windows-latest, 20)
| private async findAvailablePort(preferredPort?: number): Promise<number> { | ||
| // 如果指定了优先端口,先尝试该端口 | ||
| if (preferredPort && (await this.isPortAvailable(preferredPort))) { | ||
| return preferredPort | ||
| } | ||
|
|
||
| // 随机选择起始端口,避免总是从同一个端口开始 | ||
| const startPort = Math.floor(Math.random() * (this.MAX_PORT - this.MIN_PORT)) + this.MIN_PORT | ||
|
|
||
| // 从随机起始位置开始查找 | ||
| for (let i = 0; i < this.MAX_PORT - this.MIN_PORT; i++) { | ||
| const port = | ||
| this.MIN_PORT + ((startPort - this.MIN_PORT + i) % (this.MAX_PORT - this.MIN_PORT)) | ||
| if (await this.isPortAvailable(port)) { | ||
| return port | ||
| } | ||
| } | ||
|
|
||
| throw new Error(`无法在端口范围 ${this.MIN_PORT}-${this.MAX_PORT} 内找到可用端口`) |
There was a problem hiding this comment.
修正端口遍历范围的 off-by-one 错误
当前循环只覆盖到 8864,最大值 8865 永远不会尝试。请把区间长度加一,并在随机起点和取模时使用同一个 rangeSize。
- const startPort = Math.floor(Math.random() * (this.MAX_PORT - this.MIN_PORT)) + this.MIN_PORT
+ const rangeSize = this.MAX_PORT - this.MIN_PORT + 1
+ const startPort = Math.floor(Math.random() * rangeSize) + this.MIN_PORT
...
- for (let i = 0; i < this.MAX_PORT - this.MIN_PORT; i++) {
- const port =
- this.MIN_PORT + ((startPort - this.MIN_PORT + i) % (this.MAX_PORT - this.MIN_PORT))
+ for (let i = 0; i < rangeSize; i++) {
+ const port = this.MIN_PORT + ((startPort - this.MIN_PORT + i) % rangeSize)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private async findAvailablePort(preferredPort?: number): Promise<number> { | |
| // 如果指定了优先端口,先尝试该端口 | |
| if (preferredPort && (await this.isPortAvailable(preferredPort))) { | |
| return preferredPort | |
| } | |
| // 随机选择起始端口,避免总是从同一个端口开始 | |
| const startPort = Math.floor(Math.random() * (this.MAX_PORT - this.MIN_PORT)) + this.MIN_PORT | |
| // 从随机起始位置开始查找 | |
| for (let i = 0; i < this.MAX_PORT - this.MIN_PORT; i++) { | |
| const port = | |
| this.MIN_PORT + ((startPort - this.MIN_PORT + i) % (this.MAX_PORT - this.MIN_PORT)) | |
| if (await this.isPortAvailable(port)) { | |
| return port | |
| } | |
| } | |
| throw new Error(`无法在端口范围 ${this.MIN_PORT}-${this.MAX_PORT} 内找到可用端口`) | |
| private async findAvailablePort(preferredPort?: number): Promise<number> { | |
| // 如果指定了优先端口,先尝试该端口 | |
| if (preferredPort && (await this.isPortAvailable(preferredPort))) { | |
| return preferredPort | |
| } | |
| // 随机选择起始端口,避免总是从同一个端口开始 | |
| const rangeSize = this.MAX_PORT - this.MIN_PORT + 1 | |
| const startPort = Math.floor(Math.random() * rangeSize) + this.MIN_PORT | |
| // 从随机起始位置开始查找 | |
| for (let i = 0; i < rangeSize; i++) { | |
| const port = this.MIN_PORT + ((startPort - this.MIN_PORT + i) % rangeSize) | |
| if (await this.isPortAvailable(port)) { | |
| return port | |
| } | |
| } | |
| throw new Error(`无法在端口范围 ${this.MIN_PORT}-${this.MAX_PORT} 内找到可用端口`) | |
| } |
🤖 Prompt for AI Agents
In src/main/services/MediaServerService.ts around lines 148 to 166, the
port-search loop has an off-by-one bug that never tests the upper bound
(MAX_PORT); define a rangeSize = this.MAX_PORT - this.MIN_PORT + 1, use it when
computing the random startPort (Math.floor(Math.random() * rangeSize) +
this.MIN_PORT), use the same rangeSize in the modulo and in the loop condition
(for (let i = 0; i < rangeSize; i++)), and keep using isPortAvailable to return
the found port or throw if none found.
* feat(media-server): implement runtime runtime management system - add UV bootstrapper with cross-platform install, caching, progress, validation - manage Python venv lifecycle, dependency installs, cleanup, and phased progress - introduce MediaServerService with lifecycle control, health checks, and port management - extend FFmpeg service to handle FFprobe with unified downloads and version updates - build settings UI sections for media server and plugins with real-time status - wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control - update build configs, add ffprobe download script, auto-start server, and bump backend ref * test: Update tests * test(main): mock electron app for ipc handlers * Update src/renderer/src/pages/settings/FFprobeSection.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor(types): extract common Platform and Arch types to shared package - Create packages/shared/types/system.ts with Platform and Arch type definitions - Remove duplicate type definitions from FFmpegDownloadService - Remove duplicate type definitions from UvBootstrapperService - Import shared types from @shared/types/system in both services This change improves code maintainability by centralizing common system type definitions. * fix(media-server): clear port and notify frontend on process exit Ensure media server port is properly cleared and frontend is notified when the process stops or crashes to prevent stale port usage. Changes: - Update notifyPortChanged signature to accept number | null - Clear this.port to null in both normal stop and crash exit handlers - Notify frontend with null port when server stops - Update SessionService listener to handle null port notifications - Add appropriate logging for port clearing events This prevents the frontend from continuing to use expired ports after the media server has stopped, requiring manual app restart. * fix(settings): prevent setTimeout memory leak in MediaServerSection Fix potential memory leak and unmounted component state update in MediaServerSection by properly managing setTimeout lifecycle: - Add successTimeoutRef to store timeout handle - Clear timeout in effect cleanup on component unmount - Clear timeout before creating new one in handleInstall - Clear timeout in error handler to prevent orphaned timers - Set ref to null after clearing to maintain clean state This prevents "Can't perform a React state update on an unmounted component" warnings and ensures proper cleanup of async operations. Co-Authored-By: Claude <noreply@anthropic.com> * fix(media-server): improve status display during installation and startup - Add 'starting' status immediately after installation completes - Move fetchServerInfo() to finally block to ensure status refresh - Prevent confusing 'stopped' status display during startup process * fix(python-venv): preserve final install progress state for callers Remove finally block that immediately clears installProgress, preventing callers from reading final "completed" or "error" state. Instead: - Clear installProgress at start of new installation - Preserve final state after completion/error for caller consumption - Remove unnecessary finally block cleanup This ensures MediaServerSection and other callers can reliably read the final installation state. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(types): centralize media server types to shared package - Create shared types module for Media Server and Python Venv - Add media-server.ts with MediaServerInfo, MediaServerStatus, PythonVenvInfo, and InstallProgress types - Remove duplicate type definitions from MediaServerService, PythonVenvService, and MediaServerSection - Update imports to use @shared/types across main and renderer processes - Maintain backward compatibility by re-exporting types from service files - All type checks passing
* feat(media-server): implement runtime runtime management system - add UV bootstrapper with cross-platform install, caching, progress, validation - manage Python venv lifecycle, dependency installs, cleanup, and phased progress - introduce MediaServerService with lifecycle control, health checks, and port management - extend FFmpeg service to handle FFprobe with unified downloads and version updates - build settings UI sections for media server and plugins with real-time status - wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control - update build configs, add ffprobe download script, auto-start server, and bump backend ref * test: Update tests * test(main): mock electron app for ipc handlers * Update src/renderer/src/pages/settings/FFprobeSection.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor(types): extract common Platform and Arch types to shared package - Create packages/shared/types/system.ts with Platform and Arch type definitions - Remove duplicate type definitions from FFmpegDownloadService - Remove duplicate type definitions from UvBootstrapperService - Import shared types from @shared/types/system in both services This change improves code maintainability by centralizing common system type definitions. * fix(media-server): clear port and notify frontend on process exit Ensure media server port is properly cleared and frontend is notified when the process stops or crashes to prevent stale port usage. Changes: - Update notifyPortChanged signature to accept number | null - Clear this.port to null in both normal stop and crash exit handlers - Notify frontend with null port when server stops - Update SessionService listener to handle null port notifications - Add appropriate logging for port clearing events This prevents the frontend from continuing to use expired ports after the media server has stopped, requiring manual app restart. * fix(settings): prevent setTimeout memory leak in MediaServerSection Fix potential memory leak and unmounted component state update in MediaServerSection by properly managing setTimeout lifecycle: - Add successTimeoutRef to store timeout handle - Clear timeout in effect cleanup on component unmount - Clear timeout before creating new one in handleInstall - Clear timeout in error handler to prevent orphaned timers - Set ref to null after clearing to maintain clean state This prevents "Can't perform a React state update on an unmounted component" warnings and ensures proper cleanup of async operations. Co-Authored-By: Claude <noreply@anthropic.com> * fix(media-server): improve status display during installation and startup - Add 'starting' status immediately after installation completes - Move fetchServerInfo() to finally block to ensure status refresh - Prevent confusing 'stopped' status display during startup process * fix(python-venv): preserve final install progress state for callers Remove finally block that immediately clears installProgress, preventing callers from reading final "completed" or "error" state. Instead: - Clear installProgress at start of new installation - Preserve final state after completion/error for caller consumption - Remove unnecessary finally block cleanup This ensures MediaServerSection and other callers can reliably read the final installation state. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(types): centralize media server types to shared package - Create shared types module for Media Server and Python Venv - Add media-server.ts with MediaServerInfo, MediaServerStatus, PythonVenvInfo, and InstallProgress types - Remove duplicate type definitions from MediaServerService, PythonVenvService, and MediaServerSection - Update imports to use @shared/types across main and renderer processes - Maintain backward compatibility by re-exporting types from service files - All type checks passing
# [1.1.0-alpha.2](v1.1.0-alpha.1...v1.1.0-alpha.2) (2025-10-12) ### Features * **media-server:** implement runtime runtime management system ([#204](#204)) ([2d179f5](2d179f5)) * **player:** add animated loading progress bar to PlayerPage ([#206](#206)) ([53f7393](53f7393)) * **player:** add media server recommendation prompt for incompatible videos ([#205](#205)) ([12b4434](12b4434))
* feat(media-server): implement runtime runtime management system - add UV bootstrapper with cross-platform install, caching, progress, validation - manage Python venv lifecycle, dependency installs, cleanup, and phased progress - introduce MediaServerService with lifecycle control, health checks, and port management - extend FFmpeg service to handle FFprobe with unified downloads and version updates - build settings UI sections for media server and plugins with real-time status - wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control - update build configs, add ffprobe download script, auto-start server, and bump backend ref * test: Update tests * test(main): mock electron app for ipc handlers * Update src/renderer/src/pages/settings/FFprobeSection.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor(types): extract common Platform and Arch types to shared package - Create packages/shared/types/system.ts with Platform and Arch type definitions - Remove duplicate type definitions from FFmpegDownloadService - Remove duplicate type definitions from UvBootstrapperService - Import shared types from @shared/types/system in both services This change improves code maintainability by centralizing common system type definitions. * fix(media-server): clear port and notify frontend on process exit Ensure media server port is properly cleared and frontend is notified when the process stops or crashes to prevent stale port usage. Changes: - Update notifyPortChanged signature to accept number | null - Clear this.port to null in both normal stop and crash exit handlers - Notify frontend with null port when server stops - Update SessionService listener to handle null port notifications - Add appropriate logging for port clearing events This prevents the frontend from continuing to use expired ports after the media server has stopped, requiring manual app restart. * fix(settings): prevent setTimeout memory leak in MediaServerSection Fix potential memory leak and unmounted component state update in MediaServerSection by properly managing setTimeout lifecycle: - Add successTimeoutRef to store timeout handle - Clear timeout in effect cleanup on component unmount - Clear timeout before creating new one in handleInstall - Clear timeout in error handler to prevent orphaned timers - Set ref to null after clearing to maintain clean state This prevents "Can't perform a React state update on an unmounted component" warnings and ensures proper cleanup of async operations. Co-Authored-By: Claude <noreply@anthropic.com> * fix(media-server): improve status display during installation and startup - Add 'starting' status immediately after installation completes - Move fetchServerInfo() to finally block to ensure status refresh - Prevent confusing 'stopped' status display during startup process * fix(python-venv): preserve final install progress state for callers Remove finally block that immediately clears installProgress, preventing callers from reading final "completed" or "error" state. Instead: - Clear installProgress at start of new installation - Preserve final state after completion/error for caller consumption - Remove unnecessary finally block cleanup This ensures MediaServerSection and other callers can reliably read the final installation state. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(types): centralize media server types to shared package - Create shared types module for Media Server and Python Venv - Add media-server.ts with MediaServerInfo, MediaServerStatus, PythonVenvInfo, and InstallProgress types - Remove duplicate type definitions from MediaServerService, PythonVenvService, and MediaServerSection - Update imports to use @shared/types across main and renderer processes - Maintain backward compatibility by re-exporting types from service files - All type checks passing
# [1.1.0-beta.1](v1.0.0...v1.1.0-beta.1) (2025-10-15) ### Bug Fixes * **AppUpdater, FFmpegDownloadService:** update default mirror source to global ([83194b3](83194b3)) * **build:** adjust resource handling for media-server in packaging ([086bd1b](086bd1b)) * **codec-compatibility:** handle missing codec information gracefully ([ea29f21](ea29f21)) * **FFmpegSection:** manage completion timeout for download process ([39b43c0](39b43c0)) * **FFprobeSection:** add return statement to download progress polling function ([49636cf](49636cf)) * **FFprobeSection:** ensure timeout cleanup after download success ([81a1431](81a1431)) * **FFprobeSection:** manage success timeout for download completion ([ce55d49](ce55d49)) * **FFprobeSection:** standardize font size using theme constants ([6387445](6387445)) * **FFprobeSection:** standardize spacing in styled components ([ba3c3d4](ba3c3d4)) * **homepage:** improve bottom spacing for card grid ([#194](#194)) ([801b6cd](801b6cd)) * make subtitle overlay container semantic ([2d6ae60](2d6ae60)) * **MediaServerService:** enhance error handling for file existence check ([11b74ef](11b74ef)) * **MediaServerService:** replace fs.existsSync with async stat for file existence check ([c9c98da](c9c98da)) * **player:** apply playback rate change through orchestrator when cycling speeds ([#210](#210)) ([fa9aa09](fa9aa09)) * **player:** remove HLS player missing error handling ([c7b593e](c7b593e)) * remove green glow effect from progress bar ([#196](#196)) ([abc6f3e](abc6f3e)), closes [#e50914](https://github.com/mkdir700/EchoPlayer/issues/e50914) [#00b96](https://github.com/mkdir700/EchoPlayer/issues/00b96) * **semantic-release:** enhance version increment rules for prerelease branches ([#199](#199)) ([5d1e533](5d1e533)) * **theme:** resolve theme color not updating immediately for Switch components and progress bars ([#197](#197)) ([eed9ea2](eed9ea2)) * **TranscodeLoadingIndicator:** remove logging for loading indicator display ([085db44](085db44)) * **useSubtitleScrollStateMachine:** start auto-return timer on user interactions ([8496ae0](8496ae0)) * **UvBootstrapperService:** enhance UV download logic with cached path checks ([fc0791a](fc0791a)) * **UvBootstrapperService:** ensure temp directory cleanup after download ([02c7b16](02c7b16)) * **UvBootstrapperService:** prevent concurrent downloads by checking download controllers ([19d31e7](19d31e7)) * **VolumeIndicator:** skip indicator display on initial render ([82d2281](82d2281)) * **workflow:** update artifact listing command for better compatibility ([dfb6ee4](dfb6ee4)) ### Features * integrate session-backed HLS playback flow ([#200](#200)) ([ee972d1](ee972d1)) * intro backend for hls player ([2d34e7b](2d34e7b)) * **media-server:** add transcode cache cleanup for deleted videos ([e2de9ad](e2de9ad)) * **media-server:** implement runtime runtime management system ([#204](#204)) ([f5f68b0](f5f68b0)) * optimize media-server build output to resources directory ([#201](#201)) ([1b8c28e](1b8c28e)) * **player:** add animated loading progress bar to PlayerPage ([#206](#206)) ([8ba6f7f](8ba6f7f)) * **player:** add media server recommendation prompt for incompatible videos ([#205](#205)) ([63221a2](63221a2)) * **player:** add subtitle search functionality ([c3228c3](c3228c3)) * **player:** add toggle auto-pause functionality ([98b59ef](98b59ef)) * **player:** HLS session progress polling with media server integration ([#209](#209)) ([a76e8c2](a76e8c2)) * **PlayerSettingsLoader:** add mask mode to subtitle overlay ([56e4f65](56e4f65)) * **player:** update seek button icons from rewind/fastforward to undo/redo ([#193](#193)) ([1612c43](1612c43)) * **RegionDetection:** integrate region detection service for IP-based country identification ([dbeb077](dbeb077)) * **subtitle:** introduce mask mode for subtitle overlay ([e1fb3eb](e1fb3eb)) * **SubtitleOverlay:** enhance positioning and collision handling ([92b061a](92b061a)) * **UvBootstrapperService:** enhance download management with concurrency control ([20522e9](20522e9)) ### Reverts * "fix(build): adjust resource handling for media-server in packaging" ([2133401](2133401))
# [1.1.0](v1.0.0...v1.1.0) (2025-10-16) ### Bug Fixes * **AppUpdater, FFmpegDownloadService:** update default mirror source to global ([83194b3](83194b3)) * **build:** adjust resource handling for media-server in packaging ([086bd1b](086bd1b)) * **codec-compatibility:** handle missing codec information gracefully ([ea29f21](ea29f21)) * **FFmpegSection:** manage completion timeout for download process ([39b43c0](39b43c0)) * **FFprobeSection:** add return statement to download progress polling function ([49636cf](49636cf)) * **FFprobeSection:** ensure timeout cleanup after download success ([81a1431](81a1431)) * **FFprobeSection:** manage success timeout for download completion ([ce55d49](ce55d49)) * **FFprobeSection:** standardize font size using theme constants ([6387445](6387445)) * **FFprobeSection:** standardize spacing in styled components ([ba3c3d4](ba3c3d4)) * **homepage:** improve bottom spacing for card grid ([#194](#194)) ([801b6cd](801b6cd)) * make subtitle overlay container semantic ([2d6ae60](2d6ae60)) * **MediaServerService:** enhance error handling for file existence check ([11b74ef](11b74ef)) * **MediaServerService:** replace fs.existsSync with async stat for file existence check ([c9c98da](c9c98da)) * **player:** apply playback rate change through orchestrator when cycling speeds ([#210](#210)) ([fa9aa09](fa9aa09)) * **player:** remove HLS player missing error handling ([c7b593e](c7b593e)) * **player:** restore muted state when re-entering player page ([1ec5c56](1ec5c56)) * remove green glow effect from progress bar ([#196](#196)) ([abc6f3e](abc6f3e)), closes [#e50914](https://github.com/mkdir700/EchoPlayer/issues/e50914) [#00b96](https://github.com/mkdir700/EchoPlayer/issues/00b96) * **semantic-release:** enhance version increment rules for prerelease branches ([#199](#199)) ([5d1e533](5d1e533)) * **theme:** resolve theme color not updating immediately for Switch components and progress bars ([#197](#197)) ([eed9ea2](eed9ea2)) * **TranscodeLoadingIndicator:** remove logging for loading indicator display ([085db44](085db44)) * **useSubtitleScrollStateMachine:** start auto-return timer on user interactions ([8496ae0](8496ae0)) * **UvBootstrapperService:** enhance UV download logic with cached path checks ([fc0791a](fc0791a)) * **UvBootstrapperService:** ensure temp directory cleanup after download ([02c7b16](02c7b16)) * **UvBootstrapperService:** prevent concurrent downloads by checking download controllers ([19d31e7](19d31e7)) * **VolumeIndicator:** skip indicator display on initial render ([82d2281](82d2281)) * **workflow:** update artifact listing command for better compatibility ([dfb6ee4](dfb6ee4)) ### Features * integrate session-backed HLS playback flow ([#200](#200)) ([ee972d1](ee972d1)) * intro backend for hls player ([2d34e7b](2d34e7b)) * **media-server:** add transcode cache cleanup for deleted videos ([e2de9ad](e2de9ad)) * **media-server:** implement runtime runtime management system ([#204](#204)) ([f5f68b0](f5f68b0)) * optimize media-server build output to resources directory ([#201](#201)) ([1b8c28e](1b8c28e)) * **player:** add animated loading progress bar to PlayerPage ([#206](#206)) ([8ba6f7f](8ba6f7f)) * **player:** add media server recommendation prompt for incompatible videos ([#205](#205)) ([63221a2](63221a2)) * **player:** add subtitle search functionality ([c3228c3](c3228c3)) * **player:** add toggle auto-pause functionality ([98b59ef](98b59ef)) * **player:** HLS session progress polling with media server integration ([#209](#209)) ([a76e8c2](a76e8c2)) * **PlayerSettingsLoader:** add mask mode to subtitle overlay ([56e4f65](56e4f65)) * **player:** update seek button icons from rewind/fastforward to undo/redo ([#193](#193)) ([1612c43](1612c43)) * **RegionDetection:** integrate region detection service for IP-based country identification ([dbeb077](dbeb077)) * **subtitle:** introduce mask mode for subtitle overlay ([e1fb3eb](e1fb3eb)) * **SubtitleOverlay:** enhance positioning and collision handling ([92b061a](92b061a)) * **UvBootstrapperService:** enhance download management with concurrency control ([20522e9](20522e9)) ### Reverts * "fix(build): adjust resource handling for media-server in packaging" ([2133401](2133401))
add UV bootstrapper with cross-platform install, caching, progress, validation
manage Python venv lifecycle, dependency installs, cleanup, and phased progress
introduce MediaServerService with lifecycle control, health checks, and port management
extend FFmpeg service to handle FFprobe with unified downloads and version updates
build settings UI sections for media server and plugins with real-time status
wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control
update build configs, add ffprobe download script, auto-start server, and bump backend ref
Summary by CodeRabbit
新功能
杂务
测试