前端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

WorkBuddy 安装使用完全指南:腾讯版“小龙虾“,一句话让 AI 替你干活

不用部署云服务器,不用写代码,下载安装即可使用。WorkBuddy 是腾讯推出的 AI 原生桌面智能体工作台,让"一句话完成复杂办公任务"真正成为现实。 一、WorkBuddy 是什么? 1.1 一句话定义 WorkBuddy 是腾讯云推出的 AI 原生桌面智能体(Desktop AI Agent)工作台,基于腾讯 CodeBuddy 同源架构构建。它不是一个只会聊天的对话框,而是一个能听懂人话、自主思考、直接操作你电脑上文件的 AI 同事。 你只需用自然语言描述需求,WorkBuddy 就能自动规划、拆解、执行多步骤任务,直接交付可验收的成果——Excel 报表、PPT 演示文稿、调研报告、数据分析图表,应有尽有。 1.2

解放双手!用Windows搭建闲鱼0成本“赚米神器”!AI客服秒回复!

解放双手!用Windows搭建闲鱼0成本“赚米神器”!AI客服秒回复!

前言 在闲鱼上,卖家每天都要面对大量的私信和订单,如果手动回复,既费时间又容易出错。想象一下,如果有一套 AI 自动回复系统,能够帮你 自动处理买家消息、快速响应订单,你只需要动动手指,就能轻松管理闲鱼店铺,该有多爽!更棒的是,这套系统 完全零成本、无需服务器,只要一台 Windows 电脑,就能快速部署运行。本文将 手把手教你在 Windows 上搭建闲鱼 AI 自动回复系统,让你 轻松解放双手、提高效率,即刻开始自动化管理闲鱼店铺吧! 1 闲鱼自动回复系统介绍 闲鱼自动回复管理系统 是一个基于 Docker 部署的自动化工具,能够帮助闲鱼卖家实现消息的智能化回复和订单管理,大幅度减少手动操作的工作量。 核心功能说明自动化消息回复对闲鱼买家的消息进行自动回复,支持关键词触发和 AI 智能对话。可集成大语言模型(如通义千问)实现自然语言交流。多账号管理支持添加和管理多个闲鱼账号。账号间数据相互隔离,

OpenClaw WebSocket Channel开发实战:从零打造自定义 AI 通信通道

OpenClaw WebSocket Channel开发实战:从零打造自定义 AI 通信通道

🎯 项目背景 为什么做这个项目? 最近 OpenClaw 特别火🔥,这是一个强大的个人 AI 助手网关,支持接入 WhatsApp、Telegram、Discord 等 15+ 个消息平台。作为一个技术爱好者,我决定深入学习一下它的架构设计。 学习目标: * ✅ 理解多通道 AI 网关的架构模式 * ✅ 掌握 OpenClaw 插件化开发技能 * ✅ 实践 WebSocket 实时双向通信 * ✅ 为社区贡献一个实用的教学案例 项目定位:这不是一个生产级项目,而是一个学习性质的教学案例,帮助其他开发者快速上手 OpenClaw 插件开发。 技术栈 前端层:Vue 3 + WebSocket ↓ 服务端:Python + aiohttp + uv ↓ 通道层:Node.js + ws + OpenClaw Plugin SDK

从Copilot到Agentic:快手如何重构“人×AI×流程“研发铁三角

从Copilot到Agentic:快手如何重构“人×AI×流程“研发铁三角

编者按 一年前,行业热衷于追问“从Copilot到Coding Agent,我们离AI自主开发还有多远”;一年后,快手用万人研发组织的真实实践,给出了一个冷静而有力的回答:组织级提效的胜负手,从来不在AI是否“自主”,而在人、AI、流程三者能否完成范式级重构。 当AI代码生成率突破40%,需求交付周期却纹丝不动——这一反直觉现象戳破了“工具幻觉”的泡沫。快手的破局之道,并非等待Agent进化到完全自主,而是主动将AI从“嵌入流程的工具”升维为“重写流程的要素”,通过L1-L3分级交付体系与端到端效能度量,让个人提效真正传导至组织效能。53%的需求交付周期压缩、38%的人均交付需求增长,这些来自生产环境的数据,为行业提供了一份稀缺的规模化落地参照。 这不仅是一次技术演进,更是一场组织能力的“压力测试”:AI不会自动修复流程断点,它只会将隐性问题放大。真正的智能化转型,始于承认“人仍是流程的锚点”,终于实现“人×AI×流程”的乘数效应。 文章概要 本文基于快手技术团队首次系统披露的AI研发范式升级实践「快手万人组织AI研发范式