前端vue3解析上传的视频编码格式,同时判断是否可以在当前浏览器播放

前端vue3解析上传的视频编码格式,同时判断是否可以在当前浏览器播放

技术栈:vue3、JavaScript、vite

依赖库:mediainfo.js: "^0.2.2"、 file-type: "^21.1.1";

前言

        这段时间有接触一个在线聊天的前端项目,其中可以发送图片视频之类的。随后,便发现了一些问题:其中与本文章有关的,就是上传的视频,在当前浏览器有可能无法播放(直接无法播放、或者点击播放,有声音,但无画面)。

        经过排查,最后发现,是视频编码问题,部分浏览器不支持H265编码(HEVC)格式的视频播放,导致原生video组件播放异常。

        怎么处理呢?一开始想让后台帮忙处理,检测视频格式,并将其转换为H264的编码格式(AVC)。嗯,虽然从结果来说,完全可行,但对服务器资源的消耗还是挺大的,因此不太建议这么做。

        那么直接让前端来处理呢?有没有什么豪的方法?有的,兄弟,有的!我们可以直接解析视频的相关数据,提取出来,然后去判断当前浏览器到底能不能播放,如果不能播放,直接告诉用户放不了不就好了,然后把视频链接copy给用户,让用户自己去找能播放的浏览器去播放@w@。

如何实现

        最主要的一点就是如何拿到视频的数据,这边是用的第三方库mediainfo.js:

                NPM地址:https://www.npmjs.com/package/mediainfo.js

                官方Demo地址:https://mediainfo.js.org/demo/

        同时,需要强调一点,部分浏览器,如百度浏览器,使用的较旧的JavaScript引擎或WASM虚拟机,无法兼容最新版本mediainfo.js中的WASM模块(i64.add),导致无法使用mediainfo.js。因此强烈建议使用兼容性很好的0.2.2版本

        那么拿到数据之后呢,如何去判断当前浏览器是否可以播放该视频?这里用到的是原生的方法,目前共找到3个:

    video.canPlayType()MDN介绍

    MediaSource.isTypeSupported()MDN介绍

     MediaCapabilities.decodingInfo()MDN介绍

        从数据准确性来说,应该是MediaCapabilities.decodingInfo()最准确,不过如果只是检测这个编码格式的视频能不能播放的话,直接使用video.canPlayType()就够用了。

        将解析得到的数据,传递给上述3个接口,即可得出当前浏览器是否可以播放的结论。

具体实现方式

        按照博主当前的业务逻辑,需要将解析的数据传递给后台(数据量小,存放在文件名称中),随后从后台再取回当前视频的数据(聊天消息使用的是后台的数据),去解析是否能够播放(因为是在线聊天的功能嘛)。

一:获取文件编码信息

        需求,是否要安装第三方库:file-type

                NPM地址:https://www.npmjs.com/package/file-type

