diff --git a/registry/lib/components/utils/download-emoticons/Widget.vue b/registry/lib/components/utils/download-emoticons/Widget.vue
new file mode 100644
index 000000000..56068784f
--- /dev/null
+++ b/registry/lib/components/utils/download-emoticons/Widget.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
diff --git a/registry/lib/components/utils/download-emoticons/emoticons.ts b/registry/lib/components/utils/download-emoticons/emoticons.ts
new file mode 100644
index 000000000..c6b893d64
--- /dev/null
+++ b/registry/lib/components/utils/download-emoticons/emoticons.ts
@@ -0,0 +1,106 @@
+import { monkey } from '@/core/ajax'
+import { useScopedConsole } from '@/core/utils/log'
+
+const logger = useScopedConsole('表情包下载')
+
+interface DownloadPromise {
+ pkg_name: string // 表情包名称
+ emoticons: { emoji: string; blob: Promise }[]
+}
+
+export class Emoticons {
+ private async getEmoticonsByRoomId(roomId: string) {
+ const response = await this.apiRequest(
+ `https://api.live.bilibili.com/xlive/web-ucenter/v2/emoticon/GetEmoticons?platform=pc&room_id=${roomId}`,
+ )
+ return response
+ }
+ async downloadEmoticons(emoticonsArray) {
+ const downloadPromises: DownloadPromise[] = emoticonsArray.map(obj => {
+ const emoticons = obj.emoticons.map(item => {
+ try {
+ const blob = this.getImageBlob(item.url)
+ return {
+ emoji: item.emoji,
+ blob,
+ }
+ } catch (error) {
+ logger.error(`下载表情 ${item.emoji} 失败:`, error)
+ return null
+ }
+ })
+ return {
+ emoticons,
+ pkg_name: obj.pkg_name,
+ }
+ })
+ return downloadPromises
+ }
+
+ async getEmoticonsArray(roomId: string): Promise> {
+ try {
+ let response = await this.getEmoticonsByRoomId(roomId)
+ if (response.data.data.length === 0) {
+ logger.log('获取表情包失败,尝试获取真实roomId')
+ const realRoomId = await this.getRealRoomId(roomId)
+ response = await this.getEmoticonsByRoomId(realRoomId)
+ }
+ return response.data.data.slice(2)
+ } catch (error) {
+ throw new Error('获取表情包失败')
+ }
+ }
+
+ async batchDownload(tasks: DownloadPromise[]) {
+ const results: any[] = []
+ for (const task of tasks) {
+ const promise = (async () => {
+ const blobs: Blob[] = []
+ const blobPromises = task.emoticons.map(e => e.blob)
+ for (const blobPromise of blobPromises) {
+ const blob = await blobPromise
+ blobs.push(blob)
+ }
+ return {
+ pkg_name: task.pkg_name,
+ emoticons: task.emoticons.map((e, i) => ({ emoji: e.emoji, blob: blobs[i] })),
+ }
+ })()
+ results.push(promise)
+ }
+ return Promise.all(results)
+ }
+
+ // 有的直播间id是超短id,需要获取到原始的roomid才能通过api获取到表情包
+ async getRealRoomId(rid: string): Promise {
+ try {
+ const response = await this.apiRequest(
+ `https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${rid}`,
+ )
+ if (Object.keys(response.data).length === 0) {
+ throw new Error('获取直播间id失败')
+ }
+ return response.data.room_id
+ } catch (error) {
+ throw new Error('获取直播间id失败')
+ }
+ }
+
+ apiRequest = (url: string): Promise => {
+ const details: MonkeyXhrBasicDetails = {
+ url,
+ method: 'GET',
+ responseType: 'json',
+ }
+ return monkey(details)
+ }
+
+ getImageBlob = async (url: string) => {
+ const details: MonkeyXhrBasicDetails = {
+ url,
+ method: 'GET',
+ responseType: 'blob',
+ }
+ return monkey(details)
+ }
+}
diff --git a/registry/lib/components/utils/download-emoticons/index.md b/registry/lib/components/utils/download-emoticons/index.md
new file mode 100644
index 000000000..7afd6ff1b
--- /dev/null
+++ b/registry/lib/components/utils/download-emoticons/index.md
@@ -0,0 +1,3 @@
+## 下载up主专属表情包
+
+支持下载up主专属表情包,处于任意直播间页面时,下载按钮会在`功能`面板显示,以压缩包形式保存。
\ No newline at end of file
diff --git a/registry/lib/components/utils/download-emoticons/index.ts b/registry/lib/components/utils/download-emoticons/index.ts
new file mode 100644
index 000000000..5fc1cfa95
--- /dev/null
+++ b/registry/lib/components/utils/download-emoticons/index.ts
@@ -0,0 +1,16 @@
+import { defineComponentMetadata } from '@/components/define'
+
+export const component = defineComponentMetadata({
+ name: 'downloadEmoticons',
+ displayName: '下载表情',
+ tags: [componentsTags.utils, componentsTags.live],
+ entry: none,
+ author: {
+ name: 'Pencilqaq',
+ link: 'https://github.com/pencilqaq',
+ },
+ widget: {
+ component: () => import('./Widget.vue').then(m => m.default),
+ },
+ urlInclude: [/^https:\/\/live\.bilibili\.com\/(\d+)/],
+})