From 159009dd56b7ac9ffa006738f207a8f42c17d211 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Mon, 22 Dec 2025 21:56:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=B1=BB=E5=9E=8B):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E6=90=9C=E7=B4=A2=E7=B1=BB=E5=9E=8B=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=92=8C=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 SearchVideoData 类型定义,包含视频标题、链接、时长、作者等字段 - 新增 Message 类型的 searchVideos 属性用于存储视频搜索结果 - 添加 videoUtils.ts 视频工具函数库: - parseVideoUrl: 解析B站/YouTube/抖音视频链接 - formatDuration: 格式化视频时长显示 - getPlatformName/getPlatformColor: 获取平台名称和主题色 - detectPlatform: 检测视频平台类型 - supportsEmbed: 判断是否支持嵌入播放 - getEmbedUrlWithPage: 生成带选集参数的嵌入URL - supportsEpisodes: 判断是否支持选集功能 --- src/lib/videoUtils.ts | 185 ++++++++++++++++++++++++++++++++++++++++++ src/types/index.ts | 24 ++++++ 2 files changed, 209 insertions(+) create mode 100644 src/lib/videoUtils.ts diff --git a/src/lib/videoUtils.ts b/src/lib/videoUtils.ts new file mode 100644 index 0000000..681bd08 --- /dev/null +++ b/src/lib/videoUtils.ts @@ -0,0 +1,185 @@ +/** + * 视频工具函数 + * 处理视频链接解析、时长格式化等 + */ + +/** + * 视频平台类型 + */ +export type VideoPlatform = 'bilibili' | 'youtube' | 'douyin' | 'other'; + +/** + * 视频解析信息 + */ +export interface VideoInfo { + platform: VideoPlatform; + videoId: string; + embedUrl: string; +} + +/** + * 解析视频链接,提取平台和视频ID + * @param url 视频链接 + * @returns 视频信息,解析失败返回 null + */ +export function parseVideoUrl(url: string): VideoInfo | null { + if (!url) return null; + + try { + // B站视频解析 - av号格式 + // 格式: bilibili.com/video/av123456 或 www.bilibili.com/video/av123456 + const bilibiliAvMatch = url.match(/bilibili\.com\/video\/av(\d+)/i); + if (bilibiliAvMatch) { + const aid = bilibiliAvMatch[1]; + return { + platform: 'bilibili', + videoId: `av${aid}`, + embedUrl: `//player.bilibili.com/player.html?aid=${aid}&high_quality=1&danmaku=0`, + }; + } + + // B站视频解析 - BV号格式 + // 格式: bilibili.com/video/BV1xx411c7mD + const bilibiliBvMatch = url.match(/bilibili\.com\/video\/(BV[\w]+)/i); + if (bilibiliBvMatch) { + const bvid = bilibiliBvMatch[1]; + return { + platform: 'bilibili', + videoId: bvid, + embedUrl: `//player.bilibili.com/player.html?bvid=${bvid}&high_quality=1&danmaku=0`, + }; + } + + // YouTube 视频解析 + // 格式: youtube.com/watch?v=xxx 或 youtu.be/xxx + const youtubeMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/); + if (youtubeMatch) { + const videoId = youtubeMatch[1]; + return { + platform: 'youtube', + videoId, + embedUrl: `https://www.youtube.com/embed/${videoId}`, + }; + } + + // 抖音视频解析 (预留) + // 格式: douyin.com/video/xxx + const douyinMatch = url.match(/douyin\.com\/video\/(\d+)/); + if (douyinMatch) { + return { + platform: 'douyin', + videoId: douyinMatch[1], + embedUrl: '', // 抖音暂不支持嵌入 + }; + } + + return null; + } catch { + return null; + } +} + +/** + * 格式化视频时长 + * @param seconds 秒数(字符串或数字) + * @returns 格式化后的时长 (如 "2:34" 或 "1:02:34") + */ +export function formatDuration(seconds: string | number): string { + const totalSeconds = typeof seconds === 'string' ? parseInt(seconds, 10) : seconds; + + if (isNaN(totalSeconds) || totalSeconds < 0) { + return '--:--'; + } + + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const secs = totalSeconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + + return `${minutes}:${secs.toString().padStart(2, '0')}`; +} + +/** + * 获取视频平台显示名称 + * @param platform 平台标识 + * @returns 平台中文名称 + */ +export function getPlatformName(platform: VideoPlatform): string { + const names: Record = { + bilibili: 'B站', + youtube: 'YouTube', + douyin: '抖音', + other: '视频', + }; + return names[platform] || '视频'; +} + +/** + * 获取视频平台主题色 + * @param platform 平台标识 + * @returns 平台主题色 (hex) + */ +export function getPlatformColor(platform: VideoPlatform): string { + const colors: Record = { + bilibili: '#fb7299', + youtube: '#ff0000', + douyin: '#000000', + other: '#6b7280', + }; + return colors[platform] || '#6b7280'; +} + +/** + * 检测视频链接的平台 + * @param url 视频链接 + * @returns 平台标识 + */ +export function detectPlatform(url: string): VideoPlatform { + if (!url) return 'other'; + + if (url.includes('bilibili.com')) return 'bilibili'; + if (url.includes('youtube.com') || url.includes('youtu.be')) return 'youtube'; + if (url.includes('douyin.com')) return 'douyin'; + + return 'other'; +} + +/** + * 检查视频是否支持嵌入播放 + * @param platform 平台标识 + * @returns 是否支持嵌入 + */ +export function supportsEmbed(platform: VideoPlatform): boolean { + return platform === 'bilibili' || platform === 'youtube'; +} + +/** + * 生成带选集参数的嵌入 URL + * @param url 原始视频链接 + * @param page 选集编号(从1开始) + * @returns 嵌入 URL + */ +export function getEmbedUrlWithPage(url: string, page: number = 1): string | null { + const videoInfo = parseVideoUrl(url); + if (!videoInfo) return null; + + if (videoInfo.platform === 'bilibili') { + // B站嵌入链接支持 p 参数指定分P + return `${videoInfo.embedUrl}&p=${page}`; + } + + // 其他平台暂不支持选集 + return videoInfo.embedUrl; +} + +/** + * 检查平台是否支持选集 + * @param platform 平台标识 + * @returns 是否支持选集 + */ +export function supportsEpisodes(platform: VideoPlatform): boolean { + return platform === 'bilibili'; +} diff --git a/src/types/index.ts b/src/types/index.ts index 9bec5c5..eb2cdcd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -36,6 +36,8 @@ export interface Message { usedTools?: string[]; /** 搜索到的图片(图片搜索工具结果) */ searchImages?: SearchImageData[]; + /** 搜索到的视频(视频搜索工具结果) */ + searchVideos?: SearchVideoData[]; } // 搜索图片数据类型 @@ -49,6 +51,28 @@ export interface SearchImageData { sourceUrl?: string; } +// 搜索视频数据类型 +export interface SearchVideoData { + /** 视频标题 */ + title: string; + /** 视频链接 */ + link: string; + /** 视频摘要 */ + snippet: string; + /** 相关度评分 */ + score: string; + /** 排序位置 */ + position: number; + /** 作者列表 */ + authors: string[]; + /** 发布日期 */ + date: string; + /** 时长(秒) */ + duration: string; + /** 封面图URL */ + coverImage: string; +} + // 工具调用记录 export interface ToolCall { id: string;