<template> <!-- AllowedIMGTypes与AllowedVIDTypes是允许上传的文件格式 --> <input type="file" :accept="[...AllowedIMGTypes, ...AllowedVIDTypes].join(',')" @change="handleFileChange($event)" > </template> <script> import { renameVideoFile, useMediaInfo, getMediaInfoInstance } from '@/utils/videoInfo' const emit = defineEmits(['upload']) // 初始化媒体信息 getMediaInfoInstance() // result 是视频解析的结果,analyzeVideo是解析方法 const { result, analyzeVideo } = useMediaInfo() const handleFileChange = async (e) => { const { conversationId } = chatStore.chatInfo let selectedFile = e.target.files[0] // 获取文件类型 此处 getFileExtension 封装了第三方库 file-type,返回媒体类型mime与mime中的文件类型(如image) // 至于为什么要用第三方库,因为直接用 selectedFile.type 不准捏 const { mime, type: fileType } = await getFileExtension(selectedFile) e.target.value = '' console.log('选择的文件媒体类型', selectedFile.type, '文件类型', fileType) // 验证图片格式、大小 if (fileType === 'image') { // 图片的处理 DLC捏 if (mime !== 'image/tiff') { // 压缩图片的代码 DLC捏 tiff不能直接压缩,需要过滤或者额外处理 } } // 验证视频格式、大小 if (fileType === 'video') { // 此处判断是视频类型判断 if (!AllowedVIDTypes.includes(mime)) { // 怎么做看具体需求 DLC捏 } // 这边限制上传视频最大为 500MB if (selectedFile.size > 500 * 1024 * 1024) { // 超出限制,给出对应的操作 DLC捏 } try { // 此处返回的是一个新的file对象,主要是用来修改文件名称用的 // 有更好的方法,无需修改文件,但BZ的后台接口,是直接上传文件的,所以重新返回了一个新的文件 const videoFile = await analyzeAndRename(selectedFile) selectedFile = videoFile console.log('重命名后的视频文件', selectedFile) } catch (error) { // 如果中间出现了什么问题,那就不解析了,直接上传视频,个人认为不应该阻断上传功能 console.error('视频信息获取失败,不会添加相关编码数据', error) } } if (fileType !== 'image' && fileType !== 'video') { console.log('选择的文件类型不匹配') // 文件类型不匹配的相关操作 DLC捏 } // 此处是将文件和其他的一些用户信息传递给父组件 emit('upload', { file: selectedFile, }) } // 解析编码信息并重命名 const analyzeAndRename = async (selectedFile) => { console.log('视频:', selectedFile) // 后缀位置 const lastDotIndex = selectedFile.name.lastIndexOf('.') // 视频名称 const originName = selectedFile.name.slice(0, lastDotIndex) // 视频后缀 const fileSuffix = selectedFile.name.slice(lastDotIndex + 1) console.log('视频名称', originName, '文件后缀', fileSuffix) // 封装好的解析视频数据的方法:mediainfo.js,将数据传递给result await analyzeVideo(selectedFile) console.log('视频信息---------', result.value) // 视频编码 let codec = result.value.codec // 文件名不能有 / ,将其替换为- 将.删除 // 此处的两行replace代码已无用 codec = codec.replace(/\//g, '-') codec = codec.replace(/\./g, '') // 详细的编码信息 let codecs = result.value.codecs console.log('上传解析后,得到的编码信息', codec, codecs) // 两个编码信息合并 codec + codecs 为方便后续查找,给出一个较合理的合并方式 const mergeCodecs = `-codec_${codec}-codecs_${codecs}-` // 为视频文件名称添加编码信息 const videoFile = await renameVideoFile(selectedFile, `${originName}${mergeCodecs}.${fileSuffix}`) return videoFile } </script>

        此处其实拿到的codecs就是可以用来判断能否播放的参数,若只需要在上传文件的时候就判断,则可以在此处就进行后续的判断操作了。下面是已封装好的方法,一些操作是问的AI,然后稍微修改了一下:

        注意,此处的 getMediaInfoInstance 非常重要,建议在项目首页就调用该方法,因为加载wasm文件需要一定的时间,且若未加载完,无法使用;

        同时在vue3中,无法直接调用 MediaInfoFactory,具体为在node_modules中的MediaInfoModule.wasm无法被正常调用,需要Copy至assets文件中,直接本地调用:

return new URL('../assets/MediaInfoModule.wasm', import.meta.url).href;

        也可使用其他方式来调用该wasm文件

        相关github问题链接:https://github.com/buzz/mediainfo.js/issues/124

将该文件copy到assets目录下:

