diff --git a/plugins/huhabiaoqingbao/.gitignore b/plugins/huhabiaoqingbao/.gitignore new file mode 100644 index 000000000..e1cf1a830 --- /dev/null +++ b/plugins/huhabiaoqingbao/.gitignore @@ -0,0 +1,9 @@ +node_modules +dist +release +.DS_Store +*.log +.env +.env.* +src/components/emoji/emoji.md +src/components/emoji/kaomiji.md diff --git a/plugins/huhabiaoqingbao/123/README.md b/plugins/huhabiaoqingbao/123/README.md new file mode 100644 index 000000000..c0336bf35 --- /dev/null +++ b/plugins/huhabiaoqingbao/123/README.md @@ -0,0 +1,282 @@ +# {{PLUGIN_NAME}} + +> {{DESCRIPTION}} + +这是一个使用 **Vue 3 + Vite + TypeScript** 构建的 ZTools 插件。 + +## ✨ 功能特性 + +### 📌 已包含的示例功能 + +- **Hello** - 基础功能指令示例 + - 触发指令:`你好` / `hello` + - 展示简单的 Vue 组件界面 + +- **读文件** - 文件读取功能示例 + - 功能指令:`读文件` + - 匹配指令:支持拖拽文件触发 + - 演示如何使用 Node.js 能力读取文件内容 + +- **保存为文件** - 文件写入功能示例 + - 匹配指令:任意文本/图片 → `保存为文件` + - 演示如何将剪贴板内容保存为文件 + +## 📁 项目结构 + +``` +. +├── src/ +│ ├── main.ts # 入口文件 +│ ├── main.css # 全局样式 +│ ├── App.vue # 根组件 +│ ├── env.d.ts # 类型声明 +│ ├── Hello/ # Hello 功能组件 +│ │ └── index.vue +│ ├── Read/ # 读文件功能组件 +│ │ └── index.vue +│ └── Write/ # 写文件功能组件 +│ └── index.vue +├── src-ztools/ # ZTools 插件目录 +│ ├── logo.png # 插件图标 +│ ├── plugin.json # 插件配置文件 +│ ├── preload/ # Preload 脚本目录 +│ │ ├── package.json # Preload 依赖配置 +│ │ └── services.js # Node.js 能力扩展 +│ └── dist/ # Vite 构建产物 +├── index.html # HTML 模板 +├── vite.config.js # Vite 配置 +├── tsconfig.json # TypeScript 配置 +├── package.json # 项目依赖 +└── README.md # 项目文档 +``` + +## 🚀 快速开始 + +### 安装依赖 + +```bash +npm install +``` + +### 开发模式 + +```bash +npm run dev +``` + +开发服务器将在 `http://localhost:5173` 启动。ZTools 会自动加载开发版本。 + +### 构建生产版本 + +```bash +npm run build +``` + +构建产物将输出到 `src-ztools/dist/` 目录,插件配置、图标和 Preload 保留在同级目录。 + +## 📖 开发指南 + +### 1. 修改插件配置 + +编辑 `src-ztools/plugin.json` 文件: + +```json +{ + "name": "你的插件名称", + "description": "插件描述", + "author": "作者名称", + "version": "1.0.0", + "features": [ + // 添加你的功能配置 + ] +} +``` + +### 2. 创建新功能 + +#### 步骤 1: 创建 Vue 组件 + +在 `src/` 目录下创建新的功能组件: + +```vue + + + + + + +``` + +#### 步骤 2: 注册路由 + +在 `src/App.vue` 中添加路由: + +```vue + +``` + +#### 步骤 3: 配置功能 + +在 `plugin.json` 中添加功能配置: + +```json +{ + "code": "myfeature", + "explain": "我的新功能", + "icon": "logo.png", + "cmds": ["触发指令"] +} +``` + +### 3. 使用 Node.js 能力 + +#### 扩展 Preload 服务 + +编辑 `src-ztools/preload/services.js`: + +```javascript +const fs = require('fs') +const path = require('path') + +module.exports = { + // 示例:读取文件 + readFile: (filePath) => { + return fs.readFileSync(filePath, 'utf-8') + }, + + // 添加你的服务 + myService: (params) => { + // 实现你的逻辑 + return result + } +} +``` + +#### 在 Vue 组件中调用 + +```vue + +``` + +### 4. 使用 ZTools API + +```vue + +``` + +## 🎨 样式开发 + +### 使用 CSS 变量 + +ZTools 提供了一套 CSS 变量用于主题适配: + +```css +.my-component { + background: var(--bg-color); + color: var(--text-color); + border: 1px solid var(--border-color); +} +``` + +### 暗色模式支持 + +```css +@media (prefers-color-scheme: dark) { + .my-component { + /* 暗色模式样式 */ + } +} +``` + +## 📦 构建与发布 + +### 1. 构建插件 + +```bash +npm run build +``` + +### 2. 测试构建产物 + +将 `src-ztools/` 作为完整 ZTools 插件目录进行测试或打包。 + +### 3. 发布到插件市场 + +1. 确保 `plugin.json` 中的信息完整准确 +2. 准备好插件截图和详细说明 +3. 访问 ZTools 插件市场提交插件 + +## 📚 相关资源 + +- [ZTools 官方文档](https://github.com/ztool-center/ztools) +- [ZTools API 文档](https://github.com/ztool-center/ztools-api-types) +- [Vue 3 文档](https://vuejs.org/) +- [Vite 文档](https://vitejs.dev/) + +## ❓ 常见问题 + +### Q: 如何调试插件? + +A: 使用 `npm run dev` 启动开发服务器,在插件界面中点击插件头像图标,在弹出的菜单中选择"打开开发者工具"进行调试。 + +### Q: 如何访问 Node.js 能力? + +A: 通过 `src-ztools/preload/services.js` 文件扩展服务,然后在组件中使用 `window.services` 调用。 + +### Q: 插件图标不显示? + +A: 确保 `src-ztools/logo.png` 文件存在,且在 `plugin.json` 中正确配置了 `logo` 字段。 + +### Q: 如何处理大文件上传? + +A: 建议使用 Node.js 流式处理,在 preload 脚本中实现文件分块处理逻辑。 + +## 📄 开源协议 + +MIT License + +--- + +**祝你开发愉快!** 🎉 diff --git a/plugins/huhabiaoqingbao/123/index.html b/plugins/huhabiaoqingbao/123/index.html new file mode 100644 index 000000000..eaa17316c --- /dev/null +++ b/plugins/huhabiaoqingbao/123/index.html @@ -0,0 +1,11 @@ + + + + + + + +
+ + + diff --git a/plugins/huhabiaoqingbao/123/package.json b/plugins/huhabiaoqingbao/123/package.json new file mode 100644 index 000000000..4f0d659ec --- /dev/null +++ b/plugins/huhabiaoqingbao/123/package.json @@ -0,0 +1,20 @@ +{ + "name": "123", + "version": "1.0.0", + "description": "", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build" + }, + "dependencies": { + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "@ztools-center/ztools-api-types": "^1.0.1", + "typescript": "^5.3.0", + "vite": "^6.0.11", + "vue-tsc": "^2.0.0" + } +} diff --git a/plugins/huhabiaoqingbao/123/src-ztools/logo.png b/plugins/huhabiaoqingbao/123/src-ztools/logo.png new file mode 100644 index 000000000..f195f8779 Binary files /dev/null and b/plugins/huhabiaoqingbao/123/src-ztools/logo.png differ diff --git a/plugins/huhabiaoqingbao/123/src-ztools/plugin.json b/plugins/huhabiaoqingbao/123/src-ztools/plugin.json new file mode 100644 index 000000000..79d5fe073 --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src-ztools/plugin.json @@ -0,0 +1,60 @@ +{ + "$schema": "node_modules/@ztools-center/ztools-api-types/resource/ztools.schema.json", + "name": "123", + "title": "呼哈表情包", + "description": "", + "author": "", + "version": "1.0.0", + "main": "dist/index.html", + "preload": "preload/services.js", + "logo": "logo.png", + "development": { + "main": "http://localhost:5173" + }, + "features": [ + { + "code": "hello", + "explain": "这是插件应用的第一个功能", + "icon": "logo.png", + "cmds": [ + "你好", + "hello" + ] + }, + { + "code": "read", + "explain": "功能指令+匹配指令示例,使用 node.js 能力读文件", + "icon": "logo.png", + "cmds": [ + "读文件", + { + "type": "files", + "fileType": "file", + "maxLength": 1, + "label": "读文件" + } + ] + }, + { + "code": "write", + "explain": "匹配指令示例,使用 node.js 能力写文件", + "icon": "logo.png", + "mainHide": true, + "cmds": [ + { + "type": "over", + "label": "保存为文件" + }, + { + "type": "img", + "label": "保存为文件" + } + ] + } + ], + "platform": [ + "darwin", + "win32", + "linux" + ] +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/123/src-ztools/preload/package.json b/plugins/huhabiaoqingbao/123/src-ztools/preload/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src-ztools/preload/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/plugins/huhabiaoqingbao/123/src-ztools/preload/services.js b/plugins/huhabiaoqingbao/123/src-ztools/preload/services.js new file mode 100644 index 000000000..83c3ef1f5 --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src-ztools/preload/services.js @@ -0,0 +1,27 @@ +const fs = require('node:fs') +const path = require('node:path') + +// 通过 window 对象向渲染进程注入 nodejs 能力 +window.services = { + // 读文件 + readFile(file) { + return fs.readFileSync(file, { encoding: 'utf-8' }) + }, + // 文本写入到下载目录 + writeTextFile(text) { + const filePath = path.join(window.ztools.getPath('downloads'), Date.now().toString() + '.txt') + fs.writeFileSync(filePath, text, { encoding: 'utf-8' }) + return filePath + }, + // 图片写入到下载目录 + writeImageFile(base64Url) { + const matchs = /^data:image\/([a-z]{1,20});base64,/i.exec(base64Url) + if (!matchs) return + const filePath = path.join( + window.ztools.getPath('downloads'), + Date.now().toString() + '.' + matchs[1] + ) + fs.writeFileSync(filePath, base64Url.substring(matchs[0].length), { encoding: 'base64' }) + return filePath + } +} diff --git a/plugins/huhabiaoqingbao/123/src/App.vue b/plugins/huhabiaoqingbao/123/src/App.vue new file mode 100644 index 000000000..d4fabacba --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src/App.vue @@ -0,0 +1,25 @@ + + + diff --git a/plugins/huhabiaoqingbao/123/src/Hello/index.vue b/plugins/huhabiaoqingbao/123/src/Hello/index.vue new file mode 100644 index 000000000..8fc58146c --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src/Hello/index.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/123/src/Read/index.vue b/plugins/huhabiaoqingbao/123/src/Read/index.vue new file mode 100644 index 000000000..903fea36d --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src/Read/index.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/123/src/Write/index.vue b/plugins/huhabiaoqingbao/123/src/Write/index.vue new file mode 100644 index 000000000..9eb172bcf --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src/Write/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/plugins/huhabiaoqingbao/123/src/env.d.ts b/plugins/huhabiaoqingbao/123/src/env.d.ts new file mode 100644 index 000000000..013d4ab24 --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src/env.d.ts @@ -0,0 +1,23 @@ +/// +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent, Record, unknown> + export default component +} + +// Preload services 类型声明(对应 src-ztools/preload/services.js) +interface Services { + readFile: (file: string) => string + writeTextFile: (text: string) => string + writeImageFile: (base64Url: string) => string | undefined +} + +declare global { + interface Window { + services: Services + } +} + +export {} diff --git a/plugins/huhabiaoqingbao/123/src/main.css b/plugins/huhabiaoqingbao/123/src/main.css new file mode 100644 index 000000000..c372a2a3f --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src/main.css @@ -0,0 +1,63 @@ +:root { + --blue: rgb(88, 164, 246); + --light: #fff; +} + +html, +body { + margin: 0; + padding: 0; +} + +button { + border: none; + background: none var(--blue); + color: var(--light); + line-height: 2.5; + cursor: pointer; + transition: opacity 0.2s; +} + +button:disabled { + filter: grayscale(1); + cursor: not-allowed; +} + +button:not(:disabled):active { + opacity: 0.6; +} + +textarea { + display: block; + margin: 0; +} + +@media (prefers-color-scheme: light) { + body { + background-color: #f4f4f4; + } + + ::-webkit-scrollbar-track-piece { + background-color: #f4f4f4; + } + + ::-webkit-scrollbar-thumb { + border-color: #f4f4f4; + } +} + +@media (prefers-color-scheme: dark) { + &::-webkit-scrollbar-track-piece { + background-color: #303133; + } + + &::-webkit-scrollbar-thumb { + background-color: #666; + border-color: #303133; + } + + body { + background-color: #303133; + color: #fff; + } +} diff --git a/plugins/huhabiaoqingbao/123/src/main.ts b/plugins/huhabiaoqingbao/123/src/main.ts new file mode 100644 index 000000000..8a3c77bc5 --- /dev/null +++ b/plugins/huhabiaoqingbao/123/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './main.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/plugins/huhabiaoqingbao/123/tsconfig.json b/plugins/huhabiaoqingbao/123/tsconfig.json new file mode 100644 index 000000000..39b73bc6f --- /dev/null +++ b/plugins/huhabiaoqingbao/123/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": false, + "noImplicitAny": false, + "types": ["@ztools-center/ztools-api-types"] + }, + "include": ["src"] +} diff --git a/plugins/huhabiaoqingbao/123/vite.config.js b/plugins/huhabiaoqingbao/123/vite.config.js new file mode 100644 index 000000000..5ca697ff2 --- /dev/null +++ b/plugins/huhabiaoqingbao/123/vite.config.js @@ -0,0 +1,13 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + base: './', + build: { + outDir: fileURLToPath(new URL('./src-ztools/dist', import.meta.url)), + emptyOutDir: true + } +}) diff --git a/plugins/huhabiaoqingbao/CHANGELOG.md b/plugins/huhabiaoqingbao/CHANGELOG.md new file mode 100644 index 000000000..b44cde1a0 --- /dev/null +++ b/plugins/huhabiaoqingbao/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +## 1.0.0 + +首个 ZTools 版本。 + +### 功能 +- 本地表情包收藏管理:导入、标签分类、收藏、预览、编辑、批量导出 +- 表情包联网检索,支持多图源切换 +- emoji 与颜文字检索 +- 微信 / QQ / 飞书表情包批量导入 +- 视频转 GIF(裁剪、帧率、质量、尺寸、文字、循环) +- AI 生成表情包(需在「系统设置 → AI 生成」中填写自己的 Coze 凭据) +- 选中文本后可通过「用呼哈表情包搜索」直接检索 +- 暗黑模式与多主题色 + +### 说明 +- 预设表情与关于页图标随插件打包,运行时不依赖任何对象存储服务 +- 第三方凭据(Coze、百度翻译)由用户自行填写,仅保存在本机插件数据目录,不会上传 diff --git a/plugins/huhabiaoqingbao/LICENSE b/plugins/huhabiaoqingbao/LICENSE new file mode 100644 index 000000000..09f1b2dbe --- /dev/null +++ b/plugins/huhabiaoqingbao/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 HUHA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/huhabiaoqingbao/README.md b/plugins/huhabiaoqingbao/README.md new file mode 100644 index 000000000..a073a21e7 --- /dev/null +++ b/plugins/huhabiaoqingbao/README.md @@ -0,0 +1,191 @@ +# 呼哈表情包管理器 (BQB) + +一个优雅的 ZTools 表情包管理插件,可以保存和收藏喜欢的表情包,可以联网检索表情包,可以自己制作表情包,让你的表情包收藏和使用更加便捷。 + +-- 表情存储在本地,请注意备份,避免数据丢失,可以通过导出功能导出全部表情包,这是咱宝贵的资产,丢了可亏大了。 +-- 联网检索支持不同的接口,接口都来自互联网,如有无法使用的情况,那就是被 ban 了,请自行更换接口。 +-- 发送按钮目前仅支持分离为独立窗口运行时,粘贴图片到光标所在位置。 + +作者是个后端开发,该工具全靠 AI 辅助开发,作者几乎零编码。兄弟们有任何问题和建议都可以提,我继续去敲打 AI。 + +## ✨ 功能特点 + +让你的表情包管理更加优雅高效! + +🎯 **简洁优雅的界面设计** +- 精心设计的用户界面,让你的表情包管理体验前所未有的流畅 +- 固定窗口大小(800x600),完美适配各种使用场景 + +📂 **智能的表情包管理** +- 支持点击或粘贴导入表情包 +- 智能分类和标签管理 +- 快速预览和编辑功能 +- 支持表情包制作和简单编辑 +- 支持表情包联网检索 +- 支持 emoji 检索 +- 支持颜表情检索 +- 支持 AI 生成表情功能(需自备凭据) +- 支持视频转 GIF 功能 + +## 使用方法 + +可以通过以下任意命令唤起表情包管理器: +- 表情包 +- bqb +- BiaoQingBao +- 表情 +- emoji + +选中文本后还可以通过「用呼哈表情包搜索」直接检索该关键词。 + +### AI 生成表情功能 + +该功能依赖扣子(Coze)的 API,需要你自己的凭据: + +1. 打开插件的「系统设置 → AI 生成」 +2. 填写自己的 Coze Token 与 Bot ID 并保存 +3. 在「AI 生成表情」菜单中输入描述文本,点击「生成表情」 +4. 对生成的表情包进行保存或复制操作,生成结果会自动带上「AI 生成」标签 + +未填写凭据时该功能不可用,其余功能不受影响。凭据仅保存在本机的插件数据目录中,不会上传。 + +### 壁纸/表情联网搜索翻译(可选) + +中文关键词翻译成英文可以提升部分图源的搜索效果。在「系统设置 → 壁纸搜索翻译」中填写你自己的百度翻译开放平台 APPID 与密钥即可启用;不填则直接使用原关键词搜索。 + +### 视频转 GIF 功能 + +在「视频转 GIF」菜单中,你可以: +1. 上传视频文件(支持 MP4、AVI、MOV、WMV 等格式) +2. 预览视频内容 +3. 调整 GIF 输出设置:质量(1-10)、帧率(1-30fps)、输出尺寸(原始尺寸、480p、360p、240p) +4. 视频裁剪 +5. 添加文字 +6. 一键转换为 GIF 格式 +7. 支持 GIF 循环播放 +8. 转换完成后可以直接保存到表情包库中 + +## 技术架构 + +- 前端框架:Vue 3 + TypeScript +- UI 组件:Element Plus +- 构建工具:Vite +- 数据存储:本地文件 + IndexedDB +- 插件运行时:ZTools API(`window.ztools`)+ preload.js + +## 项目结构 + +``` +baoqingbao_ztools/ +├── src/ # 源代码目录 +│ ├── api/ # 外部接口封装(搜索、翻译、AI) +│ ├── assets/ # 静态资源 +│ ├── components/ # Vue 组件 +│ ├── config/ # 资源路径与凭据读写 +│ ├── services/ # 业务服务 +│ ├── store/ # 状态管理 +│ ├── types/ # 类型声明(含 ztools.d.ts) +│ ├── utils/ # 工具函数 +│ └── App.vue # 主组件 +├── public/ # 随插件打包的静态文件 +│ ├── images/ # 关于页图标 +│ └── preset-images/ # 预设表情包图片 +│ ├── funny/ # 搞笑类表情 +│ ├── animal/ # 动物类表情 +│ └── face/ # 表情类表情 +├── scripts/ # 构建脚本 +├── plugin.json # ZTools 插件配置 +├── preload.js # ZTools preload(CommonJS) +└── package.json # 项目依赖 +``` + +预设图片与关于页图标全部随插件打包在 `public/` 下,插件运行时不依赖任何对象存储服务。 + +## 本地运行 + +### 前置条件 + +1. 安装 Node.js 和 npm + - 访问 [Node.js 官网](https://nodejs.org/) + - 下载并安装最新的 LTS 版本 + - 验证安装: + ```bash + node --version + npm --version + ``` + +2. 安装 [ZTools](https://github.com/ZToolsCenter/ZTools) + +### 开发步骤 + +1. 安装依赖 +```bash +npm install +``` + +2. 开发模式运行 +```bash +npm run dev +``` + +`plugin.json` 中的 `development.main` 指向 `http://localhost:5173`,在 ZTools 中以开发模式加载本项目目录即可热更新调试。 + +3. 构建插件 +```bash +npm run build +``` + +构建产物在 `dist/`,其中已包含 `plugin.json`、`preload.js`、`logo.png` 及全部静态资源。 + +4. 在 ZTools 中加载插件 +- 打开 ZTools 插件管理 +- 选择开发者/本地插件 +- 选择本项目目录(开发模式)或 `dist/` 目录(构建产物) + +## 数据存储说明 + +### 存储位置 + +表情包和相关数据存储在 ZTools 的插件数据目录下: +- Windows: `%APPDATA%//bqb/emoticons/` +- macOS: `~/Library/Application Support//bqb/emoticons/` +- Linux: `~/.config//bqb/emoticons/` + +具体路径由 ZTools 的 `getPath` 决定。 + +### 存储内容 + +1. 表情包文件 + - 位置:`emoticons/[表情包ID].dat` + - 格式:二进制文件 + - 说明:每个表情包单独存储,保证数据隔离 + +2. 元数据文件 + - 位置:`emoticons/metadata.json` + - 格式:JSON + - 内容:包含表情包的名称、标签、收藏状态等信息 + - 示例: + ```json + { + "id": "unique-id", + "name": "表情包名称", + "tags": ["搞笑", "动物"], + "favorite": true, + "createdAt": 1678086524140 + } + ``` + +3. 设置文件 + - 位置:`emoticons/settings.json` + - 内容:界面设置、主题色,以及你自己填写的 Coze / 百度翻译凭据 + +### 注意事项 + +1. 请不要手动修改存储目录中的文件 +2. 如需备份,请备份整个 emoticons 目录 +3. 卸载插件前请先导出重要数据 +4. `settings.json` 中可能包含你的第三方凭据,分享数据目录前请先清空相关设置 + +## License + +见 [LICENSE](./LICENSE)。 diff --git a/plugins/huhabiaoqingbao/index.html b/plugins/huhabiaoqingbao/index.html new file mode 100644 index 000000000..9a231075c --- /dev/null +++ b/plugins/huhabiaoqingbao/index.html @@ -0,0 +1,19 @@ + + + + + + + 呼哈表情包 + + + +
+ + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/logo.png b/plugins/huhabiaoqingbao/logo.png new file mode 100644 index 000000000..0ba4c7e8f Binary files /dev/null and b/plugins/huhabiaoqingbao/logo.png differ diff --git a/plugins/huhabiaoqingbao/package-lock.json b/plugins/huhabiaoqingbao/package-lock.json new file mode 100644 index 000000000..88d43e3ef --- /dev/null +++ b/plugins/huhabiaoqingbao/package-lock.json @@ -0,0 +1,2952 @@ +{ + "name": "huhabiaoqingbao", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "huhabiaoqingbao", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@types/md5": "^2.3.5", + "axios": "^1.9.0", + "cheerio": "^1.1.2", + "element-plus": "^2.5.3", + "gifshot": "^0.4.5", + "html2canvas": "^1.4.1", + "idb": "^8.0.0", + "jszip": "^3.10.1", + "md5": "^2.3.0", + "nanoid": "^5.0.4", + "pinia": "^2.1.7", + "vue": "^3.3.11", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^20.17.50", + "@vitejs/plugin-vue": "^4.5.2", + "sass": "^1.70.0", + "typescript": "^5.2.2", + "vite": "^5.0.8", + "vue-tsc": "^2.2.12" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/md5": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/@types/md5/-/md5-2.3.6.tgz", + "integrity": "sha512-WD69gNXtRBnpknfZcb4TRQ0XJQbUPZcai/Qdhmka3sxUR3Et8NrXoeAoknG/LghYHTf4ve795rInVYHBTQdNVA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.42.tgz", + "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.42", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", + "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", + "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.42", + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-ssr": "3.5.42", + "@vue/shared": "3.5.42", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", + "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.42.tgz", + "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.42.tgz", + "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", + "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/runtime-core": "3.5.42", + "@vue/shared": "3.5.42", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.42.tgz", + "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.42.tgz", + "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.4.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.4.0.tgz", + "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.4.0", + "@vueuse/shared": "14.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.4.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.4.0.tgz", + "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.4.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.4.0.tgz", + "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.5", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.5.tgz", + "integrity": "sha512-bghYy/S+qg87enHPXELirhEdDqsVAUGcGpbGIeG8dz0kwpIkGz7gYsifulBshXX74iRtHib85XWQj0uSH2A1Yg==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.8.0", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.4.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.9" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gifshot": { + "version": "0.4.5", + "resolved": "https://registry.npmmirror.com/gifshot/-/gifshot-0.4.5.tgz", + "integrity": "sha512-oaOTT7patjxFFv7ptR0R0NNhqy3ZAmcLUQCjM/sTsvsQaUAlB2fHirLajcNAKJ6ufoVhdP+ZkXYvmUycHP1FNg==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://registry.npmmirror.com/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.103.1", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.103.1.tgz", + "integrity": "sha512-9icZURbP51S6S0QGoyaeqk9uB06GNWxsFYWfH5RgpFgqK5FA8tJcM3AdVxrZEVJ7dz+L87nG95gBKf4VuaMHGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.42.tgz", + "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-sfc": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/server-renderer": "3.5.42", + "@vue/shared": "3.5.42" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz", + "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/plugins/huhabiaoqingbao/package.json b/plugins/huhabiaoqingbao/package.json new file mode 100644 index 000000000..805c1051e --- /dev/null +++ b/plugins/huhabiaoqingbao/package.json @@ -0,0 +1,44 @@ +{ + "name": "huhabiaoqingbao", + "private": true, + "version": "1.0.0", + "type": "commonjs", + "scripts": { + "dev": "vite", + "build": "vite build && node scripts/post-build.js", + "preview": "vite preview", + "type-check": "vue-tsc --noEmit", + "generate-image-index": "node scripts/generateImageIndex.js" + }, + "keywords": [ + "ztools", + "emoticon", + "vue3" + ], + "author": "HUHA", + "license": "MIT", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@types/md5": "^2.3.5", + "axios": "^1.9.0", + "cheerio": "^1.1.2", + "element-plus": "^2.5.3", + "gifshot": "^0.4.5", + "html2canvas": "^1.4.1", + "idb": "^8.0.0", + "jszip": "^3.10.1", + "md5": "^2.3.0", + "nanoid": "^5.0.4", + "pinia": "^2.1.7", + "vue": "^3.3.11", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^20.17.50", + "@vitejs/plugin-vue": "^4.5.2", + "sass": "^1.70.0", + "typescript": "^5.2.2", + "vite": "^5.0.8", + "vue-tsc": "^2.2.12" + } +} diff --git a/plugins/huhabiaoqingbao/plugin.json b/plugins/huhabiaoqingbao/plugin.json new file mode 100644 index 000000000..ed50e5946 --- /dev/null +++ b/plugins/huhabiaoqingbao/plugin.json @@ -0,0 +1,43 @@ +{ + "name": "huhabiaoqingbao", + "title": "呼哈表情包管理器", + "description": "一个优雅的表情包管理工具:本地收藏、在线搜索、批量导入、AI 生成与视频转 GIF", + "author": "HUHA", + "homepage": "https://www.huhage.fun/", + "version": "1.0.0", + "logo": "logo.png", + "main": "index.html", + "preload": "preload.js", + "platform": ["darwin", "win32", "linux"], + "categories": ["media"], + "features": [ + { + "code": "bqb", + "explain": "表情包管理器", + "cmds": [ + "表情包", + "bqb", + "BiaoQingBao", + "表情", + "emoji" + ] + }, + { + "code": "bqb_search_with_text", + "explain": "用呼哈表情包搜索", + "cmds": [ + { + "type": "over", + "label": "用呼哈表情包搜索", + "minLength": 1 + } + ] + } + ], + "development": { + "main": "http://localhost:5173" + }, + "pluginSetting": { + "single": true + } +} diff --git a/plugins/huhabiaoqingbao/preload.js b/plugins/huhabiaoqingbao/preload.js new file mode 100644 index 000000000..18e4cbe48 --- /dev/null +++ b/plugins/huhabiaoqingbao/preload.js @@ -0,0 +1,117 @@ +const fs = require('fs') +const path = require('path') +const { promisify } = require('util') + +// 将需要的 fs 方法转换为 Promise 版本 +const readFileAsync = promisify(fs.readFile) +const writeFileAsync = promisify(fs.writeFile) +const unlinkAsync = promisify(fs.unlink) + +// 定义 preload 对象 +const preload = { + // 文件系统 API + fs: { + readFile: async (filePath, encoding) => { + try { + return await readFileAsync(filePath, encoding) + } catch (error) { + console.error('Failed to read file:', error) + throw error + } + }, + writeFile: async (filePath, data) => { + try { + await writeFileAsync(filePath, data) + } catch (error) { + console.error('Failed to write file:', error) + throw error + } + }, + unlink: async (filePath) => { + try { + await unlinkAsync(filePath) + } catch (error) { + console.error('Failed to delete file:', error) + throw error + } + }, + existsSync: (filePath) => { + try { + return fs.existsSync(filePath) + } catch (error) { + console.error('Failed to check file existence:', error) + return false + } + }, + mkdirSync: (dirPath, options) => { + try { + fs.mkdirSync(dirPath, options) + } catch (error) { + console.error('Failed to create directory:', error) + throw error + } + }, + readFileSync: (filePath, encoding) => { + try { + return fs.readFileSync(filePath, encoding) + } catch (error) { + console.error('Failed to read file sync:', error) + throw error + } + }, + readdirSync: (dirPath) => { + try { + return fs.readdirSync(dirPath) + } catch (error) { + console.error('Failed to read directory:', error) + throw error + } + }, + statSync: (filePath) => { + try { + return fs.statSync(filePath) + } catch (error) { + console.error('Failed to stat file:', error) + throw error + } + } + }, + + // 工具函数 + utils: { + getDataPath: (subPath) => { + try { + const basePath = window.ztools.getPath('appData') + return path.join(basePath, 'bqb', subPath) + } catch (error) { + console.error('Failed to get data path:', error) + throw error + } + }, + joinPath: (...paths) => { + try { + return path.join(...paths) + } catch (error) { + console.error('Failed to join paths:', error) + throw error + } + }, + getTempPath: (fileName) => { + try { + return path.join(window.ztools.getPath('temp'), fileName) + } catch (error) { + console.error('Failed to get temp path:', error) + throw error + } + } + } +} + +// 初始化导出 +if (window) { + window.preload = preload +} + +if (module && module.exports) { + module.exports = preload +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/public/huha-avatar.png b/plugins/huhabiaoqingbao/public/huha-avatar.png new file mode 100644 index 000000000..dd3df7d37 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/huha-avatar.png differ diff --git a/plugins/huhabiaoqingbao/public/images/HTML.png b/plugins/huhabiaoqingbao/public/images/HTML.png new file mode 100644 index 000000000..a443ad938 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/images/HTML.png differ diff --git a/plugins/huhabiaoqingbao/public/images/Markdown.png b/plugins/huhabiaoqingbao/public/images/Markdown.png new file mode 100644 index 000000000..0662b5cd9 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/images/Markdown.png differ diff --git "a/plugins/huhabiaoqingbao/public/images/\345\267\245\345\205\267\347\256\261.png" "b/plugins/huhabiaoqingbao/public/images/\345\267\245\345\205\267\347\256\261.png" new file mode 100644 index 000000000..ef7a96d5a Binary files /dev/null and "b/plugins/huhabiaoqingbao/public/images/\345\267\245\345\205\267\347\256\261.png" differ diff --git "a/plugins/huhabiaoqingbao/public/images/\347\225\231\350\250\200\345\242\231.png" "b/plugins/huhabiaoqingbao/public/images/\347\225\231\350\250\200\345\242\231.png" new file mode 100644 index 000000000..2d02f6af2 Binary files /dev/null and "b/plugins/huhabiaoqingbao/public/images/\347\225\231\350\250\200\345\242\231.png" differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/1679475053857084.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/1679475053857084.jpg new file mode 100644 index 000000000..f4f93ec92 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/1679475053857084.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/1682326211220133.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/1682326211220133.jpg new file mode 100644 index 000000000..2aec065a4 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/1682326211220133.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/1683713002320050.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/1683713002320050.jpg new file mode 100644 index 000000000..5c1609133 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/1683713002320050.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/1683713002326205.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/1683713002326205.jpg new file mode 100644 index 000000000..865b0a327 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/1683713002326205.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/1686290084505144.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/1686290084505144.jpg new file mode 100644 index 000000000..0c106d747 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/1686290084505144.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/R-C.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/R-C.jpg new file mode 100644 index 000000000..dd56ee0b3 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/R-C.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/index.json b/plugins/huhabiaoqingbao/public/preset-images/animal/index.json new file mode 100644 index 000000000..f044866b1 --- /dev/null +++ b/plugins/huhabiaoqingbao/public/preset-images/animal/index.json @@ -0,0 +1,15 @@ +[ + "1679475053857084.jpg", + "1682326211220133.jpg", + "1683713002320050.jpg", + "1683713002326205.jpg", + "1686290084505144.jpg", + "R-C.jpg", + "u=1397006255,818771621&fm=253&fmt=auto&app=138&f=JPEG.jpg", + "u=1702405753,3879391103&fm=253&fmt=auto&app=120&f=JPEG.jpg", + "u=336949857,152750208&fm=253&fmt=auto&app=120&f=JPEG.jpg", + "u=4042695624,3751593444&fm=253&fmt=auto&app=120&f=JPEG.jpg", + "u=481485649,3574166723&fm=253&fmt=auto&app=120&f=JPEG.jpg", + "u=73374389,2713903673&fm=253&fmt=auto&app=120&f=JPEG.jpg", + "w700d1q75cms.jpg" +] \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/u=1397006255,818771621&fm=253&fmt=auto&app=138&f=JPEG.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/u=1397006255,818771621&fm=253&fmt=auto&app=138&f=JPEG.jpg new file mode 100644 index 000000000..607cb02ff Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/u=1397006255,818771621&fm=253&fmt=auto&app=138&f=JPEG.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/u=1702405753,3879391103&fm=253&fmt=auto&app=120&f=JPEG.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/u=1702405753,3879391103&fm=253&fmt=auto&app=120&f=JPEG.jpg new file mode 100644 index 000000000..1c03b59ee Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/u=1702405753,3879391103&fm=253&fmt=auto&app=120&f=JPEG.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/u=336949857,152750208&fm=253&fmt=auto&app=120&f=JPEG.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/u=336949857,152750208&fm=253&fmt=auto&app=120&f=JPEG.jpg new file mode 100644 index 000000000..5f04af3d5 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/u=336949857,152750208&fm=253&fmt=auto&app=120&f=JPEG.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/u=4042695624,3751593444&fm=253&fmt=auto&app=120&f=JPEG.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/u=4042695624,3751593444&fm=253&fmt=auto&app=120&f=JPEG.jpg new file mode 100644 index 000000000..3354d7865 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/u=4042695624,3751593444&fm=253&fmt=auto&app=120&f=JPEG.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/u=481485649,3574166723&fm=253&fmt=auto&app=120&f=JPEG.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/u=481485649,3574166723&fm=253&fmt=auto&app=120&f=JPEG.jpg new file mode 100644 index 000000000..6d7c72a8b Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/u=481485649,3574166723&fm=253&fmt=auto&app=120&f=JPEG.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/u=73374389,2713903673&fm=253&fmt=auto&app=120&f=JPEG.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/u=73374389,2713903673&fm=253&fmt=auto&app=120&f=JPEG.jpg new file mode 100644 index 000000000..9587ced2e Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/u=73374389,2713903673&fm=253&fmt=auto&app=120&f=JPEG.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/animal/w700d1q75cms.jpg b/plugins/huhabiaoqingbao/public/preset-images/animal/w700d1q75cms.jpg new file mode 100644 index 000000000..bf45af8e0 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/animal/w700d1q75cms.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/1678086524140648.jpg b/plugins/huhabiaoqingbao/public/preset-images/face/1678086524140648.jpg new file mode 100644 index 000000000..f6cf32a24 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/1678086524140648.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/20201016013114_CJnSV.jpeg b/plugins/huhabiaoqingbao/public/preset-images/face/20201016013114_CJnSV.jpeg new file mode 100644 index 000000000..ce778ae99 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/20201016013114_CJnSV.jpeg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/20211104211837_5c5ee.jpg b/plugins/huhabiaoqingbao/public/preset-images/face/20211104211837_5c5ee.jpg new file mode 100644 index 000000000..78933ddbc Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/20211104211837_5c5ee.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/OIP-C (1).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/OIP-C (1).jpg new file mode 100644 index 000000000..95a0295bd Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/OIP-C (1).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/OIP-C (2).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/OIP-C (2).jpg new file mode 100644 index 000000000..9d408079b Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/OIP-C (2).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/OIP-C.jpg b/plugins/huhabiaoqingbao/public/preset-images/face/OIP-C.jpg new file mode 100644 index 000000000..2a898ffb1 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/OIP-C.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (1).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (1).jpg new file mode 100644 index 000000000..049d37660 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (1).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (1).png b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (1).png new file mode 100644 index 000000000..ecb0ab70a Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (1).png differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (10).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (10).jpg new file mode 100644 index 000000000..fd2087826 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (10).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (2).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (2).jpg new file mode 100644 index 000000000..886944d58 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (2).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (3).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (3).jpg new file mode 100644 index 000000000..56089eeea Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (3).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (4).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (4).jpg new file mode 100644 index 000000000..5f568b1a0 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (4).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (5).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (5).jpg new file mode 100644 index 000000000..d2f9a5d77 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (5).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (6).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (6).jpg new file mode 100644 index 000000000..bf8cc114c Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (6).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (7).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (7).jpg new file mode 100644 index 000000000..97b3eea02 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (7).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (8).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (8).jpg new file mode 100644 index 000000000..6fda9492b Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (8).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C (9).jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (9).jpg new file mode 100644 index 000000000..abbfffeaa Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C (9).jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C.jpg b/plugins/huhabiaoqingbao/public/preset-images/face/R-C.jpg new file mode 100644 index 000000000..44e939a57 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/R-C.png b/plugins/huhabiaoqingbao/public/preset-images/face/R-C.png new file mode 100644 index 000000000..d44025251 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/R-C.png differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/index.json b/plugins/huhabiaoqingbao/public/preset-images/face/index.json new file mode 100644 index 000000000..4d1699cc6 --- /dev/null +++ b/plugins/huhabiaoqingbao/public/preset-images/face/index.json @@ -0,0 +1,23 @@ +[ + "1678086524140648.jpg", + "20201016013114_CJnSV.jpeg", + "20211104211837_5c5ee.jpg", + "OIP-C (1).jpg", + "OIP-C (2).jpg", + "OIP-C.jpg", + "R-C (1).jpg", + "R-C (1).png", + "R-C (10).jpg", + "R-C (2).jpg", + "R-C (3).jpg", + "R-C (4).jpg", + "R-C (5).jpg", + "R-C (6).jpg", + "R-C (7).jpg", + "R-C (8).jpg", + "R-C (9).jpg", + "R-C.jpg", + "R-C.png", + "v2-731a39684c311f8641e38cead23462a6_720w.jpg", + "ss.jpg" +] \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/ss.jpg b/plugins/huhabiaoqingbao/public/preset-images/face/ss.jpg new file mode 100644 index 000000000..4ac30a5d9 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/ss.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/u=1414994675,754227710&fm=253&fmt=auto&app=138&f=JPEG.webp b/plugins/huhabiaoqingbao/public/preset-images/face/u=1414994675,754227710&fm=253&fmt=auto&app=138&f=JPEG.webp new file mode 100644 index 000000000..2268e5cc3 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/u=1414994675,754227710&fm=253&fmt=auto&app=138&f=JPEG.webp differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/face/v2-731a39684c311f8641e38cead23462a6_720w.jpg b/plugins/huhabiaoqingbao/public/preset-images/face/v2-731a39684c311f8641e38cead23462a6_720w.jpg new file mode 100644 index 000000000..173d6cf7f Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/face/v2-731a39684c311f8641e38cead23462a6_720w.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/051546u0rpu5ps50sbj995.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/051546u0rpu5ps50sbj995.jpg new file mode 100644 index 000000000..601a6e77c Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/051546u0rpu5ps50sbj995.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/1675321157118239.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/1675321157118239.jpg new file mode 100644 index 000000000..6384ccaa4 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/1675321157118239.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/1675669083172924.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/1675669083172924.jpg new file mode 100644 index 000000000..e16605677 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/1675669083172924.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/1676883275677089.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/1676883275677089.jpg new file mode 100644 index 000000000..366a5a70e Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/1676883275677089.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/1676883276514388.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/1676883276514388.jpg new file mode 100644 index 000000000..53c3a2970 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/1676883276514388.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/1679475053617293.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/1679475053617293.jpg new file mode 100644 index 000000000..3a64ef6d2 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/1679475053617293.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/1679475053652684.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/1679475053652684.jpg new file mode 100644 index 000000000..ded723e1a Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/1679475053652684.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/1682326210415423.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/1682326210415423.jpg new file mode 100644 index 000000000..c9aff8238 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/1682326210415423.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/1684222417311775.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/1684222417311775.jpg new file mode 100644 index 000000000..7617bcb78 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/1684222417311775.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/762d-a45873a263e138b1c6d5603abfc817d0.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/762d-a45873a263e138b1c6d5603abfc817d0.jpg new file mode 100644 index 000000000..0953b6ae3 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/762d-a45873a263e138b1c6d5603abfc817d0.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/OIP-C.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/OIP-C.jpg new file mode 100644 index 000000000..d222c203b Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/OIP-C.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/index.json b/plugins/huhabiaoqingbao/public/preset-images/funny/index.json new file mode 100644 index 000000000..a57f1f387 --- /dev/null +++ b/plugins/huhabiaoqingbao/public/preset-images/funny/index.json @@ -0,0 +1,15 @@ +[ + "051546u0rpu5ps50sbj995.jpg", + "1675321157118239.jpg", + "1675669083172924.jpg", + "1676883275677089.jpg", + "1676883276514388.jpg", + "1679475053617293.jpg", + "1679475053652684.jpg", + "1682326210415423.jpg", + "1684222417311775.jpg", + "762d-a45873a263e138b1c6d5603abfc817d0.jpg", + "OIP-C.jpg", + "v2-9777ce5d9b752aa75463a47bd70eda58_b.jpg", + "v2-b0b2c590f7457fd55059af987fe8a479_r.jpg" +] \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/u=1213551290,1261846931&fm=253&fmt=auto&app=138&f=JPEG.webp b/plugins/huhabiaoqingbao/public/preset-images/funny/u=1213551290,1261846931&fm=253&fmt=auto&app=138&f=JPEG.webp new file mode 100644 index 000000000..06bd568ff Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/u=1213551290,1261846931&fm=253&fmt=auto&app=138&f=JPEG.webp differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/u=2681260877,1227744363&fm=253&fmt=auto&app=120&f=JPEG.webp b/plugins/huhabiaoqingbao/public/preset-images/funny/u=2681260877,1227744363&fm=253&fmt=auto&app=120&f=JPEG.webp new file mode 100644 index 000000000..8ac295a0b Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/u=2681260877,1227744363&fm=253&fmt=auto&app=120&f=JPEG.webp differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/u=2967499437,2037458227&fm=253&fmt=auto&app=120&f=JPEG.webp b/plugins/huhabiaoqingbao/public/preset-images/funny/u=2967499437,2037458227&fm=253&fmt=auto&app=120&f=JPEG.webp new file mode 100644 index 000000000..198781363 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/u=2967499437,2037458227&fm=253&fmt=auto&app=120&f=JPEG.webp differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/v2-9777ce5d9b752aa75463a47bd70eda58_b.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/v2-9777ce5d9b752aa75463a47bd70eda58_b.jpg new file mode 100644 index 000000000..cb1c6baa0 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/v2-9777ce5d9b752aa75463a47bd70eda58_b.jpg differ diff --git a/plugins/huhabiaoqingbao/public/preset-images/funny/v2-b0b2c590f7457fd55059af987fe8a479_r.jpg b/plugins/huhabiaoqingbao/public/preset-images/funny/v2-b0b2c590f7457fd55059af987fe8a479_r.jpg new file mode 100644 index 000000000..2019d3620 Binary files /dev/null and b/plugins/huhabiaoqingbao/public/preset-images/funny/v2-b0b2c590f7457fd55059af987fe8a479_r.jpg differ diff --git a/plugins/huhabiaoqingbao/scripts/generateImageIndex.js b/plugins/huhabiaoqingbao/scripts/generateImageIndex.js new file mode 100644 index 000000000..d86248382 --- /dev/null +++ b/plugins/huhabiaoqingbao/scripts/generateImageIndex.js @@ -0,0 +1,42 @@ +const fs = require('fs') +const path = require('path') + +const PRESET_IMAGES_DIR = path.join(__dirname, '../public/preset-images') + +// 支持的图片格式 +const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'] + +// 为每个分类目录生成索引文件 +async function generateImageIndex() { + const categories = ['funny', 'animal', 'face'] + + for (const category of categories) { + const categoryPath = path.join(PRESET_IMAGES_DIR, category) + + // 确保目录存在 + if (!fs.existsSync(categoryPath)) { + fs.mkdirSync(categoryPath, { recursive: true }) + console.log(`Created directory for ${category}`) + continue + } + + // 读取目录中的图片文件 + const files = fs.readdirSync(categoryPath) + .filter(file => { + const ext = path.extname(file).toLowerCase() + return IMAGE_EXTENSIONS.includes(ext) + }) + + // 写入索引文件 + const indexPath = path.join(categoryPath, 'index.json') + fs.writeFileSync(indexPath, JSON.stringify(files, null, 2)) + + console.log(`Generated index for ${category}: ${files.length} images`) + } +} + +// 执行并处理错误 +generateImageIndex().catch(error => { + console.error('Error generating image index:', error) + process.exit(1) +}) \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/scripts/post-build.js b/plugins/huhabiaoqingbao/scripts/post-build.js new file mode 100644 index 000000000..884ab3ced --- /dev/null +++ b/plugins/huhabiaoqingbao/scripts/post-build.js @@ -0,0 +1,115 @@ +const { writeFileSync, existsSync, mkdirSync } = require('fs'); +const { join, dirname } = require('path'); + +// 确保 scripts 目录存在 +const scriptsDir = join(__dirname); +if (!existsSync(scriptsDir)) { + mkdirSync(scriptsDir, { recursive: true }); +} + +console.log('Post-build process completed successfully!'); + +const fs = require('fs'); +const path = require('path'); + +// 复制必要文件到发布目录 +function copyFiles() { + const files = ['plugin.json', 'preload.js', 'logo.png']; + const distDir = path.resolve(__dirname, '../dist'); + + // 确保目标目录存在 + if (!fs.existsSync(distDir)) { + fs.mkdirSync(distDir, { recursive: true }); + } + + files.forEach(file => { + const sourcePath = path.resolve(__dirname, '..', file); + const targetPath = path.join(distDir, file); + + try { + if (fs.existsSync(sourcePath)) { + fs.copyFileSync(sourcePath, targetPath); + console.log(`✓ Successfully copied ${file} to dist directory`); + } else { + console.error(`✗ Source file ${file} not found at ${sourcePath}`); + } + } catch (error) { + console.error(`Error copying ${file}:`, error); + } + }); +} + +// 确保 index.html 引用的资源路径正确 +function fixHtmlPaths() { + const htmlPath = path.resolve(__dirname, '../dist/index.html'); + + try { + if (fs.existsSync(htmlPath)) { + let html = fs.readFileSync(htmlPath, 'utf-8'); + + // 修正所有可能的资源路径问题 + html = html.replace(/\.\.\/assets\//g, './assets/'); // 修复 ../assets 为 ./assets + html = html.replace(/\/assets\//g, './assets/'); // 修复 /assets 为 ./assets + html = html.replace(/"\.\.\//g, '"./'); // 修复其他 ../ 开头的路径 + html = html.replace(/^(\s+)href="\//gm, '$1href="./'); // 修复以 / 开头的 href + html = html.replace(/^(\s+)src="\//gm, '$1src="./'); // 修复以 / 开头的 src + + fs.writeFileSync(htmlPath, html); + console.log('✓ Fixed asset paths in index.html'); + + // 输出修改后的内容以供验证 + console.log('\nFixed index.html content preview:'); + console.log(html.slice(0, 500) + '...'); + } else { + console.error('✗ index.html not found in dist directory'); + } + } catch (error) { + console.error('Error fixing HTML paths:', error); + } +} + +// 验证构建输出 +function verifyBuild() { + const distDir = path.resolve(__dirname, '../dist'); + const requiredFiles = ['index.html', 'plugin.json', 'preload.js', 'logo.png']; + const assetsDir = path.join(distDir, 'assets'); + + console.log('\nVerifying build output:'); + + // 检查必需文件 + requiredFiles.forEach(file => { + const filePath = path.join(distDir, file); + const exists = fs.existsSync(filePath); + console.log(`${exists ? '✓' : '✗'} ${file}`); + + if (exists) { + const stats = fs.statSync(filePath); + console.log(` Size: ${stats.size} bytes`); + } + }); + + // 检查资源目录 + if (fs.existsSync(assetsDir)) { + console.log('\nAssets directory content:'); + const assets = fs.readdirSync(assetsDir, { recursive: true }); + assets.forEach(asset => { + console.log(` - ${asset}`); + }); + } else { + console.error('✗ Assets directory not found'); + } +} + +// 主执行流程 +try { + console.log('Starting post-build process...\n'); + + copyFiles(); + fixHtmlPaths(); + verifyBuild(); + + console.log('\n✓ Post-build process completed successfully!'); +} catch (error) { + console.error('\n✗ Error during post-build process:', error); + process.exit(1); +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/App.vue b/plugins/huhabiaoqingbao/src/App.vue new file mode 100644 index 000000000..f52f3088d --- /dev/null +++ b/plugins/huhabiaoqingbao/src/App.vue @@ -0,0 +1,1002 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/src/api/aiGenerator.ts b/plugins/huhabiaoqingbao/src/api/aiGenerator.ts new file mode 100644 index 000000000..f63e9de61 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/api/aiGenerator.ts @@ -0,0 +1,201 @@ +import axios from 'axios' +import { getCozeCredentials, hasCozeCredentials } from '@/config/credentials' + +// 定义API响应类型 +interface CozeApiResponse { + response?: { + content: string + content_type: string + attachments?: Array<{ + type: string + url: string + }> + } + data?: { + id: string + conversation_id: string + bot_id: string + created_at: number + status: string + events?: Array<{ + event: string + data: { + content?: string + content_type?: string + delta?: { + content?: string + } + } + }> + } + code: number + msg: string +} + +// 流式响应事件类型 +interface StreamEvent { + event: string + data: string +} + +// 消息类型 +interface MessageData { + id: string + conversation_id: string + bot_id: string + role: string + type: string + content: string + content_type: string + chat_id: string + section_id: string + created_at: number + updated_at?: number +} + +// 生成表情包的函数 +export const generateEmoticonWithAI = async (prompt: string): Promise => { + const credentials = await getCozeCredentials() + if (!hasCozeCredentials(credentials)) { + throw new Error('未配置 AI 生成服务,请先在「系统设置 - AI 生成」中填写 Coze Token 与 Bot ID') + } + + try { + console.log('开始生成表情包,提示词:', prompt) + + // 使用流式响应 + const response = await axios.post( + 'https://api.coze.cn/v3/chat', + { + bot_id: credentials.botId, + user_id: '123123000', + stream: true, // 启用流式响应 + auto_save_history: true, + additional_messages: [ + { + role: 'user', + content: `${prompt}`, + content_type: 'text' + } + ] + }, + { + headers: { + 'Authorization': `Bearer ${credentials.token}`, + 'Content-Type': 'application/json', + 'Accept': 'text/event-stream' + }, + responseType: 'text', + timeout: 60000 // 设置60秒超时 + } + ) + + // 处理流式响应 + const content = response.data + console.log('流式响应原始内容:', content) + + // 解析流式响应 + const events = parseEventStream(content) + console.log(`解析出 ${events.length} 个事件`) + + // 收集所有可能包含图片URL的消息 + const imageUrls: string[] = [] + + for (const event of events) { + // 只处理消息完成事件 + if (event.event === 'conversation.message.completed' || + event.event === 'conversation.message.delta') { + try { + const messageData = JSON.parse(event.data) as MessageData + + // 检查消息类型,tool_response和answer类型通常包含图片URL + if (messageData.type === 'tool_response' || + messageData.type === 'answer') { + + console.log(`找到${messageData.type}类型消息:`, messageData.content) + + // 检查内容是否是URL + if (messageData.content && + (messageData.content.startsWith('http://') || + messageData.content.startsWith('https://'))) { + + // 添加到图片URL列表 + imageUrls.push(messageData.content) + } else { + // 尝试从内容中提取URL + const extractedUrls = extractImageUrls(messageData.content) + if (extractedUrls.length > 0) { + imageUrls.push(...extractedUrls) + } + } + } + } catch (e) { + console.error('解析消息数据失败:', e) + } + } + } + + // 去重 + const uniqueUrls = [...new Set(imageUrls)] + console.log('提取的图片URL:', uniqueUrls) + + return uniqueUrls + } catch (error) { + console.error('AI生成表情失败:', error) + throw error + } +} + +// 解析事件流 +const parseEventStream = (text: string): StreamEvent[] => { + const events: StreamEvent[] = [] + const lines = text.split('\n') + + let currentEvent: Partial = {} + + for (const line of lines) { + if (!line.trim()) { + // 空行表示事件结束 + if (currentEvent.event && currentEvent.data) { + events.push(currentEvent as StreamEvent) + } + currentEvent = {} + continue + } + + if (line.startsWith('event:')) { + currentEvent.event = line.substring(6).trim() + } else if (line.startsWith('data:')) { + currentEvent.data = line.substring(5).trim() + } + } + + // 添加最后一个事件 + if (currentEvent.event && currentEvent.data) { + events.push(currentEvent as StreamEvent) + } + + return events +} + +// 从文本中提取图片URL +const extractImageUrls = (text: string): string[] => { + // 匹配markdown图片格式 ![alt](url) + const markdownRegex = /!\[.*?\]\((https?:\/\/[^\s)]+\.(jpg|jpeg|png|gif))\)/gi + const markdownMatches = text.match(markdownRegex) || [] + const markdownUrls = markdownMatches.map(match => { + const urlMatch = /\((https?:\/\/[^\s)]+)\)/.exec(match) + return urlMatch ? urlMatch[1] : '' + }).filter(url => url) + + // 匹配普通URL + const urlRegex = /(https?:\/\/[^\s()<>]+\.(jpg|jpeg|png|gif))/gi + const urlMatches = text.match(urlRegex) || [] + + // 匹配Coze短链接 + const cozeRegex = /(https?:\/\/s\.coze\.cn\/t\/[a-zA-Z0-9]+)/gi + const cozeMatches = text.match(cozeRegex) || [] + + // 合并所有匹配结果并去重 + return [...new Set([...markdownUrls, ...urlMatches, ...cozeMatches])] +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/api/search.ts b/plugins/huhabiaoqingbao/src/api/search.ts new file mode 100644 index 000000000..3a4f1cb4c --- /dev/null +++ b/plugins/huhabiaoqingbao/src/api/search.ts @@ -0,0 +1,513 @@ +import { ONLINE_SEARCH_PAGE_SIZE, type SearchResult, type SearchSource } from '@/types/search' +import axios from 'axios' +import * as cheerio from 'cheerio' + +// 处理跨域问题的代理前缀 +const PROXY_PREFIX = '' // 移除代理前缀,直接使用完整URL + +// 搜索API配置 +const API_CONFIG = { + baidu: { + url: `${PROXY_PREFIX}/baidu/search/acjson`, + params: (query: string) => ({ + tn: 'resultjson_com', + word: query, + queryWord: query, + ie: 'utf-8', + oe: 'utf-8', + pn: '0', + rn: '30' + }) + }, + sougou: { + url: `${PROXY_PREFIX}/sougou/pics/json.jsp`, + params: (query: string) => ({ + query, + st: '5', + start: '0', + len: '30' + }) + } +} + +// 构建URL with params +const buildUrl = (baseUrl: string, params: Record) => { + const url = new URL(baseUrl, window.location.origin) + Object.entries(params).forEach(([key, value]) => { + url.searchParams.append(key, value) + }) + return url.toString() +} + +// 生成唯一ID +const generateId = (url: string, source: SearchSource): string => { + return `${source}_${url.split('/').pop()?.split('.')[0] || Date.now()}` +} + +// 验证图片URL是否可访问 +const isImageAccessible = async (url: string, timeout: number = 3000): Promise => { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), timeout) + + try { + const response = await fetch(url, { + method: 'HEAD', + signal: controller.signal, + headers: { + 'Accept': 'image/*' + } + }) + const contentType = response.headers.get('content-type') + return response.ok && (contentType?.startsWith('image/') ?? false) + } catch { + return true // 超时或错误时默认可访问,让后续加载时处理 + } finally { + clearTimeout(timer) + } +} + +// 并发控制工具 +const withConcurrency = async ( + items: T[], + mapper: (item: T) => Promise, + concurrency: number = 8 +): Promise => { + const results: R[] = [] + let index = 0 + + const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => { + while (index < items.length) { + const currentIndex = index++ + results[currentIndex] = await mapper(items[currentIndex]) + } + }) + + await Promise.all(workers) + return results +} + +// 验证并过滤图片结果 +const filterValidImages = async ( + results: SearchResult[], + options: { skipValidation?: boolean; concurrency?: number } = {} +): Promise => { + // 如果设置了跳过验证,直接返回结果 + if (options.skipValidation) { + return results + } + + const concurrency = options.concurrency ?? 6 + + const validations = await withConcurrency( + results, + async (result) => { + const isValid = await isImageAccessible(result.url) + return { result, isValid } + }, + concurrency + ) + + return validations + .filter(({ isValid }) => isValid) + .map(({ result }) => result) +} + +// 搜索百度图片 +export const searchBaidu = async ( + query: string, + page: number = 1, + options: { gifOnly?: boolean } = {} +): Promise => { + try { + const pn = (page - 1) * ONLINE_SEARCH_PAGE_SIZE + // 如果没有查询词,使用默认的"表情包"关键词 + const searchQuery = query || '表情包' + const url = `https://image.baidu.com/search/acjson?tn=resultjson_com&logid=${Date.now()}&ipn=rj&ct=201326592&is=&fp=result&fr=&word=${encodeURIComponent(searchQuery)}&queryWord=${encodeURIComponent(searchQuery)}&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=-1&z=&ic=0&hd=&latest=©right=&s=&se=&tab=&width=&height=&face=0&istype=2&qc=&nc=1&expermode=&nojc=&isAsync=&pn=${pn}&rn=${ONLINE_SEARCH_PAGE_SIZE}&gsm=1e` + + const response = await fetch(url, { + headers: { + 'Accept': '*/*', + 'Accept-Language': 'zh-CN,zh;q=0.9' + } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + + const IRRELEVANT_TITLE_KEYWORDS = /商品|广告|新闻|壁纸|海报|banner|素材网|千图|摄图|包图/i + + // 过滤并转换数据 + const results = (data.data || []) + .filter((item: any) => { + if (!item.thumbURL) return false + + // 维度过滤:排除极端宽高比和过大尺寸的图片 + const w = Number(item.width) + const h = Number(item.height) + if (w && h) { + const ratio = w / h + if (ratio > 3 || ratio < 1 / 3) return false + if (w > 2000 || h > 2000) return false + } + + // 标题相关性过滤 + const title = item.fromPageTitleEnc || '' + if (title && IRRELEVANT_TITLE_KEYWORDS.test(title)) return false + + if (!options.gifOnly) return item.type !== 'gif' + + return item.type === 'gif' + || item.thumbURL?.toLowerCase().includes('.gif') + || item.middleURL?.toLowerCase().includes('.gif') + }) + .map((item: any) => ({ + id: generateId(item.thumbURL, 'baidu'), + url: item.thumbURL, + previewUrl: item.middleURL || item.thumbURL, + thumbnailUrl: item.thumbURL, + title: item.fromPageTitleEnc || '未命名表情', + gifCandidate: options.gifOnly + ? item.type === 'gif' + || item.thumbURL?.toLowerCase().includes('.gif') + || item.middleURL?.toLowerCase().includes('.gif') + : false, + source: 'baidu', + originalUrl: item.middleURL || item.thumbURL + })) + + // 验证并过滤图片 + return await filterValidImages(results) + } catch (err) { + console.error('Baidu search failed:', err) + return [] + } +} + +// 搜索搜狗图片 +export const searchSougou = async (query: string, page: number = 1): Promise => { + try { + // 如果没有查询词,使用默认的"表情包"关键词 + const searchQuery = query || '表情包' + // 使用新的搜狗表情包 API + const url = `https://cn.apihz.cn/api/img/apihzbqbsougou.php?id=10004937&key=huhabiaoqingbao&page=${page}&words=${encodeURIComponent(searchQuery)}` + + const response = await fetch(url, { + headers: { + 'Accept': 'application/json' + } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + + // 检查API返回的状态 + if (data.code !== 200) { + console.error('API error:', data.msg || '未知错误') + return [] + } + + // 确保返回的数据有效 + if (!data.res || !Array.isArray(data.res)) { + console.error('Invalid data format:', data) + return [] + } + + // 转换数据格式 + const results = data.res + .filter((url: string) => { + // 确保URL是有效的字符串 + return typeof url === 'string' && url.startsWith('http') + }) + .map((url: string) => ({ + id: generateId(url, 'sougou'), + url: url, + title: searchQuery || '未命名表情', + source: 'sougou' as const, + originalUrl: url + })) + + // 添加调试日志 + console.log('Sougou search results:', { + query: searchQuery, + page, + resultsCount: results.length, + firstResult: results[0] + }) + + return results + + } catch (error) { + console.error('Sougou search failed:', error) + return [] + } +} + +// 搜索 Bing 图片 +export const searchBing = async ( + query: string, + page: number = 1, + options: { gifOnly?: boolean } = {} +): Promise => { + try { + const offset = (page - 1) * ONLINE_SEARCH_PAGE_SIZE + // 如果没有查询词,使用默认的"表情包"关键词 + const searchQuery = query || '表情包' + const url = `https://cn.bing.com/images/async?q=${encodeURIComponent(searchQuery)}&first=${offset}&count=${ONLINE_SEARCH_PAGE_SIZE}&mmasync=1` + + const response = await fetch(url, { + headers: { + 'Accept': 'text/html,application/xhtml+xml', + 'Accept-Language': 'zh-CN,zh;q=0.9' + } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const html = await response.text() + + // 使用正则表达式提取图片信息 + const results: SearchResult[] = [] + const regex = /murl":"(.*?)".*?t":"(.*?)"/g + let match + + while ((match = regex.exec(html)) !== null) { + const [, url, title] = match + if (url && (options.gifOnly || !url.includes('.gif'))) { + results.push({ + id: generateId(url, 'bing'), + url: url, + title: title || '未命名表情', + gifCandidate: options.gifOnly && url.toLowerCase().includes('.gif'), + source: 'bing', + originalUrl: url + }) + } + } + + // 验证并过滤图片 + return await filterValidImages(results) + } catch (err) { + console.error('Bing search failed:', err) + return [] + } +} + +// 搜索发表情网 +export const searchFaBiaoQing = async (query: string, page: number = 1): Promise => { + try { + // 使用正确的发表情网搜索接口 + const url = `/fabiaoqing/search/bqb/keyword/${encodeURIComponent(query)}/type/bq/page/${page}.html` + + const response = await fetch(url, { + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8' + } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const html = await response.text() + + // 更新正则表达式以匹配新的HTML结构 + const imgRegex = /]*src="([^"]+)"[^>]*title="([^"]+)"[^>]*>/g + const results: SearchResult[] = [] + let match + + while ((match = imgRegex.exec(html)) !== null) { + const [, url, title] = match + if (url && url.startsWith('http')) { + results.push({ + id: generateId(url, 'fabiaoqing'), + title: title || query || '未命名表情', + url: url, + originalUrl: url, + source: 'fabiaoqing' + }) + } + } + + // 添加调试日志 + console.log('FaBiaoQing search results:', { + query, + page, + resultsCount: results.length, + firstResult: results[0] + }) + + return results + } catch (error) { + console.error('FaBiaoQing search failed:', error) + return [] + } +} + +// 搜索斗图啦 +export const searchDouTu = async (keyword: string, page: number): Promise => { + try { + // 如果没有关键词,使用默认的"表情包"关键词 + const searchKeyword = keyword || '表情包' + + const response = await axios.get('https://www.52doutu.cn/api/', { + params: { + types: 'search', + action: 'searchpic', + wd: searchKeyword, + limit: ONLINE_SEARCH_PAGE_SIZE, + offset: (page - 1) * ONLINE_SEARCH_PAGE_SIZE + }, + headers: { + 'Accept': 'application/json', + 'Referer': 'https://www.52doutu.cn/' + } + }) + + const results = response.data?.data?.list || [] + + return results.map((item: any): SearchResult => ({ + id: item.id || Date.now().toString() + Math.random(), + url: item.url || '', + title: item.title || searchKeyword || '表情包', + originalUrl: item.original_url || item.url || '', + source: 'doutu' + })) + } catch (error) { + console.error('Doutu search failed:', error) + return [] + } +} + +// 更新 searchApiHz 函数 +export const searchApiHz = async (query: string, page: number): Promise => { + try { + // 确保有搜索关键词 + if (!query.trim()) { + query = '表情包' // 默认搜索关键词 + } + + // 添加 limit 参数来增加返回数量 + const limit = ONLINE_SEARCH_PAGE_SIZE + const url = `https://cn.apihz.cn/api/img/apihzbqb.php?id=10004937&key=huhabiaoqingbao&type=2&page=${page}&words=${encodeURIComponent(query)}&limit=${limit}` + + const response = await fetch(url, { + headers: { + 'Accept': 'application/json' + } + }) + + if (!response.ok) { + throw new Error('Network response was not ok') + } + + const data = await response.json() + + // 检查API返回的状态 + if (data.code !== 200) { + console.error('API error:', data.msg || '未知错误') + return [] + } + + // 确保返回的数据有效 + if (!data.res || !Array.isArray(data.res)) { + console.error('Invalid data format:', data) + return [] + } + + // 转换数据格式并添加错误处理 + const results = data.res + .filter((url: string) => { + try { + // 确保URL是有效的字符串并且是正确的URL格式 + return typeof url === 'string' && + url.startsWith('http') && + new URL(url).href === url + } catch { + return false // 如果URL格式无效,过滤掉 + } + }) + .map((url: string) => ({ + id: generateId(url, 'apihz'), + url: url.replace(/\\/g, ''), // 移除可能的转义字符 + title: query || '未命名表情', + gifCandidate: /\.gif(?:$|[?#])/i.test(url), + source: 'apihz' as const, + originalUrl: url.replace(/\\/g, '') // 同样处理 originalUrl + })) + + // 添加更详细的调试日志 + console.log('ApiHz search results:', { + query, + page, + limit, + maxPage: data.maxpage, + count: data.count, + requestedUrl: url, + resultsCount: results.length, + rawResultsCount: data.res.length, + firstResult: results[0], + rawResponse: data + }) + + return results + + } catch (error) { + console.error('ApiHz search failed:', error) + return [] + } +} + +/** + * 搜索斗了个图 + */ +export const searchDogetu = async (query: string, page: number): Promise => { + try { + // 构建URL和参数 + let url = 'https://www.dogetu.com/search.html' + let params: { page: number; keyword?: string } = { + page, + keyword: query + } + + // 如果没有关键词,加载最新表情包 + if (!query) { + url = 'https://www.dogetu.com/biaoqing.html' + params = { page } + } + + const response = await axios.get(url, { params }) + const $ = cheerio.load(response.data) + + const results: SearchResult[] = [] + $('.item-pic>a>img').each((_, img) => { + const element = $(img) + const url = element.attr('src') || '' + const title = element.attr('alt') || '表情包' + + if (url) { + results.push({ + id: generateId(url, 'dogetu'), + title, + url, + gifCandidate: /\.gif(?:$|[?#])/i.test(url), + originalUrl: url, + source: 'dogetu' as const + }) + } + }) + + return results + } catch (error) { + console.error('搜索斗了个图失败:', error) + return [] + } +} diff --git a/plugins/huhabiaoqingbao/src/api/translate.ts b/plugins/huhabiaoqingbao/src/api/translate.ts new file mode 100644 index 000000000..fdf18e47e --- /dev/null +++ b/plugins/huhabiaoqingbao/src/api/translate.ts @@ -0,0 +1,50 @@ +import axios from 'axios'; +import md5 from 'md5'; +import { getBaiduTranslateCredentials, hasBaiduTranslateCredentials } from '@/config/credentials'; + +const BAIDU_API = 'https://fanyi-api.baidu.com/api/trans/vip/translate'; + +interface BaiduTranslateResponse { + from: string; + to: string; + trans_result: { + src: string; + dst: string; + }[]; +} + +export const translateToEnglish = async (text: string): Promise => { + if (!text || !/[一-龥]/.test(text)) { + return text; + } + + const credentials = await getBaiduTranslateCredentials(); + if (!hasBaiduTranslateCredentials(credentials)) { + return text; + } + + const salt = Date.now().toString(); + const sign = md5(credentials.appid + text + salt + credentials.key); + + try { + const response = await axios.get(BAIDU_API, { + params: { + q: text, + from: 'zh', + to: 'en', + appid: credentials.appid, + salt, + sign + } + }); + + if (response.data.trans_result?.[0]?.dst) { + return response.data.trans_result[0].dst.toLowerCase(); + } + + return text; + } catch (error) { + console.error('Translation error:', error); + return text; + } +}; diff --git a/plugins/huhabiaoqingbao/src/api/video.ts b/plugins/huhabiaoqingbao/src/api/video.ts new file mode 100644 index 000000000..2e32d671e --- /dev/null +++ b/plugins/huhabiaoqingbao/src/api/video.ts @@ -0,0 +1,121 @@ +import axios from 'axios'; +import type { VideoApiResponse, VideoCategoryId, VideoCategory } from '../types'; + +// 老接口的响应类型 +interface VideoResponse { + code: number; + msg: string; + data: { + video: string; + }; +} + +// 视频分类列表 +export const VIDEO_CATEGORIES: VideoCategory[] = [ + { id: 'jk', name: 'JK系列', description: 'JK风格视频', icon: '🌸' }, + { id: 'YuMeng', name: '欲梦系列', description: '欲梦风格视频', icon: '🌙' }, + { id: 'NvDa', name: '女大系列', description: '大学生风格', icon: '🎓' }, + { id: 'NvGao', name: '女高系列', description: '高中生风格', icon: '🏫' }, + { id: 'ReWu', name: '热舞系列', description: '舞蹈视频', icon: '💃' }, + { id: 'QingCun', name: '清纯系列', description: '清纯风格', icon: '🌼' }, + { id: 'YuZu', name: '玉足系列', description: '玉足视频', icon: '👠' }, + { id: 'SheJie', name: '蛇姐系列', description: '蛇姐风格', icon: '🐍' }, + { id: 'ChuanDa', name: '穿搭系列', description: '时尚穿搭', icon: '👗' }, + { id: 'GaoZhiLiangXiaoJieJie', name: '高质量小姐姐', description: '高质量美女视频', icon: '✨' }, + { id: 'HanFu', name: '汉服系列', description: '传统汉服', icon: '🏎️' }, + { id: 'HeiSi', name: '黑丝系列', description: '黑丝视频', icon: '🧿' }, + { id: 'BianZhuang', name: '变装系列', description: '变装视频', icon: '🎭' }, + { id: 'LuoLi', name: '萝莉系列', description: '萝莉风格', icon: '🎀' }, + { id: 'TianMei', name: '甜妹系列', description: '甜美风格', icon: '🍭' }, + { id: 'BaiSi', name: '白丝系列', description: '白丝视频', icon: '☁️' } +]; + +// 获取分类视频 +export const getCategoryVideo = async (categoryId: VideoCategoryId): Promise => { + try { + const response = await axios.get(`/api/video/ksvideo`, { + params: { + type: 'json', // 使用json格式获取视频链接 + id: categoryId + }, + timeout: 10000, + headers: { + 'Accept': 'application/json, text/plain, */*', + } + }); + + console.log('API响应:', response.data); + + // 检查API响应格式 + if (response.data && typeof response.data === 'object') { + // 如果返回的是JSON对象 + if (response.data.status === 'success' && response.data.link) { + return { + code: 200, + msg: 'success', + data: { + video: response.data.link + } + }; + } else if (response.data.video) { + // 如果直接有video字段 + return { + code: 200, + msg: 'success', + data: { + video: response.data.video + } + }; + } + } + + // 如果是其他格式,尝试作为错误处理 + throw new Error('API返回格式不正确'); + + } catch (error: any) { + console.error('获取视频失败:', error); + + // 如果是网络错误或CORS错误,尝试备用方案 + if (error.code === 'ECONNABORTED' || error.message?.includes('CORS') || error.message?.includes('Network Error')) { + console.log('尝试备用API...'); + return await getBackupVideo(); + } + + throw { + code: 500, + msg: error.message || '网络错误,请稍后重试', + data: null + }; + } +}; + +// 备用视频API +const getBackupVideo = async (): Promise => { + try { + // 使用原来的API作为备用 + const response = await getGirlVideo(); + return { + code: response.code, + msg: response.msg, + data: response.data + }; + } catch (error) { + throw { + code: 500, + msg: '所有视频源都无法访问,请检查网络连接', + data: null + }; + } +}; + +// 获取随机视频(默认为jk分类) +export const getRandomVideo = async (): Promise => { + const randomCategory = VIDEO_CATEGORIES[Math.floor(Math.random() * VIDEO_CATEGORIES.length)]; + return getCategoryVideo(randomCategory.id as VideoCategoryId); +}; + +// 兼容旧的API接口 +export const getGirlVideo = async (): Promise => { + const response = await axios.get('https://api.52vmy.cn/api/video/girl'); + return response.data; +}; \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/api/wallpaper.ts b/plugins/huhabiaoqingbao/src/api/wallpaper.ts new file mode 100644 index 000000000..77b0d8336 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/api/wallpaper.ts @@ -0,0 +1,89 @@ +import axios from 'axios'; +import type { WallpaperResponse, WallpaperSearchParams } from '../types/wallpaper'; +import { translateToEnglish } from './translate'; + +export const API_KEY = 'o2HeXcnx8mYNojNXik3QAjK3wkfJWjFH'; +const BASE_URL = 'https://wallhaven.cc/api/v1'; + +const wallpaperApi = axios.create({ + baseURL: BASE_URL +}); + +export const searchWallpapers = async (params: WallpaperSearchParams): Promise => { + // 处理搜索关键词 + let searchQuery = params.q || ''; + + // 如果包含中文,先进行翻译 + if (/[\u4e00-\u9fa5]/.test(searchQuery)) { + try { + const translatedQuery = await translateToEnglish(searchQuery); + + // 添加一些相关的关键词增强搜索效果 + const keywordEnhancements: { [key: string]: string[] } = { + 'hacker': ['cyberpunk', 'cyber', 'digital'], + 'anime': ['animation', 'cartoon', 'manga'], + 'landscape': ['nature', 'scenery', 'outdoor'], + 'city': ['cityscape', 'urban', 'building'], + 'technology': ['cyber', 'digital', 'tech'], + 'space': ['galaxy', 'cosmos', 'universe'], + 'mechanical': ['machine', 'cyberpunk', 'robot'], + 'art': ['artistic', 'digital art'], + 'abstract': ['minimal', 'modern'], + 'minimalist': ['minimal', 'simple', 'clean'], + 'cyberpunk': ['cyber', 'neon', 'future'], + 'future': ['futuristic', 'sci-fi'], + 'retro': ['vintage', 'classic', 'old'], + 'game': ['gaming', 'video game'], + 'architecture': ['building', 'structure'], + 'car': ['automotive', 'vehicle'], + 'food': ['cuisine', 'cooking'], + 'animal': ['wildlife', 'nature'], + 'people': ['portrait', 'human'], + 'sports': ['athletic', 'exercise'] + }; + + // 查找翻译后的关键词是否有对应的增强词 + const words = translatedQuery.toLowerCase().split(/\s+/); + const enhancedWords = new Set(); + + words.forEach(word => { + enhancedWords.add(word); + if (keywordEnhancements[word]) { + keywordEnhancements[word].forEach(enhancement => + enhancedWords.add(enhancement) + ); + } + }); + + searchQuery = Array.from(enhancedWords).join(' '); + } catch (error) { + console.error('Translation error:', error); + } + } + + const searchParams = { + ...params, + q: searchQuery, + apikey: API_KEY, + // 默认搜索参数 + categories: params.categories || '111', // 所有分类 + purity: params.purity || '100', // 普通级别 + sorting: params.sorting || 'relevance', // 按相关度排序 + order: params.order || 'desc', // 降序 + page: params.page || 1 + }; + + const { data } = await wallpaperApi.get('/search', { params: searchParams }); + return data; +}; + +export const getWallpaperById = async (id: string) => { + const { data } = await wallpaperApi.get(`/w/${id}`, { + params: { apikey: API_KEY } + }); + return data; +}; + +export const getRandomWallpapers = async (params: Omit) => { + return searchWallpapers({ ...params, sorting: 'random' }); +}; \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/assets/default-avatar.svg b/plugins/huhabiaoqingbao/src/assets/default-avatar.svg new file mode 100644 index 000000000..fb187ebb5 Binary files /dev/null and b/plugins/huhabiaoqingbao/src/assets/default-avatar.svg differ diff --git a/plugins/huhabiaoqingbao/src/assets/tool_logo.png b/plugins/huhabiaoqingbao/src/assets/tool_logo.png new file mode 100644 index 000000000..4021667c4 Binary files /dev/null and b/plugins/huhabiaoqingbao/src/assets/tool_logo.png differ diff --git a/plugins/huhabiaoqingbao/src/assets/tool_logo2.png b/plugins/huhabiaoqingbao/src/assets/tool_logo2.png new file mode 100644 index 000000000..aa91bcbec Binary files /dev/null and b/plugins/huhabiaoqingbao/src/assets/tool_logo2.png differ diff --git a/plugins/huhabiaoqingbao/src/assets/tool_logo3.png b/plugins/huhabiaoqingbao/src/assets/tool_logo3.png new file mode 100644 index 000000000..544750248 Binary files /dev/null and b/plugins/huhabiaoqingbao/src/assets/tool_logo3.png differ diff --git a/plugins/huhabiaoqingbao/src/components/AboutAuthor.vue b/plugins/huhabiaoqingbao/src/components/AboutAuthor.vue new file mode 100644 index 000000000..4263ac0aa --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/AboutAuthor.vue @@ -0,0 +1,520 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/DeleteConfirmModal.vue b/plugins/huhabiaoqingbao/src/components/DeleteConfirmModal.vue new file mode 100644 index 000000000..5a5d1ae86 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/DeleteConfirmModal.vue @@ -0,0 +1,454 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/TagManager.vue b/plugins/huhabiaoqingbao/src/components/TagManager.vue new file mode 100644 index 000000000..81d7233b8 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/TagManager.vue @@ -0,0 +1,224 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/VideoPlayer.vue b/plugins/huhabiaoqingbao/src/components/VideoPlayer.vue new file mode 100644 index 000000000..66b091862 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/VideoPlayer.vue @@ -0,0 +1,935 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/Wallpaper.vue b/plugins/huhabiaoqingbao/src/components/Wallpaper.vue new file mode 100644 index 000000000..c3f52a05b --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/Wallpaper.vue @@ -0,0 +1,290 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/WallpaperCard.vue b/plugins/huhabiaoqingbao/src/components/WallpaperCard.vue new file mode 100644 index 000000000..ad622d103 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/WallpaperCard.vue @@ -0,0 +1,104 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/ai/AiEmoticonGenerator.vue b/plugins/huhabiaoqingbao/src/components/ai/AiEmoticonGenerator.vue new file mode 100644 index 000000000..20596004d --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/ai/AiEmoticonGenerator.vue @@ -0,0 +1,534 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/beauty/BeautyImages.vue b/plugins/huhabiaoqingbao/src/components/beauty/BeautyImages.vue new file mode 100644 index 000000000..8f0e7053a --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/beauty/BeautyImages.vue @@ -0,0 +1,910 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/src/components/emoji/EmojiPicker.vue b/plugins/huhabiaoqingbao/src/components/emoji/EmojiPicker.vue new file mode 100644 index 000000000..3ccdb3091 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/emoji/EmojiPicker.vue @@ -0,0 +1,3034 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/src/components/emoji/KaomojiPicker.vue b/plugins/huhabiaoqingbao/src/components/emoji/KaomojiPicker.vue new file mode 100644 index 000000000..d6d0339c4 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/emoji/KaomojiPicker.vue @@ -0,0 +1,1031 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/emoticon/EmoticonGrid.vue b/plugins/huhabiaoqingbao/src/components/emoticon/EmoticonGrid.vue new file mode 100644 index 000000000..8cf696298 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/emoticon/EmoticonGrid.vue @@ -0,0 +1,2006 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/src/components/emoticon/EmoticonItem.vue b/plugins/huhabiaoqingbao/src/components/emoticon/EmoticonItem.vue new file mode 100644 index 000000000..15bd54024 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/emoticon/EmoticonItem.vue @@ -0,0 +1,1021 @@ + + + + + + + + diff --git a/plugins/huhabiaoqingbao/src/components/emoticon/EmoticonLoader.vue b/plugins/huhabiaoqingbao/src/components/emoticon/EmoticonLoader.vue new file mode 100644 index 000000000..eaf00b64b --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/emoticon/EmoticonLoader.vue @@ -0,0 +1,167 @@ + + + + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/emoticon/EmptyState.vue b/plugins/huhabiaoqingbao/src/components/emoticon/EmptyState.vue new file mode 100644 index 000000000..382a5014b --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/emoticon/EmptyState.vue @@ -0,0 +1,66 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/emoticon/Pagination.vue b/plugins/huhabiaoqingbao/src/components/emoticon/Pagination.vue new file mode 100644 index 000000000..a3367d389 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/emoticon/Pagination.vue @@ -0,0 +1,167 @@ + + + + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/handsome/HandsomeImages.vue b/plugins/huhabiaoqingbao/src/components/handsome/HandsomeImages.vue new file mode 100644 index 000000000..74f885222 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/handsome/HandsomeImages.vue @@ -0,0 +1,904 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/src/components/import/ImportDialog.vue b/plugins/huhabiaoqingbao/src/components/import/ImportDialog.vue new file mode 100644 index 000000000..0da5ce4a6 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/import/ImportDialog.vue @@ -0,0 +1,757 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/src/components/layout/AppHeader.vue b/plugins/huhabiaoqingbao/src/components/layout/AppHeader.vue new file mode 100644 index 000000000..c36b73d04 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/layout/AppHeader.vue @@ -0,0 +1,1594 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/src/components/layout/AppSidebar.vue b/plugins/huhabiaoqingbao/src/components/layout/AppSidebar.vue new file mode 100644 index 000000000..d1c768d73 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/layout/AppSidebar.vue @@ -0,0 +1,748 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/src/components/online/OnlineSearch.vue b/plugins/huhabiaoqingbao/src/components/online/OnlineSearch.vue new file mode 100644 index 000000000..01a5fb7b9 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/online/OnlineSearch.vue @@ -0,0 +1,1991 @@ + + + + + diff --git a/plugins/huhabiaoqingbao/src/components/video/VideoToGif.vue b/plugins/huhabiaoqingbao/src/components/video/VideoToGif.vue new file mode 100644 index 000000000..81ff1801a --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/video/VideoToGif.vue @@ -0,0 +1,1058 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/components/workshop/EmoticonWorkshop.vue b/plugins/huhabiaoqingbao/src/components/workshop/EmoticonWorkshop.vue new file mode 100644 index 000000000..a54adbe9b --- /dev/null +++ b/plugins/huhabiaoqingbao/src/components/workshop/EmoticonWorkshop.vue @@ -0,0 +1,1388 @@ + + + + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/composables/useStaticImages.ts b/plugins/huhabiaoqingbao/src/composables/useStaticImages.ts new file mode 100644 index 000000000..a9e1597fe --- /dev/null +++ b/plugins/huhabiaoqingbao/src/composables/useStaticImages.ts @@ -0,0 +1,27 @@ +import { computed } from 'vue' +import { getStaticImageUrl, getAllStaticImageUrls, preloadStaticImages, type StaticImageKey } from '@/services/staticImages' + +/** + * 静态图片管理的组合式函数 + */ +export const useStaticImages = () => { + // 获取单个静态图片URL + const getImageUrl = (imageName: StaticImageKey) => { + return computed(() => getStaticImageUrl(imageName)) + } + + // 获取所有静态图片URL + const allImageUrls = computed(() => getAllStaticImageUrls()) + + // 预加载图片 + const preloadImages = async (imageNames?: StaticImageKey[]) => { + await preloadStaticImages(imageNames) + } + + return { + getImageUrl, + allImageUrls, + preloadImages, + getStaticImageUrl + } +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/config/assets.ts b/plugins/huhabiaoqingbao/src/config/assets.ts new file mode 100644 index 000000000..7686d007a --- /dev/null +++ b/plugins/huhabiaoqingbao/src/config/assets.ts @@ -0,0 +1,12 @@ +const getBaseUrl = () => { + const baseUrl = import.meta.env.BASE_URL || '/' + return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/` +} + +/** + * 解析插件内置静态资源的访问地址(资源随插件一起打包在 public 目录下) + */ +export const resolveAssetUrl = (assetPath: string) => { + const normalized = assetPath.replace(/^\/+/, '') + return `${getBaseUrl()}${normalized.split('/').map(encodeURIComponent).join('/')}` +} diff --git a/plugins/huhabiaoqingbao/src/config/credentials.ts b/plugins/huhabiaoqingbao/src/config/credentials.ts new file mode 100644 index 000000000..359531c42 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/config/credentials.ts @@ -0,0 +1,45 @@ +import { getStorageItem, setStorageItem, STORAGE_KEYS } from '@/utils/storage' + +export interface BaiduTranslateCredentials { + appid: string + key: string +} + +export interface CozeCredentials { + token: string + botId: string +} + +const trim = (value: unknown) => (typeof value === 'string' ? value.trim() : '') + +const asBaidu = (value: unknown): BaiduTranslateCredentials => { + const raw = (value ?? {}) as Partial> + return { appid: trim(raw.appid), key: trim(raw.key) } +} + +const asCoze = (value: unknown): CozeCredentials => { + const raw = (value ?? {}) as Partial> + return { token: trim(raw.token), botId: trim(raw.botId) } +} + +export const getBaiduTranslateCredentials = async (): Promise => { + return asBaidu(await getStorageItem(STORAGE_KEYS.BAIDU_TRANSLATE, null)) +} + +export const saveBaiduTranslateCredentials = async (credentials: BaiduTranslateCredentials) => { + await setStorageItem(STORAGE_KEYS.BAIDU_TRANSLATE, asBaidu(credentials)) +} + +export const getCozeCredentials = async (): Promise => { + return asCoze(await getStorageItem(STORAGE_KEYS.COZE_AI, null)) +} + +export const saveCozeCredentials = async (credentials: CozeCredentials) => { + await setStorageItem(STORAGE_KEYS.COZE_AI, asCoze(credentials)) +} + +export const hasBaiduTranslateCredentials = (credentials: BaiduTranslateCredentials) => + Boolean(credentials.appid && credentials.key) + +export const hasCozeCredentials = (credentials: CozeCredentials) => + Boolean(credentials.token && credentials.botId) diff --git a/plugins/huhabiaoqingbao/src/env.d.ts b/plugins/huhabiaoqingbao/src/env.d.ts new file mode 100644 index 000000000..c7d880fc1 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/env.d.ts @@ -0,0 +1,8 @@ +/// +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/plugins/huhabiaoqingbao/src/main.ts b/plugins/huhabiaoqingbao/src/main.ts new file mode 100644 index 000000000..3bae7d6d6 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/main.ts @@ -0,0 +1,21 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus, { messageConfig } from 'element-plus' +import 'element-plus/dist/index.css' +import 'element-plus/theme-chalk/dark/css-vars.css' +import App from './App.vue' +import '@/styles/common.scss' +import router from './router' +// 移除了remixicon字体导入,以减少打包体积 + +const app = createApp(App) +const pinia = createPinia() + +Object.assign(messageConfig, { + showClose: true +}) + +app.use(router) +app.use(pinia) +app.use(ElementPlus) +app.mount('#app') diff --git a/plugins/huhabiaoqingbao/src/router/index.ts b/plugins/huhabiaoqingbao/src/router/index.ts new file mode 100644 index 000000000..5ab3637c4 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/router/index.ts @@ -0,0 +1,98 @@ +import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' +import AboutAuthor from '../components/AboutAuthor.vue' +import EmoticonWorkshop from '../components/workshop/EmoticonWorkshop.vue' +import KaomojiPicker from '../components/emoji/KaomojiPicker.vue' +import EmojiPicker from '../components/emoji/EmojiPicker.vue' +import EmoticonList from '../components/emoticon/EmoticonGrid.vue' +import OnlineSearch from '../components/online/OnlineSearch.vue' +import VideoPlayer from '../components/VideoPlayer.vue' +import WallpaperView from '../views/WallpaperView.vue' +import Settings from '../views/Settings.vue' +import VideoToGif from '../components/video/VideoToGif.vue' +import AiEmoticonGenerator from '../components/ai/AiEmoticonGenerator.vue' +import BeautyImages from '../components/beauty/BeautyImages.vue' +import HandsomeImages from '../components/handsome/HandsomeImages.vue' + +const routes: Array = [ + { + path: '/', + redirect: '/all' + }, + { + path: '/all', + name: 'all', + component: EmoticonList + }, + { + path: '/favorite', + name: 'favorite', + component: EmoticonList + }, + { + path: '/online', + name: 'online', + component: OnlineSearch + }, + { + path: '/workshop', + name: 'workshop', + component: EmoticonWorkshop + }, + { + path: '/emoji', + name: 'emoji', + component: EmojiPicker + }, + { + path: '/kaomoji', + name: 'kaomoji', + component: KaomojiPicker + }, + { + path: '/about', + name: 'about', + component: AboutAuthor + }, + { + path: '/girlvideo', + name: 'girlvideo', + component: VideoPlayer + }, + { + path: '/wallpaper', + name: 'wallpaper', + component: WallpaperView + }, + { + path: '/settings', + name: 'settings', + component: Settings + }, + { + path: '/videotogif', + name: 'videotogif', + component: VideoToGif + }, + { + path: '/ai', + name: 'ai', + component: AiEmoticonGenerator + }, + { + path: '/beauty', + name: 'beauty', + component: BeautyImages + }, + { + path: '/handsome', + name: 'handsome', + component: HandsomeImages + } +] + +const router = createRouter({ + history: createWebHashHistory(), + routes +}) + +export default router \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/services/githubGistBackup.ts b/plugins/huhabiaoqingbao/src/services/githubGistBackup.ts new file mode 100644 index 000000000..31d6e96dc --- /dev/null +++ b/plugins/huhabiaoqingbao/src/services/githubGistBackup.ts @@ -0,0 +1,591 @@ +import axios from 'axios' +import type { Emoticon } from '@/types' +import { getStorageItem, setStorageItem, STORAGE_KEYS } from '@/utils/storage' + +export type BackupStatus = 'idle' | 'running' | 'success' | 'error' + +export interface GithubGistBackupSettings { + token: string + gistId: string + gistUrl: string + autoBackup: boolean + lastBackupAt: number | null + lastBackupStatus: BackupStatus + lastBackupMessage: string +} + +interface GistFileInfo { + filename: string + content?: string + truncated?: boolean + raw_url?: string +} + +interface GistResponse { + id: string + html_url: string + files: Record +} + +interface GistPatchPayload { + description?: string + files: Record +} + +interface BackupManifestItem { + id: string + name: string + type: string + favorite: boolean + createdAt: number + createTime: number + updateTime: number + tags: string[] + backupFile: string +} + +interface BackupManifest { + format: string + exportedAt: string + totalCount: number + emoticons: BackupManifestItem[] +} + +interface BackupFilePayload { + format: string + exportedAt: string + emoticon: { + id: string + name: string + type: string + favorite: boolean + createdAt: number + createTime: number + updateTime: number + tags: string[] + } + file: { + mimeType: string + size: number + dataBase64: string + } +} + +export interface BackupRestoreItem { + emoticon: Emoticon + file: Blob +} + +const BACKUP_FORMAT = 'bqb-gist-backup-v1' +const MANIFEST_FILE = 'emoticons-manifest.json' +const README_FILE = 'README.md' +const EMOTICON_FILE_PREFIX = 'emoticon-' + +const DEFAULT_BACKUP_SETTINGS: GithubGistBackupSettings = { + token: '', + gistId: '', + gistUrl: '', + autoBackup: false, + lastBackupAt: null, + lastBackupStatus: 'idle', + lastBackupMessage: '' +} + +class GithubGistBackupService { + private taskQueue: Promise = Promise.resolve() + + async getSettings(): Promise { + const settings = await getStorageItem( + STORAGE_KEYS.GITHUB_GIST_BACKUP, + DEFAULT_BACKUP_SETTINGS + ) + + return { + ...DEFAULT_BACKUP_SETTINGS, + ...settings + } + } + + async saveSettings( + partial: Partial + ): Promise { + const current = await this.getSettings() + const next = { + ...current, + ...partial + } + + await setStorageItem(STORAGE_KEYS.GITHUB_GIST_BACKUP, next) + return next + } + + async backupAllEmoticons(emoticons: Emoticon[]): Promise { + return this.enqueue(async () => { + const settings = await this.getSettings() + const token = settings.token.trim() + + if (!token) { + throw new Error('请先填写 GitHub Token') + } + + await this.saveSettings({ + lastBackupStatus: 'running', + lastBackupMessage: '正在执行全量备份...' + }) + + try { + const gist = await this.ensureGist(token, settings.gistId.trim()) + const existingGist = await this.getGist(token, gist.id) + const files: Record = { + [MANIFEST_FILE]: { + content: this.buildManifestContent(emoticons) + }, + [README_FILE]: { + content: this.buildReadmeContent() + } + } + + for (const emoticon of emoticons) { + const fileName = this.getEmoticonBackupFileName(emoticon.id) + const content = await this.buildBackupFileContent(emoticon) + files[fileName] = { content } + } + + for (const fileName of Object.keys(existingGist.files || {})) { + if ( + fileName.startsWith(EMOTICON_FILE_PREFIX) && + !files[fileName] + ) { + files[fileName] = null + } + } + + const updatedGist = await this.patchGist(token, gist.id, { + description: `BQB 表情包备份 ${new Date().toLocaleString('zh-CN')}`, + files + }) + + await this.saveSettings({ + gistId: updatedGist.id, + gistUrl: updatedGist.html_url, + lastBackupAt: Date.now(), + lastBackupStatus: 'success', + lastBackupMessage: `已备份 ${emoticons.length} 个表情包` + }) + + return updatedGist + } catch (error) { + const message = this.getErrorMessage(error) + await this.saveSettings({ + lastBackupStatus: 'error', + lastBackupMessage: message + }) + throw error + } + }) + } + + async autoBackupNewEmoticon( + emoticon: Emoticon, + file: Blob, + allEmoticons: Emoticon[] + ): Promise { + return this.enqueue(async () => { + const settings = await this.getSettings() + const token = settings.token.trim() + + if (!settings.autoBackup || !token) { + return null + } + + await this.saveSettings({ + lastBackupStatus: 'running', + lastBackupMessage: `正在自动备份 ${emoticon.name || '新表情'}...` + }) + + try { + const gist = await this.ensureGist(token, settings.gistId.trim()) + const updatedGist = await this.patchGist(token, gist.id, { + description: `BQB 自动备份 ${new Date().toLocaleString('zh-CN')}`, + files: { + [MANIFEST_FILE]: { + content: this.buildManifestContent(allEmoticons) + }, + [README_FILE]: { + content: this.buildReadmeContent() + }, + [this.getEmoticonBackupFileName(emoticon.id)]: { + content: await this.buildBackupFileContent(emoticon, file) + } + } + }) + + await this.saveSettings({ + gistId: updatedGist.id, + gistUrl: updatedGist.html_url, + lastBackupAt: Date.now(), + lastBackupStatus: 'success', + lastBackupMessage: `已自动备份 ${emoticon.name || '新表情'}` + }) + + return updatedGist + } catch (error) { + const message = this.getErrorMessage(error) + await this.saveSettings({ + lastBackupStatus: 'error', + lastBackupMessage: message + }) + throw error + } + }) + } + + async downloadBackupEmoticons(): Promise { + return this.enqueue(async () => { + const settings = await this.getSettings() + const token = settings.token.trim() + const gistId = settings.gistId.trim() + + if (!token) { + throw new Error('请先填写 GitHub Token') + } + + if (!gistId) { + throw new Error('请先填写或创建 Gist ID') + } + + await this.saveSettings({ + lastBackupStatus: 'running', + lastBackupMessage: '正在从 Gist 同步到本地...' + }) + + try { + const gist = await this.getGist(token, gistId) + const manifest = await this.readManifest(gist, token) + const restoreItems: BackupRestoreItem[] = [] + + for (const manifestItem of manifest.emoticons || []) { + const gistFile = gist.files?.[manifestItem.backupFile] + if (!gistFile) { + continue + } + + const filePayload = await this.readBackupFile(gistFile, token) + restoreItems.push({ + emoticon: { + ...filePayload.emoticon, + url: '' + }, + file: this.base64ToBlob(filePayload.file.dataBase64, filePayload.file.mimeType || filePayload.emoticon.type) + }) + } + + await this.saveSettings({ + gistId: gist.id, + gistUrl: gist.html_url, + lastBackupAt: Date.now(), + lastBackupStatus: 'success', + lastBackupMessage: `已从 Gist 读取 ${restoreItems.length} 个表情包` + }) + + return restoreItems + } catch (error) { + const message = this.getSyncErrorMessage(error) + await this.saveSettings({ + lastBackupStatus: 'error', + lastBackupMessage: message + }) + throw error + } + }) + } + + private enqueue(task: () => Promise): Promise { + const runTask = this.taskQueue.then(task, task) + this.taskQueue = runTask.then( + () => undefined, + () => undefined + ) + return runTask + } + + private async ensureGist( + token: string, + gistId: string + ): Promise { + if (gistId) { + try { + const gist = await this.getGist(token, gistId) + await this.saveSettings({ + gistId: gist.id, + gistUrl: gist.html_url + }) + return gist + } catch (error: any) { + if (axios.isAxiosError(error) && error.response?.status !== 404) { + throw error + } + } + } + + const createdGist = await this.createGist(token) + await this.saveSettings({ + gistId: createdGist.id, + gistUrl: createdGist.html_url + }) + return createdGist + } + + private async createGist(token: string): Promise { + const response = await axios.post( + 'https://api.github.com/gists', + { + description: 'BQB 表情包备份', + public: false, + files: { + [README_FILE]: { + content: this.buildReadmeContent() + }, + [MANIFEST_FILE]: { + content: this.buildManifestContent([]) + } + } + }, + { + headers: this.buildHeaders(token) + } + ) + + return response.data + } + + private async getGist(token: string, gistId: string): Promise { + const response = await axios.get( + `https://api.github.com/gists/${gistId}`, + { + headers: this.buildHeaders(token) + } + ) + + return response.data + } + + private async patchGist( + token: string, + gistId: string, + payload: GistPatchPayload + ): Promise { + const response = await axios.patch( + `https://api.github.com/gists/${gistId}`, + payload, + { + headers: this.buildHeaders(token) + } + ) + + return response.data + } + + private buildHeaders(token: string) { + return { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${token}`, + 'X-GitHub-Api-Version': '2022-11-28' + } + } + + private async readManifest( + gist: GistResponse, + token: string + ): Promise { + const manifestFile = gist.files?.[MANIFEST_FILE] + if (!manifestFile) { + throw new Error('Gist 中未找到备份索引文件') + } + + const content = await this.getGistFileContent(manifestFile, token) + const manifest = JSON.parse(content) as BackupManifest + + if (manifest.format !== BACKUP_FORMAT) { + throw new Error('Gist 备份格式不兼容') + } + + return manifest + } + + private async readBackupFile( + gistFile: GistFileInfo, + token: string + ): Promise { + const content = await this.getGistFileContent(gistFile, token) + const payload = JSON.parse(content) as BackupFilePayload + + if (payload.format !== BACKUP_FORMAT) { + throw new Error(`备份文件 ${gistFile.filename} 格式不兼容`) + } + + return payload + } + + private async getGistFileContent( + gistFile: GistFileInfo, + token: string + ): Promise { + if (gistFile.content && !gistFile.truncated) { + return gistFile.content + } + + if (!gistFile.raw_url) { + throw new Error(`无法读取备份文件 ${gistFile.filename}`) + } + + const response = await axios.get(gistFile.raw_url, { + headers: { + Authorization: `Bearer ${token}` + }, + responseType: 'text' + }) + + return response.data + } + + private buildManifestContent(emoticons: Emoticon[]): string { + return JSON.stringify( + { + format: BACKUP_FORMAT, + exportedAt: new Date().toISOString(), + totalCount: emoticons.length, + emoticons: emoticons.map(emoticon => ({ + ...this.sanitizeEmoticon(emoticon), + backupFile: this.getEmoticonBackupFileName(emoticon.id) + })) + }, + null, + 2 + ) + } + + private buildReadmeContent(): string { + return [ + '# BQB 表情包备份', + '', + '该 Gist 由呼哈表情包自动维护。', + '', + `备份格式: ${BACKUP_FORMAT}`, + '', + `- ${MANIFEST_FILE}: 备份索引与元数据`, + `- ${EMOTICON_FILE_PREFIX}*.json: 单个表情包的元数据与 Base64 内容` + ].join('\n') + } + + private getEmoticonBackupFileName(id: string): string { + return `${EMOTICON_FILE_PREFIX}${id}.json` + } + + private sanitizeEmoticon(emoticon: Emoticon) { + return { + id: emoticon.id, + name: emoticon.name, + type: emoticon.type, + favorite: emoticon.favorite, + createdAt: emoticon.createdAt, + createTime: emoticon.createTime, + updateTime: emoticon.updateTime, + tags: Array.from(emoticon.tags || []) + } + } + + private async buildBackupFileContent( + emoticon: Emoticon, + sourceBlob?: Blob + ): Promise { + const blob = sourceBlob || await this.fetchEmoticonBlob(emoticon) + const dataBase64 = await this.blobToBase64(blob) + + return JSON.stringify( + { + format: BACKUP_FORMAT, + exportedAt: new Date().toISOString(), + emoticon: this.sanitizeEmoticon(emoticon), + file: { + mimeType: blob.type || emoticon.type, + size: blob.size, + dataBase64 + } + }, + null, + 2 + ) + } + + private async fetchEmoticonBlob(emoticon: Emoticon): Promise { + if (!emoticon.url) { + throw new Error(`表情包 ${emoticon.name || emoticon.id} 缺少可读取的文件地址`) + } + + const response = await fetch(emoticon.url) + if (!response.ok) { + throw new Error(`无法读取表情包文件:${emoticon.name || emoticon.id}`) + } + + return response.blob() + } + + private blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + const result = String(reader.result || '') + const markerIndex = result.indexOf(',') + resolve(markerIndex >= 0 ? result.slice(markerIndex + 1) : result) + } + reader.onerror = () => reject(reader.error || new Error('读取文件失败')) + reader.readAsDataURL(blob) + }) + } + + private base64ToBlob(base64: string, mimeType: string): Blob { + const binary = atob(base64) + const length = binary.length + const bytes = new Uint8Array(length) + + for (let i = 0; i < length; i++) { + bytes[i] = binary.charCodeAt(i) + } + + return new Blob([bytes], { type: mimeType || 'application/octet-stream' }) + } + + private getErrorMessage(error: unknown): string { + if (axios.isAxiosError(error)) { + const apiMessage = error.response?.data?.message + if (typeof apiMessage === 'string' && apiMessage.trim()) { + return `GitHub 备份失败:${apiMessage}` + } + } + + if (error instanceof Error && error.message) { + return error.message + } + + return 'GitHub 备份失败' + } + + private getSyncErrorMessage(error: unknown): string { + if (axios.isAxiosError(error)) { + const apiMessage = error.response?.data?.message + if (typeof apiMessage === 'string' && apiMessage.trim()) { + return `GitHub 同步失败:${apiMessage}` + } + } + + if (error instanceof Error && error.message) { + return error.message + } + + return 'GitHub 同步失败' + } +} + +export const githubGistBackupService = new GithubGistBackupService() +export { DEFAULT_BACKUP_SETTINGS } diff --git a/plugins/huhabiaoqingbao/src/services/importers/larkImporter.ts b/plugins/huhabiaoqingbao/src/services/importers/larkImporter.ts new file mode 100644 index 000000000..829db795b --- /dev/null +++ b/plugins/huhabiaoqingbao/src/services/importers/larkImporter.ts @@ -0,0 +1,775 @@ +import { nanoid } from 'nanoid' +import type { Emoticon } from '@/types' + +const IMAGE_EXTENSIONS = new Set(['.gif', '.png', '.jpg', '.jpeg', '.webp', '.bmp']) +const MIME_MAP: Record = { + '.gif': 'image/gif', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + '.bmp': 'image/bmp' +} + +const MAX_SCAN_RESULTS = 5000 +const MAX_INDEX_WALK_DEPTH = 4 +const MAX_INDEX_FILE_SIZE = 8 * 1024 * 1024 +const MAX_IMAGE_FILE_SIZE = 25 * 1024 * 1024 +const MIN_CUSTOM_STICKER_SIDE = 96 +const LARK_USER_ID_PATTERN = /^[0-9a-f]{24,40}$/i +const LARK_CUSTOM_STICKER_KEY_PATTERN = /^[0-9a-f-]{32,}g?$/i +const VERIFIED_INDEX_MARKERS = [ + 'customizedStickers', + 'userStickerSets', + 'PULL_STICKERS', + 'PULL_STICKER_SETS' +] + +const LARK_STICKER_DIR_NAMES = new Set([ + 'customizedstickers', + 'stickers', + 'sticker', + 'customstickers' +]) + +export interface LarkScanResult { + name: string + filePath: string + type: string +} + +export interface LarkDetectResult { + found: boolean + path: string + description: string +} + +interface LarkIndexSticker { + name: string + keys: string[] +} + +interface LarkUserStorage { + path: string + desc: string + stickerPath: string + imageCount: number + lastModified: number +} + +function getFs() { + return (window as any).preload.fs +} + +function getHomePath(): string { + try { + return (window as any).ztools.getPath('home') + } catch { + return '' + } +} + +function getAppDataPath(): string { + try { + return (window as any).ztools.getPath('appData') + } catch { + return '' + } +} + +function getExt(filename: string): string { + const dot = filename.lastIndexOf('.') + return dot >= 0 ? filename.slice(dot).toLowerCase() : '' +} + +function getBaseName(filePath: string): string { + const parts = filePath.split(/[\\/]+/).filter(Boolean) + return parts[parts.length - 1] || filePath +} + +function getFileStem(filename: string): string { + const dot = filename.lastIndexOf('.') + return dot >= 0 ? filename.slice(0, dot) : filename +} + +function normalizeText(value: string): string { + return value.toLowerCase().replace(/[\s_-]/g, '') +} + +function joinPath(basePath: string, ...segments: string[]): string { + const separator = basePath.includes('\\') ? '\\' : '/' + return [basePath.replace(/[\\/]+$/, ''), ...segments].join(separator) +} + +function pathExists(dirPath: string): boolean { + const fs = getFs() + try { + return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory() + } catch { + return false + } +} + +function isImageExtension(filename: string): boolean { + return IMAGE_EXTENSIONS.has(getExt(filename)) +} + +function getMimeTypeByExtension(filename: string): string { + return MIME_MAP[getExt(filename)] || 'image/png' +} + +function isLikelyVersionedStickerCache(filename: string): boolean { + const normalized = filename.toLowerCase() + return normalized.startsWith('v2_') || normalized.startsWith('v3_') +} + +function isLikelyCustomStickerFilename(filename: string): boolean { + if (!isImageExtension(filename) || isLikelyVersionedStickerCache(filename)) return false + return LARK_CUSTOM_STICKER_KEY_PATTERN.test(getFileStem(filename)) +} + +function isDirectory(dirPath: string): boolean { + const fs = getFs() + try { + return fs.statSync(dirPath).isDirectory() + } catch { + return false + } +} + +function getImageSize(filePath: string): { width: number; height: number } | null { + const fs = getFs() + + try { + const buffer = fs.readFileSync(filePath) + if (!buffer || buffer.length < 10) return null + + if ( + buffer[0] === 0x89 && + buffer[1] === 0x50 && + buffer[2] === 0x4e && + buffer[3] === 0x47 && + buffer.length >= 24 + ) { + return { + width: buffer.readUInt32BE(16), + height: buffer.readUInt32BE(20) + } + } + + if ( + buffer[0] === 0x47 && + buffer[1] === 0x49 && + buffer[2] === 0x46 && + buffer[3] === 0x38 && + buffer.length >= 10 + ) { + return { + width: buffer.readUInt16LE(6), + height: buffer.readUInt16LE(8) + } + } + + if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { + let offset = 2 + while (offset + 9 < buffer.length) { + if (buffer[offset] !== 0xff) break + const marker = buffer[offset + 1] + const length = buffer.readUInt16BE(offset + 2) + if (length < 2) break + + if (marker >= 0xc0 && marker <= 0xc3) { + return { + height: buffer.readUInt16BE(offset + 5), + width: buffer.readUInt16BE(offset + 7) + } + } + + offset += 2 + length + } + } + } catch { + return null + } + + return null +} + +function isLargeEnoughCustomSticker(filePath: string): boolean { + const size = getImageSize(filePath) + if (!size) return true + return Math.max(size.width, size.height) >= MIN_CUSTOM_STICKER_SIDE +} + +function isStickerDirectory(dirPath: string): boolean { + const parts = dirPath.split(/[\\/]+/).map(normalizeText) + const lastPart = parts[parts.length - 1] + const parentPart = parts[parts.length - 2] + return lastPart === 'stickers' && parentPart === 'resources' +} + +function isLikelyLarkUserStorage(dirPath: string): boolean { + return pathExists(joinPath(dirPath, 'resources', 'stickers')) +} + +function readTextFile(filePath: string): string | null { + const fs = getFs() + + try { + const stat = fs.statSync(filePath) + if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_INDEX_FILE_SIZE) return null + + const value = fs.readFileSync(filePath, 'utf8') + return typeof value === 'string' ? value : String(value) + } catch { + return null + } +} + +function tryParseJson(value: string): unknown { + try { + return JSON.parse(value) + } catch { + return null + } +} + +function collectImageKeys(value: unknown, keys = new Set()): Set { + if (!value || keys.size >= MAX_SCAN_RESULTS) return keys + + if (typeof value === 'string') { + if (/^(v[23]_[0-9a-z_]+-)|([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})/i.test(value)) { + keys.add(value) + } + return keys + } + + if (Array.isArray(value)) { + for (const item of value) collectImageKeys(item, keys) + return keys + } + + if (typeof value !== 'object') return keys + + const record = value as Record + for (const field of ['key', 'originKey', 'middleKey', 'thumbKey']) { + collectImageKeys(record[field], keys) + } + + for (const field of ['image', 'origin', 'thumbnail', 'middle', 'thumbnailWebp', 'middleWebp']) { + collectImageKeys(record[field], keys) + } + + return keys +} + +function getStickerName(value: Record, fallback: string): string { + for (const field of ['description', 'name', 'title', 'stickerId', 'stickerSetId']) { + const raw = value[field] + if (typeof raw === 'string' && raw.trim()) return raw.trim() + } + + return fallback +} + +function collectStickerEntries(value: unknown, entries: LarkIndexSticker[] = []): LarkIndexSticker[] { + if (!value || entries.length >= MAX_SCAN_RESULTS) return entries + + if (Array.isArray(value)) { + for (const item of value) collectStickerEntries(item, entries) + return entries + } + + if (typeof value !== 'object') return entries + + const record = value as Record + + if (record.image || record.stickerId || record.stickerSetId) { + const keys = [...collectImageKeys(record)] + if (keys.length > 0) { + entries.push({ + name: getStickerName(record, keys[0]), + keys + }) + } + } + + for (const field of ['customizedStickers', 'stickers', 'userStickerSets', 'stickerSets']) { + collectStickerEntries(record[field], entries) + } + + return entries +} + +function extractVerifiedStickerIndex(filePath: string): LarkIndexSticker[] { + const text = readTextFile(filePath) + if (!text || !VERIFIED_INDEX_MARKERS.some(marker => text.includes(marker))) return [] + + const jsonStart = text.search(/[\[{]/) + const jsonEnd = Math.max(text.lastIndexOf('}'), text.lastIndexOf(']')) + if (jsonStart === -1 || jsonEnd <= jsonStart) return [] + + const parsed = tryParseJson(text.slice(jsonStart, jsonEnd + 1)) + if (!parsed) return [] + + return collectStickerEntries(parsed) +} + +function findVerifiedIndexFiles(basePath: string): string[] { + const fs = getFs() + const results: string[] = [] + const seen = new Set() + + function walk(dirPath: string, depth = 0) { + if (depth > MAX_INDEX_WALK_DEPTH || results.length >= MAX_SCAN_RESULTS) return + + let entries: string[] + try { + entries = fs.readdirSync(dirPath) + } catch { + return + } + + for (const entry of entries) { + if (entry.startsWith('.')) continue + const fullPath = joinPath(dirPath, entry) + + try { + const stat = fs.statSync(fullPath) + if (stat.isDirectory()) { + walk(fullPath, depth + 1) + continue + } + + if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_INDEX_FILE_SIZE) continue + if (seen.has(fullPath)) continue + + const text = readTextFile(fullPath) + if (text && VERIFIED_INDEX_MARKERS.some(marker => text.includes(marker))) { + seen.add(fullPath) + results.push(fullPath) + } + } catch { + // Skip unreadable entries. + } + } + } + + walk(basePath) + return results +} + +function findStickerSourcePaths(basePath: string): string[] { + const fs = getFs() + const results: string[] = [] + const seen = new Set() + + function addIfStickerPath(dirPath: string) { + if (seen.has(dirPath)) return + if (!isStickerDirectory(dirPath)) return + if (!pathExists(dirPath)) return + + seen.add(dirPath) + results.push(dirPath) + } + + addIfStickerPath(basePath) + addIfStickerPath(joinPath(basePath, 'resources', 'stickers')) + + function walk(dirPath: string, depth = 0, maxDepth = 6) { + if (depth > maxDepth) return + + try { + const entries = fs.readdirSync(dirPath) + for (const entry of entries) { + if (entry.startsWith('.')) continue + + const fullPath = joinPath(dirPath, entry) + let isDirectory = false + try { + isDirectory = fs.statSync(fullPath).isDirectory() + } catch { + continue + } + if (!isDirectory) continue + + addIfStickerPath(fullPath) + if (isStickerDirectory(fullPath)) continue + + walk(fullPath, depth + 1, maxDepth) + } + } catch { + // Directory not readable. + } + } + + walk(basePath) + return results +} + +function findStickerSourcePathsFlexible(basePath: string): string[] { + const fs = getFs() + const results: string[] = [] + const seen = new Set() + + function addIfExists(dirPath: string) { + if (seen.has(dirPath)) return + seen.add(dirPath) + if (pathExists(dirPath)) results.push(dirPath) + } + + // 1) Existing strict structure-based detection + for (const p of findStickerSourcePaths(basePath)) addIfExists(p) + + // 2) Name-based detection for common Lark sticker directories + function walkByName(dirPath: string, depth = 0) { + if (depth > MAX_INDEX_WALK_DEPTH) return + + let entries: string[] + try { + entries = fs.readdirSync(dirPath) + } catch { + return + } + + for (const entry of entries) { + if (entry.startsWith('.')) continue + const fullPath = joinPath(dirPath, entry) + + let isDir = false + try { + isDir = fs.statSync(fullPath).isDirectory() + } catch { + continue + } + if (!isDir) continue + + if (LARK_STICKER_DIR_NAMES.has(normalizeText(entry))) { + addIfExists(fullPath) + } + + walkByName(fullPath, depth + 1) + } + } + + walkByName(basePath) + return results +} + +function scanStickerImages(dirPaths: string[], customizedOnly = false): LarkScanResult[] { + const fs = getFs() + const results: LarkScanResult[] = [] + const seen = new Set() + + for (const dirPath of dirPaths) { + let entries: string[] + try { + entries = fs.readdirSync(dirPath) + } catch { + continue + } + + for (const entry of entries) { + if (results.length >= MAX_SCAN_RESULTS) return results + if (entry.startsWith('.') || !isImageExtension(entry)) continue + if (customizedOnly && !isLikelyCustomStickerFilename(entry)) continue + + const fullPath = joinPath(dirPath, entry) + if (seen.has(fullPath)) continue + + try { + const stat = fs.statSync(fullPath) + if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_IMAGE_FILE_SIZE) continue + if (customizedOnly && !isLargeEnoughCustomSticker(fullPath)) continue + + seen.add(fullPath) + results.push({ + name: getFileStem(entry), + filePath: fullPath, + type: getMimeTypeByExtension(entry) + }) + } catch { + // Skip unreadable files + } + } + } + + return results +} + +function countCustomizedStickerImages(dirPath: string): number { + return scanStickerImages([dirPath], false).length +} + +function getStickerCacheByKey(stickerPaths: string[]): Map { + const fs = getFs() + const result = new Map() + + for (const stickerPath of stickerPaths) { + let entries: string[] + try { + entries = fs.readdirSync(stickerPath) + } catch { + continue + } + + for (const entry of entries) { + if (entry.startsWith('.') || !isImageExtension(entry)) continue + const fullPath = joinPath(stickerPath, entry) + + try { + const stat = fs.statSync(fullPath) + if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_IMAGE_FILE_SIZE) continue + + const stem = getFileStem(entry) + result.set(stem, fullPath) + } catch { + // Skip unreadable entries. + } + } + } + + return result +} + +function resolveStickerFile(keys: string[], cacheByKey: Map): string | null { + for (const key of keys) { + const exact = cacheByKey.get(key) + if (exact) return exact + } + + return null +} + +function scanFromVerifiedIndexes(basePath: string): LarkScanResult[] { + if (isLikelyLarkUserStorage(basePath)) { + return scanStickerImages([joinPath(basePath, 'resources', 'stickers')], false) + } + + const stickerPaths = findStickerSourcePathsFlexible(basePath) + if (stickerPaths.length === 0) return [] + + // Try strict index-based matching first + const indexFiles = findVerifiedIndexFiles(basePath) + if (indexFiles.length > 0) { + const cacheByKey = getStickerCacheByKey(stickerPaths) + const results: LarkScanResult[] = [] + + for (const indexFile of indexFiles) { + const stickers = extractVerifiedStickerIndex(indexFile) + for (const sticker of stickers) { + if (results.length >= MAX_SCAN_RESULTS) return dedupeScans(results) + + const filePath = resolveStickerFile(sticker.keys, cacheByKey) + if (!filePath) continue + + results.push({ + name: sticker.name || getBaseName(filePath), + filePath, + type: getMimeTypeByExtension(filePath) + }) + } + } + + const deduped = dedupeScans(results) + if (deduped.length > 0) return deduped + } + + return scanStickerImages(stickerPaths, false) +} + +function dedupeScans(items: T[]): T[] { + const seen = new Set() + return items.filter(item => { + if (seen.has(item.filePath)) return false + seen.add(item.filePath) + return true + }) +} + +function getSdkStorageCandidates(): Array<{ path: string; desc: string }> { + const home = getHomePath() + const appData = getAppDataPath() + if (!home) return [] + + const isWindows = home.includes('\\') || home.indexOf(':') === 1 + const candidates: Array<{ path: string; desc: string }> = [] + + if (isWindows) { + const appDataRoots = [ + appData, + `${home}\\AppData\\Roaming`, + `${home}\\AppData\\Local` + ].filter(Boolean) + + for (const root of appDataRoots) { + candidates.push( + { path: `${root}\\LarkShell\\sdk_storage`, desc: '飞书 SDK 数据' }, + { path: `${root}\\Feishu\\sdk_storage`, desc: '飞书 SDK 数据' }, + { path: `${root}\\Lark\\sdk_storage`, desc: 'Lark SDK 数据' } + ) + } + } else { + const containerIds = [ + 'com.bytedance.macos.feishu', + 'com.larkoffice.Lark', + 'com.larksuite.desktop', + 'com.electron.lark' + ] + + for (const bundleId of containerIds) { + const root = `${home}/Library/Containers/${bundleId}/Data/Library` + candidates.push( + { path: `${root}/Application Support/LarkShell/sdk_storage`, desc: '飞书 SDK 数据' } + ) + } + + candidates.push( + { path: `${home}/Library/Application Support/LarkShell/sdk_storage`, desc: '飞书 SDK 数据' }, + { path: `${home}/Library/Application Support/Feishu/sdk_storage`, desc: '飞书 SDK 数据' }, + { path: `${home}/Library/Application Support/Lark/sdk_storage`, desc: 'Lark SDK 数据' } + ) + } + + const seen = new Set() + return candidates.filter(item => { + if (seen.has(item.path)) return false + seen.add(item.path) + return true + }) +} + +function getLarkUserStorages(): LarkUserStorage[] { + const fs = getFs() + const results: LarkUserStorage[] = [] + const seen = new Set() + + function addUserStorage(dirPath: string, desc: string) { + if (seen.has(dirPath) || !isLikelyLarkUserStorage(dirPath)) return + + const stickerPath = joinPath(dirPath, 'resources', 'stickers') + let lastModified = 0 + try { + lastModified = fs.statSync(dirPath).mtimeMs || 0 + } catch { + // Keep default value. + } + + seen.add(dirPath) + results.push({ + path: dirPath, + desc, + stickerPath, + imageCount: countCustomizedStickerImages(stickerPath), + lastModified + }) + } + + for (const candidate of getSdkStorageCandidates()) { + if (!pathExists(candidate.path)) continue + + addUserStorage(candidate.path, candidate.desc) + + let entries: string[] + try { + entries = fs.readdirSync(candidate.path) + } catch { + continue + } + + for (const entry of entries) { + if (entry.startsWith('.') || entry === 'settings') continue + if (!LARK_USER_ID_PATTERN.test(entry) && normalizeText(entry) !== 'global') continue + + const userPath = joinPath(candidate.path, entry) + if (isDirectory(userPath)) { + addUserStorage(userPath, `${candidate.desc} (${entry})`) + } + } + } + + return results.sort((a, b) => { + if (b.imageCount !== a.imageCount) return b.imageCount - a.imageCount + return b.lastModified - a.lastModified + }) +} + +function getScanBasePath(dirPath: string): string { + if (isLikelyLarkUserStorage(dirPath)) return dirPath + + const stickerSource = findStickerSourcePaths(dirPath)[0] + if (stickerSource && isStickerDirectory(stickerSource)) { + const parts = stickerSource.split(/[\\/]+/) + return parts.slice(0, -2).join(stickerSource.includes('\\') ? '\\' : '/') + } + + return dirPath +} + +export function detectLarkPath(): LarkDetectResult[] { + const best = getLarkUserStorages()[0] + if (!best) return [] + + return [{ + found: true, + path: best.path, + description: best.imageCount > 0 + ? `${best.desc} - 发现 ${best.imageCount} 张飞书表情` + : `${best.desc} - 已找到用户表情目录,未发现图片文件` + }] +} + +export function scanLarkEmoticons(dirPath: string): LarkScanResult[] { + const basePath = getScanBasePath(dirPath) + return scanFromVerifiedIndexes(basePath) +} + +export function readFileAsBlob(filePath: string, mimeType: string): Blob { + const fs = getFs() + const buffer = fs.readFileSync(filePath) + const uint8 = new Uint8Array(buffer) + return new Blob([uint8], { type: mimeType }) +} + +export function createLarkImportItem(item: LarkScanResult): { emoticon: Emoticon; file: Blob } | null { + try { + const blob = readFileAsBlob(item.filePath, item.type) + if (blob.size === 0) return null + + const now = Date.now() + const emoticon: Emoticon = { + id: nanoid(), + name: item.name || getBaseName(item.filePath), + url: '', + type: item.type, + tags: [], + favorite: false, + source: 'feishu', + createdAt: now, + createTime: now, + updateTime: now + } + + return { emoticon, file: blob } + } catch (err) { + console.warn(`Failed to read Feishu file: ${item.filePath}`, err) + return null + } +} + +export function importFromLarkPath( + dirPath: string, + onProgress?: (current: number, total: number) => void +): { emoticon: Emoticon; file: Blob }[] { + const scanned = scanLarkEmoticons(dirPath) + const results: { emoticon: Emoticon; file: Blob }[] = [] + + for (let i = 0; i < scanned.length; i++) { + const item = createLarkImportItem(scanned[i]) + if (item) results.push(item) + + onProgress?.(i + 1, scanned.length) + } + + return results +} diff --git a/plugins/huhabiaoqingbao/src/services/importers/qqImporter.ts b/plugins/huhabiaoqingbao/src/services/importers/qqImporter.ts new file mode 100644 index 000000000..e475e8f87 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/services/importers/qqImporter.ts @@ -0,0 +1,389 @@ +import { nanoid } from 'nanoid' +import type { Emoticon } from '@/types' + +const IMAGE_EXTENSIONS = new Set(['.gif', '.png', '.jpg', '.jpeg', '.webp', '.bmp']) +const QQ_LEGACY_PERSONAL_EMOTICON_DIR_NAMES = new Set([ + 'customface', + 'roamingcustomface' +]) +const QQ_BUILT_IN_EMOTICON_DIR_NAMES = new Set([ + 'emoji', + 'emoticon', + 'emotion', + 'face', + 'facestore', + 'facesource', + 'sticker', + '表情', + '贴纸' +]) +const QQ_NON_EMOTICON_DIR_KEYWORDS = [ + 'avatar', + 'cache', + 'chatimg', + 'chatimage', + 'download', + 'filerecv', + 'grouphead', + 'head', + 'image', + 'photo', + 'picture', + 'qzone', + 'richmedia', + 'screenshot', + 'temp', + 'thumbnail', + 'thumb', + 'video', + 'wallpaper' +] +const MIME_MAP: Record = { + '.gif': 'image/gif', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + '.bmp': 'image/bmp' +} + +export interface ScanResult { + name: string + filePath: string + type: string +} + +export interface QQDetectResult { + found: boolean + path: string + description: string +} + +function getFs() { + return (window as any).preload.fs +} + +function getHomePath(): string { + try { + return (window as any).ztools.getPath('home') + } catch { + return '' + } +} + +function getExt(filename: string): string { + const dot = filename.lastIndexOf('.') + return dot >= 0 ? filename.slice(dot).toLowerCase() : '' +} + +function getBaseName(filePath: string): string { + const parts = filePath.split(/[\\/]+/).filter(Boolean) + return parts[parts.length - 1] || filePath +} + +function normalizePathSegment(segment: string): string { + return segment.toLowerCase().replace(/[\s_-]/g, '') +} + +function isImageFile(filename: string): boolean { + return IMAGE_EXTENSIONS.has(getExt(filename)) +} + +function isQQPersonalEmoticonSourceDirectory(dirname: string): boolean { + const normalized = normalizePathSegment(dirname) + return QQ_LEGACY_PERSONAL_EMOTICON_DIR_NAMES.has(normalized) +} + +function shouldSkipQQDirectory(dirname: string): boolean { + const normalized = normalizePathSegment(dirname) + return QQ_NON_EMOTICON_DIR_KEYWORDS.some(keyword => normalized.includes(keyword)) +} + +function shouldSkipQQBuiltInEmoticonDirectory(dirname: string): boolean { + const normalized = normalizePathSegment(dirname) + return QQ_BUILT_IN_EMOTICON_DIR_NAMES.has(normalized) +} + +function joinPath(basePath: string, ...segments: string[]): string { + const separator = basePath.includes('\\') ? '\\' : '/' + return [basePath.replace(/[\\/]+$/, ''), ...segments].join(separator) +} + +function getMimeType(filename: string): string { + return MIME_MAP[getExt(filename)] || 'image/png' +} + +function isPersonalEmojiOriDirectory(dirPath: string): boolean { + const normalizedParts = dirPath.split(/[\\/]+/).map(normalizePathSegment) + const lastPart = normalizedParts[normalizedParts.length - 1] + const parentPart = normalizedParts[normalizedParts.length - 2] + return lastPart === 'ori' && parentPart === 'personalemoji' +} + +function isQQPersonalEmoticonSourcePath(dirPath: string): boolean { + return isQQPersonalEmoticonSourceDirectory(getBaseName(dirPath)) || isPersonalEmojiOriDirectory(dirPath) +} + +function getQQPersonalSourcePaths(basePath: string): string[] { + const fs = getFs() + const results: string[] = [] + const seen = new Set() + + function addIfExists(dirPath: string) { + if (seen.has(dirPath)) return + try { + if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) { + seen.add(dirPath) + results.push(dirPath) + } + } catch { + // Skip inaccessible paths + } + } + + if (isQQPersonalEmoticonSourcePath(basePath)) { + addIfExists(basePath) + return results + } + + function walk(dirPath: string, depth = 0, maxDepth = 6) { + if (depth > maxDepth) return + + try { + const entries = fs.readdirSync(dirPath) + for (const entry of entries) { + if (entry.startsWith('.')) continue + + const fullPath = joinPath(dirPath, entry) + let isDirectory = false + try { + isDirectory = fs.statSync(fullPath).isDirectory() + } catch { + continue + } + if (!isDirectory) continue + + const normalizedEntry = normalizePathSegment(entry) + if (isQQPersonalEmoticonSourceDirectory(entry)) { + addIfExists(fullPath) + continue + } + + if (normalizedEntry === 'personalemoji') { + addIfExists(joinPath(fullPath, 'Ori')) + continue + } + + if (isPersonalEmojiOriDirectory(fullPath)) { + addIfExists(fullPath) + continue + } + + if (shouldSkipQQDirectory(entry)) continue + if (['baseemojisyastems', 'emojirelated', 'emojirecv', 'emojiresource', 'marketface', 'pokeface'].includes(normalizedEntry)) { + continue + } + + walk(fullPath, depth + 1, maxDepth) + } + } catch { + // Directory not readable + } + } + + walk(basePath) + return results +} + +function scanFilesInDirectory(dirPath: string, depth = 0, maxDepth = 3): ScanResult[] { + if (depth > maxDepth) return [] + + const fs = getFs() + const results: ScanResult[] = [] + + try { + const entries = fs.readdirSync(dirPath) + for (const entry of entries) { + if (entry.startsWith('.')) continue + + const fullPath = joinPath(dirPath, entry) + try { + const stat = fs.statSync(fullPath) + if (stat.isDirectory()) { + if (shouldSkipQQDirectory(entry) || shouldSkipQQBuiltInEmoticonDirectory(entry)) continue + results.push(...scanFilesInDirectory(fullPath, depth + 1, maxDepth)) + } else if (stat.isFile() && isImageFile(entry)) { + results.push({ + name: entry, + filePath: fullPath, + type: getMimeType(entry) + }) + } + } catch { + // Skip files that can't be accessed + } + } + } catch { + // Directory not readable + } + + return results +} + +function scanQQPersonalEmoticons(dirPath: string): ScanResult[] { + const sourcePaths = getQQPersonalSourcePaths(dirPath) + return sourcePaths.flatMap(sourcePath => scanFilesInDirectory(sourcePath)) +} + +/** + * Detect QQ emoticon directories on the current system. + */ +export function detectQQPath(): QQDetectResult[] { + const results: QQDetectResult[] = [] + const fs = getFs() + const home = getHomePath() + if (!home) return results + + const isWindows = home.includes('\\') || home.indexOf(':') === 1 + + if (!isWindows) { + // macOS QQ paths + const macPaths = [ + { + path: `${home}/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ`, + desc: 'QQ 桌面版 (Mac)' + }, + { + path: `${home}/Library/Containers/com.tencent.qq/Data/Documents/QQ`, + desc: 'QQ 旧版 (Mac)' + } + ] + + for (const { path: basePath, desc } of macPaths) { + if (fs.existsSync(basePath)) { + const imageFiles = scanQQPersonalEmoticons(basePath) + if (imageFiles.length > 0) { + results.push({ found: true, path: basePath, description: `${desc} - 发现 ${imageFiles.length} 张个人添加 QQ 表情` }) + } else { + results.push({ found: true, path: basePath, description: `${desc} - 已找到目录,未发现个人添加 QQ 表情` }) + } + } + } + } else { + // Windows QQ paths + const docsPaths = [ + `${home}\\Documents\\Tencent Files`, + `${home}\\AppData\\Local\\Tencent\\QQ` + ] + + for (const basePath of docsPaths) { + if (fs.existsSync(basePath)) { + try { + const entries = fs.readdirSync(basePath) + for (const entry of entries) { + if (entry === 'All Users' || entry.startsWith('.')) continue + const subPath = `${basePath}\\${entry}` + try { + const stat = fs.statSync(subPath) + if (stat.isDirectory()) { + // Check for CustomFace directory + const customFace = `${subPath}\\CustomFace` + if (fs.existsSync(customFace)) { + const imageFiles = scanQQPersonalEmoticons(customFace) + results.push({ + found: true, + path: customFace, + description: `QQ (${entry}) - 发现 ${imageFiles.length} 张个人添加 QQ 表情` + }) + } + + const roamingCustomFace = `${subPath}\\RoamingCustomFace` + if (fs.existsSync(roamingCustomFace)) { + const imageFiles = scanQQPersonalEmoticons(roamingCustomFace) + results.push({ + found: true, + path: roamingCustomFace, + description: `QQ (${entry}) - 发现 ${imageFiles.length} 张漫游个人 QQ 表情` + }) + } + } + } catch { /* skip */ } + } + } catch { /* skip */ } + } + } + } + + return results +} + +/** + * Scan a directory for image files and return results. + */ +export function scanEmoticons(dirPath: string): ScanResult[] { + const scanned = scanQQPersonalEmoticons(dirPath) + const seen = new Set() + return scanned.filter(item => { + if (seen.has(item.filePath)) return false + seen.add(item.filePath) + return true + }) +} + +/** + * Read a file from disk and convert to Blob. + */ +export function readFileAsBlob(filePath: string, mimeType: string): Blob { + const fs = getFs() + const buffer = fs.readFileSync(filePath) + // buffer is a Node.js Buffer, convert to Uint8Array for Blob + const uint8 = new Uint8Array(buffer) + return new Blob([uint8], { type: mimeType }) +} + +export function createImportItem(item: ScanResult): { emoticon: Emoticon; file: Blob } | null { + try { + const blob = readFileAsBlob(item.filePath, item.type) + if (blob.size === 0) return null + + const now = Date.now() + const emoticon: Emoticon = { + id: nanoid(), + name: item.name, + url: '', + type: item.type, + tags: [], + favorite: false, + source: 'qq', + createdAt: now, + createTime: now, + updateTime: now + } + + return { emoticon, file: blob } + } catch (err) { + console.warn(`Failed to read file: ${item.filePath}`, err) + return null + } +} + +/** + * Import emoticons from a directory. + * Returns an array of { emoticon, file } pairs ready for store.addEmoticon(). + */ +export function importFromPath( + dirPath: string, + onProgress?: (current: number, total: number) => void +): { emoticon: Emoticon; file: Blob }[] { + const scanned = scanEmoticons(dirPath) + const results: { emoticon: Emoticon; file: Blob }[] = [] + + for (let i = 0; i < scanned.length; i++) { + const item = createImportItem(scanned[i]) + if (item) results.push(item) + + onProgress?.(i + 1, scanned.length) + } + + return results +} diff --git a/plugins/huhabiaoqingbao/src/services/importers/wechatImporter.ts b/plugins/huhabiaoqingbao/src/services/importers/wechatImporter.ts new file mode 100644 index 000000000..8bd8944bc --- /dev/null +++ b/plugins/huhabiaoqingbao/src/services/importers/wechatImporter.ts @@ -0,0 +1,422 @@ +import { nanoid } from 'nanoid' +import type { Emoticon } from '@/types' + +const IMAGE_EXTENSIONS = new Set(['.gif', '.png', '.jpg', '.jpeg', '.webp', '.bmp']) +const MIME_MAP: Record = { + '.gif': 'image/gif', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + '.bmp': 'image/bmp' +} + +// File header magic bytes for extension-less files +const FILE_SIGNATURES: Array<{ bytes: number[]; ext: string }> = [ + { bytes: [0xff, 0xd8, 0xff], ext: '.jpg' }, // JPEG + { bytes: [0x89, 0x50, 0x4e, 0x47], ext: '.png' }, // PNG + { bytes: [0x47, 0x49, 0x46], ext: '.gif' }, // GIF + { bytes: [0x52, 0x49, 0x46, 0x46], ext: '.webp' } // WEBP (RIFF) +] + +const WECHAT_NON_EMOTICON_DIR_KEYWORDS = [ + 'avatar', + 'cache', + 'chatimg', + 'chatimage', + 'download', + 'filerecv', + 'head', + 'image', + 'photo', + 'picture', + 'screenshot', + 'temp', + 'thumbnail', + 'thumb', + 'video', + 'wallpaper' +] + +export interface WechatScanResult { + name: string + filePath: string + type: string +} + +export interface WechatDetectResult { + found: boolean + path: string + description: string +} + +function getFs() { + return (window as any).preload.fs +} + +function getHomePath(): string { + try { + return (window as any).ztools.getPath('home') + } catch { + return '' + } +} + +function getExt(filename: string): string { + const dot = filename.lastIndexOf('.') + return dot >= 0 ? filename.slice(dot).toLowerCase() : '' +} + +function getBaseName(filePath: string): string { + const parts = filePath.split(/[\\/]+/).filter(Boolean) + return parts[parts.length - 1] || filePath +} + +function normalizePathSegment(segment: string): string { + return segment.toLowerCase().replace(/[\s_-]/g, '') +} + +function isImageFile(filename: string): boolean { + return IMAGE_EXTENSIONS.has(getExt(filename)) +} + +function shouldSkipWechatDirectory(dirname: string): boolean { + const normalized = normalizePathSegment(dirname) + return WECHAT_NON_EMOTICON_DIR_KEYWORDS.some(keyword => normalized.includes(keyword)) +} + +function joinPath(basePath: string, ...segments: string[]): string { + const separator = basePath.includes('\\') ? '\\' : '/' + return [basePath.replace(/[\\/]+$/, ''), ...segments].join(separator) +} + +function getMimeType(filename: string): string { + return MIME_MAP[getExt(filename)] || 'image/png' +} + +/** + * Detect image type from file header bytes for extension-less files. + */ +function detectImageType(filePath: string): string | null { + try { + const fs = getFs() + const fd = fs.openSync(filePath, 'r') + const buffer = Buffer.alloc(12) + const bytesRead = fs.readSync(fd, buffer, 0, 12, 0) + fs.closeSync(fd) + + if (bytesRead < 4) return null + + for (const sig of FILE_SIGNATURES) { + let match = true + for (let i = 0; i < sig.bytes.length; i++) { + if (buffer[i] !== sig.bytes[i]) { + match = false + break + } + } + if (match) return MIME_MAP[sig.ext] || 'image/png' + } + + return null + } catch { + return null + } +} + +/** + * Scan a directory for image files (with or without extensions). + * For WeChat's CustomEmotions, files often have no extension. + */ +function scanFilesInDirectory(dirPath: string, depth = 0, maxDepth = 2): WechatScanResult[] { + if (depth > maxDepth) return [] + + const fs = getFs() + const results: WechatScanResult[] = [] + + try { + const entries = fs.readdirSync(dirPath) + for (const entry of entries) { + if (entry.startsWith('.')) continue + + const fullPath = joinPath(dirPath, entry) + try { + const stat = fs.statSync(fullPath) + if (stat.isDirectory()) { + if (shouldSkipWechatDirectory(entry)) continue + results.push(...scanFilesInDirectory(fullPath, depth + 1, maxDepth)) + } else if (stat.isFile()) { + const ext = getExt(entry) + if (ext && IMAGE_EXTENSIONS.has(ext)) { + // File with image extension + results.push({ + name: entry, + filePath: fullPath, + type: getMimeType(entry) + }) + } else if (!ext && stat.size > 100 && stat.size < 25 * 1024 * 1024) { + // Extensionless file - check if it's an image by header + const detectedType = detectImageType(fullPath) + if (detectedType) { + results.push({ + name: entry, + filePath: fullPath, + type: detectedType + }) + } + } + } + } catch { + // Skip files that can't be accessed + } + } + } catch { + // Directory not readable + } + + return results +} + +/** + * Find WeChat emoticon source paths under a base directory. + * Looks for: + * - CustomEmotions/ (custom emoticons, Windows style) + * - FileStorage/Stickers/ (sticker packs) + * - CustomStickers/ or customStickers/ (macOS style) + */ +function getWechatEmoticonSourcePaths(basePath: string): string[] { + const fs = getFs() + const results: string[] = [] + const seen = new Set() + + function addIfExists(dirPath: string) { + if (seen.has(dirPath)) return + try { + if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) { + seen.add(dirPath) + results.push(dirPath) + } + } catch { + // Skip inaccessible paths + } + } + + // Check if basePath itself is an emoticon directory + const baseName = getBaseName(basePath) + const normalizedBaseName = normalizePathSegment(baseName) + if (normalizedBaseName === 'customemotions' || normalizedBaseName === 'stickers' || normalizedBaseName === 'customstickers') { + addIfExists(basePath) + return results + } + + // Walk up to 4 levels to find emoticon directories + function walk(dirPath: string, depth = 0, maxDepth = 4) { + if (depth > maxDepth) return + + try { + const entries = fs.readdirSync(dirPath) + for (const entry of entries) { + if (entry.startsWith('.')) continue + + const fullPath = joinPath(dirPath, entry) + let isDirectory = false + try { + isDirectory = fs.statSync(fullPath).isDirectory() + } catch { + continue + } + if (!isDirectory) continue + + const normalized = normalizePathSegment(entry) + + // Found CustomEmotions directory + if (normalized === 'customemotions') { + addIfExists(fullPath) + continue + } + + // Found Stickers directory + if (normalized === 'stickers') { + addIfExists(fullPath) + continue + } + + // Found custom sticker directories + if (normalized === 'customstickers' || normalized === 'customizedstickers') { + addIfExists(fullPath) + continue + } + + // Skip non-emoticon directories + if (shouldSkipWechatDirectory(entry)) continue + + walk(fullPath, depth + 1, maxDepth) + } + } catch { + // Directory not readable + } + } + + walk(basePath) + return results +} + +/** + * Detect WeChat emoticon directories on the current system. + */ +export function detectWechatPath(): WechatDetectResult[] { + const results: WechatDetectResult[] = [] + const fs = getFs() + const home = getHomePath() + if (!home) return results + + const isWindows = home.includes('\\') || home.indexOf(':') === 1 + + if (!isWindows) { + // macOS WeChat paths + const macBase = `${home}/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat` + if (fs.existsSync(macBase)) { + try { + // WeChat stores data in version-named subdirectories + const entries = fs.readdirSync(macBase) + for (const entry of entries) { + if (entry.startsWith('.')) continue + const fullPath = joinPath(macBase, entry) + try { + if (!fs.statSync(fullPath).isDirectory()) continue + } catch { continue } + + const sourcePaths = getWechatEmoticonSourcePaths(fullPath) + for (const sourcePath of sourcePaths) { + const imageFiles = scanFilesInDirectory(sourcePath) + const dirLabel = getBaseName(sourcePath) + results.push({ + found: true, + path: sourcePath, + description: `微信 (${dirLabel}) - 发现 ${imageFiles.length} 张表情` + }) + } + } + } catch { /* skip */ } + } + + // macOS alternative path + const macAltBase = `${home}/Library/Application Support/com.tencent.xinWeChat` + if (fs.existsSync(macAltBase)) { + try { + const entries = fs.readdirSync(macAltBase) + for (const entry of entries) { + if (entry.startsWith('.')) continue + const fullPath = joinPath(macAltBase, entry) + try { + if (!fs.statSync(fullPath).isDirectory()) continue + } catch { continue } + + const sourcePaths = getWechatEmoticonSourcePaths(fullPath) + for (const sourcePath of sourcePaths) { + const imageFiles = scanFilesInDirectory(sourcePath) + const dirLabel = getBaseName(sourcePath) + results.push({ + found: true, + path: sourcePath, + description: `微信 (${dirLabel}) - 发现 ${imageFiles.length} 张表情` + }) + } + } + } catch { /* skip */ } + } + } else { + // Windows WeChat paths + const winPaths = [ + `${home}\\Documents\\WeChat Files`, + `${home}\\AppData\\Local\\Tencent\\WeChat` + ] + + for (const basePath of winPaths) { + if (!fs.existsSync(basePath)) continue + try { + const entries = fs.readdirSync(basePath) + for (const entry of entries) { + if (entry === 'All Users' || entry.startsWith('.')) continue + const subPath = joinPath(basePath, entry) + try { + if (!fs.statSync(subPath).isDirectory()) continue + } catch { continue } + + // Check for CustomEmotions (Windows custom emoticons) + const customEmotions = joinPath(subPath, 'CustomEmotions') + if (fs.existsSync(customEmotions)) { + const imageFiles = scanFilesInDirectory(customEmotions) + results.push({ + found: true, + path: customEmotions, + description: `微信 (${entry} 自定义表情) - 发现 ${imageFiles.length} 张表情` + }) + } + + // Check for FileStorage/Stickers + const fileStorageStickers = joinPath(subPath, 'FileStorage', 'Stickers') + if (fs.existsSync(fileStorageStickers)) { + const imageFiles = scanFilesInDirectory(fileStorageStickers) + results.push({ + found: true, + path: fileStorageStickers, + description: `微信 (${entry} 表情包) - 发现 ${imageFiles.length} 张表情` + }) + } + } + } catch { /* skip */ } + } + } + + return results +} + +/** + * Scan a directory for WeChat emoticon files and return results. + */ +export function scanWechatEmoticons(dirPath: string): WechatScanResult[] { + const scanned = scanFilesInDirectory(dirPath) + const seen = new Set() + return scanned.filter(item => { + if (seen.has(item.filePath)) return false + seen.add(item.filePath) + return true + }) +} + +/** + * Read a file from disk and convert to Blob. + */ +function readFileAsBlob(filePath: string, mimeType: string): Blob { + const fs = getFs() + const buffer = fs.readFileSync(filePath) + const uint8 = new Uint8Array(buffer) + return new Blob([uint8], { type: mimeType }) +} + +export function createWechatImportItem(item: WechatScanResult): { emoticon: Emoticon; file: Blob } | null { + try { + const blob = readFileAsBlob(item.filePath, item.type) + if (blob.size === 0) return null + + const now = Date.now() + const emoticon: Emoticon = { + id: nanoid(), + name: item.name, + url: '', + type: item.type, + tags: [], + favorite: false, + source: 'wechat', + createdAt: now, + createTime: now, + updateTime: now + } + + return { emoticon, file: blob } + } catch (err) { + console.warn(`Failed to read file: ${item.filePath}`, err) + return null + } +} diff --git a/plugins/huhabiaoqingbao/src/services/staticImages.ts b/plugins/huhabiaoqingbao/src/services/staticImages.ts new file mode 100644 index 000000000..4ceca9b87 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/services/staticImages.ts @@ -0,0 +1,68 @@ +import { resolveAssetUrl } from '@/config/assets' + +// 静态图片资源映射 - 包含头像和图标 +export const STATIC_IMAGES = { + '工具箱.png': 'images/工具箱.png', + '留言墙.png': 'images/留言墙.png', + 'HTML.png': 'images/HTML.png', + 'Markdown.png': 'images/Markdown.png', + 'huha-avatar.png': 'huha-avatar.png' +} as const + +// 图片类型定义 +export type StaticImageKey = keyof typeof STATIC_IMAGES + +/** + * 获取内置静态图片的访问地址 + * @param imageName 图片名称 + * @returns 插件内静态资源 URL + */ +export const getStaticImageUrl = (imageName: StaticImageKey): string => { + const assetPath = STATIC_IMAGES[imageName] + if (!assetPath) { + console.warn(`Static image not found: ${imageName}`) + return '' + } + return resolveAssetUrl(assetPath) +} + +/** + * 获取所有静态图片的URL映射 + * @returns 包含所有静态图片URL的对象 + */ +export const getAllStaticImageUrls = () => { + const urls: Record = {} + + Object.keys(STATIC_IMAGES).forEach(imageName => { + urls[imageName] = getStaticImageUrl(imageName as StaticImageKey) + }) + + return urls +} + +/** + * 预加载静态图片 + * @param imageNames 要预加载的图片名称数组,如果不提供则预加载所有图片 + */ +export const preloadStaticImages = async (imageNames?: StaticImageKey[]) => { + const imagesToLoad = imageNames || Object.keys(STATIC_IMAGES) as StaticImageKey[] + + const loadPromises = imagesToLoad.map(imageName => { + return new Promise((resolve, reject) => { + const img = new Image() + img.onload = () => resolve() + img.onerror = () => { + console.warn(`Failed to preload image: ${imageName}`) + resolve() // 即使失败也继续,不阻塞其他图片加载 + } + img.src = getStaticImageUrl(imageName) + }) + }) + + try { + await Promise.all(loadPromises) + console.log('Static images preloaded successfully') + } catch (error) { + console.error('Error preloading static images:', error) + } +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/services/storage.ts b/plugins/huhabiaoqingbao/src/services/storage.ts new file mode 100644 index 000000000..b15f47ecb --- /dev/null +++ b/plugins/huhabiaoqingbao/src/services/storage.ts @@ -0,0 +1,335 @@ +import { openDB } from 'idb'; +import type { Emoticon } from '@/types'; +import { fileSystemService } from '@/utils/fileSystem'; + +class StorageService { + private db: any = null; + private readonly DB_NAME = 'emoticon-store'; + private readonly DB_VERSION = 2; + + async init() { + if (this.db) return; + + try { + this.db = await openDB(this.DB_NAME, this.DB_VERSION, { + upgrade(db, oldVersion, newVersion) { + // 版本 1:创建初始存储 + if (oldVersion < 1) { + // 创建表情包存储 + if (!db.objectStoreNames.contains('emoticons')) { + db.createObjectStore('emoticons', { keyPath: 'id' }); + } + // 创建文件存储 + if (!db.objectStoreNames.contains('files')) { + db.createObjectStore('files'); + } + } + + // 版本 2:添加新的功能或修复(如果需要) + if (oldVersion < 2) { + // 这里可以添加版本 2 需要的数据库结构变更 + // 比如新增字段、索引等 + console.log('Upgrading database to version 2'); + } + }, + }); + + // 从文件系统恢复数据 + await this.restoreFromFileSystem(); + } catch (error) { + console.error('Failed to initialize database:', error); + throw error; + } + } + + async saveEmoticon(emoticon: Emoticon, file: Blob): Promise { + if (!this.db) await this.init(); + + const tx = this.db.transaction(['emoticons', 'files'], 'readwrite'); + + try { + // 保存到 IndexedDB + const sanitizedEmoticon = this.sanitizeEmoticon(emoticon); + await tx.objectStore('emoticons').put(sanitizedEmoticon); + await tx.objectStore('files').put(file, emoticon.id); + await tx.done; + + // 同步保存到文件系统 + await fileSystemService.saveEmoticonFile(emoticon.id, file); + await fileSystemService.upsertMetadata(sanitizedEmoticon); + + return this.withObjectUrl(sanitizedEmoticon, file); + } catch (error) { + this.abortTransaction(tx); + throw error; + } + } + + async saveEmoticons(items: { emoticon: Emoticon; file: Blob }[]): Promise { + if (!this.db) await this.init(); + if (items.length === 0) return []; + + const tx = this.db.transaction(['emoticons', 'files'], 'readwrite'); + + try { + const savedEmoticons = items.map(item => this.sanitizeEmoticon(item.emoticon)); + const emoticonsStore = tx.objectStore('emoticons'); + const filesStore = tx.objectStore('files'); + + await Promise.all(items.flatMap((item, index) => [ + emoticonsStore.put(savedEmoticons[index]), + filesStore.put(item.file, item.emoticon.id) + ])); + await tx.done; + + await fileSystemService.saveEmoticonFiles( + items.map(item => ({ id: item.emoticon.id, file: item.file })) + ); + await fileSystemService.saveMetadata(await this.getAllEmoticonMetadata()); + + return savedEmoticons.map((emoticon, index) => this.withObjectUrl(emoticon, items[index].file)); + } catch (error) { + this.abortTransaction(tx); + throw error; + } + } + + async getEmoticon(id: string): Promise { + if (!this.db) await this.init(); + + try { + const emoticon = await this.db.get('emoticons', id); + if (!emoticon) return null; + + const file = await this.db.get('files', id); + if (file) { + // 如果已经有 URL,先释放它 + if (emoticon.url?.startsWith('blob:')) { + URL.revokeObjectURL(emoticon.url); + } + emoticon.url = URL.createObjectURL(file); + } + + return emoticon; + } catch (error) { + console.error('Failed to get emoticon:', error); + return null; + } + } + + async getAllEmoticons(): Promise { + if (!this.db) await this.init(); + + try { + const emoticons = await this.db.getAll('emoticons'); + + // 为每个表情包获取文件并创建新的 URL + const results = await Promise.all( + emoticons.map(async (emoticon: Emoticon) => { + const file = await this.db.get('files', emoticon.id); + if (file) { + // 如果已经有 URL,先释放它 + if (emoticon.url?.startsWith('blob:')) { + URL.revokeObjectURL(emoticon.url); + } + // 创建新的 URL + emoticon.url = URL.createObjectURL(file); + } + return emoticon; + }) + ); + + return results; + } catch (error) { + console.error('Failed to get all emoticons:', error); + return []; + } + } + + async deleteEmoticon(id: string): Promise { + if (!this.db) await this.init(); + + const tx = this.db.transaction(['emoticons', 'files'], 'readwrite'); + + try { + await tx.objectStore('emoticons').delete(id); + await tx.objectStore('files').delete(id); + await tx.done; + + // 同时从文件系统删除 + await fileSystemService.deleteEmoticonFile(id); + await fileSystemService.removeMetadataByIds([id]); + } catch (error) { + console.error('Failed to delete emoticon:', error); + this.abortTransaction(tx); + throw error; + } + } + + async deleteEmoticons(ids: string[]): Promise { + if (!this.db) await this.init(); + if (ids.length === 0) return; + + const tx = this.db.transaction(['emoticons', 'files'], 'readwrite'); + + try { + const emoticonsStore = tx.objectStore('emoticons'); + const filesStore = tx.objectStore('files'); + + await Promise.all(ids.flatMap(id => [ + emoticonsStore.delete(id), + filesStore.delete(id) + ])); + await tx.done; + + await fileSystemService.deleteEmoticonFiles(ids); + await fileSystemService.removeMetadataByIds(ids); + } catch (error) { + console.error('Failed to delete emoticons:', error); + this.abortTransaction(tx); + throw error; + } + } + + // 清空全部表情包 + async clearAllEmoticons(): Promise { + if (!this.db) await this.init(); + + const tx = this.db.transaction(['emoticons', 'files'], 'readwrite'); + + try { + // 获取所有表情包ID用于清理文件系统 + const emoticons = await this.db.getAll('emoticons'); + + // 清空数据库 + await tx.objectStore('emoticons').clear(); + await tx.objectStore('files').clear(); + await tx.done; + + // 从文件系统删除所有表情包文件 + for (const emoticon of emoticons) { + try { + await fileSystemService.deleteEmoticonFile(emoticon.id); + } catch (err) { + console.warn(`Failed to delete file for emoticon ${emoticon.id}:`, err); + } + } + + // 清空文件系统的元数据 + await fileSystemService.saveMetadata([]); + } catch (error) { + console.error('Failed to clear all emoticons:', error); + this.abortTransaction(tx); + throw error; + } + } + + private sanitizeEmoticon(emoticon: Emoticon): Emoticon { + return { + id: emoticon.id, + name: emoticon.name, + url: emoticon.url?.startsWith('blob:') ? '' : emoticon.url, + type: emoticon.type, + favorite: emoticon.favorite, + source: emoticon.source || 'local', + createdAt: emoticon.createdAt, + createTime: emoticon.createTime, + updateTime: emoticon.updateTime, + tags: Array.from(emoticon.tags || []) + }; + } + + async updateEmoticon(emoticon: Emoticon): Promise { + if (!this.db) await this.init(); + + const tx = this.db.transaction('emoticons', 'readwrite'); + + try { + const sanitizedEmoticon = this.sanitizeEmoticon(emoticon); + await tx.objectStore('emoticons').put(sanitizedEmoticon); + await tx.done; + + // 同步更新文件系统的元数据 + await fileSystemService.upsertMetadata(sanitizedEmoticon); + } catch (error) { + this.abortTransaction(tx); + throw error; + } + } + + async updateEmoticons(emoticons: Emoticon[]): Promise { + if (!this.db) await this.init(); + if (emoticons.length === 0) return; + + const tx = this.db.transaction('emoticons', 'readwrite'); + + try { + const store = tx.objectStore('emoticons'); + const sanitizedEmoticons = emoticons.map(emoticon => this.sanitizeEmoticon(emoticon)); + + await Promise.all(sanitizedEmoticons.map(emoticon => store.put(emoticon))); + await tx.done; + + await fileSystemService.saveMetadata(await this.getAllEmoticonMetadata()); + } catch (error) { + this.abortTransaction(tx); + throw error; + } + } + + private abortTransaction(tx: { abort: () => void }) { + try { + tx.abort(); + } catch { + // The transaction may already be committed when the filesystem sync fails. + } + } + + private async getAllEmoticonMetadata(): Promise { + if (!this.db) await this.init(); + return this.db.getAll('emoticons'); + } + + private withObjectUrl(emoticon: Emoticon, file: Blob): Emoticon { + return { + ...emoticon, + url: URL.createObjectURL(file) + }; + } + + // 改进从文件系统恢复数据的方法 + private async restoreFromFileSystem() { + try { + const emoticons = await fileSystemService.readMetadata(); + if (!emoticons.length) return; + + // 逐个处理表情包,确保事务不会过早结束 + for (const emoticon of emoticons) { + try { + const file = await fileSystemService.readEmoticonFile(emoticon.id); + if (!file) continue; + + // 为每个表情包创建新的事务 + const tx = this.db.transaction(['emoticons', 'files'], 'readwrite'); + const emoticonsStore = tx.objectStore('emoticons'); + const filesStore = tx.objectStore('files'); + + // 保存文件和元数据 + await Promise.all([ + filesStore.put(file, emoticon.id), + emoticonsStore.put(emoticon) + ]); + + // 等待当前事务完成 + await tx.done; + } catch (err) { + console.error(`Failed to restore emoticon ${emoticon.id}:`, err); + } + } + } catch (error) { + console.error('Failed to restore from file system:', error); + } + } +} + +export const storageService = new StorageService(); diff --git a/plugins/huhabiaoqingbao/src/store/emoticon.ts b/plugins/huhabiaoqingbao/src/store/emoticon.ts new file mode 100644 index 000000000..154584b79 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/store/emoticon.ts @@ -0,0 +1,284 @@ +import { defineStore } from 'pinia' +import { ref, computed, toRaw } from 'vue' +import type { Emoticon } from '../types' +import type { Tag } from '../types/index' +import { storageService } from '@/services/storage' +import { fileSystemService } from '@/utils/fileSystem' +import { githubGistBackupService, type BackupRestoreItem } from '@/services/githubGistBackup' + +const EMOTICONS_DOC_ID = 'emoticons_list' +const EMOTICON_PREFIX = 'emoticon_' +const CUSTOM_TAGS_DOC_ID = 'custom_tags' + +interface EmoticonDoc { + _id: string + _rev?: string + data: Array<{ + id: string + name: string + tags: string[] + favorite: boolean + createdAt: number + }> +} + +interface EmoticonFileDoc { + _id: string + _rev?: string + type: string + data: number[] | Uint8Array // 支持两种类型 +} + +export const useEmoticonStore = defineStore('emoticon', () => { + const emoticons = ref([]) + const customTags = ref([]) + const allTags = ref<{ name: string; count: number }[]>([]) + const loading = ref(false) + const initialized = ref(false) + + // 计算属性 + const allEmoticons = computed(() => { + // 返回按时间倒序排序的表情包列表 + return [...emoticons.value].sort((a, b) => b.createdAt - a.createdAt) + }) + + const favoriteEmoticons = computed(() => + // 收藏的表情包也按时间倒序排列 + emoticons.value + .filter(e => e.favorite) + .sort((a, b) => b.createdAt - a.createdAt) + ) + + // 搜索方法 + const searchEmoticons = (query: string) => { + if (!query) return allEmoticons.value + const lowerQuery = query.toLowerCase() + return emoticons.value + .filter(e => + e.name.toLowerCase().includes(lowerQuery) || + e.tags.some(tag => tag.toLowerCase().includes(lowerQuery)) + ) + .sort((a, b) => b.createdAt - a.createdAt) + } + + // 按来源分组计数 + const sourceCount = computed(() => { + const counts: Record = { all: 0, local: 0, qq: 0, wechat: 0, feishu: 0 } + emoticons.value.forEach(e => { + const src = e.source || 'local' + if (counts[src] !== undefined) counts[src]++ + counts.all++ + }) + return counts + }) + + // 按来源筛选 + const filterBySource = (source: string): Emoticon[] => { + if (source === 'all') return allEmoticons.value + return emoticons.value + .filter(e => (e.source || 'local') === source) + .sort((a, b) => b.createdAt - a.createdAt) + } + + async function initializeStore() { + if (initialized.value) return + + loading.value = true + try { + // 初始化存储服务(这会自动从文件系统恢复数据) + await storageService.init() + // 加载所有表情包 + emoticons.value = await storageService.getAllEmoticons() + initialized.value = true + } catch (error) { + console.error('Failed to initialize emoticon store:', error) + throw error + } finally { + loading.value = false + } + } + + async function refreshEmoticons() { + try { + emoticons.value = await storageService.getAllEmoticons() + } catch (error) { + console.error('Failed to refresh emoticons:', error) + } + } + + async function addEmoticon(emoticon: Emoticon, file: Blob) { + try { + const savedEmoticon = await storageService.saveEmoticon(emoticon, file) + const index = emoticons.value.findIndex(item => item.id === savedEmoticon.id) + if (index >= 0) { + emoticons.value.splice(index, 1, savedEmoticon) + } else { + emoticons.value.push(savedEmoticon) + } + + githubGistBackupService + .autoBackupNewEmoticon(savedEmoticon, file, allEmoticons.value) + .catch(error => { + console.error('Automatic GitHub Gist backup failed:', error) + }) + } catch (error) { + console.error('Failed to add emoticon:', error) + throw error + } + } + + async function addEmoticons(items: { emoticon: Emoticon; file: Blob }[]) { + try { + const savedEmoticons = await storageService.saveEmoticons(items) + const existingIds = new Set(savedEmoticons.map(item => item.id)) + + emoticons.value = [ + ...emoticons.value.filter(item => !existingIds.has(item.id)), + ...savedEmoticons + ] + } catch (error) { + console.error('Failed to add emoticons:', error) + throw error + } + } + + async function toggleFavorite(emoticon: Emoticon): Promise { + try { + // 创建一个干净的对象副本,只包含需要的属性 + const updatedEmoticon = { + id: emoticon.id, + name: emoticon.name, + url: emoticon.url, + type: emoticon.type, + source: emoticon.source || 'local', + favorite: !emoticon.favorite, + createdAt: emoticon.createdAt, + createTime: emoticon.createTime, + updateTime: Date.now(), + tags: Array.from(emoticon.tags || []) + }; + + // 先更新数据库 + await storageService.updateEmoticon(updatedEmoticon); + + // 成功后更新状态 + const index = emoticons.value.findIndex(e => e.id === emoticon.id); + if (index !== -1) { + // 使用新对象替换旧对象,触发响应式更新 + emoticons.value.splice(index, 1, updatedEmoticon); + } + + // 返回更新后的对象 + return updatedEmoticon; + } catch (error) { + console.error('Failed to toggle favorite:', error); + throw error; + } + } + + async function deleteEmoticon(id: string) { + try { + await storageService.deleteEmoticon(id) + emoticons.value = emoticons.value.filter(e => e.id !== id) + } catch (error) { + console.error('Failed to delete emoticon:', error) + throw error + } + } + + async function deleteEmoticons(ids: string[]) { + try { + await storageService.deleteEmoticons(ids) + const idSet = new Set(ids) + emoticons.value = emoticons.value.filter(e => !idSet.has(e.id)) + } catch (error) { + console.error('Failed to delete emoticons:', error) + throw error + } + } + + // 清空全部表情包 + async function clearAllEmoticons() { + try { + await storageService.clearAllEmoticons() + // 清理所有URL + clearURLs() + // 清空本地状态 + emoticons.value = [] + } catch (error) { + console.error('Failed to clear all emoticons:', error) + throw error + } + } + + async function updateEmoticon(emoticon: Emoticon) { + try { + await storageService.updateEmoticon(emoticon) + const index = emoticons.value.findIndex(item => item.id === emoticon.id) + if (index >= 0) { + emoticons.value.splice(index, 1, emoticon) + } + } catch (error) { + console.error('Failed to update emoticon:', error) + throw error + } + } + + async function updateEmoticons(updatedEmoticons: Emoticon[]) { + try { + await storageService.updateEmoticons(updatedEmoticons) + const updatedById = new Map(updatedEmoticons.map(emoticon => [emoticon.id, emoticon])) + emoticons.value = emoticons.value.map(emoticon => updatedById.get(emoticon.id) || emoticon) + } catch (error) { + console.error('Failed to update emoticons:', error) + throw error + } + } + + async function importBackupEmoticons(items: BackupRestoreItem[]) { + try { + await addEmoticons(items) + } catch (error) { + console.error('Failed to import backup emoticons:', error) + throw error + } + } + + function clearURLs() { + emoticons.value.forEach(emoticon => { + if (emoticon.url?.startsWith('blob:')) { + URL.revokeObjectURL(emoticon.url) + } + }) + } + + return { + emoticons, + customTags, + allTags, + loading, + initialized, + allEmoticons, + favoriteEmoticons, + sourceCount, + searchEmoticons, + filterBySource, + initializeStore, + refreshEmoticons, + addEmoticon, + addEmoticons, + toggleFavorite, + deleteEmoticon, + deleteEmoticons, + clearAllEmoticons, + updateEmoticon, + updateEmoticons, + importBackupEmoticons, + clearURLs + } +}) + +// 工具函数:生成唯一ID +function generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2) +} diff --git a/plugins/huhabiaoqingbao/src/styles/common.scss b/plugins/huhabiaoqingbao/src/styles/common.scss new file mode 100644 index 000000000..a6fce3cd0 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/styles/common.scss @@ -0,0 +1,354 @@ +// 导入 Element Plus 样式覆盖 +@import './element-plus.scss'; + +:root { + color-scheme: light; + --app-bg: #f3f6fb; + --app-bg-elevated: #fbfdff; + --app-surface: rgba(255, 255, 255, 0.94); + --app-surface-secondary: #eef3f9; + --app-border: rgba(148, 163, 184, 0.24); + --app-text: #111827; + --app-text-secondary: #5b6472; + --app-shadow: 0 12px 32px rgba(15, 23, 42, 0.08); + + /* Workshop & shared component variables */ + --ws-bg: #f8f9fa; + --ws-panel-bg: #ffffff; + --ws-panel-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + --ws-preview-border: #409eff; + --ws-preview-bg: #f8f9fa; + --ws-empty-color: #9ca3af; + --ws-empty-hint: #6b7280; + --ws-tab-bg: #f1f3f4; + --ws-tab-color: #6b7280; + --ws-tab-hover-color: #374151; + --ws-tab-active-bg: #ffffff; + --ws-tab-active-color: #1f2937; + --ws-tab-active-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); + --ws-control-bg: #f8f9fa; + --ws-control-label: #374151; + --ws-input-border: #e5e7eb; + --ws-input-bg: #ffffff; + --ws-input-text: #374151; + --ws-input-placeholder: #9ca3af; + --ws-slider-track: #e5e7eb; + --ws-item-bg: #f8f9fa; + --ws-filter-select-border: #e5e7eb; + --ws-preset-hover-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + --ws-img-error-bg: #f5f5f5; + --ws-img-error-color: #999; +} + +html, +body, +#app { + margin: 0; + background: var(--app-bg); + color: var(--app-text); +} + +body { + transition: background-color 0.25s ease, color 0.25s ease; +} + +// 全局滚动条样式 +* { + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--el-border-color-lighter); + border-radius: 4px; + transition: background-color 0.3s ease; + + &:hover { + background-color: var(--el-border-color-darker); + } + } +} + +// 暗黑模式全局样式 +html.dark { + color-scheme: dark; + --app-bg: #0a0f19; + --app-bg-elevated: #131b2a; + --app-surface: rgba(17, 24, 39, 0.92); + --app-surface-secondary: #182233; + --app-border: rgba(148, 163, 184, 0.18); + --app-text: #f3f4f6; + --app-text-secondary: #9ca3af; + --app-shadow: 0 20px 44px rgba(0, 0, 0, 0.34); + --el-bg-color: #1a1a1a; + --el-bg-color-page: #0a0f19; + --el-text-color-primary: #ffffff; + --el-text-color-regular: #d1d5db; + --el-border-color: #333333; + --el-border-color-light: rgba(148, 163, 184, 0.18); + --el-border-color-lighter: rgba(148, 163, 184, 0.12); + --el-fill-color-blank: #111827; + --el-fill-color-light: #1f2937; + --el-fill-color-lighter: #273244; + --el-fill-color-dark: #0f172a; + --el-fill-color-darker: #0b1220; + + /* Workshop dark mode variables */ + --ws-bg: #0a0f19; + --ws-panel-bg: #131b2a; + --ws-panel-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); + --ws-preview-border: #2e6fef; + --ws-preview-bg: #1a2332; + --ws-empty-color: #6b7280; + --ws-empty-hint: #4b5563; + --ws-tab-bg: #1a2332; + --ws-tab-color: #9ca3af; + --ws-tab-hover-color: #e5e7eb; + --ws-tab-active-bg: #2a3a4e; + --ws-tab-active-color: #f3f4f6; + --ws-tab-active-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(46, 111, 239, 0.2); + --ws-control-bg: #1a2332; + --ws-control-label: #e5e7eb; + --ws-input-border: #2a3a4e; + --ws-input-bg: #1a2332; + --ws-input-text: #f3f4f6; + --ws-input-placeholder: #6b7280; + --ws-slider-track: #2a3a4e; + --ws-item-bg: #1a2332; + --ws-filter-select-border: #2a3a4e; + --ws-preset-hover-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + --ws-img-error-bg: #1a2332; + --ws-img-error-color: #6b7280; + + * { + &::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.2); + + &:hover { + background-color: rgba(255, 255, 255, 0.3); + } + } + } +} + +.online-search, +.emoticon-workshop, +.beauty-viewer, +.handsome-viewer, +.video-viewer, +.wallpaper-view { + color: var(--app-text); +} + +html.dark { + .online-search, + .emoticon-workshop, + .beauty-viewer, + .handsome-viewer, + .video-viewer, + .wallpaper-view { + background: transparent; + color: var(--app-text); + } + + // AppHeader 搜索框和分类筛选暗黑模式覆盖 + .custom-search-input { + background-color: rgba(30, 41, 59, 0.6) !important; + border-color: rgba(100, 116, 139, 0.4) !important; + + &:hover { + border-color: rgba(100, 116, 139, 0.6) !important; + background-color: rgba(30, 41, 59, 0.8) !important; + } + + &:focus-within { + border-color: #3b82f6 !important; + background-color: rgba(30, 41, 59, 0.9) !important; + } + + .search-icon { + color: #94a3b8 !important; + } + + .search-field { + color: #f1f5f9 !important; + + &::placeholder { + color: #64748b !important; + } + } + + .clear-btn { + color: #64748b !important; + + &:hover { + color: #94a3b8 !important; + background-color: rgba(100, 116, 139, 0.3) !important; + } + } + } + + .custom-select-btn { + background-color: rgba(30, 41, 59, 0.6) !important; + border-color: rgba(100, 116, 139, 0.4) !important; + color: #cbd5e1 !important; + + &:hover { + border-color: rgba(100, 116, 139, 0.6) !important; + background-color: rgba(30, 41, 59, 0.8) !important; + } + + &.active { + border-color: #3b82f6 !important; + background-color: rgba(30, 41, 59, 0.9) !important; + } + + .dropdown-icon { + color: #94a3b8 !important; + } + } + + .custom-dropdown { + background: rgba(30, 41, 59, 0.98) !important; + border-color: rgba(100, 116, 139, 0.4) !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important; + + .dropdown-item { + color: #cbd5e1 !important; + + &:hover { + background-color: rgba(51, 65, 85, 0.6) !important; + } + + &.manage-item { + color: #60a5fa !important; + + &:hover { + background-color: rgba(59, 130, 246, 0.2) !important; + } + } + } + + .dropdown-divider { + background-color: rgba(100, 116, 139, 0.4) !important; + } + } + + // 批量操作面板暗黑模式覆盖 + .batch-actions-panel { + background: linear-gradient(135deg, + rgba(30, 41, 59, 0.98) 0%, + rgba(51, 65, 85, 0.98) 50%, + rgba(71, 85, 105, 0.98) 100%) !important; + border-color: rgba(148, 163, 184, 0.4) !important; + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.4), + 0 8px 24px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.15) !important; + + .batch-header { + background: rgba(51, 65, 85, 0.8) !important; + border-bottom-color: rgba(148, 163, 184, 0.3) !important; + + .selection-text { + color: #f9fafb !important; + } + + .count-badge { + background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%) !important; + } + } + + .select-all-btn { + background: rgba(142, 142, 147, 0.15) !important; + color: #0A84FF !important; + + &:hover { + background: rgba(10, 132, 255, 0.12) !important; + } + + &.all-selected { + background: linear-gradient(135deg, #0A84FF 0%, #5E5CE6 100%) !important; + } + } + + .close-btn { + background: rgba(142, 142, 147, 0.2) !important; + color: #aeaeb2 !important; + + &:hover { + background: rgba(142, 142, 147, 0.3) !important; + } + } + + .divider { + background: linear-gradient(90deg, + transparent 0%, + rgba(255, 255, 255, 0.12) 20%, + rgba(255, 255, 255, 0.12) 80%, + transparent 100%) !important; + } + + .action-btn { + background: rgba(71, 85, 105, 0.6) !important; + border-color: rgba(100, 116, 139, 0.4) !important; + + &:hover { + background: rgba(71, 85, 105, 0.8) !important; + border-color: rgba(100, 116, 139, 0.6) !important; + } + + &.rename-btn .btn-text { color: #818CF8 !important; } + &.download-btn .btn-text { color: #30D158 !important; } + &.favorite-btn .btn-text { color: #FF9F0A !important; } + &.tag-btn .btn-text { color: #0A84FF !important; } + &.delete-btn .btn-text { color: #FF453A !important; } + } + } + + .emoticon-workshop .preset-images, + .emoticon-workshop .edit-area, + .emoticon-workshop .preview-window, + .wallpaper-view .wallpaper-header, + .wallpaper-view .modal-content, + .beauty-viewer .image-display, + .beauty-viewer .controls, + .beauty-viewer .history-panel, + .handsome-viewer .image-display, + .handsome-viewer .controls, + .handsome-viewer .history-panel, + .video-viewer .video-display, + .video-viewer .controls, + .video-viewer .history-panel { + background: var(--app-surface); + color: var(--app-text); + border-color: var(--app-border); + box-shadow: var(--app-shadow); + } + + .emoticon-workshop .preview-window, + .wallpaper-view .search-bar input, + .wallpaper-view .filters select { + background: var(--app-surface-secondary); + color: var(--app-text); + border-color: var(--app-border); + } + + .online-search .fixed-controls { + background: var(--app-surface); + border-color: var(--app-border); + box-shadow: var(--app-shadow); + } + + .online-search .search-input .el-input__wrapper { + background: linear-gradient(180deg, #1f2937 0%, #111827 100%) !important; + box-shadow: + 0 0 0 1px rgba(148, 163, 184, 0.22), + 0 12px 28px rgba(0, 0, 0, 0.28) !important; + } +} diff --git a/plugins/huhabiaoqingbao/src/styles/element-plus.scss b/plugins/huhabiaoqingbao/src/styles/element-plus.scss new file mode 100644 index 000000000..c63f0c712 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/styles/element-plus.scss @@ -0,0 +1,136 @@ +// Element Plus 全局样式覆盖 + +// 消息提示样式 +.el-message { + right: 20px !important; + bottom: 20px !important; + left: auto !important; + top: auto !important; + transform: none !important; + position: fixed !important; + min-width: 340px !important; + max-width: 500px !important; + padding: 16px 48px 16px 20px !important; + border-radius: 12px !important; + border: 1px solid #ebeef5 !important; + background: #ffffff !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important; + backdrop-filter: blur(8px) !important; + color: #303133 !important; + z-index: 999999 !important; + margin-bottom: 16px !important; + align-items: center !important; + + // 图标样式 + .el-message__icon { + font-size: 18px !important; + margin-right: 12px !important; + flex-shrink: 0 !important; + } + + // 内容样式 + .el-message__content { + font-size: 15px !important; + line-height: 1.5 !important; + color: #303133 !important; + font-weight: 500 !important; + min-width: 0 !important; + padding-right: 0 !important; + } + + .el-message__closeBtn { + position: absolute !important; + top: 50% !important; + right: 18px !important; + transform: translateY(-50%) !important; + color: #909399 !important; + font-size: 16px !important; + opacity: 1 !important; + + &:hover { + color: #606266 !important; + } + } + + // 成功消息样式 + &.el-message--success { + background: #ffffff !important; + border-color: #d8f3e7 !important; + + .el-message__icon { + color: #079d6e !important; + } + } + + // 错误消息样式 + &.el-message--error { + background: #ffffff !important; + border-color: #fde2e2 !important; + + .el-message__icon { + color: #e03737 !important; + } + } + + // 警告消息样式 + &.el-message--warning { + background: #ffffff !important; + border-color: #faecd8 !important; + + .el-message__icon { + color: #e6a23c !important; + } + } + + // 信息消息样式 + &.el-message--info { + background: #ffffff !important; + border-color: #d9ecff !important; + + .el-message__icon { + color: #2e6fef !important; + } + } +} + +// 暗黑模式适配 +html.dark { + .el-message { + background: #ffffff !important; + color: #303133 !important; + backdrop-filter: blur(12px) !important; + + // 所有消息在暗色模式下的通用样式 + .el-message__content { + color: #303133 !important; + font-weight: 500 !important; + } + + .el-message__closeBtn { + color: #909399 !important; + + &:hover { + color: #606266 !important; + } + } + } +} + +// 消息确认框样式 +.el-message-box { + right: 20px !important; + bottom: 20px !important; + left: auto !important; + top: auto !important; + transform: none !important; + position: fixed !important; + margin: 0 !important; + border-radius: 12px !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important; + backdrop-filter: blur(8px) !important; + + // 暗黑模式下的样式 + html.dark & { + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3) !important; + } +} diff --git a/plugins/huhabiaoqingbao/src/styles/performance.scss b/plugins/huhabiaoqingbao/src/styles/performance.scss new file mode 100644 index 000000000..9f1463e26 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/styles/performance.scss @@ -0,0 +1,124 @@ +// 全屏切换性能优化样式 +// 彻底解决全屏时的闪烁问题 + +/* 全局性能优化 */ +* { + // 在全屏切换时禁用所有过渡效果 + &.fullscreen-transition-disabled { + transition: none !important; + animation: none !important; + will-change: auto !important; + } +} + +/* 容器级别的性能优化 */ +.performance-optimized { + // 强制硬件加速 + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + + // 强制复合层 + will-change: transform; + contain: layout style paint; + isolation: isolate; + + // 避免重排重绘 + position: relative; + + /* 禁用子元素的过渡效果 */ + * { + transition: none !important; + animation: none !important; + } + + /* Grid容器特殊优化 */ + &.grid-container { + // 使用固定布局避免重计算 + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important; + + // 禁用响应式变化 + @media screen { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important; + } + } +} + +/* 全屏状态检测 */ +:fullscreen { + .emoticon-grid-container, + .emoticon-grid, + .emoticon-item { + transition: none !important; + animation: none !important; + will-change: auto !important; + } + + // 强制使用固定网格 + .emoticon-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important; + } +} + +/* 兼容Webkit全屏 */ +:-webkit-full-screen { + .emoticon-grid-container, + .emoticon-grid, + .emoticon-item { + transition: none !important; + animation: none !important; + will-change: auto !important; + } + + .emoticon-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important; + } +} + +/* 兼容Mozilla全屏 */ +:-moz-full-screen { + .emoticon-grid-container, + .emoticon-grid, + .emoticon-item { + transition: none !important; + animation: none !important; + will-change: auto !important; + } + + .emoticon-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important; + } +} + +/* 强制禁用动画的工具类 */ +.no-transition { + transition: none !important; + animation: none !important; + + * { + transition: none !important; + animation: none !important; + } +} + +/* GPU加速工具类 */ +.gpu-accelerated { + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + will-change: transform; + contain: layout style paint; +} + +/* 防止闪烁的容器类 */ +.flicker-resistant { + contain: strict; + isolation: isolate; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + + // 禁用所有子元素的过渡 + * { + transition: none !important; + animation-duration: 0s !important; + animation-delay: 0s !important; + } +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/types.ts b/plugins/huhabiaoqingbao/src/types.ts new file mode 100644 index 000000000..0d2276d9b --- /dev/null +++ b/plugins/huhabiaoqingbao/src/types.ts @@ -0,0 +1,30 @@ +export type EmoticonSource = 'local' | 'qq' | 'wechat' | 'feishu' + +export interface Emoticon { + id: string + name: string + url: string + type?: string + tags: string[] + favorite: boolean + source?: EmoticonSource + createdAt: number + createTime?: number + updateTime?: number +} + +export interface EmoticonStore { + allEmoticons: Emoticon[] + favoriteEmoticons: Emoticon[] + searchEmoticons: (query: string) => Emoticon[] + addEmoticon: (file: File) => Promise + toggleFavorite: (id: string) => Promise + addTag: (id: string, tag: string) => Promise + removeTag: (id: string, tag: string) => Promise + deleteEmoticon: (id: string) => Promise + addCustomTag: (tag: string) => Promise + customTags: string[] + allTags: { name: string; count: number }[] + sourceCount: Record + filterBySource: (source: string) => Emoticon[] +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/types/gifshot.d.ts b/plugins/huhabiaoqingbao/src/types/gifshot.d.ts new file mode 100644 index 000000000..66079e795 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/types/gifshot.d.ts @@ -0,0 +1,26 @@ +declare module 'gifshot' { + interface GifshotOptions { + images: string[] + gifWidth?: number + gifHeight?: number + interval?: number + numFrames?: number + loop?: number + progressCallback?: (progress: number) => void + } + + interface GifshotResult { + error: any + image: string + } + + interface Gifshot { + createGIF( + options: GifshotOptions, + callback: (result: GifshotResult) => void + ): void + } + + const gifshot: Gifshot + export default gifshot +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/types/index.ts b/plugins/huhabiaoqingbao/src/types/index.ts new file mode 100644 index 000000000..ee08ac4e5 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/types/index.ts @@ -0,0 +1,65 @@ +export type EmoticonSource = 'local' | 'qq' | 'wechat' | 'feishu' + +export interface Emoticon { + id: string + name: string + url: string + type: string + favorite: boolean + source?: EmoticonSource + createdAt: number + createTime: number + updateTime: number + tags: string[] +} + +export interface Tag { + name: string + count: number +} + +export interface SearchResult { + id: string + url: string + title: string + source: string + originalUrl?: string +} + +export type SearchSource = 'baidu' | 'bing' | 'sougou' | 'fabiaoqing' | 'dou' + +// 视频分类相关类型 +export interface VideoCategory { + id: string + name: string + description?: string + icon?: string +} + +// 视频API响应类型 +export interface VideoApiResponse { + code?: number + msg?: string + data?: { + video: string + } +} + +// 视频分类枚举 +export type VideoCategoryId = + | 'jk' // JK类型视频 + | 'YuMeng' // 欲梦视频 + | 'NvDa' // 女大视频 + | 'NvGao' // 女高视频 + | 'ReWu' // 热舞类型视频 + | 'QingCun' // 清纯类型视频 + | 'YuZu' // 玉足类型视频 + | 'SheJie' // 蛇姐类型视频 + | 'ChuanDa' // 穿搭类型视频 + | 'GaoZhiLiangXiaoJieJie' // 高质量小姐姐视频 + | 'HanFu' // 汉服类型视频 + | 'HeiSi' // 黑丝类型视频 + | 'BianZhuang' // 变装类型视频 + | 'LuoLi' // 萝莉类型视频 + | 'TianMei' // 甜妹类型视频 + | 'BaiSi' // 白丝类型视频 \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/types/search.ts b/plugins/huhabiaoqingbao/src/types/search.ts new file mode 100644 index 000000000..6fe41b2db --- /dev/null +++ b/plugins/huhabiaoqingbao/src/types/search.ts @@ -0,0 +1,32 @@ +export const ONLINE_SEARCH_PAGE_SIZE = 24 + +export type SearchSource = + | 'sougou' + | 'baidu' + | 'bing' + | 'gif' + | 'dou' + | 'apihz' + | 'adoutu' + | 'dogetu' + | 'pkdoutu' + | 'doutula' + | 'fabiaoqing' + | 'doutuba' + | 'doutuwang' + | 'qudoutu' + | 'dbbqb' + | 'randomgirl' + | 'randomboy' + | 'yaohud' + +export interface SearchResult { + id: string + url: string + originalUrl?: string + previewUrl?: string + thumbnailUrl?: string + title?: string + gifCandidate?: boolean + source: SearchSource +} diff --git a/plugins/huhabiaoqingbao/src/types/wallpaper.ts b/plugins/huhabiaoqingbao/src/types/wallpaper.ts new file mode 100644 index 000000000..332636fcf --- /dev/null +++ b/plugins/huhabiaoqingbao/src/types/wallpaper.ts @@ -0,0 +1,50 @@ +export interface WallpaperResponse { + data: Wallpaper[]; + meta: { + current_page: number; + last_page: number; + per_page: number; + total: number; + }; +} + +export interface Wallpaper { + id: string; + url: string; + short_url: string; + views: number; + favorites: number; + source: string; + purity: string; + category: string; + dimension_x: number; + dimension_y: number; + resolution: string; + ratio: string; + file_size: number; + file_type: string; + created_at: string; + colors: string[]; + path: string; + thumbs: { + large: string; + original: string; + small: string; + }; +} + +export interface WallpaperSearchParams { + q?: string; + categories?: string; + purity?: string; + sorting?: 'date_added' | 'relevance' | 'random' | 'views' | 'favorites' | 'toplist'; + order?: 'desc' | 'asc'; + topRange?: '1d' | '3d' | '1w' | '1M' | '3M' | '6M' | '1y'; + page?: number; + apikey?: string; +} + +export type WallpaperCategory = { + label: string + value: string +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/types/ztools.d.ts b/plugins/huhabiaoqingbao/src/types/ztools.d.ts new file mode 100644 index 000000000..67a450fa6 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/types/ztools.d.ts @@ -0,0 +1,56 @@ +interface ZToolsPluginEnterAction { + code: string + type: string + payload?: any + option?: Record +} + +interface ZToolsApi { + showNotification(body: string, clickFeatureCode?: string): void + copyText(text: string): boolean + copyFile(file: string | string[]): boolean + hideMainWindow(isRestorePreWindow?: boolean): boolean + showMainWindow(): boolean + outPlugin(isKill?: boolean): boolean + shellOpenExternal(url: string): void + shellShowItemInFolder(path: string): void + simulateKeyboardTap(key: string, ...modifiers: string[]): void + getPath(name: string): string + showOpenDialog(options: { + title?: string + defaultPath?: string + buttonLabel?: string + filters?: Array<{ name: string; extensions: string[] }> + properties?: string[] + }): string[] | undefined + onPluginEnter(callback: (action: ZToolsPluginEnterAction) => void): void + onPluginOut?(callback: (isKill: boolean) => void): void + isDev?(): boolean +} + +interface ZToolsPreload { + fs: { + readFile(path: string, encoding?: string): Promise + writeFile(path: string, data: any): Promise + unlink(path: string): Promise + existsSync(path: string): boolean + mkdirSync(path: string, options?: { recursive?: boolean }): void + readFileSync(path: string, encoding?: string): string | Uint8Array + } + utils: { + getDataPath(subPath: string): string + joinPath(...paths: string[]): string + getTempPath(fileName: string): string + } +} + +declare global { + interface Window { + ztools: ZToolsApi + preload: ZToolsPreload + } + + const ztools: ZToolsApi +} + +export {} diff --git a/plugins/huhabiaoqingbao/src/utils/animatedImage.ts b/plugins/huhabiaoqingbao/src/utils/animatedImage.ts new file mode 100644 index 000000000..ee7f6acbe --- /dev/null +++ b/plugins/huhabiaoqingbao/src/utils/animatedImage.ts @@ -0,0 +1,874 @@ +import axios from 'axios' + +const GIF_HEADER_87A = 'GIF87a' +const GIF_HEADER_89A = 'GIF89a' +const PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]) + +const readAscii = (bytes: Uint8Array, offset: number, length: number) => + String.fromCharCode(...bytes.slice(offset, offset + length)) + +const readUint32BE = (bytes: Uint8Array, offset: number) => + ((bytes[offset] << 24) >>> 0) + + (bytes[offset + 1] << 16) + + (bytes[offset + 2] << 8) + + bytes[offset + 3] + +const readUint32LE = (bytes: Uint8Array, offset: number) => + bytes[offset] + + (bytes[offset + 1] << 8) + + (bytes[offset + 2] << 16) + + ((bytes[offset + 3] << 24) >>> 0) + +const hasSignature = (bytes: Uint8Array, signature: Uint8Array) => { + if (bytes.length < signature.length) return false + + for (let index = 0; index < signature.length; index++) { + if (bytes[index] !== signature[index]) { + return false + } + } + + return true +} + +const skipSubBlocks = (bytes: Uint8Array, offset: number) => { + let cursor = offset + + while (cursor < bytes.length) { + const blockSize = bytes[cursor] + cursor += 1 + + if (blockSize === 0) { + return cursor + } + + cursor += blockSize + } + + return -1 +} + +const isAnimatedGif = (bytes: Uint8Array) => { + if (bytes.length < 13) return false + + const header = readAscii(bytes, 0, 6) + if (header !== GIF_HEADER_87A && header !== GIF_HEADER_89A) { + return false + } + + let offset = 13 + const globalColorTableFlag = (bytes[10] & 0x80) !== 0 + + if (globalColorTableFlag) { + const globalColorTableSize = 3 * (2 ** ((bytes[10] & 0x07) + 1)) + offset += globalColorTableSize + } + + let frameCount = 0 + + while (offset < bytes.length) { + const blockId = bytes[offset] + offset += 1 + + if (blockId === 0x3B) { + break + } + + if (blockId === 0x21) { + if (offset >= bytes.length) return false + + offset += 1 + offset = skipSubBlocks(bytes, offset) + if (offset === -1) return false + continue + } + + if (blockId === 0x2C) { + if (offset + 9 > bytes.length) return false + + frameCount += 1 + if (frameCount > 1) { + return true + } + + const packedField = bytes[offset + 8] + offset += 9 + + if ((packedField & 0x80) !== 0) { + const localColorTableSize = 3 * (2 ** ((packedField & 0x07) + 1)) + offset += localColorTableSize + } + + if (offset >= bytes.length) return false + + offset += 1 + offset = skipSubBlocks(bytes, offset) + if (offset === -1) return false + continue + } + + return false + } + + return false +} + +const isAnimatedPng = (bytes: Uint8Array) => { + if (!hasSignature(bytes, PNG_SIGNATURE)) { + return false + } + + let offset = PNG_SIGNATURE.length + + while (offset + 8 <= bytes.length) { + const chunkLength = readUint32BE(bytes, offset) + const chunkType = readAscii(bytes, offset + 4, 4) + const nextOffset = offset + 12 + chunkLength + + if (nextOffset > bytes.length) { + return false + } + + if (chunkType === 'acTL') { + return true + } + + if (chunkType === 'IEND') { + break + } + + offset = nextOffset + } + + return false +} + +const isAnimatedWebp = (bytes: Uint8Array) => { + if (bytes.length < 16) return false + if (readAscii(bytes, 0, 4) !== 'RIFF' || readAscii(bytes, 8, 4) !== 'WEBP') { + return false + } + + let offset = 12 + let hasAnimationFlag = false + + while (offset + 8 <= bytes.length) { + const chunkType = readAscii(bytes, offset, 4) + const chunkLength = readUint32LE(bytes, offset + 4) + + if (chunkType === 'ANIM') { + return true + } + + if (chunkType === 'VP8X' && offset + 9 <= bytes.length) { + hasAnimationFlag = (bytes[offset + 8] & 0x02) !== 0 + } + + offset += 8 + chunkLength + (chunkLength % 2) + } + + return hasAnimationFlag +} + +export const isAnimatedImageBuffer = (buffer: ArrayBuffer | Uint8Array, mimeType = '') => { + const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer) + const normalizedMimeType = mimeType.toLowerCase() + + if (!bytes.length) return false + + if (!normalizedMimeType || normalizedMimeType === 'image/gif') { + if (isAnimatedGif(bytes)) { + return true + } + } + + if (!normalizedMimeType || normalizedMimeType === 'image/png' || normalizedMimeType === 'image/apng') { + if (isAnimatedPng(bytes)) { + return true + } + } + + if (!normalizedMimeType || normalizedMimeType === 'image/webp') { + if (isAnimatedWebp(bytes)) { + return true + } + } + + return false +} + +// ==================== 智能缓存系统 ==================== + +interface CacheEntry { + result: boolean + timestamp: number + url: string +} + +const MEMORY_CACHE_MAX_SIZE = 500 +const PERSISTENT_CACHE_KEY = 'gif_validation_cache_v2' +const CACHE_TTL = 7 * 24 * 60 * 60 * 1000 // 7天 + +class ValidationCache { + // Map天然维护插入顺序,delete+set可实现O(1)的LRU更新 + private cache = new Map() + + constructor() { + this.loadFromStorage() + } + + private loadFromStorage() { + try { + const stored = localStorage.getItem(PERSISTENT_CACHE_KEY) + if (stored) { + const parsed = JSON.parse(stored) as Record + const now = Date.now() + + for (const [url, entry] of Object.entries(parsed)) { + if (now - entry.timestamp < CACHE_TTL) { + this.cache.set(url, entry) + } + } + + this.trimCache() + } + } catch { + // 忽略存储错误 + } + } + + private saveToStorage() { + try { + const obj: Record = {} + this.cache.forEach((entry, url) => { + obj[url] = entry + }) + localStorage.setItem(PERSISTENT_CACHE_KEY, JSON.stringify(obj)) + } catch { + // 忽略存储错误(可能是存储已满) + } + } + + private trimCache() { + // Map.keys() 按插入顺序返回,最早的在最前面 + while (this.cache.size > MEMORY_CACHE_MAX_SIZE) { + const oldest = this.cache.keys().next().value + if (oldest !== undefined) { + this.cache.delete(oldest) + } else { + break + } + } + } + + get(url: string): boolean | null { + const entry = this.cache.get(url) + if (!entry) return null + + // 检查是否过期 + if (Date.now() - entry.timestamp > CACHE_TTL) { + this.cache.delete(url) + return null + } + + // O(1) LRU更新:删除后重新插入,Map会将其移到末尾 + this.cache.delete(url) + this.cache.set(url, entry) + + return entry.result + } + + set(url: string, result: boolean) { + // O(1)更新:先删除旧的(如果有),再插入新的 + this.cache.delete(url) + this.cache.set(url, { + url, + result, + timestamp: Date.now() + }) + + this.trimCache() + this.debouncedSave() + } + + private saveTimeout: number | null = null + + private debouncedSave() { + if (this.saveTimeout) { + clearTimeout(this.saveTimeout) + } + this.saveTimeout = window.setTimeout(() => { + this.saveToStorage() + }, 1000) + } + + // 批量预加载缓存 + preload(urls: string[]): Record { + const result: Record = {} + for (const url of urls) { + const cached = this.get(url) + if (cached !== null) { + result[url] = cached + } + } + return result + } +} + +const validationCache = new ValidationCache() + +// ==================== 智能预检测 ==================== + +// 高置信度 GIF URL 模式 +const HIGH_CONFIDENCE_GIF_PATTERNS = [ + /\/gif\//i, + /\/animated\//i, + /\/animation\//i, + /_animated\./i, + /-animated\./i, + /_gif\./i, + /-gif\./i, +] + +// 中置信度 GIF URL 模式 +const MEDIUM_CONFIDENCE_GIF_PATTERNS = [ + /\.gif(?:\?.*)?$/i, + /giphy/i, + /tenor/i, + /media\.giphy/i, + /media1\.tenor/i, +] + +// 低置信度(可能是假 GIF) +const LOW_CONFIDENCE_PATTERNS = [ + /thumb/i, + /thumbnail/i, + /preview/i, + /small/i, + /mini/i, +] + +export interface GifConfidenceScore { + score: number // 0-100 + reason: string + needsValidation: boolean +} + +/** + * 智能评估 URL 为 GIF 的置信度 + * 分数 >= 80: 高置信度,可直接认为是 GIF + * 分数 >= 50: 中置信度,建议验证 + * 分数 < 50: 低置信度,可跳过或延迟验证 + */ +export const evaluateGifConfidence = (url: string): GifConfidenceScore => { + if (!url) { + return { score: 0, reason: '空URL', needsValidation: false } + } + + const lowerUrl = url.toLowerCase() + const isLikelyThumbnail = LOW_CONFIDENCE_PATTERNS.some(pattern => pattern.test(lowerUrl)) + + // 高置信度模式 + for (const pattern of HIGH_CONFIDENCE_GIF_PATTERNS) { + if (pattern.test(url)) { + return { score: 90, reason: '高置信度模式匹配', needsValidation: false } + } + } + + // 直接以 .gif 结尾且不像缩略图的资源,通常可以直接信任 + if (/\.gif(?:\?.*)?$/i.test(lowerUrl) && !isLikelyThumbnail) { + return { score: 85, reason: 'GIF扩展名明确', needsValidation: false } + } + + // 中置信度模式 + for (const pattern of MEDIUM_CONFIDENCE_GIF_PATTERNS) { + if (pattern.test(url)) { + return { score: 70, reason: '中置信度模式匹配', needsValidation: true } + } + } + + // 低置信度(缩略图等) + if (isLikelyThumbnail) { + return { score: 20, reason: '可能是缩略图', needsValidation: true } + } + + // 默认情况 + return { score: 40, reason: '无明确特征', needsValidation: true } +} + +// ==================== 优化的流式验证 ==================== + +const PROBE_BYTE_LIMIT = 128 * 1024 // 减少到 128KB,足以检测大多数 GIF +const STREAM_TIMEOUT = 3000 // 减少到 3 秒 + +const concatChunks = (chunks: Uint8Array[], totalLength: number) => { + const merged = new Uint8Array(totalLength) + let offset = 0 + + for (const chunk of chunks) { + merged.set(chunk, offset) + offset += chunk.length + } + + return merged +} + +// 快速 GIF 检测:只需要检测前几个数据块就能确定是否为动画 +const quickAnimatedGifCheck = (bytes: Uint8Array): boolean | null => { + if (bytes.length < 13) return null + + const header = readAscii(bytes, 0, 6) + if (header !== GIF_HEADER_87A && header !== GIF_HEADER_89A) { + return false + } + + let offset = 13 + const globalColorTableFlag = (bytes[10] & 0x80) !== 0 + + if (globalColorTableFlag) { + const globalColorTableSize = 3 * (2 ** ((bytes[10] & 0x07) + 1)) + offset += globalColorTableSize + } + + let frameCount = 0 + let hasGraphicControl = false + + while (offset < bytes.length && offset < 32768) { // 只检查前 32KB + if (offset >= bytes.length) return null + + const blockId = bytes[offset] + offset += 1 + + if (blockId === 0x3B) { + break + } + + if (blockId === 0x21) { + if (offset >= bytes.length) return null + + const label = bytes[offset] + offset += 1 + + // Graphic Control Extension + if (label === 0xF9) { + hasGraphicControl = true + } + + offset = skipSubBlocks(bytes, offset) + if (offset === -1) return null + continue + } + + if (blockId === 0x2C) { + if (offset + 9 > bytes.length) return null + + frameCount += 1 + if (frameCount > 1) { + return true + } + + const packedField = bytes[offset + 8] + offset += 9 + + if ((packedField & 0x80) !== 0) { + const localColorTableSize = 3 * (2 ** ((packedField & 0x07) + 1)) + offset += localColorTableSize + } + + if (offset >= bytes.length) return null + + offset += 1 // LZW minimum code size + offset = skipSubBlocks(bytes, offset) + if (offset === -1) return null + continue + } + + // 遇到未知块,可能是数据不足 + return null + } + + // 只找到一帧,但数据可能不完整 + return frameCount === 1 && !hasGraphicControl ? false : null +} + +const probeAnimatedImageByStreaming = async (url: string, timeout: number): Promise => { + if (typeof fetch === 'undefined' || typeof AbortController === 'undefined') { + return null + } + + const controller = new AbortController() + const timer = window.setTimeout(() => controller.abort(), timeout) + + try { + const response = await fetch(url, { + signal: controller.signal, + headers: { + 'Accept': 'image/*', + 'Range': 'bytes=0-131071' // 请求前 128KB + } + }) + + if (!response.ok) { + // 如果服务器不支持 Range,尝试普通请求 + if (response.status === 416) { + return null // 让上层用普通方式请求 + } + return false + } + + const contentType = String(response.headers.get('content-type') || '').split(';')[0].toLowerCase() + + if (!response.body) { + return null + } + + const reader = response.body.getReader() + const chunks: Uint8Array[] = [] + let totalLength = 0 + + while (totalLength < PROBE_BYTE_LIMIT) { + const { done, value } = await reader.read() + + if (done) { + const bytes = concatChunks(chunks, totalLength) + // 对于 GIF 使用快速检测 + if (!contentType || contentType === 'image/gif') { + const quickResult = quickAnimatedGifCheck(bytes) + if (quickResult !== null) return quickResult + } + return isAnimatedImageBuffer(bytes, contentType) + } + + if (!value?.length) { + continue + } + + chunks.push(value) + totalLength += value.length + + // 累积到一定数据就尝试快速检测 + if (totalLength >= 8192) { // 8KB 足以检测大多数 GIF + const bytes = concatChunks(chunks, totalLength) + + if (!contentType || contentType === 'image/gif') { + const quickResult = quickAnimatedGifCheck(bytes) + if (quickResult === true) { + controller.abort() + return true + } + // 如果快速检测不能确定,继续读取 + } else { + // 其他类型直接完整检测 + const result = isAnimatedImageBuffer(bytes, contentType) + if (result) { + controller.abort() + return true + } + } + } + } + + controller.abort() + return null + } catch (error) { + if ((error as Error).name === 'AbortError') { + return null + } + + return false + } finally { + window.clearTimeout(timer) + } +} + +// ==================== 主验证函数 ==================== + +const pendingValidations = new Map>() + +export const validateAnimatedImageUrl = async ( + url: string, + options: { timeout?: number; useCache?: boolean; skipLowConfidence?: boolean } = {} +): Promise => { + const normalizedUrl = url.trim() + const timeout = options.timeout ?? 4000 + const useCache = options.useCache ?? true + const skipLowConfidence = options.skipLowConfidence ?? false + + if (!normalizedUrl) { + return false + } + + // 1. 检查缓存 + if (useCache) { + const cachedResult = validationCache.get(normalizedUrl) + if (cachedResult !== null) { + return cachedResult + } + + // 检查进行中的验证 + const pending = pendingValidations.get(normalizedUrl) + if (pending) { + return pending + } + } + + // 2. 智能置信度评估 + const confidence = evaluateGifConfidence(normalizedUrl) + + // 高置信度直接返回,跳过网络请求 + if (confidence.score >= 80) { + if (useCache) { + validationCache.set(normalizedUrl, true) + } + return true + } + + // 低置信度且设置了跳过,直接返回 false + if (skipLowConfidence && confidence.score < 40) { + return false + } + + // 3. 执行验证 + const validationTask = (async () => { + try { + // 优先使用流式验证 + const quickProbeResult = await probeAnimatedImageByStreaming(normalizedUrl, STREAM_TIMEOUT) + if (quickProbeResult !== null) { + if (useCache) { + validationCache.set(normalizedUrl, quickProbeResult) + } + return quickProbeResult + } + + // 流式失败,使用 axios 完整请求 + const response = await axios.get(normalizedUrl, { + responseType: 'arraybuffer', + timeout, + headers: { + 'Accept': 'image/*' + }, + // 限制最大下载 256KB + maxContentLength: 256 * 1024, + maxBodyLength: 256 * 1024 + }) + + const contentType = String(response.headers['content-type'] || '').split(';')[0] + const result = isAnimatedImageBuffer(response.data, contentType) + + if (useCache) { + validationCache.set(normalizedUrl, result) + } + + return result + } catch { + // 验证失败,缓存为 false + if (useCache) { + validationCache.set(normalizedUrl, false) + } + return false + } finally { + // 清理进行中的验证 + pendingValidations.delete(normalizedUrl) + } + })() + + if (useCache) { + pendingValidations.set(normalizedUrl, validationTask) + } + + return validationTask +} + +// ==================== 批量验证优化 ==================== + +interface ValidationPriority { + url: string + priority: number + confidence: GifConfidenceScore +} + +/** + * 智能排序 URL 验证优先级 + * 优先验证高置信度的 URL,提高用户体验 + */ +export const prioritizeUrlsForValidation = (urls: string[]): ValidationPriority[] => { + return urls.map(url => { + const confidence = evaluateGifConfidence(url) + return { + url, + priority: confidence.score, + confidence + } + }).sort((a, b) => b.priority - a.priority) +} + +/** + * 批量验证,支持提前终止 + */ +export const validateUrlsWithEarlyTermination = async ( + urls: string[], + options: { + targetCount?: number + concurrency?: number + timeout?: number + onProgress?: (validated: number, found: number) => void + } = {} +): Promise => { + const { + targetCount = urls.length, + concurrency = 12, + timeout = 4000, + onProgress + } = options + + if (urls.length === 0) return [] + + const prioritized = prioritizeUrlsForValidation(urls) + const animatedUrls: string[] = [] + const processed = new Set() + + // 首先利用缓存快速返回已知结果 + for (const item of prioritized) { + const cached = validationCache.get(item.url) + if (cached === true) { + animatedUrls.push(item.url) + processed.add(item.url) + + if (animatedUrls.length >= targetCount) { + onProgress?.(processed.size, animatedUrls.length) + return animatedUrls + } + } else if (cached === false) { + processed.add(item.url) + } + } + + // 对剩余的需要验证的 URL 进行并发验证 + const toValidate = prioritized.filter(item => !processed.has(item.url)) + let completed = processed.size + + await new Promise((resolve) => { + let activeCount = 0 + let index = 0 + let resolved = false + + const checkComplete = () => { + if (resolved) return + if (animatedUrls.length >= targetCount || (activeCount === 0 && index >= toValidate.length)) { + resolved = true + resolve() + } + } + + const processNext = async () => { + if (resolved) return + if (index >= toValidate.length) { + checkComplete() + return + } + + const item = toValidate[index++] + activeCount++ + + try { + const isAnimated = await validateAnimatedImageUrl(item.url, { timeout }) + if (isAnimated && !resolved) { + animatedUrls.push(item.url) + } + } catch { + // 忽略错误 + } finally { + activeCount-- + completed++ + onProgress?.(completed, animatedUrls.length) + + // 检查是否达到目标 + if (animatedUrls.length >= targetCount) { + checkComplete() + } else { + processNext() + } + } + } + + // 启动并发 workers + const workers = Math.min(concurrency, toValidate.length) + for (let i = 0; i < workers; i++) { + processNext() + } + }) + + return animatedUrls +} + +// ==================== 工具函数 ==================== + +export const filterAsyncWithConcurrency = async ( + items: T[], + predicate: (item: T, index: number) => Promise, + concurrency: number = 12 +) => { + if (items.length === 0) { + return [] + } + + const results = new Array(items.length).fill(false) + let nextIndex = 0 + + const worker = async () => { + while (nextIndex < items.length) { + const currentIndex = nextIndex + nextIndex += 1 + results[currentIndex] = await predicate(items[currentIndex], currentIndex) + } + } + + await Promise.all( + Array.from({ length: Math.min(concurrency, items.length) }, () => worker()) + ) + + return items.filter((_, index) => results[index]) +} + +export const mapAsyncWithConcurrency = async ( + items: T[], + mapper: (item: T, index: number) => Promise, + concurrency: number = 12 +) => { + if (items.length === 0) { + return [] as R[] + } + + const results = new Array(items.length) + let nextIndex = 0 + + const worker = async () => { + while (nextIndex < items.length) { + const currentIndex = nextIndex + nextIndex += 1 + results[currentIndex] = await mapper(items[currentIndex], currentIndex) + } + } + + await Promise.all( + Array.from({ length: Math.min(concurrency, items.length) }, () => worker()) + ) + + return results +} + +/** + * 预加载缓存中的结果 + */ +export const preloadValidationCache = (urls: string[]): Record => { + return validationCache.preload(urls) +} + +/** + * 批量设置缓存(用于从搜索结果元数据直接标记) + */ +export const batchSetValidationCache = (entries: Array<{ url: string; isAnimated: boolean }>) => { + entries.forEach(({ url, isAnimated }) => { + validationCache.set(url, isAnimated) + }) +} diff --git a/plugins/huhabiaoqingbao/src/utils/domSafety.ts b/plugins/huhabiaoqingbao/src/utils/domSafety.ts new file mode 100644 index 000000000..274a25c50 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/utils/domSafety.ts @@ -0,0 +1,68 @@ +// DOM安全操作工具函数 + +/** + * 验证属性名是否安全 + * @param attributeName 属性名 + * @returns 是否安全 + */ +export const isValidAttributeName = (attributeName: string): boolean => { + // 属性名必须以字母开头,只能包含字母、数字、连字符、下划线 + const validPattern = /^[a-zA-Z][a-zA-Z0-9\-_]*$/ + return validPattern.test(attributeName) +} + +/** + * 安全地设置DOM属性 + * @param element DOM元素 + * @param attributeName 属性名 + * @param value 属性值 + * @returns 是否设置成功 + */ +export const safeSetAttribute = ( + element: Element, + attributeName: string, + value: string +): boolean => { + try { + // 验证属性名 + if (!isValidAttributeName(attributeName)) { + console.warn(`Invalid attribute name: ${attributeName}`) + return false + } + + // 设置属性 + element.setAttribute(attributeName, value) + return true + } catch (error) { + console.error('Failed to set attribute:', error) + return false + } +} + +/** + * 生成安全的随机ID + * @param prefix 前缀(可选) + * @returns 安全的ID字符串 + */ +export const generateSafeId = (prefix = 'id'): string => { + // 确保前缀以字母开头 + const safePrefix = /^[a-zA-Z]/.test(prefix) ? prefix : 'id' + + // 生成只包含有效字符的随机字符串 + const timestamp = Date.now().toString() + const randomPart = Math.random() + .toString(36) + .replace(/[^a-z0-9]/g, '') + .substring(2, 11) + + return `${safePrefix}${timestamp}${randomPart}` +} + +/** + * 清理字符串,移除不安全的字符 + * @param str 输入字符串 + * @returns 清理后的安全字符串 + */ +export const sanitizeString = (str: string): string => { + return str.replace(/[^a-zA-Z0-9\-_]/g, '') +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/utils/fileSystem.ts b/plugins/huhabiaoqingbao/src/utils/fileSystem.ts new file mode 100644 index 000000000..54552ea18 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/utils/fileSystem.ts @@ -0,0 +1,299 @@ +import { Emoticon } from '@/types' + +export class FileSystemService { + private readonly BASE_DIR = 'emoticons' + private isZToolsEnvironment: boolean + + constructor() { + // 检查是否在 ZTools 环境中 + this.isZToolsEnvironment = this.checkZToolsEnvironment() + + // 只在 ZTools 环境中初始化文件系统 + if (this.isZToolsEnvironment) { + this.ensureBaseDir() + } + } + + // 检查是否在 ZTools 环境中 + private checkZToolsEnvironment(): boolean { + return typeof window !== 'undefined' && + window.preload && + window.preload.fs && + window.preload.utils && + typeof window.preload.utils.getDataPath === 'function' + } + + // 确保基础目录存在 + private ensureBaseDir(): void { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return + } + + try { + const basePath = this.getBasePath() + if (!window.preload?.fs.existsSync(basePath)) { + window.preload?.fs.mkdirSync(basePath, { recursive: true }) + } + + // 确保元数据文件存在 + const metadataPath = this.getMetadataPath() + if (!window.preload?.fs.existsSync(metadataPath)) { + window.preload?.fs.writeFile(metadataPath, '[]') + } + + // 确保设置文件存在 + const settingsPath = this.getSettingsPath() + if (!window.preload?.fs.existsSync(settingsPath)) { + window.preload?.fs.writeFile(settingsPath, '{}') + } + } catch (error) { + console.error('Failed to ensure base directory:', error) + throw new Error('Failed to initialize file system service') + } + } + + // 获取基础路径 + private getBasePath(): string { + if (!this.isZToolsEnvironment) { + throw new Error('File system operations are only available in ZTools environment') + } + return window.preload.utils.getDataPath(this.BASE_DIR) + } + + // 获取表情包文件路径 + private getEmoticonPath(id: string): string { + if (!this.isZToolsEnvironment) { + throw new Error('File system operations are only available in ZTools environment') + } + return window.preload.utils.joinPath(this.getBasePath(), `${id}.dat`) + } + + // 获取元数据文件路径 + private getMetadataPath(): string { + if (!this.isZToolsEnvironment) { + throw new Error('File system operations are only available in ZTools environment') + } + return window.preload.utils.joinPath(this.getBasePath(), 'metadata.json') + } + + // 获取设置文件路径 + private getSettingsPath(): string { + if (!this.isZToolsEnvironment) { + throw new Error('File system operations are only available in ZTools environment') + } + return window.preload.utils.joinPath(this.getBasePath(), 'settings.json') + } + + // 公共方法:获取设置文件路径 + getSettingsFilePath(): string { + return this.getSettingsPath() + } + + // 保存表情包文件 + async saveEmoticonFile(id: string, file: Blob): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return + } + + try { + const buffer = await file.arrayBuffer() + const path = this.getEmoticonPath(id) + await window.preload?.fs.writeFile(path, new Uint8Array(buffer)) + } catch (error) { + console.error('Failed to save emoticon file:', error) + throw error + } + } + + async saveEmoticonFiles(items: { id: string; file: Blob }[], concurrency = 12): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return + } + + const workers = Array.from({ length: Math.min(concurrency, items.length) }, async (_, workerIndex) => { + for (let index = workerIndex; index < items.length; index += concurrency) { + const item = items[index] + await this.saveEmoticonFile(item.id, item.file) + } + }) + + await Promise.all(workers) + } + + // 读取表情包文件 + async readEmoticonFile(id: string): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return null + } + + try { + const path = this.getEmoticonPath(id) + const buffer = await window.preload?.fs.readFile(path) + return new Blob([buffer]) + } catch { + return null + } + } + + // 保存元数据 + async saveMetadata(emoticons: Emoticon[]): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return + } + + try { + const metadata = emoticons.map(e => ({ + ...e, + url: undefined // 不保存 URL,因为它是临时的 + })) + await window.preload?.fs.writeFile( + this.getMetadataPath(), + JSON.stringify(metadata) + ) + } catch (error) { + console.error('Failed to save metadata:', error) + throw error + } + } + + async upsertMetadata(emoticon: Emoticon): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return + } + + const metadata = await this.readMetadata() + const nextItem = { + ...emoticon, + url: undefined + } + const existingIndex = metadata.findIndex(item => item.id === emoticon.id) + + if (existingIndex >= 0) { + metadata.splice(existingIndex, 1, nextItem) + } else { + metadata.push(nextItem) + } + + await window.preload?.fs.writeFile( + this.getMetadataPath(), + JSON.stringify(metadata) + ) + } + + async removeMetadataByIds(ids: string[]): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return + } + + const idSet = new Set(ids) + const metadata = await this.readMetadata() + await window.preload?.fs.writeFile( + this.getMetadataPath(), + JSON.stringify(metadata.filter(item => !idSet.has(item.id))) + ) + } + + // 读取元数据 + async readMetadata(): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return [] + } + + try { + const content = await window.preload?.fs.readFile(this.getMetadataPath(), 'utf8') + return JSON.parse(content as string) + } catch { + return [] + } + } + + // 删除表情包文件 + async deleteEmoticonFile(id: string): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return + } + + try { + const path = this.getEmoticonPath(id) + await window.preload?.fs.unlink(path) + } catch { + // 忽略文件不存在的错误 + } + } + + async deleteEmoticonFiles(ids: string[], concurrency = 24): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return + } + + const workers = Array.from({ length: Math.min(concurrency, ids.length) }, async (_, workerIndex) => { + for (let index = workerIndex; index < ids.length; index += concurrency) { + await this.deleteEmoticonFile(ids[index]) + } + }) + + await Promise.all(workers) + } + + // 保存设置 + async saveSettings(settings: Record): Promise { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return + } + + try { + const settingsPath = this.getSettingsPath() + + // 确保目录存在 + this.ensureBaseDir() + + await window.preload?.fs.writeFile( + settingsPath, + JSON.stringify(settings, null, 2) + ) + } catch (error) { + console.error('Failed to save settings:', error) + throw error + } + } + + // 读取设置 + async readSettings(): Promise> { + if (!this.isZToolsEnvironment) { + console.warn('File system operations are only available in ZTools environment') + return {} + } + + try { + const settingsPath = this.getSettingsPath() + + if (!window.preload?.fs.existsSync(settingsPath)) { + return {} + } + + const content = await window.preload?.fs.readFile(settingsPath, 'utf8') + return JSON.parse(content as string) + } catch (error) { + console.error('Failed to read settings:', error) + return {} + } + } + + // 检查是否在 ZTools 环境中(公共方法) + isInZToolsEnvironment(): boolean { + return this.isZToolsEnvironment + } +} + +export const fileSystemService = new FileSystemService() diff --git a/plugins/huhabiaoqingbao/src/utils/presetImages.ts b/plugins/huhabiaoqingbao/src/utils/presetImages.ts new file mode 100644 index 000000000..2678f1a1c --- /dev/null +++ b/plugins/huhabiaoqingbao/src/utils/presetImages.ts @@ -0,0 +1,47 @@ +import { resolveAssetUrl } from '@/config/assets' + +interface PresetImage { + url: string + name: string + category: string +} + +const PRESET_CATEGORIES = ['funny', 'animal', 'face', 'cute'] + +const fetchPresetImageList = async (category: string): Promise => { + try { + const response = await fetch(resolveAssetUrl(`preset-images/${category}/index.json`)) + if (!response.ok) { + throw new Error(`Failed to fetch index.json for ${category}: ${response.statusText}`) + } + + const files = await response.json() + return Array.isArray(files) ? files : [] + } catch (error) { + console.error(`Failed to fetch image list for category ${category}:`, error) + return [] + } +} + +// 获取预设图片列表 +export const getPresetImages = async () => { + const images: Array = [] + + for (const category of PRESET_CATEGORIES) { + const files = await fetchPresetImageList(category) + + files.forEach((filename: string) => { + images.push({ + url: resolveAssetUrl(`preset-images/${category}/${filename}`), + name: filename.replace(/\.[^/.]+$/, ''), + category + }) + }) + } + + if (images.length === 0) { + console.error('No preset images loaded') + } + + return images +} diff --git a/plugins/huhabiaoqingbao/src/utils/storage.ts b/plugins/huhabiaoqingbao/src/utils/storage.ts new file mode 100644 index 000000000..7ade81388 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/utils/storage.ts @@ -0,0 +1,163 @@ +import { fileSystemService } from './fileSystem' + +// 本地存储的key常量 +export const STORAGE_KEYS = { + DARK_MODE: 'darkMode', + THEME_MODE: 'themeMode', + APP_LANGUAGE: 'appLanguage', + SIDEBAR_STATE: 'sidebarState', + THEME_COLOR: 'themeColor', + GITHUB_GIST_BACKUP: 'githubGistBackup', + BAIDU_TRANSLATE: 'baiduTranslateCredentials', + COZE_AI: 'cozeAiCredentials' +} as const + +// 预定义主题颜色配置 +export const THEME_COLORS = { + blue: { + name: '静谧蓝', + primary: '#5B9BD5', + primaryLight: '#8BB8E8', + primaryDark: '#4A7BA7' + }, + green: { + name: '清新绿', + primary: '#42be77', + primaryLight: '#6FD199', + primaryDark: '#359A5F' + }, + purple: { + name: '优雅紫', + primary: '#A569BD', + primaryLight: '#C39BD3', + primaryDark: '#8E44AD' + }, + orange: { + name: '温暖橙', + primary: '#F39C12', + primaryLight: '#F8C471', + primaryDark: '#D68910' + }, + red: { + name: '柔和红', + primary: '#E74C3C', + primaryLight: '#EC7063', + primaryDark: '#C0392B' + }, + cyan: { + name: '清澈青', + primary: '#48C9B0', + primaryLight: '#76D7C4', + primaryDark: '#17A2B8' + }, + indigo: { + name: '深邃靛', + primary: '#5D6D7E', + primaryLight: '#85929E', + primaryDark: '#34495E' + }, + pink: { + name: '甜美粉', + primary: '#F1948A', + primaryLight: '#F5B7B1', + primaryDark: '#CD6155' + } +} as const + +export type ThemeColorKey = keyof typeof THEME_COLORS +export type ThemeMode = 'system' | 'light' | 'dark' +export type AppLanguage = 'zh-CN' | 'en-US' + +// 设置缓存,避免频繁读取文件 +let settingsCache: Record | null = null +let cacheInitialized = false + +// 初始化设置缓存 +async function initCache(): Promise { + if (cacheInitialized) return + + try { + settingsCache = await fileSystemService.readSettings() + cacheInitialized = true + } catch (error) { + console.error('Failed to initialize settings cache:', error) + settingsCache = {} + cacheInitialized = true + } +} + +// 获取本地存储的值 +export async function getStorageItem(key: string, defaultValue: T): Promise { + await initCache() + + if (settingsCache && settingsCache.hasOwnProperty(key)) { + return settingsCache[key] as T + } + return defaultValue +} + +// 设置本地存储的值 +export async function setStorageItem(key: string, value: T): Promise { + await initCache() + + if (!settingsCache) { + settingsCache = {} + } + + settingsCache[key] = value + + try { + await fileSystemService.saveSettings(settingsCache) + } catch (error) { + console.error('Failed to save settings:', error) + throw error + } +} + +// 同步版本的函数,用于向后兼容 +export function getStorageItemSync(key: string, defaultValue: T): T { + // 先尝试从缓存读取 + if (settingsCache && settingsCache.hasOwnProperty(key)) { + return settingsCache[key] as T + } + + // 如果缓存未初始化,尝试同步初始化(仅在 ZTools 环境中) + if (!cacheInitialized && fileSystemService.isInZToolsEnvironment()) { + try { + // 尝试同步读取设置文件 + const settingsPath = fileSystemService.getSettingsFilePath() + if (window.preload?.fs.existsSync(settingsPath)) { + const content = window.preload.fs.readFileSync(settingsPath, 'utf8') + settingsCache = JSON.parse(content as string) + cacheInitialized = true + + if (settingsCache && settingsCache.hasOwnProperty(key)) { + return settingsCache[key] as T + } + } + } catch (error) { + console.error('Failed to sync read settings:', error) + } + } + + // 最后降级到 localStorage + const item = localStorage.getItem(key) + return item ? JSON.parse(item) : defaultValue +} + +// 同步版本的设置函数 +export function setStorageItemSync(key: string, value: T): void { + // 同时更新缓存和文件 + if (!settingsCache) { + settingsCache = {} + } + settingsCache[key] = value + + // 异步保存到文件系统 + fileSystemService.saveSettings(settingsCache).catch(error => { + console.error('Failed to save settings (sync):', error) + }) + + // 同时保存到 localStorage 作为降级 + localStorage.setItem(key, JSON.stringify(value)) +} diff --git a/plugins/huhabiaoqingbao/src/utils/theme.ts b/plugins/huhabiaoqingbao/src/utils/theme.ts new file mode 100644 index 000000000..38f2cdb26 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/utils/theme.ts @@ -0,0 +1,138 @@ +import { THEME_COLORS, type ThemeColorKey } from './storage' + +/** + * 主题系统工具类 + */ +export class ThemeSystem { + private static instance: ThemeSystem + private currentTheme: ThemeColorKey = 'blue' + + private constructor() {} + + static getInstance(): ThemeSystem { + if (!ThemeSystem.instance) { + ThemeSystem.instance = new ThemeSystem() + } + return ThemeSystem.instance + } + + /** + * 应用主题颜色 + */ + applyTheme(themeKey: ThemeColorKey): void { + this.currentTheme = themeKey + const theme = THEME_COLORS[themeKey] + + if (!theme) { + console.warn(`Theme ${themeKey} not found, falling back to blue`) + this.applyTheme('blue') + return + } + + // 设置 CSS 自定义属性 + const root = document.documentElement + + // Element Plus 主题颜色变量 + root.style.setProperty('--el-color-primary', theme.primary) + root.style.setProperty('--el-color-primary-light-1', this.lighten(theme.primary, 0.1)) + root.style.setProperty('--el-color-primary-light-2', this.lighten(theme.primary, 0.2)) + root.style.setProperty('--el-color-primary-light-3', this.lighten(theme.primary, 0.3)) + root.style.setProperty('--el-color-primary-light-4', this.lighten(theme.primary, 0.4)) + root.style.setProperty('--el-color-primary-light-5', this.lighten(theme.primary, 0.5)) + root.style.setProperty('--el-color-primary-light-6', this.lighten(theme.primary, 0.6)) + root.style.setProperty('--el-color-primary-light-7', this.lighten(theme.primary, 0.7)) + root.style.setProperty('--el-color-primary-light-8', this.lighten(theme.primary, 0.8)) + root.style.setProperty('--el-color-primary-light-9', this.lighten(theme.primary, 0.9)) + + // 深色变体 + root.style.setProperty('--el-color-primary-dark-1', this.darken(theme.primary, 0.1)) + root.style.setProperty('--el-color-primary-dark-2', this.darken(theme.primary, 0.2)) + + // 自定义主题变量 + root.style.setProperty('--theme-primary', theme.primary) + root.style.setProperty('--theme-primary-light', theme.primaryLight) + root.style.setProperty('--theme-primary-dark', theme.primaryDark) + + // 为应用组件设置数据属性 + root.setAttribute('data-theme-color', themeKey) + } + + /** + * 获取当前主题 + */ + getCurrentTheme(): ThemeColorKey { + return this.currentTheme + } + + /** + * 获取当前主题配置 + */ + getCurrentThemeConfig() { + return THEME_COLORS[this.currentTheme] + } + + /** + * 颜色变亮 + */ + private lighten(color: string, amount: number): string { + return this.adjustBrightness(color, amount) + } + + /** + * 颜色变暗 + */ + private darken(color: string, amount: number): string { + return this.adjustBrightness(color, -amount) + } + + /** + * 调整颜色亮度 + */ + private adjustBrightness(color: string, amount: number): string { + // 移除 # 前缀 + const hex = color.replace('#', '') + + // 转换为 RGB + const num = parseInt(hex, 16) + const r = (num >> 16) & 255 + const g = (num >> 8) & 255 + const b = num & 255 + + // 计算新的颜色值 + const newR = Math.max(0, Math.min(255, Math.round(r + (255 - r) * amount))) + const newG = Math.max(0, Math.min(255, Math.round(g + (255 - g) * amount))) + const newB = Math.max(0, Math.min(255, Math.round(b + (255 - b) * amount))) + + // 转换回十六进制 + const newHex = ((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, '0') + return `#${newHex}` + } + + /** + * 重置为默认主题 + */ + resetToDefault(): void { + this.applyTheme('blue') + } + + /** + * 获取所有可用主题 + */ + getAllThemes() { + return Object.entries(THEME_COLORS).map(([key, config]) => ({ + key: key as ThemeColorKey, + ...config + })) + } +} + +// 导出单例实例 +export const themeSystem = ThemeSystem.getInstance() + +// 导出便捷函数 +export const applyTheme = (themeKey: ThemeColorKey) => themeSystem.applyTheme(themeKey) +export const getCurrentTheme = () => themeSystem.getCurrentTheme() +export const getCurrentThemeConfig = () => themeSystem.getCurrentThemeConfig() +export const resetTheme = () => themeSystem.resetToDefault() + + diff --git a/plugins/huhabiaoqingbao/src/views/Settings.vue b/plugins/huhabiaoqingbao/src/views/Settings.vue new file mode 100644 index 000000000..6e2542410 --- /dev/null +++ b/plugins/huhabiaoqingbao/src/views/Settings.vue @@ -0,0 +1,454 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/src/views/WallpaperView.vue b/plugins/huhabiaoqingbao/src/views/WallpaperView.vue new file mode 100644 index 000000000..a0f783aee --- /dev/null +++ b/plugins/huhabiaoqingbao/src/views/WallpaperView.vue @@ -0,0 +1,760 @@ + + + + + \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/tsconfig.json b/plugins/huhabiaoqingbao/tsconfig.json new file mode 100644 index 000000000..695279784 --- /dev/null +++ b/plugins/huhabiaoqingbao/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "node", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path Alias */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue", + "src/types/**/*.d.ts" + ], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/tsconfig.node.json b/plugins/huhabiaoqingbao/tsconfig.node.json new file mode 100644 index 000000000..862dfb2b3 --- /dev/null +++ b/plugins/huhabiaoqingbao/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/plugins/huhabiaoqingbao/vite.config.ts b/plugins/huhabiaoqingbao/vite.config.ts new file mode 100644 index 000000000..dc2c96508 --- /dev/null +++ b/plugins/huhabiaoqingbao/vite.config.ts @@ -0,0 +1,99 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + base: './', // 使用相对路径 + build: { + outDir: 'dist', + assetsDir: 'assets', + minify: true, + sourcemap: false, + target: 'es2015', // 确保兼容性 + cssTarget: 'chrome61', // CSS 兼容性 + rollupOptions: { + external: ['remixicon/fonts/remixicon.woff2', 'remixicon/fonts/remixicon.woff', 'remixicon/fonts/remixicon.ttf', 'remixicon/fonts/remixicon.eot'], + input: { + main: resolve(__dirname, 'index.html') + }, + output: { + // 确保生成的文件名不包含 hash,ZTools 需要固定文件名 + chunkFileNames: 'assets/js/[name].js', + entryFileNames: 'assets/js/[name].js', + assetFileNames: ({name}) => { + if (/\.(gif|jpe?g|png|svg)$/.test(name ?? '')) { + return 'assets/images/[name][extname]'; + } + if (/\.css$/.test(name ?? '')) { + return 'assets/css/[name][extname]'; + } + if (/\.(woff2?|eot|ttf|otf)$/.test(name ?? '')) { + return 'assets/fonts/[name][extname]'; + } + return 'assets/[name][extname]'; + } + } + } + }, + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, + server: { + proxy: { + '/api/baidu': { + target: 'https://image.baidu.com', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/baidu/, '') + }, + '/api.btstu.cn': { + target: 'https://api.btstu.cn', + changeOrigin: true, + headers: { + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + }, + '/fabiaoqing': { + target: 'https://fabiaoqing.com', + changeOrigin: true, + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + }, + '/doutu': { + target: 'https://www.doutula.com', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/doutu/, '') + }, + '/api/doutu': { + target: 'https://doutu.lccyy.com', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/doutu/, '/doutu/items'), + headers: { + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + }, + '/api/video': { + target: 'http://api.mmp.cc', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/video/, '/api'), + headers: { + 'Accept': 'application/json, text/plain, */*', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + }, + } + }, + experimental: { + renderBuiltUrl(filename, { hostType }) { + return './' + filename; // 确保所有URL都使用相对路径 + } + } +}) \ No newline at end of file