// videoInfo.js import { ref } from 'vue' import MediaInfoFactory from 'mediainfo.js'; // 存储初始化过程的 Promise let mediaInfoInitPromise = null; // 重命名文件 export const renameVideoFile = async (file, newName) => { // 创建新的 File 对象,保留其他属性 return new File( [file], newName, { type: file.type, lastModified: file.lastModified } ) } // 初始化 MediaInfo 实例 export const getMediaInfoInstance = async () => { console.log('初始化 MediaInfo') if (mediaInfoInitPromise) { console.log('实例正在初始化中,等待共享的 Promise 完成...'); return await mediaInfoInitPromise; } mediaInfoInitPromise = (async () => { console.log('还未初始化') try { const mediaInfoInstance = await MediaInfoFactory({ format: 'object', locateFile: (path) => { // 如果请求的是 .wasm 文件,返回自定义的路径 if (path.endsWith('.wasm')) { console.warn('----------------------') // 返回本地兼容版 WASM 文件的路径 0.2.2 最新版本在部分浏览器上不兼容 // 如百度浏览器 内核为chrome 97.0.4692.98 不支持最新版本 return new URL('../assets/MediaInfoModule.wasm', import.meta.url).href; } // 其他文件(如 .js worker)按原路径返回 return path; } }); console.log('MediaInfo 加载成功') return mediaInfoInstance; } catch (err) { console.error('加载 MediaInfo 失败:', err) console.log('具体信息 1', err.message, '具体信息2', err.stack) throw new Error('无法加载媒体分析库') } })() return await mediaInfoInitPromise; } // 使用mediainfo.js export const useMediaInfo = () => { const loading = ref(false) const error = ref(null) const result = ref(null) // 分析视频文件 const analyzeVideo = async (file) => { loading.value = true error.value = null result.value = null console.log('开始分析视频文件...') try { // 先调用初始化实例 const mediainfo = await getMediaInfoInstance() console.log('MediaInfo 实例已初始化') const analysisResult = await mediainfo.analyzeData( () => file.size, (chunkSize, offset) => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = (e) => { resolve(new Uint8Array(e.target.result)) } reader.onerror = reject reader.readAsArrayBuffer(file.slice(offset, offset + chunkSize)) }) } ) console.log('视频分析结果 => ', analysisResult) // 解析结果 const parsedResult = parseMediaInfoResult(analysisResult) result.value = parsedResult return parsedResult } catch (err) { console.error('视频分析失败:', err) error.value = err.message throw err } finally { loading.value = false } } // 专门检测是否为 H.265 此方法可以在你上传文件后,立即调用 // 以此来判断该视频是否可以在当前浏览器播放 const detectH265 = async (file) => { try { const result = await analyzeVideo(file) return { isH265: result.isH265, codec: result.codec, confidence: result.confidence } } catch (err) { console.error('H.265 检测失败:', err) throw err } } return { loading, error, result, analyzeVideo, detectH265, } } // 解析 MediaInfo 结果 const parseMediaInfoResult = (analysis) => { console.log('开始解析 MediaInfo 结果...') if (!analysis?.media?.track) { throw new Error('无法解析媒体信息') } const tracks = Array.isArray(analysis.media.track) ? analysis.media.track : [analysis.media.track] console.log('视频轨道信息:', tracks) const videoTrack = tracks.find(track => track['@type'] === 'Video') const generalTrack = tracks.find(track => track['@type'] === 'General') // 检测 HEVC 编码 const codecs = H256Codecs(videoTrack) console.log('codecs', codecs) // 映射 HEVC 兼容性 const codecsCompatibility = browserSupportsH265(codecs) console.warn('codecsCompatibility', codecsCompatibility) console.log('videoTrack', videoTrack, 'generalTrack', generalTrack) if (!videoTrack) { return { codec: '未知', isH265: false, isH264: false, confidence: 0, message: '未检测到视频轨道' } } const format = videoTrack.Format || '' const codecId = videoTrack.CodecID || '' const formatLower = format.toLowerCase() const codecIdLower = codecId.toLowerCase() console.log('视频编码类型', format, codecId, formatLower, codecIdLower) // 判断编码类型 const isH265 = formatLower.includes('hevc') || codecIdLower.includes('hvc') || codecIdLower.includes('hev') const isH264 = formatLower.includes('avc') || codecIdLower.includes('avc') || formatLower.includes('h264') let codec = format let confidence = 95 if (isH265) { console.log('H.265 检测成功', codec) codec = 'H265-HEVC' } else if (isH264) { console.log('H264 检测成功', codec) codec = 'H264-AVC' } else if (format) { console.log('未知编码检测成功', format) codec = format confidence = 85 } else { codec = '未知编码' confidence = 0 } return { codec, isH265, isH264, confidence, format, codecId, codecs, // profile: videoTrack.Format_Profile, // resolution: videoTrack.Width && videoTrack.Height // ? `${videoTrack.Width} × ${videoTrack.Height}` // : null, // duration: generalTrack?.Duration, // frameRate: videoTrack.FrameRate, // bitRate: videoTrack.BitRate || generalTrack?.OverallBitRate, // bitDepth: videoTrack.BitDepth, // tracks: tracks } } // 映射 Profile const mapProfileIdc = (profile) => { if (!profile) return null; console.log("mapProfileIdc->profile:", profile) const p = profile.toLowerCase(); if (p.includes("main 10")) return 2; if (p.includes("main")) return 1; return null; // Web 不支持的 Profile } // 检测 HEVC 兼容性 const mapCompatibility = (profileIdc) => { console.log("mapCompatibility->profileIdc:", profileIdc) // 工程实践中的固定映射 if (profileIdc === 1) return 6; // Main if (profileIdc === 2) return 4; // Main10 return null; } // 检测 HEVC 等级 const mapLevel = (levelStr) => { if (!levelStr) return null; console.log("mapLevel->levelStr:", levelStr) // 修复:处理 "4.0" -> "4" '3.0' -> "3" // const key = levelStr.split('.')[0]; let key = levelStr; if (levelStr.includes('.') && levelStr.split('.')[1] === '0') { key = levelStr.split('.')[0]; } const levelMap = { "3": "L90", "3.1": "L93", // 这个可以保留,以防万一 "4": "L120", "4.1": "L123", // 同上 "5": "L150", "5.1": "L153", "5.2": "L156", // 额外添加的 "6": "L180", "6.1": "L183", '6.2': "L186", }; return levelMap[key] || null; // 使用处理后的 key } // 检测 HEVC 采样入口 const detectHevcSampleEntry = (miVideo) => { const codecId = (miVideo.CodecID || "").toLowerCase(); console.log('detectHevcSampleEntry->codecId:', codecId) if (codecId.startsWith("hev1")) return "hev1"; if (codecId.startsWith("hvc1")) return "hvc1"; // 无法判断 → Web 工程兜底策略 return "hvc1"; } // const H256Codecs = (videoTracks) => { console.log('videoTracks:', videoTracks) if (videoTracks.Format !== "HEVC") return null; console.log('准备判断 profileIdc') const profileIdc = mapProfileIdc(videoTracks.Format_Profile); console.log("得到的 profileIdc:", profileIdc) if (!profileIdc) return null; console.log('准备判断 compatibility') const compatibility = mapCompatibility(profileIdc); console.log("得到的 compatibility:", compatibility) if (!compatibility) return null; console.log('准备判断 level') const level = mapLevel(videoTracks.Format_Level); console.log("得到的 level:", level) if (!level) return null; console.log('准备判断 entry') const entry = detectHevcSampleEntry(videoTracks); console.log("得到的 entry:", entry) return `${entry}.${profileIdc}.${compatibility}.${level}.B0`; } // 检测浏览器是否支持 H.265 export const browserSupportsH265 = (codecs) => { // 创建一个离屏元素,随后及时销毁 const video = document.createElement('video') // console.warn('--video--', video) // 以下为一些基本编码类型数据 const types = [ 'video/mp4; codecs="hvc1"', 'video/mp4; codecs="hev1"', 'video/mp4; codecs="hvc1.1.6.L93.B0"', 'video/mp4; codecs="hev1.1.6.L93.B0"', 'video/mp4; codecs="hvc1.1.6.L120.B0"', 'video/mp4; codecs="hev1.1.6.L120.B0"', 'video/x-matroska; codecs="V_MPEGH/ISO/HEVC"', ]; // 将解析的相关数据也放入判断中 types.push(`video/mp4; codecs="${codecs}"`); const isSupported = types.some(type => { const result = video.canPlayType(type); console.log(`检测 ${type}:${result}`); return result === "probably" || result === "maybe"; }); video.src = '' video.load() return isSupported } // 获取浏览器信息,当前是什么浏览器 export const getBrowserInfo = () => { const ua = navigator.userAgent // const witchBrowser = getBrowser(ua) console.warn('navigator.userAgent', ua) let console.log('浏览器信息:', ua, ua.split(' ')[1]) if (ua.includes('Edg') || ua.includes('Edge')) { console.log('Microsoft Edge'); browser = 'Microsoft Edge'; } else if (ua.includes('Chrome') && !ua.includes('Edg')) { console.log('Google Chrome'); browser = 'Google Chrome'; } else if (ua.includes('Firefox')) { console.log('Mozilla Firefox'); browser = 'Mozilla Firefox'; } else if (ua.includes('Safari') && !ua.includes('Chrome')) { console.log('Apple Safari'); browser = 'Apple Safari'; } else { console.log('Unknown Browser'); // 直接将浏览器名称返回 browser = ua.split(' ')[1]; } // 识别百度APP if (ua.includes('baiduboxapp')) { browser += '?baiduboxapp' } else { browser += '?' } return browser }

二:识别当前编码格式是否可以播放

        完成视频的解析工作后,这一步就相对简单多了,直接调用上述代码中的browserSupportsH265,去解析codecs参数就行:

import { browserSupportsH265 } from '@/utils/videoInfo' const isH265Support = () => { // 简单的类型检测,以及识别文件名称中是否有对应的关键词 H265-HEVC // props为父组件传递过来的相关信息 if (props.content.includes('H265-HEVC')) { console.log('视频名称:', props.content) // 识别名称末尾的编码信息 此处为识别对应的格式,各位可按照自己的需求去判断 const regex = /-codec_(.*?)-codecs_(.*?)-/ const match = props.content.match(regex); if (match) { const codec = match[1]; const codecs = match[2]; console.log('查找到的 codec', codec, '与 codecs', codecs) const supportsH265 = browserSupportsH265(codecs) console.log('是否支持H265', supportsH265) console.log('props.browserInfo', props.browserInfo) // 返回结果为不支持 if (!supportsH265) { // 百度浏览器是个特例,因为是它用的自己的播放器的缘故吗?不太清楚 if (props.browserInfo.includes('baiduboxapp')) { return } console.log('不支持H265') // 不支持,后续可按照需求去执行对应的操作 } } } }

三:多种判断是否可以播放的方式

// 检测浏览器是否支持 H.265 export const browserSupportsH265 = (codecs) => { // 创建一个离屏元素,随后及时销毁 const video = document.createElement('video') // console.warn('--video--', video) const types = [ 'video/mp4; codecs="hvc1"', 'video/mp4; codecs="hev1"', 'video/mp4; codecs="hvc1.1.6.L93.B0"', 'video/mp4; codecs="hev1.1.6.L93.B0"', 'video/mp4; codecs="hvc1.1.6.L120.B0"', 'video/mp4; codecs="hev1.1.6.L120.B0"', 'video/x-matroska; codecs="V_MPEGH/ISO/HEVC"', ]; types.push(`video/mp4; codecs="${codecs}"`); const isSupported = types.some(type => { const result = video.canPlayType(type); console.log(`检测 ${type}:${result}`); return result === "probably" || result === "maybe"; }); video.src = '' video.load() return isSupported }

        这是上述的方法,但使用的是canPlayType,剩下两种方法该如何使用?

MediaSource.isTypeSupported()

console.log('window.MediaSource', window.MediaSource) if (typeof window.MediaSource !== 'undefined') { const result = types.some(type => MediaSource.isTypeSupported(type)); console.log('MediaSource.isTypeSupported', result) }

        types仍无需改变,只需这样使用即可,但实测发现,IOS移动端设备,大部分都不兼容该方法,具体原因可自行查阅资料。

MediaCapabilities.decodingInfo()

const config = { type: 'file', video: { contentType: `video/mp4; codecs="${codecs}"`, // 视频宽度 width: 888, // 视频高度 height: 1920, // 必传参数 比特率 bitrate: 7572391, // 帧率 framerate: 47.498 } } const info = await navigator.mediaCapabilities.decodingInfo(config) console.log('info-----', info) }

        此方法,则不再使用types数组来遍历判断,而是直接传递视频的具体参数去判断,其中,type、contentType 与 bitrate 为必传参数,其他参数,如代码中的示例(不是全部的参数,但已经够用了)等,均可在前文中的解析视频数据videoTrack中获取:

// 解析 MediaInfo 结果 const parseMediaInfoResult = (analysis) => { console.log('开始解析 MediaInfo 结果...') if (!analysis?.media?.track) { throw new Error('无法解析媒体信息') } const tracks = Array.isArray(analysis.media.track) ? analysis.media.track : [analysis.media.track] console.log('视频轨道信息:', tracks) // videoTrack 就是最重要的视频信息对象 const videoTrack = tracks.find(track => track['@type'] === 'Video') const generalTrack = tracks.find(track => track['@type'] === 'General') // 检测 HEVC 编码 const codecs = H256Codecs(videoTrack) console.log('codecs', codecs) // 映射 HEVC 兼容性 const codecsCompatibility = browserSupportsH265(codecs) console.warn('codecsCompatibility', codecsCompatibility) console.log('videoTrack', videoTrack, 'generalTrack', generalTrack) if (!videoTrack) { return { codec: '未知', isH265: false, isH264: false, confidence: 0, message: '未检测到视频轨道' } } const format = videoTrack.Format || '' const codecId = videoTrack.CodecID || '' const formatLower = format.toLowerCase() const codecIdLower = codecId.toLowerCase() console.log('视频编码类型', format, codecId, formatLower, codecIdLower) // 判断编码类型 const isH265 = formatLower.includes('hevc') || codecIdLower.includes('hvc') || codecIdLower.includes('hev') const isH264 = formatLower.includes('avc') || codecIdLower.includes('avc') || formatLower.includes('h264') let codec = format let confidence = 95 if (isH265) { console.log('H.265 检测成功', codec) codec = 'H265-HEVC' } else if (isH264) { console.log('H264 检测成功', codec) codec = 'H264-AVC' } else if (format) { console.log('未知编码检测成功', format) codec = format confidence = 85 } else { codec = '未知编码' confidence = 0 } return { codec, isH265, isH264, confidence, format, codecId, codecs, // 根据自己的需求去传递相关参数 // profile: videoTrack.Format_Profile, // resolution: videoTrack.Width && videoTrack.Height // ? `${videoTrack.Width} × ${videoTrack.Height}` // : null, // duration: generalTrack?.Duration, // frameRate: videoTrack.FrameRate, // bitRate: videoTrack.BitRate || generalTrack?.OverallBitRate, // bitDepth: videoTrack.BitDepth, // tracks: tracks } }

        如果使用 MediaCapabilities.decodingInfo() 的话,那么之前的存储视频编码信息的相关操作就得改一下了,毕竟数据较多,不建议全存在文件名中,而是以参数回调的形式传递给后台,再从后台接口中获取才是最好的。

总结

        前端自己来获取视频消息,直接提示用户无法播放的方式,虽然消耗了一定的用户性能,且并没有解决无法播放的根源性问题。不过嘛,也还是挺不错的了,至少服务器资源不会在解析和转码的时候资源消耗过大了@w@。

        觉得有用的话,记得三联加关注鸭。

Read more

2026 AI元年:AI原生重构低代码,开发行业迎来范式革命

2026 AI元年:AI原生重构低代码,开发行业迎来范式革命

前言         2026 年,被全球科技产业正式定义为AI 规模化落地元年。 从实验室走向生产线、从对话交互走向系统内核、从锦上添花的功能插件走向底层驱动引擎,AI 不再是概念炒作,而是重构软件研发、企业服务、数字化转型的核心生产力。低代码开发平台,作为过去十年企业数字化落地最轻量化、最普及的工具,在 2026 年迎来最彻底的一次变革:AI 全面注入低代码,从 “可视化拖拽” 迈向 “意图驱动生成”。         长期以来,低代码行业始终面临两大争议:一是被技术开发者嘲讽 “只能做玩具系统,无法支撑企业级复杂场景”;二是被业务人员抱怨 “依旧需要懂技术、配规则、调逻辑,门槛依然很高”。而随着大模型技术成熟、国产模型规模化商用、AI 工程化能力落地,这一切正在被改写。         JNPF 作为企业级低代码平台的代表,在 2026 年全面完成 AI 原生架构升级,深度对接 Deepseek、通义千问、

Stable Diffusion模型下载器中文版:零基础掌握AI绘画模型下载技巧

Stable Diffusion模型下载器中文版:零基础掌握AI绘画模型下载技巧 【免费下载链接】sd-webui-model-downloader-cn 项目地址: https://gitcode.com/gh_mirrors/sd/sd-webui-model-downloader-cn 还在为下载Stable Diffusion模型而烦恼吗?🤔 这个专为国内用户设计的模型下载器中文版,让你彻底告别复杂的网络配置,轻松获取高质量AI绘画模型!本文将带你从零开始,全面掌握这款强大工具的使用方法。 🚀 一键安装:三种方式任你选择 WebUI界面直接安装(最推荐) 打开你的Stable Diffusion WebUI,进入"Extensions"标签页,选择"Install from URL",输入仓库地址即可完成安装。这种方式最安全可靠,避免手动操作可能出现的错误。 手动下载安装 如果你习惯传统方式,可以下载整个仓库文件,解压后放入WebUI目录下的extensions文件夹中。记得重启WebUI才能生效哦! 命令行快速安装 对于技术爱好者,使用git命令安装是