diff --git a/.cursor/commands/ui-ux-pro-max.md b/.cursor/commands/ui-ux-pro-max.md new file mode 100644 index 00000000..04dd05fe --- /dev/null +++ b/.cursor/commands/ui-ux-pro-max.md @@ -0,0 +1,226 @@ +# ui-ux-pro-max + +Searchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices. + +## Prerequisites + +Check if Python is installed: + +```bash +python3 --version || python --version +``` + +If Python is not installed, install it based on user's OS: + +**macOS:** +```bash +brew install python3 +``` + +**Ubuntu/Debian:** +```bash +sudo apt update && sudo apt install python3 +``` + +**Windows:** +```powershell +winget install Python.Python.3.12 +``` + +--- + +## How to Use This Workflow + +When user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow: + +### Step 1: Analyze User Requirements + +Extract key information from user request: +- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc. +- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc. +- **Industry**: healthcare, fintech, gaming, education, etc. +- **Stack**: React, Vue, Next.js, or default to `html-tailwind` + +### Step 2: Search Relevant Domains + +Use `search.py` multiple times to gather comprehensive information. Search until you have enough context. + +```bash +python3 .shared/ui-ux-pro-max/scripts/search.py "" --domain [-n ] +``` + +**Recommended search order:** + +1. **Product** - Get style recommendations for product type +2. **Style** - Get detailed style guide (colors, effects, frameworks) +3. **Typography** - Get font pairings with Google Fonts imports +4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border) +5. **Landing** - Get page structure (if landing page) +6. **Chart** - Get chart recommendations (if dashboard/analytics) +7. **UX** - Get best practices and anti-patterns +8. **Stack** - Get stack-specific guidelines (default: html-tailwind) + +### Step 3: Stack Guidelines (Default: html-tailwind) + +If user doesn't specify a stack, **default to `html-tailwind`**. + +```bash +python3 .shared/ui-ux-pro-max/scripts/search.py "" --stack html-tailwind +``` + +Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter` + +--- + +## Search Reference + +### Available Domains + +| Domain | Use For | Example Keywords | +|--------|---------|------------------| +| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service | +| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism | +| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern | +| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service | +| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | +| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | +| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | +| `prompt` | AI prompts, CSS keywords | (style name) | + +### Available Stacks + +| Stack | Focus | +|-------|-------| +| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) | +| `react` | State, hooks, performance, patterns | +| `nextjs` | SSR, routing, images, API routes | +| `vue` | Composition API, Pinia, Vue Router | +| `svelte` | Runes, stores, SvelteKit | +| `swiftui` | Views, State, Navigation, Animation | +| `react-native` | Components, Navigation, Lists | +| `flutter` | Widgets, State, Layout, Theming | + +--- + +## Example Workflow + +**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp" + +**AI should:** + +```bash +# 1. Search product type +python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --domain product + +# 2. Search style (based on industry: beauty, elegant) +python3 .shared/ui-ux-pro-max/scripts/search.py "elegant minimal soft" --domain style + +# 3. Search typography +python3 .shared/ui-ux-pro-max/scripts/search.py "elegant luxury" --domain typography + +# 4. Search color palette +python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --domain color + +# 5. Search landing page structure +python3 .shared/ui-ux-pro-max/scripts/search.py "hero-centric social-proof" --domain landing + +# 6. Search UX guidelines +python3 .shared/ui-ux-pro-max/scripts/search.py "animation" --domain ux +python3 .shared/ui-ux-pro-max/scripts/search.py "accessibility" --domain ux + +# 7. Search stack guidelines (default: html-tailwind) +python3 .shared/ui-ux-pro-max/scripts/search.py "layout responsive" --stack html-tailwind +``` + +**Then:** Synthesize all search results and implement the design. + +--- + +## Tips for Better Results + +1. **Be specific with keywords** - "healthcare SaaS dashboard" > "app" +2. **Search multiple times** - Different keywords reveal different insights +3. **Combine domains** - Style + Typography + Color = Complete design system +4. **Always check UX** - Search "animation", "z-index", "accessibility" for common issues +5. **Use stack flag** - Get implementation-specific best practices +6. **Iterate** - If first search doesn't match, try different keywords +7. **Split Into Multiple Files** - For better maintainability: + - Separate components into individual files (e.g., `Header.tsx`, `Footer.tsx`) + - Extract reusable styles into dedicated files + - Keep each file focused and under 200-300 lines + +--- + +## Common Rules for Professional UI + +These are frequently overlooked issues that make UI look unprofessional: + +### Icons & Visual Elements + +| Rule | Do | Don't | +|------|----|----- | +| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons | +| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout | +| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths | +| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly | + +### Interaction & Cursor + +| Rule | Do | Don't | +|------|----|----- | +| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements | +| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive | +| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) | + +### Light/Dark Mode Contrast + +| Rule | Do | Don't | +|------|----|----- | +| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) | +| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text | +| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter | +| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) | + +### Layout & Spacing + +| Rule | Do | Don't | +|------|----|----- | +| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` | +| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements | +| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths | + +--- + +## Pre-Delivery Checklist + +Before delivering UI code, verify these items: + +### Visual Quality +- [ ] No emojis used as icons (use SVG instead) +- [ ] All icons from consistent icon set (Heroicons/Lucide) +- [ ] Brand logos are correct (verified from Simple Icons) +- [ ] Hover states don't cause layout shift + +### Interaction +- [ ] All clickable elements have `cursor-pointer` +- [ ] Hover states provide clear visual feedback +- [ ] Transitions are smooth (150-300ms) +- [ ] Focus states visible for keyboard navigation + +### Light/Dark Mode +- [ ] Light mode text has sufficient contrast (4.5:1 minimum) +- [ ] Glass/transparent elements visible in light mode +- [ ] Borders visible in both modes +- [ ] Test both modes before delivery + +### Layout +- [ ] Floating elements have proper spacing from edges +- [ ] No content hidden behind fixed navbars +- [ ] Responsive at 320px, 768px, 1024px, 1440px +- [ ] No horizontal scroll on mobile + +### Accessibility +- [ ] All images have alt text +- [ ] Form inputs have labels +- [ ] Color is not the only indicator +- [ ] `prefers-reduced-motion` respected diff --git a/.gitignore b/.gitignore index 9a6dadc7..6b3d10ad 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,6 @@ logs *.sln *.sw? miniprogram -api/test +.shared +.claude +docs/mydocs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..575f1bc4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# Repository Guidelines + +## 项目结构与模块组织 +- `api/`:Go + Gin 后端,包含 `core/` 业务、`handler/` 控制器、`service/` 接口调用以及 `store/` 数据访问,`config.toml` 存放默认配置,`Makefile` 用于多架构交叉构建。 +- `web/`:Vue3 + Vite 前端,源码集中在 `src/`,`public/` 为静态资源,`dist/` 存放构建结果并可被 `api/static/` 或 `desktop/` 引用。 +- `desktop/`:Electron 客户端入口为 `index.js`,配合 `electron-builder` 可打包 AppImage/DMG/NSIS。 +- `miniprogram/`、`docs/`、`database/` 与 `config/` 分别承载小程序壳、部署文档、SQL 脚本及全局 YAML 配置;`build/` 包含 Dockerfile、安装脚本。 + +## 构建、测试与开发命令 +- `docker-compose up -d`:根目录拉起全部容器,需提前准备好 MySQL、Redis 与模型密钥。 +- `cd api && go run main.go`:本地热调试;`make amd64` / `make arm64` 生成无 CGO 二进制至 `api/bin/` 便于镜像打包。 +- `cd web && pnpm install && pnpm dev --host`:Vite 开发模式;`pnpm build` 产出静态文件;`pnpm lint` 运行 ESLint 自动修复。 +- `cd desktop && npm install && npm run start`:调试 Electron;`npm run package` 通过 electron-builder 生成多平台安装包。 + +## 编码风格与命名规范 +- Go 代码必须经过 `gofmt`/`goimports`,保持 tab 缩进与驼峰命名;HTTP 路由遵循 `/api/v1/resources` 模式,与 handler 函数命名 (`ResourceHandler`) 对应。 +- Vue 组件文件使用 PascalCase(如 `ChatPanel.vue`),Pinia store 与工具采用 kebab-case 文件名(如 `chat-session.ts`);统一通过 ESLint、Tailwind 与 `postcss.config.js` 约束样式。 + +## 测试指南 +- `cd api && go test ./... -race` 是最低要求,新增 service/handler 需补 `_test.go` 并用 mock 隔离第三方 API;涉及时序逻辑可新增 `Test*Integration` 验证。 +- 前端暂未启用单测框架,至少运行 `pnpm lint` 并在 PR 中附关键页面截图或录屏证明交互可用;桌面端如修改构建脚本,需在 macOS/Linux/Windows 中至少验证一个安装包。 + +## 提交与 Pull Request 规范 +- 参考历史记录(如“支持腾讯云短信服务”),提交信息使用中文动词开头、聚焦单一变更,并可加子系统前缀:`web: 优化聊天动画`。 +- PR 描述需包含变更背景、实现概述、验证方式(命令、截图或日志)与关联 issue/任务号;涉及配置或部署脚本,还要说明回滚流程并 @ 相关 reviewer。 + +## 安全与配置提示 +- 禁止提交真实密钥,请复制 `config.sample.toml` 或 `config/config.yaml` 生成私有文件,并用 `git update-index --skip-worktree` 忽略。 +- 对象存储、短信、支付等凭证统一放入 Vault 或 CI Secret,代码中仅引用占位常量;`docs/` 中同步记录新增敏感字段与启用步骤。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 896f3ba3..43a8cfce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # 更新日志 +## v4.3.0 + +- 功能新增:**PPT 生成功能**,复刻 NotebookLLM 的演示文稿生成功能,支持编辑和导出 🔥🔥🔥 +- 功能新增:AI 绘图与视频创作提示词支持 @ 引用图片 🔥🔥🔥 +- 功能新增:接入豆包视频生成,优化 Sora 图生视频体验 🔥🔥🔥 +- 功能重构:移除 Stable Diffusion(SD)生图模块及相关前后端代码;数据库表 `geekai_sd_jobs` 保留不删除;管理后台算力配置移除 SD、可灵、Luma 等相关算力项 +- 功能重构:图片生成模块从 dalle 重命名为 image,支持查看任务详情 🔥🔥🔥 +- 功能优化:任务详情弹窗优化并补充图片任务创建时间 +- 功能优化:图片生成页尺寸支持自定义输入与预设选择;优化比例与尺寸参数展示 +- 功能优化:优化上传组件,支持拖拽上传;更新 iconfont;图片接口改造以兼容 kapon 聚合服务 +- 功能优化:MJ 支持局部重绘与算力配置优化;任务列表返回创建时间 +- 功能重构:重构 Gem(智能体)模块,调整智能体标识与工作区存储 +- 功能优化:优化 ChatPlus 欢迎页与会话输入体验,与 ImageMj 交互及 UI 一致性 +- 功能优化:支持批量导入用户信息,优化管理后台 UI 样式 +- 功能优化:重构管理后台 UI,采用现代科技风格 +- 功能优化:优化左侧菜单 UI,增加选中菜单对比度 +- Bug修复:完善数据迁移中各表自增主键及用户表主键迁移;迁移时清理历史 Dalle 与 SD 相关数据 + +## v4.2.9 + +- 功能优化:支持缩略图功能配置,支持本地存储,七牛云,腾讯云 OSS 和阿里云 OSS 四种存储介质。 +- 功能优化:重构视频生成模块,支持 Sora2, Veo3.1, Luma, 可灵,通义万相,MiniMax 等视频生成模型。 +- 功能新增:腾讯云短信服务 🔥🔥🔥 +- 功能新增:支持腾讯云 OSS 存储,支持腾讯云 COS 文件上传和下载 🔥🔥🔥 +- 功能新增:支持微信支付原生的 JSAPI 支付,支持微信公众号授权登录。🔥🔥🔥 +- 功能优化:对话页面输入框支持粘贴剪切板内容上传截图 🔥🔥🔥 +- 功能优化:给 Sora2 生成视频下载增加重试机制,防止因为网络不稳定导致下载失败 🎉🎉🎉 +- Bug 修复:删除即梦 4.0 生图不支持的分片率参数,支持 1K, 2K, 4K 分辨率 +- 功能优化:即梦 AI 新增 3.0 电商营销产品背景替换功能 🔥🔥🔥 +- Bug 修复:调整 PC 端会员页面的列表样式,重构页面布局,移除多余的 css 样式,采用 tailwindcss 样式 +- Bug 修复:修复管理后台重置管理密码不生效的问题 +- Bug 修复:修复 Chat 页面输入框输入内容会自动滚动到最底部的问题 +- Bug 修复:修复即梦视频生成任务 duration(视频时长) 参数不生效的问题 🎉🎉🎉 +- Bug 修复:修复管理后台系统配置邮箱白名单更改不生效问题 +- Bug 修复:修复聊天页面角色列表显示 system prompt 提示词的问题 + ## v4.2.8 - Bug 修复:修复管理后台邮件配置报“参数错误”问题 @@ -12,7 +48,6 @@ - Bug 修复:移动端登录页面输入密码的时候会覆盖在确认按钮上层 - 功能优化:**移动端的 DALL-E 绘图页面支持上传参考图** 🔥🔥🔥 - 功能优化:优化 WebFooter 组件,如果配置了备案号,则显示备案号,否则不显示 -- 功能优化:启动时自动同步 model 与数据表字段,缺列自动新建、多余列自动删除 ## v4.2.7 diff --git a/CLAUDE.md b/CLAUDE.md index 64652d3b..8008fbbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ Key entities: User, ChatItem, ChatMessage, ChatRole, ChatModel, Order, Product, ### API Structure - User APIs: `/api/user/*` (auth, profile, settings) - Chat APIs: `/api/chat/*` (conversations, messages) -- AI Service APIs: `/api/mj/*`, `/api/sd/*`, `/api/dall/*`, `/api/suno/*`, `/api/video/*` +- AI Service APIs: `/api/mj/*`, `/api/dall/*`, `/api/suno/*`, `/api/video/*` - Admin APIs: `/api/admin/*` (management functions) ### Configuration diff --git a/PROMPT.md b/PROMPT.md new file mode 100644 index 00000000..d3ade0c1 --- /dev/null +++ b/PROMPT.md @@ -0,0 +1,8 @@ +# 项目固定指令(Cursor 必须遵守) + +1. 代码必须使用清晰命名,禁止单字母变量 +2. 代码必须加注释 +3. 统一使用 4 空格缩进 +4. 前端样式代码永远优先适应 Tailwind CSS 的样式, 别自定义样式。除非 Tailwind CSS 不支持或者做不到。 +5. 不要生成多余代码,保持简洁 +6. 每次生成代码前遵守以上规则 diff --git a/README.md b/README.md index fad888ba..72c0c008 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# 🚀 GeekAI-PLUS:一站式 AI 创意生产力平台 +# 🚀 GeekAI:一站式 AI 创意生产力平台 -**重新定义 AI 创作体验,让每个人都能成为内容创作大师** +**让创意触手可及,让创作变得简单** -基于 GeekAI 项目开发的高级版,增加了很多高级功能,比如思维导图,Dalle 绘画等。**高级版源码不会一次性开放,只提供镜像给大家免费使用**,源码会逐步逐步按照版同步迁移到[社区版(GeekAI)](https://github.com/yangjian102621/geekai)。所以如果大家想要二次开发,请移步去社区版。 +一个功能完整、开箱即用的 AI 多模态内容创作平台,集成了对话、绘画、音乐、视频、思维导图等全链路 AI 创作能力。无论是个人创作者还是企业团队,都能快速搭建属于自己的 AI 创作工作台。 ## ✨ 核心特色 ### 🎨 **全能 AI 创作矩阵** - **智能对话**:集成 ChatGPT、Claude 等多款顶级 AI 模型,支持角色扮演和专业对话 -- **图像生成**:整合 MidJourney、DALL-E、Stable Diffusion 三大主流 AI 绘画引擎 +- **图像生成**:整合 MidJourney、DALL-E、Nano-Banana,即梦,可灵等主流 AI 绘画引擎 - **音频创作**:Suno AI 音乐生成,从旋律到歌词一键创作专属音乐 - **视频制作**:Luma 和 KeLing,即梦,Veo3 视频 AI,文本到视频,创意无限 - **思维导图**:AI 辅助思维整理,复杂想法可视化呈现 @@ -34,8 +34,8 @@ - **响应式设计**:完美适配桌面、平板、手机等全终端设备 - **暗黑模式**:支持明暗主题切换,护眼舒适 -- **实时交互**:WebSocket 实时通信,创作过程流畅无卡顿 -- **文件管理**:支持多种云存储,作品安全可靠 +- **实时交互**:流式响应,创作过程流畅无卡顿 +- **文件管理**:支持七牛云,阿里云 OSS,腾讯云 OSS,Minio 等多种云存储,作品安全可靠 ## 🎪 **应用场景** @@ -44,7 +44,7 @@ - **教育培训**:课件制作、知识图谱、互动内容 - **个人娱乐**:AI 聊天、创意绘画、音乐创作 -## 🔥 **为什么选择 GeekAI-PLUS?** +## 🔥 **为什么选择 GeekAI?** 1. **技术领先**:集成当前最先进的 AI 技术,始终保持创新前沿 2. **开箱即用**:完整的商业化解决方案,无需从零开发 @@ -54,11 +54,11 @@ ## 演示站点 -[Geek-AI 创作系统](https://www.geekai.me) +[Geek-AI 创作系统](https://chat.geekai.me) ## 文档地址 -[Geek-AI 文档](https://www.geekai.me/docs/) +[Geek-AI 文档](https://docs.geekai.me) ## 部署 diff --git a/api/config.sample.toml b/api/config.sample.toml index 8cdd2a00..fa24094b 100644 --- a/api/config.sample.toml +++ b/api/config.sample.toml @@ -1,6 +1,6 @@ Listen = "0.0.0.0:5678" ProxyURL = "" # 如 http://127.0.0.1:7777 -MysqlDns = "root:12345678@tcp(localhost:3306)/geekai?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local" +MysqlDns = "root:password@tcp(127.0.0.1:3306)/geekai?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local" StaticDir = "./static" # 静态资源的目录 StaticUrl = "/static" # 静态资源访问 URL TikaHost = "http://tika:9998" @@ -14,102 +14,3 @@ TikaHost = "http://tika:9998" Port = 6379 Password = "" DB = 0 - -[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 - ApiURL = "https://sapi.geekai.me" - AppId = "" - Token = "" - - -[SMS] # Sms 配置,用于发送短信 - Active = "Ali" # 当前启用的短信服务,默认使用阿里云 - [SMS.Bao] - Username = "" - Password = "" - Domain = "api.smsbao.com" - Sign = "【极客学长】" - CodeTemplate = "您的验证码是{code}。5分钟有效,若非本人操作,请忽略本短信。" - [SMS.Ali] - AccessKey = "" - AccessSecret = "" - Product = "Dysmsapi" - Domain = "dysmsapi.aliyuncs.com" - Sign = "" - CodeTempId = "" - -[OSS] # OSS 配置,用于存储 MJ 绘画图片 - Active = "local" # 默认使用本地文件存储引擎 - [OSS.Local] - BasePath = "./static/upload" # 本地文件上传根路径 - BaseURL = "http://localhost:5678/static/upload" # 本地上传文件前缀 URL,线上需要把 localhost 替换成自己的实际域名或者IP - [OSS.Minio] - Endpoint = "" # 如 172.22.11.200:9000 - AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key - AccessSecret = "" - Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。 - UseSSL = false - Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。 - [OSS.QiNiu] # 七牛云 OSS 配置 - Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 - AccessKey = "" - AccessSecret = "" - Bucket = "" - Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com - [OSS.AliYun] - Endpoint = "oss-cn-hangzhou.aliyuncs.com" - AccessKey = "" - AccessSecret = "" - Bucket = "chatgpt-plus" - SubDir = "" - Domain = "" - -[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动 - Enabled = false # 是否启用 XXL JOB 服务 - ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址 - ExecutorIp = "172.22.11.47" # 执行器 IP 地址 - ExecutorPort = "9999" # 执行器服务端口 - AccessToken = "xxl-job-api-token" # 执行器 API 通信 token - RegistryKey = "chatgpt-plus" # 任务注册 key - - [SmtpConfig] # 注意,阿里云服务器禁用了25号端口,请使用 465 端口,并开启 TLS 连接 - UseTls = false - Host = "smtp.163.com" - Port = 25 - AppName = "极客学长" - From = "test@163.com" # 发件邮箱人地址 - Password = "" #邮箱 stmp 服务授权码 - -# 支付宝商户支付 -[AlipayConfig] - Enabled = false # 启用支付宝支付通道 - SandBox = false # 是否启用沙盒模式 - UserId = "2088721020750581" # 商户ID - AppId = "9021000131658023" # App Id - PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥 - PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书 - AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书 - RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书 - -# 虎皮椒支付 -[HuPiPayConfig] - Enabled = false - AppId = "" - AppSecret = "" - ApiURL = "https://api.xunhupay.com" - -# 微信商户支付 -[WechatPayConfig] - Enabled = false - AppId = "" # 商户应用ID - MchId = "" # 商户号 - SerialNo = "" # API 证书序列号 - PrivateKey = "certs/alipay/privateKey.txt" # API 证书私钥文件路径,跟支付宝一样,把私钥文件拷贝到对应的路径,证书路径要映射到容器内 - ApiV3Key = "" # APIV3 私钥,这个是你自己在微信支付平台设置的 - -# 易支付 -[GeekPayConfig] - Enabled = true - AppId = "" # 商户ID - PrivateKey = "" # 商户私钥 - ApiURL = "https://pay.geekai.cn" - Methods = ["alipay", "wxpay", "qqpay", "jdpay", "douyin", "paypal"] # 支持的支付方式 diff --git a/api/core/config.go b/api/core/config.go index 4b6ff58f..278e2b5b 100644 --- a/api/core/config.go +++ b/api/core/config.go @@ -10,7 +10,7 @@ package core import ( "bytes" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "geekai/store/model" "geekai/utils" "os" @@ -19,7 +19,7 @@ import ( "gorm.io/gorm" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() func NewDefaultConfig() *types.AppConfig { return &types.AppConfig{ @@ -157,6 +157,15 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig { logger.Error("load jimeng config error: ", err) } + // 加载微信公众号配置 + var wxGzhConfig types.WxGzhConfig + sysConfig.Id = 0 + db.Where("name", types.ConfigKeyWxGzh).First(&sysConfig) + err = utils.JsonDecode(sysConfig.Value, &wxGzhConfig) + if err != nil { + logger.Error("load wx gzh config error: ", err) + } + return &types.SystemConfig{ Base: baseConfig, SMS: smsConfig, @@ -167,5 +176,6 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig { WxLogin: wxLoginConfig, Moderation: moderationConfig, Jimeng: jimengConfig, + WxGzh: wxGzhConfig, } } diff --git a/api/core/middleware/auth.go b/api/core/middleware/auth.go index d5f77265..6da5499e 100644 --- a/api/core/middleware/auth.go +++ b/api/core/middleware/auth.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "geekai/utils" "geekai/utils/resp" "time" @@ -14,7 +14,7 @@ import ( "github.com/golang-jwt/jwt" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() // 前端用户授权验证 func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc { diff --git a/api/core/types/config.go b/api/core/types/config.go index 66733afe..ec0592ea 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -57,8 +57,11 @@ type BaseConfig struct { DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力 InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值 MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力 - MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 - SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力 + MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力,未配置分项时回退用 + MjUpscalePower int `json:"mj_upscale_power,omitempty"` // MJ 放大/变换消耗算力 + MjBlendPower int `json:"mj_blend_power,omitempty"` // MJ 融图消耗算力 + MjSwapFacePower int `json:"mj_swap_face_power,omitempty"` // MJ 换脸消耗算力 + MjModalPower int `json:"mj_modal_power,omitempty"` // MJ 局部重绘消耗算力 SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力 LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力 KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力 @@ -69,8 +72,7 @@ type BaseConfig struct { EnableContext bool `json:"enable_context,omitempty"` ContextDeep int `json:"context_deep,omitempty"` - SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词 - MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo + MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单 IndexPage string `json:"index_page"` // 首页显示的页面 @@ -95,22 +97,26 @@ type SystemConfig struct { WxLogin WxLoginConfig Jimeng JimengConfig Moderation ModerationConfig + WxGzh WxGzhConfig } // 配置键名常量 const ( - ConfigKeySystem = "system" - ConfigKeyNotice = "notice" - ConfigKeyAgreement = "agreement" - ConfigKeyPrivacy = "privacy" - ConfigKeyMarkMap = "mark_map" - ConfigKeyCaptcha = "captcha" - ConfigKeyWxLogin = "wx_login" - ConfigKeySms = "sms" - ConfigKeySmtp = "smtp" - ConfigKeyOss = "oss" - ConfigKeyPayment = "payment" - ConfigKeyModeration = "moderation" - ConfigKeyAI3D = "ai3d" - ConfigKeyJimeng = "jimeng" + ConfigKeySystem = "system" // 系统配置 + ConfigKeyNotice = "notice" // 公告配置 + ConfigKeyAgreement = "agreement" // 用户协议配置 + ConfigKeyPrivacy = "privacy" // 隐私政策配置 + ConfigKeyMarkMap = "mark_map" // 水印配置 + ConfigKeyCaptcha = "captcha" // 验证码配置 + ConfigKeyWxLogin = "wx_login" // 微信扫码登录配置 + ConfigKeyWxGzh = "wx_gzh" // 微信公众号配置 + ConfigKeySms = "sms" // 短信配置 + ConfigKeySmtp = "smtp" // SMTP 配置 + ConfigKeyOss = "oss" // OSS 配置 + ConfigKeyPayment = "payment" // 支付配置 + ConfigKeyModeration = "moderation" // 文本审查配置 + ConfigKeyAI3D = "ai3d" // AI3D 配置 + ConfigKeyJimeng = "jimeng" // 即梦AI配置 + ConfigKeyVideo = "video" // 视频生成配置 + ConfigKeyPPT = "ppt" // PPT 生成配置 ) diff --git a/api/core/types/geekai.go b/api/core/types/geekai.go index 39ca2a48..16f054be 100644 --- a/api/core/types/geekai.go +++ b/api/core/types/geekai.go @@ -25,9 +25,18 @@ type CaptchaConfig struct { Enabled bool `json:"enabled,omitempty"` } -// WxLoginConfig 微信登录配置 +// WxLoginConfig 微信扫码登录配置 type WxLoginConfig struct { ApiKey string `json:"api_key,omitempty"` NotifyURL string `json:"notify_url,omitempty"` // 登录成功回调 URL Enabled bool `json:"enabled,omitempty"` // 是否启用微信登录 } + +// 微信公众号配置 +type WxGzhConfig struct { + AppId string `json:"app_id,omitempty"` + Secret string `json:"secret,omitempty"` + Token string `json:"token"` + EncodingAESKey string `json:"encoding_aes_key"` + Enabled bool `json:"enabled"` +} diff --git a/api/core/types/moderation.go b/api/core/types/moderation.go index 504d0195..8e85d5da 100644 --- a/api/core/types/moderation.go +++ b/api/core/types/moderation.go @@ -65,8 +65,7 @@ var ModerationCategories = map[string]string{ const ( ModerationSourceChat = "chat" ModerationSourceMJ = "mj" - ModerationSourceDalle = "dalle" - ModerationSourceSD = "sd" + ModerationSourceImage = "image" ModerationSourceSuno = "suno" ModerationSourceVideo = "video" ModerationSourceJiMeng = "jimeng" diff --git a/api/core/types/oss.go b/api/core/types/oss.go index 5e65e9ed..67885ee5 100644 --- a/api/core/types/oss.go +++ b/api/core/types/oss.go @@ -8,39 +8,53 @@ package types // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ type OSSConfig struct { - Active string `json:"active,omitempty"` - Local LocalStorageConfig `json:"local,omitempty"` - Minio MiniOssConfig `json:"minio,omitempty"` - QiNiu QiNiuOssConfig `json:"qiniu,omitempty"` - AliYun AliYunOssConfig `json:"aliyun,omitempty"` + Active string `json:"active,omitempty"` + Local LocalStorageConfig `json:"local,omitempty"` + Minio MiniOssConfig `json:"minio,omitempty"` + QiNiu QiNiuOssConfig `json:"qiniu,omitempty"` + AliYun AliYunOssConfig `json:"aliyun,omitempty"` + Tencent TencentOssConfig `json:"tencent,omitempty"` } type MiniOssConfig struct { - Endpoint string `json:"endpoint,omitempty"` - AccessKey string `json:"access_key,omitempty"` - AccessSecret string `json:"access_secret,omitempty"` - Bucket string `json:"bucket,omitempty"` - UseSSL bool `json:"use_ssl,omitempty"` - Domain string `json:"domain,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + AccessKey string `json:"access_key,omitempty"` + AccessSecret string `json:"access_secret,omitempty"` + Bucket string `json:"bucket,omitempty"` + UseSSL bool `json:"use_ssl,omitempty"` + Domain string `json:"domain,omitempty"` + ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符 } type QiNiuOssConfig struct { - Zone string `json:"zone,omitempty"` - AccessKey string `json:"access_key,omitempty"` - AccessSecret string `json:"access_secret,omitempty"` - Bucket string `json:"bucket,omitempty"` - Domain string `json:"domain,omitempty"` + Zone string `json:"zone,omitempty"` + AccessKey string `json:"access_key,omitempty"` + AccessSecret string `json:"access_secret,omitempty"` + Bucket string `json:"bucket,omitempty"` + Domain string `json:"domain,omitempty"` + ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?imageView2/4/w/{width}/h/{height}/q/75 } type AliYunOssConfig struct { - Endpoint string `json:"endpoint,omitempty"` - AccessKey string `json:"access_key,omitempty"` - AccessSecret string `json:"access_secret,omitempty"` - Bucket string `json:"bucket,omitempty"` - Domain string `json:"domain,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + AccessKey string `json:"access_key,omitempty"` + AccessSecret string `json:"access_secret,omitempty"` + Bucket string `json:"bucket,omitempty"` + Domain string `json:"domain,omitempty"` + ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?x-oss-process=image/resize,m_lfit,w_{width},h_{height} } type LocalStorageConfig struct { - BasePath string `json:"base_path,omitempty"` - BaseURL string `json:"base_url,omitempty"` + BasePath string `json:"base_path,omitempty"` + BaseURL string `json:"base_url,omitempty"` + ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?imageView2/4/w/{width}/h/{height}/q/75 +} + +type TencentOssConfig struct { + Region string `json:"region,omitempty"` + SecretId string `json:"secret_id,omitempty"` + SecretKey string `json:"secret_key,omitempty"` + Bucket string `json:"bucket,omitempty"` + Domain string `json:"domain,omitempty"` + ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?imageView2/1/w/{width}/h/{height}/format/jpg } diff --git a/api/core/types/ppt_config.go b/api/core/types/ppt_config.go new file mode 100644 index 00000000..57490b71 --- /dev/null +++ b/api/core/types/ppt_config.go @@ -0,0 +1,47 @@ +package types + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +// PPTImageProvider PPT 图片生成提供方 +type PPTImageProvider string + +const ( + PPTImageProviderNanoBanana PPTImageProvider = "nano_banana" + PPTImageProviderSeedream PPTImageProvider = "seedream" +) + +// PPTConfig PPT 生成配置(存储在 config 表,name = 'ppt') +type PPTConfig struct { + // 分镜 LLM 配置 + OutlineLLMApiURL string `json:"outline_llm_api_url"` // 分镜 LLM API 地址 + OutlineLLMApiKey string `json:"outline_llm_api_key"` // 分镜 LLM API Key + OutlineLLMModel string `json:"outline_llm_model"` // 分镜 LLM 模型名称,如 gpt-4o-mini + + // 图片生成通用配置 + ActiveImageProvider PPTImageProvider `json:"active_image_provider"` // 当前启用的图片模型提供方 + MaxSlidesPerTask int `json:"max_slides_per_task"` // 单个任务最多生成的 PPT 页数 + PowerCostPerSlide int `json:"power_cost_per_slide"` // 每张 PPT 图片消耗的算力 + MaxConcurrentRequests int `json:"max_concurrent_requests"` // 图片生成最大并发数 + QPSLimit int `json:"qps_limit"` // 外部图片 API 的 QPS 限制 + + // Nano Banana 配置 + NanoBananaApiURL string `json:"nano_banana_api_url"` // Nano Banana API 地址,如 https://xxx/v1/images/generations + NanoBananaApiKey string `json:"nano_banana_api_key"` // Nano Banana API Key + NanoBananaModel string `json:"nano_banana_model"` // 模型名称,如 nano-banana、nano-banana-hd + NanoBananaResponseFormat string `json:"nano_banana_response_format"` // 响应格式:url 或 b64_json + NanoBananaAspectRatio string `json:"nano_banana_aspect_ratio"` // 宽高比,如 1:1、4:3、16:9;空则默认 16:9 + + // Doubao Seedream 配置 + SeedreamBaseURL string `json:"seedream_base_url"` // Seedream base url,例如:https://ark.cn-beijing.volces.com/api/v3 + SeedreamApiKey string `json:"seedream_api_key"` // Seedream API Key(ARK_API_KEY) + SeedreamModel string `json:"seedream_model"` // Seedream 模型 ID,例如:doubao-seedream-5-0-260128 + SeedreamSize string `json:"seedream_size"` // 图片尺寸 WxH(如 1920x1080)或 2K/4K 等;空则默认 1920x1080(16:9) + SeedreamOutputFormat string `json:"seedream_output_format"` // 输出格式,例如:png + SeedreamResponseType string `json:"seedream_response_format"` // 响应格式,例如:url + SeedreamWatermark bool `json:"seedream_watermark"` // 是否开启水印 +} diff --git a/api/core/types/sms.go b/api/core/types/sms.go index 608fe9e6..b2719f6f 100644 --- a/api/core/types/sms.go +++ b/api/core/types/sms.go @@ -8,9 +8,10 @@ package types // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ type SMSConfig struct { - Active string `json:"active,omitempty"` - Ali SmsConfigAli `json:"aliyun,omitempty"` - Bao SmsConfigBao `json:"bao,omitempty"` + Active string `json:"active,omitempty"` + Ali SmsConfigAli `json:"aliyun,omitempty"` + Bao SmsConfigBao `json:"bao,omitempty"` + Tencent SmsConfigTencent `json:"tencent,omitempty"` } // SmsConfigAli 阿里云短信平台配置 @@ -28,3 +29,13 @@ type SmsConfigBao struct { Sign string `json:"sign,omitempty"` // 短信签名 CodeTemplate string `json:"code_template,omitempty"` // 验证码短信模板 匹配 } + +// SmsConfigTencent 腾讯云短信平台配置 +type SmsConfigTencent struct { + SecretId string `json:"secret_id,omitempty"` // 腾讯云 SecretId + SecretKey string `json:"secret_key,omitempty"` // 腾讯云 SecretKey + SmsSdkAppId string `json:"sms_sdk_app_id,omitempty"` // 短信应用ID + Sign string `json:"sign,omitempty"` // 短信签名 + CodeTempId string `json:"code_temp_id,omitempty"` // 验证码短信模板ID + Region string `json:"region,omitempty"` // 地区,默认 ap-guangzhou +} diff --git a/api/core/types/task.go b/api/core/types/task.go index 0fe37174..c6d41d9b 100644 --- a/api/core/types/task.go +++ b/api/core/types/task.go @@ -20,6 +20,7 @@ const ( TaskSwapFace = TaskType("swapFace") TaskUpscale = TaskType("upscale") TaskVariation = TaskType("variation") + TaskModal = TaskType("modal") // 局部重绘 ) // MjTask MidJourney 任务 @@ -38,43 +39,17 @@ type MjTask struct { ChannelId string `json:"channel_id"` // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道 Mode string `json:"mode"` // 绘画模式,relax, fast, turbo TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID + MaskBase64 string `json:"mask_base64,omitempty"` // 局部重绘蒙版 base64(仅 TaskModal 使用) } -type SdTask struct { - Id int `json:"id"` // job 数据库ID - Type TaskType `json:"type"` - UserId int `json:"user_id"` - Params SdTaskParams `json:"params"` - RetryCount int `json:"retry_count"` - TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID -} - -type SdTaskParams struct { - TaskId string `json:"task_id"` - Prompt string `json:"prompt"` // 提示词 - NegPrompt string `json:"neg_prompt"` // 反向提示词 - Steps int `json:"steps"` // 迭代步数,默认20 - Sampler string `json:"sampler"` // 采样器 - Scheduler string `json:"scheduler"` // 采样调度 - FaceFix bool `json:"face_fix"` // 面部修复 - CfgScale float32 `json:"cfg_scale"` //引导系数,默认 7 - Seed int64 `json:"seed"` // 随机数种子 - Height int `json:"height"` - Width int `json:"width"` - HdFix bool `json:"hd_fix"` // 启用高清修复 - HdRedrawRate float32 `json:"hd_redraw_rate"` // 高清修复重绘幅度 - HdScale int `json:"hd_scale"` // 放大倍数 - HdScaleAlg string `json:"hd_scale_alg"` // 放大算法 - HdSteps int `json:"hd_steps"` // 高清修复迭代步数 -} - -// DallTask DALL-E task -type DallTask struct { +// ImageTask Image generation task +type ImageTask struct { ModelId uint `json:"model_id"` ModelName string `json:"model_name"` ModelValue string `json:"model_value"` Image []string `json:"image,omitempty"` Id uint `json:"id"` + TaskId string `json:"task_id"` // Kapon 异步任务 ID,与 API 返回的 task_id 一致 UserId uint `json:"user_id"` Prompt string `json:"prompt"` AspectRatio string `json:"aspect_ratio"` @@ -102,60 +77,22 @@ type SunoTask struct { } const ( - VideoLuma = "luma" - VideoRunway = "runway" - VideoCog = "cog" - VideoKeLing = "keling" + VideoLuma = "luma" + VideoSora = "sora" + VideoVeo = "veo" + VideoKeLing = "keling" + VideoMiniMax = "minimax" + VideoWan = "wan" + VideoDoubao = "doubao" ) type VideoTask struct { - Id uint `json:"id"` - Channel string `json:"channel"` - UserId int `json:"user_id"` - Type string `json:"type"` - TaskId string `json:"task_id"` - Prompt string `json:"prompt"` // 提示词 - Params interface{} `json:"params"` - TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID -} - -type LumaVideoParams struct { - PromptOptimize bool `json:"prompt_optimize"` // 是否优化提示词 - Loop bool `json:"loop"` // 是否循环参考图 - StartImgURL string `json:"start_img_url"` // 第一帧参考图地址 - EndImgURL string `json:"end_img_url"` // 最后一帧参考图地址 - Model string `json:"model"` // 使用哪个模型生成视频 - Radio string `json:"radio"` // 视频尺寸 - Style string `json:"style"` // 风格 - Duration int `json:"duration"` // 视频时长(秒) -} - -type KeLingVideoParams struct { - TaskType string `json:"task_type"` // 任务类型: text2video/image2video - Model string `json:"model"` // 模型: default/anime - Prompt string `json:"prompt"` // 视频描述 - NegPrompt string `json:"negative_prompt"` // 负面提示词 - CfgScale float64 `json:"cfg_scale"` // 相关性系数(0-1) - Mode string `json:"mode"` // 生成模式: std/pro - AspectRatio string `json:"aspect_ratio"` // 画面比例: 16:9/9:16/1:1 - Duration string `json:"duration"` // 视频时长: 5/10 - CameraControl CameraControl `json:"camera_control"` // 摄像机控制 - Image string `json:"image"` // 参考图片URL(image2video) - ImageTail string `json:"image_tail"` // 尾帧图片URL(image2video) -} - -// CameraControl 摄像机控制 -type CameraControl struct { - Type string `json:"type"` // 控制类型: simple/down_back/forward_up/right_turn_forward/left_turn_forward - Config CameraConfig `json:"config"` // 控制参数(仅simple类型时使用) -} - -// CameraConfig 摄像机参数 -type CameraConfig struct { - Horizontal int `json:"horizontal"` // 水平移动(-10到10) - Vertical int `json:"vertical"` // 垂直移动(-10到10) - Pan int `json:"pan"` // 左右旋转(-10到10) - Tilt int `json:"tilt"` // 上下旋转(-10到10) - Roll int `json:"roll"` // 横向翻转(-10到10) - Zoom int `json:"zoom"` // 镜头缩放(-10到10) + Id uint `json:"id"` + Channel string `json:"channel"` + UserId int `json:"user_id"` + Type string `json:"type"` // provider(不带版本号:veo, sora, luma) + TaskId string `json:"task_id"` + Prompt string `json:"prompt"` // 提示词 + Params any `json:"params"` + TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID } diff --git a/api/core/types/video_config.go b/api/core/types/video_config.go new file mode 100644 index 00000000..061b5edc --- /dev/null +++ b/api/core/types/video_config.go @@ -0,0 +1,37 @@ +package types + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +// VideoConfig 视频生成配置(存储在 config 表,name = 'video') +type VideoConfig struct { + ApiURL string `json:"api_url"` // API 地址 + ApiKey string `json:"api_key"` // API 密钥 + VideoPowers map[string]VideoModelPower `json:"video_powers"` // 模型算力配置 +} + +// VideoModelPower 单个模型的算力配置 +// PowerConfig 说明: +// 新的价格配置方式:根据模型的 priceParams 生成笛卡尔乘积,每个组合对应一个价格 +// 固定价格示例:{"fixed": 20} +// 多参数组合示例:{"5_720P": 10, "5_1080P": 20, "10_720P": 10, "10_1080P": 40} +// 复杂组合示例:{"std_5_sound": 10, "std_5_silent": 5, "pro_10_sound": 20, "pro_10_silent": 10} +type VideoModelPower struct { + Provider string `json:"provider"` // 服务提供商(不带版本号:veo, sora, luma) + Model string `json:"model"` // 模型名称(带版本号:veo-2.0, sora-2.0) + PowerConfig map[string]int `json:"power_config"` // 算力配置(基于 priceParams 的笛卡尔乘积) + ApiKeyType string `json:"api_key_type"` // ApiKey 表的 type 字段(可选,用于多 API Key 场景) +} + +// 视频任务状态常量 +const ( + VideoStatusPending = "pending" // 等待处理 + VideoStatusInProgress = "in_progress" // 处理中 + VideoStatusDownloading = "downloading" // 视频下载中 + VideoStatusSuccess = "success" // 成功 + VideoStatusFailed = "failed" // 失败 +) diff --git a/api/core/types/web.go b/api/core/types/web.go index 0546f18e..67563c80 100644 --- a/api/core/types/web.go +++ b/api/core/types/web.go @@ -36,9 +36,8 @@ const ( ChPing = WsChannel("ping") ChChat = WsChannel("chat") - ChMj = WsChannel("mj") - ChSd = WsChannel("sd") - ChDall = WsChannel("dall") + ChMj = WsChannel("mj") + ChImage = WsChannel("image") ChSuno = WsChannel("suno") ChLuma = WsChannel("luma") ChKeLing = WsChannel("keling") diff --git a/api/go.mod b/api/go.mod index 8ae01b21..26732332 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,8 +1,6 @@ module geekai -go 1.21 - -toolchain go1.22.4 +go 1.24.0 require ( github.com/BurntSushi/toml v1.1.0 @@ -18,6 +16,7 @@ require ( github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 github.com/qiniu/go-sdk/v7 v7.17.1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/tencentyun/cos-go-sdk-v5 v0.7.47 github.com/volcengine/volc-sdk-golang v1.0.23 go.uber.org/zap v1.23.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 @@ -28,17 +27,24 @@ require ( github.com/go-pay/gopay v1.5.101 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-tika v0.3.1 + github.com/jung-kurt/gofpdf/v2 v2.17.3 + github.com/ktye/pptx v0.0.0-20250326170941-2a6bc4329df6 github.com/microcosm-cc/bluemonday v1.0.26 github.com/sashabaranov/go-openai v1.38.1 github.com/shirou/gopsutil v3.21.11+incompatible github.com/shopspring/decimal v1.3.1 github.com/syndtr/goleveldb v1.0.0 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.49 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.1.49 github.com/volcengine/volcengine-go-sdk v1.1.34 - golang.org/x/image v0.15.0 + github.com/xuri/excelize/v2 v2.10.0 + golang.org/x/image v0.25.0 ) require ( github.com/aymerick/douceur v0.2.0 // indirect + github.com/beevik/etree v1.1.0 // indirect + github.com/clbanning/mxj v1.8.4 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-pay/crypto v0.0.1 // indirect github.com/go-pay/errgroup v0.0.2 // indirect @@ -46,9 +52,17 @@ require ( github.com/go-pay/xlog v0.0.2 // indirect github.com/go-pay/xtime v0.0.2 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/mozillazg/go-httpheader v0.2.1 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/tiendc/go-deepcopy v1.7.1 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/mock v0.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -91,12 +105,12 @@ require ( go.uber.org/dig v1.16.1 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.5.0 + golang.org/x/tools v0.37.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -116,7 +130,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/fx v1.19.3 go.uber.org/multierr v1.7.0 // indirect - golang.org/x/crypto v0.23.0 - golang.org/x/sys v0.20.0 // indirect + golang.org/x/crypto v0.43.0 + golang.org/x/sys v0.37.0 // indirect gorm.io/gorm v1.25.1 ) diff --git a/api/go.sum b/api/go.sum index 0a8b0d75..19460656 100644 --- a/api/go.sum +++ b/api/go.sum @@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4= github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs= github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k= @@ -11,6 +12,8 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= @@ -22,11 +25,14 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= @@ -114,11 +120,14 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA= github.com/google/go-tika v0.3.1/go.mod h1:DJh5N8qxXIl85QkqmXknd+PeeRkUOTbvwyYf7ieDz6c= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= @@ -146,6 +155,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY= +github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -161,6 +172,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ktye/pptx v0.0.0-20250326170941-2a6bc4329df6 h1:pItMMIM7cHfp43QyuZFeYJyKHPk4yFs8JQUIdjlcWYo= +github.com/ktye/pptx v0.0.0-20250326170941-2a6bc4329df6/go.mod h1:X+eOu1OuD+D/IKBa+1HnJBcSaKBB75n5TmKkFVFDPU8= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= @@ -176,12 +189,16 @@ github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRne github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -217,6 +234,11 @@ github.com/quic-go/quic-go v0.45.0 h1:OHmkQGM37luZITyTSu6ff03HP/2IrwDX1ZFiNEhSFU github.com/quic-go/quic-go v0.45.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= @@ -243,10 +265,21 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.49 h1:BQwUw2V21zIRJxstnaxtG/22lBL3+FbUgWhaC6Qd9ws= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.49/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.1.49 h1:8mlcG8TmoeEIDQGLYvqc9fdBQrNwKEb56I2HVNy9jdw= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.1.49/go.mod h1:YAdTku4GgK5H2LKea4bPgWf3qFLBibIBzgU1LW5WgEc= +github.com/tencentyun/cos-go-sdk-v5 v0.7.47 h1:uoS4Sob16qEYoapkqJq1D1Vnsy9ira9BfNUMtoFYTI4= +github.com/tencentyun/cos-go-sdk-v5 v0.7.47/go.mod h1:DH9US8nB+AJXqwu/AMOrCFN1COv3dpytXuJWHgdg7kE= +github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= +github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= @@ -263,6 +296,12 @@ github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9 github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU= github.com/volcengine/volcengine-go-sdk v1.1.34 h1:ha90JycCCTJNCse0UDziBgBsuX98ITOrkwYlDWcm7NI= github.com/volcengine/volcengine-go-sdk v1.1.34/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= +github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -290,20 +329,20 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= -golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -317,16 +356,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -343,8 +382,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -361,8 +400,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -373,8 +412,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/api/handler/admin/admin_handler.go b/api/handler/admin/admin_handler.go index 77950911..c3a1dbc9 100644 --- a/api/handler/admin/admin_handler.go +++ b/api/handler/admin/admin_handler.go @@ -14,7 +14,7 @@ import ( "geekai/core/middleware" "geekai/core/types" "geekai/handler" - logger2 "geekai/logger" + "geekai/log" "geekai/service" "geekai/store/model" "geekai/store/vo" @@ -29,7 +29,7 @@ import ( "gorm.io/gorm" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() const SuperUsername = "admin" @@ -293,7 +293,7 @@ func (h *ManagerHandler) ResetPass(c *gin.Context) { password := utils.GenPassword(data.Password, user.Salt) user.Password = password - res = h.DB.Updates(&user) + res = h.DB.Model(&model.AdminUser{}).Where("id", data.Id).UpdateColumn("password", password) if res.Error != nil { resp.ERROR(c, res.Error.Error()) return diff --git a/api/handler/admin/chat_app_handler.go b/api/handler/admin/chat_app_handler.go index f2ee1c83..a39210d1 100644 --- a/api/handler/admin/chat_app_handler.go +++ b/api/handler/admin/chat_app_handler.go @@ -8,7 +8,6 @@ package admin // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( - "fmt" "geekai/core" "geekai/core/middleware" "geekai/core/types" @@ -59,15 +58,14 @@ func (h *ChatAppHandler) Save(c *gin.Context) { resp.ERROR(c, types.InvalidArgs) return } + // 管理后台创建/编辑的 Gem 均为系统内置 + role.UserId = 0 + if data.SystemPrompt != "" { + role.SystemPrompt = data.SystemPrompt + } role.Id = data.Id if data.CreatedAt > 0 { role.CreatedAt = time.Unix(data.CreatedAt, 0) - } else { - err = h.DB.Where("marker", data.Key).First(&role).Error - if err == nil { - resp.ERROR(c, fmt.Sprintf("角色 %s 已存在", data.Key)) - return - } } err = h.DB.Save(&role).Error if err != nil { @@ -83,7 +81,8 @@ func (h *ChatAppHandler) Save(c *gin.Context) { func (h *ChatAppHandler) List(c *gin.Context) { var items []model.ChatApp var roles = make([]vo.ChatApp, 0) - res := h.DB.Order("sort_num ASC").Find(&items) + // 仅展示系统内置智能体(user_id = 0),不展示用户自行创建的智能体 + res := h.DB.Where("user_id = 0").Order("sort_num ASC").Find(&items) if res.Error != nil { resp.ERROR(c, "No data found") return diff --git a/api/handler/admin/config_handler.go b/api/handler/admin/config_handler.go index 8b8f7c94..56b7ebfe 100644 --- a/api/handler/admin/config_handler.go +++ b/api/handler/admin/config_handler.go @@ -8,6 +8,7 @@ package admin // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "errors" "geekai/core" "geekai/core/middleware" "geekai/core/types" @@ -35,6 +36,7 @@ type ConfigHandler struct { smtpService *service.SmtpService captchaService *service.CaptchaService wxLoginService *service.WxLoginService + wxGzhService *service.WxGzhService } func NewConfigHandler( @@ -49,6 +51,7 @@ func NewConfigHandler( smtpService *service.SmtpService, captchaService *service.CaptchaService, wxLoginService *service.WxLoginService, + wxChatService *service.WxGzhService, ) *ConfigHandler { return &ConfigHandler{ BaseHandler: handler.BaseHandler{App: app, DB: db}, @@ -61,6 +64,7 @@ func NewConfigHandler( smtpService: smtpService, captchaService: captchaService, wxLoginService: wxLoginService, + wxGzhService: wxChatService, } } @@ -84,6 +88,7 @@ func (h *ConfigHandler) RegisterRoutes() { rg.POST("update/oss", h.UpdateOss) rg.POST("update/smtp", h.UpdateStmp) rg.GET("get", h.Get) + rg.POST("update/wx_gzh", h.UpdateWxGzh) } } @@ -110,15 +115,18 @@ func (h *ConfigHandler) UpdateBase(c *gin.Context) { // UpdatePower 更新系统配置 func (h *ConfigHandler) UpdatePower(c *gin.Context) { var data struct { - InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值 - DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力 - InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值 - MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力 - MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 - SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力 - SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力 - LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力 - KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力 + InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值 + DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力 + InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值 + MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力 + MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 + MjUpscalePower int `json:"mj_upscale_power,omitempty"` // MJ 放大/变换消耗算力 + MjBlendPower int `json:"mj_blend_power,omitempty"` // MJ 融图消耗算力 + MjSwapFacePower int `json:"mj_swap_face_power,omitempty"` // MJ 换脸消耗算力 + MjModalPower int `json:"mj_modal_power,omitempty"` // MJ 局部重绘消耗算力 + SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力 + LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力 + KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力 } if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) @@ -130,7 +138,10 @@ func (h *ConfigHandler) UpdatePower(c *gin.Context) { h.sysConfig.Base.InvitePower = data.InvitePower h.sysConfig.Base.MjPower = data.MjPower h.sysConfig.Base.MjActionPower = data.MjActionPower - h.sysConfig.Base.SdPower = data.SdPower + h.sysConfig.Base.MjUpscalePower = data.MjUpscalePower + h.sysConfig.Base.MjBlendPower = data.MjBlendPower + h.sysConfig.Base.MjSwapFacePower = data.MjSwapFacePower + h.sysConfig.Base.MjModalPower = data.MjModalPower h.sysConfig.Base.SunoPower = data.SunoPower h.sysConfig.Base.LumaPower = data.LumaPower h.sysConfig.Base.KeLingPowers = data.KeLingPowers @@ -374,14 +385,19 @@ func (h *ConfigHandler) Update(name string, value any) error { func (h *ConfigHandler) Get(c *gin.Context) { name := c.Query("key") var config model.Config - res := h.DB.Where("name", name).First(&config) - if res.Error != nil { - resp.ERROR(c, res.Error.Error()) + err := h.DB.Where("name", name).First(&config).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + resp.SUCCESS(c, nil) + return + } + + if err != nil { + resp.ERROR(c, err.Error()) return } var value map[string]any - err := utils.JsonDecode(config.Value, &value) + err = utils.JsonDecode(config.Value, &value) if err != nil { resp.ERROR(c, err.Error()) return @@ -389,3 +405,20 @@ func (h *ConfigHandler) Get(c *gin.Context) { resp.SUCCESS(c, value) } + +func (h *ConfigHandler) UpdateWxGzh(c *gin.Context) { + var data types.WxGzhConfig + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + err := h.Update(types.ConfigKeyWxGzh, data) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + h.wxGzhService.UpdateConfig(data) + h.sysConfig.WxGzh = data + resp.SUCCESS(c, data) +} diff --git a/api/handler/admin/dashboard_handler.go b/api/handler/admin/dashboard_handler.go index 74befe6d..cce33263 100644 --- a/api/handler/admin/dashboard_handler.go +++ b/api/handler/admin/dashboard_handler.go @@ -123,22 +123,20 @@ func (h *DashboardHandler) Stats(c *gin.Context) { h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Count(&stats.TodayOrders) // 图片生成任务统计 - var mjJobs, sdJobs, dallJobs, jimengImageJobs int64 + var mjJobs, imageJobs, jimengImageJobs int64 h.DB.Model(&model.MidJourneyJob{}).Count(&mjJobs) - h.DB.Model(&model.SdJob{}).Count(&sdJobs) - h.DB.Model(&model.DallJob{}).Count(&dallJobs) + h.DB.Model(&model.ImageJob{}).Count(&imageJobs) h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Count(&jimengImageJobs) - stats.ImageJobs = mjJobs + sdJobs + dallJobs + jimengImageJobs + stats.ImageJobs = mjJobs + imageJobs + jimengImageJobs logger.Info("stats.ImageJobs", stats.ImageJobs) // 今日图片生成任务统计 - var todayMjJobs, todaySdJobs, todayDallJobs, todayJimengImageJobs int64 + var todayMjJobs, todayImageJobs, todayJimengImageJobs int64 h.DB.Model(&model.MidJourneyJob{}).Where("created_at > ?", zeroTime).Count(&todayMjJobs) - h.DB.Model(&model.SdJob{}).Where("created_at > ?", zeroTime).Count(&todaySdJobs) - h.DB.Model(&model.DallJob{}).Where("created_at > ?", zeroTime).Count(&todayDallJobs) + h.DB.Model(&model.ImageJob{}).Where("created_at > ?", zeroTime).Count(&todayImageJobs) h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Where("created_at > ?", zeroTime).Count(&todayJimengImageJobs) - stats.TodayImageJobs = todayMjJobs + todaySdJobs + todayDallJobs + todayJimengImageJobs + stats.TodayImageJobs = todayMjJobs + todayImageJobs + todayJimengImageJobs // 视频生成任务统计 var videoJobs, jimengVideoJobs int64 diff --git a/api/handler/admin/image_handler.go b/api/handler/admin/image_handler.go index 045cb367..9bec9668 100644 --- a/api/handler/admin/image_handler.go +++ b/api/handler/admin/image_handler.go @@ -42,8 +42,7 @@ func (h *ImageHandler) RegisterRoutes() { group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis)) { group.POST("list/mj", h.MjList) - group.POST("list/sd", h.SdList) - group.POST("list/dall", h.DallList) + group.POST("list/image", h.ImageList) group.GET("remove", h.Remove) } } @@ -100,8 +99,8 @@ func (h *ImageHandler) MjList(c *gin.Context) { resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) } -// SdList Stable Diffusion 任务列表 -func (h *ImageHandler) SdList(c *gin.Context) { +// ImageList Image generation 任务列表 +func (h *ImageHandler) ImageList(c *gin.Context) { var data imageQuery if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) @@ -123,59 +122,15 @@ func (h *ImageHandler) SdList(c *gin.Context) { session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) } var total int64 - session.Model(&model.SdJob{}).Count(&total) - var list []model.SdJob - var items = make([]vo.SdJob, 0) + session.Model(&model.ImageJob{}).Count(&total) + var list []model.ImageJob + var items = make([]vo.ImageJob, 0) offset := (data.Page - 1) * data.PageSize err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error if err == nil { // 填充数据 for _, item := range list { - var job vo.SdJob - err = utils.CopyObject(item, &job) - if err != nil { - continue - } - job.CreatedAt = item.CreatedAt.Unix() - items = append(items, job) - } - } - - resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) -} - -// DallList DALL-E 任务列表 -func (h *ImageHandler) DallList(c *gin.Context) { - var data imageQuery - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - - session := h.DB.Session(&gorm.Session{}) - if data.Username != "" { - var user model.User - err := h.DB.Where("username", data.Username).First(&user).Error - if err == nil { - session = session.Where("user_id", user.Id) - } - } - if data.Prompt != "" { - session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%") - } - if len(data.CreatedAt) == 2 { - session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) - } - var total int64 - session.Model(&model.DallJob{}).Count(&total) - var list []model.DallJob - var items = make([]vo.DallJob, 0) - offset := (data.Page - 1) * data.PageSize - err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error - if err == nil { - // 填充数据 - for _, item := range list { - var job vo.DallJob + var job vo.ImageJob err = utils.CopyObject(item, &job) if err != nil { continue @@ -209,8 +164,8 @@ func (h *ImageHandler) Remove(c *gin.Context) { remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) progress = job.Progress imgURL = job.ImgURL - case "sd": - var job model.SdJob + case "image": + var job model.ImageJob if res := h.DB.Where("id", id).First(&job); res.Error != nil { resp.ERROR(c, "记录不存在") return @@ -218,22 +173,7 @@ func (h *ImageHandler) Remove(c *gin.Context) { // 删除任务 tx.Delete(&job) - md = "stable-diffusion" - power = job.Power - userId = int(job.UserId) - remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) - progress = job.Progress - imgURL = job.ImgURL - case "dall": - var job model.DallJob - if res := h.DB.Where("id", id).First(&job); res.Error != nil { - resp.ERROR(c, "记录不存在") - return - } - - // 删除任务 - tx.Delete(&job) - md = "dall-e-3" + md = "image-generation" power = job.Power userId = int(job.UserId) remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) diff --git a/api/handler/admin/media_handler.go b/api/handler/admin/media_handler.go deleted file mode 100644 index 6510b5a8..00000000 --- a/api/handler/admin/media_handler.go +++ /dev/null @@ -1,215 +0,0 @@ -package admin - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * Copyright 2023 The Geek-AI Authors. All rights reserved. -// * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. -// * @Author yangjian102621@163.com -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import ( - "fmt" - "geekai/core" - "geekai/core/middleware" - "geekai/core/types" - "geekai/handler" - "geekai/service" - "geekai/service/oss" - "geekai/store/model" - "geekai/store/vo" - "geekai/utils" - "geekai/utils/resp" - - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -type MediaHandler struct { - handler.BaseHandler - userService *service.UserService - uploader *oss.UploaderManager -} - -func NewMediaHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *MediaHandler { - return &MediaHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager} -} - -// RegisterRoutes 注册路由 -func (h *MediaHandler) RegisterRoutes() { - group := h.App.Engine.Group("/api/admin/media/") - - // 需要管理员授权的接口 - group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis)) - { - group.POST("suno", h.SunoList) - group.POST("videos", h.Videos) - group.GET("remove", h.Remove) - } -} - -type mediaQuery struct { - Type string `json:"type"` // 任务类型 luma, keling - Prompt string `json:"prompt"` - Username string `json:"username"` - CreatedAt []string `json:"created_at"` - Page int `json:"page"` - PageSize int `json:"page_size"` -} - -// SunoList Suno 任务列表 -func (h *MediaHandler) SunoList(c *gin.Context) { - var data mediaQuery - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - - session := h.DB.Session(&gorm.Session{}) - if data.Username != "" { - var user model.User - err := h.DB.Where("username", data.Username).First(&user).Error - if err == nil { - session = session.Where("user_id", user.Id) - } - } - if data.Prompt != "" { - session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%") - } - if len(data.CreatedAt) == 2 { - session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) - } - var total int64 - session.Model(&model.SunoJob{}).Count(&total) - var list []model.SunoJob - var items = make([]vo.SunoJob, 0) - offset := (data.Page - 1) * data.PageSize - err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error - if err == nil { - // 填充数据 - for _, item := range list { - var job vo.SunoJob - err = utils.CopyObject(item, &job) - if err != nil { - continue - } - job.CreatedAt = item.CreatedAt.Unix() - items = append(items, job) - } - } - - resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) -} - -// Videos 视频任务列表 -func (h *MediaHandler) Videos(c *gin.Context) { - var data mediaQuery - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - - session := h.DB.Session(&gorm.Session{}).Where("type", data.Type) - if data.Username != "" { - var user model.User - err := h.DB.Where("username", data.Username).First(&user).Error - if err == nil { - session = session.Where("user_id", user.Id) - } - } - if data.Prompt != "" { - session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%") - } - if len(data.CreatedAt) == 2 { - session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) - } - var total int64 - session.Model(&model.VideoJob{}).Count(&total) - var list []model.VideoJob - var items = make([]vo.VideoJob, 0) - offset := (data.Page - 1) * data.PageSize - err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error - if err == nil { - // 填充数据 - for _, item := range list { - var job vo.VideoJob - err = utils.CopyObject(item, &job) - if err != nil { - continue - } - job.CreatedAt = item.CreatedAt.Unix() - if job.VideoURL == "" { - job.VideoURL = job.WaterURL - } - items = append(items, job) - } - } - - resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) -} - -func (h *MediaHandler) Remove(c *gin.Context) { - id := h.GetInt(c, "id", 0) - tab := c.Query("tab") - - tx := h.DB.Begin() - var md, remark, fileURL string - var power, userId, progress int - switch tab { - case "suno": - var job model.SunoJob - if err := h.DB.Where("id", id).First(&job).Error; err != nil { - resp.ERROR(c, "记录不存在") - return - } - tx.Delete(&job) - md = "suno" - power = job.Power - userId = int(job.UserId) - remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) - progress = job.Progress - fileURL = job.AudioURL - case "luma": - case "keling": - var job model.VideoJob - if res := h.DB.Where("id", id).First(&job); res.Error != nil { - resp.ERROR(c, "记录不存在") - return - } - - // 删除任务 - tx.Delete(&job) - md = job.Type - power = job.Power - userId = int(job.UserId) - remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) - progress = job.Progress - fileURL = job.VideoURL - if fileURL == "" { - fileURL = job.WaterURL - } - default: - resp.ERROR(c, types.InvalidArgs) - return - } - - if progress != 100 { - err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{ - Type: types.PowerRefund, - Model: md, - Remark: remark, - }) - if err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } - } - tx.Commit() - // remove image - err := h.uploader.GetUploadHandler().Delete(fileURL) - if err != nil { - logger.Error("remove image failed: ", err) - } - - resp.SUCCESS(c) -} diff --git a/api/handler/admin/moderation_handler.go b/api/handler/admin/moderation_handler.go index 86317332..b577a13d 100644 --- a/api/handler/admin/moderation_handler.go +++ b/api/handler/admin/moderation_handler.go @@ -212,12 +212,8 @@ func (h *ModerationHandler) GetSourceList(c *gin.Context) { "name": "Midjourney 绘图", }, { - "id": types.ModerationSourceDalle, - "name": "Dalle 绘图", - }, - { - "id": types.ModerationSourceSD, - "name": "StableDiffusion 绘图", + "id": types.ModerationSourceImage, + "name": "AI图像生成", }, { "id": types.ModerationSourceSuno, diff --git a/api/handler/admin/ppt_handler.go b/api/handler/admin/ppt_handler.go new file mode 100644 index 00000000..737bc836 --- /dev/null +++ b/api/handler/admin/ppt_handler.go @@ -0,0 +1,284 @@ +package admin + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "geekai/core" + "geekai/core/middleware" + "geekai/core/types" + "geekai/handler" + "geekai/service/ppt" + "geekai/store/model" + "geekai/utils" + "geekai/utils/resp" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// PPTHandler 管理后台 PPT 生成配置处理器 +type PPTHandler struct { + handler.BaseHandler + pptService *ppt.PptService +} + +// NewPPTHandler 创建管理后台 PPT 配置处理器 +func NewPPTHandler(app *core.AppServer, db *gorm.DB, pptService *ppt.PptService) *PPTHandler { + return &PPTHandler{ + BaseHandler: handler.BaseHandler{App: app, DB: db}, + pptService: pptService, + } +} + +// RegisterRoutes 注册 PPT 配置相关路由 +func (h *PPTHandler) RegisterRoutes() { + rg := h.App.Engine.Group("/api/admin/ppt/") + rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis)) + { + rg.GET("config", h.GetConfig) + rg.POST("config/update", h.UpdateConfig) + rg.GET("jobs", h.Jobs) + rg.GET("jobs/:task_id", h.JobDetail) + rg.GET("jobs/:task_id/export", h.ExportJob) + rg.GET("stats", h.Stats) + } +} + +// GetConfig 获取 PPT 生成配置 +func (h *PPTHandler) GetConfig(c *gin.Context) { + var cfg model.Config + err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + // 返回一个默认空配置 + resp.SUCCESS(c, types.PPTConfig{ + OutlineLLMModel: "gpt-4o-mini", + MaxSlidesPerTask: 30, + PowerCostPerSlide: 0, + MaxConcurrentRequests: 3, + QPSLimit: 1, + NanoBananaModel: "nano-banana", + NanoBananaAspectRatio: "16:9", + SeedreamSize: "1920x1080", + }) + return + } + resp.ERROR(c, "获取配置失败: "+err.Error()) + return + } + + var pptConfig types.PPTConfig + err = utils.JsonDecode(cfg.Value, &pptConfig) + if err != nil { + resp.ERROR(c, "解析配置失败: "+err.Error()) + return + } + + resp.SUCCESS(c, pptConfig) +} + +// UpdateConfig 更新 PPT 生成配置 +func (h *PPTHandler) UpdateConfig(c *gin.Context) { + var req types.PPTConfig + if err := c.ShouldBindJSON(&req); err != nil { + resp.ERROR(c, "参数错误") + return + } + + // 基础校验 + if req.MaxSlidesPerTask <= 0 { + resp.ERROR(c, "单个任务最多 PPT 页数必须大于 0") + return + } + + if req.PowerCostPerSlide < 0 { + resp.ERROR(c, "每张 PPT 图片消耗算力不能小于 0") + return + } + + if req.MaxConcurrentRequests <= 0 { + req.MaxConcurrentRequests = 3 + } + if req.QPSLimit <= 0 { + req.QPSLimit = 1 + } + + // 根据当前生图提供方做必填校验 + switch req.ActiveImageProvider { + case types.PPTImageProviderNanoBanana: + if req.NanoBananaApiURL == "" { + resp.ERROR(c, "Nano Banana API 地址不能为空") + return + } + if req.NanoBananaApiKey == "" { + resp.ERROR(c, "Nano Banana API Key 不能为空") + return + } + case types.PPTImageProviderSeedream: + if req.SeedreamBaseURL == "" { + resp.ERROR(c, "Seedream Base URL 不能为空") + return + } + if req.SeedreamApiKey == "" { + resp.ERROR(c, "Seedream API Key 不能为空") + return + } + if req.SeedreamModel == "" { + resp.ERROR(c, "Seedream 模型 ID 不能为空") + return + } + default: + // 允许为空,未来可以扩展更多 provider + } + + value := utils.JsonEncode(&req) + var cfg model.Config + err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + cfg.Name = types.ConfigKeyPPT + cfg.Value = value + if err = h.DB.Create(&cfg).Error; err != nil { + resp.ERROR(c, "创建配置失败: "+err.Error()) + return + } + resp.SUCCESS(c, gin.H{"message": "配置创建成功"}) + return + } + resp.ERROR(c, "获取配置失败: "+err.Error()) + return + } + + cfg.Value = value + if err = h.DB.Updates(&cfg).Error; err != nil { + resp.ERROR(c, "更新配置失败: "+err.Error()) + return + } + + resp.SUCCESS(c, gin.H{"message": "配置更新成功"}) +} + +// Jobs 管理后台查看 PPT 任务列表(内存任务) +func (h *PPTHandler) Jobs(c *gin.Context) { + page := h.GetInt(c, "page", 1) + pageSize := h.GetInt(c, "page_size", 20) + filterUserId := h.GetInt(c, "user_id", 0) + status := h.GetTrim(c, "status") + + filtered, total := h.pptService.ListAdminJobs(c.Request.Context(), page, pageSize, filterUserId, status) + + jobs := make([]gin.H, 0, len(filtered)) + for _, t := range filtered { + job := t.TaskSummaryMap() + job["user_id"] = t.UserID + job["error_message"] = t.ErrorMessage + jobs = append(jobs, job) + } + + resp.SUCCESS(c, gin.H{ + "jobs": jobs, + "total": total, + "page": page, + "page_size": pageSize, + }) +} + +func buildAdminPPTTaskDetail(task *ppt.Task) gin.H { + percentage := 0 + if task.Total > 0 { + percentage = int(float64(task.Completed) / float64(task.Total) * 100) + } + + return gin.H{ + "task_id": task.TaskID, + "user_id": task.UserID, + "status": task.Status, + "progress": gin.H{ + "total_slides": task.Total, + "completed_slides": task.Completed, + "percentage": percentage, + }, + "slides": task.Slides, + "error_message": task.ErrorMessage, + "content": task.Content, + "prompt": task.Prompt, + "title": task.Title, + "thumb": task.Thumb, + } +} + +// JobDetail 管理后台查看指定 PPT 任务详情 +func (h *PPTHandler) JobDetail(c *gin.Context) { + taskID := c.Param("task_id") + if taskID == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + + task, ok := h.pptService.GetTask(taskID) + if !ok { + resp.ERROR(c, "任务不存在") + return + } + h.pptService.EnsureTaskMeta(c.Request.Context(), task) + resp.SUCCESS(c, buildAdminPPTTaskDetail(task)) +} + +// ExportJob 管理后台导出 PPT 任务 +func (h *PPTHandler) ExportJob(c *gin.Context) { + taskID := c.Param("task_id") + if taskID == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + ef, ok := ppt.ParseExportFormat(c.Query("format")) + if !ok { + resp.ERROR(c, "format 参数无效,支持 pdf 或 pptx") + return + } + + task, exists := h.pptService.GetTask(taskID) + if !exists { + resp.ERROR(c, "任务不存在") + return + } + if task.Status != ppt.TaskStatusCompleted { + resp.ERROR(c, "仅已完成任务可导出") + return + } + + h.pptService.EnsureTaskMeta(c.Request.Context(), task) + + ossCfg := types.OSSConfig{} + if h.App.SysConfig != nil { + ossCfg = h.App.SysConfig.OSS + } + data, err := ppt.BuildExportBytes(c.Request.Context(), task.Slides, ef, ossCfg, h.App.Config) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + base := ppt.SanitizeExportBaseName(task.Title, task.TaskID) + filename := base + ppt.ExportFileExt(ef) + c.Header("Content-Disposition", ppt.ContentDispositionAttachment(filename)) + c.Data(200, ppt.ExportMimeType(ef), data) +} + +// Stats PPT 任务统计信息 +func (h *PPTHandler) Stats(c *gin.Context) { + total, completed, processing, failed, pending := h.pptService.Stats() + + resp.SUCCESS(c, gin.H{ + "totalTasks": total, + "completedTasks": completed, + "processingTasks": processing, + "failedTasks": failed, + "pendingTasks": pending, + }) +} diff --git a/api/handler/admin/suno_handler.go b/api/handler/admin/suno_handler.go new file mode 100644 index 00000000..4fbb66e8 --- /dev/null +++ b/api/handler/admin/suno_handler.go @@ -0,0 +1,137 @@ +package admin + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "fmt" + "geekai/core" + "geekai/core/middleware" + "geekai/core/types" + "geekai/handler" + "geekai/service" + "geekai/service/oss" + "geekai/store/model" + "geekai/store/vo" + "geekai/utils" + "geekai/utils/resp" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type SunoHandler struct { + handler.BaseHandler + userService *service.UserService + uploader *oss.UploaderManager +} + +func NewSunoHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *SunoHandler { + return &SunoHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager} +} + +// RegisterRoutes 注册路由 +func (h *SunoHandler) RegisterRoutes() { + group := h.App.Engine.Group("/api/admin/suno/") + + // 需要管理员授权的接口 + group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis)) + { + group.POST("list", h.SunoList) + group.GET("remove", h.Remove) + } +} + +type sunoQuery struct { + Title string `json:"title"` + Prompt string `json:"prompt"` + CreatedAt []string `json:"created_at"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +// SunoList Suno 任务列表 +func (h *SunoHandler) SunoList(c *gin.Context) { + var data sunoQuery + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + session := h.DB.Session(&gorm.Session{}) + if data.Title != "" { + session = session.Where("title LIKE ?", "%"+data.Title+"%") + } + if data.Prompt != "" { + // 同时查询 prompt 字段和 params JSON 字段中的 prompt + session = session.Where("prompt LIKE ? OR JSON_EXTRACT(params, '$.prompt') LIKE ?", "%"+data.Prompt+"%", "%"+data.Prompt+"%") + } + if len(data.CreatedAt) == 2 { + session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) + } + var total int64 + session.Model(&model.SunoJob{}).Count(&total) + var list []model.SunoJob + var items = make([]vo.SunoJob, 0) + offset := (data.Page - 1) * data.PageSize + err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error + if err == nil { + // 填充数据 + for _, item := range list { + var job vo.SunoJob + err = utils.CopyObject(item, &job) + if err != nil { + continue + } + job.CreatedAt = item.CreatedAt.Unix() + items = append(items, job) + } + } + + resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) +} + +func (h *SunoHandler) Remove(c *gin.Context) { + id := h.GetInt(c, "id", 0) + + tx := h.DB.Begin() + var job model.SunoJob + if err := h.DB.Where("id", id).First(&job).Error; err != nil { + resp.ERROR(c, "记录不存在") + return + } + + // 删除任务 + tx.Delete(&job) + md := "suno" + power := job.Power + userId := int(job.UserId) + remark := fmt.Sprintf("SUNO 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) + needRefund := job.Progress != 100 + fileURL := job.AudioURL + + if needRefund { + err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{ + Type: types.PowerRefund, + Model: md, + Remark: remark, + }) + if err != nil { + tx.Rollback() + resp.ERROR(c, err.Error()) + return + } + } + tx.Commit() + // remove file + err := h.uploader.GetUploadHandler().Delete(fileURL) + if err != nil { + logger.Error("remove file failed: ", err) + } + + resp.SUCCESS(c) +} diff --git a/api/handler/admin/user_handler.go b/api/handler/admin/user_handler.go index beeef80b..64e66478 100644 --- a/api/handler/admin/user_handler.go +++ b/api/handler/admin/user_handler.go @@ -8,6 +8,7 @@ package admin // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "bytes" "fmt" "geekai/core" "geekai/core/middleware" @@ -17,11 +18,16 @@ import ( "geekai/store/vo" "geekai/utils" "geekai/utils/resp" + "net/http" + "path/filepath" + "strconv" + "strings" "time" "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" "github.com/golang-jwt/jwt/v5" + "github.com/xuri/excelize/v2" "gorm.io/gorm" ) @@ -47,9 +53,220 @@ func (h *UserHandler) RegisterRoutes() { group.GET("loginLog", h.LoginLog) group.GET("genLoginLink", h.GenLoginLink) group.POST("resetPass", h.ResetPass) + group.GET("import/template", h.ImportTemplate) + group.POST("import", h.ImportUsers) } } +// ImportTemplate 下载用户导入模板 +func (h *UserHandler) ImportTemplate(c *gin.Context) { + f := excelize.NewFile() + sheetName := "Sheet1" + // 表头 + headers := []string{"用户名", "密码", "手机", "邮箱", "剩余算力", "启用状态"} + for i, title := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + _ = f.SetCellValue(sheetName, cell, title) + } + // 示例数据 + sample := []interface{}{"user001", "Passw0rd!", "13800000000", "user001@example.com", 100, 1} + for i, v := range sample { + cell, _ := excelize.CoordinatesToCellName(i+1, 2) + _ = f.SetCellValue(sheetName, cell, v) + } + + buf, err := f.WriteToBuffer() + if err != nil { + logger.Error("failed to generate user import template: ", err) + resp.ERROR(c, "生成模板失败") + return + } + + c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + c.Header("Content-Disposition", "attachment; filename=\"user_import_template.xlsx\"") + c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buf.Bytes()) +} + +// ImportUsers 批量导入用户 +func (h *UserHandler) ImportUsers(c *gin.Context) { + fileHeader, err := c.FormFile("file") + if err != nil { + resp.ERROR(c, "文件上传失败: "+err.Error()) + return + } + + ext := strings.ToLower(filepath.Ext(fileHeader.Filename)) + if ext != ".xlsx" { + resp.ERROR(c, "只支持 .xlsx 格式的 Excel 文件") + return + } + + src, err := fileHeader.Open() + if err != nil { + resp.ERROR(c, "无法读取上传文件: "+err.Error()) + return + } + defer src.Close() + + // 读取到内存,避免多次读取问题 + var buf bytes.Buffer + if _, err = buf.ReadFrom(src); err != nil { + resp.ERROR(c, "读取文件内容失败: "+err.Error()) + return + } + + excel, err := excelize.OpenReader(bytes.NewReader(buf.Bytes())) + if err != nil { + resp.ERROR(c, "解析 Excel 失败: "+err.Error()) + return + } + defer func() { + _ = excel.Close() + }() + + rows, err := excel.GetRows("Sheet1") + if err != nil { + resp.ERROR(c, "读取工作表失败: "+err.Error()) + return + } + if len(rows) < 2 { + resp.ERROR(c, "Excel 中没有可导入的数据") + return + } + + type rowError struct { + Row int `json:"row"` + Error string `json:"error"` + } + + var ( + successCount int + failedCount int + errorsList []rowError + ) + + usernameSet := make(map[string]struct{}) + + // 从第二行开始读取 + for index, row := range rows[1:] { + line := index + 2 // Excel 行号 + if len(row) == 0 { + continue + } + + get := func(i int) string { + if i < len(row) { + return strings.TrimSpace(row[i]) + } + return "" + } + + username := get(0) + password := get(1) + mobile := get(2) + email := get(3) + powerStr := get(4) + statusStr := get(5) + + // 基础校验 + if username == "" { + failedCount++ + errorsList = append(errorsList, rowError{Row: line, Error: "用户名不能为空"}) + continue + } + if _, ok := usernameSet[username]; ok { + failedCount++ + errorsList = append(errorsList, rowError{Row: line, Error: "同一文件中用户名重复"}) + continue + } + usernameSet[username] = struct{}{} + + if len(password) < 8 || len(password) > 16 { + failedCount++ + errorsList = append(errorsList, rowError{Row: line, Error: "密码必须为 8-16 位"}) + continue + } + + if mobile != "" && len(mobile) != 11 { + failedCount++ + errorsList = append(errorsList, rowError{Row: line, Error: "手机号必须为 11 位"}) + continue + } + + // 解析算力 + power := 0 + if powerStr != "" { + p, err := strconv.Atoi(powerStr) + if err != nil { + failedCount++ + errorsList = append(errorsList, rowError{Row: line, Error: "剩余算力必须为数字"}) + continue + } + if p < 0 { + failedCount++ + errorsList = append(errorsList, rowError{Row: line, Error: "剩余算力不能为负数"}) + continue + } + power = p + } + + // 解析启用状态 + status := true + if statusStr != "" { + switch strings.TrimSpace(statusStr) { + case "0", "否", "false", "停用": + status = false + case "1", "是", "true", "启用": + status = true + default: + failedCount++ + errorsList = append(errorsList, rowError{Row: line, Error: "启用状态只支持 1/是 或 0/否"}) + continue + } + } + + // 检查用户名是否已存在 + var exist model.User + if err = h.DB.Where("username = ?", username).First(&exist).Error; err == nil && exist.Id > 0 { + failedCount++ + errorsList = append(errorsList, rowError{Row: line, Error: "用户名已存在"}) + continue + } + + salt := utils.RandString(8) + u := model.User{ + Username: username, + Password: utils.GenPassword(password, salt), + Mobile: mobile, + Email: email, + Avatar: "/images/avatar/user.png", + Salt: salt, + Power: power, + Status: status, + ChatRoles: utils.JsonEncode([]string{}), + ChatConfig: "{}", + ChatModels: utils.JsonEncode([]int{}), + ExpiredTime: 0, // 长期有效 + Vip: false, + } + u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6)) + + if err = h.DB.Create(&u).Error; err != nil { + failedCount++ + errorsList = append(errorsList, rowError{Row: line, Error: "写入数据库失败: " + err.Error()}) + continue + } + + successCount++ + } + + resp.SUCCESS(c, gin.H{ + "success": successCount, + "failed": failedCount, + "errors": errorsList, + }) +} + // List 用户列表 func (h *UserHandler) List(c *gin.Context) { page := h.GetInt(c, "page", 1) @@ -96,17 +313,16 @@ func (h *UserHandler) List(c *gin.Context) { func (h *UserHandler) Save(c *gin.Context) { var data struct { - Id uint `json:"id"` - Password string `json:"password"` - Username string `json:"username"` - Mobile string `json:"mobile"` - Email string `json:"email"` - ChatRoles []string `json:"chat_roles"` - ChatModels []int `json:"chat_models"` - ExpiredTime string `json:"expired_time"` - Status bool `json:"status"` - Vip bool `json:"vip"` - Power int `json:"power"` + Id uint `json:"id"` + Password string `json:"password"` + Username string `json:"username"` + Mobile string `json:"mobile"` + Email string `json:"email"` + ChatModels []int `json:"chat_models"` + ExpiredTime string `json:"expired_time"` + Status bool `json:"status"` + Vip bool `json:"vip"` + Power int `json:"power"` } if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) @@ -128,11 +344,10 @@ func (h *UserHandler) Save(c *gin.Context) { user.Status = data.Status user.Vip = data.Vip user.Power = data.Power - user.ChatRoles = utils.JsonEncode(data.ChatRoles) user.ChatModels = utils.JsonEncode(data.ChatModels) user.ExpiredTime = utils.Str2stamp(data.ExpiredTime) - res = h.DB.Select("username", "mobile", "email", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user) + res = h.DB.Select("username", "mobile", "email", "status", "vip", "power", "chat_models_json", "expired_time").Updates(&user) if res.Error != nil { logger.Error("error with update database:", res.Error) @@ -184,7 +399,6 @@ func (h *UserHandler) Save(c *gin.Context) { Salt: salt, Power: data.Power, Status: true, - ChatRoles: utils.JsonEncode(data.ChatRoles), ChatConfig: "{}", ChatModels: utils.JsonEncode(data.ChatModels), ExpiredTime: utils.Str2stamp(data.ExpiredTime), @@ -278,10 +492,7 @@ func (h *UserHandler) Remove(c *gin.Context) { if err = tx.Where("user_id = ?", id).Delete(&model.MidJourneyJob{}).Error; err != nil { break } - if err = tx.Where("user_id = ?", id).Delete(&model.SdJob{}).Error; err != nil { - break - } - if err = tx.Where("user_id = ?", id).Delete(&model.DallJob{}).Error; err != nil { + if err = tx.Where("user_id = ?", id).Delete(&model.ImageJob{}).Error; err != nil { break } if err = tx.Where("user_id = ?", id).Delete(&model.SunoJob{}).Error; err != nil { diff --git a/api/handler/admin/video_handler.go b/api/handler/admin/video_handler.go new file mode 100644 index 00000000..35714779 --- /dev/null +++ b/api/handler/admin/video_handler.go @@ -0,0 +1,269 @@ +package admin + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "fmt" + "geekai/core" + "geekai/core/middleware" + "geekai/core/types" + "geekai/handler" + "geekai/service" + "geekai/service/oss" + "geekai/store/model" + "geekai/store/vo" + "geekai/utils" + "geekai/utils/resp" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// VideoHandler 管理后台视频生成处理器 +type VideoHandler struct { + handler.BaseHandler + userService *service.UserService + uploader *oss.UploaderManager +} + +// NewVideoHandler 创建管理后台视频生成处理器 +func NewVideoHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *VideoHandler { + return &VideoHandler{ + BaseHandler: handler.BaseHandler{App: app, DB: db}, + userService: userService, + uploader: manager, + } +} + +// RegisterRoutes 注册视频生成管理后台路由 +func (h *VideoHandler) RegisterRoutes() { + rg := h.App.Engine.Group("/api/admin/video/") + rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis)) + { + rg.GET("config", h.GetConfig) + rg.POST("config/update", h.UpdateConfig) + rg.POST("list", h.Videos) + rg.GET("remove", h.Remove) + } +} + +// GetConfig 获取视频生成配置 +func (h *VideoHandler) GetConfig(c *gin.Context) { + var config model.Config + err := h.DB.Where("name", types.ConfigKeyVideo).First(&config).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + // 返回空配置 + resp.SUCCESS(c, types.VideoConfig{ + ApiURL: "", + ApiKey: "", + VideoPowers: make(map[string]types.VideoModelPower), + }) + return + } + resp.ERROR(c, "获取配置失败: "+err.Error()) + return + } + + var videoConfig types.VideoConfig + err = utils.JsonDecode(config.Value, &videoConfig) + if err != nil { + resp.ERROR(c, "解析配置失败: "+err.Error()) + return + } + + resp.SUCCESS(c, videoConfig) +} + +// UpdateConfig 更新视频生成配置 +func (h *VideoHandler) UpdateConfig(c *gin.Context) { + var req types.VideoConfig + if err := c.ShouldBindJSON(&req); err != nil { + resp.ERROR(c, "参数错误") + return + } + + // 验证必填字段 + if req.ApiURL == "" { + resp.ERROR(c, "API地址不能为空") + return + } + if req.ApiKey == "" { + resp.ERROR(c, "API密钥不能为空") + return + } + + // 验证算力配置 + if len(req.VideoPowers) == 0 { + resp.ERROR(c, "请至少配置一个模型的算力") + return + } + + // 新的价格配置方式直接使用 power_config 中的 key(如 "fixed"、"5_720P" 等) + // 不再区分固定收费和按秒收费,所有价格配置都在 power_config 中 + for key, modelPower := range req.VideoPowers { + // 验证 provider + if modelPower.Provider == "" { + resp.ERROR(c, fmt.Sprintf("模型 %s 的 provider 不能为空", key)) + return + } + + // 验证 model + if modelPower.Model == "" { + resp.ERROR(c, fmt.Sprintf("模型 %s 的 model 不能为空", key)) + return + } + + // 验证 power_config + if len(modelPower.PowerConfig) == 0 { + resp.ERROR(c, fmt.Sprintf("模型 %s 的 power_config 不能为空", key)) + return + } + + // 验证 power_config 中的值必须大于0 + for configKey, configValue := range modelPower.PowerConfig { + if configValue <= 0 { + resp.ERROR(c, fmt.Sprintf("模型 %s 的 power_config.%s 必须大于0", key, configKey)) + return + } + } + + } + + // 保存配置 + tx := h.DB.Begin() + value := utils.JsonEncode(&req) + var exist model.Config + tx.Where("name", types.ConfigKeyVideo).First(&exist) + + if exist.Id > 0 { + exist.Value = value + err := tx.Updates(&exist).Error + if err != nil { + resp.ERROR(c, "更新配置失败: "+err.Error()) + tx.Rollback() + return + } + } else { + exist.Name = types.ConfigKeyVideo + exist.Value = value + err := tx.Create(&exist).Error + if err != nil { + resp.ERROR(c, "创建配置失败: "+err.Error()) + tx.Rollback() + return + } + } + + tx.Commit() + resp.SUCCESS(c, gin.H{"message": "配置更新成功"}) +} + +type videoQuery struct { + Type string `json:"type"` // 任务类型 luma, keling + Status string `json:"status"` // 任务状态 pending, in_progress, downloading, success, failed + Prompt string `json:"prompt"` + CreatedAt []string `json:"created_at"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +// Videos 视频任务列表 +func (h *VideoHandler) Videos(c *gin.Context) { + var data videoQuery + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + session := h.DB.Session(&gorm.Session{}) + if data.Type != "" { + session = session.Where("type", data.Type) + } + if data.Status != "" { + session = session.Where("status", data.Status) + } + if data.Prompt != "" { + session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%") + } + if len(data.CreatedAt) == 2 { + session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) + } + var total int64 + session.Model(&model.VideoJob{}).Count(&total) + var list []model.VideoJob + var items = make([]vo.VideoJob, 0) + offset := (data.Page - 1) * data.PageSize + err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error + if err == nil { + // 填充数据 + for _, item := range list { + var job vo.VideoJob + err = utils.CopyObject(item, &job) + if err != nil { + continue + } + job.CreatedAt = item.CreatedAt.Unix() + items = append(items, job) + } + } + + resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) +} + +func (h *VideoHandler) Remove(c *gin.Context) { + id := h.GetInt(c, "id", 0) + tab := c.Query("tab") + + tx := h.DB.Begin() + var md, remark, fileURL string + var power, userId int + var needRefund bool + + switch tab { + case "luma", "keling": + var job model.VideoJob + if res := h.DB.Where("id", id).First(&job); res.Error != nil { + resp.ERROR(c, "记录不存在") + return + } + + // 删除任务 + tx.Delete(&job) + md = job.Type + power = job.Power + userId = int(job.UserId) + remark = fmt.Sprintf("视频任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) + needRefund = job.Status != types.VideoStatusSuccess + fileURL = job.VideoURL + default: + resp.ERROR(c, types.InvalidArgs) + return + } + + if needRefund { + err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{ + Type: types.PowerRefund, + Model: md, + Remark: remark, + }) + if err != nil { + tx.Rollback() + resp.ERROR(c, err.Error()) + return + } + } + tx.Commit() + // remove file + err := h.uploader.GetUploadHandler().Delete(fileURL) + if err != nil { + logger.Error("remove file failed: ", err) + } + + resp.SUCCESS(c) +} diff --git a/api/handler/base_handler.go b/api/handler/base_handler.go index 229cd090..3b54041f 100644 --- a/api/handler/base_handler.go +++ b/api/handler/base_handler.go @@ -12,7 +12,7 @@ import ( "fmt" "geekai/core" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "geekai/store/model" "geekai/utils" "strings" @@ -22,7 +22,7 @@ import ( "github.com/gin-gonic/gin" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() type BaseHandler struct { App *core.AppServer diff --git a/api/handler/chat_app_handler.go b/api/handler/chat_app_handler.go index ba2c0772..9c8eb423 100644 --- a/api/handler/chat_app_handler.go +++ b/api/handler/chat_app_handler.go @@ -37,7 +37,11 @@ func (h *ChatAppHandler) RegisterRoutes() { group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) { group.GET("list/user", h.ListByUser) + group.POST("create", h.Create) + group.POST("copy", h.Copy) group.POST("update", h.UpdateApp) + group.POST("workspace", h.UpdateWorkArea) + group.POST("remove", h.Remove) } } @@ -45,7 +49,7 @@ func (h *ChatAppHandler) RegisterRoutes() { func (h *ChatAppHandler) List(c *gin.Context) { tid := h.GetInt(c, "tid", 0) var roles []model.ChatApp - session := h.DB.Where("enable", true) + session := h.DB.Where("enable = ? AND user_id = 0", true) if tid > 0 { session = session.Where("tid", tid) } @@ -61,6 +65,9 @@ func (h *ChatAppHandler) List(c *gin.Context) { err := utils.CopyObject(r, &v) if err == nil { v.Id = r.Id + if r.UserId == 0 { + v.SystemPrompt = "" + } roleVos = append(roleVos, v) } } @@ -72,23 +79,11 @@ func (h *ChatAppHandler) ListByUser(c *gin.Context) { id := h.GetInt(c, "id", 0) userId := h.GetLoginUserId(c) var roles []model.ChatApp - session := h.DB.Where("enable", true) - // 如果用户没登录,则获取所有角色 + session := h.DB.Where("enable = ?", true) if userId > 0 { - var user model.User - h.DB.First(&user, userId) - var roleKeys []string - if user.ChatRoles != "" { - err := utils.JsonDecode(user.ChatRoles, &roleKeys) - if err != nil { - resp.ERROR(c, "角色解析失败!") - return - } - } - // 保证用户至少有一个角色可用 - if len(roleKeys) > 0 { - session = session.Where("marker IN ?", roleKeys) - } + session = session.Where("(user_id = 0 OR user_id = ?)", userId) + } else { + session = session.Where("user_id = 0") } if id > 0 { @@ -106,33 +101,173 @@ func (h *ChatAppHandler) ListByUser(c *gin.Context) { err := utils.CopyObject(r, &v) if err == nil { v.Id = r.Id + if r.UserId == 0 { + v.SystemPrompt = "" + } roleVos = append(roleVos, v) } } resp.SUCCESS(c, roleVos) } -// UpdateApp 更新用户聊天应用 -func (h *ChatAppHandler) UpdateApp(c *gin.Context) { - user, err := h.GetLoginUser(c) - if err != nil { +// Create 用户创建智能体 +func (h *ChatAppHandler) Create(c *gin.Context) { + userId := h.GetLoginUserId(c) + if userId == 0 { resp.NotAuth(c) return } - - var data struct { - Keys []string `json:"keys"` - } - if err = c.ShouldBindJSON(&data); err != nil { + var data vo.ChatApp + if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) return } - - err = h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys)).Error - if err != nil { + role := model.ChatApp{ + Name: data.Name, + Tid: data.Tid, + UserId: userId, + SystemPrompt: data.SystemPrompt, + HelloMsg: data.HelloMsg, + Icon: data.Icon, + Enable: true, + SortNum: int(data.SortNum), + ModelId: data.ModelId, + } + if role.Icon == "" { + role.Icon = "/images/avatar/gpt.png" + } + if err := h.DB.Create(&role).Error; err != nil { resp.ERROR(c, err.Error()) return } - - resp.SUCCESS(c) + data.Id = role.Id + data.UserId = role.UserId + resp.SUCCESS(c, data) +} + +// Copy 用户复制智能体(复制为当前用户名下) +func (h *ChatAppHandler) Copy(c *gin.Context) { + userId := h.GetLoginUserId(c) + if userId == 0 { + resp.NotAuth(c) + return + } + var body struct { + SourceId uint `json:"source_id"` + } + if err := c.ShouldBindJSON(&body); err != nil || body.SourceId == 0 { + resp.ERROR(c, types.InvalidArgs) + return + } + var src model.ChatApp + if err := h.DB.First(&src, body.SourceId).Error; err != nil { + resp.ERROR(c, "智能体不存在") + return + } + role := model.ChatApp{ + Name: src.Name, + Tid: src.Tid, + UserId: userId, + SystemPrompt: src.SystemPrompt, + HelloMsg: src.HelloMsg, + Icon: src.Icon, + Enable: true, + SortNum: src.SortNum, + ModelId: src.ModelId, + } + if err := h.DB.Create(&role).Error; err != nil { + resp.ERROR(c, err.Error()) + return + } + resp.SUCCESS(c, gin.H{"id": role.Id}) +} + +// UpdateApp 更新用户聊天应用(仅允许更新自己创建的) +func (h *ChatAppHandler) UpdateApp(c *gin.Context) { + userId := h.GetLoginUserId(c) + if userId == 0 { + resp.NotAuth(c) + return + } + var data vo.ChatApp + if err := c.ShouldBindJSON(&data); err != nil || data.Id == 0 { + resp.ERROR(c, types.InvalidArgs) + return + } + var role model.ChatApp + if err := h.DB.First(&role, data.Id).Error; err != nil { + resp.ERROR(c, "智能体不存在") + return + } + if role.UserId != userId { + resp.ERROR(c, "无权限修改该智能体") + return + } + updates := map[string]interface{}{ + "name": data.Name, + "hello_msg": data.HelloMsg, + "icon": data.Icon, + "model_id": data.ModelId, + "system_prompt": data.SystemPrompt, + } + if err := h.DB.Model(&role).Updates(updates).Error; err != nil { + resp.ERROR(c, err.Error()) + return + } + resp.SUCCESS(c, nil) +} + +// UpdateWorkArea 更新用户工作区应用列表(存为应用 id 数组) +func (h *ChatAppHandler) UpdateWorkArea(c *gin.Context) { + userId := h.GetLoginUserId(c) + if userId == 0 { + resp.NotAuth(c) + return + } + var body struct { + Ids []uint `json:"ids"` + } + if err := c.ShouldBindJSON(&body); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + if err := h.DB.Model(&model.User{}).Where("id = ?", userId).Update("chat_roles_json", utils.JsonEncode(body.Ids)).Error; err != nil { + resp.ERROR(c, err.Error()) + return + } + resp.SUCCESS(c, nil) +} + +// Remove 删除用户智能体(仅允许删除自己创建的) +func (h *ChatAppHandler) Remove(c *gin.Context) { + userId := h.GetLoginUserId(c) + if userId == 0 { + resp.NotAuth(c) + return + } + var body struct { + Id uint `json:"id"` + } + _ = c.ShouldBindJSON(&body) + if body.Id == 0 { + body.Id = uint(h.GetInt(c, "id", 0)) + } + if body.Id == 0 { + resp.ERROR(c, types.InvalidArgs) + return + } + var role model.ChatApp + if err := h.DB.First(&role, body.Id).Error; err != nil { + resp.ERROR(c, "智能体不存在") + return + } + if role.UserId != userId { + resp.ERROR(c, "无权限删除该智能体") + return + } + if err := h.DB.Delete(&role).Error; err != nil { + resp.ERROR(c, err.Error()) + return + } + resp.SUCCESS(c, nil) } diff --git a/api/handler/chat_handler.go b/api/handler/chat_handler.go index 53356784..24832b68 100644 --- a/api/handler/chat_handler.go +++ b/api/handler/chat_handler.go @@ -95,20 +95,21 @@ func NewChatHandler(app *core.AppServer, // RegisterRoutes 注册路由 func (h *ChatHandler) RegisterRoutes() { group := h.App.Engine.Group("/api/chat/") + group.GET("detail", h.Detail) + group.GET("history", h.History) // 其他接口需要用户授权 group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) { group.Any("message", h.Chat) group.GET("list", h.List) - group.GET("detail", h.Detail) group.POST("update", h.Update) group.GET("remove", h.Remove) - group.GET("history", h.History) group.GET("clear", h.Clear) group.POST("tokens", h.Tokens) group.GET("stop", h.StopGenerate) group.POST("tts", h.TextToSpeech) } + } // Chat 处理聊天请求 @@ -272,7 +273,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C chatCtx := make([]any, 0) messages := make([]any, 0) if h.App.SysConfig.Base.EnableContext { - _ = utils.JsonDecode(input.ChatRole.Context, &messages) + _ = utils.JsonDecode(input.ChatRole.SystemPrompt, &messages) if h.App.SysConfig.Base.ContextDeep > 0 { var historyMessages []model.ChatMessage dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId) @@ -668,7 +669,10 @@ func (h *ChatHandler) saveChatHistory( files := make([]vo.File, 0) if strings.HasPrefix(req.Model, "sora") { video, err := h.soraService.DownloadVideoURL(message.Content) - if err == nil { + if err != nil { + logger.Error("failed to download video: ", err) + pushMessage(c, ChatEventError, "视频下载失败:"+err.Error()) + } else { files = append(files, *video) } } diff --git a/api/handler/config_handler.go b/api/handler/config_handler.go index 17742ea9..4198b300 100644 --- a/api/handler/config_handler.go +++ b/api/handler/config_handler.go @@ -8,7 +8,10 @@ package handler // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "errors" "geekai/core" + "geekai/core/types" + "geekai/service/oss" "geekai/store/model" "geekai/utils" "geekai/utils/resp" @@ -19,10 +22,16 @@ import ( type ConfigHandler struct { BaseHandler + uploaderManager *oss.UploaderManager + sysConfig *types.SystemConfig } -func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler { - return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}} +func NewConfigHandler(app *core.AppServer, db *gorm.DB, uploaderManager *oss.UploaderManager, sysConfig *types.SystemConfig) *ConfigHandler { + return &ConfigHandler{ + BaseHandler: BaseHandler{App: app, DB: db}, + uploaderManager: uploaderManager, + sysConfig: sysConfig, + } } // RegisterRoutes 注册路由 @@ -31,24 +40,44 @@ func (h *ConfigHandler) RegisterRoutes() { // 无需授权的接口 group.GET("get", h.Get) + group.GET("oss/thumb", h.GetOssThumbTemplate) } // Get 获取指定的系统配置 func (h *ConfigHandler) Get(c *gin.Context) { key := c.Query("key") var config model.Config - res := h.DB.Where("name", key).First(&config) - if res.Error != nil { - resp.ERROR(c, res.Error.Error()) + err := h.DB.Where("name", key).First(&config).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + resp.SUCCESS(c, nil) return } - var value map[string]any - err := utils.JsonDecode(config.Value, &value) if err != nil { resp.ERROR(c, err.Error()) return } + var value map[string]any + err = utils.JsonDecode(config.Value, &value) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + if key == types.ConfigKeyWxGzh { + delete(value, "secret") + delete(value, "token") + delete(value, "encoding_aes_key") + } resp.SUCCESS(c, value) } + +// GetOssThumbTemplate 获取当前存储引擎的缩略图模板 +func (h *ConfigHandler) GetOssThumbTemplate(c *gin.Context) { + template := h.uploaderManager.GetThumbTemplate() + resp.SUCCESS(c, gin.H{ + "template": template, + "active": h.sysConfig.OSS.Active, + }) +} diff --git a/api/handler/function_handler.go b/api/handler/function_handler.go index 0251629a..5a474398 100644 --- a/api/handler/function_handler.go +++ b/api/handler/function_handler.go @@ -13,7 +13,7 @@ import ( "geekai/core" "geekai/core/types" "geekai/service" - "geekai/service/dalle" + "geekai/service/image" "geekai/service/oss" "geekai/store/model" "geekai/store/vo" @@ -31,7 +31,7 @@ import ( type FunctionHandler struct { BaseHandler uploadManager *oss.UploaderManager - dallService *dalle.Service + imageService *image.Service userService *service.UserService } @@ -40,7 +40,7 @@ func NewFunctionHandler( db *gorm.DB, config *types.AppConfig, manager *oss.UploaderManager, - dallService *dalle.Service, + imageService *image.Service, userService *service.UserService) *FunctionHandler { return &FunctionHandler{ BaseHandler: BaseHandler{ @@ -48,7 +48,7 @@ func NewFunctionHandler( DB: db, }, uploadManager: manager, - dallService: dallService, + imageService: imageService, userService: userService, } } @@ -61,7 +61,7 @@ func (h *FunctionHandler) RegisterRoutes() { // 需要用户授权的接口 group.POST("weibo", h.WeiBo) group.POST("zaobao", h.ZaoBao) - group.POST("dalle3", h.Dall3) + group.POST("image3", h.Image3) } type resVo struct { @@ -176,8 +176,8 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) { resp.SUCCESS(c, strings.Join(builder, "\n\n")) } -// Dall3 DallE3 AI 绘图 -func (h *FunctionHandler) Dall3(c *gin.Context) { +// Image3 AI 图像生成 +func (h *FunctionHandler) Image3(c *gin.Context) { if err := h.checkAuth(c); err != nil { resp.ERROR(c, err.Error()) return @@ -209,9 +209,9 @@ func (h *FunctionHandler) Dall3(c *gin.Context) { return } - // create dall task + // create image task prompt := utils.InterfaceToString(params["prompt"]) - task := types.DallTask{ + task := types.ImageTask{ UserId: user.Id, Prompt: prompt, ModelId: chatModel.Id, @@ -220,11 +220,11 @@ func (h *FunctionHandler) Dall3(c *gin.Context) { TranslateModelId: h.App.SysConfig.Base.AssistantModelId, Power: chatModel.Power, } - job := model.DallJob{ - UserId: user.Id, - Prompt: prompt, - Power: chatModel.Power, - TaskInfo: utils.JsonEncode(task), + job := model.ImageJob{ + UserId: user.Id, + Prompt: prompt, + Power: chatModel.Power, + Params: utils.JsonEncode(task), } err := h.DB.Create(&job).Error if err != nil { @@ -233,7 +233,7 @@ func (h *FunctionHandler) Dall3(c *gin.Context) { } task.Id = job.Id - content, err := h.dallService.Image(task, true) + content, err := h.imageService.Image(task, true) if err != nil { resp.ERROR(c, "任务执行失败:"+err.Error()) return diff --git a/api/handler/dalle_handler.go b/api/handler/image_handler.go similarity index 79% rename from api/handler/dalle_handler.go rename to api/handler/image_handler.go index b8b00c3d..24d24980 100644 --- a/api/handler/dalle_handler.go +++ b/api/handler/image_handler.go @@ -3,7 +3,7 @@ package handler // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * Copyright 2023 The Geek-AI Authors. All rights reserved. // * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. +// * that can be found in LICENSE file. // * @Author yangjian102621@163.com // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -13,7 +13,7 @@ import ( "geekai/core/middleware" "geekai/core/types" "geekai/service" - "geekai/service/dalle" + "geekai/service/image" "geekai/service/moderation" "geekai/service/oss" "geekai/store/model" @@ -25,17 +25,17 @@ import ( "gorm.io/gorm" ) -type DallJobHandler struct { +type ImageJobHandler struct { BaseHandler - dallService *dalle.Service + imageService *image.Service uploader *oss.UploaderManager userService *service.UserService moderationManager *moderation.ServiceManager } -func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *DallJobHandler { - return &DallJobHandler{ - dallService: service, +func NewImageJobHandler(app *core.AppServer, db *gorm.DB, service *image.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *ImageJobHandler { + return &ImageJobHandler{ + imageService: service, uploader: manager, userService: userService, moderationManager: moderationManager, @@ -47,8 +47,8 @@ func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, } // RegisterRoutes 注册路由 -func (h *DallJobHandler) RegisterRoutes() { - group := h.App.Engine.Group("/api/dall/") +func (h *ImageJobHandler) RegisterRoutes() { + group := h.App.Engine.Group("/api/image/") // 公开接口,不需要授权 group.GET("imgWall", h.ImgWall) @@ -65,8 +65,8 @@ func (h *DallJobHandler) RegisterRoutes() { } // Image 创建一个绘画任务 -func (h *DallJobHandler) Image(c *gin.Context) { - var data types.DallTask +func (h *ImageJobHandler) Image(c *gin.Context) { + var data types.ImageTask if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" { resp.ERROR(c, types.InvalidArgs) return @@ -82,7 +82,7 @@ func (h *DallJobHandler) Image(c *gin.Context) { // 记录违规内容 moderation := model.Moderation{ UserId: h.GetLoginUserId(c), - Source: types.ModerationSourceDalle, + Source: types.ModerationSourceImage, Input: data.Prompt, Result: utils.JsonEncode(moderationResult), } @@ -114,7 +114,7 @@ func (h *DallJobHandler) Image(c *gin.Context) { idValue, _ := c.Get(types.LoginUserID) userId := utils.IntValue(utils.InterfaceToString(idValue), 0) - task := types.DallTask{ + task := types.ImageTask{ UserId: uint(userId), ModelId: chatModel.Id, ModelName: chatModel.Name, @@ -126,11 +126,11 @@ func (h *DallJobHandler) Image(c *gin.Context) { TranslateModelId: h.App.SysConfig.Base.AssistantModelId, Power: chatModel.Power, } - job := model.DallJob{ - UserId: uint(userId), - Prompt: data.Prompt, - Power: chatModel.Power, - TaskInfo: utils.JsonEncode(task), + job := model.ImageJob{ + UserId: uint(userId), + Prompt: data.Prompt, + Power: chatModel.Power, + Params: utils.JsonEncode(task), } res := h.DB.Create(&job) if res.Error != nil { @@ -139,7 +139,7 @@ func (h *DallJobHandler) Image(c *gin.Context) { } task.Id = job.Id - h.dallService.PushTask(task) + h.imageService.PushTask(task) // 扣减算力 err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{ @@ -155,7 +155,7 @@ func (h *DallJobHandler) Image(c *gin.Context) { } // ImgWall 照片墙 -func (h *DallJobHandler) ImgWall(c *gin.Context) { +func (h *ImageJobHandler) ImgWall(c *gin.Context) { page := h.GetInt(c, "page", 0) pageSize := h.GetInt(c, "page_size", 0) err, jobs := h.getData(true, 0, page, pageSize, true) @@ -167,8 +167,8 @@ func (h *DallJobHandler) ImgWall(c *gin.Context) { resp.SUCCESS(c, jobs) } -// JobList 获取 SD 任务列表 -func (h *DallJobHandler) JobList(c *gin.Context) { +// JobList 获取 Image 任务列表 +func (h *ImageJobHandler) JobList(c *gin.Context) { finish := h.GetBool(c, "finish") userId := h.GetLoginUserId(c) page := h.GetInt(c, "page", 0) @@ -185,7 +185,7 @@ func (h *DallJobHandler) JobList(c *gin.Context) { } // JobList 获取任务列表 -func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) { +func (h *ImageJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) { session := h.DB.Session(&gorm.Session{}) if finish { @@ -205,21 +205,22 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in } // 统计总数 var total int64 - session.Model(&model.DallJob{}).Count(&total) + session.Model(&model.ImageJob{}).Count(&total) - var items []model.DallJob + var items []model.ImageJob res := session.Find(&items) if res.Error != nil { return res.Error, vo.Page{} } - var jobs = make([]vo.DallJob, 0) + var jobs = make([]vo.ImageJob, 0) for _, item := range items { - var job vo.DallJob + var job vo.ImageJob err := utils.CopyObject(item, &job) if err != nil { continue } + job.CreatedAt = item.CreatedAt.Unix() jobs = append(jobs, job) } @@ -227,10 +228,10 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in } // Remove remove task image -func (h *DallJobHandler) Remove(c *gin.Context) { +func (h *ImageJobHandler) Remove(c *gin.Context) { id := h.GetInt(c, "id", 0) userId := h.GetLoginUserId(c) - var job model.DallJob + var job model.ImageJob if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil { resp.ERROR(c, "记录不存在") return @@ -253,12 +254,12 @@ func (h *DallJobHandler) Remove(c *gin.Context) { } // Publish 发布/取消发布图片到画廊显示 -func (h *DallJobHandler) Publish(c *gin.Context) { +func (h *ImageJobHandler) Publish(c *gin.Context) { id := h.GetInt(c, "id", 0) userId := h.GetLoginUserId(c) action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享 - err := h.DB.Model(&model.DallJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error + err := h.DB.Model(&model.ImageJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error if err != nil { resp.ERROR(c, err.Error()) return @@ -267,7 +268,7 @@ func (h *DallJobHandler) Publish(c *gin.Context) { resp.SUCCESS(c) } -func (h *DallJobHandler) GetModels(c *gin.Context) { +func (h *ImageJobHandler) GetModels(c *gin.Context) { var models []model.ChatModel err := h.DB.Where("type", "img").Where("enabled", true).Find(&models).Error if err != nil { diff --git a/api/handler/mj_handler.go b/api/handler/mj_handler.go index 245dc993..ea185a61 100644 --- a/api/handler/mj_handler.go +++ b/api/handler/mj_handler.go @@ -63,6 +63,7 @@ func (h *MidJourneyHandler) RegisterRoutes() { group.POST("image", h.Image) group.POST("upscale", h.Upscale) group.POST("variation", h.Variation) + group.POST("modal", h.Modal) group.GET("jobs", h.JobList) group.GET("remove", h.Remove) group.GET("publish", h.Publish) @@ -82,7 +83,31 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool { } return true +} +// preCheckPower 检查用户算力是否 >= required,不足时写 ERROR 并返回 false +func (h *MidJourneyHandler) preCheckPower(c *gin.Context, required int) bool { + user, err := h.GetLoginUser(c) + if err != nil { + resp.NotAuth(c) + return false + } + if required <= 0 { + required = h.App.SysConfig.Base.MjActionPower + } + if user.Power < required { + resp.ERROR(c, "当前用户剩余算力不足以完成本次操作!") + return false + } + return true +} + +// mjActionPower 取分项算力,若未配置则回退 MjActionPower +func mjActionPower(base int, fallback int) int { + if base > 0 { + return base + } + return fallback } // Image 创建一个绘画任务 @@ -109,7 +134,14 @@ func (h *MidJourneyHandler) Image(c *gin.Context) { resp.ERROR(c, types.InvalidArgs) return } - if !h.preCheck(c) { + // 按任务类型计算所需算力 + power := h.App.SysConfig.Base.MjPower + if data.TaskType == types.TaskBlend.String() { + power = mjActionPower(h.App.SysConfig.Base.MjBlendPower, h.App.SysConfig.Base.MjActionPower) + } else if data.TaskType == types.TaskSwapFace.String() { + power = mjActionPower(h.App.SysConfig.Base.MjSwapFacePower, h.App.SysConfig.Base.MjActionPower) + } + if !h.preCheckPower(c, power) { return } @@ -215,7 +247,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) { TaskInfo: utils.JsonEncode(task), Progress: 0, Prompt: fmt.Sprintf("%s %s", data.Prompt, params), - Power: h.App.SysConfig.Base.MjPower, + Power: power, CreatedAt: time.Now(), } opt := "绘图" @@ -264,7 +296,8 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) { return } - if !h.preCheck(c) { + power := mjActionPower(h.App.SysConfig.Base.MjUpscalePower, h.App.SysConfig.Base.MjActionPower) + if !h.preCheckPower(c, power) { return } @@ -286,7 +319,7 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) { TaskId: taskId, TaskInfo: utils.JsonEncode(task), Progress: 0, - Power: h.App.SysConfig.Base.MjActionPower, + Power: power, CreatedAt: time.Now(), } if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { @@ -319,7 +352,8 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) { return } - if !h.preCheck(c) { + power := mjActionPower(h.App.SysConfig.Base.MjUpscalePower, h.App.SysConfig.Base.MjActionPower) + if !h.preCheckPower(c, power) { return } @@ -342,7 +376,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) { TaskId: taskId, TaskInfo: utils.JsonEncode(task), Progress: 0, - Power: h.App.SysConfig.Base.MjActionPower, + Power: power, CreatedAt: time.Now(), } if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { @@ -366,6 +400,81 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) { resp.SUCCESS(c) } +// modalReq 局部重绘请求参数 +type modalReq struct { + TaskId string `json:"task_id"` // 原图任务 ID(message_id) + ChannelId string `json:"channel_id"` // 渠道 ID + Prompt string `json:"prompt"` // 提示词 + MaskBase64 string `json:"mask_base64,omitempty"` // 蒙版 base64,可选 +} + +// Modal 提交局部重绘(inpaint) +func (h *MidJourneyHandler) Modal(c *gin.Context) { + var data modalReq + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + if data.TaskId == "" || data.ChannelId == "" { + resp.ERROR(c, "task_id 与 channel_id 必填") + return + } + if data.Prompt == "" { + resp.ERROR(c, "请填写局部重绘提示词") + return + } + + power := mjActionPower(h.App.SysConfig.Base.MjModalPower, h.App.SysConfig.Base.MjActionPower) + if !h.preCheckPower(c, power) { + return + } + + idValue, _ := c.Get(types.LoginUserID) + userId := utils.IntValue(utils.InterfaceToString(idValue), 0) + taskId, _ := h.snowflake.Next(true) + // 原图 message_id 必须传入 API,同时写入 TaskId/MessageId 避免序列化 omitempty 丢失 + task := types.MjTask{ + Type: types.TaskModal, + UserId: userId, + ChannelId: data.ChannelId, + TaskId: data.TaskId, + MessageId: data.TaskId, + Prompt: data.Prompt, + MaskBase64: data.MaskBase64, + Mode: h.App.SysConfig.Base.MjMode, + } + job := model.MidJourneyJob{ + Type: types.TaskModal.String(), + ChannelId: data.ChannelId, + UserId: uint(userId), + TaskId: taskId, + TaskInfo: utils.JsonEncode(task), + Progress: 0, + Prompt: data.Prompt, + Power: power, + CreatedAt: time.Now(), + } + if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { + resp.ERROR(c, "添加任务失败:"+res.Error.Error()) + return + } + + task.Id = job.Id + h.mjService.PushTask(task) + + err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ + Type: types.PowerConsume, + Model: "mid-journey", + Remark: fmt.Sprintf("局部重绘操作,任务ID:%s", job.TaskId), + }) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c) +} + // ImgWall 照片墙 func (h *MidJourneyHandler) ImgWall(c *gin.Context) { page := h.GetInt(c, "page", 0) @@ -432,6 +541,7 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize if err != nil { continue } + job.CreatedAt = item.CreatedAt.Unix() jobs = append(jobs, job) } return nil, vo.NewPage(total, page, pageSize, jobs) diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go index 1db2f268..404f519c 100644 --- a/api/handler/payment_handler.go +++ b/api/handler/payment_handler.go @@ -216,7 +216,7 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) { data.Domain = h.config.WxPay.Domain } notifyURL = fmt.Sprintf("%s/api/payment/notify/wxpay", data.Domain) - payURL, err = h.wxpayService.Pay(payment.PayRequest{ + params := payment.PayRequest{ OutTradeNo: orderNo, TotalFee: fmt.Sprintf("%d", int(amount*100)), Subject: product.Name, @@ -224,7 +224,11 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) { ClientIP: c.ClientIP(), Device: data.Device, PayWay: payment.PayWayWX, - }) + } + if data.Device == "mobile" { + params.OpenID = user.OpenId + } + payURL, err = h.wxpayService.Pay(params) if err != nil { resp.ERROR(c, err.Error()) return diff --git a/api/handler/ppt_task_handler.go b/api/handler/ppt_task_handler.go new file mode 100644 index 00000000..7d0f28e4 --- /dev/null +++ b/api/handler/ppt_task_handler.go @@ -0,0 +1,547 @@ +package handler + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "context" + "errors" + "fmt" + "geekai/core" + "geekai/core/middleware" + "geekai/core/types" + "geekai/service" + "geekai/service/ppt" + "geekai/utils" + "geekai/utils/resp" + "net/http" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// PPTTaskHandler 用户侧 PPT 生成任务处理器(薄层:参数、鉴权、调用 PptService) +type PPTTaskHandler struct { + BaseHandler + snowflake *service.Snowflake + pptService *ppt.PptService +} + +// NewPPTTaskHandler 创建 PPT 任务处理器 +func NewPPTTaskHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, pptService *ppt.PptService) *PPTTaskHandler { + return &PPTTaskHandler{ + BaseHandler: BaseHandler{App: app, DB: db}, + snowflake: snowflake, + pptService: pptService, + } +} + +// RegisterRoutes 注册 PPT 任务相关路由 +func (h *PPTTaskHandler) RegisterRoutes() { + group := h.App.Engine.Group("/api/v1/tasks") + group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) + { + group.POST("generate-slides", h.CreateTask) + group.POST("generate-slides/from-file", h.CreateTaskFromFile) + group.GET("", h.ListTasks) // 当前用户任务列表,须在 :task_id 前注册 + group.GET(":task_id/export", h.ExportTask) + group.POST(":task_id/resume", h.ResumeTask) + group.POST(":task_id/slides/:slide_index/edit-image", h.EditSlideImage) + group.PATCH(":task_id/slides/:slide_index/active-image", h.SetActiveSlideImage) + group.GET(":task_id", h.GetTask) + group.DELETE(":task_id", h.DeleteTask) + } +} + +type createPPTTaskRequest struct { + Content string `json:"content"` + Prompt string `json:"prompt"` + Language string `json:"language"` // 如 zh-CN, en,约束分镜输出语言 + Pages int `json:"pages"` // 目标页数,0 表示用配置默认值 + Mode string `json:"mode"` // 生成模式:detailed=详细演示文稿,slides=演示用幻灯片,空则默认 slides +} + +type createPPTTaskFromFileRequest struct { + FileURL string `json:"file_url"` + Prompt string `json:"prompt"` + Language string `json:"language"` // 如 zh-CN, en,约束分镜输出语言 + Pages int `json:"pages"` // 目标页数,0 表示用配置默认值 + Mode string `json:"mode"` // 生成模式:detailed=详细演示文稿,slides=演示用幻灯片,空则默认 slides +} + +// CreateTask 创建 PPT 生成任务(异步) +func (h *PPTTaskHandler) CreateTask(c *gin.Context) { + var req createPPTTaskRequest + if err := c.ShouldBindJSON(&req); err != nil || req.Content == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + + user, err := h.GetLoginUser(c) + if err != nil { + resp.NotAuth(c) + return + } + + taskID, err := h.snowflake.Next(true) + if err != nil { + resp.ERROR(c, "生成任务ID失败:"+err.Error()) + return + } + + task, cfg, err := h.pptService.BuildPendingTask(taskID, user.Id, int(user.Power), req.Content, req.Prompt, req.Language, req.Mode, req.Pages) + if err != nil { + if errors.Is(err, ppt.ErrInsufficientPower) { + resp.ERROR(c, "当前用户算力不足以完成本次 PPT 生成任务!") + return + } + resp.ERROR(c, "加载 PPT 配置或校验失败:"+err.Error()) + return + } + + h.pptService.CreateTask(c.Request.Context(), task, cfg) + go h.pptService.RunTask(context.Background(), task, cfg) + + resp.SUCCESS(c, map[string]any{ + "task_id": taskID, + "status": ppt.TaskStatusPending, + }) +} + +// CreateTaskFromFile 创建 PPT 生成任务(基于上传材料文本提炼) +// 支持文件:PDF、Word(doc/docx)、TXT、Markdown(md/markdown) +func (h *PPTTaskHandler) CreateTaskFromFile(c *gin.Context) { + var req createPPTTaskFromFileRequest + if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.FileURL) == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + + fileURL := strings.TrimSpace(req.FileURL) + + // 去掉 query,避免 ".pdf?xxx" 这种导致 ext 识别失败 + urlNoQuery := strings.Split(fileURL, "?")[0] + ext := strings.ToLower(filepath.Ext(urlNoQuery)) + if ext == "" { + resp.ERROR(c, "不支持的文件格式") + return + } + + allowedExts := map[string]bool{ + ".pdf": true, + ".doc": true, + ".docx": true, + ".txt": true, + ".md": true, + ".markdown": true, + } + if !allowedExts[ext] { + resp.ERROR(c, "不支持的文件格式") + return + } + + designPrompt := strings.TrimSpace(req.Prompt) + language := strings.TrimSpace(req.Language) + if language == "" { + language = "zh-CN" + } + mode := strings.TrimSpace(req.Mode) + + pages := req.Pages + + user, err := h.GetLoginUser(c) + if err != nil { + resp.NotAuth(c) + return + } + + taskID, err := h.snowflake.Next(true) + if err != nil { + resp.ERROR(c, "生成任务ID失败:"+err.Error()) + return + } + + // 1) 下载文件并提取原始文本(Notebook 风格提炼前的输入) + rawText, err := h.extractMaterialTextFromURL(c.Request.Context(), fileURL, ext) + if err != nil || strings.TrimSpace(rawText) == "" { + if err == nil { + err = errors.New("empty extracted text") + } + resp.ERROR(c, "读取文件失败:"+err.Error()) + return + } + + // 2) 校验算力与页数,组装待写入的 Task(先在提炼前做 power 检查) + task, cfg, err := h.pptService.BuildPendingTask(taskID, user.Id, int(user.Power), rawText, designPrompt, language, mode, pages) + if err != nil { + if errors.Is(err, ppt.ErrInsufficientPower) { + resp.ERROR(c, "当前用户算力不足以完成本次 PPT 生成任务!") + return + } + resp.ERROR(c, "加载 PPT 配置或校验失败:"+err.Error()) + return + } + + // 3) NotebookLM 风格提炼:rawText -> PPT 可用的 content(大纲/结构化要点) + notebookContent, err := h.pptService.GenerateNotebookContent(c.Request.Context(), cfg, rawText, designPrompt, language) + if err != nil || strings.TrimSpace(notebookContent) == "" { + if err == nil { + err = errors.New("empty notebook content") + } + resp.ERROR(c, "提炼材料失败:"+err.Error()) + return + } + task.Content = notebookContent + + // 4) 落库 + 异步生成分镜与图片 + err = h.pptService.CreateTask(c.Request.Context(), task, cfg) + if err != nil { + resp.ERROR(c, "创建任务失败:"+err.Error()) + return + } + go h.pptService.RunTask(context.Background(), task, cfg) + + resp.SUCCESS(c, map[string]any{ + "task_id": taskID, + "status": ppt.TaskStatusPending, + }) +} + +func (h *PPTTaskHandler) extractMaterialTextFromURL(ctx context.Context, fileURL string, ext string) (string, error) { + if strings.TrimSpace(fileURL) == "" { + return "", errors.New("empty file url") + } + + switch ext { + case ".txt", ".md", ".markdown": + b, status, err := utils.FetchURLBytes(ctx, fileURL, "", 30*time.Second, 2, 8<<20) + if err != nil { + // status=0 时通常是请求阶段错误(例如 TLS 握手超时) + return "", fmt.Errorf("download file failed: status=%d: %w", status, err) + } + return strings.TrimSpace(string(b)), nil + default: + // PDF/Word 等走 Tika + return utils.ReadFileContent(fileURL, h.App.Config.TikaHost) + } +} + +// ListTasks 当前用户的 PPT 任务列表(分页) +func (h *PPTTaskHandler) ListTasks(c *gin.Context) { + user, err := h.GetLoginUser(c) + if err != nil { + resp.NotAuth(c) + return + } + + page := h.GetInt(c, "page", 1) + pageSize := h.GetInt(c, "page_size", 20) + if page <= 0 { + page = 1 + } + if pageSize <= 0 || pageSize > 100 { + pageSize = 20 + } + + slice, total := h.pptService.ListUserTasks(c.Request.Context(), user.Id, page, pageSize) + + jobs := make([]map[string]any, 0, len(slice)) + for _, t := range slice { + job := t.TaskSummaryMap() + job["prompt"] = t.Prompt + if t.ErrorMessage != "" { + job["error_message"] = t.ErrorMessage + } + jobs = append(jobs, job) + } + + resp.SUCCESS(c, map[string]any{ + "jobs": jobs, + "total": total, + "page": page, + "page_size": pageSize, + }) +} + +// ExportTask 导出任务幻灯片为 PDF 或 PPTX(按图片逐页) +func (h *PPTTaskHandler) ExportTask(c *gin.Context) { + taskID := strings.TrimSpace(c.Param("task_id")) + if taskID == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + ef, ok := ppt.ParseExportFormat(c.Query("format")) + if !ok { + resp.ERROR(c, "format 参数无效,支持 pdf 或 pptx") + return + } + + user, err := h.GetLoginUser(c) + if err != nil { + resp.NotAuth(c) + return + } + + task, exists := h.pptService.GetTask(taskID) + if !exists { + resp.ERROR(c, "任务不存在") + return + } + if user.Id != task.UserID { + resp.NotAuth(c) + return + } + if task.Status != ppt.TaskStatusCompleted { + resp.ERROR(c, "仅已完成任务可导出") + return + } + + ossCfg := types.OSSConfig{} + if h.App.SysConfig != nil { + ossCfg = h.App.SysConfig.OSS + } + data, err := ppt.BuildExportBytes(c.Request.Context(), task.Slides, ef, ossCfg, h.App.Config) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + base := ppt.SanitizeExportBaseName(task.Title, task.TaskID) + filename := base + ppt.ExportFileExt(ef) + c.Header("Content-Disposition", ppt.ContentDispositionAttachment(filename)) + c.Data(http.StatusOK, ppt.ExportMimeType(ef), data) +} + +// ResumeTask 继续生成缺图页(POST /api/v1/tasks/:task_id/resume) +func (h *PPTTaskHandler) ResumeTask(c *gin.Context) { + taskID := strings.TrimSpace(c.Param("task_id")) + if taskID == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + + user, err := h.GetLoginUser(c) + if err != nil { + resp.NotAuth(c) + return + } + + err = h.pptService.ResumeTask(c.Request.Context(), taskID, user.Id) + if err != nil { + if errors.Is(err, ppt.ErrPptTaskNotFound) { + resp.ERROR(c, "任务不存在") + return + } + if errors.Is(err, ppt.ErrPptTaskBusy) { + resp.ERROR(c, "任务正在处理中,请稍后再试") + return + } + if errors.Is(err, ppt.ErrPptTaskNotResumable) { + resp.ERROR(c, "当前任务无法继续生成(已完成或分镜数据不完整)") + return + } + if errors.Is(err, ppt.ErrInsufficientPower) { + resp.ERROR(c, "当前用户算力不足以完成剩余图片生成") + return + } + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, map[string]any{ + "task_id": taskID, + "status": ppt.TaskStatusProcessing, + }) +} + +type editSlideImageRequest struct { + Prompt string `json:"prompt"` +} + +type activeSlideImageRequest struct { + VersionIndex int `json:"version_index"` +} + +// EditSlideImage 图生图编辑当前页(基于激活图) +func (h *PPTTaskHandler) EditSlideImage(c *gin.Context) { + taskID := strings.TrimSpace(c.Param("task_id")) + slideIndexStr := strings.TrimSpace(c.Param("slide_index")) + if taskID == "" || slideIndexStr == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + slideIndex, err := strconv.Atoi(slideIndexStr) + if err != nil || slideIndex < 1 { + resp.ERROR(c, types.InvalidArgs) + return + } + + var req editSlideImageRequest + if err := c.ShouldBindJSON(&req); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + user, err := h.GetLoginUser(c) + if err != nil { + resp.NotAuth(c) + return + } + + ossCfg := types.OSSConfig{} + if h.App.SysConfig != nil { + ossCfg = h.App.SysConfig.OSS + } + + slides, err := h.pptService.EditSlideImage(c.Request.Context(), taskID, user.Id, slideIndex, req.Prompt, ossCfg, h.App.Config) + if err != nil { + if errors.Is(err, ppt.ErrPptTaskNotFound) { + resp.ERROR(c, "任务不存在") + return + } + if errors.Is(err, ppt.ErrPptSlideNotFound) { + resp.ERROR(c, "幻灯片不存在") + return + } + if errors.Is(err, ppt.ErrPptSlideNoImage) { + resp.ERROR(c, "该页暂无配图,无法编辑") + return + } + if errors.Is(err, ppt.ErrInsufficientPower) { + resp.ERROR(c, "当前用户算力不足以完成本次编辑") + return + } + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, map[string]any{"slides": slides}) +} + +// SetActiveSlideImage 切换当前页激活的历史版本 +func (h *PPTTaskHandler) SetActiveSlideImage(c *gin.Context) { + taskID := strings.TrimSpace(c.Param("task_id")) + slideIndexStr := strings.TrimSpace(c.Param("slide_index")) + if taskID == "" || slideIndexStr == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + slideIndex, err := strconv.Atoi(slideIndexStr) + if err != nil || slideIndex < 1 { + resp.ERROR(c, types.InvalidArgs) + return + } + + var req activeSlideImageRequest + if err := c.ShouldBindJSON(&req); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + user, err := h.GetLoginUser(c) + if err != nil { + resp.NotAuth(c) + return + } + + slides, err := h.pptService.SetActiveSlideVersion(taskID, user.Id, slideIndex, req.VersionIndex) + if err != nil { + if errors.Is(err, ppt.ErrPptTaskNotFound) { + resp.ERROR(c, "任务不存在") + return + } + if errors.Is(err, ppt.ErrPptSlideNotFound) { + resp.ERROR(c, "幻灯片不存在") + return + } + if errors.Is(err, ppt.ErrPptInvalidVersionIndex) { + resp.ERROR(c, "无效的历史版本序号") + return + } + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, map[string]any{"slides": slides}) +} + +// GetTask 查询 PPT 任务进度 +func (h *PPTTaskHandler) GetTask(c *gin.Context) { + taskId := c.Param("task_id") + if taskId == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + + task, ok := h.pptService.GetTask(taskId) + if !ok { + resp.ERROR(c, "任务不存在") + return + } + + user, err := h.GetLoginUser(c) + if err != nil || user.Id != task.UserID { + resp.NotAuth(c) + return + } + + percentage := 0 + if task.Total > 0 { + percentage = int(float64(task.Completed) / float64(task.Total) * 100) + } + + resp.SUCCESS(c, map[string]any{ + "task_id": task.TaskID, + "status": task.Status, + "progress": map[string]any{ + "total_slides": task.Total, + "completed_slides": task.Completed, + "percentage": percentage, + }, + "slides": task.Slides, + "error_message": task.ErrorMessage, + "content": task.Content, + "prompt": task.Prompt, + "title": task.Title, + "thumb": task.Thumb, + }) +} + +// DeleteTask 删除用户 PPT 任务(仅允许 completed / failed),同时删除该任务生成的图片对象。 +func (h *PPTTaskHandler) DeleteTask(c *gin.Context) { + taskID := c.Param("task_id") + if taskID == "" { + resp.ERROR(c, types.InvalidArgs) + return + } + + user, err := h.GetLoginUser(c) + if err != nil { + resp.NotAuth(c) + return + } + + if err := h.pptService.DeleteTask(taskID, user.Id); err != nil { + if errors.Is(err, ppt.ErrPptTaskNotFound) { + resp.ERROR(c, "任务不存在") + return + } + if errors.Is(err, ppt.ErrPptTaskNotDeletable) { + resp.ERROR(c, "仅允许删除已完成/已失败任务") + return + } + // 前端会统一在文案里拼接“删除失败:”,这里避免重复。 + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, gin.H{"message": "删除成功"}) +} diff --git a/api/handler/realtime_handler.go b/api/handler/realtime_handler.go index f66e891a..69507127 100644 --- a/api/handler/realtime_handler.go +++ b/api/handler/realtime_handler.go @@ -179,7 +179,7 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) { } apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL) logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, "advanced-voice") - r, err := client.R().SetHeader("Body-Type", "application/json"). + r, err := client.R().SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bearer "+apiKey.Value). SetBody(types.ApiRequest{ Model: "advanced-voice", @@ -221,11 +221,12 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) { return } - logger.Infof("Response: %v", response.Choices[0].Message.Content) + replyText := utils.NormalizeAssistantContent(response.Choices[0].Message.Content) + logger.Infof("Response: %v", replyText) // 提取链接 re := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`) - links := re.FindAllStringSubmatch(response.Choices[0].Message.Content, -1) + links := re.FindAllStringSubmatch(replyText, -1) var url = "" if len(links) > 0 { url = links[0][2] diff --git a/api/handler/sd_handler.go b/api/handler/sd_handler.go deleted file mode 100644 index ce19f42f..00000000 --- a/api/handler/sd_handler.go +++ /dev/null @@ -1,328 +0,0 @@ -package handler - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * Copyright 2023 The Geek-AI Authors. All rights reserved. -// * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. -// * @Author yangjian102621@163.com -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import ( - "fmt" - "geekai/core" - "geekai/core/middleware" - "geekai/core/types" - "geekai/service" - "geekai/service/moderation" - "geekai/service/oss" - "geekai/service/sd" - "geekai/store" - "geekai/store/model" - "geekai/store/vo" - "geekai/utils" - "geekai/utils/resp" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-redis/redis/v8" - "gorm.io/gorm" -) - -type SdJobHandler struct { - BaseHandler - redis *redis.Client - sdService *sd.Service - uploader *oss.UploaderManager - snowflake *service.Snowflake - leveldb *store.LevelDB - userService *service.UserService - moderationManager *moderation.ServiceManager -} - -func NewSdJobHandler(app *core.AppServer, - db *gorm.DB, - service *sd.Service, - manager *oss.UploaderManager, - snowflake *service.Snowflake, - userService *service.UserService, - levelDB *store.LevelDB, - moderationManager *moderation.ServiceManager) *SdJobHandler { - return &SdJobHandler{ - sdService: service, - uploader: manager, - snowflake: snowflake, - leveldb: levelDB, - userService: userService, - moderationManager: moderationManager, - BaseHandler: BaseHandler{ - App: app, - DB: db, - }, - } -} - -// RegisterRoutes 注册路由 -func (h *SdJobHandler) RegisterRoutes() { - group := h.App.Engine.Group("/api/sd/") - - // 公开接口,不需要授权 - group.GET("imgWall", h.ImgWall) - - // 需要用户授权的接口 - group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) - { - group.POST("image", h.Image) - group.GET("jobs", h.JobList) - group.GET("remove", h.Remove) - group.GET("publish", h.Publish) - } -} - -func (h *SdJobHandler) preCheck(c *gin.Context) bool { - user, err := h.GetLoginUser(c) - if err != nil { - resp.NotAuth(c) - return false - } - - if user.Power < h.App.SysConfig.Base.SdPower { - resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!") - return false - } - - return true - -} - -// Image 创建一个绘画任务 -func (h *SdJobHandler) Image(c *gin.Context) { - if !h.preCheck(c) { - return - } - - var data types.SdTaskParams - if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" { - resp.ERROR(c, types.InvalidArgs) - return - } - - if h.App.SysConfig.Moderation.Enable { - moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt) - if err != nil { - logger.Error("failed to moderate content: ", err) - } - if moderationResult.Flagged { - // 记录违规内容 - moderation := model.Moderation{ - UserId: h.GetLoginUserId(c), - Source: types.ModerationSourceSD, - Input: data.Prompt, - Result: utils.JsonEncode(moderationResult), - } - err = h.DB.Create(&moderation).Error - if err != nil { - logger.Error("failed to save moderation: ", err) - } - resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!") - return - } - - } - - if data.Width <= 0 { - data.Width = 512 - } - if data.Height <= 0 { - data.Height = 512 - } - if data.CfgScale <= 0 { - data.CfgScale = 7 - } - if data.Seed == 0 { - data.Seed = -1 - } - if data.Steps <= 0 { - data.Steps = 20 - } - if data.Sampler == "" { - data.Sampler = "Euler a" - } - - idValue, _ := c.Get(types.LoginUserID) - userId := utils.IntValue(utils.InterfaceToString(idValue), 0) - taskId, err := h.snowflake.Next(true) - if err != nil { - resp.ERROR(c, "error with generate task id: "+err.Error()) - return - } - - task := types.SdTask{ - Type: types.TaskImage, - Params: types.SdTaskParams{ - TaskId: taskId, - Prompt: data.Prompt, - NegPrompt: data.NegPrompt, - Steps: data.Steps, - Sampler: data.Sampler, - FaceFix: data.FaceFix, - CfgScale: data.CfgScale, - Seed: data.Seed, - Height: data.Height, - Width: data.Width, - HdFix: data.HdFix, - HdRedrawRate: data.HdRedrawRate, - HdScale: data.HdScale, - HdScaleAlg: data.HdScaleAlg, - HdSteps: data.HdSteps, - }, - UserId: userId, - TranslateModelId: h.App.SysConfig.Base.AssistantModelId, - } - - job := model.SdJob{ - UserId: uint(userId), - Type: types.TaskImage.String(), - TaskId: taskId, - Params: utils.JsonEncode(task.Params), - TaskInfo: utils.JsonEncode(task), - Prompt: data.Prompt, - Progress: 0, - Power: h.App.SysConfig.Base.SdPower, - CreatedAt: time.Now(), - } - res := h.DB.Create(&job) - if res.Error != nil { - resp.ERROR(c, "error with save job: "+res.Error.Error()) - return - } - - task.Id = int(job.Id) - h.sdService.PushTask(task) - - // update user's power - err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ - Type: types.PowerConsume, - Model: "stable-diffusion", - Remark: fmt.Sprintf("绘图操作,任务ID:%s", job.TaskId), - }) - if err != nil { - resp.ERROR(c, err.Error()) - return - } - - resp.SUCCESS(c) -} - -// ImgWall 照片墙 -func (h *SdJobHandler) ImgWall(c *gin.Context) { - page := h.GetInt(c, "page", 0) - pageSize := h.GetInt(c, "page_size", 0) - err, jobs := h.getData(true, 0, page, pageSize, true) - if err != nil { - resp.ERROR(c, err.Error()) - return - } - - resp.SUCCESS(c, jobs) -} - -// JobList 获取 SD 任务列表 -func (h *SdJobHandler) JobList(c *gin.Context) { - finish := h.GetBool(c, "finish") - userId := h.GetLoginUserId(c) - page := h.GetInt(c, "page", 0) - pageSize := h.GetInt(c, "page_size", 0) - publish := h.GetBool(c, "publish") - - err, jobs := h.getData(finish, userId, page, pageSize, publish) - if err != nil { - resp.ERROR(c, err.Error()) - return - } - - resp.SUCCESS(c, jobs) -} - -// JobList 获取 MJ 任务列表 -func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) { - - session := h.DB.Session(&gorm.Session{}) - if finish { - session = session.Where("progress >= ?", 100).Order("id DESC") - } else { - session = session.Where("progress < ?", 100).Order("id ASC") - } - if userId > 0 { - session = session.Where("user_id = ?", userId) - } - if publish { - session = session.Where("publish", publish) - } - if page > 0 && pageSize > 0 { - offset := (page - 1) * pageSize - session = session.Offset(offset).Limit(pageSize) - } - - // 统计总数 - var total int64 - session.Model(&model.SdJob{}).Count(&total) - - var items []model.SdJob - res := session.Find(&items) - if res.Error != nil { - return res.Error, vo.Page{} - } - - var jobs = make([]vo.SdJob, 0) - for _, item := range items { - var job vo.SdJob - err := utils.CopyObject(item, &job) - if err != nil { - continue - } - jobs = append(jobs, job) - } - - return nil, vo.NewPage(total, page, pageSize, jobs) -} - -// Remove remove task image -func (h *SdJobHandler) Remove(c *gin.Context) { - id := h.GetInt(c, "id", 0) - userId := h.GetLoginUserId(c) - var job model.SdJob - if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil { - resp.ERROR(c, "记录不存在") - return - } - - // 删除任务 - err := h.DB.Delete(&job).Error - if err != nil { - resp.ERROR(c, err.Error()) - return - } - - // remove image - err = h.uploader.GetUploadHandler().Delete(job.ImgURL) - if err != nil { - logger.Error("remove image failed: ", err) - } - - resp.SUCCESS(c) -} - -// Publish 发布/取消发布图片到画廊显示 -func (h *SdJobHandler) Publish(c *gin.Context) { - id := h.GetInt(c, "id", 0) - userId := h.GetLoginUserId(c) - action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享 - - err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error - if err != nil { - resp.ERROR(c, err.Error()) - return - } - - resp.SUCCESS(c) -} diff --git a/api/handler/suno_handler.go b/api/handler/suno_handler.go index 73db2e42..7e3e65eb 100644 --- a/api/handler/suno_handler.go +++ b/api/handler/suno_handler.go @@ -125,9 +125,9 @@ func (h *SunoHandler) Create(c *gin.Context) { if data.SongId != "" && data.Type == 3 { var song model.SunoJob if err := h.DB.Where("song_id = ?", data.SongId).First(&song).Error; err == nil { - data.Instrumental = song.Instrumental - data.Model = song.ModelName - data.Tags = song.Tags + data.Instrumental = song.Params.Instrumental + data.Model = song.Params.Model + data.Tags = song.Params.Tags } // 拼接歌词 var refSong model.SunoJob @@ -153,22 +153,26 @@ func (h *SunoHandler) Create(c *gin.Context) { // 插入数据库 job := model.SunoJob{ - UserId: uint(task.UserId), - Prompt: data.Prompt, - Instrumental: data.Instrumental, - ModelName: data.Model, - TaskInfo: utils.JsonEncode(task), - Tags: data.Tags, - Title: data.Title, - Type: data.Type, - RefSongId: data.RefSongId, - RefTaskId: data.RefTaskId, - ExtendSecs: data.ExtendSecs, - Power: h.App.SysConfig.Base.SunoPower, - SongId: utils.RandString(32), + UserId: uint(task.UserId), + Prompt: data.Prompt, + Params: vo.SunoParam{ + Prompt: data.Prompt, + Instrumental: data.Instrumental, + Tags: data.Tags, + ExtendSecs: data.ExtendSecs, + Lyrics: data.Lyrics, + Model: data.Model, + }, + Title: data.Title, + Type: data.Type, + RefSongId: data.RefSongId, + RefTaskId: data.RefTaskId, + Power: h.App.SysConfig.Base.SunoPower, + SongId: utils.RandString(32), } if data.Lyrics != "" { job.Prompt = data.Lyrics + job.Params.Prompt = data.Lyrics } tx := h.DB.Create(&job) if tx.Error != nil { @@ -183,8 +187,8 @@ func (h *SunoHandler) Create(c *gin.Context) { // update user's power err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ Type: types.PowerConsume, - Model: job.ModelName, - Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName), + Model: job.Params.Model, + Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.Params.Model), CreatedAt: time.Now(), }) if err != nil { diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index 3fb4d47a..83869f7a 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -39,6 +39,7 @@ type UserHandler struct { userService *service.UserService wxLoginService *service.WxLoginService ipSearcher *xdb.Searcher + wechatService *service.WxGzhService } func NewUserHandler( @@ -50,6 +51,7 @@ func NewUserHandler( captcha *service.CaptchaService, userService *service.UserService, wxLoginService *service.WxLoginService, + wechatService *service.WxGzhService, ipSearcher *xdb.Searcher) *UserHandler { return &UserHandler{ BaseHandler: BaseHandler{DB: db, App: app}, @@ -59,6 +61,7 @@ func NewUserHandler( captchaService: captcha, userService: userService, wxLoginService: wxLoginService, + wechatService: wechatService, ipSearcher: ipSearcher, } } @@ -75,6 +78,7 @@ func (h *UserHandler) RegisterRoutes() { group.POST("login/callback", h.WxLoginCallback) group.GET("login/status", h.GetWxLoginState) group.GET("logout", h.Logout) + group.POST("wxAuthLogin", h.WxAuthLogin) // 需要用户授权的接口 group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) @@ -319,11 +323,22 @@ func (h *UserHandler) GetWxLoginState(c *gin.Context) { func (h *UserHandler) createNewUser(user model.User, code string) (model.User, error) { if user.OpenId != "" { user.Platform = "wechat" - user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6)) - user.Username = fmt.Sprintf("wx@%d", utils.RandomNumber(8)) - user.Password = "geekai123" + // 如果未设置昵称,则生成默认昵称 + if user.Nickname == "" { + user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6)) + } + // 如果未设置用户名,则生成默认用户名 + if user.Username == "" { + user.Username = fmt.Sprintf("wx@%d", utils.RandomNumber(8)) + } + // 如果未设置密码,则生成默认密码 + if user.Password == "" { + user.Password = "geekai123" + } } else { - user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6)) + if user.Nickname == "" { + user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6)) + } if user.Username == "" || user.Password == "" { return user, fmt.Errorf("用户名或密码不能为空") } @@ -332,9 +347,11 @@ func (h *UserHandler) createNewUser(user model.User, code string) (model.User, e salt := utils.RandString(8) user.Salt = salt user.Password = utils.GenPassword(user.Password, salt) - user.Avatar = "/images/avatar/user.png" + // 如果未设置头像,则使用默认头像 + if user.Avatar == "" { + user.Avatar = "/images/avatar/user.png" + } user.Status = true - user.ChatRoles = utils.JsonEncode([]string{"gpt"}) user.ChatConfig = "{}" user.ChatModels = "{}" user.Power = h.App.SysConfig.Base.InitPower @@ -471,6 +488,20 @@ func (h *UserHandler) Session(c *gin.Context) { h.DB.Model(&user).UpdateColumn("vip", false) } userVo.Id = user.Id + // 工作区应用 ID 列表(历史可能为 key 数组,仅解析数字 ID) + if user.ChatRoles != "" { + var raw []interface{} + if utils.JsonDecode(user.ChatRoles, &raw) == nil { + for _, v := range raw { + if n, ok := v.(float64); ok && n >= 0 { + userVo.ChatRoles = append(userVo.ChatRoles, uint(n)) + } + } + } + } + if userVo.ChatRoles == nil { + userVo.ChatRoles = []uint{} + } resp.SUCCESS(c, userVo) } @@ -483,6 +514,7 @@ type userProfile struct { Power int `json:"power"` ExpiredTime int64 `json:"expired_time"` Vip bool `json:"vip"` + GemIds []uint `json:"gem_ids"` } func (h *UserHandler) Profile(c *gin.Context) { @@ -502,6 +534,19 @@ func (h *UserHandler) Profile(c *gin.Context) { } profile.Id = user.Id + if user.GemIds != "" { + var raw []interface{} + if utils.JsonDecode(user.GemIds, &raw) == nil { + for _, v := range raw { + if n, ok := v.(float64); ok { + profile.GemIds = append(profile.GemIds, uint(n)) + } + } + } + } + if profile.GemIds == nil { + profile.GemIds = []uint{} + } resp.SUCCESS(c, profile) } @@ -520,6 +565,12 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) { h.DB.First(&user, user.Id) user.Avatar = data.Avatar user.Nickname = data.Nickname + if data.GemIds != nil { + if len(data.GemIds) > 8 { + data.GemIds = data.GemIds[:8] + } + user.GemIds = utils.JsonEncode(data.GemIds) + } res := h.DB.Updates(&user) if res.Error != nil { resp.ERROR(c, "更新用户信息失败") @@ -584,13 +635,14 @@ func (h *UserHandler) ResetPass(c *gin.Context) { session := h.DB.Session(&gorm.Session{}) var key string - if data.Type == "email" { + switch data.Type { + case "email": session = session.Where("email", data.Email) key = CodeStorePrefix + data.Email - } else if data.Type == "mobile" { + case "mobile": session = session.Where("mobile", data.Mobile) key = CodeStorePrefix + data.Mobile - } else { + default: resp.ERROR(c, "验证类别错误") return } @@ -722,3 +774,81 @@ func (h *UserHandler) SignIn(c *gin.Context) { } resp.SUCCESS(c) } + +// 微信公众号 小程序授权登录 +func (h *UserHandler) WxAuthLogin(c *gin.Context) { + var data struct { + Code string `json:"code"` + InviteCode string `json:"invite_code"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + // 根据 code 获取 openid + openID, accessToken, err := h.wechatService.GetOpenIDByCode(data.Code) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + // 获取微信用户昵称头像 + userInfo, err := h.wechatService.GetUserInfo(accessToken, openID) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + nickname := userInfo["nickname"].(string) + headimgurl := userInfo["headimgurl"].(string) + + // 查询用户是否存在 + var user model.User + h.DB.Where("openid = ?", openID).First(&user) + if user.Id > 0 { + // 用户存在,更新用户信息并登录 + user.Nickname = nickname + user.Avatar = headimgurl + if err := h.DB.Save(&user).Error; err != nil { + resp.ERROR(c, "更新用户信息失败") + return + } + + token, err := h.doLogin(&user, c.ClientIP()) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username}) + return + } + + // 用户不存在,创建新用户 + user = model.User{ + OpenId: openID, + Nickname: nickname, + Avatar: headimgurl, + } + + // 被邀请人也获得赠送算力 + if data.InviteCode != "" { + user.Power = h.App.SysConfig.Base.InitPower * 2 + } + + user, err = h.createNewUser(user, data.InviteCode) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + // 自动登录 + token, err := h.doLogin(&user, c.ClientIP()) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username}) +} diff --git a/api/handler/video_handler.go b/api/handler/video_handler.go index d55873d8..cfddb352 100644 --- a/api/handler/video_handler.go +++ b/api/handler/video_handler.go @@ -20,7 +20,6 @@ import ( "geekai/store/vo" "geekai/utils" "geekai/utils/resp" - "time" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -54,33 +53,50 @@ func (h *VideoHandler) RegisterRoutes() { // 需要用户授权的接口 group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) { - group.POST("luma/create", h.LumaCreate) - group.POST("keling/create", h.KeLingCreate) + group.POST("create", h.Create) group.GET("list", h.List) group.GET("remove", h.Remove) group.GET("publish", h.Publish) + group.GET("power-config", h.GetPowerConfig) // 获取算力配置 + group.GET("power-by-key", h.GetPowerByPriceKey) // 根据 priceKey 获取算力 } } -func (h *VideoHandler) LumaCreate(c *gin.Context) { +type VideoTaskRequest struct { + Provider string `json:"provider"` // 服务提供商(不带版本号:veo, sora) + Model string `json:"model"` // 模型标识(带版本号:veo-2.0, sora-2.0) + Prompt string `json:"prompt"` // 提示词 + Params map[string]any `json:"params"` // 模型特定参数 + PriceKey string `json:"price_key"` // 价格键(如 "fixed", "5_720P" 等) +} - var data struct { - Prompt string `json:"prompt"` - FirstFrameImg string `json:"first_frame_img,omitempty"` - EndFrameImg string `json:"end_frame_img,omitempty"` - ExpandPrompt bool `json:"expand_prompt,omitempty"` - Loop bool `json:"loop,omitempty"` - } +// Create 统一的创建视频任务接口 +func (h *VideoHandler) Create(c *gin.Context) { + var data VideoTaskRequest if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) return } - // 检查 Prompt 长度 + + // 验证必填字段 + if data.Provider == "" { + resp.ERROR(c, "provider 不能为空") + return + } + if data.Model == "" { + resp.ERROR(c, "model 不能为空") + return + } if data.Prompt == "" { - resp.ERROR(c, "prompt is needed") + resp.ERROR(c, "prompt 不能为空") + return + } + if data.PriceKey == "" { + resp.ERROR(c, "price_key 不能为空") return } + // 文本审查 if h.App.SysConfig.Moderation.Enable { moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt) if err != nil { @@ -101,138 +117,45 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) { resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!") return } - } + // 获取用户信息 user, err := h.GetLoginUser(c) if err != nil { resp.NotAuth(c) return } - if user.Power < h.App.SysConfig.Base.LumaPower { - resp.ERROR(c, "您的算力不足,请充值后再试!") - return - } - - userId := int(h.GetLoginUserId(c)) - params := types.LumaVideoParams{ - PromptOptimize: data.ExpandPrompt, - Loop: data.Loop, - StartImgURL: data.FirstFrameImg, - EndImgURL: data.EndFrameImg, - } - task := types.VideoTask{ - UserId: userId, - Type: types.VideoLuma, - Prompt: data.Prompt, - Params: params, - TranslateModelId: h.App.SysConfig.Base.AssistantModelId, - } - // 插入数据库 - job := model.VideoJob{ - UserId: uint(userId), - Type: types.VideoLuma, - Prompt: data.Prompt, - Power: h.App.SysConfig.Base.LumaPower, - TaskInfo: utils.JsonEncode(task), - } - tx := h.DB.Create(&job) - if tx.Error != nil { - resp.ERROR(c, tx.Error.Error()) - return - } - - // 创建任务 - task.Id = job.Id - h.videoService.PushTask(task) - - // update user's power - err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ - Type: types.PowerConsume, - Model: "luma", - Remark: fmt.Sprintf("Luma 文生视频,任务ID:%d", job.Id), - }) + // 计算算力 + power, err := video.CalculatePower(h.DB, data.Model, data.PriceKey) if err != nil { resp.ERROR(c, err.Error()) return } - resp.SUCCESS(c) -} -func (h *VideoHandler) KeLingCreate(c *gin.Context) { - - var data struct { - Channel string `json:"channel"` - TaskType string `json:"task_type"` // 任务类型: text2video/image2video - Model string `json:"model"` // 模型: kling-v1-5,kling-v1-6 - Prompt string `json:"prompt"` // 视频描述 - NegPrompt string `json:"negative_prompt"` // 负面提示词 - CfgScale float64 `json:"cfg_scale"` // 相关性系数(0-1) - Mode string `json:"mode"` // 生成模式: std/pro - AspectRatio string `json:"aspect_ratio"` // 画面比例: 16:9/9:16/1:1 - Duration string `json:"duration"` // 视频时长: 5/10 - CameraControl types.CameraControl `json:"camera_control"` // 摄像机控制 - Image string `json:"image"` // 参考图片URL(image2video) - ImageTail string `json:"image_tail"` // 尾帧图片URL(image2video) - } - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - - user, err := h.GetLoginUser(c) - if err != nil { - resp.NotAuth(c) - return - } - - // 计算当前任务所需算力 - key := fmt.Sprintf("%s_%s_%s", data.Model, data.Mode, data.Duration) - power := h.App.SysConfig.Base.KeLingPowers[key] - if power == 0 { - resp.ERROR(c, "当前模型暂不支持") - return - } + // 检查算力是否充足 if user.Power < power { resp.ERROR(c, "您的算力不足,请充值后再试!") return } - if data.Prompt == "" { - resp.ERROR(c, "prompt is needed") - return - } - + // 构建任务 userId := int(h.GetLoginUserId(c)) - params := types.KeLingVideoParams{ - TaskType: data.TaskType, - Model: data.Model, - Prompt: data.Prompt, - NegPrompt: data.NegPrompt, - CfgScale: data.CfgScale, - Mode: data.Mode, - AspectRatio: data.AspectRatio, - Duration: data.Duration, - CameraControl: data.CameraControl, - Image: data.Image, - ImageTail: data.ImageTail, - } task := types.VideoTask{ UserId: userId, - Type: types.VideoKeLing, + Type: data.Provider, // provider 作为 type Prompt: data.Prompt, - Params: params, + Params: data.Params, TranslateModelId: h.App.SysConfig.Base.AssistantModelId, - Channel: data.Channel, } + // 插入数据库 job := model.VideoJob{ - UserId: uint(userId), - Type: types.VideoKeLing, - Prompt: data.Prompt, - Power: power, - TaskInfo: utils.JsonEncode(task), + UserId: uint(userId), + Type: data.Provider, + Prompt: data.Prompt, + Power: power, + Params: utils.JsonEncode(task), } tx := h.DB.Create(&job) if tx.Error != nil { @@ -244,17 +167,52 @@ func (h *VideoHandler) KeLingCreate(c *gin.Context) { task.Id = job.Id h.videoService.PushTask(task) - // update user's power + // 扣减算力 err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ Type: types.PowerConsume, - Model: "keling", - Remark: fmt.Sprintf("keling 文生视频,任务ID:%d", job.Id), + Model: data.Provider, + Remark: fmt.Sprintf("%s 视频生成,任务ID:%d", data.Provider, job.Id), }) if err != nil { resp.ERROR(c, err.Error()) return } - resp.SUCCESS(c) + + resp.SUCCESS(c, gin.H{"job_id": job.Id}) +} + +// GetPowerConfig 获取算力配置 +func (h *VideoHandler) GetPowerConfig(c *gin.Context) { + config, err := video.GetVideoConfig(h.DB) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, config.VideoPowers) +} + +// GetPowerByPriceKey 根据 modelKey 和 priceKey 获取算力值 +func (h *VideoHandler) GetPowerByPriceKey(c *gin.Context) { + modelKey := c.Query("model_key") + priceKey := c.Query("price_key") + + if modelKey == "" { + resp.ERROR(c, "model_key 不能为空") + return + } + if priceKey == "" { + resp.ERROR(c, "price_key 不能为空") + return + } + + power, err := video.CalculatePower(h.DB, modelKey, priceKey) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, gin.H{"power": power}) } func (h *VideoHandler) List(c *gin.Context) { @@ -268,7 +226,7 @@ func (h *VideoHandler) List(c *gin.Context) { session = session.Where("type", t) } if all { - session = session.Where("publish", 0).Where("progress", 100) + session = session.Where("publish", 0).Where("status", types.VideoStatusSuccess) } else { session = session.Where("user_id", userId) } @@ -296,36 +254,51 @@ func (h *VideoHandler) List(c *gin.Context) { continue } item.CreatedAt = v.CreatedAt.Unix() - if item.VideoURL == "" { - item.VideoURL = v.WaterURL - } - // 解析任务详情 - if item.Type == types.VideoKeLing { + // 解析任务详情(用于前端展示标签) + if v.Params != "" { task := types.VideoTask{} - err = utils.JsonDecode(v.TaskInfo, &task) - if err != nil { - continue - } - var params types.KeLingVideoParams - err = utils.JsonDecode(utils.JsonEncode(task.Params), ¶ms) - if err != nil { - continue - } - item.RawData = map[string]interface{}{ - "task_type": params.TaskType, - "model": params.Model, - "cfg_scale": params.CfgScale, - "mode": params.Mode, - "aspect_ratio": params.AspectRatio, - "duration": params.Duration, - "model_name": fmt.Sprintf("%s_%s_%s", params.Model, params.Mode, params.Duration), - } - - // 如果视频URL不为空,则设置为生成成功 - if item.VideoURL != "" { - item.Progress = 100 + if err := utils.JsonDecode(v.Params, &task); err == nil { + // 默认从 params map 中提取常用字段 + if paramsMap, ok := task.Params.(map[string]any); ok { + if item.Params == nil { + item.Params = make(map[string]any) + } + if _, ok := item.Params["task_type"]; !ok { + if taskType, ok := paramsMap["task_type"]; ok { + item.Params["task_type"] = taskType + } + } + if _, ok := item.Params["model"]; !ok { + if modelKey, ok := paramsMap["model"]; ok { + item.Params["model"] = modelKey + } + } + if _, ok := item.Params["duration"]; !ok { + if duration, ok := paramsMap["duration"]; ok { + item.Params["duration"] = duration + } + } + if _, ok := item.Params["size"]; !ok { + if size, ok := paramsMap["size"]; ok { + item.Params["size"] = size + } else if size, ok := paramsMap["resolution"].(string); ok { + item.Params["size"] = size + } + } + if _, ok := item.Params["mode"]; !ok { + if mode, ok := paramsMap["mode"]; ok { + item.Params["mode"] = mode + } + } + if _, ok := item.Params["sound"]; !ok { + if sound, ok := paramsMap["sound"]; ok { + item.Params["sound"] = sound + } + } + } } } + items = append(items, item) } @@ -341,9 +314,9 @@ func (h *VideoHandler) Remove(c *gin.Context) { resp.ERROR(c, err.Error()) return } - // 只有失败或者超时的任务才能删除 - if !(job.Progress == service.FailTaskProgress || time.Now().After(job.CreatedAt.Add(time.Minute*30))) { - resp.ERROR(c, "只有失败和超时(30分钟)的任务才能删除!") + // 只有失败的任务才能删除 + if job.Status != types.VideoStatusFailed { + resp.ERROR(c, "只有失败的任务才能删除!") return } @@ -355,7 +328,6 @@ func (h *VideoHandler) Remove(c *gin.Context) { } // 删除文件 - _ = h.uploader.GetUploadHandler().Delete(job.CoverURL) _ = h.uploader.GetUploadHandler().Delete(job.VideoURL) resp.SUCCESS(c) diff --git a/api/handler/wxgzh_handler.go b/api/handler/wxgzh_handler.go new file mode 100644 index 00000000..0cd091f9 --- /dev/null +++ b/api/handler/wxgzh_handler.go @@ -0,0 +1,95 @@ +package handler + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "crypto/sha1" + "encoding/hex" + "geekai/core" + "geekai/utils/resp" + "log" + "net/http" + "sort" + "strings" + + "github.com/gin-gonic/gin" +) + +type WxGzhHandler struct { + BaseHandler +} + +func NewWxGzhHandler(server *core.AppServer) *WxGzhHandler { + return &WxGzhHandler{ + BaseHandler: BaseHandler{ + App: server, + }, + } +} + +func (h *WxGzhHandler) RegisterRoutes() { + group := h.App.Engine.Group("/api/wx/") + group.GET("verify", h.WechatVerify) +} + +// 处理微信服务器验证请求 +func (h *WxGzhHandler) WechatVerify(c *gin.Context) { + logger.Info("WechatVerify") + // 只处理 GET 请求 + if c.Request.Method != "GET" { + resp.ERROR(c, "Method Not Allowed") + return + } + + // 解析 URL 参数 + signature := c.Query("signature") + timestamp := c.Query("timestamp") + nonce := c.Query("nonce") + echostr := c.Query("echostr") + + // 验证参数完整性 + if signature == "" || timestamp == "" || nonce == "" || echostr == "" { + log.Println("Missing parameters") + resp.ERROR(c, "Missing parameters") + return + } + + // 验证签名 + if validateSignature(signature, h.App.SysConfig.WxGzh.Token, timestamp, nonce) { + // 验证成功,返回 echostr(必须是纯文本) + c.String(http.StatusOK, echostr) + log.Println("Token verification success") + } else { + // 验证失败 + resp.ERROR(c, "Forbidden: Invalid signature") + log.Println("Token verification failed") + } +} + +func validateSignature(signature, token, timestamp, nonce string) bool { + // 1. 将 token、timestamp、nonce 按字典序排序 + strs := []string{token, timestamp, nonce} + sort.Strings(strs) + + // 2. 拼接字符串 + joined := strings.Join(strs, "") + + // 3. 计算 SHA1 哈希 + hash := sha1.New() + hash.Write([]byte(joined)) + hashed := hex.EncodeToString(hash.Sum(nil)) + + // 4. 与 signature 比对 + return hashed == signature +} + +// 创建微信菜单 +func (h *WxGzhHandler) CreateMenu(c *gin.Context) { + + resp.SUCCESS(c, "创建菜单成功") +} diff --git a/api/logger/logger.go b/api/log/logger.go similarity index 99% rename from api/logger/logger.go rename to api/log/logger.go index 40ce15a2..67b06a49 100644 --- a/api/logger/logger.go +++ b/api/log/logger.go @@ -1,4 +1,4 @@ -package logger +package log // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * Copyright 2023 The Geek-AI Authors. All rights reserved. diff --git a/api/main.go b/api/main.go index 3f412813..3346403a 100644 --- a/api/main.go +++ b/api/main.go @@ -14,22 +14,21 @@ import ( "geekai/core/types" "geekai/handler" "geekai/handler/admin" - logger2 "geekai/logger" + "geekai/log" "geekai/service" - "geekai/service/dalle" + "geekai/service/image" "geekai/service/jimeng" "geekai/service/mj" "geekai/service/moderation" "geekai/service/oss" "geekai/service/payment" - "geekai/service/sd" + "geekai/service/ppt" "geekai/service/sms" "geekai/service/sora" "geekai/service/suno" "geekai/service/video" "geekai/store" "io" - "log" "os" "os/signal" "runtime/debug" @@ -43,7 +42,7 @@ import ( "gorm.io/gorm" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() //go:embed res var xdbFS embed.FS @@ -89,7 +88,7 @@ func main() { fx.Provide(func() *types.AppConfig { config, err := core.LoadConfig(configFile) if err != nil { - log.Fatal(err) + logger.Fatal(err) } config.Path = configFile return config @@ -136,16 +135,23 @@ func main() { fx.Provide(handler.NewSmsHandler), fx.Provide(handler.NewRedeemHandler), fx.Provide(handler.NewCaptchaHandler), + fx.Provide(func(db *gorm.DB, userService *service.UserService, uploadManager *oss.UploaderManager) *ppt.PptService { + return ppt.NewPptService(db, userService, uploadManager) + }), + fx.Provide(handler.NewPPTTaskHandler), + fx.Invoke(func(ppt *ppt.PptService) { + ppt.RecoverStaleProcessingTasks() + }), fx.Provide(handler.NewMidJourneyHandler), fx.Provide(handler.NewChatModelHandler), - fx.Provide(handler.NewSdJobHandler), fx.Provide(handler.NewPaymentHandler), fx.Provide(handler.NewOrderHandler), fx.Provide(handler.NewProductHandler), fx.Provide(handler.NewConfigHandler), fx.Provide(handler.NewPowerLogHandler), fx.Provide(handler.NewJimengHandler), - + fx.Provide(service.NewWxGzhService), + fx.Provide(handler.NewWxGzhHandler), fx.Provide(service.NewMigrationService), fx.Invoke(func(migrationService *service.MigrationService) { migrationService.StartMigrate() @@ -164,12 +170,15 @@ func main() { fx.Provide(admin.NewOrderHandler), fx.Provide(admin.NewPowerLogHandler), fx.Provide(admin.NewAdminJimengHandler), + fx.Provide(admin.NewVideoHandler), + fx.Provide(admin.NewSunoHandler), + fx.Provide(admin.NewPPTHandler), // 邮件服务 fx.Provide(service.NewSmtpService), - // Dalle 服务 - fx.Provide(dalle.NewService), - fx.Invoke(func(s *dalle.Service) { + // Image 服务 + fx.Provide(image.NewService), + fx.Invoke(func(s *image.Service) { s.Run() s.DownloadImages() s.CheckTaskStatus() @@ -187,13 +196,6 @@ func main() { // Sora service fx.Provide(sora.NewSoraService), - // Stable Diffusion 机器人 - fx.Provide(sd.NewService), - fx.Invoke(func(s *sd.Service, config *types.AppConfig) { - s.Run() - s.CheckTaskStatus() - }), - fx.Provide(suno.NewService), fx.Invoke(func(s *suno.Service) { s.Run() @@ -219,6 +221,7 @@ func main() { // 创建短信服务 fx.Provide(sms.NewAliYunSmsService), fx.Provide(sms.NewBaoSmsService), + fx.Provide(sms.NewTencentSmsService), fx.Provide(sms.NewSmsManager), fx.Provide(service.NewCaptchaService), fx.Provide(service.NewWxLoginService), @@ -233,6 +236,7 @@ func main() { fx.Provide(oss.NewMiniOss), fx.Provide(oss.NewQiNiuOss), fx.Provide(oss.NewAliYunOss), + fx.Provide(oss.NewTencentOss), fx.Provide(oss.NewUploaderManager), // 用户服务 @@ -267,15 +271,15 @@ func main() { fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) { h.RegisterRoutes() }), + fx.Invoke(func(s *core.AppServer, h *handler.PPTTaskHandler) { + h.RegisterRoutes() + }), fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) { h.RegisterRoutes() }), fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) { h.RegisterRoutes() }), - fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) { - h.RegisterRoutes() - }), fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) { h.RegisterRoutes() }), @@ -366,8 +370,8 @@ func main() { fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) { h.RegisterRoutes() }), - fx.Provide(handler.NewDallJobHandler), - fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) { + fx.Provide(handler.NewImageJobHandler), + fx.Invoke(func(s *core.AppServer, h *handler.ImageJobHandler) { h.RegisterRoutes() }), fx.Provide(handler.NewSunoHandler), @@ -386,6 +390,15 @@ func main() { fx.Invoke(func(s *core.AppServer, h *admin.AdminJimengHandler) { h.RegisterRoutes() }), + fx.Invoke(func(s *core.AppServer, h *admin.VideoHandler) { + h.RegisterRoutes() + }), + fx.Invoke(func(s *core.AppServer, h *admin.SunoHandler) { + h.RegisterRoutes() + }), + fx.Invoke(func(s *core.AppServer, h *admin.PPTHandler) { + h.RegisterRoutes() + }), fx.Provide(admin.NewChatAppTypeHandler), fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) { h.RegisterRoutes() @@ -402,6 +415,11 @@ func main() { fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) { h.RegisterRoutes() }), + + // 微信公众号路由 + fx.Invoke(func(s *core.AppServer, h *handler.WxGzhHandler) { + h.RegisterRoutes() + }), fx.Invoke(func(s *core.AppServer, db *gorm.DB) { go func() { err := s.Run(db) @@ -427,10 +445,6 @@ func main() { fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) { h.RegisterRoutes() }), - fx.Provide(admin.NewMediaHandler), - fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) { - h.RegisterRoutes() - }), fx.Provide(handler.NewRealtimeHandler), fx.Invoke(func(s *core.AppServer, h *handler.RealtimeHandler) { h.RegisterRoutes() @@ -439,7 +453,7 @@ func main() { // 启动应用程序 go func() { if err := app.Start(context.Background()); err != nil { - log.Fatal(err) + logger.Fatal(err) } }() @@ -452,7 +466,7 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := app.Stop(ctx); err != nil { - log.Fatal(err) + logger.Fatal(err) } } diff --git a/api/service/dalle/dalle_service.go b/api/service/image/image_service.go similarity index 83% rename from api/service/dalle/dalle_service.go rename to api/service/image/image_service.go index 876bbbde..b1c0bb10 100644 --- a/api/service/dalle/dalle_service.go +++ b/api/service/image/image_service.go @@ -1,4 +1,4 @@ -package dalle +package image // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * Copyright 2023 The Geek-AI Authors. All rights reserved. @@ -10,12 +10,13 @@ package dalle import ( "fmt" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "geekai/service" "geekai/service/oss" "geekai/store" "geekai/store/model" "geekai/utils" + "io" "time" "github.com/go-redis/redis/v8" @@ -24,9 +25,9 @@ import ( "gorm.io/gorm" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() -// DALL-E 绘画服务 +// Image Generation Service type Service struct { httpClient *req.Client @@ -40,50 +41,50 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien return &Service{ httpClient: req.C().SetTimeout(time.Minute * 3), db: db, - taskQueue: store.NewRedisQueue("DallE_Task_Queue", redisCli), + taskQueue: store.NewRedisQueue("Image_Task_Queue", redisCli), uploadManager: manager, userService: userService, } } -// PushTask push a new mj task in to task queue -func (s *Service) PushTask(task types.DallTask) { - logger.Infof("add a new DALL-E task to the task list: %+v", task) +// PushTask push a new image task in to task queue +func (s *Service) PushTask(task types.ImageTask) { + logger.Infof("add a new Image generation task to the task list: %+v", task) if err := s.taskQueue.RPush(task); err != nil { - logger.Errorf("push dall-e task to queue failed: %v", err) + logger.Errorf("push image task to queue failed: %v", err) } } func (s *Service) Run() { // 将数据库中未提交的任务加载到队列 - var jobs []model.DallJob + var jobs []model.ImageJob s.db.Where("progress", 0).Find(&jobs) for _, v := range jobs { - var task types.DallTask - err := utils.JsonDecode(v.TaskInfo, &task) + var task types.ImageTask + err := utils.JsonDecode(v.Params, &task) if err != nil { - logger.Errorf("decode task info with error: %v", err) + logger.Errorf("decode task params with error: %v", err) continue } task.Id = v.Id s.PushTask(task) } - logger.Info("Starting DALL-E job consumer...") + logger.Info("Starting Image generation job consumer...") go func() { for { - var task types.DallTask + var task types.ImageTask err := s.taskQueue.LPop(&task) if err != nil { logger.Errorf("taking task with error: %v", err) continue } - logger.Infof("handle a new DALL-E task: %+v", task) + logger.Infof("handle a new Image generation task: %+v", task) go func() { _, err = s.Image(task, false) if err != nil { logger.Errorf("error with image task: %v", err) - s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ + s.db.Model(&model.ImageJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ "progress": service.FailTaskProgress, "err_msg": err.Error(), }) @@ -120,7 +121,7 @@ type ErrRes struct { } `json:"error"` } -func (s *Service) Image(task types.DallTask, sync bool) (string, error) { +func (s *Service) Image(task types.ImageTask, sync bool) (string, error) { logger.Debugf("绘画参数:%+v", task) var chatModel model.ChatModel @@ -136,7 +137,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { if chatModel.KeyId > 0 { session = session.Where("id = ?", chatModel.KeyId) } else { - session = session.Where("type = ?", "dalle") + session = session.Where("type = ?", "image") } err := session.Order("last_used_at ASC").First(&apiKey).Error if err != nil { @@ -179,6 +180,11 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { return "", fmt.Errorf("error with send request, status: %s, %+v", r.Status, errRes.Error) } + if len(res.Data) == 0 && r.Body != nil { + body, _ := io.ReadAll(r.Body) + return "", fmt.Errorf("%s", string(body)) + } + // update the api key last use time s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix()) var imgURL string @@ -199,7 +205,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { } data["org_url"] = imgURL // update task progress - err = s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(data).Error + err = s.db.Model(&model.ImageJob{Id: task.Id}).UpdateColumns(data).Error if err != nil { return "", fmt.Errorf("err with update database: %v", err) } @@ -214,10 +220,10 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { func (s *Service) CheckTaskStatus() { go func() { - logger.Info("Running DALL-E task status checking ...") + logger.Info("Running Image generation task status checking ...") for { // 检查未完成任务进度 - var jobs []model.DallJob + var jobs []model.ImageJob s.db.Where("progress < ?", 100).Find(&jobs) for _, job := range jobs { // 超时的任务标记为失败 @@ -231,8 +237,8 @@ func (s *Service) CheckTaskStatus() { // 找出失败的任务,并恢复其扣减算力 s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) for _, job := range jobs { - var task types.DallTask - err := utils.JsonDecode(job.TaskInfo, &task) + var task types.ImageTask + err := utils.JsonDecode(job.Params, &task) if err != nil { continue } @@ -254,7 +260,7 @@ func (s *Service) CheckTaskStatus() { func (s *Service) DownloadImages() { go func() { - var items []model.DallJob + var items []model.ImageJob for { res := s.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items) if res.Error != nil { @@ -291,7 +297,7 @@ func (s *Service) downloadImage(jobId uint, orgURL string) (string, error) { } // update img_url - res := s.db.Model(&model.DallJob{Id: jobId}).UpdateColumn("img_url", imgURL) + res := s.db.Model(&model.ImageJob{Id: jobId}).UpdateColumn("img_url", imgURL) if res.Error != nil { return "", err } diff --git a/api/service/jimeng/service.go b/api/service/jimeng/jimemg_service.go similarity index 98% rename from api/service/jimeng/service.go rename to api/service/jimeng/jimemg_service.go index df9136f5..c1cc6b2a 100644 --- a/api/service/jimeng/service.go +++ b/api/service/jimeng/jimemg_service.go @@ -10,7 +10,7 @@ import ( "gorm.io/gorm" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "geekai/service" "geekai/service/oss" "geekai/store" @@ -20,7 +20,7 @@ import ( "github.com/go-redis/redis/v8" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() // Service 即梦服务(合并了消费者功能) type Service struct { @@ -249,8 +249,10 @@ func (s *Service) buildTaskRequest(req *types.JimengTaskRequest) (map[string]any // duration 转成 frames if duration, ok := params["duration"]; ok { - if secs, ok := duration.(int); ok { - params["frames"] = secs*24 + 1 + if v, ok := duration.(int); ok { + params["frames"] = v*24 + 1 + } else if v, ok := duration.(float64); ok { + params["frames"] = int(v*24) + 1 } delete(params, "duration") } diff --git a/api/service/migration_service.go b/api/service/migration_service.go index b0268056..db6a3f7c 100644 --- a/api/service/migration_service.go +++ b/api/service/migration_service.go @@ -8,17 +8,15 @@ package service // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "context" "encoding/json" "fmt" "geekai/core/types" - "geekai/store" "geekai/store/model" "strings" - "sync" "github.com/go-redis/redis/v8" "gorm.io/gorm" - "gorm.io/gorm/schema" ) const ( @@ -33,23 +31,21 @@ type MigrationService struct { db *gorm.DB redisClient *redis.Client appConfig *types.AppConfig - levelDB *store.LevelDB } -func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig, levelDB *store.LevelDB) *MigrationService { +func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig) *MigrationService { return &MigrationService{ db: db, redisClient: redisClient, appConfig: appConfig, - levelDB: levelDB, } } func (s *MigrationService) StartMigrate() { - // 表结构同步必须在对外服务前完成,避免缺列导致业务报错 + // 表结构迁移必须在业务服务启动前完成,避免新表和新列尚未创建就被后台任务查询。 s.TableMigration() go func() { - _ = s.MigrateConfig(s.appConfig) + s.MigrateConfig(s.appConfig) }() } @@ -126,163 +122,386 @@ func (s *MigrationService) MigrateConfigContent() error { return nil } -// 永不删除的保护列(大小写不敏感) -var protectedColumns = map[string]struct{}{ - "id": {}, - "created_at": {}, - "updated_at": {}, -} - -// allModels 全部需要同步的数据表 model -func allModels() []any { - return []any{ +// fullTableMigration 第一步:全量表迁移,同步所有表结构(新增表、新增字段、字段类型) +// 适用于首次安装或导入旧版数据库后同步 schema,AutoMigrate 会补齐缺失的表和列 +func (s *MigrationService) fullTableMigration() { + logger.Info("执行全量表迁移(同步 schema)...") + models := []any{ + &model.Config{}, &model.AdminUser{}, + &model.ChatApp{}, &model.ApiKey{}, &model.AppType{}, - &model.ChatApp{}, + &model.ChatModel{}, + &model.User{}, &model.ChatItem{}, &model.ChatMessage{}, - &model.ChatModel{}, - &model.Config{}, - &model.DallJob{}, - &model.File{}, + &model.Order{}, + &model.Product{}, &model.Function{}, + &model.Menu{}, &model.InviteCode{}, &model.InviteLog{}, - &model.JimengJob{}, - &model.Menu{}, - &model.MidJourneyJob{}, - &model.Moderation{}, - &model.Order{}, - &model.PowerLog{}, - &model.Product{}, &model.Redeem{}, - &model.SdJob{}, - &model.SunoJob{}, - &model.User{}, + &model.PowerLog{}, + &model.File{}, &model.UserLoginLog{}, + &model.MidJourneyJob{}, + &model.SunoJob{}, &model.VideoJob{}, + &model.JimengJob{}, + &model.PPTJob{}, + &model.Moderation{}, + &model.ImageJob{}, } -} - -// 数据表迁移:先处理字段重命名(保数据),再全量同步 schema -func (s *MigrationService) TableMigration() { - logger.Info("开始数据表迁移...") - s.renameColumns() - if err := s.SyncAllModels(); err != nil { - logger.Errorf("同步数据表字段失败: %v", err) + if err := s.db.AutoMigrate(models...); err != nil { + logger.Errorf("全量表迁移失败: %v", err) return } - logger.Info("数据表迁移完成") + logger.Info("全量表迁移完成") } -// renameColumns 只处理「改名」场景:删旧加新会丢数据,必须先 Rename -func (s *MigrationService) renameColumns() { - m := s.db.Migrator() +// fixTableConstraints 第一步之后:根据模型定义修复各表的主键、自增和关键索引 +// 主要解决初始化 SQL 中缺少 AUTO_INCREMENT 或 PRIMARY KEY 导致插入失败的问题 +func (s *MigrationService) fixTableConstraints() { + logger.Info("开始修复各表的主键、自增属性和索引...") - if m.HasColumn(&model.JimengJob{}, "task_params") { - _ = m.RenameColumn(&model.JimengJob{}, "task_params", "params") + // 当前数据库名 + var dbName string + if err := s.db.Raw("SELECT DATABASE()").Scan(&dbName).Error; err != nil { + logger.Errorf("获取当前数据库名失败: %v", err) + return } - if m.HasColumn(&model.Order{}, "pay_type") { - _ = m.RenameColumn(&model.Order{}, "pay_type", "channel") + if dbName == "" { + logger.Warn("当前连接未选择数据库,跳过约束修复") + return } - if m.HasColumn(&model.Config{}, "config_json") { - _ = m.RenameColumn(&model.Config{}, "config_json", "value") - } - if m.HasColumn(&model.Config{}, "marker") { - _ = m.RenameColumn(&model.Config{}, "marker", "name") - } - if m.HasIndex(&model.Config{}, "idx_chatgpt_configs_key") { - _ = m.DropIndex(&model.Config{}, "idx_chatgpt_configs_key") - } - if m.HasIndex(&model.Config{}, "marker") { - _ = m.DropIndex(&model.Config{}, "marker") - } -} -// SyncAllModels 按 model 定义同步所有数据表:缺列新建,多余列删除 -func (s *MigrationService) SyncAllModels() error { - var firstErr error - for _, m := range allModels() { - if err := s.syncModel(m); err != nil { - logger.Errorf("同步 model %T 失败: %v", m, err) - if firstErr == nil { - firstErr = err + // === 修复所有包含 id 字段的表的主键 + 自增 === + type columnInfo struct { + TableName string `gorm:"column:TABLE_NAME"` + ColumnName string `gorm:"column:COLUMN_NAME"` + ColumnKey string `gorm:"column:COLUMN_KEY"` + Extra string `gorm:"column:EXTRA"` + DataType string `gorm:"column:DATA_TYPE"` + } + + var idColumns []columnInfo + if err := s.db.Raw(` + SELECT TABLE_NAME, COLUMN_NAME, COLUMN_KEY, EXTRA, DATA_TYPE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = ? AND COLUMN_NAME = 'id' + `, dbName).Scan(&idColumns).Error; err != nil { + logger.Errorf("查询各表 id 字段信息失败: %v", err) + } else { + for _, col := range idColumns { + if col.ColumnName == "" { + continue + } + + // 检查当前表是否已经存在主键 + type pkInfo struct { + ColumnName string `gorm:"column:COLUMN_NAME"` + } + var pkColumns []pkInfo + if err := s.db.Raw(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY' + `, dbName, col.TableName).Scan(&pkColumns).Error; err != nil { + logger.Errorf("查询表 %s 的主键信息失败: %v", col.TableName, err) + continue + } + + hasPK := len(pkColumns) > 0 + pkOnIdOnly := hasPK && len(pkColumns) == 1 && pkColumns[0].ColumnName == "id" + + needAlter := false + switch { + case !hasPK: + // 没有任何主键:允许将 id 设置为自增主键 + if col.ColumnKey != "PRI" || col.Extra == "" || !strings.Contains(col.Extra, "auto_increment") { + needAlter = true + } + case pkOnIdOnly: + // 只有 id 作为主键:只补充自增属性 + if col.Extra == "" || !strings.Contains(col.Extra, "auto_increment") { + needAlter = true + } + default: + // 已存在非 id 或组合主键:避免破坏原有主键,直接跳过 + logger.Infof("表 %s 已存在非 id 主键,跳过 id 自增主键修复", col.TableName) + } + if !needAlter { + continue + } + + // 维持原来的数据类型,避免与历史 SQL 冲突 + dataType := col.DataType + if dataType == "" { + dataType = "int" + } + + alterSQL := fmt.Sprintf( + "ALTER TABLE `%s` MODIFY COLUMN id %s NOT NULL AUTO_INCREMENT PRIMARY KEY", + col.TableName, + dataType, + ) + if err := s.db.Exec(alterSQL).Error; err != nil { + logger.Errorf("修复表 %s 的 id 自增主键失败: %v", col.TableName, err) + } else { + logger.Infof("已修复表 %s 的 id 为 AUTO_INCREMENT PRIMARY KEY", col.TableName) } } } - return firstErr + + // === 为 geekai_users.username 补充唯一索引(根据模型 uniqueIndex 定义) === + var usernameUniqueCount int64 + err := s.db.Raw(` + SELECT COUNT(1) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = ? + AND TABLE_NAME = 'geekai_users' + AND COLUMN_NAME = 'username' + AND NON_UNIQUE = 0 + `, dbName).Scan(&usernameUniqueCount).Error + if err != nil { + logger.Errorf("检查 geekai_users.username 唯一索引失败: %v", err) + } else if usernameUniqueCount == 0 { + // 索引名尽量固定,避免重复创建 + if err := s.db.Exec("ALTER TABLE geekai_users ADD UNIQUE KEY idx_geekai_users_username (username)").Error; err != nil { + logger.Errorf("创建 geekai_users.username 唯一索引失败: %v", err) + } else { + logger.Info("已为 geekai_users.username 创建唯一索引 idx_geekai_users_username") + } + } else { + logger.Info("geekai_users.username 唯一索引已存在,跳过创建") + } + + logger.Info("关键表主键、自增和索引修复完成") } -func (s *MigrationService) syncModel(dst any) error { - tableName := s.tableName(dst) - if err := s.db.AutoMigrate(dst); err != nil { - return fmt.Errorf("AutoMigrate %s: %w", tableName, err) +// incrementalTableMigration 第二步:增量迁移,仅处理删除字段与数据迁移 +// AutoMigrate 不会删除列,故需在此显式 DropColumn;字段重命名需先拷贝数据再删除旧列 +func (s *MigrationService) incrementalTableMigration() { + logger.Info("执行增量迁移(删除字段 + 数据迁移)...") + + // ========== 字段重命名:全量迁移已添加新列,需将旧列数据拷贝到新列后删除旧列 ========== + + // ChatApp(geekai_chat_roles): context_json -> system_prompt 历史数据迁移 + if s.db.Migrator().HasColumn(&model.ChatApp{}, "context_json") { + // 将旧列 context_json 的值拷贝到 system_prompt(NULL 转为空字符串,保证 NOT NULL 约束) + if err := s.db.Exec(` + UPDATE geekai_chat_roles + SET system_prompt = IFNULL(NULLIF(TRIM(COALESCE(context_json, '')), ''), '') + `).Error; err != nil { + logger.Errorf("迁移 geekai_chat_roles.context_json -> system_prompt 失败: %v", err) + } else { + if err := s.db.Migrator().DropColumn(&model.ChatApp{}, "context_json"); err != nil { + logger.Errorf("删除 geekai_chat_roles.context_json 失败: %v", err) + } else { + logger.Info("geekai_chat_roles: context_json 已迁移至 system_prompt 并删除旧列") + } + } } - if err := s.dropUnusedColumns(dst); err != nil { - return fmt.Errorf("drop unused columns %s: %w", tableName, err) + + // ChatApp: 将 user_id 为 NULL 的历史记录置为 0(系统内置) + if s.db.Migrator().HasColumn(&model.ChatApp{}, "user_id") { + if err := s.db.Exec(`UPDATE geekai_chat_roles SET user_id = 0 WHERE user_id IS NULL`).Error; err != nil { + logger.Errorf("初始化 geekai_chat_roles.user_id 失败: %v", err) + } } - logger.Infof("已同步数据表: %s", tableName) - return nil + + // ChatApp(geekai_chat_roles): 删除 marker 列(应用仅通过 id 区分) + var hasMarker int + if s.db.Raw("SELECT COUNT(1) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'geekai_chat_roles' AND COLUMN_NAME = 'marker'").Scan(&hasMarker).Error == nil && hasMarker > 0 { + // 先删除可能存在的唯一索引(不同版本 SQL 索引名不同) + for _, idxName := range []string{"marker", "idx_chatgpt_chat_roles_marker", "idx_chatgpt_chat_roles_key", "idx_geekai_chat_roles_marker"} { + _ = s.db.Exec(fmt.Sprintf("ALTER TABLE geekai_chat_roles DROP INDEX `%s`", idxName)).Error + } + if err := s.db.Exec("ALTER TABLE geekai_chat_roles DROP COLUMN marker").Error; err != nil { + logger.Errorf("删除 geekai_chat_roles.marker 失败: %v", err) + } else { + logger.Info("geekai_chat_roles: 已删除 marker 列") + } + } + + // Config: config_json -> value, marker -> name + if s.db.Migrator().HasColumn(&model.Config{}, "config_json") { + s.db.Exec("UPDATE geekai_configs SET `value` = config_json WHERE config_json IS NOT NULL AND config_json != ''") + s.db.Migrator().DropColumn(&model.Config{}, "config_json") + } + if s.db.Migrator().HasColumn(&model.Config{}, "marker") { + s.db.Exec("UPDATE geekai_configs SET `name` = marker WHERE marker IS NOT NULL AND marker != ''") + s.db.Migrator().DropColumn(&model.Config{}, "marker") + } + if s.db.Migrator().HasIndex(&model.Config{}, "idx_chatgpt_configs_key") { + s.db.Migrator().DropIndex(&model.Config{}, "idx_chatgpt_configs_key") + } + if s.db.Migrator().HasIndex(&model.Config{}, "marker") { + s.db.Migrator().DropIndex(&model.Config{}, "marker") + } + + // Order: pay_type -> channel + if s.db.Migrator().HasColumn(&model.Order{}, "pay_type") { + s.db.Exec("UPDATE geekai_orders SET channel = pay_type WHERE pay_type IS NOT NULL AND pay_type != ''") + s.db.Migrator().DropColumn(&model.Order{}, "pay_type") + } + + // JimengJob: task_params -> params + if s.db.Migrator().HasColumn(&model.JimengJob{}, "task_params") { + s.db.Exec("UPDATE geekai_jimeng_jobs SET params = task_params WHERE task_params IS NOT NULL AND task_params != ''") + s.db.Migrator().DropColumn(&model.JimengJob{}, "task_params") + } + + // VideoJob: task_info -> params, raw_data -> output + if s.db.Migrator().HasColumn(&model.VideoJob{}, "task_info") { + s.db.Exec("UPDATE geekai_video_jobs SET params = task_info WHERE task_info IS NOT NULL AND task_info != ''") + s.db.Migrator().DropColumn(&model.VideoJob{}, "task_info") + } + if s.db.Migrator().HasColumn(&model.VideoJob{}, "raw_data") { + s.db.Exec("UPDATE geekai_video_jobs SET `output` = raw_data WHERE raw_data IS NOT NULL AND raw_data != ''") + s.db.Migrator().DropColumn(&model.VideoJob{}, "raw_data") + } + + // SunoJob: task_info -> params, raw_data -> output + if s.db.Migrator().HasColumn(&model.SunoJob{}, "task_info") { + s.db.Exec("UPDATE geekai_suno_jobs SET params = task_info WHERE task_info IS NOT NULL AND task_info != ''") + s.db.Migrator().DropColumn(&model.SunoJob{}, "task_info") + } + if s.db.Migrator().HasColumn(&model.SunoJob{}, "raw_data") { + s.db.Exec("UPDATE geekai_suno_jobs SET `output` = raw_data WHERE raw_data IS NOT NULL AND raw_data != ''") + s.db.Migrator().DropColumn(&model.SunoJob{}, "raw_data") + } + + // ========== 删除不再使用的字段 ========== + + if s.db.Migrator().HasColumn(&model.Order{}, "deleted_at") { + s.db.Migrator().DropColumn(&model.Order{}, "deleted_at") + } + if s.db.Migrator().HasColumn(&model.ChatItem{}, "deleted_at") { + s.db.Migrator().DropColumn(&model.ChatItem{}, "deleted_at") + } + if s.db.Migrator().HasColumn(&model.ChatMessage{}, "deleted_at") { + s.db.Migrator().DropColumn(&model.ChatMessage{}, "deleted_at") + } + if s.db.Migrator().HasColumn(&model.User{}, "chat_config") { + s.db.Migrator().DropColumn(&model.User{}, "chat_config") + } + if s.db.Migrator().HasColumn(&model.ChatModel{}, "category") { + s.db.Migrator().DropColumn(&model.ChatModel{}, "category") + } + if s.db.Migrator().HasColumn(&model.ChatModel{}, "description") { + s.db.Migrator().DropColumn(&model.ChatModel{}, "description") + } + if s.db.Migrator().HasColumn(&model.Product{}, "discount") { + s.db.Migrator().DropColumn(&model.Product{}, "discount") + } + if s.db.Migrator().HasColumn(&model.Product{}, "days") { + s.db.Migrator().DropColumn(&model.Product{}, "days") + } + if s.db.Migrator().HasColumn(&model.Product{}, "app_url") { + s.db.Migrator().DropColumn(&model.Product{}, "app_url") + } + if s.db.Migrator().HasColumn(&model.Product{}, "url") { + s.db.Migrator().DropColumn(&model.Product{}, "url") + } + if s.db.Migrator().HasColumn(&model.VideoJob{}, "water_url") { + s.db.Migrator().DropColumn(&model.VideoJob{}, "water_url") + } + if s.db.Migrator().HasColumn(&model.VideoJob{}, "cover_url") { + s.db.Migrator().DropColumn(&model.VideoJob{}, "cover_url") + } + if s.db.Migrator().HasColumn(&model.VideoJob{}, "prompt_ext") { + s.db.Migrator().DropColumn(&model.VideoJob{}, "prompt_ext") + } + if s.db.Migrator().HasColumn(&model.SunoJob{}, "instrumental") { + s.db.Migrator().DropColumn(&model.SunoJob{}, "instrumental") + } + if s.db.Migrator().HasColumn(&model.SunoJob{}, "tags") { + s.db.Migrator().DropColumn(&model.SunoJob{}, "tags") + } + if s.db.Migrator().HasColumn(&model.SunoJob{}, "extend_secs") { + s.db.Migrator().DropColumn(&model.SunoJob{}, "extend_secs") + } + if s.db.Migrator().HasColumn(&model.SunoJob{}, "model_name") { + s.db.Migrator().DropColumn(&model.SunoJob{}, "model_name") + } + + // ========== 数据迁移:根据业务逻辑更新现有数据 ========== + + // video_job: 根据 progress 填充 status + if s.db.Migrator().HasColumn(&model.VideoJob{}, "status") { + s.db.Exec(`UPDATE geekai_video_jobs SET status = CASE + WHEN progress < 100 THEN 'in_progress' + WHEN progress = 100 THEN 'success' + WHEN progress = 101 THEN 'failed' + WHEN progress = 102 THEN 'downloading' + ELSE 'pending' + END WHERE status = '' OR status IS NULL`) + } + + // suno_job: 从 output 提取 tags/model_name 填入 params + s.migrateSunoJobData() + + logger.Info("增量迁移完成") } -func (s *MigrationService) dropUnusedColumns(dst any) error { - dbCols, err := s.db.Migrator().ColumnTypes(dst) - if err != nil { - return err +// TableMigration 数据表迁移入口:先全量同步 schema,再增量删除字段并迁移数据 +func (s *MigrationService) TableMigration() { + s.fullTableMigration() + s.fixTableConstraints() + s.incrementalTableMigration() + s.migrateChatAppSystemPromptFromJSON() +} + +// migrateChatAppSystemPromptFromJSON 将智能体 system_prompt 字段中历史 JSON 数组 +// 解析后取出 role 为 system 的 content,覆盖回 system_prompt(纯文本) +func (s *MigrationService) migrateChatAppSystemPromptFromJSON() { + key := "migrate:chat_app_system_prompt_json" + if s.redisClient.Get(context.Background(), key).Val() == "1" { + logger.Info("ChatApp system_prompt JSON 已迁移,跳过") + return } - modelCols, err := s.modelColumnNames(dst) - if err != nil { - return err + logger.Info("开始迁移智能体 system_prompt 历史 JSON 数据...") + + var apps []model.ChatApp + if err := s.db.Find(&apps).Error; err != nil { + logger.Errorf("查询 ChatApp 失败: %v", err) + return } - for _, col := range dbCols { - name := col.Name() - if s.isProtectedColumn(name) { + updated := 0 + for i := range apps { + raw := strings.TrimSpace(apps[i].SystemPrompt) + if raw == "" { continue } - if _, ok := modelCols[strings.ToLower(name)]; ok { + if len(raw) < 2 || raw[0] != '[' { continue } - logger.Infof("删除多余字段: %s.%s", s.tableName(dst), name) - if err := s.db.Migrator().DropColumn(dst, name); err != nil { - return fmt.Errorf("DropColumn %s: %w", name, err) - } - } - return nil -} - -func (s *MigrationService) modelColumnNames(dst any) (map[string]struct{}, error) { - parsed, err := schema.Parse(dst, &schemaCache, s.db.Config.NamingStrategy) - if err != nil { - return nil, err - } - cols := make(map[string]struct{}, len(parsed.Fields)) - for _, field := range parsed.Fields { - if field.DBName == "" || field.IgnoreMigration { + var messages []types.Message + if err := json.Unmarshal([]byte(raw), &messages); err != nil { continue } - cols[strings.ToLower(field.DBName)] = struct{}{} + var systemContent string + for _, m := range messages { + if strings.ToLower(strings.TrimSpace(m.Role)) == "system" && m.Content != "" { + systemContent = m.Content + break + } + } + if err := s.db.Model(&model.ChatApp{}).Where("id = ?", apps[i].Id).Update("system_prompt", systemContent).Error; err != nil { + logger.Warnf("更新 ChatApp id=%d system_prompt 失败: %v", apps[i].Id, err) + continue + } + updated++ } - return cols, nil -} -func (s *MigrationService) isProtectedColumn(name string) bool { - _, ok := protectedColumns[strings.ToLower(name)] - return ok + logger.Infof("智能体 system_prompt JSON 迁移完成,共更新 %d 条", updated) + s.redisClient.Set(context.Background(), key, "1", 0) } -func (s *MigrationService) tableName(dst any) string { - stmt := &gorm.Statement{DB: s.db} - if err := stmt.Parse(dst); err != nil { - return fmt.Sprintf("%T", dst) - } - return stmt.Schema.Table -} - -// schema.Parse 进程内复用的 schema cache -var schemaCache sync.Map - // 迁移配置数据 func (s *MigrationService) MigrateConfig(config *types.AppConfig) error { @@ -374,6 +593,14 @@ func (s *MigrationService) migrateCommunicationConfig(config *types.AppConfig) e "sign": config.SMS.Bao.Sign, "code_template": config.SMS.Bao.CodeTemplate, }, + "tencent": map[string]any{ + "secret_id": config.SMS.Tencent.SecretId, + "secret_key": config.SMS.Tencent.SecretKey, + "sms_sdk_app_id": config.SMS.Tencent.SmsSdkAppId, + "sign": config.SMS.Tencent.Sign, + "code_temp_id": config.SMS.Tencent.CodeTempId, + "region": config.SMS.Tencent.Region, + }, } return s.saveConfig(types.ConfigKeySms, smsConfig) } @@ -406,3 +633,73 @@ func (s *MigrationService) saveConfig(key string, config any) error { logger.Infof("成功迁移配置 %s", key) return nil } + +// migrateSunoJobData 合并 suno_job 数据:从 Output 原始数据中解析出 tags 和 model_name 填入 params 字段 +func (s *MigrationService) migrateSunoJobData() { + key := "migrate:suno_job_data" + if s.redisClient.Get(context.Background(), key).Val() == "1" { + logger.Info("SunoJob 数据已合并,跳过迁移") + return + } + + logger.Info("开始合并 SunoJob 数据...") + + // 查询所有有 output 数据的记录 + var jobs []model.SunoJob + if err := s.db.Where("output != ? AND output != ''", "").Find(&jobs).Error; err != nil { + logger.Errorf("查询 SunoJob 数据失败: %v", err) + return + } + + updatedCount := 0 + for _, job := range jobs { + if job.Output == "" { + continue + } + + // 解析 Output JSON 数据 + var outputData struct { + Metadata struct { + Tags string `json:"tags"` + } `json:"metadata"` + ModelName string `json:"model_name"` + } + + if err := json.Unmarshal([]byte(job.Output), &outputData); err != nil { + logger.Warnf("解析 Output 数据失败 (ID: %d): %v", job.Id, err) + continue + } + + // 检查是否需要更新 params + needUpdate := false + params := job.Params + + // 如果 params 中的 tags 为空,但 output 中有 tags,则更新 + if params.Tags == "" && outputData.Metadata.Tags != "" { + params.Tags = outputData.Metadata.Tags + // 修复 tags 字段过长导致更新失败 + if len(params.Tags) > 255 { + params.Tags = params.Tags[:255] + } + needUpdate = true + } + + // 如果 params 中的 model 为空,但 output 中有 model_name,则更新 + if params.Model == "" && outputData.ModelName != "" { + params.Model = outputData.ModelName + needUpdate = true + } + + // 如果需要更新,则保存 + if needUpdate { + if err := s.db.Model(&model.SunoJob{}).Where("id = ?", job.Id).Update("params", params).Error; err != nil { + logger.Errorf("更新 SunoJob 数据失败 (ID: %d): %v", job.Id, err) + continue + } + updatedCount++ + } + } + + logger.Infof("SunoJob 数据合并完成,共更新 %d 条记录", updatedCount) + s.redisClient.Set(context.Background(), key, "1", 0) +} diff --git a/api/service/mj/client.go b/api/service/mj/client.go index 50604947..75298fc3 100644 --- a/api/service/mj/client.go +++ b/api/service/mj/client.go @@ -12,17 +12,20 @@ import ( "errors" "fmt" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "geekai/store/model" "geekai/utils" - "github.com/imroc/req/v3" - "gorm.io/gorm" "io" "time" + "github.com/imroc/req/v3" + "gorm.io/gorm" + "github.com/gin-gonic/gin" ) +var logger = log.GetLogger() + // Client MidJourney client type Client struct { client *req.Client @@ -73,8 +76,6 @@ type QueryRes struct { SubmitTime int `json:"submitTime"` } -var logger = logger2.GetLogger() - func NewClient(db *gorm.DB) *Client { return &Client{ client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"), @@ -182,6 +183,28 @@ func (c *Client) Variation(task types.MjTask) (ImageRes, error) { return c.doRequest(body, apiPath, task.ChannelId) } +// Modal 提交局部重绘(inpaint)/ ZOOM,请求体 taskId 必填(提交成功返回的 taskId,不是查询结果里的 messageId),prompt、maskBase64 可选 +func (c *Client) Modal(task types.MjTask) (ImageRes, error) { + apiPath := fmt.Sprintf("mj-%s/mj/submit/modal", task.Mode) + taskId := task.TaskId + if taskId == "" { + taskId = task.MessageId + } + if taskId == "" { + return ImageRes{}, fmt.Errorf("modal 任务缺少原图 taskId(提交成功返回的 ID)") + } + body := map[string]any{ + "taskId": taskId, + } + if task.Prompt != "" { + body["prompt"] = task.Prompt + } + if task.MaskBase64 != "" { + body["maskBase64"] = task.MaskBase64 + } + return c.doRequest(body, apiPath, task.ChannelId) +} + func (c *Client) doRequest(body interface{}, apiPath string, channel string) (ImageRes, error) { var res ImageRes session := c.db.Session(&gorm.Session{}).Where("type", "mj").Where("enabled", true) diff --git a/api/service/mj/service.go b/api/service/mj/service.go index 498d3bb2..ec299031 100644 --- a/api/service/mj/service.go +++ b/api/service/mj/service.go @@ -97,6 +97,9 @@ func (s *Service) Run() { case types.TaskSwapFace: res, err = s.client.SwapFace(task) break + case types.TaskModal: + res, err = s.client.Modal(task) + break } if err != nil || (res.Code != 1 && res.Code != 22) { diff --git a/api/service/moderation/moderation_manager.go b/api/service/moderation/moderation_manager.go index 25125a93..3176054b 100644 --- a/api/service/moderation/moderation_manager.go +++ b/api/service/moderation/moderation_manager.go @@ -2,8 +2,6 @@ package moderation import ( "geekai/core/types" - - logger2 "geekai/logger" ) // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -13,8 +11,6 @@ import ( // * @Author yangjian102621@163.com // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -var logger = logger2.GetLogger() - type Service interface { Moderate(text string) (types.ModerationResult, error) } diff --git a/api/service/oss/tencent_oss.go b/api/service/oss/tencent_oss.go new file mode 100644 index 00000000..1ab1af39 --- /dev/null +++ b/api/service/oss/tencent_oss.go @@ -0,0 +1,167 @@ +package oss + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "geekai/core/types" + "geekai/utils" + "net/http" + "net/url" + "path/filepath" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/tencentyun/cos-go-sdk-v5" +) + +type TencentOss struct { + config types.TencentOssConfig + client *cos.Client + proxyURL string +} + +func NewTencentOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) (*TencentOss, error) { + s := &TencentOss{ + proxyURL: appConfig.ProxyURL, + } + err := s.UpdateConfig(sysConfig.OSS.Tencent) + if err != nil { + logger.Warnf("腾讯云COS初始化失败: %v", err) + } + return s, nil +} + +func (s *TencentOss) UpdateConfig(config types.TencentOssConfig) error { + if config.Bucket == "" || config.Region == "" || config.SecretId == "" || config.SecretKey == "" { + // 配置不完整时不初始化客户端 + s.config = config + return nil + } + + // 构建 COS 客户端 URL + cosURL := fmt.Sprintf("https://%s.cos.%s.myqcloud.com", config.Bucket, config.Region) + u, err := url.Parse(cosURL) + if err != nil { + return fmt.Errorf("error parsing COS URL: %v", err) + } + + // 创建 COS 客户端 + b := &cos.BaseURL{BucketURL: u} + client := cos.NewClient(b, &http.Client{ + Transport: &cos.AuthorizationTransport{ + SecretID: config.SecretId, + SecretKey: config.SecretKey, + }, + }) + + s.client = client + s.config = config + return nil +} + +func (s TencentOss) PutFile(ctx *gin.Context, name string) (File, error) { + // 解析表单 + file, err := ctx.FormFile(name) + if err != nil { + return File{}, err + } + // 打开上传文件 + src, err := file.Open() + if err != nil { + return File{}, err + } + defer src.Close() + + fileExt := filepath.Ext(file.Filename) + objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt) + // 上传文件 + _, err = s.client.Object.Put(ctx, objectKey, src, nil) + if err != nil { + return File{}, err + } + + // 生成文件 URL + fileURL := s.generateURL(objectKey) + + return File{ + Name: file.Filename, + ObjKey: objectKey, + URL: fileURL, + Ext: fileExt, + Size: file.Size, + }, nil +} + +func (s TencentOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string, error) { + var fileData []byte + var err error + if useProxy { + fileData, err = utils.DownloadImage(fileURL, s.proxyURL) + } else { + fileData, err = utils.DownloadImage(fileURL, "") + } + if err != nil { + return "", fmt.Errorf("error with download image: %v", err) + } + parse, err := url.Parse(fileURL) + if err != nil { + return "", fmt.Errorf("error with parse image URL: %v", err) + } + if ext == "" { + ext = filepath.Ext(parse.Path) + } + objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), ext) + // 上传文件字节数据 + _, err = s.client.Object.Put(context.Background(), objectKey, bytes.NewReader(fileData), nil) + if err != nil { + return "", err + } + return s.generateURL(objectKey), nil +} + +func (s TencentOss) PutBase64(base64Img string) (string, error) { + imageData, err := base64.StdEncoding.DecodeString(base64Img) + if err != nil { + return "", fmt.Errorf("error decoding base64:%v", err) + } + objectKey := fmt.Sprintf("%d.png", time.Now().UnixMicro()) + // 上传文件字节数据 + _, err = s.client.Object.Put(context.Background(), objectKey, bytes.NewReader(imageData), nil) + if err != nil { + return "", err + } + return s.generateURL(objectKey), nil +} + +func (s TencentOss) Delete(fileURL string) error { + var objectKey string + if strings.HasPrefix(fileURL, "http") { + objectKey = filepath.Base(fileURL) + } else { + objectKey = fileURL + } + _, err := s.client.Object.Delete(context.Background(), objectKey) + return err +} + +// generateURL 生成文件访问 URL +func (s TencentOss) generateURL(objectKey string) string { + if s.config.Domain != "" { + // 使用自定义域名 + return fmt.Sprintf("%s/%s", strings.TrimSuffix(s.config.Domain, "/"), objectKey) + } + // 使用 COS 默认域名 + return fmt.Sprintf("https://%s.cos.%s.myqcloud.com/%s", s.config.Bucket, s.config.Region, objectKey) +} + +var _ Uploader = TencentOss{} diff --git a/api/service/oss/uploader.go b/api/service/oss/uploader.go index 255bcfe0..1fcb05d7 100644 --- a/api/service/oss/uploader.go +++ b/api/service/oss/uploader.go @@ -13,6 +13,7 @@ const Local = "local" const Minio = "minio" const QiNiu = "qiniu" const AliYun = "aliyun" +const Tencent = "tencent" type File struct { Name string `json:"name"` diff --git a/api/service/oss/uploader_manager.go b/api/service/oss/uploader_manager.go index dc437178..d0cdb149 100644 --- a/api/service/oss/uploader_manager.go +++ b/api/service/oss/uploader_manager.go @@ -8,32 +8,41 @@ package oss // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "fmt" "geekai/core/types" + "strings" - logger2 "geekai/logger" + "geekai/log" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() + +// 默认缩略图模板(本地存储格式) +const DefaultThumbTemplate = "?imageView2/4/w/{width}/h/{height}/q/75" type UploaderManager struct { - local *LocalStorage - aliyun *AliYunOss - mini *MiniOss - qiniu *QiNiuOss - active string + local *LocalStorage + aliyun *AliYunOss + mini *MiniOss + qiniu *QiNiuOss + tencent *TencentOss + active string + ossConfig types.OSSConfig // 保存当前OSS配置 } -func NewUploaderManager(sysConfig *types.SystemConfig, local *LocalStorage, aliyun *AliYunOss, mini *MiniOss, qiniu *QiNiuOss) (*UploaderManager, error) { +func NewUploaderManager(sysConfig *types.SystemConfig, local *LocalStorage, aliyun *AliYunOss, mini *MiniOss, qiniu *QiNiuOss, tencent *TencentOss) (*UploaderManager, error) { if sysConfig.OSS.Active == "" { sysConfig.OSS.Active = Local } return &UploaderManager{ - active: sysConfig.OSS.Active, - local: local, - aliyun: aliyun, - mini: mini, - qiniu: qiniu, + active: sysConfig.OSS.Active, + local: local, + aliyun: aliyun, + mini: mini, + qiniu: qiniu, + tencent: tencent, + ossConfig: sysConfig.OSS, }, nil } @@ -47,6 +56,8 @@ func (m *UploaderManager) GetUploadHandler() Uploader { return m.mini case QiNiu: return m.qiniu + case Tencent: + return m.tencent } return m.local } @@ -61,6 +72,71 @@ func (m *UploaderManager) UpdateConfig(config types.OSSConfig) { m.mini.UpdateConfig(config.Minio) case QiNiu: m.qiniu.UpdateConfig(config.QiNiu) + case Tencent: + m.tencent.UpdateConfig(config.Tencent) } m.active = config.Active + m.ossConfig = config +} + +// GetThumbURL 根据原始图片URL和尺寸生成缩略图URL +// 如果模板为空,使用默认模板(本地存储格式) +// 如果明确设置为空字符串(表示不支持缩略图),返回原图URL +func (m *UploaderManager) GetThumbURL(originalURL string, width, height int) string { + var template string + + // 根据当前激活的存储引擎获取对应的模板 + switch m.active { + case Local: + template = m.ossConfig.Local.ThumbTemplate + case AliYun: + template = m.ossConfig.AliYun.ThumbTemplate + case Minio: + template = m.ossConfig.Minio.ThumbTemplate + case QiNiu: + template = m.ossConfig.QiNiu.ThumbTemplate + case Tencent: + template = m.ossConfig.Tencent.ThumbTemplate + default: + template = m.ossConfig.Local.ThumbTemplate + } + + // 如果模板为空,使用默认模板(兼容旧配置) + if template == "" { + template = DefaultThumbTemplate + } + + // 替换变量 + thumbURL := strings.ReplaceAll(template, "{width}", fmt.Sprintf("%d", width)) + thumbURL = strings.ReplaceAll(thumbURL, "{height}", fmt.Sprintf("%d", height)) + + // 拼接原始URL和缩略图参数 + return originalURL + thumbURL +} + +// GetThumbTemplate 获取当前存储引擎的缩略图模板 +func (m *UploaderManager) GetThumbTemplate() string { + var template string + + switch m.active { + case Local: + template = m.ossConfig.Local.ThumbTemplate + case AliYun: + template = m.ossConfig.AliYun.ThumbTemplate + case Minio: + template = m.ossConfig.Minio.ThumbTemplate + case QiNiu: + template = m.ossConfig.QiNiu.ThumbTemplate + case Tencent: + template = m.ossConfig.Tencent.ThumbTemplate + default: + template = m.ossConfig.Local.ThumbTemplate + } + + // 如果模板为空,返回默认模板 + if template == "" { + return DefaultThumbTemplate + } + + return template } diff --git a/api/service/payment/alipay_service.go b/api/service/payment/alipay_service.go index ce567f2a..f941d515 100644 --- a/api/service/payment/alipay_service.go +++ b/api/service/payment/alipay_service.go @@ -11,7 +11,7 @@ import ( "context" "fmt" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "net/http" "os" @@ -24,7 +24,7 @@ type AlipayService struct { config *types.AlipayConfig } -var logger = logger2.GetLogger() +var logger = log.GetLogger() func NewAlipayService(sysConfig *types.SystemConfig) (*AlipayService, error) { config := sysConfig.Payment.Alipay diff --git a/api/service/payment/wxpay_service.go b/api/service/payment/wxpay_service.go index 89ca27a9..09257cc0 100644 --- a/api/service/payment/wxpay_service.go +++ b/api/service/payment/wxpay_service.go @@ -9,6 +9,7 @@ package payment import ( "context" + "encoding/json" "fmt" "geekai/core/types" "geekai/utils" @@ -88,7 +89,18 @@ func (s *WxPayService) Pay(params PayRequest) (string, error) { if wxRsp.Code != wechat.Success { return "", fmt.Errorf("error status with generating pay url: %v", wxRsp.Error) } - return wxRsp.Response.PrepayId, nil + // 签名 + payParams, err := s.client.PaySignOfJSAPI(s.config.AppId, wxRsp.Response.PrepayId) + + if err != nil { + return "", fmt.Errorf("error with generating jsapi pay sign: %v", err) + } + payParamsBytes, err := json.Marshal(payParams) + if err != nil { + return "", fmt.Errorf("error with marshaling pay params: %v", err) + } + + return string(payParamsBytes), nil } else if params.Device == "pc" { wxRsp, err := s.client.V3TransactionNative(context.Background(), bm) if err != nil { diff --git a/api/service/ppt/embed/minimal.pptx b/api/service/ppt/embed/minimal.pptx new file mode 100644 index 00000000..e1095c2e Binary files /dev/null and b/api/service/ppt/embed/minimal.pptx differ diff --git a/api/service/ppt/export.go b/api/service/ppt/export.go new file mode 100644 index 00000000..d1052c41 --- /dev/null +++ b/api/service/ppt/export.go @@ -0,0 +1,404 @@ +package ppt + +import ( + "bytes" + "context" + _ "embed" + "fmt" + "geekai/core/types" + "image" + _ "image/gif" + "io" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "sort" + "strings" + "time" + "unicode" + + "github.com/jung-kurt/gofpdf/v2" + "github.com/ktye/pptx" + _ "golang.org/x/image/webp" +) + +//go:embed embed/minimal.pptx +var minimalPptxTemplate []byte + +const ( + exportHTTPTimeout = 60 * time.Second + // 必须与 embed/minimal.pptx 中 p:sldSz 一致(当前模板为 4:3:10\"×7.5\") + slideEmuW pptx.Dimension = 9144000 + slideEmuH pptx.Dimension = 6858000 +) + +// ExportFormat 导出类型 +type ExportFormat string + +const ( + ExportFormatPDF ExportFormat = "pdf" + ExportFormatPPTX ExportFormat = "pptx" +) + +// ExportMimeType 返回 Content-Type +func ExportMimeType(f ExportFormat) string { + switch f { + case ExportFormatPDF: + return "application/pdf" + case ExportFormatPPTX: + return "application/vnd.openxmlformats-officedocument.presentationml.presentation" + default: + return "application/octet-stream" + } + +} + +// ExportFileExt 返回文件扩展名(含点) +func ExportFileExt(f ExportFormat) string { + switch f { + case ExportFormatPDF: + return ".pdf" + case ExportFormatPPTX: + return ".pptx" + default: + return "" + } +} + +// ParseExportFormat 解析 query format +func ParseExportFormat(s string) (ExportFormat, bool) { + switch strings.ToLower(strings.TrimSpace(s)) { + case "pdf": + return ExportFormatPDF, true + case "pptx", "ppt": + return ExportFormatPPTX, true + default: + return "", false + } +} + +// SanitizeExportBaseName 用于下载文件名的主体(不含扩展名) +func SanitizeExportBaseName(title, taskID string) string { + s := strings.TrimSpace(title) + repl := strings.NewReplacer( + "/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_", + ) + s = repl.Replace(s) + var b strings.Builder + for _, r := range s { + if r == unicode.ReplacementChar || r < 32 { + continue + } + b.WriteRune(r) + } + s = strings.TrimSpace(b.String()) + if s == "" { + s = strings.TrimSpace(taskID) + } + if len([]rune(s)) > 120 { + rs := []rune(s) + s = string(rs[:120]) + } + return s +} + +// ContentDispositionAttachment RFC 5987,兼容旧客户端 +func ContentDispositionAttachment(filename string) string { + ascii := filename + for _, r := range filename { + if r > 127 || r == '"' || r == '\\' { + ascii = "export" + strings.ToLower(filepath.Ext(filename)) + if ascii == "export" { + ascii = "export.bin" + } + break + } + } + return fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, ascii, url.PathEscape(filename)) +} + +// mapLocalUploadFile 将站点相对路径或完整 BaseURL 前缀映射为本地文件路径(local OSS) +func mapLocalUploadFile(raw string, local types.LocalStorageConfig) (string, bool) { + raw = strings.TrimSpace(raw) + if raw == "" || local.BasePath == "" { + return "", false + } + bp := filepath.Clean(local.BasePath) + bu := strings.TrimSuffix(strings.TrimSpace(local.BaseURL), "/") + if bu != "" && strings.HasPrefix(raw, bu) { + suffix := strings.TrimPrefix(strings.TrimPrefix(raw, bu), "/") + return filepath.Join(bp, suffix), true + } + if strings.HasPrefix(raw, local.BaseURL) { + return filepath.Join(bp, strings.TrimPrefix(raw, local.BaseURL)), true + } + return "", false +} + +func originFromBaseURL(baseURL string) string { + u, err := url.Parse(strings.TrimSpace(baseURL)) + if err != nil || u.Scheme == "" || u.Host == "" { + return "" + } + return u.Scheme + "://" + u.Host +} + +func defaultOriginFromListen(listen string) string { + listen = strings.TrimSpace(listen) + if listen == "" { + return "" + } + host, port, err := net.SplitHostPort(listen) + if err != nil { + if strings.HasPrefix(listen, ":") { + return "http://127.0.0.1" + listen + } + return "" + } + if host == "0.0.0.0" || host == "::" || host == "" { + host = "127.0.0.1" + } + return "http://" + net.JoinHostPort(host, port) +} + +// resolveAbsoluteImageURL 将可能为相对路径的地址转为可 HTTP 访问的绝对 URL +func resolveAbsoluteImageURL(raw string, local types.LocalStorageConfig, app *types.AppConfig) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return raw + } + if u, err := url.Parse(raw); err == nil && u.Scheme != "" && u.Host != "" { + return raw + } + if strings.HasPrefix(raw, "//") { + return "https:" + raw + } + origin := originFromBaseURL(local.BaseURL) + if origin == "" && app != nil { + origin = originFromBaseURL(app.StaticUrl) + } + if origin == "" && app != nil { + origin = defaultOriginFromListen(app.Listen) + } + if origin == "" { + return raw + } + if strings.HasPrefix(raw, "/") { + return strings.TrimSuffix(origin, "/") + raw + } + return strings.TrimSuffix(origin, "/") + "/" + raw +} + +// BuildExportBytes 按幻灯片顺序拉取图片并生成 PDF 或 PPTX(oss/app 用于解析相对路径图片 URL) +func BuildExportBytes(ctx context.Context, slides []SlideData, format ExportFormat, oss types.OSSConfig, app *types.AppConfig) ([]byte, error) { + raws, err := fetchSlideImages(ctx, slides, oss, app) + if err != nil { + return nil, err + } + if len(raws) == 0 { + return nil, fmt.Errorf("没有可导出的幻灯片图片") + } + switch format { + case ExportFormatPDF: + return buildPDF(raws) + case ExportFormatPPTX: + return buildPPTX(raws) + default: + return nil, fmt.Errorf("不支持的导出格式") + } +} + +func fetchSlideImages(ctx context.Context, slides []SlideData, oss types.OSSConfig, app *types.AppConfig) ([][]byte, error) { + cp := append([]SlideData(nil), slides...) + sort.Slice(cp, func(i, j int) bool { return cp[i].SlideIndex < cp[j].SlideIndex }) + + client := &http.Client{Timeout: exportHTTPTimeout} + var out [][]byte + for _, s := range cp { + u := strings.TrimSpace(s.ImageURL) + if u == "" { + continue + } + + var body []byte + if oss.Active == "local" { + if fp, ok := mapLocalUploadFile(u, oss.Local); ok { + b, err := os.ReadFile(fp) + if err == nil { + if _, err := decodeImageBytes(b); err == nil { + body = b + } + } + } + } + if len(body) == 0 { + absURL := resolveAbsoluteImageURL(u, oss.Local, app) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, absURL, nil) + if err != nil { + return nil, fmt.Errorf("幻灯片 %d: %w", s.SlideIndex, err) + } + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("幻灯片 %d 下载失败: %w", s.SlideIndex, err) + } + body, err = io.ReadAll(io.LimitReader(resp.Body, 32<<20)) + _ = resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("幻灯片 %d 读取失败: %w", s.SlideIndex, err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("幻灯片 %d 下载失败: HTTP %d", s.SlideIndex, resp.StatusCode) + } + } + if _, err := decodeImageBytes(body); err != nil { + return nil, fmt.Errorf("幻灯片 %d 不是有效图片: %v", s.SlideIndex, err) + } + out = append(out, body) + } + return out, nil +} + +func decodeImageBytes(b []byte) (image.Image, error) { + m, _, err := image.Decode(bytes.NewReader(b)) + return m, err +} + +// fitSlideBoundsEMU 在幻灯片 EMU 框(与模板 p:sldSz 一致)内按原图比例 contain 居中(不裁切) +func fitSlideBoundsEMU(imgW, imgH int) (x, y, w, h pptx.Dimension) { + if imgW <= 0 || imgH <= 0 { + return 0, 0, slideEmuW, slideEmuH + } + sw := float64(slideEmuW) + sh := float64(slideEmuH) + iw := float64(imgW) + ih := float64(imgH) + scale := sw / iw + if ih*scale > sh { + scale = sh / ih + } + wf := iw * scale + hf := ih * scale + w = pptx.Dimension(wf + 0.5) + h = pptx.Dimension(hf + 0.5) + x = pptx.Dimension((sw-wf)*0.5 + 0.5) + y = pptx.Dimension((sh-hf)*0.5 + 0.5) + return x, y, w, h +} + +func buildPDF(images [][]byte) ([]byte, error) { + pdf := gofpdf.New("L", "mm", "A4", "") + pdf.SetMargins(0, 0, 0) + pdf.SetAutoPageBreak(false, 0) + // 像素 → mm(按 96 DPI),再按页面对比缩放以 contain 放入整页 + const pxPerMM = 96.0 / 25.4 + + for i, raw := range images { + im, err := decodeImageBytes(raw) + if err != nil { + return nil, fmt.Errorf("第 %d 页: %w", i+1, err) + } + b := im.Bounds() + pxW := float64(b.Dx()) + pxH := float64(b.Dy()) + if pxW <= 0 || pxH <= 0 { + return nil, fmt.Errorf("第 %d 页: 图片尺寸无效", i+1) + } + pdf.AddPage() + pageW, pageH := pdf.GetPageSize() + imgWmm := pxW / pxPerMM + imgHmm := pxH / pxPerMM + scale := pageW / imgWmm + if imgHmm*scale > pageH { + scale = pageH / imgHmm + } + w := imgWmm * scale + h := imgHmm * scale + x := (pageW - w) / 2 + y := (pageH - h) / 2 + + name := fmt.Sprintf("slide%d", i) + opt := gofpdf.ImageOptions{ReadDpi: false} + tp := sniffImageType(raw) + if tp != "" { + opt.ImageType = tp + } + if pdf.RegisterImageOptionsReader(name, opt, bytes.NewReader(raw)) == nil { + return nil, fmt.Errorf("第 %d 页: 无法写入 PDF 图片", i+1) + } + pdf.ImageOptions(name, x, y, w, h, false, opt, 0, "") + } + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func sniffImageType(b []byte) string { + if len(b) < 12 { + return "" + } + switch { + case len(b) >= 2 && b[0] == 0xFF && b[1] == 0xD8: + return "jpg" + case len(b) >= 8 && string(b[0:8]) == "\x89PNG\r\n\x1a\n": + return "png" + case len(b) >= 6 && string(b[0:6]) == "GIF87a" || string(b[0:6]) == "GIF89a": + return "gif" + case len(b) >= 12 && string(b[0:4]) == "RIFF" && string(b[8:12]) == "WEBP": + return "webp" + default: + return "" + } +} + +func buildPPTX(images [][]byte) ([]byte, error) { + if len(minimalPptxTemplate) == 0 { + return nil, fmt.Errorf("内置 PPT 模板缺失") + } + tmp, err := os.CreateTemp("", "ppt-export-*.pptx") + if err != nil { + return nil, err + } + path := tmp.Name() + if _, err := tmp.Write(minimalPptxTemplate); err != nil { + _ = tmp.Close() + _ = os.Remove(path) + return nil, err + } + if err := tmp.Close(); err != nil { + _ = os.Remove(path) + return nil, err + } + defer func() { _ = os.Remove(path) }() + + f, err := pptx.Open(path) + if err != nil { + return nil, err + } + + for _, raw := range images { + im, err := decodeImageBytes(raw) + if err != nil { + f.Abort() + return nil, err + } + b := im.Bounds() + ex, ey, ew, eh := fitSlideBoundsEMU(b.Dx(), b.Dy()) + slide := pptx.Slide{ + Images: []pptx.Image{ + pptx.NewImage(im, ex, ey, ew, eh), + }, + } + if err := f.Add(slide); err != nil { + f.Abort() + return nil, err + } + } + if err := f.Close(); err != nil { + return nil, err + } + return os.ReadFile(path) +} diff --git a/api/service/ppt/image_adapter.go b/api/service/ppt/image_adapter.go new file mode 100644 index 00000000..6adbc9fb --- /dev/null +++ b/api/service/ppt/image_adapter.go @@ -0,0 +1,263 @@ +package ppt + +import ( + "context" + "fmt" + "geekai/core/types" + "geekai/log" + "net/http" + "time" + + "github.com/imroc/req/v3" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" + "github.com/volcengine/volcengine-go-sdk/volcengine" + "golang.org/x/time/rate" +) + +var imageLogger = log.GetLogger() + +// ImageGenerator 图片生成适配器接口 +type ImageGenerator interface { + Provider() string + Generate(ctx context.Context, prompt string) (string, error) + // GenerateWithReference 图生图;referenceImages 为公网 URL 或 data:image/...;base64,...(本地文件应在调用前经 PrepareReferenceInputsForImg2Img 转换) + GenerateWithReference(ctx context.Context, prompt string, referenceImages []string) (string, error) +} + +// Nano Banana 适配器(OpenAI DALL-E 风格 API) +type nanoBananaImageGenerator struct { + client *req.Client + cfg types.PPTConfig + limiter *rate.Limiter +} + +// Seedream 适配器(火山引擎 arkruntime SDK) +type seedreamImageGenerator struct { + cfg types.PPTConfig + limiter *rate.Limiter +} + +// NewImageGenerator 根据配置创建对应的图片生成适配器 +func NewImageGenerator(cfg types.PPTConfig) (ImageGenerator, error) { + qps := cfg.QPSLimit + if qps <= 0 { + qps = 1 + } + limiter := rate.NewLimiter(rate.Limit(qps), 1) + + switch cfg.ActiveImageProvider { + case types.PPTImageProviderNanoBanana: + if cfg.NanoBananaApiURL == "" || cfg.NanoBananaApiKey == "" { + return nil, fmt.Errorf("nano banana api not configured") + } + return &nanoBananaImageGenerator{ + client: req.C().SetTimeout(3 * time.Minute), + cfg: cfg, + limiter: limiter, + }, nil + case types.PPTImageProviderSeedream: + if cfg.SeedreamBaseURL == "" || cfg.SeedreamApiKey == "" || cfg.SeedreamModel == "" { + return nil, fmt.Errorf("seedream api not configured") + } + return &seedreamImageGenerator{ + cfg: cfg, + limiter: limiter, + }, nil + default: + return nil, fmt.Errorf("unsupported image provider: %s", cfg.ActiveImageProvider) + } +} + +func (g *nanoBananaImageGenerator) Provider() string { + return string(types.PPTImageProviderNanoBanana) +} + +// nanoBananaReq 按 OpenAI DALL-E 风格 / Nano-banana API 文档 +type nanoBananaReq struct { + Model string `json:"model"` + Prompt string `json:"prompt"` + ResponseFormat string `json:"response_format,omitempty"` // url 或 b64_json + AspectRatio string `json:"aspect_ratio,omitempty"` // 1:1, 4:3, 3:4, 16:9, 9:16, 2:3, 3:2, 4:5, 5:4, 21:9 + Image []string `json:"image,omitempty"` // 参考图 url 或 b64 +} + +// nanoBananaRes 响应为 data[].url(DALL-E 风格) +type nanoBananaRes struct { + Data []struct { + URL string `json:"url,omitempty"` + B64JSON string `json:"b64_json,omitempty"` + } `json:"data"` +} + +type nanoBananaErr struct { + Error struct { + Message string `json:"message"` + } `json:"error"` +} + +func (g *nanoBananaImageGenerator) buildReqBody(prompt string, referenceImages []string) nanoBananaReq { + modelName := g.cfg.NanoBananaModel + if modelName == "" { + modelName = "nano-banana" + } + reqBody := nanoBananaReq{ + Model: modelName, + Prompt: prompt, + } + if len(referenceImages) > 0 { + reqBody.Image = referenceImages + } + if g.cfg.NanoBananaResponseFormat != "" { + reqBody.ResponseFormat = g.cfg.NanoBananaResponseFormat + } else { + reqBody.ResponseFormat = "url" + } + if g.cfg.NanoBananaAspectRatio != "" { + reqBody.AspectRatio = g.cfg.NanoBananaAspectRatio + } else { + reqBody.AspectRatio = "16:9" + } + return reqBody +} + +func (g *nanoBananaImageGenerator) Generate(ctx context.Context, prompt string) (string, error) { + return g.GenerateWithReference(ctx, prompt, nil) +} + +func (g *nanoBananaImageGenerator) GenerateWithReference(ctx context.Context, prompt string, referenceImages []string) (string, error) { + reqBody := g.buildReqBody(prompt, referenceImages) + + var ( + result nanoBananaRes + errRes nanoBananaErr + ) + + do := func() (int, error) { + if err := g.limiter.Wait(ctx); err != nil { + return 0, err + } + + imageLogger.Infof("nano banana generate image, api: %s", g.cfg.NanoBananaApiURL) + r, err := g.client.R(). + SetContext(ctx). + SetHeader("Content-Type", "application/json"). + SetHeader("Authorization", "Bearer "+g.cfg.NanoBananaApiKey). + SetBody(reqBody). + SetSuccessResult(&result). + SetErrorResult(&errRes). + Post(g.cfg.NanoBananaApiURL) + if err != nil { + return 0, err + } + if r.IsErrorState() { + return r.StatusCode, fmt.Errorf("nano banana error: %s, %s", r.Status, errRes.Error.Message) + } + if len(result.Data) == 0 || result.Data[0].URL == "" { + return r.StatusCode, fmt.Errorf("nano banana returned empty data") + } + return r.StatusCode, nil + } + + if err := callWithRetry(ctx, do); err != nil { + return "", err + } + + return result.Data[0].URL, nil +} + +func (g *seedreamImageGenerator) Provider() string { + return string(types.PPTImageProviderSeedream) +} + +func (g *seedreamImageGenerator) Generate(ctx context.Context, prompt string) (string, error) { + return g.GenerateWithReference(ctx, prompt, nil) +} + +func (g *seedreamImageGenerator) GenerateWithReference(ctx context.Context, prompt string, referenceImages []string) (string, error) { + if err := g.limiter.Wait(ctx); err != nil { + return "", err + } + + client := arkruntime.NewClientWithApiKey(g.cfg.SeedreamApiKey, arkruntime.WithBaseUrl(g.cfg.SeedreamBaseURL)) + + size := g.cfg.SeedreamSize + if size == "" { + size = "1920x1080" + } + responseFormat := g.cfg.SeedreamResponseType + if responseFormat == "" { + responseFormat = "url" + } + + generateReq := model.GenerateImagesRequest{ + Model: g.cfg.SeedreamModel, + Prompt: prompt, + Size: volcengine.String(size), + ResponseFormat: volcengine.String(responseFormat), + Watermark: volcengine.Bool(g.cfg.SeedreamWatermark), + } + if len(referenceImages) > 0 { + generateReq.Image = referenceImages + } + + var lastErr error + for attempt := 0; attempt < 3; attempt++ { + if attempt > 0 { + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(time.Duration(attempt) * 2 * time.Second): + } + } + imageLogger.Infof("seedream generate image, api: %s", g.cfg.SeedreamBaseURL) + if err := generateReq.NormalizeImages(); err != nil { + return "", fmt.Errorf("seedream normalize images: %w", err) + } + resp, err := client.GenerateImages(ctx, generateReq) + if err != nil { + lastErr = fmt.Errorf("seedream error: %w", err) + continue + } + if resp.Data == nil || len(resp.Data) == 0 { + lastErr = fmt.Errorf("seedream returned empty data") + continue + } + if resp.Data[0].Url == nil || *resp.Data[0].Url == "" { + lastErr = fmt.Errorf("seedream returned empty url") + continue + } + return *resp.Data[0].Url, nil + } + return "", lastErr +} + +// callWithRetry 对 429 错误做指数退避重试 +func callWithRetry(ctx context.Context, fn func() (int, error)) error { + var ( + retries = 3 + backoffs = []time.Duration{2 * time.Second, 4 * time.Second, 8 * time.Second} + lastError error + ) + + for i := 0; i < retries; i++ { + status, err := fn() + if err == nil { + return nil + } + lastError = err + + // 仅对 429 做指数退避重试 + if status != http.StatusTooManyRequests || i == retries-1 { + break + } + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(backoffs[i]): + } + } + + return lastError +} diff --git a/api/service/ppt/llm_client.go b/api/service/ppt/llm_client.go new file mode 100644 index 00000000..1d554cab --- /dev/null +++ b/api/service/ppt/llm_client.go @@ -0,0 +1,373 @@ +package ppt + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "context" + "encoding/json" + "fmt" + "geekai/core/types" + "geekai/log" + "strings" + "time" + + "github.com/imroc/req/v3" +) + +var logger = log.GetLogger() + +// slidePlan LLM 输出的分镜结构 +type slidePlan struct { + SlideIndex int `json:"slide_index"` + Theme string `json:"theme"` + Title string `json:"title"` + Points []string `json:"points"` + ImagePrompt string `json:"image_prompt"` +} + +// systemPrompt 图文并茂幻灯片:每页有插图且画面上含与主题一致的文字,文字量与生成模式相关 +const systemPrompt = ` +# Role + +你是一位顶级的专业演示文稿(PPT)策划专家和 AI 图像提示词(Prompt)工程师。任务是根据用户提供的「内容大纲」或「设计要求」,生成一套逻辑清晰、视觉风格高度统一的幻灯片分镜数据。目标是生成**图文并茂**的幻灯片:每页既有文字又有插图,图片上直接呈现与本页内容一致的文字,而不是留白让用户后加文字。 + +# Rules + +1. 全局风格锚定:根据大纲推断或遵循用户要求的全局视觉风格。所有配图必须严格遵循此风格。 +2. 结构化拆解:合理拆分为多张幻灯片,单页最多 3-4 个简短要点。 +3. 视觉转译(图文并茂): + - 为每页构思的 image_prompt 既要描述**插图画面**,也要明确**画面上应出现的文字**(如本页标题、要点或短句),与当页 theme、title、points 内容一致。不要描述留白或“用于排版文字的空间”。 + - 图片中出现的所有文字必须使用与 theme、title、points **相同的语言**(即本次请求指定的输出语言)。 + - 图片上文字的量由「生成模式」决定(见用户输入中的模式说明): + - **详细演示文稿**:图片上的说明文字可适当多一些,如本页要点、一两句说明。 + - **演示用幻灯片**:图片上的文字尽量精简,如仅主标题或少量关键词,便于演讲时配合口述。 + - image_prompt 须包含前缀「[全局风格描述]」,并清晰描述画面中的插图与文字内容(含具体要出现的文字及其语言),不要包含“不要在图片中生成任何文字”的约束。 +4. 严格输出合法的纯 JSON 数组: +[ + {"slide_index": 1, "theme": "...", "title": "...", "points": ["..."], "image_prompt": "..."} +] +禁止输出任何 Markdown 标记或多余文本。` + +// notebookSystemPrompt 文档提炼:NotebookLM 风格输出 PPT 可用大纲文本(纯文本/Markdown)。 +const notebookSystemPrompt = ` +# Role + +你是一位“NotebookLM 风格”的专业文档理解与提炼助手。任务是基于用户提供的「原始文档文本」和「设计要求」,提炼出可用于制作 PPT 的结构化大纲内容。 + +# Output Requirements + +1. 输出必须是纯文本/Markdown(允许使用标题与列表),禁止输出任何 JSON。 +2. 禁止输出代码块(不要出现代码块语法)。 +3. 不要输出解释过程、不要复述提示词。 +4. 大纲必须是“内容大纲/要点”,用于后续继续拆分成幻灯片,而不是直接输出最终幻灯片分镜。 + +# Rules + +1. 文档优先:尽可能从原始文档中提取信息与措辞;若文档缺失关键点,则给出合理补全的“建议方向”,并明确标注为“(建议)”。 +2. 贴合设计要求:根据设计要求调整大纲的语气、侧重点、术语风格,使内容更符合目标受众与整体风格。 +3. 结构清晰:使用分层标题(例如:# 总主题、## 模块/章节、### 要点),并为每个模块给出 2-4 个要点句(可直接用于 PPT 每页标题/要点)。 +4. 语言一致:所有输出语言必须与本次请求指定的语言一致。 +` + +// LLMClient 分镜 LLM 客户端 +type LLMClient struct { + httpClient *req.Client +} + +func NewLLMClient() *LLMClient { + return &LLMClient{ + httpClient: req.C().SetTimeout(2 * time.Minute), + } +} + +// GenerateSlides 调用大模型生成分镜列表。language 约束输出语言,mode 约束图中文字量,maxPages 约束恰好生成 N 页。 +func (c *LLMClient) GenerateSlides(ctx context.Context, cfg types.PPTConfig, content, prompt, language, mode string, maxPages int) ([]slidePlan, error) { + if cfg.OutlineLLMApiURL == "" { + return nil, fmt.Errorf("outline LLM api url is empty") + } + if cfg.OutlineLLMApiKey == "" { + return nil, fmt.Errorf("outline LLM api key is empty") + } + if maxPages <= 0 { + maxPages = 10 + } + if mode != "detailed" && mode != "slides" { + mode = "slides" + } + + type message struct { + Role string `json:"role"` + Content string `json:"content"` + } + + // 动态 system:加入页数约束 + systemContent := systemPrompt + fmt.Sprintf("\n\n# 页数约束\n请将内容拆分为恰好 %d 页的幻灯片分镜,保证逻辑完整、故事线连贯,不要多也不要少。输出 JSON 数组长度必须为 %d。", maxPages, maxPages) + + // 组装用户输入 + userContent := fmt.Sprintf("下面是用户提供的演示文稿大纲内容:\n\n%s", content) + if prompt != "" { + userContent = fmt.Sprintf("%s\n\n额外的设计要求:%s", userContent, prompt) + } + if language != "" { + langHint := "中文" + if language == "en" || language == "en-US" { + langHint = "英文" + } else if language == "zh-CN" || language == "zh" { + langHint = "中文" + } else { + langHint = "语言代码 " + language + " 对应的语言" + } + userContent = fmt.Sprintf("%s\n\n请用%s输出所有分镜内容(theme、title、points、image_prompt 等均使用该语言;图片中出现的文字也必须是%s)。", userContent, langHint, langHint) + } + modeHint := "演示用幻灯片" + if mode == "detailed" { + modeHint = "详细演示文稿" + } + userContent = fmt.Sprintf("%s\n\n本次生成模式为:%s。请按上述规则控制每页插图中文字的量。", userContent, modeHint) + + modelName := cfg.OutlineLLMModel + if modelName == "" { + modelName = "gpt-5.2" + } + body := map[string]any{ + "model": modelName, + "messages": []message{ + {Role: "user", Content: systemContent + "\n\n" + userContent}, + }, + "temperature": 0.8, + } + + var respBody struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + } + + logger.Infof("generate PPT slides with outline LLM, api: %s", cfg.OutlineLLMApiURL) + r, err := c.httpClient.R(). + SetContext(ctx). + SetHeader("Content-Type", "application/json"). + SetHeader("Authorization", "Bearer "+cfg.OutlineLLMApiKey). + SetBody(body). + SetSuccessResult(&respBody). + Post(cfg.OutlineLLMApiURL) + if err != nil { + return nil, fmt.Errorf("request outline LLM failed: %v", err) + } + if r.IsErrorState() { + return nil, fmt.Errorf("outline LLM returned error status: %s", r.Status) + } + + if len(respBody.Choices) == 0 { + return nil, fmt.Errorf("outline LLM returned empty choices") + } + + contentStr := respBody.Choices[0].Message.Content + var plans []slidePlan + if err := json.Unmarshal([]byte(contentStr), &plans); err != nil { + return nil, fmt.Errorf("parse outline LLM json failed: %v, raw: %s", err, contentStr) + } + + return plans, nil +} + +// GenerateNotebookContent 调用文档提炼 LLM,把 rawDocText -> PPT 可用的 content(大纲/结构化要点)。 +func (c *LLMClient) GenerateNotebookContent(ctx context.Context, cfg types.PPTConfig, rawDocText, designPrompt, language string) (string, error) { + if cfg.OutlineLLMApiURL == "" { + return "", fmt.Errorf("outline LLM api url is empty") + } + if cfg.OutlineLLMApiKey == "" { + return "", fmt.Errorf("outline LLM api key is empty") + } + + rawDocText = strings.TrimSpace(rawDocText) + if rawDocText == "" { + return "", fmt.Errorf("rawDocText is empty") + } + + // 对超长输入做保守截断,避免请求体过大或上下文溢出。 + // 这里按“字符数”截断,真实 token 仍可能超出,但作为兜底足够。 + const maxChars = 25000 + runes := []rune(rawDocText) + if len(runes) > maxChars { + rawDocText = string(runes[:maxChars]) + } + + langHint := "中文" + if language == "en" || language == "en-US" { + langHint = "英文" + } else if language == "zh-CN" || language == "zh" { + langHint = "中文" + } else if language != "" { + langHint = "语言代码 " + language + " 对应的语言" + } + + maxSlides := cfg.MaxSlidesPerTask + if maxSlides <= 0 { + maxSlides = 10 + } + + userContent := fmt.Sprintf("原始文档文本如下(可能很长):\n\n%s", rawDocText) + if strings.TrimSpace(designPrompt) != "" { + userContent = fmt.Sprintf("%s\n\n设计要求(风格/受众/侧重点等):\n%s", userContent, designPrompt) + } + userContent = fmt.Sprintf( + "%s\n\n请用%s输出 PPT 大纲内容。该大纲应便于拆分为不超过 %d 页的 PPT。", + userContent, + langHint, + maxSlides, + ) + + type message struct { + Role string `json:"role"` + Content string `json:"content"` + } + + systemContent := notebookSystemPrompt + fmt.Sprintf("\n\n# 语言约束\n输出语言:%s。", langHint) + + modelName := cfg.OutlineLLMModel + if modelName == "" { + modelName = "gpt-4o-mini" + } + + body := map[string]any{ + "model": modelName, + "messages": []message{ + {Role: "user", Content: systemContent + "\n\n" + userContent}, + }, + "temperature": 0.4, + } + + var respBody struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + } + + logger.Infof("generate PPT content with outline LLM, api: %s", cfg.OutlineLLMApiURL) + r, err := c.httpClient.R(). + SetContext(ctx). + SetHeader("Content-Type", "application/json"). + SetHeader("Authorization", "Bearer "+cfg.OutlineLLMApiKey). + SetBody(body). + SetSuccessResult(&respBody). + Post(cfg.OutlineLLMApiURL) + if err != nil { + return "", fmt.Errorf("request outline LLM failed: %v", err) + } + if r.IsErrorState() { + return "", fmt.Errorf("outline LLM returned error status: %s", r.Status) + } + if len(respBody.Choices) == 0 { + return "", fmt.Errorf("outline LLM returned empty choices") + } + + return strings.TrimSpace(respBody.Choices[0].Message.Content), nil +} + +// titleSystemPrompt 根据大纲生成用于任务列表的短标题(单行纯文本) +const titleSystemPrompt = ` +# Role + +你是「演示文稿命名助手」。用户会提供一份 PPT 内容大纲(可能含 Markdown)。请根据大纲主题与受众,生成一个**适合出现在任务列表中的短标题**。 + +# Output Rules + +1. 只输出**一行**纯文本,不要换行、不要编号、不要引号包裹。 +2. 长度建议 **20 个字以内**(中文)或 **8 个英文单词以内**;若大纲极长,仍只给概括性标题。 +3. 输出语言必须与本次指定的「输出语言」一致。 +4. 不要输出“标题:”“Title:”等前缀,不要复述本说明。 +` + +// GeneratePPTTitle 调用大模型根据 content 生成列表用短标题;失败时由调用方降级。 +func (c *LLMClient) GeneratePPTTitle(ctx context.Context, cfg types.PPTConfig, content, language string) (string, error) { + if cfg.OutlineLLMApiURL == "" { + return "", fmt.Errorf("outline LLM api url is empty") + } + if cfg.OutlineLLMApiKey == "" { + return "", fmt.Errorf("outline LLM api key is empty") + } + + content = strings.TrimSpace(content) + if content == "" { + return "", fmt.Errorf("content is empty") + } + + const maxChars = 10000 + runes := []rune(content) + if len(runes) > maxChars { + content = string(runes[:maxChars]) + } + + langHint := "中文" + if language == "en" || language == "en-US" { + langHint = "英文" + } else if language == "zh-CN" || language == "zh" { + langHint = "中文" + } else if language != "" { + langHint = "语言代码 " + language + " 对应的语言" + } + + type message struct { + Role string `json:"role"` + Content string `json:"content"` + } + + userContent := fmt.Sprintf("输出语言:%s。\n\n下面是用户提供的 PPT 大纲内容,请只返回列表标题:\n\n%s", langHint, content) + systemContent := titleSystemPrompt + fmt.Sprintf("\n\n# 语言约束\n请用%s撰写标题。", langHint) + + modelName := cfg.OutlineLLMModel + if modelName == "" { + modelName = "gpt-4o-mini" + } + + body := map[string]any{ + "model": modelName, + "messages": []message{ + {Role: "user", Content: systemContent + "\n\n" + userContent}, + }, + "temperature": 0.5, + } + + var respBody struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + } + + logger.Infof("generate PPT list title with outline LLM, api: %s", cfg.OutlineLLMApiURL) + r, err := c.httpClient.R(). + SetContext(ctx). + SetHeader("Content-Type", "application/json"). + SetHeader("Authorization", "Bearer "+cfg.OutlineLLMApiKey). + SetBody(body). + SetSuccessResult(&respBody). + Post(cfg.OutlineLLMApiURL) + if err != nil { + return "", fmt.Errorf("request outline LLM failed: %v", err) + } + if r.IsErrorState() { + return "", fmt.Errorf("outline LLM returned error status: %s", r.Status) + } + if len(respBody.Choices) == 0 { + return "", fmt.Errorf("outline LLM returned empty choices") + } + + raw := strings.TrimSpace(respBody.Choices[0].Message.Content) + if idx := strings.IndexAny(raw, "\r\n"); idx >= 0 { + raw = strings.TrimSpace(raw[:idx]) + } + raw = strings.Trim(raw, `"'「」`) + return raw, nil +} diff --git a/api/service/ppt/ppt_service.go b/api/service/ppt/ppt_service.go new file mode 100644 index 00000000..c26a0425 --- /dev/null +++ b/api/service/ppt/ppt_service.go @@ -0,0 +1,884 @@ +package ppt + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "context" + "errors" + "fmt" + "geekai/core/types" + "geekai/service" + "geekai/service/oss" + "geekai/store/model" + "geekai/store/vo" + "geekai/utils" + "sort" + "strings" + "sync" + "time" + + "golang.org/x/sync/errgroup" + "gorm.io/gorm" +) + +// ErrInsufficientPower 用户算力不足以完成本次 PPT 任务(由 handler 映射文案) +var ErrInsufficientPower = errors.New("insufficient power for ppt task") + +var ( + // ErrPptTaskNotFound 表示任务不存在 + ErrPptTaskNotFound = errors.New("ppt task not found") + // ErrPptTaskNotDeletable 表示任务状态不允许删除 + ErrPptTaskNotDeletable = errors.New("ppt task not deletable") + // ErrPptTaskBusy 任务正在处理中 + ErrPptTaskBusy = errors.New("ppt task is processing") + // ErrPptTaskNotResumable 无法继续生成(无分镜占位或已完成) + ErrPptTaskNotResumable = errors.New("ppt task cannot be resumed") + // ErrPptSlideNotFound 指定 slide_index 不存在 + ErrPptSlideNotFound = errors.New("ppt slide not found") + // ErrPptSlideNoImage 该页尚无配图 + ErrPptSlideNoImage = errors.New("ppt slide has no image") + // ErrPptInvalidVersionIndex 历史版本下标无效 + ErrPptInvalidVersionIndex = errors.New("invalid slide version index") +) + +// TaskStatus 任务状态 +type TaskStatus string + +const ( + TaskStatusPending TaskStatus = "pending" + TaskStatusProcessing TaskStatus = "processing" + TaskStatusCompleted TaskStatus = "completed" + TaskStatusFailed TaskStatus = "failed" +) + +// SlideData 单页 PPT 数据 +type SlideData struct { + SlideIndex int `json:"slide_index"` + Theme string `json:"theme"` + Title string `json:"title"` + Points []string `json:"points"` + ImagePrompt string `json:"image_prompt"` + ImageURL string `json:"image_url"` + ImageHistory []vo.PPTSlideImageVersion `json:"image_history,omitempty"` +} + +// Task PPT 生成任务(用于业务层与 API 返回,持久化在 DB) +type Task struct { + TaskID string `json:"task_id"` + UserID uint `json:"user_id"` + Status TaskStatus `json:"status"` + Content string `json:"content"` + Prompt string `json:"prompt"` + Language string `json:"language"` + Mode string `json:"mode"` + Pages int `json:"pages"` + Total int `json:"total_slides"` + Completed int `json:"completed_slides"` + Slides []SlideData `json:"slides"` + Title string `json:"title"` + Thumb string `json:"thumb"` + + ErrorMessage string `json:"error_message"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// TaskSummaryMap 返回任务摘要 map(公共字段),供 handler 补充独有字段后返回。 +func (t *Task) TaskSummaryMap() map[string]any { + return map[string]any{ + "task_id": t.TaskID, + "status": t.Status, + "total_slides": t.Total, + "completed_slides": t.Completed, + "created_at": t.CreatedAt.Unix(), + "updated_at": t.UpdatedAt.Unix(), + "title": t.Title, + "thumb": t.Thumb, + } +} + +// Progress 任务进度信息 +type Progress struct { + Total int `json:"total_slides"` + Completed int `json:"completed_slides"` +} + +// PptService PPT 任务与生成流程(持久化、LLM 分镜、生图、转存、算力) +type PptService struct { + db *gorm.DB + userService *service.UserService + uploadManager *oss.UploaderManager + llm *LLMClient + // slidesLock 全局互斥:串行化 slides JSON 的写库,避免并发覆盖(写库极短,可接受排队) + slidesLock sync.Mutex +} + +// NewPptService 创建 PptService +func NewPptService(db *gorm.DB, userService *service.UserService, uploadManager *oss.UploaderManager) *PptService { + return &PptService{ + db: db, + userService: userService, + uploadManager: uploadManager, + llm: NewLLMClient(), + } +} + +// GenerateNotebookContent 将原始文档文本提炼成 PPT 可用的大纲 content。 +func (s *PptService) GenerateNotebookContent(ctx context.Context, cfg types.PPTConfig, rawDocText, designPrompt, language string) (string, error) { + if s.llm == nil { + s.llm = NewLLMClient() + } + return s.llm.GenerateNotebookContent(ctx, cfg, rawDocText, designPrompt, language) +} + +func slideToVO(s SlideData) vo.PPTSlideData { + return vo.PPTSlideData{ + SlideIndex: s.SlideIndex, + Theme: s.Theme, + Title: s.Title, + Points: s.Points, + ImagePrompt: s.ImagePrompt, + ImageURL: s.ImageURL, + ImageHistory: vo.PPTSlideImageVersions(s.ImageHistory), + } +} + +func voToSlide(s vo.PPTSlideData) SlideData { + return SlideData{ + SlideIndex: s.SlideIndex, + Theme: s.Theme, + Title: s.Title, + Points: s.Points, + ImagePrompt: s.ImagePrompt, + ImageURL: s.ImageURL, + ImageHistory: []vo.PPTSlideImageVersion(s.ImageHistory), + } +} + +func voSlidesToBiz(slides vo.PPTSlides) []SlideData { + out := make([]SlideData, len(slides)) + for i, sv := range slides { + out[i] = voToSlide(sv) + } + return out +} + +func voSlidesToBizNormalized(slides vo.PPTSlides) []SlideData { + out := make([]SlideData, len(slides)) + for i, sv := range slides { + sd := voToSlide(sv) + normalizeSlideImageHistory(&sd) + out[i] = sd + } + return out +} + +// normalizeSlideImageHistory 旧数据仅有 image_url 时补全 image_history,便于前端展示历史 +func normalizeSlideImageHistory(s *SlideData) { + if strings.TrimSpace(s.ImageURL) != "" && len(s.ImageHistory) == 0 { + s.ImageHistory = []vo.PPTSlideImageVersion{ + {ImageURL: s.ImageURL, Prompt: strings.TrimSpace(s.ImagePrompt)}, + } + } +} + +// DerivePPTThumbFromSlides 按 slide_index 升序取第一张有图 URL(与 vo.PPTSlides 规则一致) +func DerivePPTThumbFromSlides(slides []SlideData) string { + if len(slides) == 0 { + return "" + } + cp := make([]SlideData, len(slides)) + copy(cp, slides) + sort.Slice(cp, func(i, j int) bool { + return cp[i].SlideIndex < cp[j].SlideIndex + }) + for _, s := range cp { + if strings.TrimSpace(s.ImageURL) != "" { + return s.ImageURL + } + } + return "" +} + +func truncateTitleRunes(s string, max int) string { + if max <= 0 { + return s + } + r := []rune(s) + if len(r) <= max { + return s + } + return string(r[:max]) +} + +func taskToModel(t *Task) *model.PPTJob { + now := time.Now() + job := &model.PPTJob{ + TaskId: t.TaskID, + UserId: t.UserID, + Status: string(t.Status), + ErrMsg: t.ErrorMessage, + Prompt: t.Prompt, + Title: t.Title, + Thumb: t.Thumb, + Content: t.Content, + Params: vo.PPTParams{Language: t.Language, Mode: t.Mode, Pages: t.Pages}, + Slides: nil, + TotalSlides: t.Total, + CompletedSlides: t.Completed, + CreatedAt: now, + UpdatedAt: now, + } + if t.CreatedAt.IsZero() { + job.CreatedAt = now + job.UpdatedAt = now + } else { + job.CreatedAt = t.CreatedAt + job.UpdatedAt = t.UpdatedAt + } + return job +} + +func modelToTask(j *model.PPTJob) *Task { + slides := voSlidesToBizNormalized(j.Slides) + return &Task{ + TaskID: j.TaskId, + UserID: j.UserId, + Status: TaskStatus(j.Status), + Content: j.Content, + Prompt: j.Prompt, + Title: j.Title, + Thumb: j.Thumb, + Language: j.Params.Language, + Mode: j.Params.Mode, + Pages: j.Params.Pages, + Total: j.TotalSlides, + Completed: j.CompletedSlides, + Slides: slides, + ErrorMessage: j.ErrMsg, + CreatedAt: j.CreatedAt, + UpdatedAt: j.UpdatedAt, + } +} + +// BuildPendingTask 校验算力与页数,组装待写入的 Task(未落库) +func (s *PptService) BuildPendingTask(taskID string, userID uint, userPower int, content, prompt, language, mode string, reqPages int) (*Task, types.PPTConfig, error) { + cfg, err := s.loadPPTConfig() + if err != nil { + return nil, cfg, err + } + + // 目标页数:用户指定时取 min(请求页数, 服务端上限);未指定(0)时按服务端上限作为默认生成规模 + effectivePages := cfg.MaxSlidesPerTask + if reqPages > 0 { + effectivePages = reqPages + if effectivePages > cfg.MaxSlidesPerTask { + effectivePages = cfg.MaxSlidesPerTask + } + } + + estimatePower := effectivePages * cfg.PowerCostPerSlide + if estimatePower > 0 && userPower < estimatePower { + return nil, cfg, ErrInsufficientPower + } + + effectiveMode := mode + if effectiveMode != "detailed" && effectiveMode != "slides" { + effectiveMode = "slides" + } + + task := &Task{ + TaskID: taskID, + UserID: userID, + Status: TaskStatusPending, + Content: content, + Prompt: prompt, + Language: language, + Pages: effectivePages, + Mode: effectiveMode, + } + return task, cfg, nil +} + +// CreateTask 创建新任务并写入数据库(调用大模型生成列表标题后落库) +func (s *PptService) CreateTask(ctx context.Context, task *Task, cfg types.PPTConfig) error { + if s.llm == nil { + s.llm = NewLLMClient() + } + title, err := s.llm.GeneratePPTTitle(ctx, cfg, task.Content, task.Language) + if err != nil { + logger.Warnf("GeneratePPTTitle failed task_id=%s: %v", task.TaskID, err) + title = "未命名演示文稿" + } else { + title = strings.TrimSpace(title) + if title == "" { + title = "未命名演示文稿" + } + } + task.Title = truncateTitleRunes(title, 255) + + task.CreatedAt = time.Now() + task.UpdatedAt = task.CreatedAt + task.Status = TaskStatusPending + job := taskToModel(task) + return s.db.Create(job).Error +} + +// GetTask 从数据库获取任务 +func (s *PptService) GetTask(taskID string) (*Task, bool) { + var job model.PPTJob + err := s.db.Where("task_id = ?", taskID).First(&job).Error + if err != nil || job.TaskId == "" { + return nil, false + } + return modelToTask(&job), true +} + +// UpdateStatus 更新任务状态 +func (s *PptService) UpdateStatus(taskID string, status TaskStatus) { + s.db.Model(&model.PPTJob{}).Where("task_id = ?", taskID). + Updates(map[string]interface{}{"status": string(status), "updated_at": time.Now()}) +} + +func countSlidesWithImage(slides []SlideData) int { + n := 0 + for _, sl := range slides { + if strings.TrimSpace(sl.ImageURL) != "" { + n++ + } + } + return n +} + +func slidePlansToOutlines(plans []slidePlan) []SlideData { + out := make([]SlideData, len(plans)) + for i, p := range plans { + out[i] = SlideData{ + SlideIndex: p.SlideIndex, + Theme: p.Theme, + Title: p.Title, + Points: p.Points, + ImagePrompt: p.ImagePrompt, + ImageURL: "", + } + } + sort.Slice(out, func(i, j int) bool { + return out[i].SlideIndex < out[j].SlideIndex + }) + return out +} + +// saveSlidesOutline 分镜一出即落库:每页含 theme/title/points/image_prompt,image_url 为空 +func (s *PptService) saveSlidesOutline(taskID string, total int, slides []SlideData) error { + s.slidesLock.Lock() + defer s.slidesLock.Unlock() + + voSlides := make(vo.PPTSlides, len(slides)) + for i := range slides { + voSlides[i] = slideToVO(slides[i]) + } + completed := countSlidesWithImage(slides) + return s.db.Model(&model.PPTJob{}).Where("task_id = ?", taskID).Updates(map[string]interface{}{ + "slides": voSlides, + "total_slides": total, + "completed_slides": completed, + "updated_at": time.Now(), + }).Error +} + +// ApplySlideImage 按 slide_index 原地写入 image_url,并刷新 completed_slides、thumb +func (s *PptService) ApplySlideImage(taskID string, slide SlideData) error { + s.slidesLock.Lock() + defer s.slidesLock.Unlock() + + var job model.PPTJob + if err := s.db.Where("task_id = ?", taskID).First(&job).Error; err != nil { + return err + } + slides := job.Slides + found := false + for i := range slides { + if slides[i].SlideIndex == slide.SlideIndex { + slides[i].ImageURL = slide.ImageURL + if strings.TrimSpace(slide.ImageURL) != "" && len(slides[i].ImageHistory) == 0 { + slides[i].ImageHistory = vo.PPTSlideImageVersions{ + {ImageURL: slide.ImageURL, Prompt: strings.TrimSpace(slide.ImagePrompt)}, + } + } + found = true + break + } + } + if !found { + return fmt.Errorf("slide index %d not found", slide.SlideIndex) + } + job.Slides = slides + biz := voSlidesToBiz(slides) + return s.refreshJobMeta(&job, biz) +} + +// refreshJobMeta 刷新 job 的 CompletedSlides/Thumb/UpdatedAt 并 Save。 +// 调用前必须已持有 slidesLock。 +func (s *PptService) refreshJobMeta(job *model.PPTJob, biz []SlideData) error { + job.CompletedSlides = countSlidesWithImage(biz) + job.Thumb = DerivePPTThumbFromSlides(biz) + job.UpdatedAt = time.Now() + return s.db.Save(job).Error +} + +func (s *PptService) validateSlideOutline(task *Task) error { + if task.Total <= 0 { + return ErrPptTaskNotResumable + } + if len(task.Slides) < task.Total { + return ErrPptTaskNotResumable + } + seen := make(map[int]bool, len(task.Slides)) + for _, sl := range task.Slides { + seen[sl.SlideIndex] = true + } + for i := 1; i <= task.Total; i++ { + if !seen[i] { + return ErrPptTaskNotResumable + } + } + return nil +} + +func slidesNeedingImages(task *Task) []SlideData { + var need []SlideData + for _, sl := range task.Slides { + if strings.TrimSpace(sl.ImageURL) == "" { + need = append(need, sl) + } + } + sort.Slice(need, func(i, j int) bool { + return need[i].SlideIndex < need[j].SlideIndex + }) + return need +} + +func (s *PptService) userPower(userID uint) (int, error) { + var u model.User + if err := s.db.Where("id = ?", userID).First(&u).Error; err != nil { + return 0, err + } + return u.Power, nil +} + +// runSlideImageJobs 为给定幻灯片列表并发生图(每张成功后 ApplySlideImage) +func (s *PptService) runSlideImageJobs(ctx context.Context, task *Task, cfg types.PPTConfig, generator ImageGenerator, slides []SlideData) error { + if len(slides) == 0 { + return nil + } + if cfg.MaxConcurrentRequests <= 0 { + cfg.MaxConcurrentRequests = 3 + } + group, ctx := errgroup.WithContext(ctx) + group.SetLimit(cfg.MaxConcurrentRequests) + for _, item := range slides { + slide := item + group.Go(func() error { + imgURL, err := generator.Generate(ctx, slide.ImagePrompt) + if err != nil { + return err + } + storedURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(imgURL, ".png", false) + if err != nil { + return fmt.Errorf("转存图片失败:%w", err) + } + full := slide + full.ImageURL = storedURL + if err := s.ApplySlideImage(task.TaskID, full); err != nil { + return err + } + if cfg.PowerCostPerSlide > 0 { + err = s.userService.DecreasePower(task.UserID, cfg.PowerCostPerSlide, model.PowerLog{ + Type: types.PowerConsume, + Model: generator.Provider(), + Remark: fmt.Sprintf("PPT 任务 %s 第 %d 页图片生成", task.TaskID, slide.SlideIndex), + }) + if err != nil { + return fmt.Errorf("扣减算力失败:%v", err) + } + } + return nil + }) + } + return group.Wait() +} + +// startSlideImageGenerationAsync 在后台为 missing 页并发生图;若 setProcessingBeforeRun 为 true 则先置为 processing(用户主动 resume)。 +func (s *PptService) startSlideImageGenerationAsync(task *Task, missing []SlideData, setProcessingBeforeRun bool) error { + if len(missing) == 0 { + return nil + } + cfg, err := s.loadPPTConfig() + if err != nil { + return err + } + cost := len(missing) * cfg.PowerCostPerSlide + if cost > 0 { + power, err := s.userPower(task.UserID) + if err != nil { + return err + } + if power < cost { + return ErrInsufficientPower + } + } + + generator, err := NewImageGenerator(cfg) + if err != nil { + return fmt.Errorf("初始化图片生成器失败:%w", err) + } + + if setProcessingBeforeRun { + s.UpdateStatus(task.TaskID, TaskStatusProcessing) + } + t := task + go func() { + bg := context.Background() + if err := s.runSlideImageJobs(bg, t, cfg, generator, missing); err != nil { + s.MarkAsFailed(task.TaskID, fmt.Sprintf("图片生成失败:%v", err)) + return + } + s.UpdateStatus(task.TaskID, TaskStatusCompleted) + }() + return nil +} + +// RecoverStaleProcessingTasks 进程启动时扫描 DB 中仍为 processing 且存在缺图页的任务,重新拉起生图协程(用于服务中断后的恢复)。 +func (s *PptService) RecoverStaleProcessingTasks() { + var jobs []model.PPTJob + if err := s.db.Where("status = ?", string(TaskStatusProcessing)).Find(&jobs).Error; err != nil { + logger.Warnf("PPT recover: list processing jobs failed: %v", err) + return + } + for i := range jobs { + task := modelToTask(&jobs[i]) + missing := slidesNeedingImages(task) + if len(missing) == 0 { + s.UpdateStatus(task.TaskID, TaskStatusCompleted) + logger.Infof("PPT recover: task %s was processing but all slides had images, marked completed", task.TaskID) + continue + } + if err := s.validateSlideOutline(task); err != nil { + logger.Warnf("PPT recover: task %s skip (invalid outline): %v", task.TaskID, err) + continue + } + if err := s.startSlideImageGenerationAsync(task, missing, false); err != nil { + if errors.Is(err, ErrInsufficientPower) { + logger.Warnf("PPT recover: task %s skip (insufficient power for %d slides)", task.TaskID, len(missing)) + continue + } + logger.Warnf("PPT recover: task %s failed to restart: %v", task.TaskID, err) + continue + } + logger.Infof("PPT recover: restarted image generation for task %s (%d slides)", task.TaskID, len(missing)) + } +} + +// ResumeTask 继续为缺图页生图(需完整分镜占位;processing 时返回 ErrPptTaskBusy) +func (s *PptService) ResumeTask(ctx context.Context, taskID string, userID uint) error { + task, ok := s.GetTask(taskID) + if !ok { + return ErrPptTaskNotFound + } + if task.UserID != userID { + return ErrPptTaskNotFound + } + if task.Status == TaskStatusProcessing { + return ErrPptTaskBusy + } + if task.Status == TaskStatusCompleted { + return ErrPptTaskNotResumable + } + if err := s.validateSlideOutline(task); err != nil { + return err + } + missing := slidesNeedingImages(task) + if len(missing) == 0 { + s.UpdateStatus(taskID, TaskStatusCompleted) + return nil + } + + return s.startSlideImageGenerationAsync(task, missing, true) +} + +// MarkAsFailed 标记任务失败 +func (s *PptService) MarkAsFailed(taskID string, msg string) { + s.db.Model(&model.PPTJob{}).Where("task_id = ?", taskID). + Updates(map[string]interface{}{"status": string(TaskStatusFailed), "err_msg": msg, "updated_at": time.Now()}) +} + +// DeleteTask 删除任务并删除关联生成图片 +// 仅允许删除 completed / failed 状态的任务,避免并发任务生成过程被打断。 +func (s *PptService) DeleteTask(taskID string, userID uint) error { + var job model.PPTJob + if err := s.db.Where("task_id = ? AND user_id = ?", taskID, userID).First(&job).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) || job.TaskId == "" { + return ErrPptTaskNotFound + } + return err + } + + if job.Status != string(TaskStatusCompleted) && job.Status != string(TaskStatusFailed) { + return ErrPptTaskNotDeletable + } + + // 删除所有幻灯片对应的图片对象。 + uploader := s.uploadManager.GetUploadHandler() + for _, slide := range job.Slides { + if slide.ImageURL == "" { + continue + } + if err := uploader.Delete(slide.ImageURL); err != nil { + // 图片可能已过期/不存在/对象已被清理,此时不应阻断“删除任务记录”的主流程。 + // 这里只记录日志,确保数据库记录被删除后前端认为任务删除成功。 + logger.Warnf("delete ppt image failed (task_id=%s, url=%s): %v", taskID, slide.ImageURL, err) + } + } + + // 最后删除任务记录(slides 会随之从数据库消失)。 + return s.db.Where("task_id = ? AND user_id = ?", taskID, userID).Delete(&model.PPTJob{}).Error +} + +// List 返回所有任务列表(从数据库按创建时间倒序),供调用方按用户/状态过滤与分页 +func (s *PptService) List() []*Task { + var jobs []model.PPTJob + s.db.Order("created_at DESC").Find(&jobs) + tasks := make([]*Task, 0, len(jobs)) + for i := range jobs { + tasks = append(tasks, modelToTask(&jobs[i])) + } + sort.Slice(tasks, func(i, j int) bool { + return tasks[i].CreatedAt.After(tasks[j].CreatedAt) + }) + return tasks +} + +// ListUserTasks 当前用户的任务分页列表(缺 title/thumb 时补全并写库) +func (s *PptService) ListUserTasks(ctx context.Context, userID uint, page, pageSize int) ([]*Task, int) { + all := s.List() + filtered := make([]*Task, 0, len(all)) + for _, t := range all { + if t.UserID == userID { + filtered = append(filtered, t) + } + } + total := len(filtered) + start := (page - 1) * pageSize + if start > total { + start = total + } + end := start + pageSize + if end > total { + end = total + } + slice := filtered[start:end] + cfg, cfgErr := s.loadPPTConfig() + if cfgErr != nil { + logger.Warnf("ListUserTasks loadPPTConfig: %v", cfgErr) + } + if s.llm == nil { + s.llm = NewLLMClient() + } + for _, t := range slice { + s.ensureTaskMeta(ctx, t, cfg) + } + return slice, total +} + +// EnsureTaskMeta 对外暴露标题/缩略图补全逻辑,供管理端列表/详情复用。 +func (s *PptService) EnsureTaskMeta(ctx context.Context, task *Task) { + if task == nil { + return + } + cfg, cfgErr := s.loadPPTConfig() + if cfgErr != nil { + logger.Warnf("EnsureTaskMeta loadPPTConfig: %v", cfgErr) + } + if s.llm == nil { + s.llm = NewLLMClient() + } + s.ensureTaskMeta(ctx, task, cfg) +} + +func (s *PptService) ensureTaskMeta(ctx context.Context, task *Task, cfg types.PPTConfig) { + updates := map[string]interface{}{} + if strings.TrimSpace(task.Title) == "" && strings.TrimSpace(task.Content) != "" { + title := "未命名演示文稿" + if cfg.OutlineLLMApiURL != "" && cfg.OutlineLLMApiKey != "" { + ti, err := s.llm.GeneratePPTTitle(ctx, cfg, task.Content, task.Language) + if err != nil { + logger.Warnf("ensureTaskMeta GeneratePPTTitle task_id=%s: %v", task.TaskID, err) + } else { + ti = strings.TrimSpace(ti) + if ti != "" { + title = truncateTitleRunes(ti, 255) + } + } + } + task.Title = title + updates["title"] = task.Title + } + if task.Thumb == "" && len(task.Slides) > 0 { + thumb := DerivePPTThumbFromSlides(task.Slides) + if thumb != "" { + task.Thumb = thumb + updates["thumb"] = thumb + } + } + if len(updates) > 0 { + updates["updated_at"] = time.Now() + _ = s.db.Model(&model.PPTJob{}).Where("task_id = ?", task.TaskID).Updates(updates).Error + } +} + +// ListAdminJobs 管理后台任务列表(筛选 + 分页) +func (s *PptService) ListAdminJobs(ctx context.Context, page, pageSize, filterUserID int, status string) ([]*Task, int) { + items := s.List() + filtered := make([]*Task, 0, len(items)) + for _, t := range items { + if filterUserID > 0 && int(t.UserID) != filterUserID { + continue + } + if status != "" && string(t.Status) != status { + continue + } + filtered = append(filtered, t) + } + total := len(filtered) + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 20 + } + start := (page - 1) * pageSize + if start > total { + start = total + } + end := start + pageSize + if end > total { + end = total + } + slice := filtered[start:end] + for _, t := range slice { + s.EnsureTaskMeta(ctx, t) + } + return slice, total +} + +// Stats 任务状态统计(管理后台) +func (s *PptService) Stats() (total, completed, processing, failed, pending int64) { + for _, t := range s.List() { + total++ + switch t.Status { + case TaskStatusCompleted: + completed++ + case TaskStatusProcessing: + processing++ + case TaskStatusFailed: + failed++ + case TaskStatusPending: + pending++ + } + } + return +} + +// RunTask 执行 PPT 生成:分镜、并发生图、转存图片、扣算力 +func (s *PptService) RunTask(ctx context.Context, task *Task, cfg types.PPTConfig) { + s.UpdateStatus(task.TaskID, TaskStatusProcessing) + + maxPages := task.Pages + if maxPages <= 0 { + maxPages = cfg.MaxSlidesPerTask + } + plans, err := s.llm.GenerateSlides(ctx, cfg, task.Content, task.Prompt, task.Language, task.Mode, maxPages) + if err != nil { + s.MarkAsFailed(task.TaskID, fmt.Sprintf("生成分镜失败:%v", err)) + return + } + + if len(plans) == 0 { + s.MarkAsFailed(task.TaskID, "分镜结果为空") + return + } + + total := len(plans) + if cfg.MaxSlidesPerTask > 0 && total > cfg.MaxSlidesPerTask { + plans = plans[:cfg.MaxSlidesPerTask] + total = len(plans) + } + + outlines := slidePlansToOutlines(plans) + if err := s.saveSlidesOutline(task.TaskID, total, outlines); err != nil { + s.MarkAsFailed(task.TaskID, fmt.Sprintf("保存分镜占位失败:%v", err)) + return + } + + generator, err := NewImageGenerator(cfg) + if err != nil { + s.MarkAsFailed(task.TaskID, fmt.Sprintf("初始化图片生成器失败:%v", err)) + return + } + + if err := s.runSlideImageJobs(ctx, task, cfg, generator, outlines); err != nil { + s.MarkAsFailed(task.TaskID, fmt.Sprintf("图片生成失败:%v", err)) + return + } + + s.UpdateStatus(task.TaskID, TaskStatusCompleted) +} + +func (s *PptService) loadPPTConfig() (types.PPTConfig, error) { + var cfgModel model.Config + var pptCfg types.PPTConfig + + err := s.db.Where("name", types.ConfigKeyPPT).First(&cfgModel).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + pptCfg.MaxSlidesPerTask = 30 + pptCfg.MaxConcurrentRequests = 3 + pptCfg.QPSLimit = 1 + pptCfg.PowerCostPerSlide = 0 + return pptCfg, nil + } + return pptCfg, err + } + + err = utils.JsonDecode(cfgModel.Value, &pptCfg) + if err != nil { + return pptCfg, err + } + + legacyMax10 := pptCfg.MaxSlidesPerTask == 10 + if pptCfg.MaxSlidesPerTask <= 0 { + pptCfg.MaxSlidesPerTask = 30 + } + if legacyMax10 { + // 与前端 PPT 页数控件 max=30 对齐;历史默认 10 会导致用户选择 12/15 仍被截断为 10 + pptCfg.MaxSlidesPerTask = 30 + } + if pptCfg.MaxConcurrentRequests <= 0 { + pptCfg.MaxConcurrentRequests = 3 + } + if pptCfg.QPSLimit <= 0 { + pptCfg.QPSLimit = 1 + } + + if legacyMax10 { + val := utils.JsonEncode(pptCfg) + _ = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyPPT).Update("value", val) + } + + return pptCfg, nil +} diff --git a/api/service/ppt/reference_image.go b/api/service/ppt/reference_image.go new file mode 100644 index 00000000..66c1e726 --- /dev/null +++ b/api/service/ppt/reference_image.go @@ -0,0 +1,50 @@ +package ppt + +import ( + "encoding/base64" + "fmt" + "geekai/core/types" + "net/http" + "net/url" + "os" + "strings" +) + +// PrepareReferenceInputsForImg2Img 将幻灯片参考图转为第三方 API 可消费的输入:本地存储时读文件并转为 data URI(base64),公网 URL 原样传递。 +func PrepareReferenceInputsForImg2Img(rawURL string, oss types.OSSConfig, app *types.AppConfig) ([]string, error) { + rawURL = strings.TrimSpace(rawURL) + if rawURL == "" { + return nil, fmt.Errorf("empty reference image url") + } + if strings.HasPrefix(rawURL, "data:") { + return []string{rawURL}, nil + } + + if oss.Active == "local" { + if fp, ok := mapLocalUploadFile(rawURL, oss.Local); ok { + b, err := os.ReadFile(fp) + if err != nil { + return nil, fmt.Errorf("read local reference image: %w", err) + } + if _, err := decodeImageBytes(b); err != nil { + return nil, fmt.Errorf("reference is not a valid image: %w", err) + } + mime := http.DetectContentType(b) + if !strings.HasPrefix(mime, "image/") { + mime = "image/png" + } + dataURI := fmt.Sprintf("data:%s;base64,%s", mime, base64.StdEncoding.EncodeToString(b)) + return []string{dataURI}, nil + } + } + + if u, err := url.Parse(rawURL); err == nil && u.Scheme != "" && u.Host != "" { + return []string{rawURL}, nil + } + + abs := resolveAbsoluteImageURL(rawURL, oss.Local, app) + if strings.TrimSpace(abs) == "" { + return nil, fmt.Errorf("cannot resolve reference image url") + } + return []string{abs}, nil +} diff --git a/api/service/ppt/slide_edit.go b/api/service/ppt/slide_edit.go new file mode 100644 index 00000000..4fdd90eb --- /dev/null +++ b/api/service/ppt/slide_edit.go @@ -0,0 +1,182 @@ +package ppt + +import ( + "context" + "fmt" + "geekai/core/types" + "geekai/store/model" + "geekai/store/vo" + "strings" +) + +// EditSlideImage 基于当前激活图做图生图,追加 image_history 并将 image_url 设为新版。 +func (s *PptService) EditSlideImage(ctx context.Context, taskID string, userID uint, slideIndex int, prompt string, oss types.OSSConfig, app *types.AppConfig) ([]SlideData, error) { + prompt = strings.TrimSpace(prompt) + if prompt == "" { + return nil, fmt.Errorf("请输入修改说明") + } + task, ok := s.GetTask(taskID) + if !ok { + return nil, ErrPptTaskNotFound + } + if task.UserID != userID { + return nil, ErrPptTaskNotFound + } + refURL := "" + for _, sl := range task.Slides { + if sl.SlideIndex == slideIndex { + normalizeSlideImageHistory(&sl) + refURL = strings.TrimSpace(sl.ImageURL) + break + } + } + if refURL == "" { + if slideExists(task.Slides, slideIndex) { + return nil, ErrPptSlideNoImage + } + return nil, ErrPptSlideNotFound + } + + cfg, err := s.loadPPTConfig() + if err != nil { + return nil, err + } + power, err := s.userPower(userID) + if err != nil { + return nil, err + } + if cfg.PowerCostPerSlide > 0 && power < cfg.PowerCostPerSlide { + return nil, ErrInsufficientPower + } + + generator, err := NewImageGenerator(cfg) + if err != nil { + return nil, err + } + + refInputs, err := PrepareReferenceInputsForImg2Img(refURL, oss, app) + if err != nil { + return nil, fmt.Errorf("准备参考图失败:%w", err) + } + + imgURL, err := generator.GenerateWithReference(ctx, prompt, refInputs) + if err != nil { + return nil, err + } + + storedURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(imgURL, ".png", false) + if err != nil { + return nil, fmt.Errorf("转存图片失败:%w", err) + } + + if err := s.applySlideImageEdit(taskID, slideIndex, storedURL, prompt); err != nil { + return nil, err + } + + if cfg.PowerCostPerSlide > 0 { + err = s.userService.DecreasePower(userID, cfg.PowerCostPerSlide, model.PowerLog{ + Type: types.PowerConsume, + Model: generator.Provider(), + Remark: fmt.Sprintf("PPT 任务 %s 第 %d 页图生图编辑", taskID, slideIndex), + }) + if err != nil { + return nil, fmt.Errorf("扣减算力失败:%v", err) + } + } + + task2, _ := s.GetTask(taskID) + return task2.Slides, nil +} + +func slideExists(slides []SlideData, slideIndex int) bool { + for _, sl := range slides { + if sl.SlideIndex == slideIndex { + return true + } + } + return false +} + +func (s *PptService) applySlideImageEdit(taskID string, slideIndex int, newURL string, editPrompt string) error { + s.slidesLock.Lock() + defer s.slidesLock.Unlock() + + var job model.PPTJob + if err := s.db.Where("task_id = ?", taskID).First(&job).Error; err != nil { + return err + } + slides := job.Slides + found := false + for i := range slides { + if slides[i].SlideIndex != slideIndex { + continue + } + found = true + sd := voToSlide(slides[i]) + normalizeSlideImageHistory(&sd) + if strings.TrimSpace(sd.ImageURL) == "" { + return ErrPptSlideNoImage + } + sd.ImageHistory = append(sd.ImageHistory, vo.PPTSlideImageVersion{ + ImageURL: newURL, + Prompt: editPrompt, + }) + sd.ImageURL = newURL + slides[i] = slideToVO(sd) + break + } + if !found { + return ErrPptSlideNotFound + } + job.Slides = slides + biz := voSlidesToBiz(slides) + return s.refreshJobMeta(&job, biz) +} + +// SetActiveSlideVersion 将 image_url 切换为 image_history[versionIndex]。 +func (s *PptService) SetActiveSlideVersion(taskID string, userID uint, slideIndex int, versionIndex int) ([]SlideData, error) { + task, ok := s.GetTask(taskID) + if !ok { + return nil, ErrPptTaskNotFound + } + if task.UserID != userID { + return nil, ErrPptTaskNotFound + } + + s.slidesLock.Lock() + defer s.slidesLock.Unlock() + + var job model.PPTJob + if err := s.db.Where("task_id = ?", taskID).First(&job).Error; err != nil { + return nil, err + } + slides := job.Slides + found := false + for i := range slides { + if slides[i].SlideIndex != slideIndex { + continue + } + found = true + sd := voToSlide(slides[i]) + normalizeSlideImageHistory(&sd) + hist := sd.ImageHistory + if versionIndex < 0 || versionIndex >= len(hist) { + return nil, ErrPptInvalidVersionIndex + } + sd.ImageURL = hist[versionIndex].ImageURL + slides[i] = slideToVO(sd) + break + } + if !found { + return nil, ErrPptSlideNotFound + } + + job.Slides = slides + biz := voSlidesToBiz(slides) + if err := s.refreshJobMeta(&job, biz); err != nil { + return nil, err + } + + out := voSlidesToBizNormalized(job.Slides) + return out, nil +} diff --git a/api/service/sd/service.go b/api/service/sd/service.go deleted file mode 100644 index f9e4437c..00000000 --- a/api/service/sd/service.go +++ /dev/null @@ -1,299 +0,0 @@ -package sd - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * Copyright 2023 The Geek-AI Authors. All rights reserved. -// * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. -// * @Author yangjian102621@163.com -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import ( - "fmt" - "geekai/core/types" - logger2 "geekai/logger" - "geekai/service" - "geekai/service/oss" - "geekai/store" - "geekai/store/model" - "geekai/utils" - "time" - - "github.com/go-redis/redis/v8" - - "github.com/imroc/req/v3" - "gorm.io/gorm" -) - -var logger = logger2.GetLogger() - -// SD 绘画服务 - -type Service struct { - httpClient *req.Client - taskQueue *store.RedisQueue - db *gorm.DB - uploadManager *oss.UploaderManager - userService *service.UserService -} - -func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service { - return &Service{ - httpClient: req.C(), - taskQueue: store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli), - db: db, - uploadManager: manager, - userService: userService, - } -} - -func (s *Service) Run() { - // 将数据库中未提交的人物加载到队列 - var jobs []model.SdJob - s.db.Where("progress", 0).Find(&jobs) - for _, v := range jobs { - var task types.SdTask - err := utils.JsonDecode(v.TaskInfo, &task) - if err != nil { - logger.Errorf("decode task info with error: %v", err) - continue - } - task.Id = int(v.Id) - s.PushTask(task) - } - logger.Infof("Starting Stable-Diffusion job consumer") - go func() { - for { - var task types.SdTask - err := s.taskQueue.LPop(&task) - if err != nil { - logger.Errorf("taking task with error: %v", err) - continue - } - - // translate prompt - if utils.HasChinese(task.Params.Prompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.Prompt), task.TranslateModelId) - if err == nil { - task.Params.Prompt = content - } else { - logger.Warnf("error with translate prompt: %v", err) - } - } - - // translate negative prompt - if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt), task.TranslateModelId) - if err == nil { - task.Params.NegPrompt = content - } else { - logger.Warnf("error with translate prompt: %v", err) - } - } - - logger.Infof("handle a new Stable-Diffusion task: %+v", task) - err = s.Txt2Img(task) - if err != nil { - logger.Error("绘画任务执行失败:", err.Error()) - // update the task progress - s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(map[string]interface{}{ - "progress": service.FailTaskProgress, - "err_msg": err.Error(), - }) - continue - } - } - }() -} - -// Txt2ImgReq 文生图请求实体 -type Txt2ImgReq struct { - Prompt string `json:"prompt"` - NegativePrompt string `json:"negative_prompt"` - Seed int64 `json:"seed,omitempty"` - Steps int `json:"steps"` - CfgScale float32 `json:"cfg_scale"` - Width int `json:"width"` - Height int `json:"height"` - SamplerName string `json:"sampler_name"` - Scheduler string `json:"scheduler"` - EnableHr bool `json:"enable_hr,omitempty"` - HrScale int `json:"hr_scale,omitempty"` - HrUpscaler string `json:"hr_upscaler,omitempty"` - HrSecondPassSteps int `json:"hr_second_pass_steps,omitempty"` - DenoisingStrength float32 `json:"denoising_strength,omitempty"` - ForceTaskId string `json:"force_task_id,omitempty"` -} - -// Txt2ImgResp 文生图响应实体 -type Txt2ImgResp struct { - Images []string `json:"images"` - Parameters struct { - } `json:"parameters"` - Info string `json:"info"` -} - -// TaskProgressResp 任务进度响应实体 -type TaskProgressResp struct { - Progress float64 `json:"progress"` - EtaRelative float64 `json:"eta_relative"` -} - -// Txt2Img 文生图 API -func (s *Service) Txt2Img(task types.SdTask) error { - body := Txt2ImgReq{ - Prompt: task.Params.Prompt, - NegativePrompt: task.Params.NegPrompt, - Steps: task.Params.Steps, - CfgScale: task.Params.CfgScale, - Width: task.Params.Width, - Height: task.Params.Height, - SamplerName: task.Params.Sampler, - Scheduler: task.Params.Scheduler, - ForceTaskId: task.Params.TaskId, - } - if task.Params.Seed > 0 { - body.Seed = task.Params.Seed - } - if task.Params.HdFix { - body.EnableHr = true - body.HrScale = task.Params.HdScale - body.HrUpscaler = task.Params.HdScaleAlg - body.HrSecondPassSteps = task.Params.HdSteps - body.DenoisingStrength = task.Params.HdRedrawRate - } - var res Txt2ImgResp - var errChan = make(chan error) - - var apiKey model.ApiKey - err := s.db.Where("type", "sd").Where("enabled", true).Order("last_used_at ASC").First(&apiKey).Error - if err != nil { - return fmt.Errorf("no available Stable-Diffusion api key: %v", err) - } - - apiURL := fmt.Sprintf("%s/sdapi/v1/txt2img", apiKey.ApiURL) - logger.Infof("send image request to %s", apiURL) - // send a request to sd api endpoint - go func() { - response, err := s.httpClient.R(). - SetHeader("Authorization", apiKey.Value). - SetBody(body). - SetSuccessResult(&res). - Post(apiURL) - if err != nil { - errChan <- err - return - } - if response.IsErrorState() { - errChan <- fmt.Errorf("error http code status: %v", response.Status) - return - } - - // update the last used time - apiKey.LastUsedAt = time.Now().Unix() - s.db.Updates(&apiKey) - - // 保存 Base64 图片 - imgURL, err := s.uploadManager.GetUploadHandler().PutBase64(res.Images[0]) - if err != nil { - errChan <- fmt.Errorf("error with upload image: %v", err) - return - } - // 获取绘画真实的 seed - var info map[string]interface{} - err = utils.JsonDecode(res.Info, &info) - if err != nil { - errChan <- fmt.Errorf("error with decode task response: %v", err) - return - } - task.Params.Seed = int64(utils.IntValue(utils.InterfaceToString(info["seed"]), -1)) - s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(model.SdJob{ImgURL: imgURL, Params: utils.JsonEncode(task.Params), Prompt: task.Params.Prompt}) - errChan <- nil - }() - - // waiting for task finish - for { - select { - case err := <-errChan: - if err != nil { - return err - } - - // task finished - s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100) - return nil - default: - resp, err := s.checkTaskProgress(apiKey) - // 更新任务进度 - if err == nil && resp.Progress > 0 { - s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", int(resp.Progress*100)) - } - time.Sleep(time.Second) - } - } - -} - -// 执行任务 -func (s *Service) checkTaskProgress(apiKey model.ApiKey) (*TaskProgressResp, error) { - apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", apiKey.ApiURL) - var res TaskProgressResp - response, err := s.httpClient.R(). - SetHeader("Authorization", apiKey.Value). - SetSuccessResult(&res). - Get(apiURL) - if err != nil { - return nil, err - } - if response.IsErrorState() { - return nil, fmt.Errorf("error http code status: %v", response.Status) - } - - return &res, nil -} - -func (s *Service) PushTask(task types.SdTask) { - logger.Debugf("add a new MidJourney task to the task list: %+v", task) - if err := s.taskQueue.RPush(task); err != nil { - logger.Errorf("push sd task to queue failed: %v", err) - } -} - -// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务 -func (s *Service) CheckTaskStatus() { - go func() { - logger.Info("Running Stable-Diffusion task status checking ...") - for { - var jobs []model.SdJob - res := s.db.Where("progress < ?", 100).Find(&jobs) - if res.Error != nil { - time.Sleep(5 * time.Second) - continue - } - - for _, job := range jobs { - // 5 分钟还没完成的任务标记为失败 - if time.Since(job.CreatedAt) > time.Minute*5 { - job.Progress = service.FailTaskProgress - job.ErrMsg = "任务超时" - s.db.Updates(&job) - } - } - - // 找出失败的任务,并恢复其扣减算力 - s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) - for _, job := range jobs { - err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ - Type: types.PowerRefund, - Model: "stable-diffusion", - Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d, Err: %s", job.Id, job.ErrMsg), - }) - if err != nil { - continue - } - // 更新任务状态 - s.db.Model(&job).UpdateColumn("power", 0) - } - time.Sleep(time.Second * 5) - } - }() -} diff --git a/api/service/sms/bao.go b/api/service/sms/bao.go index a220180d..ab8c3edf 100644 --- a/api/service/sms/bao.go +++ b/api/service/sms/bao.go @@ -8,14 +8,14 @@ package sms // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "context" "fmt" "geekai/core/types" "geekai/utils" - "io" - "net/http" "net/url" "strconv" "strings" + "time" ) type BaoSmsService struct { @@ -58,15 +58,9 @@ func (s *BaoSmsService) SendVerifyCode(mobile string, code int) error { params.Set("c", content) apiURL := fmt.Sprintf("https://%s/sms?%s", s.domain, params.Encode()) - response, err := http.Get(apiURL) + body, status, err := utils.FetchURLBytes(context.Background(), apiURL, "", 30*time.Second, 2, 2<<20) if err != nil { - return err - } - defer response.Body.Close() - - body, err := io.ReadAll(response.Body) - if err != nil { - return err + return fmt.Errorf("smsbao request failed: status=%d: %w", status, err) } result := string(body) logger.Debugf("send SmsBao result: %v", errMsg[result]) diff --git a/api/service/sms/service.go b/api/service/sms/service.go index 82d0a914..215ab944 100644 --- a/api/service/sms/service.go +++ b/api/service/sms/service.go @@ -9,6 +9,7 @@ package sms const Ali = "aliyun" const Bao = "bao" +const Tencent = "tencent" type Service interface { SendVerifyCode(mobile string, code int) error diff --git a/api/service/sms/sms_manager.go b/api/service/sms/sms_manager.go index 32921827..c3258e39 100644 --- a/api/service/sms/sms_manager.go +++ b/api/service/sms/sms_manager.go @@ -9,23 +9,25 @@ package sms import ( "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" ) type SmsManager struct { - aliyun *AliYunSmsService - bao *BaoSmsService - active string + aliyun *AliYunSmsService + bao *BaoSmsService + tencent *TencentSmsService + active string } -var logger = logger2.GetLogger() +var logger = log.GetLogger() -func NewSmsManager(sysConfig *types.SystemConfig, aliyun *AliYunSmsService, bao *BaoSmsService) (*SmsManager, error) { +func NewSmsManager(sysConfig *types.SystemConfig, aliyun *AliYunSmsService, bao *BaoSmsService, tencent *TencentSmsService) (*SmsManager, error) { return &SmsManager{ - active: sysConfig.SMS.Active, - aliyun: aliyun, - bao: bao, + active: sysConfig.SMS.Active, + aliyun: aliyun, + bao: bao, + tencent: tencent, }, nil } @@ -35,6 +37,8 @@ func (m *SmsManager) GetService() Service { return m.aliyun case Bao: return m.bao + case Tencent: + return m.tencent } return nil } @@ -49,6 +53,8 @@ func (m *SmsManager) UpdateConfig(config types.SMSConfig) { m.aliyun.UpdateConfig(config.Ali) case Bao: m.bao.UpdateConfig(config.Bao) + case Tencent: + m.tencent.UpdateConfig(config.Tencent) } m.active = config.Active } diff --git a/api/service/sms/tencent.go b/api/service/sms/tencent.go new file mode 100644 index 00000000..6fbc154d --- /dev/null +++ b/api/service/sms/tencent.go @@ -0,0 +1,127 @@ +package sms + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "fmt" + "geekai/core/types" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" +) + +type TencentSmsService struct { + config types.SmsConfigTencent + client *sms.Client + region string +} + +func NewTencentSmsService(sysConfig *types.SystemConfig) (*TencentSmsService, error) { + config := sysConfig.SMS.Tencent + region := config.Region + if region == "" { + region = "ap-guangzhou" // 默认使用广州地区 + } + + s := TencentSmsService{ + config: config, + region: region, + } + if sysConfig.SMS.Active == Tencent { + err := s.UpdateConfig(config) + if err != nil { + logger.Errorf("腾讯云短信初始化失败: %v", err) + } + } + return &s, nil +} + +func (s *TencentSmsService) UpdateConfig(config types.SmsConfigTencent) error { + if config.SecretId == "" || config.SecretKey == "" { + // 配置不完整时不初始化客户端 + s.config = config + if config.Region != "" { + s.region = config.Region + } else { + s.region = "ap-guangzhou" + } + return nil + } + + region := config.Region + if region == "" { + region = "ap-guangzhou" + } + + // 创建凭证 + credential := common.NewCredential( + config.SecretId, + config.SecretKey, + ) + + // 创建客户端配置 + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "sms.tencentcloudapi.com" + + // 创建客户端 + client, err := sms.NewClient(credential, region, cpf) + if err != nil { + return fmt.Errorf("failed to create client: %v", err) + } + + s.client = client + s.config = config + s.region = region + return nil +} + +// SendVerifyCode 发送验证码短信 +// 注意:腾讯云后台配置的短信模板内容应与配置中的 code_template 一致 +// 模板只需要1个参数:{1} 表示验证码,例如:{1}为您的验证码,请于5分钟内填写,如非本人操作,请忽略本短信。 +func (s *TencentSmsService) SendVerifyCode(mobile string, code int) error { + if s.client == nil { + return fmt.Errorf("腾讯云短信服务未初始化") + } + + // 创建发送短信请求 + request := sms.NewSendSmsRequest() + request.SmsSdkAppId = common.StringPtr(s.config.SmsSdkAppId) + request.SignName = common.StringPtr(s.config.Sign) + request.TemplateId = common.StringPtr(s.config.CodeTempId) + request.PhoneNumberSet = common.StringPtrs([]string{mobile}) + request.TemplateParamSet = common.StringPtrs([]string{fmt.Sprintf("%d", code), "5"}) + + // 发送短信 + response, err := s.client.SendSms(request) + if err != nil { + return fmt.Errorf("failed to send SMS: %v", err) + } + + // 检查响应 + if response.Response == nil { + return fmt.Errorf("failed to send SMS: response is nil") + } + + if len(response.Response.SendStatusSet) == 0 { + return fmt.Errorf("failed to send SMS: no send status") + } + + sendStatus := response.Response.SendStatusSet[0] + if sendStatus.Code == nil || *sendStatus.Code != "Ok" { + message := "unknown error" + if sendStatus.Message != nil { + message = *sendStatus.Message + } + return fmt.Errorf("failed to send SMS: %s", message) + } + + return nil +} + +var _ Service = &TencentSmsService{} diff --git a/api/service/smtp_sms_service.go b/api/service/smtp_sms_service.go index cb057d92..6d430482 100644 --- a/api/service/smtp_sms_service.go +++ b/api/service/smtp_sms_service.go @@ -120,7 +120,7 @@ func (s *SmtpService) sendTLS(auth smtp.Auth, to string, subject string, body st } _, _ = fmt.Fprintln(wc) // 将邮件内容写入 - _, err = fmt.Fprintf(wc, body) + _, err = fmt.Fprint(wc, body) if err != nil { return fmt.Errorf("error sending email: %v", err) } diff --git a/api/service/sora/sora_service.go b/api/service/sora/sora_service.go index 2d5691ca..3060eab0 100644 --- a/api/service/sora/sora_service.go +++ b/api/service/sora/sora_service.go @@ -1,21 +1,20 @@ package sora import ( + "context" "encoding/json" "errors" "geekai/service/oss" "geekai/store/vo" "geekai/utils" - "io" - "net/http" "path/filepath" "regexp" "time" - logger2 "geekai/logger" + "geekai/log" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() type SoraService struct { uploadManager *oss.UploaderManager @@ -34,15 +33,10 @@ func (s *SoraService) DownloadVideoURL(text string) (*vo.File, error) { return nil, err } - // 获取 JSON 数据 - resp, err := http.Get(videoDataURL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) + // 用统一的超时/重试策略,避免“偶发 HTTPS 握手超时”直接导致失败 + body, _, err := utils.FetchURLBytes(context.Background(), videoDataURL, "", 30*time.Second, 2, 2<<20) if err != nil { + logger.Errorf("failed to get video data: %v", err) return nil, err } @@ -50,13 +44,14 @@ func (s *SoraService) DownloadVideoURL(text string) (*vo.File, error) { var videoData map[string]any err = json.Unmarshal(body, &videoData) if err != nil { + logger.Errorf("failed to unmarshal video data: %v", err) return nil, err } if v, ok := videoData["url"].(string); ok && v != "" { logger.Infof("try to download video: %s", v) videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v, ".mp4", true) - if err != nil { + if err != nil { // 如果上传失败,则返回原始错误 return nil, err } diff --git a/api/service/suno/service.go b/api/service/suno/service.go index 4df37870..e53868b1 100644 --- a/api/service/suno/service.go +++ b/api/service/suno/service.go @@ -12,11 +12,12 @@ import ( "errors" "fmt" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "geekai/service" "geekai/service/oss" "geekai/store" "geekai/store/model" + "geekai/store/vo" "geekai/utils" "io" "time" @@ -27,7 +28,7 @@ import ( "gorm.io/gorm" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() type Service struct { httpClient *req.Client @@ -61,13 +62,24 @@ func (s *Service) Run() { var jobs []model.SunoJob s.db.Where("task_id", "").Where("progress", 0).Find(&jobs) for _, v := range jobs { - var task types.SunoTask - err := utils.JsonDecode(v.TaskInfo, &task) - if err != nil { - logger.Errorf("decode task info with error: %v", err) - continue + // 从 Params 中提取字段构建 task + task := types.SunoTask{ + Id: v.Id, + UserId: int(v.UserId), + Channel: v.Channel, + Type: v.Type, + Title: v.Title, + RefTaskId: v.RefTaskId, + RefSongId: v.RefSongId, + Prompt: v.Params.Prompt, + Lyrics: v.Params.Lyrics, + Tags: v.Params.Tags, + Model: v.Params.Model, + Instrumental: v.Params.Instrumental, + ExtendSecs: v.Params.ExtendSecs, + SongId: v.SongId, + AudioURL: v.AudioURL, } - task.Id = v.Id s.PushTask(task) } logger.Info("Starting Suno job consumer...") @@ -335,15 +347,22 @@ func (s *Service) SyncTaskProgress() { job.SongId = v.Id job.Duration = int(v.Metadata.Duration) job.Prompt = v.Metadata.Prompt + + // 设置 Params + tags := v.Metadata.Tags // 修复 tags 字段过长导致插入数据库失败 - if len(v.Metadata.Tags) > 255 { - job.Tags = v.Metadata.Tags[:255] - } else { - job.Tags = v.Metadata.Tags + if len(tags) > 255 { + tags = tags[:255] + } + job.Params = vo.SunoParam{ + Prompt: v.Metadata.Prompt, + Tags: tags, + Model: v.ModelName, + Instrumental: job.Params.Instrumental, // 保持原任务参数 + ExtendSecs: job.Params.ExtendSecs, // 保持原任务参数 } - job.ModelName = v.ModelName - job.RawData = utils.JsonEncode(v) + job.Output = utils.JsonEncode(v) job.CoverURL = v.ImageLargeUrl job.AudioURL = v.AudioUrl @@ -372,11 +391,12 @@ func (s *Service) SyncTaskProgress() { } // 找出失败的任务,并恢复其扣减算力 - s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) + s.db.Select("id", "user_id", "power", "task_id", "err_msg", "params"). + Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) for _, job := range jobs { err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ Type: types.PowerRefund, - Model: job.ModelName, + Model: job.Params.Model, Remark: fmt.Sprintf("Suno 任务失败,退回算力。任务ID:%s,Err:%s", job.TaskId, job.ErrMsg), }) if err != nil { diff --git a/api/service/types.go b/api/service/types.go index 25863471..29c5076e 100644 --- a/api/service/types.go +++ b/api/service/types.go @@ -1,6 +1,6 @@ package service -import logger2 "geekai/logger" +import "geekai/log" const FailTaskProgress = 101 const ( @@ -17,7 +17,7 @@ type NotifyMessage struct { Type string `json:"type"` } -var logger = logger2.GetLogger() +var logger = log.GetLogger() const TranslatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]" diff --git a/api/service/video/adapters/adapter.go b/api/service/video/adapters/adapter.go new file mode 100644 index 00000000..1c712040 --- /dev/null +++ b/api/service/video/adapters/adapter.go @@ -0,0 +1,32 @@ +package adapters + +import "geekai/log" + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +var logger = log.GetLogger() + +// CreateTaskResponse 创建任务响应 +type CreateTaskResponse struct { + TaskId string `json:"task_id"` // 任务ID + Channel string `json:"channel"` // 渠道标识 + Prompt string `json:"prompt"` // 优化后的提示词(如果有) + State string `json:"state"` // 任务状态 + CreatedAt string `json:"created_at"` // 创建时间 +} + +// QueryTaskResponse 查询任务响应 +type QueryTaskResponse struct { + TaskId string `json:"task_id"` // 任务ID + Status string `json:"status"` // 任务状态 + Progress int `json:"progress"` // 进度(0-100) + VideoURL string `json:"video_url"` // 视频URL + Prompt string `json:"prompt"` // 提示词 + ErrMsg string `json:"err_msg"` // 错误信息 + StatusMsg string `json:"status_msg"` // 状态消息 + Output string `json:"output"` // 任务输出的原始信息(JSON字符串) +} diff --git a/api/service/video/adapters/doubao_adapter.go b/api/service/video/adapters/doubao_adapter.go new file mode 100644 index 00000000..c968f747 --- /dev/null +++ b/api/service/video/adapters/doubao_adapter.go @@ -0,0 +1,302 @@ +package adapters + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "geekai/core/types" + "io" + "net/http" + "time" + + "gorm.io/gorm" +) + +// DoubaoAdapter 豆包 Seedance 视频生成适配器(通过 Kapon VolcArk 接入) +type DoubaoAdapter struct { + db *gorm.DB +} + +// NewDoubaoAdapter 创建 Doubao 适配器 +func NewDoubaoAdapter(db *gorm.DB) *DoubaoAdapter { + return &DoubaoAdapter{ + db: db, + } +} + +// GetProvider 获取服务提供商名称 +func (a *DoubaoAdapter) GetProvider() string { + return types.VideoDoubao +} + +// doubaoContentItem 请求体中的 content 子项 +type doubaoContentItem struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL map[string]string `json:"image_url,omitempty"` + Extra map[string]interface{} `json:"extra,omitempty"` // 预留扩展 +} + +// doubaoCreateRequest 创建任务请求体 +type doubaoCreateRequest struct { + Model string `json:"model"` + Content []doubaoContentItem `json:"content"` + Duration int `json:"duration,omitempty"` + Frames int `json:"frames,omitempty"` + Ratio string `json:"ratio,omitempty"` + Resolution string `json:"resolution,omitempty"` + Seed int64 `json:"seed,omitempty"` + // 其他官方支持的字段,按需追加 +} + +// doubaoCreateResponse 创建任务响应 +type doubaoCreateResponse struct { + Id string `json:"id"` + PlatformId string `json:"platform_id"` + // 其余字段目前用不到,先不展开 +} + +// doubaoQueryContent 查询任务 content 字段 +type doubaoQueryContent struct { + VideoURL string `json:"video_url"` + LastFrameURL string `json:"last_frame_url"` + Width int `json:"width"` + Height int `json:"height"` +} + +// doubaoQueryUsage 查询任务 usage 字段 +type doubaoQueryUsage struct { + VideoTokens int `json:"video_tokens"` +} + +// doubaoQueryResponse 查询任务响应 +type doubaoQueryResponse struct { + Id string `json:"id"` + PlatformId string `json:"platform_id"` + Model string `json:"model"` + Status string `json:"status"` + Content doubaoQueryContent `json:"content"` + Duration int `json:"duration"` + Frames int `json:"framespersecond"` + Usage doubaoQueryUsage `json:"usage"` + Error string `json:"error,omitempty"` +} + +// CreateTask 创建豆包 Seedance 视频任务 +func (a *DoubaoAdapter) CreateTask(task types.VideoTask, videoConfig *types.VideoConfig) (CreateTaskResponse, error) { + if videoConfig == nil { + return CreateTaskResponse{}, errors.New("视频配置为空") + } + if videoConfig.ApiURL == "" || videoConfig.ApiKey == "" { + return CreateTaskResponse{}, errors.New("豆包视频未配置 ApiURL 或 ApiKey") + } + + paramsMap, ok := task.Params.(map[string]interface{}) + if !ok { + return CreateTaskResponse{}, errors.New("invalid params type for Doubao video task") + } + + // 模型名称:优先从 params.model 读取,否则默认 doubao-seedance-1-5-pro + modelName := "doubao-seedance-1-5-pro" + if v, ok := paramsMap["model"].(string); ok && v != "" { + modelName = v + } + + // 解析基础参数 + duration := 0 + if v, ok := paramsMap["duration"].(float64); ok { + duration = int(v) + } + if v, ok := paramsMap["duration"].(int); ok { + duration = v + } + + ratio := "" + if v, ok := paramsMap["aspect_ratio"].(string); ok { + ratio = v + } + + resolution := "" + if v, ok := paramsMap["resolution"].(string); ok { + resolution = v + } + + var seed int64 + switch v := paramsMap["seed"].(type) { + case float64: + seed = int64(v) + case int: + seed = int64(v) + case int64: + seed = v + } + + // 构建 content 数组:文本提示词为必填 + content := []doubaoContentItem{ + { + Type: "text", + Text: task.Prompt, + }, + } + + // 如果存在 input_reference(图片 URL),则追加 image_url 项,用于 I2V + if ref, ok := paramsMap["input_reference"].(string); ok && ref != "" { + content = append(content, doubaoContentItem{ + Type: "image_url", + ImageURL: map[string]string{ + "url": ref, + }, + }) + } + + reqBody := doubaoCreateRequest{ + Model: modelName, + Content: content, + Duration: duration, + Ratio: ratio, + Resolution: resolution, + Seed: seed, + } + + payload, err := json.Marshal(reqBody) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("序列化豆包请求失败: %v", err) + } + logger.Debugf("DoubaoCreateRequest: %s", string(payload)) + + url := fmt.Sprintf("%s/seedance/v3/contents/generations/tasks", videoConfig.ApiURL) + req, err := http.NewRequest("POST", url, bytes.NewReader(payload)) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("创建豆包请求失败: %v", err) + } + + req.Header.Set("Authorization", "Bearer "+videoConfig.ApiKey) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 60 * time.Second} + resp, err := client.Do(req) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("调用豆包接口失败: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("读取豆包响应失败: %v", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return CreateTaskResponse{}, fmt.Errorf("豆包接口返回错误状态码: %d, %s", resp.StatusCode, string(body)) + } + + var apiResp doubaoCreateResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return CreateTaskResponse{}, fmt.Errorf("解析豆包创建任务响应失败: %v, body=%s", err, string(body)) + } + + taskId := apiResp.PlatformId + if taskId == "" { + taskId = apiResp.Id + } + if taskId == "" { + return CreateTaskResponse{}, fmt.Errorf("豆包创建任务响应缺少任务 ID, body=%s", string(body)) + } + + return CreateTaskResponse{ + TaskId: taskId, + Channel: videoConfig.ApiURL, + Prompt: task.Prompt, + State: types.VideoStatusPending, + CreatedAt: time.Now().Format(time.RFC3339), + }, nil +} + +// QueryTask 查询豆包 Seedance 视频任务状态 +func (a *DoubaoAdapter) QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) { + if videoConfig == nil { + return QueryTaskResponse{}, errors.New("视频配置为空") + } + if videoConfig.ApiURL == "" || videoConfig.ApiKey == "" { + return QueryTaskResponse{}, errors.New("豆包视频未配置 ApiURL 或 ApiKey") + } + + url := fmt.Sprintf("%s/seedance/v3/contents/generations/tasks/%s", videoConfig.ApiURL, taskId) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("创建查询请求失败: %v", err) + } + + req.Header.Set("Authorization", "Bearer "+videoConfig.ApiKey) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 60 * time.Second} + resp, err := client.Do(req) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("调用豆包查询接口失败: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("读取豆包查询响应失败: %v", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return QueryTaskResponse{}, fmt.Errorf("豆包查询接口返回错误状态码: %d, %s", resp.StatusCode, string(body)) + } + + var apiResp doubaoQueryResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return QueryTaskResponse{}, fmt.Errorf("解析豆包查询任务响应失败: %v, body=%s", err, string(body)) + } + + status := apiResp.Status + progress := 0 + + switch status { + case "queued": + status = types.VideoStatusPending + progress = 10 + case "running": + status = types.VideoStatusInProgress + progress = 60 + case "succeeded": + status = types.VideoStatusSuccess + progress = 100 + case "failed", "cancelled": + status = types.VideoStatusFailed + default: + // 保持原样或视为 pending + status = types.VideoStatusPending + } + + errMsg := apiResp.Error + if errMsg == "" && status == types.VideoStatusFailed { + errMsg = "doubao task failed" + } + + result := QueryTaskResponse{ + TaskId: apiResp.PlatformId, + Status: status, + Progress: progress, + VideoURL: apiResp.Content.VideoURL, + Prompt: "", + ErrMsg: errMsg, + StatusMsg: status, + Output: string(body), + } + + if result.TaskId == "" { + result.TaskId = apiResp.Id + } + + return result, nil +} diff --git a/api/service/video/adapters/keling_adapter.go b/api/service/video/adapters/keling_adapter.go new file mode 100644 index 00000000..2fb3dbc0 --- /dev/null +++ b/api/service/video/adapters/keling_adapter.go @@ -0,0 +1,267 @@ +package adapters + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "geekai/core/types" + "io" + "net/http" + "time" + + "gorm.io/gorm" +) + +// KelingAdapter 可灵视频生成适配器 +type KelingAdapter struct { + db *gorm.DB +} + +// NewKelingAdapter 创建可灵适配器 +func NewKelingAdapter(db *gorm.DB) *KelingAdapter { + return &KelingAdapter{ + db: db, + } +} + +// GetProvider 获取服务提供商名称 +func (a *KelingAdapter) GetProvider() string { + return "keling" +} + +// KelingCreateRequest 可灵创建任务请求 +type KelingCreateRequest struct { + ModelName string `json:"model_name"` + Prompt string `json:"prompt"` + NegativePrompt string `json:"negative_prompt,omitempty"` + CfgScale float64 `json:"cfg_scale,omitempty"` + Mode string `json:"mode,omitempty"` + AspectRatio string `json:"aspect_ratio,omitempty"` + Duration string `json:"duration,omitempty"` + Sound bool `json:"sound,omitempty"` + Image string `json:"image,omitempty"` + ImageTail string `json:"image_tail,omitempty"` +} + +// KelingCreateResponse 可灵创建任务响应 +type KelingCreateResponse struct { + Code int `json:"code"` + Message string `json:"message"` + RequestID string `json:"request_id"` + Data struct { + TaskID string `json:"task_id"` + TaskStatus string `json:"task_status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } `json:"data"` +} + +// KelingQueryResponse 可灵查询任务响应 +type KelingQueryResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + TaskID string `json:"task_id"` + TaskStatus string `json:"task_status"` + TaskStatusMsg string `json:"task_status_msg"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + TaskResult struct { + Images []struct { + Index int `json:"index"` + URL string `json:"url"` + } `json:"images,omitempty"` + Videos []struct { + ID string `json:"id"` + URL string `json:"url"` + Duration string `json:"duration"` + } `json:"videos,omitempty"` + } `json:"task_result"` + } `json:"data"` +} + +// CreateTask 创建视频生成任务 +func (a *KelingAdapter) CreateTask(task types.VideoTask, videoConfig *types.VideoConfig) (CreateTaskResponse, error) { + // 解析任务参数 + paramsMap, ok := task.Params.(map[string]interface{}) + if !ok { + return CreateTaskResponse{}, errors.New("invalid params type for KeLing video task") + } + + // 构建请求参数 + payload := KelingCreateRequest{ + Prompt: task.Prompt, + } + + // 从 params 中提取参数 + if modelName, ok := paramsMap["model_name"].(string); ok { + payload.ModelName = modelName + } + if prompt, ok := paramsMap["prompt"].(string); ok { + payload.Prompt = prompt + } + if negativePrompt, ok := paramsMap["negative_prompt"].(string); ok { + payload.NegativePrompt = negativePrompt + } + if cfgScale, ok := paramsMap["cfg_scale"].(float64); ok { + payload.CfgScale = cfgScale + } + if mode, ok := paramsMap["mode"].(string); ok { + payload.Mode = mode + } + if aspectRatio, ok := paramsMap["aspect_ratio"].(string); ok { + payload.AspectRatio = aspectRatio + } + if duration, ok := paramsMap["duration"].(string); ok { + payload.Duration = duration + } + + if sound, ok := paramsMap["sound"].(bool); ok { + payload.Sound = sound + } + + // 处理图生视频 + taskType, ok := paramsMap["task_type"].(string) + if ok && taskType == "image2video" { + if image, ok := paramsMap["image"].(string); ok { + payload.Image = image + } + if imageTail, ok := paramsMap["image_tail"].(string); ok { + payload.ImageTail = imageTail + } + } + + jsonPayload, err := json.Marshal(payload) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("failed to marshal payload: %v", err) + } + logger.Debugf("KelingCreateRequest: %+v", string(jsonPayload)) + + // 发送请求 + url := fmt.Sprintf("%s/kling/v1/videos/%s", videoConfig.ApiURL, taskType) + req, err := http.NewRequest("POST", url, bytes.NewReader(jsonPayload)) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("failed to create request: %v", err) + } + + req.Header.Set("Authorization", "Bearer "+videoConfig.ApiKey) + req.Header.Set("Content-Type", "application/json") + + // 发送请求 + client := &http.Client{Timeout: time.Duration(30) * time.Second} + resp, err := client.Do(req) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + // 处理响应 + body, err := io.ReadAll(resp.Body) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("failed to read response: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return CreateTaskResponse{}, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + var apiResponse KelingCreateResponse + if err := json.Unmarshal(body, &apiResponse); err != nil { + return CreateTaskResponse{}, fmt.Errorf("failed to parse response: %v", err) + } + + if apiResponse.Code != 0 { + return CreateTaskResponse{}, fmt.Errorf("API error: %s", apiResponse.Message) + } + + return CreateTaskResponse{ + TaskId: apiResponse.Data.TaskID, + Channel: videoConfig.ApiURL, + Prompt: task.Prompt, + State: types.VideoStatusPending, + CreatedAt: time.Now().Format(time.RFC3339), + }, nil +} + +// QueryTask 查询任务状态 +func (a *KelingAdapter) QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) { + // 从 taskId 中提取 action(可灵的 taskId 格式可能包含 action 信息) + // 这里需要从任务信息中获取 task_type,暂时使用 text2video 作为默认值 + action := "text2video" + + // 尝试从 channel 或其他地方获取 action,这里简化处理 + // 实际应该从任务信息中获取 + + url := fmt.Sprintf("%s/kling/v1/videos/%s/%s", videoConfig.ApiURL, action, taskId) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+videoConfig.ApiKey) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: time.Duration(30) * time.Second} + res, err := client.Do(req) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("failed to execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + body, _ := io.ReadAll(res.Body) + return QueryTaskResponse{}, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, string(body)) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("failed to read response body: %w", err) + } + + var response KelingQueryResponse + if err := json.Unmarshal(body, &response); err != nil { + return QueryTaskResponse{}, fmt.Errorf("failed to unmarshal response: %w", err) + } + + if response.Code != 0 { + return QueryTaskResponse{}, fmt.Errorf("API error: %s", response.Message) + } + + // 转换状态 + state := response.Data.TaskStatus + status := state + switch state { + case "in_progress", "processing": + status = types.VideoStatusInProgress + case "completed", "succeed", "success": + status = types.VideoStatusSuccess + case "failed": + status = types.VideoStatusFailed + default: + status = types.VideoStatusPending + } + + // 构建响应 + result := QueryTaskResponse{ + TaskId: response.Data.TaskID, + Status: status, + ErrMsg: response.Data.TaskStatusMsg, + StatusMsg: response.Data.TaskStatusMsg, + Output: string(body), + } + + // 提取视频URL + if len(response.Data.TaskResult.Videos) > 0 { + result.VideoURL = response.Data.TaskResult.Videos[0].URL + } + + return result, nil +} diff --git a/api/service/video/adapters/luma_adapter.go b/api/service/video/adapters/luma_adapter.go new file mode 100644 index 00000000..ac3f3af2 --- /dev/null +++ b/api/service/video/adapters/luma_adapter.go @@ -0,0 +1,197 @@ +package adapters + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "encoding/json" + "errors" + "fmt" + "geekai/core/types" + "io" + "time" + + "github.com/imroc/req/v3" + "gorm.io/gorm" +) + +// LumaAdapter Luma 视频生成适配器 +type LumaAdapter struct { + db *gorm.DB + httpClient *req.Client +} + +// NewLumaAdapter 创建 Luma 适配器 +func NewLumaAdapter(db *gorm.DB) *LumaAdapter { + return &LumaAdapter{ + db: db, + httpClient: req.C().SetTimeout(time.Minute * 3), + } +} + +// GetProvider 获取服务提供商名称 +func (a *LumaAdapter) GetProvider() string { + return "luma" +} + +// LumaCreateRequest Luma 创建任务请求 +type LumaCreateRequest struct { + ModelName string `json:"model_name"` + UserPrompt string `json:"user_prompt"` + ExpandPrompt bool `json:"expand_prompt,omitempty"` + Loop bool `json:"loop,omitempty"` + ImageURL string `json:"image_url,omitempty"` // 图生视频 + ImageEndURL string `json:"image_end_url,omitempty"` // 图生视频 + Duration string `json:"duration,omitempty"` // 视频时长 + Resolution string `json:"resolution,omitempty"` // 视频分辨率 +} + +// LumaCreateResponse Luma 创建任务响应 +type LumaCreateResponse struct { + Id string `json:"id"` + Prompt string `json:"prompt"` + State string `json:"state"` + CreatedAt string `json:"created_at"` + Channel string `json:"channel,omitempty"` +} + +// LumaQueryResponse Luma 查询任务响应 +type LumaQueryResponse struct { + Id string `json:"id"` + State string `json:"state"` + Video struct { + URL string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` + Thumbnail string `json:"thumbnail"` + DownloadURL string `json:"download_url"` + } `json:"video"` + Prompt string `json:"prompt"` + Thumbnail struct { + URL string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` + } `json:"thumbnail"` +} + +// CreateTask 创建视频生成任务 +func (a *LumaAdapter) CreateTask(task types.VideoTask, videoConfig *types.VideoConfig) (CreateTaskResponse, error) { + // 解析任务参数 + paramsMap, ok := task.Params.(map[string]any) + if !ok { + return CreateTaskResponse{}, errors.New("invalid params type for Luma video task") + } + + // 构建请求参数 + reqBody := LumaCreateRequest{ + UserPrompt: task.Prompt, + } + + // 从 params 中提取参数 + if expandPrompt, ok := paramsMap["expand_prompt"].(bool); ok { + reqBody.ExpandPrompt = expandPrompt + } + if model, ok := paramsMap["model"].(string); ok { + reqBody.ModelName = model + } + if loop, ok := paramsMap["loop"].(bool); ok { + reqBody.Loop = loop + } + if imageURL, ok := paramsMap["image_url"].(string); ok { + reqBody.ImageURL = imageURL + } + if imageEndURL, ok := paramsMap["image_end_url"].(string); ok { + reqBody.ImageEndURL = imageEndURL + } + if duration, ok := paramsMap["duration"].(string); ok { + reqBody.Duration = duration + } + if resolution, ok := paramsMap["resolution"].(string); ok { + reqBody.Resolution = resolution + } + // 发送请求 + apiURL := fmt.Sprintf("%s/luma/generations", videoConfig.ApiURL) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + SetBody(reqBody). + Post(apiURL) + + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%v", err) + } + + if r.StatusCode != 200 && r.StatusCode != 201 { + body, _ := io.ReadAll(r.Body) + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res LumaCreateResponse + err = json.Unmarshal(body, &res) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + return CreateTaskResponse{ + TaskId: res.Id, + Channel: videoConfig.ApiURL, + Prompt: res.Prompt, + State: types.VideoStatusPending, + CreatedAt: res.CreatedAt, + }, nil +} + +// QueryTask 查询任务状态 +func (a *LumaAdapter) QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) { + apiURL := fmt.Sprintf("%s/luma/generations/%s", videoConfig.ApiURL, taskId) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + Get(apiURL) + + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("请求 API 失败:%v", err) + } + defer r.Body.Close() + + if r.StatusCode != 200 { + body, _ := io.ReadAll(r.Body) + return QueryTaskResponse{}, fmt.Errorf("API 返回失败:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res LumaQueryResponse + err = json.Unmarshal(body, &res) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + switch res.State { + case "completed", "succeed", "success": + res.State = types.VideoStatusSuccess + case "in_progress", "running": + res.State = types.VideoStatusInProgress + case "failed": + res.State = types.VideoStatusFailed + default: + res.State = types.VideoStatusPending + } + + // 构建响应 + response := QueryTaskResponse{ + TaskId: res.Id, + Status: res.State, + VideoURL: res.Video.DownloadURL, + Prompt: res.Prompt, + } + + // 如果有原始数据,转换为 JSON 字符串 + if len(body) > 0 { + response.Output = string(body) + } + + return response, nil +} diff --git a/api/service/video/adapters/minimax_adapter.go b/api/service/video/adapters/minimax_adapter.go new file mode 100644 index 00000000..cef0bb1d --- /dev/null +++ b/api/service/video/adapters/minimax_adapter.go @@ -0,0 +1,227 @@ +package adapters + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "encoding/json" + "fmt" + "geekai/core/types" + "io" + "strings" + "time" + + "github.com/imroc/req/v3" + "gorm.io/gorm" +) + +// MiniMaxAdapter MiniMax 视频生成适配器 +type MiniMaxAdapter struct { + db *gorm.DB + httpClient *req.Client +} + +// NewMiniMaxAdapter 创建 MiniMax 适配器 +func NewMiniMaxAdapter(db *gorm.DB) *MiniMaxAdapter { + return &MiniMaxAdapter{ + db: db, + httpClient: req.C().SetTimeout(time.Minute * 3), + } +} + +// GetProvider 获取服务提供商名称 +func (a *MiniMaxAdapter) GetProvider() string { + return "minimax" +} + +// MiniMaxCreateRequest MiniMax 创建任务请求 +type MiniMaxCreateRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt"` + Duration int `json:"duration,omitempty"` + Resolution string `json:"resolution,omitempty"` + FirstFrameImage string `json:"first_frame_image,omitempty"` + LastFrameImage string `json:"last_frame_image,omitempty"` + PromptOptimizer bool `json:"prompt_optimizer,omitempty"` +} + +// MiniMaxCreateResponse MiniMax 创建任务响应 +type MiniMaxCreateResponse struct { + TaskId string `json:"task_id"` + BaseResp struct { + StatusCode int `json:"status_code"` + StatusMsg string `json:"status_msg"` + } `json:"base_resp"` +} + +// MiniMaxFile MiniMax 文件信息 +type MiniMaxFile struct { + Bytes int `json:"bytes"` + CreatedAt int64 `json:"created_at"` + DownloadURL string `json:"download_url"` + FileId int64 `json:"file_id"` + Filename string `json:"filename"` + Purpose string `json:"purpose"` +} + +// MiniMaxQueryResponse MiniMax 查询任务响应 +type MiniMaxQueryResponse struct { + TaskId string `json:"task_id"` + Status string `json:"status"` + FileId string `json:"file_id,omitempty"` // 顶层 file_id 可能是字符串 + File *MiniMaxFile `json:"file,omitempty"` // file 对象包含详细信息 + VideoWidth int `json:"video_width,omitempty"` + VideoHeight int `json:"video_height,omitempty"` + VideoURL string `json:"video_url,omitempty"` + Prompt string `json:"prompt,omitempty"` + ErrMsg string `json:"err_msg,omitempty"` + StatusMsg string `json:"status_msg,omitempty"` + BaseResp struct { + StatusCode int `json:"status_code"` + StatusMsg string `json:"status_msg"` + } `json:"base_resp"` +} + +// CreateTask 创建视频生成任务 +func (a *MiniMaxAdapter) CreateTask(task types.VideoTask, videoConfig *types.VideoConfig) (CreateTaskResponse, error) { + // 解析任务参数 + paramsMap, ok := task.Params.(map[string]interface{}) + if !ok { + return CreateTaskResponse{}, fmt.Errorf("invalid params type for MiniMax video task") + } + + // 构建请求参数 + reqBody := MiniMaxCreateRequest{ + Prompt: task.Prompt, + } + + // 从 params 中提取参数 + if model, ok := paramsMap["model"].(string); ok { + reqBody.Model = model + } + if duration, ok := paramsMap["duration"].(float64); ok { + reqBody.Duration = int(duration) + } else if duration, ok := paramsMap["duration"].(int); ok { + reqBody.Duration = duration + } + if resolution, ok := paramsMap["resolution"].(string); ok { + reqBody.Resolution = resolution + } + if firstFrameImage, ok := paramsMap["first_frame_image"].(string); ok { + reqBody.FirstFrameImage = firstFrameImage + } + if lastFrameImage, ok := paramsMap["last_frame_image"].(string); ok { + reqBody.LastFrameImage = lastFrameImage + } + if promptOptimizer, ok := paramsMap["prompt_optimizer"].(bool); ok { + reqBody.PromptOptimizer = promptOptimizer + } else { + reqBody.PromptOptimizer = true // 默认值 + } + + // 发送请求 + apiURL := fmt.Sprintf("%s/minimax/v1/video_generation", videoConfig.ApiURL) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + SetHeader("Content-Type", "application/json"). + SetBody(reqBody). + Post(apiURL) + + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%v", err) + } + + if r.StatusCode != 200 && r.StatusCode != 201 { + body, _ := io.ReadAll(r.Body) + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res MiniMaxCreateResponse + err = json.Unmarshal(body, &res) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + if res.BaseResp.StatusCode != 0 { + return CreateTaskResponse{}, fmt.Errorf("API 返回错误:%s", res.BaseResp.StatusMsg) + } + + return CreateTaskResponse{ + TaskId: res.TaskId, + Channel: videoConfig.ApiURL, + Prompt: task.Prompt, + State: types.VideoStatusPending, + CreatedAt: time.Now().Format(time.RFC3339), + }, nil +} + +// QueryTask 查询任务状态 +func (a *MiniMaxAdapter) QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) { + // MiniMax 查询接口 + apiURL := fmt.Sprintf("%s/minimax/v1/query/video_generation?task_id=%s", videoConfig.ApiURL, taskId) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + Get(apiURL) + + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("请求 API 失败:%v", err) + } + defer r.Body.Close() + + if r.StatusCode != 200 { + body, _ := io.ReadAll(r.Body) + return QueryTaskResponse{}, fmt.Errorf("API 返回失败:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res MiniMaxQueryResponse + err = json.Unmarshal(body, &res) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + if res.BaseResp.StatusCode != 0 { + return QueryTaskResponse{}, fmt.Errorf("API 返回错误:%s", res.BaseResp.StatusMsg) + } + + // 转换状态(处理大小写) + state := strings.ToLower(res.Status) + switch state { + case "completed", "succeed", "success": + state = types.VideoStatusSuccess + case "in_progress", "running": + state = types.VideoStatusInProgress + case "failed": + state = types.VideoStatusFailed + default: + state = types.VideoStatusPending + } + + // 获取视频URL,优先从 file.download_url 获取 + videoURL := res.VideoURL + if res.File != nil && res.File.DownloadURL != "" { + videoURL = res.File.DownloadURL + } + + // 构建响应 + response := QueryTaskResponse{ + TaskId: res.TaskId, + Status: state, + VideoURL: videoURL, + Prompt: res.Prompt, + ErrMsg: res.ErrMsg, + StatusMsg: res.StatusMsg, + } + + // 如果有原始数据,转换为 JSON 字符串 + if len(body) > 0 { + response.Output = string(body) + } + + return response, nil +} diff --git a/api/service/video/adapters/sora_adapter.go b/api/service/video/adapters/sora_adapter.go new file mode 100644 index 00000000..2df71c99 --- /dev/null +++ b/api/service/video/adapters/sora_adapter.go @@ -0,0 +1,347 @@ +package adapters + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "geekai/core/types" + "io" + "mime/multipart" + "net/http" + "geekai/utils" + "time" + + "github.com/imroc/req/v3" + "gorm.io/gorm" +) + +// SoraAdapter Sora 视频生成适配器 +type SoraAdapter struct { + db *gorm.DB + httpClient *req.Client +} + +// NewSoraAdapter 创建 Sora 适配器 +func NewSoraAdapter(db *gorm.DB) *SoraAdapter { + return &SoraAdapter{ + db: db, + httpClient: req.C().SetTimeout(time.Minute * 3), + } +} + +// GetProvider 获取服务提供商名称 +func (a *SoraAdapter) GetProvider() string { + return "sora" +} + +// SoraCreateRequest Sora 创建任务请求 +type SoraCreateRequest struct { + Model string `json:"model"` // 模型名称:sora-2, sora-2-pro + Prompt string `json:"prompt"` // 提示词 + Size string `json:"size,omitempty"` // 分辨率:1280x720, 720x1280, 1792x1024, 1024x1792 + InputReference interface{} `json:"input_reference,omitempty"` // 图生视频的参考图片,官方为对象 {"image_url": "..."},也兼容字符串 URL + Seconds string `json:"seconds,omitempty"` // 视频时长(秒),默认4秒 + Watermark bool `json:"watermark,omitempty"` // 是否添加水印 +} + +// SoraCreateResponse Sora 创建任务响应 +type SoraCreateResponse struct { + ID string `json:"id"` // 任务ID + Object string `json:"object"` // 对象类型,固定为 "video" + Model string `json:"model"` // 模型名称 + Status string `json:"status"` // 状态:queued, in_progress, completed, failed + CreatedAt int64 `json:"created_at"` // 创建时间戳 + Seconds string `json:"seconds"` // 视频时长 + Size string `json:"size"` // 分辨率 + Error *SoraError `json:"error,omitempty"` // 错误信息(成功时为null) +} + +// SoraQueryResponse Sora 查询任务响应 +type SoraQueryResponse struct { + ID string `json:"id"` // 任务ID + Object string `json:"object"` // 对象类型,固定为 "video" + Model string `json:"model"` // 模型名称 + Status string `json:"status"` // 状态:queued, in_progress, completed, failed + Progress int `json:"progress"` // 进度(0-100) + CreatedAt int64 `json:"created_at"` // 创建时间戳 + Seconds string `json:"seconds"` // 视频时长 + Size string `json:"size"` // 分辨率 + Error *SoraError `json:"error,omitempty"` // 错误信息(成功时为null) + VideoURL string `json:"video_url"` // 视频URL(成功时生成) +} + +type SoraError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// CreateTask 创建视频生成任务 +func (a *SoraAdapter) CreateTask(task types.VideoTask, videoConfig *types.VideoConfig) (CreateTaskResponse, error) { + // 解析任务参数 + paramsMap, ok := task.Params.(map[string]any) + if !ok { + return CreateTaskResponse{}, fmt.Errorf("invalid params type for Sora video task") + } + + // 是否调用官方 Sora 接口 + isOfficial := false + if v, ok := paramsMap["is_official"].(bool); ok { + isOfficial = v + } + + // 提取通用参数 + model, ok := paramsMap["model"].(string) + if !ok || model == "" { + return CreateTaskResponse{}, fmt.Errorf("model 参数必填") + } + + size, _ := paramsMap["size"].(string) + + seconds := "10" // 默认 10 秒 + if v, ok := paramsMap["seconds"].(string); ok && v != "" { + seconds = v + } else if duration, ok := paramsMap["duration"].(float64); ok { + seconds = fmt.Sprintf("%.0f", duration) + } else if duration, ok := paramsMap["duration"].(int); ok { + seconds = fmt.Sprintf("%d", duration) + } + + watermark := false + if v, ok := paramsMap["watermark"].(bool); ok { + watermark = v + } + + // 处理图生视频(input_reference 参数) + // 支持单个字符串或数组的第一个元素 + var imageURL string + if inputRef, ok := paramsMap["input_reference"].(string); ok && inputRef != "" { + imageURL = inputRef + } else if images, ok := paramsMap["images"].([]interface{}); ok && len(images) > 0 { + // 兼容旧的 images 参数格式 + if imgStr, ok := images[0].(string); ok && imgStr != "" { + imageURL = imgStr + } + } else if image, ok := paramsMap["image"].(string); ok && image != "" { + // 兼容 image 参数 + imageURL = image + } + + // 官方 Sora:使用 multipart/form-data 携带文件 + if isOfficial && imageURL != "" { + return a.createOfficialSoraTask(task, videoConfig, model, size, seconds, imageURL) + } + + // 其他场景:保持原来的 JSON 调用,input_reference 继续传 URL 字符串 + reqBody := SoraCreateRequest{ + Model: model, + Prompt: task.Prompt, + Size: size, + Seconds: seconds, + Watermark: watermark, + } + + if imageURL != "" { + reqBody.InputReference = imageURL + } + + // 发送 JSON 请求 + apiURL := fmt.Sprintf("%s/v1/videos", videoConfig.ApiURL) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + SetHeader("Content-Type", "application/json"). + SetBody(reqBody). + Post(apiURL) + + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%v", err) + } + + if r.StatusCode != 200 && r.StatusCode != 201 { + body, _ := io.ReadAll(r.Body) + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res SoraCreateResponse + err = json.Unmarshal(body, &res) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + // 转换状态:queued -> pending + state := res.Status + if state == "queued" || state == "in_progress" || state == "" { + state = "pending" + } + + return CreateTaskResponse{ + TaskId: res.ID, + Channel: videoConfig.ApiURL, + Prompt: task.Prompt, + State: state, + CreatedAt: time.Unix(res.CreatedAt, 0).Format(time.RFC3339), + }, nil +} + +// createOfficialSoraTask 调用官方 Sora API,使用 multipart/form-data 携带图片文件 +func (a *SoraAdapter) createOfficialSoraTask(task types.VideoTask, videoConfig *types.VideoConfig, model, size, seconds, imageURL string) (CreateTaskResponse, error) { + if videoConfig == nil || videoConfig.ApiURL == "" || videoConfig.ApiKey == "" { + return CreateTaskResponse{}, fmt.Errorf("Sora 视频配置不完整") + } + + imgData, err := downloadImageBytes(imageURL) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("下载参考图片失败:%v", err) + } + + var buf bytes.Buffer + writer := multipart.NewWriter(&buf) + + // 文本字段 + if err = writer.WriteField("prompt", task.Prompt); err != nil { + return CreateTaskResponse{}, err + } + if err = writer.WriteField("model", model); err != nil { + return CreateTaskResponse{}, err + } + if size != "" { + if err = writer.WriteField("size", size); err != nil { + return CreateTaskResponse{}, err + } + } + if seconds != "" { + if err = writer.WriteField("seconds", seconds); err != nil { + return CreateTaskResponse{}, err + } + } + + // 文件字段 + fileWriter, err := writer.CreateFormFile("input_reference", "image") + if err != nil { + return CreateTaskResponse{}, err + } + if _, err = fileWriter.Write(imgData); err != nil { + return CreateTaskResponse{}, err + } + + if err = writer.Close(); err != nil { + return CreateTaskResponse{}, err + } + + apiURL := fmt.Sprintf("%s/v1/videos", videoConfig.ApiURL) + req, err := http.NewRequest(http.MethodPost, apiURL, &buf) + if err != nil { + return CreateTaskResponse{}, err + } + req.Header.Set("Authorization", "Bearer "+videoConfig.ApiKey) + req.Header.Set("Content-Type", writer.FormDataContentType()) + + client := &http.Client{Timeout: 3 * time.Minute} + resp, err := client.Do(req) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("请求官方 Sora API 出错:%v", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return CreateTaskResponse{}, fmt.Errorf("请求官方 Sora API 出错:%d, %s", resp.StatusCode, string(body)) + } + + var res SoraCreateResponse + if err = json.Unmarshal(body, &res); err != nil { + return CreateTaskResponse{}, fmt.Errorf("解析官方 Sora API 数据失败:%v, %s", err, string(body)) + } + + state := res.Status + if state == "queued" || state == "in_progress" || state == "" { + state = "pending" + } + + return CreateTaskResponse{ + TaskId: res.ID, + Channel: videoConfig.ApiURL, + Prompt: task.Prompt, + State: state, + CreatedAt: time.Unix(res.CreatedAt, 0).Format(time.RFC3339), + }, nil +} + +// downloadImageBytes 下载远程图片并返回二进制内容,用于 multipart 文件上传 +func downloadImageBytes(imageURL string) ([]byte, error) { + body, _, err := utils.FetchURLBytes(context.Background(), imageURL, "", 3*time.Minute, 2, 32<<20) + return body, err +} + +// downloadImageAsDataURL 下载远程图片并转为 data URL,避免向官方 Sora 直接传地址 +// QueryTask 查询任务状态 +func (a *SoraAdapter) QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) { + apiURL := fmt.Sprintf("%s/v1/videos/%s", videoConfig.ApiURL, taskId) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + Get(apiURL) + + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("请求 API 失败:%v", err) + } + defer r.Body.Close() + + if r.StatusCode != 200 { + body, _ := io.ReadAll(r.Body) + return QueryTaskResponse{}, fmt.Errorf("API 返回失败:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res SoraQueryResponse + err = json.Unmarshal(body, &res) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + // 转换状态:queued -> pending, completed -> success + state := res.Status + switch state { + case "completed", "succeed", "success": + state = types.VideoStatusSuccess + case "in_progress", "running": + state = types.VideoStatusInProgress + case "failed": + state = types.VideoStatusFailed + default: + state = types.VideoStatusPending + } + + // 处理错误信息 + errMsg := "" + if res.Error != nil { + errMsg = res.Error.Message + } else { + errMsg = fmt.Sprintf("进度: %d%%", res.Progress) + } + + // 构建响应 + response := QueryTaskResponse{ + TaskId: res.ID, + Status: state, + Progress: res.Progress, + VideoURL: res.VideoURL, + Prompt: "", // Sora API 响应中不包含 prompt 字段 + ErrMsg: errMsg, + StatusMsg: fmt.Sprintf("进度: %d%%", res.Progress), + } + + // 如果有原始数据,转换为 JSON 字符串 + if len(body) > 0 { + response.Output = string(body) + } + + return response, nil +} diff --git a/api/service/video/adapters/veo_adapter.go b/api/service/video/adapters/veo_adapter.go new file mode 100644 index 00000000..90898561 --- /dev/null +++ b/api/service/video/adapters/veo_adapter.go @@ -0,0 +1,226 @@ +package adapters + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "encoding/json" + "fmt" + "geekai/core/types" + "io" + "strings" + "time" + + "github.com/imroc/req/v3" + "gorm.io/gorm" +) + +// VideoAdapter 视频生成适配器接口 +type VideoAdapter interface { + // CreateTask 创建视频生成任务 + CreateTask(task types.VideoTask, videoConfig *types.VideoConfig) (CreateTaskResponse, error) + + // QueryTask 查询任务状态 + QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) + + // GetProvider 获取服务提供商名称(不带版本号:veo, sora, luma) + GetProvider() string +} + +// VeoAdapter Veo 视频生成适配器 +type VeoAdapter struct { + db *gorm.DB + httpClient *req.Client +} + +// NewVeoAdapter 创建 Veo 适配器 +func NewVeoAdapter(db *gorm.DB) *VeoAdapter { + return &VeoAdapter{ + db: db, + httpClient: req.C().SetTimeout(time.Minute * 3), + } +} + +// GetProvider 获取服务提供商名称 +func (a *VeoAdapter) GetProvider() string { + return "veo" +} + +// VeoCreateRequest Veo 创建任务请求 +type VeoCreateRequest struct { + Prompt string `json:"prompt"` + Model string `json:"model"` + EnhancePrompt bool `json:"enhance_prompt,omitempty"` + EnableUpsample bool `json:"enable_upsample,omitempty"` + AspectRatio string `json:"aspect_ratio,omitempty"` + Images []string `json:"images,omitempty"` // 图生视频时使用 +} + +// VeoCreateResponse Veo 创建任务响应 +type VeoCreateResponse struct { + TaskId string `json:"task_id"` +} + +// VeoQueryResponse Veo 查询任务响应 +type VeoQueryResponse struct { + TaskId string `json:"task_id"` + Platform string `json:"platform"` + Action string `json:"action"` + Status string `json:"status"` + FailReason string `json:"fail_reason"` + SubmitTime int64 `json:"submit_time"` + StartTime int64 `json:"start_time"` + FinishTime int64 `json:"finish_time"` + Progress string `json:"progress"` + Data VeoQueryData `json:"data"` + SearchItem string `json:"search_item"` +} + +// VeoQueryData Veo 查询响应中的 data 字段 +type VeoQueryData struct { + Output string `json:"output"` +} + +// CreateTask 创建视频生成任务 +func (a *VeoAdapter) CreateTask(task types.VideoTask, videoConfig *types.VideoConfig) (CreateTaskResponse, error) { + // 解析任务参数 + paramsMap, ok := task.Params.(map[string]any) + if !ok { + return CreateTaskResponse{}, fmt.Errorf("invalid params type for Veo video task") + } + + // 构建请求参数 + reqBody := VeoCreateRequest{ + Prompt: task.Prompt, + } + + // 从 params 中提取参数 + if model, ok := paramsMap["model"].(string); ok { + reqBody.Model = model + } + if enhancePrompt, ok := paramsMap["enhance_prompt"].(bool); ok { + reqBody.EnhancePrompt = enhancePrompt + } + if enableUpsample, ok := paramsMap["enable_upsample"].(bool); ok { + reqBody.EnableUpsample = enableUpsample + } + if aspectRatio, ok := paramsMap["aspect_ratio"].(string); ok { + reqBody.AspectRatio = aspectRatio + } + + // 处理图生视频(images 参数) + if images, ok := paramsMap["images"].([]interface{}); ok { + imageUrls := make([]string, 0) + for _, img := range images { + if imgStr, ok := img.(string); ok { + imageUrls = append(imageUrls, imgStr) + } + } + if len(imageUrls) > 0 { + reqBody.Images = imageUrls + } + } + + // 发送请求 + apiURL := fmt.Sprintf("%s/v2/videos/generations", videoConfig.ApiURL) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + SetHeader("Content-Type", "application/json"). + SetBody(reqBody). + Post(apiURL) + + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%v", err) + } + + if r.StatusCode != 200 && r.StatusCode != 201 { + body, _ := io.ReadAll(r.Body) + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res VeoCreateResponse + err = json.Unmarshal(body, &res) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + return CreateTaskResponse{ + TaskId: res.TaskId, + Channel: videoConfig.ApiURL, + Prompt: task.Prompt, + State: types.VideoStatusPending, + CreatedAt: time.Now().Format(time.RFC3339), + }, nil +} + +// QueryTask 查询任务状态 +func (a *VeoAdapter) QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) { + apiURL := fmt.Sprintf("%s/v2/videos/generations/%s", videoConfig.ApiURL, taskId) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + Get(apiURL) + + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("请求 API 失败:%v", err) + } + defer r.Body.Close() + + if r.StatusCode != 200 { + body, _ := io.ReadAll(r.Body) + return QueryTaskResponse{}, fmt.Errorf("API 返回失败:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res VeoQueryResponse + err = json.Unmarshal(body, &res) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + // 转换状态(SUCCESS -> success, FAILED -> failed, 其他保持原样) + state := strings.ToLower(res.Status) + switch state { + case "in_progress", "running": + state = types.VideoStatusInProgress + case "completed", "succeed", "success": + state = types.VideoStatusSuccess + case "failed": + state = types.VideoStatusFailed + default: + state = types.VideoStatusPending + } + + // 解析进度(从 "100%" 转换为 100) + progress := 0 + if res.Progress != "" { + // 移除 % 符号并转换为整数 + progressStr := strings.TrimSuffix(res.Progress, "%") + if p, err := fmt.Sscanf(progressStr, "%d", &progress); err == nil && p == 1 { + // 成功解析 + } + } + + // 从 data.output 中提取视频 URL + videoURL := res.Data.Output + + // 构建响应 + response := QueryTaskResponse{ + TaskId: res.TaskId, + Status: state, + Progress: progress, + VideoURL: videoURL, + ErrMsg: res.FailReason, + } + + // 如果有原始数据,转换为 JSON 字符串 + if len(body) > 0 { + response.Output = string(body) + } + + return response, nil +} diff --git a/api/service/video/adapters/wan_adapter.go b/api/service/video/adapters/wan_adapter.go new file mode 100644 index 00000000..b6bf01b9 --- /dev/null +++ b/api/service/video/adapters/wan_adapter.go @@ -0,0 +1,220 @@ +package adapters + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "encoding/json" + "fmt" + "geekai/core/types" + "io" + "strings" + "time" + + "github.com/imroc/req/v3" + "gorm.io/gorm" +) + +// WanAdapter Wan(通义万相)视频生成适配器 +type WanAdapter struct { + db *gorm.DB + httpClient *req.Client +} + +// NewWanAdapter 创建 Wan 适配器 +func NewWanAdapter(db *gorm.DB) *WanAdapter { + return &WanAdapter{ + db: db, + httpClient: req.C().SetTimeout(time.Minute * 3), + } +} + +// GetProvider 获取服务提供商名称 +func (a *WanAdapter) GetProvider() string { + return "wan" +} + +// WanCreateRequest Wan 创建任务请求 +type WanCreateRequest struct { + Prompt string `json:"prompt"` + Model string `json:"model"` + Duration int `json:"duration,omitempty"` + Resolution string `json:"resolution,omitempty"` + NegativePrompt string `json:"negative_prompt,omitempty"` + Images []string `json:"images,omitempty"` + PromptExtend bool `json:"prompt_extend,omitempty"` +} + +// WanCreateResponse Wan 创建任务响应 +type WanCreateResponse struct { + TaskId string `json:"task_id"` +} + +// WanQueryResponse Wan 查询任务响应 +type WanQueryResponse struct { + TaskId string `json:"task_id"` + Platform string `json:"platform"` + Action string `json:"action"` + Status string `json:"status"` + FailReason string `json:"fail_reason"` + SubmitTime int64 `json:"submit_time"` + StartTime int64 `json:"start_time"` + FinishTime int64 `json:"finish_time"` + Progress string `json:"progress"` + Data WanQueryData `json:"data"` + SearchItem string `json:"search_item"` +} + +// WanQueryData Wan 查询响应中的 data 字段 +type WanQueryData struct { + Output string `json:"output"` +} + +// CreateTask 创建视频生成任务 +func (a *WanAdapter) CreateTask(task types.VideoTask, videoConfig *types.VideoConfig) (CreateTaskResponse, error) { + // 解析任务参数 + paramsMap, ok := task.Params.(map[string]any) + if !ok { + return CreateTaskResponse{}, fmt.Errorf("invalid params type for Wan video task") + } + + // 构建请求参数 + reqBody := WanCreateRequest{ + Prompt: task.Prompt, + } + + // 从 params 中提取参数 + if model, ok := paramsMap["model"].(string); ok { + reqBody.Model = model + } + if duration, ok := paramsMap["duration"].(float64); ok { + reqBody.Duration = int(duration) + } else if duration, ok := paramsMap["duration"].(int); ok { + reqBody.Duration = duration + } + if resolution, ok := paramsMap["resolution"].(string); ok { + reqBody.Resolution = resolution + } + if images, ok := paramsMap["images"].([]any); ok { + imageUrls := make([]string, 0) + for _, img := range images { + if imgStr, ok := img.(string); ok { + imageUrls = append(imageUrls, imgStr) + } + } + if len(imageUrls) > 0 { + reqBody.Images = imageUrls + } + } + if negativePrompt, ok := paramsMap["negative_prompt"].(string); ok { + reqBody.NegativePrompt = negativePrompt + } + if promptExtend, ok := paramsMap["prompt_extend"].(bool); ok { + reqBody.PromptExtend = promptExtend + } + + logger.Debugf("WanCreateRequest: %+v", reqBody) + + // 发送请求 + apiURL := fmt.Sprintf("%s/v2/videos/generations", videoConfig.ApiURL) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + SetHeader("Content-Type", "application/json"). + SetBody(reqBody). + Post(apiURL) + + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%v", err) + } + + if r.StatusCode != 200 && r.StatusCode != 201 { + body, _ := io.ReadAll(r.Body) + return CreateTaskResponse{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res WanCreateResponse + err = json.Unmarshal(body, &res) + if err != nil { + return CreateTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + return CreateTaskResponse{ + TaskId: res.TaskId, + Channel: videoConfig.ApiURL, + Prompt: task.Prompt, + State: "pending", + CreatedAt: time.Now().Format(time.RFC3339), + }, nil +} + +// QueryTask 查询任务状态 +func (a *WanAdapter) QueryTask(taskId string, channel string, videoConfig *types.VideoConfig) (QueryTaskResponse, error) { + apiURL := fmt.Sprintf("%s/v2/videos/generations/%s", videoConfig.ApiURL, taskId) + r, err := a.httpClient.R(). + SetHeader("Authorization", "Bearer "+videoConfig.ApiKey). + Get(apiURL) + + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("请求 API 失败:%v", err) + } + defer r.Body.Close() + + if r.StatusCode != 200 { + body, _ := io.ReadAll(r.Body) + return QueryTaskResponse{}, fmt.Errorf("API 返回失败:%d, %s", r.StatusCode, string(body)) + } + + body, _ := io.ReadAll(r.Body) + var res WanQueryResponse + err = json.Unmarshal(body, &res) + if err != nil { + return QueryTaskResponse{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) + } + + // 转换状态(SUCCESS -> success, FAILED -> failed, 其他保持原样) + state := strings.ToLower(res.Status) + switch state { + case "in_progress", "running": + state = types.VideoStatusInProgress + case "completed", "succeed", "success": + state = types.VideoStatusSuccess + case "failed", "failure": + state = types.VideoStatusFailed + default: + state = types.VideoStatusPending + } + + // 解析进度(从 "100%" 转换为 100) + progress := 0 + if res.Progress != "" { + // 移除 % 符号并转换为整数 + progressStr := strings.TrimSuffix(res.Progress, "%") + if p, err := fmt.Sscanf(progressStr, "%d", &progress); err == nil && p == 1 { + // 成功解析 + } + } + + // 从 data.output 中提取视频 URL + videoURL := res.Data.Output + + // 构建响应 + response := QueryTaskResponse{ + TaskId: res.TaskId, + Status: state, + Progress: progress, + VideoURL: videoURL, + ErrMsg: res.FailReason, + } + + // 如果有原始数据,转换为 JSON 字符串 + if len(body) > 0 { + response.Output = string(body) + } + + return response, nil +} diff --git a/api/service/video/power_calculator.go b/api/service/video/power_calculator.go new file mode 100644 index 00000000..222391ee --- /dev/null +++ b/api/service/video/power_calculator.go @@ -0,0 +1,77 @@ +package video + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "errors" + "fmt" + "geekai/core/types" + "geekai/store/model" + "geekai/utils" + "gorm.io/gorm" +) + +// GetVideoConfig 从数据库获取视频配置 +func GetVideoConfig(db *gorm.DB) (*types.VideoConfig, error) { + var config model.Config + err := db.Where("name", types.ConfigKeyVideo).First(&config).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, errors.New("视频配置不存在,请在管理后台配置") + } + return nil, fmt.Errorf("获取视频配置失败: %v", err) + } + + var videoConfig types.VideoConfig + err = utils.JsonDecode(config.Value, &videoConfig) + if err != nil { + return nil, fmt.Errorf("解析视频配置失败: %v", err) + } + + return &videoConfig, nil +} + +// GetModelPowerConfig 获取指定模型的算力配置 +func GetModelPowerConfig(db *gorm.DB, modelKey string) (*types.VideoModelPower, error) { + config, err := GetVideoConfig(db) + if err != nil { + return nil, err + } + + modelPower, ok := config.VideoPowers[modelKey] + if !ok { + return nil, fmt.Errorf("模型 %s 的算力配置不存在", modelKey) + } + + return &modelPower, nil +} + +// CalculatePower 根据 modelKey 和 priceKey 计算算力 +// modelKey: 模型标识(如 "veo-2.0", "sora-2.0") +// priceKey: 价格键(如 "fixed", "5_720P", "std_5_sound" 等) +func CalculatePower(db *gorm.DB, modelKey string, priceKey string) (int, error) { + if priceKey == "" { + return 0, errors.New("priceKey 不能为空") + } + + modelPower, err := GetModelPowerConfig(db, modelKey) + if err != nil { + return 0, err + } + + power, ok := modelPower.PowerConfig[priceKey] + if !ok { + return 0, fmt.Errorf("模型 %s 的价格配置 %s 不存在", modelKey, priceKey) + } + + if power <= 0 { + return 0, fmt.Errorf("模型 %s 的价格配置 %s 的值无效", modelKey, priceKey) + } + + return power, nil +} diff --git a/api/service/video/video.go b/api/service/video/video.go deleted file mode 100644 index b8581067..00000000 --- a/api/service/video/video.go +++ /dev/null @@ -1,663 +0,0 @@ -package video - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * Copyright 2023 The Geek-AI Authors. All rights reserved. -// * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. -// * @Author yangjian102621@163.com -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "geekai/core/types" - logger2 "geekai/logger" - "geekai/service" - "geekai/service/oss" - "geekai/store" - "geekai/store/model" - "geekai/utils" - "io" - "net/http" - "time" - - "github.com/go-redis/redis/v8" - - "github.com/imroc/req/v3" - "gorm.io/gorm" -) - -var logger = logger2.GetLogger() - -type Service struct { - httpClient *req.Client - db *gorm.DB - uploadManager *oss.UploaderManager - taskQueue *store.RedisQueue - userService *service.UserService -} - -func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service { - return &Service{ - httpClient: req.C().SetTimeout(time.Minute * 3), - db: db, - taskQueue: store.NewRedisQueue("Video_Task_Queue", redisCli), - uploadManager: manager, - userService: userService, - } -} - -func (s *Service) PushTask(task types.VideoTask) { - logger.Infof("add a new Video task to the task list: %+v", task) - if err := s.taskQueue.RPush(task); err != nil { - logger.Errorf("push video task to queue failed: %v", err) - } -} - -func (s *Service) Run() { - // 将数据库中未提交的任务加载到队列 - var jobs []model.VideoJob - s.db.Where("task_id", "").Where("progress", 0).Find(&jobs) - for _, v := range jobs { - var task types.VideoTask - err := utils.JsonDecode(v.TaskInfo, &task) - if err != nil { - logger.Errorf("decode task info with error: %v", err) - continue - } - task.Id = v.Id - s.PushTask(task) - } - logger.Info("Starting Video job consumer...") - go func() { - for { - var task types.VideoTask - err := s.taskQueue.LPop(&task) - if err != nil { - logger.Errorf("taking task with error: %v", err) - continue - } - - if task.Type == types.VideoLuma { - // translate prompt - if utils.HasChinese(task.Prompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), task.TranslateModelId) - if err == nil { - task.Prompt = content - } else { - logger.Warnf("error with translate prompt: %v", err) - } - } - var r LumaRespVo - r, err = s.LumaCreate(task) - if err != nil { - logger.Errorf("create task with error: %v", err) - err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ - "err_msg": err.Error(), - "progress": service.FailTaskProgress, - "cover_url": "/images/failed.jpg", - }).Error - if err != nil { - logger.Errorf("update task with error: %v", err) - } - continue - } - - // 更新任务信息 - err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ - "task_id": r.Id, - "channel": r.Channel, - "prompt_ext": r.Prompt, - }).Error - if err != nil { - logger.Errorf("update task with error: %v", err) - s.PushTask(task) - } - } else if task.Type == types.VideoKeLing { - var r KeLingRespVo - r, err = s.KeLingCreate(task) - logger.Debugf("ke ling create task result: %+v", r) - - if err != nil { - logger.Errorf("create task with error: %v", err) - err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ - "err_msg": err.Error(), - "progress": service.FailTaskProgress, - "cover_url": "/images/failed.jpg", - }).Error - if err != nil { - logger.Errorf("update task with error: %v", err) - } - continue - } - - // 更新任务信息 - err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ - "task_id": r.Data.TaskID, - "channel": r.Channel, - "prompt_ext": task.Prompt, - }).Error - if err != nil { - logger.Errorf("update task with error: %v", err) - s.PushTask(task) - } - } - - } - }() -} - -func (s *Service) DownloadFiles() { - go func() { - var items []model.VideoJob - for { - res := s.db.Where("progress", 102).Find(&items) - if res.Error != nil { - continue - } - - for _, v := range items { - if v.WaterURL == "" { - continue - } - - logger.Infof("try download video: %s", v.WaterURL) - videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.WaterURL, ".mp4", true) - if err != nil { - logger.Errorf("download video with error: %v", err) - continue - } - logger.Infof("download video success: %s", videoURL) - v.WaterURL = videoURL - - if v.VideoURL != "" { - logger.Infof("try download no water video: %s", v.VideoURL) - videoURL, err = s.uploadManager.GetUploadHandler().PutUrlFile(v.VideoURL, ".mp4", true) - if err != nil { - logger.Errorf("download video with error: %v", err) - continue - } - } - logger.Infof("download no water video success: %s", videoURL) - v.VideoURL = videoURL - v.Progress = 100 - s.db.Updates(&v) - - // Convert TaskInfo to VideoTask - var videoTask types.VideoTask - if err := json.Unmarshal([]byte(v.TaskInfo), &videoTask); err != nil { - logger.Errorf("failed to unmarshal task info to VideoTask: %v", err) - continue - } - - } - - time.Sleep(time.Second * 10) - } - }() -} - -// SyncTaskProgress 异步拉取任务 -func (s *Service) SyncTaskProgress() { - go func() { - var jobs []model.VideoJob - for { - res := s.db.Where("progress < ?", 100).Where("task_id <> ?", "").Find(&jobs) - if res.Error != nil { - continue - } - - for _, job := range jobs { - if job.Type == types.VideoLuma { - task, err := s.QueryLumaTask(job.TaskId, job.Channel) - if err != nil { - logger.Errorf("query task with error: %v", err) - // 更新任务信息 - s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]interface{}{ - "progress": service.FailTaskProgress, // 102 表示资源未下载完成, - "err_msg": err.Error(), - "cover_url": "/images/failed.jpg", - }) - continue - } - - logger.Debugf("task: %+v", task) - if task.State == "completed" { // 更新任务信息 - data := map[string]interface{}{ - "progress": 102, // 102 表示资源未下载完成, - "water_url": task.Video.Url, - "raw_data": utils.JsonEncode(task), - "prompt_ext": task.Prompt, - "cover_url": task.Thumbnail.Url, - } - if task.Video.DownloadUrl != "" { - data["video_url"] = task.Video.DownloadUrl - } - err = s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(data).Error - if err != nil { - logger.Errorf("更新数据库失败:%v", err) - continue - } - } - } else if job.Type == types.VideoKeLing { - // Convert TaskInfo to VideoTask - var videoTask types.VideoTask - if err := json.Unmarshal([]byte(job.TaskInfo), &videoTask); err != nil { - logger.Errorf("failed to unmarshal task info to VideoTask: %v", err) - continue - } - - // Type assert task.Params to KeLingVideoParams - paramsMap, ok := videoTask.Params.(map[string]interface{}) - if !ok { - continue - } - - // Convert map to KeLingVideoParams - paramsBytes, err := json.Marshal(paramsMap) - if err != nil { - continue - } - - var params types.KeLingVideoParams - if err := json.Unmarshal(paramsBytes, ¶ms); err != nil { - continue - } - - task, err := s.QueryKeLingTask(job.TaskId, job.Channel, params.TaskType) - if err != nil { - logger.Errorf("query task with error: %v", err) - // 更新任务信息 - s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]interface{}{ - "progress": service.FailTaskProgress, // 102 表示资源未下载完成, - "err_msg": err.Error(), - "cover_url": "/images/failed.jpg", - }) - continue - } - - logger.Debugf("task: %+v", task) - if task.TaskStatus == "succeed" { // 更新任务信息 - data := map[string]interface{}{ - "progress": 102, // 102 表示资源未下载完成, - "water_url": task.TaskResult.Videos[0].URL, - "raw_data": utils.JsonEncode(task), - "prompt_ext": job.Prompt, - "cover_url": "", - } - if len(task.TaskResult.Videos) > 0 { - data["video_url"] = task.TaskResult.Videos[0].URL - } - err = s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(data).Error - if err != nil { - logger.Errorf("更新数据库失败:%v", err) - continue - } - } else if task.TaskStatus == "failed" { - // 更新任务信息 - s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]interface{}{ - "progress": service.FailTaskProgress, - "err_msg": task.TaskStatusMsg, - "cover_url": "/images/failed.jpg", - }) - } - } - - } - - // 找出失败的任务,并恢复其扣减算力 - s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) - for _, job := range jobs { - err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ - Type: types.PowerRefund, - Model: job.Type, - Remark: fmt.Sprintf("%s 任务失败,退回算力。任务ID:%s,Err:%s", job.Type, job.TaskId, job.ErrMsg), - }) - if err != nil { - continue - } - // 更新任务状态 - s.db.Model(&job).UpdateColumn("power", 0) - } - time.Sleep(time.Second * 10) - } - }() -} - -type LumaTaskVo struct { - Id string `json:"id"` - Liked interface{} `json:"liked"` - State string `json:"state"` - Video struct { - Url string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` - Thumbnail string `json:"thumbnail"` - DownloadUrl string `json:"download_url"` - } `json:"video"` - Prompt string `json:"prompt"` - UserId string `json:"user_id"` - BatchId string `json:"batch_id"` - Thumbnail struct { - Url string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` - } `json:"thumbnail"` - VideoRaw struct { - Url string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` - } `json:"video_raw"` - CreatedAt string `json:"created_at"` - LastFrame struct { - Url string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` - } `json:"last_frame"` -} - -type LumaRespVo struct { - Id string `json:"id"` - Prompt string `json:"prompt"` - State string `json:"state"` - QueueState interface{} `json:"queue_state"` - CreatedAt string `json:"created_at"` - Video interface{} `json:"video"` - VideoRaw interface{} `json:"video_raw"` - Liked interface{} `json:"liked"` - EstimateWaitSeconds interface{} `json:"estimate_wait_seconds"` - Thumbnail interface{} `json:"thumbnail"` - Channel string `json:"channel,omitempty"` -} - -func (s *Service) LumaCreate(task types.VideoTask) (LumaRespVo, error) { - // 读取 API KEY - var apiKey model.ApiKey - session := s.db.Session(&gorm.Session{}).Where("type", "luma").Where("enabled", true) - if task.Channel != "" { - session = session.Where("api_url", task.Channel) - } - tx := session.Order("last_used_at DESC").First(&apiKey) - if tx.Error != nil { - return LumaRespVo{}, errors.New("no available API KEY for Luma") - } - - // Type assert task.Params to LumaVideoParams - paramsMap, ok := task.Params.(map[string]interface{}) - if !ok { - return LumaRespVo{}, errors.New("invalid params type for Luma video task") - } - - // Convert map to LumaVideoParams - paramsBytes, err := json.Marshal(paramsMap) - if err != nil { - return LumaRespVo{}, fmt.Errorf("failed to marshal params: %v", err) - } - - var params types.LumaVideoParams - if err := json.Unmarshal(paramsBytes, ¶ms); err != nil { - return LumaRespVo{}, fmt.Errorf("failed to unmarshal params: %v", err) - } - - reqBody := map[string]interface{}{ - "user_prompt": task.Prompt, - "expand_prompt": params.PromptOptimize, - "loop": params.Loop, - "image_url": params.StartImgURL, // 图生视频 - "image_end_url": params.EndImgURL, // 图生视频 - } - - var res LumaRespVo - apiURL := fmt.Sprintf("%s/luma/generations", apiKey.ApiURL) - logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody) - r, err := req.C().R(). - SetHeader("Authorization", "Bearer "+apiKey.Value). - SetBody(reqBody). - Post(apiURL) - if err != nil { - return LumaRespVo{}, fmt.Errorf("请求 API 出错:%v", err) - } - - if r.StatusCode != 200 && r.StatusCode != 201 { - return LumaRespVo{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, r.String()) - } - - body, _ := io.ReadAll(r.Body) - err = json.Unmarshal(body, &res) - if err != nil { - return LumaRespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) - } - - // update the last_use_at for api key - apiKey.LastUsedAt = time.Now().Unix() - session.Updates(&apiKey) - res.Channel = apiKey.ApiURL - return res, nil -} - -func (s *Service) QueryLumaTask(taskId string, channel string) (LumaTaskVo, error) { - // 读取 API KEY - var apiKey model.ApiKey - err := s.db.Session(&gorm.Session{}).Where("type", "luma"). - Where("api_url", channel). - Where("enabled", true). - Order("last_used_at DESC").First(&apiKey).Error - if err != nil { - return LumaTaskVo{}, errors.New("no available API KEY for Luma") - } - - apiURL := fmt.Sprintf("%s/luma/generations/%s", apiKey.ApiURL, taskId) - var res LumaTaskVo - r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL) - - if err != nil { - return LumaTaskVo{}, fmt.Errorf("请求 API 失败:%v", err) - } - defer r.Body.Close() - - if r.StatusCode != 200 { - return LumaTaskVo{}, fmt.Errorf("API 返回失败:%v", r.String()) - } - - body, _ := io.ReadAll(r.Body) - err = json.Unmarshal(body, &res) - if err != nil { - return LumaTaskVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) - } - - return res, nil -} - -type KeLingRespVo struct { - Code int `json:"code"` - Message string `json:"message"` - RequestID string `json:"request_id"` - Data struct { - TaskID string `json:"task_id"` - TaskStatus string `json:"task_status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - } `json:"data"` - Channel string `json:"channel,omitempty"` -} - -func (s *Service) KeLingCreate(task types.VideoTask) (KeLingRespVo, error) { - var apiKey model.ApiKey - session := s.db.Session(&gorm.Session{}).Where("type", "keling").Where("enabled", true) - if task.Channel != "" { - session = session.Where("api_url", task.Channel) - } - tx := session.Order("last_used_at DESC").First(&apiKey) - if tx.Error != nil { - return KeLingRespVo{}, errors.New("no available API KEY for keling") - } - - // Type assert task.Params to KeLingVideoParams - paramsMap, ok := task.Params.(map[string]interface{}) - if !ok { - return KeLingRespVo{}, errors.New("invalid params type for KeLing video task") - } - - // Convert map to KeLingVideoParams - paramsBytes, err := json.Marshal(paramsMap) - if err != nil { - return KeLingRespVo{}, fmt.Errorf("failed to marshal params: %v", err) - } - - var params types.KeLingVideoParams - if err := json.Unmarshal(paramsBytes, ¶ms); err != nil { - return KeLingRespVo{}, fmt.Errorf("failed to unmarshal params: %v", err) - } - - // 2. 构建API请求参数 - payload := map[string]interface{}{ - "model_name": params.Model, - "prompt": task.Prompt, - "negative_prompt": params.NegPrompt, - "cfg_scale": params.CfgScale, - "mode": params.Mode, - "aspect_ratio": params.AspectRatio, - "duration": params.Duration, - } - - // 只有当 CameraControl 的类型不为空时,才处理摄像机控制参数 - if params.CameraControl.Type != "" { - cameraControl := map[string]interface{}{ - "type": params.CameraControl.Type, - } - - // 只有在 simple 类型时才添加 config 参数 - if params.CameraControl.Type == "simple" { - cameraControl["config"] = params.CameraControl.Config - } - - payload["camera_control"] = cameraControl - } - - // 处理图生视频 - if params.TaskType == "image2video" { - payload["image"] = params.Image - payload["image_tail"] = params.ImageTail - } - - jsonPayload, err := json.Marshal(payload) - if err != nil { - return KeLingRespVo{}, fmt.Errorf("failed to marshal payload: %v", err) - } - - // 3. 准备HTTP请求 - url := fmt.Sprintf("%s/kling/v1/videos/%s", apiKey.ApiURL, params.TaskType) - req, err := http.NewRequest("POST", url, bytes.NewReader(jsonPayload)) - if err != nil { - return KeLingRespVo{}, fmt.Errorf("failed to create request: %v", err) - } - - req.Header.Set("Authorization", "Bearer "+apiKey.Value) - req.Header.Set("Content-Type", "application/json") - - // 4. 发送请求 - client := &http.Client{Timeout: time.Duration(30) * time.Second} - resp, err := client.Do(req) - if err != nil { - return KeLingRespVo{}, fmt.Errorf("failed to send request: %v", err) - } - defer resp.Body.Close() - - // 5. 处理响应 - body, err := io.ReadAll(resp.Body) - if err != nil { - return KeLingRespVo{}, fmt.Errorf("failed to read response: %v", err) - } - - if resp.StatusCode != http.StatusOK { - return KeLingRespVo{}, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) - } - - var apiResponse = KeLingRespVo{} - if err := json.Unmarshal(body, &apiResponse); err != nil { - return KeLingRespVo{}, fmt.Errorf("failed to parse response: %v", err) - } - // 设置 API 通道 - apiResponse.Channel = apiKey.ApiURL - return apiResponse, nil -} - -// VideoCallbackData 表示视频生成任务的回调数据 -type VideoCallbackData struct { - TaskID string `json:"task_id"` - TaskStatus string `json:"task_status"` - TaskStatusMsg string `json:"task_status_msg"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - TaskResult TaskResult `json:"task_result"` -} - -type TaskResult struct { - Images []CallBackImageResult `json:"images,omitempty"` - Videos []CallBackVideoResult `json:"videos,omitempty"` -} - -type CallBackImageResult struct { - Index int `json:"index"` - URL string `json:"url"` -} - -type CallBackVideoResult struct { - ID string `json:"id"` - URL string `json:"url"` - Duration string `json:"duration"` -} - -func (s *Service) QueryKeLingTask(taskId string, channel string, action string) (VideoCallbackData, error) { - var apiKey model.ApiKey - err := s.db.Session(&gorm.Session{}).Where("type", "keling"). - //Where("api_url", channel). - Where("enabled", true). - Order("last_used_at DESC").First(&apiKey).Error - if err != nil { - return VideoCallbackData{}, errors.New("no available API KEY for keling") - } - - url := fmt.Sprintf("%s/kling/v1/videos/%s/%s", apiKey.ApiURL, action, taskId) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return VideoCallbackData{}, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Authorization", "Bearer "+apiKey.Value) - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - res, err := client.Do(req) - if err != nil { - return VideoCallbackData{}, fmt.Errorf("failed to execute request: %w", err) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return VideoCallbackData{}, fmt.Errorf("unexpected status code: %d", res.StatusCode) - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return VideoCallbackData{}, fmt.Errorf("failed to read response body: %w", err) - } - - var response struct { - Code int `json:"code"` - Message string `json:"message"` - Data VideoCallbackData `json:"data"` - } - - if err := json.Unmarshal(body, &response); err != nil { - return VideoCallbackData{}, fmt.Errorf("failed to unmarshal response: %w", err) - } - - if response.Code != 0 { - return VideoCallbackData{}, fmt.Errorf("API error: %s", response.Message) - } - - return response.Data, nil -} diff --git a/api/service/video/video_service.go b/api/service/video/video_service.go new file mode 100644 index 00000000..16a1b213 --- /dev/null +++ b/api/service/video/video_service.go @@ -0,0 +1,333 @@ +package video + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "encoding/json" + "fmt" + "geekai/core/types" + "geekai/log" + "geekai/service" + "geekai/service/oss" + "geekai/service/video/adapters" + "geekai/store" + "geekai/store/model" + "geekai/utils" + "time" + + "github.com/go-redis/redis/v8" + "gorm.io/gorm" +) + +var logger = log.GetLogger() + +type Service struct { + db *gorm.DB + uploadManager *oss.UploaderManager + taskQueue *store.RedisQueue + userService *service.UserService + adapters map[string]adapters.VideoAdapter // provider -> adapter +} + +func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service { + service := &Service{ + db: db, + taskQueue: store.NewRedisQueue("Video_Task_Queue", redisCli), + uploadManager: manager, + userService: userService, + adapters: make(map[string]VideoAdapter), + } + + // 注册所有适配器 + service.registerAdapters() + + return service +} + +// VideoAdapter 类型别名,指向 adapters.VideoAdapter +type VideoAdapter = adapters.VideoAdapter + +// registerAdapters 注册所有视频生成适配器 +func (s *Service) registerAdapters() { + // 注册 Veo 适配器 + veoAdapter := adapters.NewVeoAdapter(s.db) + s.adapters[veoAdapter.GetProvider()] = veoAdapter + + // 注册 Sora 适配器 + soraAdapter := adapters.NewSoraAdapter(s.db) + s.adapters[soraAdapter.GetProvider()] = soraAdapter + + // 注册 Luma 适配器 + lumaAdapter := adapters.NewLumaAdapter(s.db) + s.adapters[lumaAdapter.GetProvider()] = lumaAdapter + + // 注册可灵适配器 + kelingAdapter := adapters.NewKelingAdapter(s.db) + s.adapters[kelingAdapter.GetProvider()] = kelingAdapter + + // 注册 MiniMax 适配器 + minimaxAdapter := adapters.NewMiniMaxAdapter(s.db) + s.adapters[minimaxAdapter.GetProvider()] = minimaxAdapter + + // 注册 Wan 适配器 + wanAdapter := adapters.NewWanAdapter(s.db) + s.adapters[wanAdapter.GetProvider()] = wanAdapter + + // 注册 Doubao 适配器 + doubaoAdapter := adapters.NewDoubaoAdapter(s.db) + s.adapters[doubaoAdapter.GetProvider()] = doubaoAdapter +} + +// getAdapter 获取指定 provider 的适配器 +func (s *Service) getAdapter(provider string) (adapters.VideoAdapter, error) { + adapter, ok := s.adapters[provider] + if !ok { + return nil, fmt.Errorf("不支持的视频生成服务提供商: %s", provider) + } + return adapter, nil +} + +// getVideoConfig 获取视频配置 +func (s *Service) getVideoConfig() (*types.VideoConfig, error) { + return GetVideoConfig(s.db) +} + +// CreateTask 统一的创建任务方法 +func (s *Service) CreateTask(task types.VideoTask) (adapters.CreateTaskResponse, error) { + // 获取适配器 + adapter, err := s.getAdapter(task.Type) + if err != nil { + return adapters.CreateTaskResponse{}, err + } + + // 获取视频配置 + videoConfig, err := s.getVideoConfig() + if err != nil { + return adapters.CreateTaskResponse{}, err + } + + // 调用适配器创建任务 + return adapter.CreateTask(task, videoConfig) +} + +// QueryTask 统一的查询任务方法 +func (s *Service) QueryTask(provider string, taskId string, channel string, modelKey string) (adapters.QueryTaskResponse, error) { + // 获取适配器 + adapter, err := s.getAdapter(provider) + if err != nil { + return adapters.QueryTaskResponse{}, err + } + + // 获取视频配置 + videoConfig, err := s.getVideoConfig() + if err != nil { + return adapters.QueryTaskResponse{}, err + } + + // 调用适配器查询任务 + return adapter.QueryTask(taskId, channel, videoConfig) +} + +func (s *Service) PushTask(task types.VideoTask) { + logger.Infof("[video] push task to queue jobId=%d type=%s", task.Id, task.Type) + if err := s.taskQueue.RPush(task); err != nil { + logger.Errorf("[video] push task to queue failed jobId=%d: %v", task.Id, err) + } +} + +func (s *Service) Run() { + // 将数据库中未提交的任务加载到队列 + var jobs []model.VideoJob + s.db.Where("task_id", "").Where("progress", 0).Find(&jobs) + for _, v := range jobs { + var task types.VideoTask + err := utils.JsonDecode(v.Params, &task) + if err != nil { + logger.Errorf("decode task info with error: %v", err) + continue + } + task.Id = v.Id + s.PushTask(task) + } + logger.Infof("[video] job consumer started, loaded %d pending jobs from DB", len(jobs)) + go func() { + for { + var task types.VideoTask + err := s.taskQueue.LPop(&task) + if err != nil { + logger.Errorf("taking task with error: %v", err) + continue + } + + logger.Debugf("[video] submitting task jobId=%d type=%s prompt=%q", task.Id, task.Type, task.Prompt) + r, err := s.CreateTask(task) + if err != nil { + logger.Errorf("[video] submit failed jobId=%d type=%s: %v", task.Id, task.Type, err) + err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ + "err_msg": err.Error(), + "status": types.VideoStatusFailed, + }).Error + if err != nil { + logger.Errorf("update task with error: %v", err) + } + continue + } + + logger.Infof("[video] submit success jobId=%d type=%s taskId=%s channel=%s", task.Id, task.Type, r.TaskId, r.Channel) + err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ + "task_id": r.TaskId, + "channel": r.Channel, + "status": types.VideoStatusPending, + }).Error + if err != nil { + logger.Errorf("update task with error: %v", err) + s.PushTask(task) + } + + } + }() +} + +func (s *Service) DownloadFiles() { + go func() { + var items []model.VideoJob + logger.Info("[video] download files started") + for { + err := s.db.Where("status", types.VideoStatusDownloading).Find(&items).Error + if err != nil { + logger.Errorf("get downloading tasks with error: %v", err) + continue + } + + for _, v := range items { + if v.VideoURL == "" { + continue + } + + logger.Infof("try download video: %s", v.VideoURL) + videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.VideoURL, ".mp4", true) + if err != nil { + logger.Errorf("download video with error: %v", err) + continue + } + logger.Infof("download video success: %s", videoURL) + s.db.Model(&model.VideoJob{Id: v.Id}).UpdateColumns(map[string]any{ + "video_url": videoURL, + "status": types.VideoStatusSuccess, + "progress": 100, + }) + } + + time.Sleep(time.Second * 10) + } + }() +} + +// SyncTaskProgress 异步拉取任务 +func (s *Service) SyncTaskProgress() { + go func() { + logger.Info("[video] task status poller started") + var jobs []model.VideoJob + for { + res := s.db.Where("status IN ?", []string{types.VideoStatusInProgress, types.VideoStatusPending}).Where("task_id <> ?", "").Find(&jobs) + if res.Error != nil { + continue + } + if len(jobs) > 0 { + logger.Infof("[video] polling task status, in_progress count=%d", len(jobs)) + } + + for _, job := range jobs { + // 检查任务是否超时(超过 2 小时) + if time.Since(job.CreatedAt) > 2*time.Hour { + logger.Warnf("[video] task timeout jobId=%d taskId=%s created_at=%s", job.Id, job.TaskId, job.CreatedAt.Format(time.RFC3339)) + err := s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]any{ + "status": types.VideoStatusFailed, + "err_msg": "任务超时", + }).Error + if err != nil { + logger.Errorf("[video] update timeout task failed jobId=%d: %v", job.Id, err) + } + continue + } + + modelKey := "" + var videoTask types.VideoTask + if err := json.Unmarshal([]byte(job.Params), &videoTask); err == nil { + if paramsMap, ok := videoTask.Params.(map[string]any); ok { + if model, ok := paramsMap["model"].(string); ok { + modelKey = model + } + } + } + + logger.Debugf("[video] querying task jobId=%d taskId=%s provider=%s", job.Id, job.TaskId, job.Type) + task, err := s.QueryTask(job.Type, job.TaskId, job.Channel, modelKey) + if err != nil { + logger.Errorf("[video] query failed jobId=%d taskId=%s: %v", job.Id, job.TaskId, err) + // 更新任务信息 + s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]any{ + "status": types.VideoStatusFailed, + "err_msg": err.Error(), + }) + continue + } + + logger.Debugf("[video] task status jobId=%d taskId=%s status=%s", job.Id, job.TaskId, task.Status) + logger.Debugf("[video] output=%s", task.Output) + + if task.Status == types.VideoStatusSuccess { + data := map[string]any{ + "status": types.VideoStatusDownloading, + "progress": 100, + "output": task.Output, + } + if task.VideoURL != "" { + data["video_url"] = task.VideoURL + } + err = s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(data).Error + if err != nil { + logger.Errorf("更新数据库失败:%v", err) + continue + } + logger.Infof("[video] task completed jobId=%d taskId=%s", job.Id, job.TaskId) + } else if task.Status == "failed" { + logger.Warnf("[video] task failed jobId=%d taskId=%s err=%s", job.Id, job.TaskId, task.ErrMsg) + s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]any{ + "status": types.VideoStatusFailed, + "err_msg": task.ErrMsg, + }) + } else { + s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]any{ + "status": task.Status, + "progress": task.Progress, + }) + continue + } + + } + + // 找出失败的任务,并恢复其扣减算力 + s.db.Select("id", "user_id", "power", "task_id", "err_msg", "type"). + Where("status", types.VideoStatusFailed).Where("power > ?", 0).Find(&jobs) + for _, job := range jobs { + err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ + Type: types.PowerRefund, + Model: job.Type, + Remark: fmt.Sprintf("%s 任务失败,退回算力。任务ID:%s,Err:%s", job.Type, job.TaskId, job.ErrMsg), + }) + if err != nil { + continue + } + // 更新任务状态 + s.db.Model(&job).UpdateColumn("power", 0) + } + time.Sleep(time.Second * 10) + } + }() +} diff --git a/api/service/wxgzh_service.go b/api/service/wxgzh_service.go new file mode 100644 index 00000000..d8d6cf0e --- /dev/null +++ b/api/service/wxgzh_service.go @@ -0,0 +1,94 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "geekai/core/types" + "geekai/store/model" + "geekai/utils" + "time" + + "gorm.io/gorm" +) + +// WxGzhService 微信公众号服务 +type WxGzhService struct { + config types.WxGzhConfig + DB *gorm.DB +} + +func (s *WxGzhService) UpdateConfig(config types.WxGzhConfig) { + s.config = config +} + +func (s *WxGzhService) GetConfig() types.WxGzhConfig { + return s.config +} + +func (s *WxGzhService) SetConfig(config types.WxGzhConfig) { + s.config = config +} + +func NewWxGzhService(config *types.SystemConfig, db *gorm.DB) *WxGzhService { + return &WxGzhService{config: config.WxGzh, DB: db} +} + +// GetOpenIDByCode 根据 code 获取 openid 和 access_token +func (s *WxGzhService) GetOpenIDByCode(code string) (string, string, error) { + + var config model.Config + s.DB.Where("name", types.ConfigKeyWxGzh).First(&config) + + var value map[string]any + err := utils.JsonDecode(config.Value, &value) + + url := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", + value["app_id"], value["secret"], code) + + body, status, err := utils.FetchURLBytes(context.Background(), url, "", 30*time.Second, 2, 2<<20) + if err != nil { + return "", "", fmt.Errorf("wx get openid failed: status=%d: %w", status, err) + } + + var result map[string]interface{} + err = json.Unmarshal(body, &result) + if err != nil { + return "", "", err + } + + if openID, ok := result["openid"].(string); ok { + if accessToken, ok := result["access_token"].(string); ok { + return openID, accessToken, nil + } + } + + if errMsg, ok := result["errmsg"].(string); ok { + return "", "", fmt.Errorf("微信 API 错误: %s", errMsg) + } + + return "", "", fmt.Errorf("获取 openid 和 access_token 失败: %s", string(body)) +} + +// GetUserInfo 获取微信用户昵称和头像 +func (s *WxGzhService) GetUserInfo(accessToken string, openID string) (map[string]any, error) { + url := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN", + accessToken, openID) + + body, status, err := utils.FetchURLBytes(context.Background(), url, "", 30*time.Second, 2, 2<<20) + if err != nil { + return nil, fmt.Errorf("wx get userinfo failed: status=%d: %w", status, err) + } + + var result map[string]any + err = json.Unmarshal(body, &result) + if err != nil { + return nil, err + } + + if errMsg, ok := result["errmsg"].(string); ok { + return nil, fmt.Errorf("微信 API 错误: %s", errMsg) + } + + return result, nil +} diff --git a/api/store/model/chat_app.go b/api/store/model/chat_app.go index 9336aca4..1df9b2f7 100644 --- a/api/store/model/chat_app.go +++ b/api/store/model/chat_app.go @@ -5,18 +5,18 @@ import ( ) type ChatApp struct { - Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - Name string `gorm:"column:name;type:varchar(30);not null;comment:角色名称" json:"name"` - Tid uint `gorm:"column:tid;type:int(11);not null;comment:分类ID" json:"tid"` - Key string `gorm:"column:marker;type:varchar(30);uniqueIndex;not null;comment:角色标识" json:"marker"` - Context string `gorm:"column:context_json;type:text;not null;comment:角色语料 json" json:"context_json"` - HelloMsg string `gorm:"column:hello_msg;type:varchar(255);not null;comment:打招呼信息" json:"hello_msg"` - Icon string `gorm:"column:icon;type:varchar(255);not null;comment:角色图标" json:"icon"` - Enable bool `gorm:"column:enable;type:tinyint(1);not null;comment:是否被启用" json:"enable"` - SortNum int `gorm:"column:sort_num;type:smallint;not null;default:0;comment:角色排序" json:"sort_num"` - ModelId uint `gorm:"column:model_id;type:int(11);not null;default:0;comment:绑定模型ID" json:"model_id"` - CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"` + Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + Name string `gorm:"column:name;type:varchar(30);not null;comment:角色名称" json:"name"` + Tid uint `gorm:"column:tid;type:int(11);not null;comment:分类ID" json:"tid"` + UserId uint `gorm:"column:user_id;type:int(11);not null;default:0;comment:所属用户ID,为 0 表示系统内置" json:"user_id"` + SystemPrompt string `gorm:"column:system_prompt;type:text;not null;comment:系统提示词" json:"system_prompt"` + HelloMsg string `gorm:"column:hello_msg;type:varchar(255);not null;comment:打招呼信息" json:"hello_msg"` + Icon string `gorm:"column:icon;type:varchar(255);not null;comment:角色图标" json:"icon"` + Enable bool `gorm:"column:enable;type:tinyint(1);not null;comment:是否被启用" json:"enable"` + SortNum int `gorm:"column:sort_num;type:smallint;not null;default:0;comment:角色排序" json:"sort_num"` + ModelId uint `gorm:"column:model_id;type:int(11);not null;default:0;comment:绑定模型ID" json:"model_id"` + CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"` } func (m *ChatApp) TableName() string { diff --git a/api/store/model/dalle_job.go b/api/store/model/dalle_job.go deleted file mode 100644 index 0120f50f..00000000 --- a/api/store/model/dalle_job.go +++ /dev/null @@ -1,21 +0,0 @@ -package model - -import "time" - -type DallJob struct { - Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户ID" json:"user_id"` - Prompt string `gorm:"column:prompt;type:text;not null;comment:提示词" json:"prompt"` - TaskInfo string `gorm:"column:task_info;type:text;not null;comment:任务详情" json:"task_info"` - ImgURL string `gorm:"column:img_url;type:varchar(255);not null;comment:图片地址" json:"img_url"` - OrgURL string `gorm:"column:org_url;type:varchar(1024);comment:原图地址" json:"org_url"` - Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"` - Power int `gorm:"column:power;type:smallint;not null;comment:消耗算力" json:"power"` - Progress int `gorm:"column:progress;type:smallint;not null;comment:任务进度" json:"progress"` - ErrMsg string `gorm:"column:err_msg;type:varchar(1024);not null;comment:错误信息" json:"err_msg"` - CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` -} - -func (m *DallJob) TableName() string { - return "geekai_dall_jobs" -} diff --git a/api/store/model/image_job.go b/api/store/model/image_job.go new file mode 100644 index 00000000..5a785757 --- /dev/null +++ b/api/store/model/image_job.go @@ -0,0 +1,22 @@ +package model + +import "time" + +type ImageJob struct { + Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户ID" json:"user_id"` + Prompt string `gorm:"column:prompt;type:text;not null;comment:提示词" json:"prompt"` + Params string `gorm:"column:params;type:text;not null;comment:任务参数" json:"params"` + TaskId string `gorm:"column:task_id;type:varchar(64);comment:Kapon 异步任务 ID" json:"task_id"` + ImgURL string `gorm:"column:img_url;type:varchar(255);not null;comment:图片地址" json:"img_url"` + OrgURL string `gorm:"column:org_url;type:varchar(1024);comment:原图地址" json:"org_url"` + Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"` + Power int `gorm:"column:power;type:smallint;not null;comment:消耗算力" json:"power"` + Progress int `gorm:"column:progress;type:smallint;not null;comment:任务进度" json:"progress"` + ErrMsg string `gorm:"column:err_msg;type:varchar(1024);not null;comment:错误信息" json:"err_msg"` + CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` +} + +func (m *ImageJob) TableName() string { + return "geekai_image_jobs" +} diff --git a/api/store/model/ppt_job.go b/api/store/model/ppt_job.go new file mode 100644 index 00000000..07465b67 --- /dev/null +++ b/api/store/model/ppt_job.go @@ -0,0 +1,29 @@ +package model + +import ( + "geekai/store/vo" + "time" +) + +// PPTJob PPT 生成任务(持久化到数据库) +type PPTJob struct { + Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + TaskId string `gorm:"column:task_id;type:varchar(64);uniqueIndex;not null;comment:任务 ID" json:"task_id"` + UserId uint `gorm:"column:user_id;type:int(11);not null;index;comment:用户 ID" json:"user_id"` + Status string `gorm:"column:status;type:varchar(32);not null;default:pending;comment:任务状态 pending,processing,completed,failed" json:"status"` + ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"` + Prompt string `gorm:"column:prompt;type:text;comment:用户补充提示" json:"prompt"` + Title string `gorm:"column:title;type:varchar(256);default:'';comment:列表展示标题(LLM)" json:"title"` + Thumb string `gorm:"column:thumb;type:varchar(1024);default:'';comment:列表缩略图 URL" json:"thumb"` + Content string `gorm:"column:content;type:text;not null;comment:PPT 内容大纲" json:"content"` + Params vo.PPTParams `gorm:"column:params;type:text;comment:生成参数 JSON(语言,生成模式,页数)" json:"params"` + Slides vo.PPTSlides `gorm:"column:slides;type:text;comment:生成的幻灯片列表 JSON" json:"slides"` + TotalSlides int `gorm:"column:total_slides;type:int(11);default:0;comment:总页数" json:"total_slides"` + CompletedSlides int `gorm:"column:completed_slides;type:int(11);default:0;comment:已完成页数" json:"completed_slides"` + CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"` +} + +func (m *PPTJob) TableName() string { + return "geekai_ppt_jobs" +} diff --git a/api/store/model/sd_job.go b/api/store/model/sd_job.go deleted file mode 100644 index 5d16a244..00000000 --- a/api/store/model/sd_job.go +++ /dev/null @@ -1,23 +0,0 @@ -package model - -import "time" - -type SdJob struct { - Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户 ID" json:"user_id"` - Type string `gorm:"column:type;type:varchar(20);default:txt2img;comment:任务类别" json:"type"` - TaskId string `gorm:"column:task_id;type:char(30);uniqueIndex;not null;comment:任务 ID" json:"task_id"` - TaskInfo string `gorm:"column:task_info;type:text;not null;comment:任务详情" json:"task_info"` - Prompt string `gorm:"column:prompt;type:text;not null;comment:会话提示词" json:"prompt"` - ImgURL string `gorm:"column:img_url;type:varchar(255);comment:图片URL" json:"img_url"` - Params string `gorm:"column:params;type:text;comment:绘画参数json" json:"params"` - Progress int `gorm:"column:progress;type:smallint;default:0;comment:任务进度" json:"progress"` - Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"` - ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"` - Power int `gorm:"column:power;type:smallint;not null;default:0;comment:消耗算力" json:"power"` - CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` -} - -func (m *SdJob) TableName() string { - return "geekai_sd_jobs" -} diff --git a/api/store/model/suno_job.go b/api/store/model/suno_job.go index 583823d2..6110ced3 100644 --- a/api/store/model/suno_job.go +++ b/api/store/model/suno_job.go @@ -1,33 +1,32 @@ package model -import "time" +import ( + "geekai/store/vo" + "time" +) type SunoJob struct { - Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"` - Channel string `gorm:"column:channel;type:varchar(100);not null;comment:渠道" json:"channel"` - Title string `gorm:"column:title;type:varchar(100);comment:歌曲标题" json:"title"` - Type int `gorm:"column:type;type:tinyint(1);default:0;comment:任务类型,1:灵感创作,2:自定义创作" json:"type"` - TaskId string `gorm:"column:task_id;type:varchar(50);comment:任务 ID" json:"task_id"` - TaskInfo string `gorm:"column:task_info;type:text;not null;comment:任务详情" json:"task_info"` - RefTaskId string `gorm:"column:ref_task_id;type:char(50);comment:引用任务 ID" json:"ref_task_id"` - Tags string `gorm:"column:tags;type:varchar(255);comment:歌曲风格" json:"tags"` - Instrumental bool `gorm:"column:instrumental;type:tinyint(1);default:0;comment:是否为纯音乐" json:"instrumental"` - ExtendSecs int `gorm:"column:extend_secs;type:smallint;default:0;comment:延长秒数" json:"extend_secs"` - SongId string `gorm:"column:song_id;type:varchar(50);comment:要续写的歌曲 ID" json:"song_id"` - RefSongId string `gorm:"column:ref_song_id;type:varchar(50);not null;comment:引用的歌曲ID" json:"ref_song_id"` - Prompt string `gorm:"column:prompt;type:varchar(2000);not null;comment:提示词" json:"prompt"` - CoverURL string `gorm:"column:cover_url;type:varchar(512);comment:封面图地址" json:"cover_url"` - AudioURL string `gorm:"column:audio_url;type:varchar(512);comment:音频地址" json:"audio_url"` - ModelName string `gorm:"column:model_name;type:varchar(30);comment:模型地址" json:"model_name"` - Progress int `gorm:"column:progress;type:smallint;default:0;comment:任务进度" json:"progress"` - Duration int `gorm:"column:duration;type:smallint;not null;default:0;comment:歌曲时长" json:"duration"` - Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"` - ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"` - RawData string `gorm:"column:raw_data;type:text;comment:原始数据" json:"raw_data"` - Power int `gorm:"column:power;type:smallint;not null;default:0;comment:消耗算力" json:"power"` - PlayTimes int `gorm:"column:play_times;type:int;comment:播放次数" json:"play_times"` - CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` + Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"` + Channel string `gorm:"column:channel;type:varchar(100);not null;comment:渠道" json:"channel"` + Title string `gorm:"column:title;type:varchar(100);comment:歌曲标题" json:"title"` + Type int `gorm:"column:type;type:tinyint(1);default:0;comment:任务类型,1:灵感创作,2:自定义创作" json:"type"` + TaskId string `gorm:"column:task_id;type:varchar(50);comment:任务 ID" json:"task_id"` + Params vo.SunoParam `gorm:"column:params;type:text;comment:任务参数" json:"params"` + RefTaskId string `gorm:"column:ref_task_id;type:char(50);comment:引用任务 ID" json:"ref_task_id"` + SongId string `gorm:"column:song_id;type:varchar(50);comment:要续写的歌曲 ID" json:"song_id"` + RefSongId string `gorm:"column:ref_song_id;type:varchar(50);not null;comment:引用的歌曲ID" json:"ref_song_id"` + Prompt string `gorm:"column:prompt;type:varchar(2000);not null;comment:提示词" json:"prompt"` + CoverURL string `gorm:"column:cover_url;type:varchar(512);comment:封面图地址" json:"cover_url"` + AudioURL string `gorm:"column:audio_url;type:varchar(512);comment:音频地址" json:"audio_url"` + Progress int `gorm:"column:progress;type:smallint;default:0;comment:任务进度" json:"progress"` + Duration int `gorm:"column:duration;type:smallint;not null;default:0;comment:歌曲时长" json:"duration"` + Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"` + ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"` + Output string `gorm:"column:output;type:text;comment:原始输出数据" json:"output"` + Power int `gorm:"column:power;type:smallint;not null;default:0;comment:消耗算力" json:"power"` + PlayTimes int `gorm:"column:play_times;type:int;comment:播放次数" json:"play_times"` + CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` } func (m *SunoJob) TableName() string { diff --git a/api/store/model/user.go b/api/store/model/user.go index d407c497..ecea442a 100644 --- a/api/store/model/user.go +++ b/api/store/model/user.go @@ -17,13 +17,14 @@ type User struct { ExpiredTime int64 `gorm:"column:expired_time;type:int;not null;comment:用户过期时间" json:"expired_time"` Status bool `gorm:"column:status;type:tinyint(1);not null;comment:当前状态" json:"status"` ChatConfig string `gorm:"column:chat_config_json;type:text;default:null;comment:聊天配置json" json:"chat_config"` - ChatRoles string `gorm:"column:chat_roles_json;type:text;default:null;comment:聊天角色 json" json:"chat_roles"` + ChatRoles string `gorm:"column:chat_roles_json;type:text;default:null;comment:聊天角色 json" json:"-"` ChatModels string `gorm:"column:chat_models_json;type:text;default:null;comment:AI模型 json" json:"chat_models"` LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"` Vip bool `gorm:"column:vip;type:tinyint(1);not null;default:0;comment:是否会员" json:"vip"` LastLoginIp string `gorm:"column:last_login_ip;type:char(32);not null;comment:最后登录 IP" json:"last_login_ip"` OpenId string `gorm:"column:openid;type:varchar(100);comment:第三方登录账号ID" json:"openid"` Platform string `gorm:"column:platform;type:varchar(30);comment:登录平台" json:"platform"` + GemIds string `gorm:"column:gem_ids_json;type:text;default:null;comment:用户固定的智能体ID列表(JSON数组)" json:"gem_ids"` CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"` } diff --git a/api/store/model/video_job.go b/api/store/model/video_job.go index 7fe8728b..9afe8bc1 100644 --- a/api/store/model/video_job.go +++ b/api/store/model/video_job.go @@ -7,17 +7,15 @@ type VideoJob struct { UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户 ID" json:"user_id"` Channel string `gorm:"column:channel;type:varchar(100);not null;comment:渠道" json:"channel"` TaskId string `gorm:"column:task_id;type:varchar(100);not null;comment:任务 ID" json:"task_id"` - TaskInfo string `gorm:"column:task_info;type:text;comment:原始任务信息" json:"task_info"` + Params string `gorm:"column:params;type:text;comment:视频任务参数 JSON" json:"params"` Type string `gorm:"column:type;type:varchar(20);comment:任务类型,luma,runway,cogvideo" json:"type"` Prompt string `gorm:"column:prompt;type:text;not null;comment:提示词" json:"prompt"` - PromptExt string `gorm:"column:prompt_ext;type:text;comment:优化后提示词" json:"prompt_ext"` - CoverURL string `gorm:"column:cover_url;type:varchar(512);comment:封面图地址" json:"cover_url"` - VideoURL string `gorm:"column:video_url;type:varchar(512);comment:视频地址" json:"video_url"` - WaterURL string `gorm:"column:water_url;type:varchar(512);comment:带水印的视频地址" json:"water_url"` - Progress int `gorm:"column:progress;type:smallint;default:0;comment:任务进度" json:"progress"` + VideoURL string `gorm:"column:video_url;type:varchar(2000);not null;comment:视频地址" json:"video_url"` + Status string `gorm:"column:status;type:varchar(20);default:pending;comment:任务状态:pending,in_progress,downloading,success,failed" json:"status"` + Progress int `gorm:"column:progress;type:smallint;default:0;comment:任务进度(0-100)" json:"progress"` Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"` ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"` - RawData string `gorm:"column:raw_data;type:text;comment:原始数据" json:"raw_data"` + Output string `gorm:"column:output;type:text;comment:任务输出的原始信息" json:"output"` Power int `gorm:"column:power;type:smallint;not null;default:0;comment:消耗算力" json:"power"` CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"` } diff --git a/api/store/mysql.go b/api/store/mysql.go index 95cd9f55..8e81d2b3 100644 --- a/api/store/mysql.go +++ b/api/store/mysql.go @@ -10,20 +10,20 @@ package store import ( "fmt" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "time" "gorm.io/driver/mysql" "gorm.io/gorm" - "gorm.io/gorm/logger" + logger2 "gorm.io/gorm/logger" "gorm.io/gorm/schema" ) -var log = logger2.GetLogger() +var logger = log.GetLogger() func NewGormConfig() *gorm.Config { return &gorm.Config{ - Logger: logger.Default.LogMode(logger.Warn), + Logger: logger2.Default.LogMode(logger2.Warn), NamingStrategy: schema.NamingStrategy{ TablePrefix: "geekai_", // 设置表前缀 SingularTable: false, // 使用单数表名形式 @@ -46,7 +46,7 @@ func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) sqlDB.SetMaxOpenConns(512) sqlDB.SetConnMaxLifetime(time.Hour) - log.Info("开始重命名数据表...") + logger.Info("开始重命名数据表...") // 重命名数据表 tableRenames := map[string]string{ @@ -61,12 +61,12 @@ func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) "chatgpt_orders": "geekai_orders", "chatgpt_products": "geekai_products", "chatgpt_configs": "geekai_configs", - "chatgpt_sd_jobs": "geekai_sd_jobs", "chatgpt_mj_jobs": "geekai_mj_jobs", "chatgpt_suno_jobs": "geekai_suno_jobs", "chatgpt_dall_jobs": "geekai_dall_jobs", "chatgpt_video_jobs": "geekai_video_jobs", "chatgpt_jimeng_jobs": "geekai_jimeng_jobs", + "chatgpt_ppt_jobs": "geekai_ppt_jobs", "chatgpt_files": "geekai_files", "chatgpt_menus": "geekai_menus", "chatgpt_functions": "geekai_functions", @@ -83,9 +83,9 @@ func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) if !db.Migrator().HasTable(newTableName) { err := db.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s", oldTableName, newTableName)).Error if err != nil { - log.Errorf("重命名数据表 %s 到 %s 失败: %v", oldTableName, newTableName, err) + logger.Errorf("重命名数据表 %s 到 %s 失败: %v", oldTableName, newTableName, err) } else { - log.Infof("成功重命名数据表: %s -> %s", oldTableName, newTableName) + logger.Infof("成功重命名数据表: %s -> %s", oldTableName, newTableName) } } } diff --git a/api/store/vo/chat_app.go b/api/store/vo/chat_app.go index 12311bcb..747353c2 100644 --- a/api/store/vo/chat_app.go +++ b/api/store/vo/chat_app.go @@ -1,18 +1,16 @@ package vo -import "geekai/core/types" - type ChatApp struct { BaseVo - Key string `json:"key"` // 角色唯一标识 - Tid uint `json:"tid"` - Name string `json:"name"` // 角色名称 - Context []types.Message `json:"context"` // 角色语料信息 - HelloMsg string `json:"hello_msg"` // 打招呼的消息 - Icon string `json:"icon"` // 角色聊天图标 - Enable bool `json:"enable"` // 是否启用被启用 - SortNum int `json:"sort"` // 排序 - ModelId uint `json:"model_id"` // 绑定模型 ID - ModelName string `json:"model_name"` // 模型名称 - TypeName string `json:"type_name"` // 分类名称 + Tid uint `json:"tid"` + Name string `json:"name"` // 角色名称 + UserId uint `json:"user_id"` // 所属用户 ID,0 表示系统内置 + SystemPrompt string `json:"system_prompt,omitempty"` // 系统提示词(列表接口对内置智能体不下发) + HelloMsg string `json:"hello_msg"` // 打招呼的消息 + Icon string `json:"icon"` // 角色聊天图标 + Enable bool `json:"enable"` // 是否启用 + SortNum int `json:"sort"` // 排序 + ModelId uint `json:"model_id"` // 绑定模型 ID + ModelName string `json:"model_name"` // 模型名称 + TypeName string `json:"type_name"` // 分类名称 } diff --git a/api/store/vo/dalle_job.go b/api/store/vo/dalle_job.go deleted file mode 100644 index 28a6906d..00000000 --- a/api/store/vo/dalle_job.go +++ /dev/null @@ -1,14 +0,0 @@ -package vo - -type DallJob struct { - Id uint `json:"id"` - UserId int `json:"user_id"` - Prompt string `json:"prompt"` - ImgURL string `json:"img_url"` - OrgURL string `json:"org_url"` - Publish bool `json:"publish"` - Power int `json:"power"` - Progress int `json:"progress"` - ErrMsg string `json:"err_msg"` - CreatedAt int64 `json:"created_at"` -} diff --git a/api/store/vo/image_job.go b/api/store/vo/image_job.go new file mode 100644 index 00000000..3959d694 --- /dev/null +++ b/api/store/vo/image_job.go @@ -0,0 +1,15 @@ +package vo + +type ImageJob struct { + Id uint `json:"id"` + UserId int `json:"user_id"` + Prompt string `json:"prompt"` + Params string `json:"params"` + ImgURL string `json:"img_url"` + OrgURL string `json:"org_url"` + Publish bool `json:"publish"` + Power int `json:"power"` + Progress int `json:"progress"` + ErrMsg string `json:"err_msg"` + CreatedAt int64 `json:"created_at"` +} diff --git a/api/store/vo/ppt_job.go b/api/store/vo/ppt_job.go new file mode 100644 index 00000000..bd2982c7 --- /dev/null +++ b/api/store/vo/ppt_job.go @@ -0,0 +1,110 @@ +package vo + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "fmt" +) + +// PPTSlideImageVersion 单页配图历史中的一条(图片地址 + 文生图/图生图提示) +type PPTSlideImageVersion struct { + ImageURL string `json:"image_url"` + Prompt string `json:"prompt"` +} + +// PPTSlideImageVersions 存库 JSON 数组;UnmarshalJSON 兼容旧版仅字符串数组 +type PPTSlideImageVersions []PPTSlideImageVersion + +// UnmarshalJSON 兼容 ["url1","url2"] 与 [{"image_url":"...","prompt":"..."}] +func (v *PPTSlideImageVersions) UnmarshalJSON(data []byte) error { + if len(data) == 0 || string(data) == "null" { + *v = nil + return nil + } + var raw []json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + out := make([]PPTSlideImageVersion, 0, len(raw)) + for _, item := range raw { + item = bytes.TrimSpace(item) + if len(item) == 0 || string(item) == "null" { + continue + } + if item[0] == '"' { + var s string + if err := json.Unmarshal(item, &s); err != nil { + return err + } + out = append(out, PPTSlideImageVersion{ImageURL: s, Prompt: ""}) + continue + } + var o PPTSlideImageVersion + if err := json.Unmarshal(item, &o); err != nil { + return err + } + out = append(out, o) + } + *v = out + return nil +} + +// PPTParams 生成参数(语言、生成模式、页数) +type PPTParams struct { + Language string `json:"language"` // 输出语言,如 zh-CN, en + Mode string `json:"mode"` // 生成模式:detailed | slides + Pages int `json:"pages"` // 目标页数 +} + +// Value 实现 driver.Valuer,序列化为 JSON 存储 +func (p PPTParams) Value() (driver.Value, error) { + return json.Marshal(p) +} + +// Scan 实现 sql.Scanner,从数据库读取 JSON +func (p *PPTParams) Scan(value any) error { + if value == nil { + return nil + } + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("failed to unmarshal PPTParams value: %v", value) + } + return json.Unmarshal(bytes, p) +} + +// PPTSlideData 单页幻灯片数据(与 service/ppt SlideData 一致) +type PPTSlideData struct { + SlideIndex int `json:"slide_index"` + Theme string `json:"theme"` + Title string `json:"title"` + Points []string `json:"points"` + ImagePrompt string `json:"image_prompt"` + ImageURL string `json:"image_url"` + ImageHistory PPTSlideImageVersions `json:"image_history,omitempty"` +} + +// PPTSlides 幻灯片列表(JSON 数组) +type PPTSlides []PPTSlideData + +// Value 实现 driver.Valuer +func (s PPTSlides) Value() (driver.Value, error) { + if len(s) == 0 { + return "[]", nil + } + return json.Marshal(s) +} + +// Scan 实现 sql.Scanner +func (s *PPTSlides) Scan(value any) error { + if value == nil { + *s = nil + return nil + } + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("failed to unmarshal PPTSlides value: %v", value) + } + return json.Unmarshal(bytes, s) +} diff --git a/api/store/vo/sd_job.go b/api/store/vo/sd_job.go deleted file mode 100644 index 861d9712..00000000 --- a/api/store/vo/sd_job.go +++ /dev/null @@ -1,20 +0,0 @@ -package vo - -import ( - "geekai/core/types" -) - -type SdJob struct { - Id uint `json:"id"` - Type string `json:"type"` - UserId uint `json:"user_id"` - TaskId string `json:"task_id"` - ImgURL string `json:"img_url"` - Params types.SdTaskParams `json:"params"` - Progress int `json:"progress"` - Prompt string `json:"prompt"` - Publish bool `json:"publish"` - ErrMsg string `json:"err_msg"` - Power int `json:"power"` - CreatedAt int64 `json:"created_at"` -} diff --git a/api/store/vo/suno_job.go b/api/store/vo/suno_job.go index 3827d358..5258620c 100644 --- a/api/store/vo/suno_job.go +++ b/api/store/vo/suno_job.go @@ -1,5 +1,41 @@ package vo +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +// SunoParam Suno 任务参数 +type SunoParam struct { + Prompt string `json:"prompt"` // 提示词 + Instrumental bool `json:"instrumental"` // 是否为纯音乐 + Tags string `json:"tags"` // 歌曲风格和标签 + ExtendSecs int `json:"extend_secs"` // 续写秒数 + Lyrics string `json:"lyrics,omitempty"` // 歌词(可选) + Model string `json:"model"` // 模型名称 +} + +// Value 实现 driver.Valuer 接口,用于将 SunoParam 序列化为 JSON 字符串存储到数据库 +func (p SunoParam) Value() (driver.Value, error) { + if p.Prompt == "" && !p.Instrumental && p.Tags == "" && p.ExtendSecs == 0 && p.Lyrics == "" && p.Model == "" { + return nil, nil + } + return json.Marshal(p) +} + +// Scan 实现 sql.Scanner 接口,用于从数据库读取 JSON 字符串并反序列化为 SunoParam +func (p *SunoParam) Scan(value any) error { + if value == nil { + return nil + } + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("failed to unmarshal SunoParam value: %v", value) + } + return json.Unmarshal(bytes, p) +} + type SunoJob struct { Id uint `json:"id"` UserId uint `json:"user_id"` @@ -21,7 +57,8 @@ type SunoJob struct { Duration int `json:"duration"` // 银屏时长,秒 Publish bool `json:"publish"` // 是否发布 ErrMsg string `json:"err_msg"` // 错误信息 - RawData map[string]interface{} `json:"raw_data"` // 原始数据 json + Output map[string]interface{} `json:"output"` // 原始输出数据 json + Params SunoParam `json:"params"` // 任务参数 Power int `json:"power"` // 消耗算力 RefSong map[string]interface{} `json:"ref_song,omitempty"` User map[string]interface{} `json:"user,omitempty"` //关联用户信息 diff --git a/api/store/vo/user.go b/api/store/vo/user.go index 06d9d55d..55db9ab1 100644 --- a/api/store/vo/user.go +++ b/api/store/vo/user.go @@ -2,20 +2,20 @@ package vo type User struct { BaseVo - Username string `json:"username"` - Nickname string `json:"nickname"` - Mobile string `json:"mobile"` - Email string `json:"email"` - Avatar string `json:"avatar"` - Salt string `json:"salt"` // 密码盐 - Power int `json:"power"` // 剩余算力 - ChatRoles []string `json:"chat_roles"` // 聊天角色集合 - ChatModels []int `json:"chat_models"` // AI模型集合 - ExpiredTime int64 `json:"expired_time"` // 账户到期时间 - Status bool `json:"status"` // 当前状态 - LastLoginAt int64 `json:"last_login_at"` // 最后登录时间 - LastLoginIp string `json:"last_login_ip"` // 最后登录 IP - Vip bool `json:"vip"` - OpenId string `json:"openid"` // 第三方登录 OpenID - Platform string `json:"platform"` // 第三方登录平台 + Username string `json:"username"` + Nickname string `json:"nickname"` + Mobile string `json:"mobile"` + Email string `json:"email"` + Avatar string `json:"avatar"` + Salt string `json:"salt"` // 密码盐 + Power int `json:"power"` // 剩余算力 + ChatModels []int `json:"chat_models"` // AI模型集合 + ChatRoles []uint `json:"chat_roles"` // 工作区应用 ID 列表 + ExpiredTime int64 `json:"expired_time"` // 账户到期时间 + Status bool `json:"status"` // 当前状态 + LastLoginAt int64 `json:"last_login_at"` + LastLoginIp string `json:"last_login_ip"` + Vip bool `json:"vip"` + OpenId string `json:"openid"` // 第三方登录 OpenID + Platform string `json:"platform"` // 第三方登录平台 } diff --git a/api/store/vo/video_job.go b/api/store/vo/video_job.go index 8ca12291..bc869af1 100644 --- a/api/store/vo/video_job.go +++ b/api/store/vo/video_job.go @@ -1,20 +1,19 @@ package vo type VideoJob struct { - Id uint `json:"id"` - UserId uint `json:"user_id"` - Channel string `json:"channel"` - Type string `json:"type"` - TaskId string `json:"task_id"` - Prompt string `json:"prompt"` // 提示词 - PromptExt string `json:"prompt_ext"` // 提示词 - CoverURL string `json:"cover_url"` // 封面图 URL - VideoURL string `json:"video_url"` // 无水印视频 URL - WaterURL string `json:"water_url"` // 有水印视频 URL - Progress int `json:"progress"` // 任务进度 - Publish bool `json:"publish"` // 是否发布 - ErrMsg string `json:"err_msg"` // 错误信息 - RawData map[string]interface{} `json:"raw_data"` // 原始数据 json - Power int `json:"power"` // 消耗算力 - CreatedAt int64 `json:"created_at"` + Id uint `json:"id"` + UserId uint `json:"user_id"` + Channel string `json:"channel"` + Type string `json:"type"` + TaskId string `json:"task_id"` + Prompt string `json:"prompt"` // 提示词 + VideoURL string `json:"video_url"` // 视频 URL + Status string `json:"status"` // 任务状态 + Progress int `json:"progress"` // 任务进度(0-100) + Publish bool `json:"publish"` // 是否发布 + ErrMsg string `json:"err_msg"` // 错误信息 + Params map[string]any `json:"params"` // 任务参数 json(用于前端显示) + Output map[string]any `json:"output"` // 任务输出信息 json(管理后台使用) + Power int `json:"power"` // 消耗算力 + CreatedAt int64 `json:"created_at"` } diff --git a/api/test/test.go b/api/test/test.go new file mode 100644 index 00000000..3adae1eb --- /dev/null +++ b/api/test/test.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "geekai/service/sora" +) + +func main() { + text := ` **Analyzing input prompt** > ***- The input provides a JSON with an empty history and a prompt in Chinese: "女孩在雪山上滑雪,欢快的节奏,姿势优雅,伴随女孩的欢声笑语".*** > ID: task_01k8pye324ef7t6heq6jyaxbfe >[数据预览](https://asyncdata.net/web/task_01k8pye324ef7t6heq6jyaxbfe) | [原始数据](https://asyncdata.net/source/task_01k8pye324ef7t6heq6jyaxbfe) > 排队中... > 生成中. >🏃‍ 进度 36..47..57..61..69..76..79..81..` + + soraService := sora.NewSoraService(nil) + url, err := soraService.DownloadVideoURL(text) + if err != nil { + fmt.Printf("extract video URL failed: %v", err) + return + } + fmt.Println(url) +} diff --git a/api/utils/common.go b/api/utils/common.go index 18645c2f..7b0da4dd 100644 --- a/api/utils/common.go +++ b/api/utils/common.go @@ -115,7 +115,7 @@ func Ip2Region(searcher *xdb.Searcher, ip string) string { return fmt.Sprintf("%s-%s-%s", arr[0], arr[2], arr[3]) } -func IsEmptyValue(obj interface{}) bool { +func IsEmptyValue(obj any) bool { if obj == nil { return true } diff --git a/api/utils/file.go b/api/utils/file.go index b9bd5006..672eaca5 100644 --- a/api/utils/file.go +++ b/api/utils/file.go @@ -6,9 +6,11 @@ import ( "github.com/microcosm-cc/bluemonday" "io" "net/http" + urlpkg "net/url" "os" "path/filepath" "strings" + "time" "github.com/google/go-tika/tika" ) @@ -84,23 +86,69 @@ func cleanBlankLine(content string) string { // 下载文件 func downloadFile(url string) (string, error) { - base := filepath.Base(url) + u, err := urlpkg.Parse(url) + if err != nil { + return "", err + } + + base := filepath.Base(u.Path) + if base == "" || base == "." || base == "/" { + base = "download" + } dir := os.TempDir() filename := filepath.Join(dir, base) - out, err := os.Create(filename) - if err != nil { - return "", err - } - defer out.Close() - // 获取数据 - resp, err := http.Get(url) - if err != nil { - return "", err - } - defer resp.Body.Close() + client := newHTTPClient(60*time.Second, "") + retries := 2 + var lastErr error + for attempt := 0; attempt <= retries; attempt++ { + // 每次重试都重新创建文件,避免写一半残留 + out, err := os.Create(filename) + if err != nil { + return "", err + } - // 写入数据到文件 - _, err = io.Copy(out, resp.Body) - return filename, err + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + _ = out.Close() + return "", err + } + + resp, err := client.Do(req) + if err != nil { + _ = out.Close() + lastErr = err + _ = os.Remove(filename) + if attempt < retries && isRetryableError(err) { + time.Sleep(retryDelay(attempt)) + continue + } + return "", err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + _ = resp.Body.Close() + _ = out.Close() + _ = os.Remove(filename) + lastErr = fmt.Errorf("download file failed: status=%d", resp.StatusCode) + return "", lastErr + } + + _, copyErr := io.Copy(out, resp.Body) + _ = resp.Body.Close() + _ = out.Close() + if copyErr != nil { + lastErr = copyErr + _ = os.Remove(filename) + if attempt < retries && isRetryableError(copyErr) { + time.Sleep(retryDelay(attempt)) + continue + } + return "", copyErr + } + + return filename, nil + } + + return "", lastErr } diff --git a/api/utils/net.go b/api/utils/net.go index e29c39ca..72591b4a 100644 --- a/api/utils/net.go +++ b/api/utils/net.go @@ -8,17 +8,174 @@ package utils // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "context" "encoding/json" + "errors" "fmt" "geekai/core/types" - logger2 "geekai/logger" + "geekai/log" "io" + "net" "net/http" "net/url" "path/filepath" + "strings" + "time" ) -var logger = logger2.GetLogger() +var logger = log.GetLogger() + +const ( + defaultHTTPTimeout = 45 * time.Second + defaultTLSHandshakeTimeout = 10 * time.Second + defaultDialTimeout = 10 * time.Second + defaultResponseHeaderTimeout = 30 * time.Second + defaultIdleConnTimeout = 90 * time.Second + defaultMaxIdleConns = 100 + defaultMaxIdleConnsPerHost = 10 +) + +func newHTTPClient(timeout time.Duration, proxy string) *http.Client { + if timeout <= 0 { + timeout = defaultHTTPTimeout + } + + var proxyFn func(*http.Request) (*url.URL, error) + if strings.TrimSpace(proxy) != "" { + if proxyURL, err := url.Parse(proxy); err == nil && proxyURL != nil { + proxyFn = http.ProxyURL(proxyURL) + } + } + + transport := &http.Transport{ + Proxy: proxyFn, + DialContext: (&net.Dialer{ + Timeout: defaultDialTimeout, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: defaultTLSHandshakeTimeout, + ResponseHeaderTimeout: defaultResponseHeaderTimeout, + IdleConnTimeout: defaultIdleConnTimeout, + MaxIdleConns: defaultMaxIdleConns, + MaxIdleConnsPerHost: defaultMaxIdleConnsPerHost, + } + + return &http.Client{ + Timeout: timeout, + Transport: transport, + } +} + +func isRetryableError(err error) bool { + if err == nil { + return false + } + + var ne net.Error + if errors.As(err, &ne) { + if ne.Timeout() || ne.Temporary() { + return true + } + } + + // 兼容不同 Go/系统对握手超时的错误文本 + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "tls handshake timeout") || + strings.Contains(msg, "handshake timeout") || + strings.Contains(msg, "connection reset by peer") || + strings.Contains(msg, "connection timed out") +} + +func retryDelay(attempt int) time.Duration { + // attempt 从 0 开始:1s, 2s, 4s... + d := time.Second * time.Duration(1< 8*time.Second { + d = 8 * time.Second + } + return d +} + +func readAllLimit(r io.Reader, maxBytes int64) ([]byte, error) { + if maxBytes > 0 { + return io.ReadAll(io.LimitReader(r, maxBytes)) + } + return io.ReadAll(r) +} + +// FetchURLBytes 发起 GET 并返回响应体(只在 2xx 认为成功)。 +// 为了降低“偶发 HTTPS 握手超时”,对可重试错误/状态码会做有限重试。 +func FetchURLBytes(ctx context.Context, rawURL string, proxy string, timeout time.Duration, retries int, maxBytes int64) ([]byte, int, error) { + if ctx == nil { + ctx = context.Background() + } + if timeout <= 0 { + timeout = defaultHTTPTimeout + } + if retries < 0 { + retries = 0 + } + if maxBytes <= 0 { + maxBytes = 8 << 20 // 默认最多读取 8MiB + } + + var lastErr error + for attempt := 0; attempt <= retries; attempt++ { + if ctx.Err() != nil { + return nil, 0, ctx.Err() + } + + client := newHTTPClient(timeout, proxy) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) + if err != nil { + return nil, 0, err + } + + resp, err := client.Do(req) + if err != nil { + lastErr = err + if attempt < retries && isRetryableError(err) { + time.Sleep(retryDelay(attempt)) + continue + } + return nil, 0, err + } + + body, readErr := readAllLimit(resp.Body, maxBytes) + _ = resp.Body.Close() + if readErr != nil { + lastErr = readErr + if attempt < retries && isRetryableError(readErr) { + time.Sleep(retryDelay(attempt)) + continue + } + return nil, resp.StatusCode, readErr + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + // 429/5xx 可能是短暂错误,允许有限重试 + status := resp.StatusCode + lastErr = fmt.Errorf("request failed: status=%d", status) + if attempt < retries && (status == http.StatusTooManyRequests || status >= 500 && status <= 599) { + time.Sleep(retryDelay(attempt)) + continue + } + + // 避免把大 body 全塞进日志/错误里 + preview := strings.TrimSpace(string(body)) + if len(preview) > 256 { + preview = preview[:256] + } + if preview != "" { + return body, status, fmt.Errorf("request failed: status=%d body=%s", status, preview) + } + return body, status, lastErr + } + + return body, resp.StatusCode, nil + } + + return nil, 0, lastErr +} // SendMsg 回复客户片段端消息 func SendMsg(client *types.WsClient, message types.ReplyMessage) { @@ -54,36 +211,8 @@ func SendChannelMsg(ws *types.WsClient, channel types.WsChannel, message interfa } func DownloadImage(imageURL string, proxy string) ([]byte, error) { - var client *http.Client - if proxy == "" { - client = http.DefaultClient - } else { - proxyURL, _ := url.Parse(proxy) - client = &http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyURL(proxyURL), - }, - } - } - request, err := http.NewRequest("GET", imageURL, nil) - if err != nil { - return nil, err - } - - resp, err := client.Do(request) - if err != nil { - return nil, err - } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(resp.Body) - - imageBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - return imageBytes, nil + body, _, err := FetchURLBytes(context.Background(), imageURL, proxy, defaultHTTPTimeout, 2, 32<<20) + return body, err } func GetBaseURL(strURL string) string { @@ -103,14 +232,26 @@ func GetImgExt(filename string) string { } func GetFileSize(url string) (int64, error) { - resp, err := http.Get(url) + // 优先走 HEAD 获取 Content-Length,减少下载成本;若对方不支持,再 fallback 到 GET 统计字节数。 + client := newHTTPClient(defaultHTTPTimeout, "") + if req, err := http.NewRequest(http.MethodHead, url, nil); err == nil { + if resp, err := client.Do(req); err == nil { + _ = resp.Body.Close() + if resp.StatusCode >= 200 && resp.StatusCode <= 299 && resp.ContentLength > 0 { + return resp.ContentLength, nil + } + } + } + + resp, err := client.Get(url) if err != nil { return 0, err } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return 0, err + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + // 尽量不把大 body 都读进来,只返回状态码 + return 0, fmt.Errorf("get file size failed: status=%d", resp.StatusCode) } - return int64(len(body)), nil + n, err := io.Copy(io.Discard, resp.Body) + return n, err } diff --git a/api/utils/openai.go b/api/utils/openai.go index 1ca7613a..4e5cfa69 100644 --- a/api/utils/openai.go +++ b/api/utils/openai.go @@ -14,6 +14,7 @@ import ( "geekai/store/model" "io" "net/url" + "strings" "time" "github.com/imroc/req/v3" @@ -35,18 +36,63 @@ func CalcTokens(text string, model string) (int, error) { return len(token), nil } +// OpenAIResponse 非流式 chat/completions 响应;content 使用 RawMessage 以兼容 string 与多段式数组。 type OpenAIResponse struct { Model string `json:"model"` Choices []struct { Index int `json:"index"` Message struct { - Role string `json:"role"` - Content string `json:"content"` + Role string `json:"role"` + Content json.RawMessage `json:"content"` } `json:"message"` FinishReason string `json:"finish_reason"` } `json:"choices"` } +// assistantContentPart 兼容 OpenAI 多模态 / 多段文本:{"type":"text","text":"..."} 等。 +type assistantContentPart struct { + Type string `json:"type"` + Text string `json:"text"` + // 少数网关使用 content 字段承载文本 + Inner string `json:"content"` +} + +// NormalizeAssistantContent 将 message.content 规范为纯文本。 +// 兼容:JSON string、null、OpenAI 数组段、以及单段对象。 +func NormalizeAssistantContent(raw json.RawMessage) string { + s := strings.TrimSpace(string(raw)) + if s == "" || s == "null" { + return "" + } + var plain string + if err := json.Unmarshal(raw, &plain); err == nil { + return plain + } + var parts []assistantContentPart + if err := json.Unmarshal(raw, &parts); err == nil && len(parts) > 0 { + var b strings.Builder + for _, p := range parts { + switch { + case p.Text != "": + b.WriteString(p.Text) + case p.Inner != "" && p.Type != "image_url" && p.Type != "image": + b.WriteString(p.Inner) + } + } + return b.String() + } + var one assistantContentPart + if err := json.Unmarshal(raw, &one); err == nil { + if one.Text != "" { + return one.Text + } + if one.Inner != "" { + return one.Inner + } + } + return "" +} + func OpenAIRequest(db *gorm.DB, prompt string, modelId int) (string, error) { messages := make([]any, 1) messages[0] = types.Message{ @@ -86,12 +132,16 @@ func SendOpenAIMessage(db *gorm.DB, messages []any, modelId int) (string, error) apiURL = apiKey.ApiURL } logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiURL, apiKey.ApiURL, apiKey.ProxyURL, chatModel.Name) - r, err := client.R().SetHeader("Body-Type", "application/json"). + r, err := client.R(). + SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bearer "+apiKey.Value). SetBody(types.ApiRequest{ Model: chatModel.Value, Temperature: 0.9, - MaxTokens: 1024, + // gpt-5/o 系模型在复杂提示下可能先消耗大量 token 于推理过程, + // 1024 容易导致 finish_reason=length 且 content 为空。 + MaxTokens: 4096, + MaxCompletionTokens: 4096, Stream: false, Messages: messages, }).Post(apiURL) @@ -108,9 +158,17 @@ func SendOpenAIMessage(db *gorm.DB, messages []any, modelId int) (string, error) if err != nil { return "", fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) } + if len(response.Choices) == 0 { + return "", fmt.Errorf("模型返回 choices 为空:%s", string(body)) + } + + out := strings.TrimSpace(NormalizeAssistantContent(response.Choices[0].Message.Content)) + if out == "" { + return "", fmt.Errorf("模型返回内容为空,请重试或更换模型(若使用推理模型,请确认网关已返回 content 或 reasoning_content)") + } // 更新 API KEY 的最后使用时间 db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix()) - return response.Choices[0].Message.Content, nil + return out, nil } diff --git a/database/archive/geekai_plus-v4.2.3.sql b/database/archive/geekai_plus-v4.2.3.sql index b499a98f..dce31ee7 100644 --- a/database/archive/geekai_plus-v4.2.3.sql +++ b/database/archive/geekai_plus-v4.2.3.sql @@ -20,7 +20,7 @@ SET time_zone = "+00:00"; -- -- 数据库: `geekai_plus` -- -CREATE DATABASE IF NOT EXISTS `geekai_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; +CREATE DATABASE IF NOT EXISTS `geekai_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `geekai_plus`; -- -------------------------------------------------------- @@ -32,15 +32,15 @@ USE `geekai_plus`; DROP TABLE IF EXISTS `chatgpt_admin_users`; CREATE TABLE `chatgpt_admin_users` ( `id` int NOT NULL, - `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名', - `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码', - `salt` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码盐', + `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码', + `salt` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码盐', `status` tinyint(1) NOT NULL COMMENT '当前状态', `last_login_at` bigint NOT NULL COMMENT '最后登录时间', - `last_login_ip` char(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '最后登录 IP', + `last_login_ip` char(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '最后登录 IP', `created_at` datetime NOT NULL COMMENT '创建时间', `updated_at` datetime NOT NULL COMMENT '更新时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户' ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统用户' ROW_FORMAT=DYNAMIC; -- -- 转存表中的数据 `chatgpt_admin_users` @@ -59,7 +59,7 @@ DROP TABLE IF EXISTS `chatgpt_api_keys`; CREATE TABLE `chatgpt_api_keys` ( `id` int NOT NULL, `name` varchar(30) DEFAULT NULL COMMENT '名称', - `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'API KEY value', + `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'API KEY value', `type` varchar(10) NOT NULL DEFAULT 'chat' COMMENT '用途(chat=>聊天,img=>图片)', `last_used_at` bigint NOT NULL COMMENT '最后使用时间', `api_url` varchar(255) DEFAULT NULL COMMENT 'API 地址', @@ -67,7 +67,7 @@ CREATE TABLE `chatgpt_api_keys` ( `proxy_url` varchar(100) DEFAULT NULL COMMENT '代理地址', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API '; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='OpenAI API '; -- -------------------------------------------------------- @@ -83,7 +83,7 @@ CREATE TABLE `chatgpt_app_types` ( `sort_num` tinyint NOT NULL COMMENT '排序', `enabled` tinyint(1) NOT NULL COMMENT '是否启用', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='应用分类表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应用分类表'; -- -- 转存表中的数据 `chatgpt_app_types` @@ -108,7 +108,7 @@ CREATE TABLE `chatgpt_chat_history` ( `user_id` bigint NOT NULL COMMENT '用户 ID', `chat_id` char(40) NOT NULL COMMENT '会话 ID', `type` varchar(10) NOT NULL COMMENT '类型:prompt|reply', - `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色图标', + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色图标', `role_id` bigint NOT NULL COMMENT '角色 ID', `model` varchar(255) DEFAULT NULL COMMENT '模型名称', `content` text NOT NULL COMMENT '聊天内容', @@ -118,7 +118,7 @@ CREATE TABLE `chatgpt_chat_history` ( `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `deleted_at` datetime DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天历史记录'; -- -------------------------------------------------------- @@ -138,7 +138,7 @@ CREATE TABLE `chatgpt_chat_items` ( `created_at` datetime NOT NULL COMMENT '创建时间', `updated_at` datetime NOT NULL COMMENT '更新时间', `deleted_at` datetime DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户会话列表'; -- -------------------------------------------------------- @@ -152,8 +152,8 @@ CREATE TABLE `chatgpt_chat_models` ( `description` varchar(1024) NOT NULL DEFAULT '' COMMENT '模型类型描述', `category` varchar(1024) NOT NULL DEFAULT '' COMMENT '模型类别', `type` varchar(10) NOT NULL DEFAULT 'chat' COMMENT '模型类型(chat,img)', - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '模型名称', - `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '模型值', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模型名称', + `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模型值', `sort_num` tinyint(1) NOT NULL COMMENT '排序数字', `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用模型', `power` smallint NOT NULL COMMENT '消耗算力点数', @@ -165,7 +165,7 @@ CREATE TABLE `chatgpt_chat_models` ( `options` text NOT NULL COMMENT '模型自定义选项', `created_at` datetime DEFAULT NULL, `updated_at` datetime DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 模型表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI 模型表'; -- -- 转存表中的数据 `chatgpt_chat_models` @@ -206,7 +206,7 @@ CREATE TABLE `chatgpt_chat_roles` ( `name` varchar(30) NOT NULL COMMENT '角色名称', `tid` bigint NOT NULL COMMENT '分类ID', `marker` varchar(30) NOT NULL COMMENT '角色标识', - `context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色语料 json', + `context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色语料 json', `hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息', `icon` varchar(255) NOT NULL COMMENT '角色图标', `enable` tinyint(1) NOT NULL COMMENT '是否被启用', @@ -214,7 +214,7 @@ CREATE TABLE `chatgpt_chat_roles` ( `model_id` bigint NOT NULL DEFAULT '0' COMMENT '绑定模型ID', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天角色表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天角色表'; -- -- 转存表中的数据 `chatgpt_chat_roles` @@ -252,7 +252,7 @@ CREATE TABLE `chatgpt_configs` ( `id` int NOT NULL, `marker` varchar(20) NOT NULL COMMENT '标识', `config_json` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- 转存表中的数据 `chatgpt_configs` @@ -272,16 +272,16 @@ DROP TABLE IF EXISTS `chatgpt_dall_jobs`; CREATE TABLE `chatgpt_dall_jobs` ( `id` int NOT NULL, `user_id` bigint NOT NULL COMMENT '用户ID', - `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '提示词', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提示词', `task_info` text NOT NULL COMMENT '任务详情', `img_url` varchar(255) NOT NULL COMMENT '图片地址', - `org_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原图地址', + `org_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '原图地址', `publish` tinyint(1) NOT NULL COMMENT '是否发布', `power` smallint NOT NULL COMMENT '消耗算力', `progress` smallint NOT NULL COMMENT '任务进度', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '错误信息', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='DALLE 绘图任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='DALLE 绘图任务表'; -- -------------------------------------------------------- @@ -293,13 +293,13 @@ DROP TABLE IF EXISTS `chatgpt_files`; CREATE TABLE `chatgpt_files` ( `id` int NOT NULL, `user_id` bigint NOT NULL COMMENT '用户 ID', - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件名', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件名', `obj_key` varchar(100) DEFAULT NULL COMMENT '文件标识', `url` varchar(255) NOT NULL COMMENT '文件地址', `ext` varchar(10) NOT NULL COMMENT '文件后缀', `size` bigint NOT NULL DEFAULT '0' COMMENT '文件大小', `created_at` datetime NOT NULL COMMENT '创建时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户文件表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户文件表'; -- -------------------------------------------------------- @@ -311,13 +311,13 @@ DROP TABLE IF EXISTS `chatgpt_functions`; CREATE TABLE `chatgpt_functions` ( `id` int NOT NULL, `name` varchar(30) NOT NULL COMMENT '函数名称', - `label` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '函数标签', + `label` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '函数标签', `description` varchar(255) DEFAULT NULL COMMENT '函数描述', `parameters` text COMMENT '函数参数(JSON)', `token` varchar(255) DEFAULT NULL COMMENT 'API授权token', `action` varchar(255) DEFAULT NULL COMMENT '函数处理 API', `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='函数插件表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='函数插件表'; -- -- 转存表中的数据 `chatgpt_functions` @@ -342,7 +342,7 @@ CREATE TABLE `chatgpt_invite_codes` ( `hits` bigint NOT NULL COMMENT '点击次数', `reg_num` smallint NOT NULL COMMENT '注册数量', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户邀请码'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户邀请码'; -- -------------------------------------------------------- @@ -355,11 +355,11 @@ CREATE TABLE `chatgpt_invite_logs` ( `id` int NOT NULL, `inviter_id` bigint NOT NULL COMMENT '邀请人ID', `user_id` bigint NOT NULL COMMENT '注册用户ID', - `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名', - `invite_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '邀请码', - `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注', + `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `invite_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邀请码', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备注', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='邀请注册日志'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='邀请注册日志'; -- -------------------------------------------------------- @@ -375,7 +375,7 @@ CREATE TABLE `chatgpt_menus` ( `url` varchar(100) NOT NULL COMMENT '地址', `sort_num` smallint NOT NULL COMMENT '排序', `enabled` tinyint(1) NOT NULL COMMENT '是否启用' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='前端菜单表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='前端菜单表'; -- -- 转存表中的数据 `chatgpt_menus` @@ -411,19 +411,19 @@ CREATE TABLE `chatgpt_mj_jobs` ( `task_info` text NOT NULL COMMENT '任务详情', `type` varchar(20) DEFAULT 'image' COMMENT '任务类别', `message_id` char(40) NOT NULL COMMENT '消息 ID', - `channel_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '频道ID', + `channel_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '频道ID', `reference_id` char(40) DEFAULT NULL COMMENT '引用消息 ID', - `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '会话提示词', - `img_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '图片URL', - `org_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原始图片地址', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '会话提示词', + `img_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图片URL', + `org_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '原始图片地址', `hash` varchar(100) DEFAULT NULL COMMENT 'message hash', `progress` smallint DEFAULT '0' COMMENT '任务进度', `use_proxy` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否使用反代', `publish` tinyint(1) NOT NULL COMMENT '是否发布', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '错误信息', `power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MidJourney 任务表'; -- -------------------------------------------------------- @@ -438,18 +438,18 @@ CREATE TABLE `chatgpt_orders` ( `product_id` bigint NOT NULL COMMENT '产品ID', `username` varchar(30) NOT NULL COMMENT '用户名', `order_no` varchar(30) NOT NULL COMMENT '订单ID', - `trade_no` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付平台交易流水号', + `trade_no` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '支付平台交易流水号', `subject` varchar(100) NOT NULL COMMENT '订单产品', `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '订单状态(0:待支付,1:已扫码,2:支付成功)', - `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备注', `pay_time` bigint DEFAULT NULL COMMENT '支付时间', `pay_way` varchar(20) NOT NULL COMMENT '支付方式', `pay_type` varchar(30) NOT NULL COMMENT '支付类型', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `deleted_at` datetime DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='充值订单表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='充值订单表'; -- -------------------------------------------------------- @@ -466,10 +466,10 @@ CREATE TABLE `chatgpt_power_logs` ( `amount` smallint NOT NULL COMMENT '算力数值', `balance` bigint NOT NULL COMMENT '余额', `model` varchar(255) NOT NULL COMMENT '模型', - `remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注', + `remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备注', `mark` tinyint(1) NOT NULL COMMENT '资金类型(0:支出,1:收入)', `created_at` datetime NOT NULL COMMENT '创建时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户算力消费日志'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户算力消费日志'; -- -------------------------------------------------------- @@ -492,7 +492,7 @@ CREATE TABLE `chatgpt_products` ( `updated_at` datetime NOT NULL, `app_url` varchar(255) DEFAULT NULL COMMENT 'App跳转地址', `url` varchar(255) DEFAULT NULL COMMENT '跳转地址' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员套餐表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会员套餐表'; -- -- 转存表中的数据 `chatgpt_products` @@ -518,7 +518,7 @@ CREATE TABLE `chatgpt_redeems` ( `enabled` tinyint(1) NOT NULL COMMENT '是否启用', `created_at` datetime NOT NULL, `redeemed_at` bigint NOT NULL COMMENT '兑换时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='兑换码'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='兑换码'; -- -------------------------------------------------------- @@ -530,18 +530,18 @@ DROP TABLE IF EXISTS `chatgpt_sd_jobs`; CREATE TABLE `chatgpt_sd_jobs` ( `id` int NOT NULL, `user_id` bigint NOT NULL COMMENT '用户 ID', - `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT 'txt2img' COMMENT '任务类别', - `task_id` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务 ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'txt2img' COMMENT '任务类别', + `task_id` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '任务 ID', `task_info` text NOT NULL COMMENT '任务详情', - `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '会话提示词', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '会话提示词', `img_url` varchar(255) DEFAULT NULL COMMENT '图片URL', - `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '绘画参数json', + `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '绘画参数json', `progress` smallint DEFAULT '0' COMMENT '任务进度', `publish` tinyint(1) NOT NULL COMMENT '是否发布', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '错误信息', `power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Stable Diffusion 任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Stable Diffusion 任务表'; -- -------------------------------------------------------- @@ -558,25 +558,25 @@ CREATE TABLE `chatgpt_suno_jobs` ( `type` tinyint(1) DEFAULT '0' COMMENT '任务类型,1:灵感创作,2:自定义创作', `task_id` varchar(50) DEFAULT NULL COMMENT '任务 ID', `task_info` text NOT NULL COMMENT '任务详情', - `ref_task_id` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '引用任务 ID', + `ref_task_id` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '引用任务 ID', `tags` varchar(100) DEFAULT NULL COMMENT '歌曲风格', `instrumental` tinyint(1) DEFAULT '0' COMMENT '是否为纯音乐', `extend_secs` smallint DEFAULT '0' COMMENT '延长秒数', `song_id` varchar(50) DEFAULT NULL COMMENT '要续写的歌曲 ID', `ref_song_id` varchar(50) NOT NULL COMMENT '引用的歌曲ID', `prompt` varchar(2000) NOT NULL COMMENT '提示词', - `cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址', - `audio_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '音频地址', + `cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '封面图地址', + `audio_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '音频地址', `model_name` varchar(30) DEFAULT NULL COMMENT '模型地址', `progress` smallint DEFAULT '0' COMMENT '任务进度', `duration` smallint NOT NULL DEFAULT '0' COMMENT '歌曲时长', `publish` tinyint(1) NOT NULL COMMENT '是否发布', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '错误信息', `raw_data` text COMMENT '原始数据', `power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力', `play_times` bigint DEFAULT NULL COMMENT '播放次数', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MidJourney 任务表'; -- -------------------------------------------------------- @@ -587,19 +587,19 @@ CREATE TABLE `chatgpt_suno_jobs` ( DROP TABLE IF EXISTS `chatgpt_users`; CREATE TABLE `chatgpt_users` ( `id` int NOT NULL, - `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名', + `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', `mobile` char(11) DEFAULT NULL COMMENT '手机号', `email` varchar(50) DEFAULT NULL COMMENT '邮箱地址', `nickname` varchar(30) NOT NULL COMMENT '昵称', - `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码', - `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '头像', + `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '头像', `salt` char(12) NOT NULL COMMENT '密码盐', `power` bigint NOT NULL DEFAULT '0' COMMENT '剩余算力', `expired_time` bigint NOT NULL COMMENT '用户过期时间', `status` tinyint(1) NOT NULL COMMENT '当前状态', - `chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天配置json', - `chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天角色 json', - `chat_models_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'AI模型 json', + `chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '聊天配置json', + `chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '聊天角色 json', + `chat_models_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'AI模型 json', `last_login_at` bigint NOT NULL COMMENT '最后登录时间', `vip` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否会员', `last_login_ip` char(16) NOT NULL COMMENT '最后登录 IP', @@ -608,7 +608,7 @@ CREATE TABLE `chatgpt_users` ( `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `chat_config` text NOT NULL COMMENT '聊天配置json' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; -- -- 转存表中的数据 `chatgpt_users` @@ -634,7 +634,7 @@ CREATE TABLE `chatgpt_user_login_logs` ( `login_address` varchar(30) NOT NULL COMMENT '登录地址', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户登录日志'; -- -------------------------------------------------------- @@ -648,20 +648,20 @@ CREATE TABLE `chatgpt_video_jobs` ( `user_id` bigint NOT NULL COMMENT '用户 ID', `channel` varchar(100) NOT NULL COMMENT '渠道', `task_id` varchar(100) NOT NULL COMMENT '任务 ID', - `task_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '原始任务信息', + `task_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '原始任务信息', `type` varchar(20) DEFAULT NULL COMMENT '任务类型,luma,runway,cogvideo', - `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '提示词', - `prompt_ext` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '优化后提示词', - `cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址', - `video_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '视频地址', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提示词', + `prompt_ext` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '优化后提示词', + `cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '封面图地址', + `video_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '视频地址', `water_url` varchar(512) DEFAULT NULL COMMENT '带水印的视频地址', `progress` smallint DEFAULT '0' COMMENT '任务进度', `publish` tinyint(1) NOT NULL COMMENT '是否发布', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '错误信息', `raw_data` text COMMENT '原始数据', `power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MidJourney 任务表'; -- -- 转储表的索引 diff --git a/database/geekai_plus-v4.2.6.sql b/database/geekai_plus-v4.2.6.sql index 1d3d1571..267d8775 100644 --- a/database/geekai_plus-v4.2.6.sql +++ b/database/geekai_plus-v4.2.6.sql @@ -20,7 +20,7 @@ SET time_zone = "+00:00"; -- -- 数据库: `geekai` -- -CREATE DATABASE IF NOT EXISTS `geekai` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; +CREATE DATABASE IF NOT EXISTS `geekai` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `geekai`; -- -------------------------------------------------------- @@ -45,7 +45,7 @@ CREATE TABLE `geekai_3d_jobs` ( `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `raw_data` text COMMENT 'API返回的原始数据' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- @@ -56,15 +56,15 @@ CREATE TABLE `geekai_3d_jobs` ( DROP TABLE IF EXISTS `geekai_admin_users`; CREATE TABLE `geekai_admin_users` ( `id` int NOT NULL, - `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名', - `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码', - `salt` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码盐', + `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码', + `salt` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码盐', `status` tinyint(1) NOT NULL COMMENT '当前状态', `last_login_at` bigint NOT NULL COMMENT '最后登录时间', - `last_login_ip` char(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '最后登录 IP', + `last_login_ip` char(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '最后登录 IP', `created_at` datetime NOT NULL COMMENT '创建时间', `updated_at` datetime NOT NULL COMMENT '更新时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户' ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统用户' ROW_FORMAT=DYNAMIC; -- -- 转存表中的数据 `geekai_admin_users` @@ -83,7 +83,7 @@ DROP TABLE IF EXISTS `geekai_api_keys`; CREATE TABLE `geekai_api_keys` ( `id` int NOT NULL, `name` varchar(30) DEFAULT NULL COMMENT '名称', - `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'API KEY value', + `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'API KEY value', `type` varchar(10) NOT NULL DEFAULT 'chat' COMMENT '用途(chat=>聊天,img=>图片)', `last_used_at` bigint NOT NULL COMMENT '最后使用时间', `api_url` varchar(255) DEFAULT NULL COMMENT 'API 地址', @@ -91,7 +91,7 @@ CREATE TABLE `geekai_api_keys` ( `proxy_url` varchar(100) DEFAULT NULL COMMENT '代理地址', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API '; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='OpenAI API '; -- -------------------------------------------------------- @@ -107,7 +107,7 @@ CREATE TABLE `geekai_app_types` ( `sort_num` tinyint NOT NULL COMMENT '排序', `enabled` tinyint(1) NOT NULL COMMENT '是否启用', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='应用分类表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应用分类表'; -- -- 转存表中的数据 `geekai_app_types` @@ -132,7 +132,7 @@ CREATE TABLE `geekai_chat_history` ( `user_id` int NOT NULL COMMENT '用户 ID', `chat_id` char(40) NOT NULL COMMENT '会话 ID', `type` varchar(10) NOT NULL COMMENT '类型:prompt|reply', - `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色图标', + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色图标', `role_id` int NOT NULL COMMENT '角色 ID', `model` varchar(255) DEFAULT NULL COMMENT '模型名称', `content` text NOT NULL COMMENT '聊天内容', @@ -141,7 +141,7 @@ CREATE TABLE `geekai_chat_history` ( `use_context` tinyint(1) NOT NULL COMMENT '是否允许作为上下文语料', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天历史记录'; -- -------------------------------------------------------- @@ -160,7 +160,7 @@ CREATE TABLE `geekai_chat_items` ( `model` varchar(30) DEFAULT NULL COMMENT '模型名称', `created_at` datetime NOT NULL COMMENT '创建时间', `updated_at` datetime NOT NULL COMMENT '更新时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户会话列表'; -- -------------------------------------------------------- @@ -172,8 +172,8 @@ DROP TABLE IF EXISTS `geekai_chat_models`; CREATE TABLE `geekai_chat_models` ( `id` int NOT NULL, `type` varchar(10) NOT NULL DEFAULT 'chat' COMMENT '模型类型(chat,img)', - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '模型名称', - `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '模型值', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模型名称', + `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模型值', `sort_num` tinyint(1) NOT NULL COMMENT '排序数字', `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用模型', `power` smallint NOT NULL COMMENT '消耗算力点数', @@ -187,7 +187,7 @@ CREATE TABLE `geekai_chat_models` ( `updated_at` datetime DEFAULT NULL, `desc` varchar(1024) NOT NULL DEFAULT '' COMMENT '模型类型描述', `tag` varchar(1024) NOT NULL DEFAULT '' COMMENT '模型标签' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 模型表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI 模型表'; -- -- 转存表中的数据 `geekai_chat_models` @@ -220,7 +220,7 @@ CREATE TABLE `geekai_chat_roles` ( `name` varchar(30) NOT NULL COMMENT '角色名称', `tid` int NOT NULL COMMENT '分类ID', `marker` varchar(30) NOT NULL COMMENT '角色标识', - `context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色语料 json', + `context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色语料 json', `hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息', `icon` varchar(255) NOT NULL COMMENT '角色图标', `enable` tinyint(1) NOT NULL COMMENT '是否被启用', @@ -228,7 +228,7 @@ CREATE TABLE `geekai_chat_roles` ( `model_id` int NOT NULL DEFAULT '0' COMMENT '绑定模型ID', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天角色表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天角色表'; -- -- 转存表中的数据 `geekai_chat_roles` @@ -266,7 +266,7 @@ CREATE TABLE `geekai_configs` ( `id` int NOT NULL, `name` varchar(20) NOT NULL COMMENT '配置名称', `value` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- 转存表中的数据 `geekai_configs` @@ -289,16 +289,16 @@ DROP TABLE IF EXISTS `geekai_dall_jobs`; CREATE TABLE `geekai_dall_jobs` ( `id` int NOT NULL, `user_id` int NOT NULL COMMENT '用户ID', - `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '提示词', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提示词', `task_info` text NOT NULL COMMENT '任务详情', `img_url` varchar(255) NOT NULL COMMENT '图片地址', - `org_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原图地址', + `org_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '原图地址', `publish` tinyint(1) NOT NULL COMMENT '是否发布', `power` smallint NOT NULL COMMENT '消耗算力', `progress` smallint NOT NULL COMMENT '任务进度', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '错误信息', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='DALLE 绘图任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='DALLE 绘图任务表'; -- -------------------------------------------------------- @@ -310,13 +310,13 @@ DROP TABLE IF EXISTS `geekai_files`; CREATE TABLE `geekai_files` ( `id` int NOT NULL, `user_id` int NOT NULL COMMENT '用户 ID', - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件名', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件名', `obj_key` varchar(100) DEFAULT NULL COMMENT '文件标识', `url` varchar(255) NOT NULL COMMENT '文件地址', `ext` varchar(10) NOT NULL COMMENT '文件后缀', `size` bigint NOT NULL DEFAULT '0' COMMENT '文件大小', `created_at` datetime NOT NULL COMMENT '创建时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户文件表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户文件表'; -- -------------------------------------------------------- @@ -328,13 +328,13 @@ DROP TABLE IF EXISTS `geekai_functions`; CREATE TABLE `geekai_functions` ( `id` int NOT NULL, `name` varchar(30) NOT NULL COMMENT '函数名称', - `label` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '函数标签', + `label` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '函数标签', `description` varchar(255) DEFAULT NULL COMMENT '函数描述', `parameters` text COMMENT '函数参数(JSON)', `token` varchar(255) DEFAULT NULL COMMENT 'API授权token', `action` varchar(255) DEFAULT NULL COMMENT '函数处理 API', `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='函数插件表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='函数插件表'; -- -- 转存表中的数据 `geekai_functions` @@ -359,7 +359,7 @@ CREATE TABLE `geekai_invite_codes` ( `hits` bigint NOT NULL COMMENT '点击次数', `reg_num` smallint NOT NULL COMMENT '注册数量', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户邀请码'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户邀请码'; -- -------------------------------------------------------- @@ -372,11 +372,11 @@ CREATE TABLE `geekai_invite_logs` ( `id` int NOT NULL, `inviter_id` int NOT NULL COMMENT '邀请人ID', `user_id` int NOT NULL COMMENT '注册用户ID', - `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名', - `invite_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '邀请码', - `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注', + `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `invite_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邀请码', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备注', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='邀请注册日志'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='邀请注册日志'; -- -------------------------------------------------------- @@ -402,7 +402,7 @@ CREATE TABLE `geekai_jimeng_jobs` ( `power` int DEFAULT '0' COMMENT '消耗算力', `created_at` datetime NOT NULL COMMENT '创建时间', `updated_at` datetime NOT NULL COMMENT '更新时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- @@ -418,7 +418,7 @@ CREATE TABLE `geekai_menus` ( `url` varchar(100) NOT NULL COMMENT '地址', `sort_num` smallint NOT NULL COMMENT '排序', `enabled` tinyint(1) NOT NULL COMMENT '是否启用' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='前端菜单表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='前端菜单表'; -- -- 转存表中的数据 `geekai_menus` @@ -454,19 +454,19 @@ CREATE TABLE `geekai_mj_jobs` ( `task_info` text NOT NULL COMMENT '任务详情', `type` varchar(20) DEFAULT 'image' COMMENT '任务类别', `message_id` char(40) NOT NULL COMMENT '消息 ID', - `channel_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '频道ID', + `channel_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '频道ID', `reference_id` char(40) DEFAULT NULL COMMENT '引用消息 ID', - `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '会话提示词', - `img_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '图片URL', - `org_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原始图片地址', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '会话提示词', + `img_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图片URL', + `org_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '原始图片地址', `hash` varchar(100) DEFAULT NULL COMMENT 'message hash', `progress` smallint DEFAULT '0' COMMENT '任务进度', `use_proxy` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否使用反代', `publish` tinyint(1) NOT NULL COMMENT '是否发布', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '错误信息', `power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MidJourney 任务表'; -- -------------------------------------------------------- @@ -483,7 +483,7 @@ CREATE TABLE `geekai_moderation` ( `output` text NOT NULL COMMENT 'AI 输出', `result` text NOT NULL COMMENT '鉴别结果', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- @@ -498,18 +498,18 @@ CREATE TABLE `geekai_orders` ( `product_id` bigint NOT NULL COMMENT '产品ID', `username` varchar(30) NOT NULL COMMENT '用户名', `order_no` varchar(30) NOT NULL COMMENT '订单ID', - `trade_no` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付平台交易流水号', + `trade_no` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '支付平台交易流水号', `subject` varchar(100) NOT NULL COMMENT '订单产品', `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '订单状态(0:待支付,1:已扫码,2:支付成功)', - `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备注', `pay_time` bigint DEFAULT NULL COMMENT '支付时间', `pay_way` varchar(20) NOT NULL COMMENT '支付方式', `channel` varchar(30) NOT NULL COMMENT '支付类型', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `checked` tinyint NOT NULL DEFAULT '0' COMMENT '是否已检查' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='充值订单表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='充值订单表'; -- -------------------------------------------------------- @@ -526,10 +526,10 @@ CREATE TABLE `geekai_power_logs` ( `amount` smallint NOT NULL COMMENT '算力数值', `balance` bigint NOT NULL COMMENT '余额', `model` varchar(255) NOT NULL COMMENT '模型', - `remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注', + `remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备注', `mark` tinyint(1) NOT NULL COMMENT '资金类型(0:支出,1:收入)', `created_at` datetime NOT NULL COMMENT '创建时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户算力消费日志'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户算力消费日志'; -- -------------------------------------------------------- @@ -548,7 +548,7 @@ CREATE TABLE `geekai_products` ( `sort_num` tinyint NOT NULL DEFAULT '0' COMMENT '排序', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员套餐表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会员套餐表'; -- -- 转存表中的数据 `geekai_products` @@ -574,7 +574,7 @@ CREATE TABLE `geekai_redeems` ( `enabled` tinyint(1) NOT NULL COMMENT '是否启用', `created_at` datetime NOT NULL, `redeemed_at` bigint NOT NULL COMMENT '兑换时间' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='兑换码'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='兑换码'; -- -------------------------------------------------------- @@ -586,18 +586,18 @@ DROP TABLE IF EXISTS `geekai_sd_jobs`; CREATE TABLE `geekai_sd_jobs` ( `id` int NOT NULL, `user_id` int NOT NULL COMMENT '用户 ID', - `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT 'txt2img' COMMENT '任务类别', - `task_id` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务 ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'txt2img' COMMENT '任务类别', + `task_id` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '任务 ID', `task_info` text NOT NULL COMMENT '任务详情', - `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '会话提示词', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '会话提示词', `img_url` varchar(255) DEFAULT NULL COMMENT '图片URL', - `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '绘画参数json', + `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '绘画参数json', `progress` smallint DEFAULT '0' COMMENT '任务进度', `publish` tinyint(1) NOT NULL COMMENT '是否发布', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '错误信息', `power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Stable Diffusion 任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Stable Diffusion 任务表'; -- -------------------------------------------------------- @@ -614,25 +614,25 @@ CREATE TABLE `geekai_suno_jobs` ( `type` tinyint(1) DEFAULT '0' COMMENT '任务类型,1:灵感创作,2:自定义创作', `task_id` varchar(50) DEFAULT NULL COMMENT '任务 ID', `task_info` text NOT NULL COMMENT '任务详情', - `ref_task_id` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '引用任务 ID', + `ref_task_id` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '引用任务 ID', `tags` varchar(255) DEFAULT NULL COMMENT '歌曲风格', `instrumental` tinyint(1) DEFAULT '0' COMMENT '是否为纯音乐', `extend_secs` smallint DEFAULT '0' COMMENT '延长秒数', `song_id` varchar(50) DEFAULT NULL COMMENT '要续写的歌曲 ID', `ref_song_id` varchar(50) NOT NULL COMMENT '引用的歌曲ID', `prompt` varchar(2000) NOT NULL COMMENT '提示词', - `cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址', - `audio_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '音频地址', + `cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '封面图地址', + `audio_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '音频地址', `model_name` varchar(30) DEFAULT NULL COMMENT '模型地址', `progress` smallint DEFAULT '0' COMMENT '任务进度', `duration` smallint NOT NULL DEFAULT '0' COMMENT '歌曲时长', `publish` tinyint(1) NOT NULL COMMENT '是否发布', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '错误信息', `raw_data` text COMMENT '原始数据', `power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力', `play_times` bigint DEFAULT NULL COMMENT '播放次数', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MidJourney 任务表'; -- -------------------------------------------------------- @@ -643,19 +643,19 @@ CREATE TABLE `geekai_suno_jobs` ( DROP TABLE IF EXISTS `geekai_users`; CREATE TABLE `geekai_users` ( `id` int NOT NULL, - `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名', + `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', `mobile` char(11) DEFAULT NULL COMMENT '手机号', `email` varchar(50) DEFAULT NULL COMMENT '邮箱地址', `nickname` varchar(30) NOT NULL COMMENT '昵称', - `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码', - `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '头像', + `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '头像', `salt` char(12) NOT NULL COMMENT '密码盐', `power` bigint NOT NULL DEFAULT '0' COMMENT '剩余算力', `expired_time` bigint NOT NULL COMMENT '用户过期时间', `status` tinyint(1) NOT NULL COMMENT '当前状态', - `chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天配置json', - `chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天角色 json', - `chat_models_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'AI模型 json', + `chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '聊天配置json', + `chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '聊天角色 json', + `chat_models_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'AI模型 json', `last_login_at` bigint NOT NULL COMMENT '最后登录时间', `vip` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否会员', `last_login_ip` char(16) NOT NULL COMMENT '最后登录 IP', @@ -663,7 +663,7 @@ CREATE TABLE `geekai_users` ( `platform` varchar(30) DEFAULT NULL COMMENT '登录平台', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; -- -- 转存表中的数据 `geekai_users` @@ -692,7 +692,7 @@ CREATE TABLE `geekai_user_login_logs` ( `login_address` varchar(30) NOT NULL COMMENT '登录地址', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户登录日志'; -- -------------------------------------------------------- @@ -706,20 +706,20 @@ CREATE TABLE `geekai_video_jobs` ( `user_id` int NOT NULL COMMENT '用户 ID', `channel` varchar(100) NOT NULL COMMENT '渠道', `task_id` varchar(100) NOT NULL COMMENT '任务 ID', - `task_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '原始任务信息', + `task_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '原始任务信息', `type` varchar(20) DEFAULT NULL COMMENT '任务类型,luma,runway,cogvideo', - `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '提示词', - `prompt_ext` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '优化后提示词', - `cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址', - `video_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '视频地址', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提示词', + `prompt_ext` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '优化后提示词', + `cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '封面图地址', + `video_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '视频地址', `water_url` varchar(512) DEFAULT NULL COMMENT '带水印的视频地址', `progress` smallint DEFAULT '0' COMMENT '任务进度', `publish` tinyint(1) NOT NULL COMMENT '是否发布', - `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息', + `err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '错误信息', `raw_data` text COMMENT '原始数据', `power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力', `created_at` datetime NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MidJourney 任务表'; -- -- 转储表的索引 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index d29eb714..9d2ffd51 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -46,7 +46,7 @@ services: # 后端 API 程序 geekai-api: - image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.2.8-amd64 + image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.3.0-amd64 container_name: geekai-api restart: always depends_on: @@ -69,7 +69,7 @@ services: # 前端应用 geekai-web: - image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.2.8-amd64 + image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.3.0-amd64 container_name: geekai-web restart: always depends_on: diff --git a/docs/ai3d.md b/docs/ai3d.md deleted file mode 100644 index ed3fb44a..00000000 --- a/docs/ai3d.md +++ /dev/null @@ -1,510 +0,0 @@ -## 开发 3D 图片生成功能 - -对接 3D 图片生成接口,为当前系统添加 3D 模型生成功能,默认支持腾讯云和 Gitee 的图生 3D API 接口。 - -## 要求 - -1. 完成数据库设计,后端 API 设计,前端页面设计。 -2. 完成前端功能页面以及后台管理页面,具体设计结构可以参考即梦 AI,在对应的模块建立独立的模块 。 -3. 页面设计要精美,但是整体风格要跟整站风格一致。 -4. 支持前端 3D 模型预览,支持 3D 模型下载。 - -## 腾讯云图生 3D API 接口文档 - -1. 提交任务: https://cloud.tencent.com/document/product/1804/120826 -2. 查询任务: https://cloud.tencent.com/document/product/1804/120827 -3. Golang SDK: https://gitee.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ai3d/v20250513/client.go 依赖已经安装到本地了,如果你无法读取远程文件,下面是文件主要内容: - -client.go - -```go -// Copyright (c) 2017-2025 Tencent. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package v20250513 - -import ( - "context" - "errors" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" -) - -const APIVersion = "2025-05-13" - -type Client struct { - common.Client -} - -// Deprecated -func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) { - cpf := profile.NewClientProfile() - client = &Client{} - client.Init(region).WithSecretId(secretId, secretKey).WithProfile(cpf) - return -} - -func NewClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *Client, err error) { - client = &Client{} - client.Init(region). - WithCredential(credential). - WithProfile(clientProfile) - return -} - - -func NewQueryHunyuanTo3DJobRequest() (request *QueryHunyuanTo3DJobRequest) { - request = &QueryHunyuanTo3DJobRequest{ - BaseRequest: &tchttp.BaseRequest{}, - } - - request.Init().WithApiInfo("ai3d", APIVersion, "QueryHunyuanTo3DJob") - - - return -} - -func NewQueryHunyuanTo3DJobResponse() (response *QueryHunyuanTo3DJobResponse) { - response = &QueryHunyuanTo3DJobResponse{ - BaseResponse: &tchttp.BaseResponse{}, - } - return - -} - -// QueryHunyuanTo3DJob -// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。 -// -// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。 -func (c *Client) QueryHunyuanTo3DJob(request *QueryHunyuanTo3DJobRequest) (response *QueryHunyuanTo3DJobResponse, err error) { - return c.QueryHunyuanTo3DJobWithContext(context.Background(), request) -} - -// QueryHunyuanTo3DJob -// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。 -// -// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。 -func (c *Client) QueryHunyuanTo3DJobWithContext(ctx context.Context, request *QueryHunyuanTo3DJobRequest) (response *QueryHunyuanTo3DJobResponse, err error) { - if request == nil { - request = NewQueryHunyuanTo3DJobRequest() - } - c.InitBaseRequest(&request.BaseRequest, "ai3d", APIVersion, "QueryHunyuanTo3DJob") - - if c.GetCredential() == nil { - return nil, errors.New("QueryHunyuanTo3DJob require credential") - } - - request.SetContext(ctx) - - response = NewQueryHunyuanTo3DJobResponse() - err = c.Send(request, response) - return -} - -func NewSubmitHunyuanTo3DJobRequest() (request *SubmitHunyuanTo3DJobRequest) { - request = &SubmitHunyuanTo3DJobRequest{ - BaseRequest: &tchttp.BaseRequest{}, - } - - request.Init().WithApiInfo("ai3d", APIVersion, "SubmitHunyuanTo3DJob") - - - return -} - -func NewSubmitHunyuanTo3DJobResponse() (response *SubmitHunyuanTo3DJobResponse) { - response = &SubmitHunyuanTo3DJobResponse{ - BaseResponse: &tchttp.BaseResponse{}, - } - return - -} - -// SubmitHunyuanTo3DJob -// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。 -// -// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。 -func (c *Client) SubmitHunyuanTo3DJob(request *SubmitHunyuanTo3DJobRequest) (response *SubmitHunyuanTo3DJobResponse, err error) { - return c.SubmitHunyuanTo3DJobWithContext(context.Background(), request) -} - -// SubmitHunyuanTo3DJob -// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。 -// -// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。 -func (c *Client) SubmitHunyuanTo3DJobWithContext(ctx context.Context, request *SubmitHunyuanTo3DJobRequest) (response *SubmitHunyuanTo3DJobResponse, err error) { - if request == nil { - request = NewSubmitHunyuanTo3DJobRequest() - } - c.InitBaseRequest(&request.BaseRequest, "ai3d", APIVersion, "SubmitHunyuanTo3DJob") - - if c.GetCredential() == nil { - return nil, errors.New("SubmitHunyuanTo3DJob require credential") - } - - request.SetContext(ctx) - - response = NewSubmitHunyuanTo3DJobResponse() - err = c.Send(request, response) - return -} - -``` - -model.go - -```go -// Copyright (c) 2017-2025 Tencent. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package v20250513 - -import ( - tcerr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" - tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/json" -) - -type File3D struct { - // 文件格式 - Type *string `json:"Type,omitnil,omitempty" name:"Type"` - - // 文件的Url(有效期24小时) - Url *string `json:"Url,omitnil,omitempty" name:"Url"` - - // 预览图片Url - PreviewImageUrl *string `json:"PreviewImageUrl,omitnil,omitempty" name:"PreviewImageUrl"` -} - -// Predefined struct for user -type QueryHunyuanTo3DJobRequestParams struct { - // 任务ID。 - JobId *string `json:"JobId,omitnil,omitempty" name:"JobId"` -} - -type QueryHunyuanTo3DJobRequest struct { - *tchttp.BaseRequest - - // 任务ID。 - JobId *string `json:"JobId,omitnil,omitempty" name:"JobId"` -} - -func (r *QueryHunyuanTo3DJobRequest) ToJsonString() string { - b, _ := json.Marshal(r) - return string(b) -} - -// FromJsonString It is highly **NOT** recommended to use this function -// because it has no param check, nor strict type check -func (r *QueryHunyuanTo3DJobRequest) FromJsonString(s string) error { - f := make(map[string]interface{}) - if err := json.Unmarshal([]byte(s), &f); err != nil { - return err - } - delete(f, "JobId") - if len(f) > 0 { - return tcerr.NewTencentCloudSDKError("ClientError.BuildRequestError", "QueryHunyuanTo3DJobRequest has unknown keys!", "") - } - return json.Unmarshal([]byte(s), &r) -} - -// Predefined struct for user -type QueryHunyuanTo3DJobResponseParams struct { - // 任务状态。WAIT:等待中,RUN:执行中,FAIL:任务失败,DONE:任务成功 - Status *string `json:"Status,omitnil,omitempty" name:"Status"` - - // 错误码 - ErrorCode *string `json:"ErrorCode,omitnil,omitempty" name:"ErrorCode"` - - // 错误信息 - ErrorMessage *string `json:"ErrorMessage,omitnil,omitempty" name:"ErrorMessage"` - - // 生成的3D文件数组。 - ResultFile3Ds []*File3D `json:"ResultFile3Ds,omitnil,omitempty" name:"ResultFile3Ds"` - - // 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 - RequestId *string `json:"RequestId,omitnil,omitempty" name:"RequestId"` -} - -type QueryHunyuanTo3DJobResponse struct { - *tchttp.BaseResponse - Response *QueryHunyuanTo3DJobResponseParams `json:"Response"` -} - -func (r *QueryHunyuanTo3DJobResponse) ToJsonString() string { - b, _ := json.Marshal(r) - return string(b) -} - -// FromJsonString It is highly **NOT** recommended to use this function -// because it has no param check, nor strict type check -func (r *QueryHunyuanTo3DJobResponse) FromJsonString(s string) error { - return json.Unmarshal([]byte(s), &r) -} - -// Predefined struct for user -type SubmitHunyuanTo3DJobRequestParams struct { - // 文生3D,3D内容的描述,中文正向提示词。 - // 最多支持1024个 utf-8 字符。 - // 文生3D, image、image_url和 prompt必填其一,且prompt和image/image_url不能同时存在。 - Prompt *string `json:"Prompt,omitnil,omitempty" name:"Prompt"` - - // 输入图 Base64 数据。 - // 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m) - // 格式:jpg,png,jpeg,webp。 - // ImageBase64、ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。 - ImageBase64 *string `json:"ImageBase64,omitnil,omitempty" name:"ImageBase64"` - - // 输入图Url。 - // 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m) - // 格式:jpg,png,jpeg,webp。 - // ImageBase64/ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。 - ImageUrl *string `json:"ImageUrl,omitnil,omitempty" name:"ImageUrl"` - - // 多视角的模型图片,视角参考值: - // left:左视图; - // right:右视图; - // back:后视图; - // - // 每个视角仅限制一张图片。 - // ●图片大小限制:编码后大小不可超过8M。 - // ●图片分辨率限制:单边分辨率小于5000且大于128。 - // ●支持图片格式:支持jpg或png - MultiViewImages []*ViewImage `json:"MultiViewImages,omitnil,omitempty" name:"MultiViewImages"` - - // 生成模型的格式,仅限制生成一种格式。 - // 生成模型文件组默认返回obj格式。 - // 可选值:OBJ,GLB,STL,USDZ,FBX,MP4。 - ResultFormat *string `json:"ResultFormat,omitnil,omitempty" name:"ResultFormat"` - - // 是否开启 PBR材质生成,默认 false。 - EnablePBR *bool `json:"EnablePBR,omitnil,omitempty" name:"EnablePBR"` -} - -type SubmitHunyuanTo3DJobRequest struct { - *tchttp.BaseRequest - - // 文生3D,3D内容的描述,中文正向提示词。 - // 最多支持1024个 utf-8 字符。 - // 文生3D, image、image_url和 prompt必填其一,且prompt和image/image_url不能同时存在。 - Prompt *string `json:"Prompt,omitnil,omitempty" name:"Prompt"` - - // 输入图 Base64 数据。 - // 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m) - // 格式:jpg,png,jpeg,webp。 - // ImageBase64、ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。 - ImageBase64 *string `json:"ImageBase64,omitnil,omitempty" name:"ImageBase64"` - - // 输入图Url。 - // 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m) - // 格式:jpg,png,jpeg,webp。 - // ImageBase64/ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。 - ImageUrl *string `json:"ImageUrl,omitnil,omitempty" name:"ImageUrl"` - - // 多视角的模型图片,视角参考值: - // left:左视图; - // right:右视图; - // back:后视图; - // - // 每个视角仅限制一张图片。 - // ●图片大小限制:编码后大小不可超过8M。 - // ●图片分辨率限制:单边分辨率小于5000且大于128。 - // ●支持图片格式:支持jpg或png - MultiViewImages []*ViewImage `json:"MultiViewImages,omitnil,omitempty" name:"MultiViewImages"` - - // 生成模型的格式,仅限制生成一种格式。 - // 生成模型文件组默认返回obj格式。 - // 可选值:OBJ,GLB,STL,USDZ,FBX,MP4。 - ResultFormat *string `json:"ResultFormat,omitnil,omitempty" name:"ResultFormat"` - - // 是否开启 PBR材质生成,默认 false。 - EnablePBR *bool `json:"EnablePBR,omitnil,omitempty" name:"EnablePBR"` -} - -func (r *SubmitHunyuanTo3DJobRequest) ToJsonString() string { - b, _ := json.Marshal(r) - return string(b) -} - -// FromJsonString It is highly **NOT** recommended to use this function -// because it has no param check, nor strict type check -func (r *SubmitHunyuanTo3DJobRequest) FromJsonString(s string) error { - f := make(map[string]interface{}) - if err := json.Unmarshal([]byte(s), &f); err != nil { - return err - } - delete(f, "Prompt") - delete(f, "ImageBase64") - delete(f, "ImageUrl") - delete(f, "MultiViewImages") - delete(f, "ResultFormat") - delete(f, "EnablePBR") - if len(f) > 0 { - return tcerr.NewTencentCloudSDKError("ClientError.BuildRequestError", "SubmitHunyuanTo3DJobRequest has unknown keys!", "") - } - return json.Unmarshal([]byte(s), &r) -} - -// Predefined struct for user -type SubmitHunyuanTo3DJobResponseParams struct { - // 任务ID(有效期24小时) - JobId *string `json:"JobId,omitnil,omitempty" name:"JobId"` - - // 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 - RequestId *string `json:"RequestId,omitnil,omitempty" name:"RequestId"` -} - -type SubmitHunyuanTo3DJobResponse struct { - *tchttp.BaseResponse - Response *SubmitHunyuanTo3DJobResponseParams `json:"Response"` -} - -func (r *SubmitHunyuanTo3DJobResponse) ToJsonString() string { - b, _ := json.Marshal(r) - return string(b) -} - -// FromJsonString It is highly **NOT** recommended to use this function -// because it has no param check, nor strict type check -func (r *SubmitHunyuanTo3DJobResponse) FromJsonString(s string) error { - return json.Unmarshal([]byte(s), &r) -} - -type ViewImage struct { - // 视角类型。 - // 取值:back、left、right - ViewType *string `json:"ViewType,omitnil,omitempty" name:"ViewType"` - - // 图片Url地址 - ViewImageUrl *string `json:"ViewImageUrl,omitnil,omitempty" name:"ViewImageUrl"` -} -``` - -## Gitee 图生 3D API 接口文档 - -1. 提交任务: https://ai.gitee.com/docs/openapi/v1#tag/3d-%E7%94%9F%E6%88%90/post/async/image-to-3d -2. 查询任务:https://ai.gitee.com/docs/openapi/v1#tag/%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1/get/task/{task_id}/get - -首先,你需要认真阅读上述接口文档,然后按照接口文档的示例代码实现腾讯云和 Gitee 的图生 3D API 接口,并且将接口集成到现有的系统中。 - -📋 功能概述 - - 为现有的GeekAI-Plus系统添加3D图片生成功能,集成腾讯云和Gitee的图生3D API接口,包含完整的前后端功能和管理界面。 - - 🗄️ 数据库设计 - - 新增数据表:geekai_3d_jobs - - id (uint): 主键 - - type (string): API类型 (tencent/gitee) - - user_id (uint): 用户ID - - power (int): 消耗算力 - - task_id (string): 第三方任务ID - - img_url (string): 生成的3D模型文件地址 - - model (string): 使用的3D模型类型 - - status (string): 任务状态 - - err_msg (string): 错误信息 - - params (JSON): 任务参数(包含输入图片、提示词等所有参数) - - created_at (int64): 创建时间 - - 🔧 后端API实现 - - 路由结构:/api/3d/* - - POST /api/3d/generate - 创建3D生成任务 - - GET /api/3d/jobs - 获取任务列表 - - GET /api/3d/job/{id} - 获取任务详情 - - GET /api/3d/download/{id} - 下载3D模型 - - DELETE /api/3d/job/{id} - 删除任务 - - 核心服务: - - service/3d/tencent_client.go - 腾讯云3D API客户端 - - service/3d/gitee_client.go - Gitee 3D API客户端 - - service/3d/service.go - 3D生成服务统一接口 - - handler/3d_handler.go - HTTP处理器 - - store/vo/3d_job.go - 数据模型 - - 🎨 前端界面设计 - - 用户端页面:/3d - 3D生成主页面 - - 参考JiMeng.vue的设计风格和布局 - - 使用CustomTab组件分离平台参数: - - Tab 1: "魔力方舟" (Gitee平台参数) - - Tab 2: "腾讯混元" (腾讯云平台参数) - - 每个Tab内包含: - - 图片上传区域 - - 模型选择下拉框 - - 算力消耗实时显示 - - 平台特定的参数配置 - - 生成按钮 - - 任务列表和状态显示 - - 集成3D模型预览器 (three.js) - - 模型下载功能 - - 移动端适配: - - mobile/3dCreate.vue - 移动端3D生成页面 - - 保持Tab切换功能 - - 响应式设计,触控优化 - - 🛠️ 管理后台 - - 管理功能: - - admin/3d/3dJobs.vue - 任务管理列表 - - admin/3d/3dSetting.vue - API配置页面 - - 模型配置管理: - - 分平台配置API秘钥和模型列表 - - 设置每个模型的算力消耗值 - - API密钥和端点配置 - - 🔌 API集成方案 - - 腾讯云集成: - - 使用官方Golang SDK - - 支持异步任务提交和状态查询 - - Gitee集成: - - HTTP客户端实现 - - 标准化响应处理 - - 🎯 核心功能特性 - - - 平台切换:通过CustomTab在魔力方舟和腾讯混元间切换 - - 模型选择:每个平台支持不同的3D模型 - - 动态算力:切换模型时实时更新算力消耗显示 - - 参数隔离:不同平台的参数配置完全分离 - - 3D预览:集成Three.js实现模型预览 - - 统一体验:保持与JiMeng.vue相似的交互风格 - - 📱 用户体验 - - - JiMeng.vue风格的简洁界面 - - Tab切换流畅的平台选择 - - 模型选择时算力消耗实时更新 - - 支持拖拽上传图片 - - 实时任务状态显示 - - 3D模型交互式预览 - - 这个设计将创建一个与现有JiMeng功能风格一致的3D生成模块,通过Tab分离实现平台参数的清晰管理。 - -整个实现严格按照现有系统的代码规范和架构模式,与 JiMeng 等模块保持一致的用户体验! diff --git a/docs/plans/ppt-resume-generation.md b/docs/plans/ppt-resume-generation.md new file mode 100644 index 00000000..fcb263c5 --- /dev/null +++ b/docs/plans/ppt-resume-generation.md @@ -0,0 +1,54 @@ +# PPT 断点续生成(简化方案 · 修订版) + +## 设计原则(按产品确认) + +- **不新增数据库字段**,只复用现有 `geekai_ppt_jobs.slides`(JSON)。 +- 分镜结构已在 [`vo.PPTSlideData`](api/store/vo/ppt_job.go) 中体现:`theme/title/points/image_prompt` + `image_url`。 +- **大模型生成分镜后立刻整表写入 `slides`**:每条记录带完整分镜字段,**`image_url` 为空**,表示「待生图」。 +- **列表「继续生成」条件**:`status !== 'completed'` 即可展示(实现时需排除无意义的态:如从未写入分镜的异常数据;**`processing` 时按钮应禁用或显示加载**,避免与进行中的任务重复提交)。 +- **继续生成**:读出 `slides`,**有 `image_url` 则跳过**,无则取当条分镜调生图 API;**每成功一张就更新一次数据库**。 +- **并发更新 `slides`**:多 goroutine 同时写同一任务的 `slides` 字段时,必须 **加锁**(推荐在 [`PptService`](api/service/ppt/ppt_service.go) 内 **`sync.Mutex` 按 `task_id` 维度**保护「读 JSON → 改一条 slide → 写回」的整段逻辑,避免后写覆盖先写)。 + +## 后端改动要点 + +### 1. `RunTask` 流程调整([`ppt_service.go`](api/service/ppt/ppt_service.go)) + +1. `GenerateSlides` 成功后,将 `[]slidePlan` 转为 `[]SlideData` / `vo.PPTSlides`,**全部 `ImageURL` 置空**,一次性 `Save` / `Updates` 写入 `slides`,并 `SetTotalSlides(total)`。 +2. **`completed_slides` 语义**:表示「已有配图」的页数,等于 `slides` 中 `image_url` 非空的条数(或单独在更新时维护,与 `len(slides)` 区分)。 +3. 抽取 **`runSlideImageGeneration`**:对「需要生图」的条目并发生图;每条成功后调用新的 **`applySlideImage(taskID, slideIndex, url)`**(内部持锁、按 index 更新对应元素的 `image_url`,再扣算力)。 + +### 2. 替换「仅 append」的 `UpdateProgress` + +- 现有 `UpdateProgress` 是 **append** 一条 slide,与「先写满占位再填图」冲突。 +- 改为 **按 `slide_index` 原地更新** 指定项的 `image_url`(及必要时 `thumb`),并在持锁下 **整份序列化写回**。 + +### 3. `ResumeTask` / `POST .../resume` + +- 校验用户、任务存在;若 `status === processing` 建议直接拒绝或返回「任务进行中」。 +- 若 `status !== completed`:加载 `slides`,筛出 `image_url` 为空的项,**仅对这些项**走与 `RunTask` 相同的生图 + `applySlideImage`。 +- 全部非空后:`UpdateStatus(completed)`,清空或保留 `err_msg` 按产品定。 +- 若某次仍失败:可 `MarkAsFailed` 保留已生成页(与现网一致)。 + +### 4. 失败路径 + +- 任一页生图失败时,仍可 `MarkAsFailed`,但 **分镜与已生成图均已在 `slides` 中**,续跑只需补空 `image_url`。 + +## 前端([`PPTCreate.vue`](web/src/views/PPTCreate.vue)) + +- 列表与详情:当 **`status !== 'completed'`**(且建议 **`status !== 'processing'` 才可点**)显示「继续生成」,**`el-tooltip`** 文案:继续生成尚未完成的幻灯片,已有页面保持不变。 +- 调用 `POST /api/v1/tasks/:id/resume`(路由注册方式同既有 export)。 + +## 与旧方案差异 + +- **不增加** `slide_plans` 等字段;**不重复存储**分镜,全部以 `slides` 中「无图占位」表达待生成状态。 + +## 验证 + +- 人为让第 N 页失败:列表出现继续生成,点击后仅补第 N 页及之后空图项。 +- 并发压测:多页同时完成时 `slides` JSON 无丢失、无覆盖。 + +## 实施状态(已实现) + +- 后端:`saveSlidesOutline` + `ApplySlideImage`(`sync.Map` 按 `task_id` 互斥)、`runSlideImageJobs`、`ResumeTask`、`POST /api/v1/tasks/:task_id/resume`。 +- 前端:`PPTCreate.vue` 列表/详情「继续生成」+ `el-tooltip`,`processing` 时禁用或 loading。 +- **旧任务**:若 `slides` 条数小于 `total_slides`(无完整占位),`ResumeTask` 返回不可续跑。 diff --git a/docs/requirement.md b/docs/requirement.md new file mode 100644 index 00000000..3e3a07fc --- /dev/null +++ b/docs/requirement.md @@ -0,0 +1,122 @@ +# 📄 PRD: 智能演示文稿 (PPT) 生成后端 API (Agentic Workflow) + +## 1. 项目概述 (Project Overview) + +本项目旨在开发一个基于 Go (Gin 框架) 的后端 API 服务。该服务接收用户提供的「文本大纲」和「设计提示词(可选)」,通过大语言模型 (LLM) 进行结构化内容拆解与提示词工程,并利用并发调度图像生成模型 (如 Nano Banana 2) 生成幻灯片配图。 + +为保证系统稳定性和极佳的用户体验,系统采用**异步任务+前端定时轮询(3秒/次)**的架构。后端负责核心的并发控制、速率限制(Rate Limiting)和状态管理。 + +## 2. 技术栈约束 (Tech Stack) + +- **语言/框架**: Go 1.21+, Gin Web Framework +- **并发控制**: `golang.org/x/sync/errgroup` +- **速率限制**: `golang.org/x/time/rate` +- **状态管理**: `sync.Map` (内存级,后续可扩展至 Redis) +- **数据交互**: JSON 格式,RESTful 风格 + +## 3. 核心 API 规范 (API Specification) + +### 3.1 创建生成任务 (Create Task) + +接收用户的原始文本和要求,立即返回任务 ID,不阻塞等待生成结果。 + +- **Endpoint**: `POST /api/v1/tasks/generate-slides` +- **Request Body**: + +```json +{ + "content": "用户的 Markdown 笔记或大纲内容 (必填)", + "prompt": "用户的附加设计要求,如:极简商务风、卡通手绘风 (选填)", + "language": "用户选择的语言,如:中文、英文,必填", + "pages": "用户选择的页数,如:5-20,必填" +} +``` + +- **Response Body**: + +```json +{ + "code": 200, + "message": "Task created successfully", + "data": { + "task_id": "uuid-v4-string", + "status": "pending" + } +} +``` + +### 3.2 查询任务进度 (Query Task Progress) + +前端每隔 3 秒调用一次此接口,获取最新进度和已生成的幻灯片数据。 + +- **Endpoint**: `GET /api/v1/tasks/:task_id` +- **Response Body**: + +```json +{ + "code": 200, + "data": { + "task_id": "uuid-v4-string", + "status": "processing", // 状态枚举: pending, processing, completed, failed + "progress": { + "total_slides": 10, + "completed_slides": 3, + "percentage": 30 + }, + "slides": [ + { + "slide_index": 1, + "theme": "早晨环节", + "title": "第一关:起床咕噜咕噜喝温水!", + "points": ["水量:300-500ml", "唤醒身体小怪兽"], + "image_url": "https://example.com/generated-image-url-1.png" + } + ], + "error_message": "" // 仅在 status 为 failed 时返回具体错误 + } +} +``` + +## 4. 核心工作流与状态机 (State Machine & Workflow) + +### 4.1 任务初始化阶段 + +1. 接收 `POST` 请求,生成全局唯一的 `task_id`。 +2. 在 `TaskManager` (基于 `sync.Map`) 中初始化任务实例,初始化状态 `status = pending`,并加上并发安全锁 (`sync.Mutex`)。 +3. 启动独立的后台 Goroutine 接管耗时业务,主线程立即向客户端返回 `task_id`。 + +### 4.2 后台异步执行阶段 + +1. **LLM 解析 (状态变更: processing)** + 调用大语言模型解析传入的 `content` 和 `prompt`。必须注入以下 System Prompt 常量约束大模型输出: + > # Role + +> 你是一位顶级的专业演示文稿(PPT)策划专家和 AI 图像提示词(Prompt)工程师。任务是根据用户提供的「内容大纲」或「设计要求」,生成一套逻辑清晰、视觉风格高度统一的幻灯片分镜数据。 + +> # Rules + +> 1. 全局风格锚定:根据大纲推断或遵循用户要求的全局视觉风格。所有配图必须严格遵循此风格。 +> 2. 结构化拆解:合理拆分为多张幻灯片,单页最多 3-4 个简短要点。 +> 3. 视觉转译:为每页构思具体的画面描述 (image_prompt)。必须包含前缀 `[全局风格描述]`,必须包含后缀 `[画面左侧或右侧留出干净的纯色或虚化空间,用于排版文字。绝对不要在图片中生成任何英文字母、汉字或乱码。]` +> 4. 严格输出合法的纯 JSON 数组 `[{"slide_index": 1, "theme": "...", "title": "...", "points": ["..."], "image_prompt": "..."}]`,禁止使用 Markdown 标记。 + +2. **获取总数** + 解析成功后,更新该任务的 `total_slides` 字段,`completed_slides` 保持为 0。 +3. **并发绘图与限流调度 (核心挑战)** + +- 启动 `errgroup.WithContext` 进行并发调度,设置 `g.SetLimit(3)` 控制最大并发线程。 +- 引入令牌桶限流器 `rate.NewLimiter(rate.Every(1*time.Second), 1)` 控制 API 外部请求频率(QPS = 1)。 +- 必须实现**指数退避重试 (Exponential Backoff)**:遇到 HTTP 429 错误时,等待 2s, 4s, 8s 后重试,最高重试 3 次。 +- **进度更新**:每成功生成一张图片,必须获取 `Task` 实例的写锁,将 `completed_slides` 加 1,并将完整的 `SlideData` 追加到任务的 `slides` 数组中,以便前端下一秒轮询时能拿到最新切片。 + +### 4.3 任务收尾阶段 + +- 正常结束:`errgroup.Wait()` 返回 nil,状态变更为 `completed`。 +- 异常中断:发生超过最大重试次数的错误或解析失败,状态变更为 `failed`,并记录错误原因至 `error_message`。 + +请按照以下顺序逐步实现 Go 后端代码,每完成一步请与我确认: + +1. **核心模型与接口定义**:定义 `Task` 结构体(包含 sync.Mutex)、DTO 结构体以及 Gin 的路由组搭建。 +2. **状态管理器实现**:实现一个基于 `sync.Map` 的 `TaskManager`,提供安全的 `CreateTask`、`GetTask`、`UpdateProgress` 和 `MarkAsFailed` 方法。 +3. **限流与重试 HTTP 客户端**:实现 `callImageAPIWithRetry` 函数,内部封装 `rate.Limiter` 的等待逻辑和针对 429 状态码的指数退避重试逻辑(可先用 Mock 数据代替真实 HTTP 请求)。 +4. **组装核心 Handler**:实现 `POST` 和 `GET` 控制器。在 `POST` 的后台 Goroutine 中,串联 LLM Mock 解析和 `errgroup` 并发画图流程,确保在循环中安全地调用 `TaskManager.UpdateProgress` 更新进度和结果数组。 diff --git a/web/.env.development b/web/.env.development index 5cb87d98..0da53c54 100644 --- a/web/.env.development +++ b/web/.env.development @@ -6,7 +6,7 @@ VITE_ADMIN_USER=admin VITE_ADMIN_PASS=admin123 VITE_KEY_PREFIX=GeekAI_DEV_ VITE_TITLE="Geek-AI 创作系统" -VITE_VERSION=v4.2.8 +VITE_VERSION=v4.3.0 VITE_DOCS_URL=https://docs.geekai.me VITE_GITHUB_URL=https://github.com/yangjian102621/geekai VITE_GITEE_URL=https://gitee.com/blackfox/geekai diff --git a/web/.env.production b/web/.env.production index 0efb4cf4..b6073e0f 100644 --- a/web/.env.production +++ b/web/.env.production @@ -1,7 +1,7 @@ VITE_API_HOST= VITE_WS_HOST= VITE_KEY_PREFIX=GeekAI_ -VITE_VERSION=v4.2.8 +VITE_VERSION=v4.3.0 VITE_TITLE="Geek-AI 创作系统" VITE_DOCS_URL=https://docs.geekai.me VITE_GITHUB_URL=https://github.com/yangjian102621/geekai diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 00000000..0b2a2d7b --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,7446 @@ +{ + "name": "geekai-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "geekai-web", + "version": "0.1.0", + "dependencies": { + "@better-scroll/core": "^2.5.1", + "@better-scroll/mouse-wheel": "^2.5.1", + "@better-scroll/observe-dom": "^2.5.1", + "@better-scroll/pull-up": "^2.5.1", + "@better-scroll/scroll-bar": "^2.5.1", + "@element-plus/icons-vue": "^2.3.1", + "@microsoft/fetch-event-source": "^2.0.1", + "animate.css": "^4.1.1", + "axios": "^0.27.2", + "clipboard": "^2.0.11", + "compressorjs": "^1.2.1", + "core-js": "^3.8.3", + "echarts": "^5.5.0", + "element-plus": "^2.4.0", + "good-storage": "^1.1.1", + "highlight.js": "^11.7.0", + "json-bigint": "^1.0.0", + "lodash": "^4.17.21", + "markdown-it": "^13.0.1", + "markdown-it-emoji": "^2.0.0", + "markdown-it-mathjax3": "^4.3.2", + "marked": "^15.0.11", + "markmap-common": "^0.16.0", + "markmap-lib": "^0.16.1", + "markmap-toolbar": "^0.17.0", + "markmap-view": "^0.16.0", + "md-editor-v3": "^2.2.1", + "memfs": "^4.9.3", + "pinia": "^2.1.4", + "qrcode": "^1.5.3", + "qs": "^6.11.1", + "sortablejs": "^1.15.0", + "three": "^0.160.0", + "unplugin-auto-import": "^0.18.5", + "vant": "^4.5.0", + "vue": "^3.2.13", + "vue-router": "^4.0.15", + "vue-waterfall-plugin-next": "^2.6.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "sass-embedded": "^1.89.2", + "stylus": "^0.58.1", + "stylus-loader": "^7.0.0", + "tailwindcss": "^3.4.17", + "vite": "^5.4.10" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@better-scroll/core": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@better-scroll/core/-/core-2.5.1.tgz", + "integrity": "sha512-koKOuYA55dQ04FJRIVUpMGDr1hbCfWmfX0MGp1hKagkQSWSRpwblqACiwtggVauoj9aaJRJZ9hDsTM4weaavlg==", + "license": "MIT", + "dependencies": { + "@better-scroll/shared-utils": "^2.5.1" + } + }, + "node_modules/@better-scroll/mouse-wheel": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@better-scroll/mouse-wheel/-/mouse-wheel-2.5.1.tgz", + "integrity": "sha512-DGnrirRMY6zMM7xwgx09D/cA9A//3J1/uDkq8iBVEyE5p0sEr/keQpjEfFHGkBRa505BnbBwdbN6f5lugEDSPw==", + "license": "MIT", + "dependencies": { + "@better-scroll/core": "^2.5.1" + } + }, + "node_modules/@better-scroll/observe-dom": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@better-scroll/observe-dom/-/observe-dom-2.5.1.tgz", + "integrity": "sha512-TCMGFLRfpXBPIwtUV/efliUmfmrhSNI7NXdSyjdWjsLOS7dh3eFkmcom5ERVWMaXVELSmujGXLqobT+dT0C/jg==", + "license": "MIT", + "dependencies": { + "@better-scroll/core": "^2.5.1" + } + }, + "node_modules/@better-scroll/pull-up": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@better-scroll/pull-up/-/pull-up-2.5.1.tgz", + "integrity": "sha512-1hu3xSMxdB8T391KffpNZ7g93lMwZEHjfb1F1Y4KvIkciDt8nXqkGpqrZF+YwR+EJTgYcWqUO8kgmI6XXu7Pkg==", + "dependencies": { + "@better-scroll/core": "^2.5.1" + } + }, + "node_modules/@better-scroll/scroll-bar": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@better-scroll/scroll-bar/-/scroll-bar-2.5.1.tgz", + "integrity": "sha512-i6r60pWG/ztkFK2j5Gj54I0LJb2jGh5TWJNQBoW0gUkp28B+0JvBFTwZn9tF7beZCBorKR7Hvvu4O9A1TJy94Q==", + "dependencies": { + "@better-scroll/core": "^2.5.1" + } + }, + "node_modules/@better-scroll/shared-utils": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@better-scroll/shared-utils/-/shared-utils-2.5.1.tgz", + "integrity": "sha512-AplkfSjXVYP9LZiD6JsKgmgQJ/mG4uuLmBuwLz8W5OsYc7AYTfN8kw6GqZ5OwCGoXkVhBGyd8NeC4xwYItp0aw==" + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.10.1", + "resolved": "https://registry.npmmirror.com/@bufbuild/protobuf/-/protobuf-2.10.1.tgz", + "integrity": "sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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.7.3", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@gera2ld/jsx-dom": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/@gera2ld/jsx-dom/-/jsx-dom-2.2.2.tgz", + "integrity": "sha512-EOqf31IATRE6zS1W1EoWmXZhGfLAoO9FIlwTtHduSrBdud4npYBxYAkv8dZ5hudDPwJeeSjn40kbCL4wAzr8dA==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.21.5" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmmirror.com/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@microsoft/fetch-event-source": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", + "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "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-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "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.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "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.7", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", + "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", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.2", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.10.2.tgz", + "integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", + "license": "MIT" + }, + "node_modules/@vant/popperjs": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@vant/popperjs/-/popperjs-1.3.0.tgz", + "integrity": "sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==", + "license": "MIT" + }, + "node_modules/@vant/use": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@vant/use/-/use-1.6.0.tgz", + "integrity": "sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/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/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "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/reactivity": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.25.tgz", + "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.25.tgz", + "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", + "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.25.tgz", + "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "vue": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "license": "MIT", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.8", + "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.8.tgz", + "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/animate.css": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/animate.css/-/animate.css-4.1.1.tgz", + "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==", + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "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/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/autolinker": { + "version": "3.16.2", + "resolved": "https://registry.npmmirror.com/autolinker/-/autolinker-3.16.2.tgz", + "integrity": "sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.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==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.5", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz", + "integrity": "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blueimp-canvas-to-blob": { + "version": "3.29.0", + "resolved": "https://registry.npmmirror.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz", + "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==", + "license": "MIT" + }, + "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": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-builder": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/buffer-builder/-/buffer-builder-0.2.0.tgz", + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", + "dev": true, + "license": "MIT/X11" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmmirror.com/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/cheerio-select/-/cheerio-select-1.6.0.tgz", + "integrity": "sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==", + "license": "BSD-2-Clause", + "dependencies": { + "css-select": "^4.3.0", + "css-what": "^6.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.3.1", + "domutils": "^2.8.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "license": "MIT", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "dev": true, + "license": "MIT" + }, + "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/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compressorjs": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/compressorjs/-/compressorjs-1.2.1.tgz", + "integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==", + "license": "MIT", + "dependencies": { + "blueimp-canvas-to-blob": "^3.29.0", + "is-blob": "^2.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.47.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "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/css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-flextree": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/d3-flextree/-/d3-flextree-2.1.2.tgz", + "integrity": "sha512-gJiHrx5uTTHq44bjyIb3xpbmmdZcWLYPKeO9EPVOq8EylMFOiH2+9sWqKAiQ4DcFuOZTAxPOQyv0Rnmji/g15A==", + "license": "WTFPL", + "dependencies": { + "d3-hierarchy": "^1.1.5" + } + }, + "node_modules/d3-flextree/node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "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/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?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": "4.3.1", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "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/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/element-plus": { + "version": "2.12.0", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.12.0.tgz", + "integrity": "sha512-M9YLSn2np9OnqrSKWsiXvGe3qnF8pd94+TScsHj1aTMCD+nSEvucXermf807qNt6hOP040le0e5Aft7E9ZfHmA==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "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-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmmirror.com/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "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/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "license": "MIT", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/good-storage": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/good-storage/-/good-storage-1.1.1.tgz", + "integrity": "sha512-+0UEpRT03fJgEJoeTVVz41ISC5+jaQrDIWatQ4BoDQfOiBBsjNanymi9sAtLFdEAADQN0FYR8mGpeqxSCe8oXQ==", + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "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/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-blob": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-blob/-/is-blob-2.1.0.tgz", + "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/juice": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/juice/-/juice-8.1.0.tgz", + "integrity": "sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==", + "license": "MIT", + "dependencies": { + "cheerio": "1.0.0-rc.10", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/katex": { + "version": "0.16.27", + "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.27.tgz", + "integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "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/markdown-it": { + "version": "13.0.2", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-emoji": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz", + "integrity": "sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==", + "license": "MIT" + }, + "node_modules/markdown-it-mathjax3": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/markdown-it-mathjax3/-/markdown-it-mathjax3-4.3.2.tgz", + "integrity": "sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==", + "license": "MIT", + "dependencies": { + "juice": "^8.0.0", + "mathjax-full": "^3.2.0" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/markmap-common": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/markmap-common/-/markmap-common-0.16.0.tgz", + "integrity": "sha512-q3nlNDMKuWXTm3VwZFY9V5zteL/+iBLZanUK5vS+e26bUbzTSG5VtAzsyJbmgJm1WhwmIIAxbXEnp6JdvtTduA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.6", + "@gera2ld/jsx-dom": "^2.2.2", + "npm2url": "^0.2.4" + } + }, + "node_modules/markmap-html-parser": { + "version": "0.16.1", + "resolved": "https://registry.npmmirror.com/markmap-html-parser/-/markmap-html-parser-0.16.1.tgz", + "integrity": "sha512-/Mgm4g1qMQ8uEOz8h8K+jPspdgjfw29NqmfTLZSt8yG+vW7fWWduPjGRFc5axAZxCzP7PTzZLEuOxAqOwEg8Bg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.6", + "cheerio": "1.0.0-rc.12" + }, + "peerDependencies": { + "markmap-common": "*" + } + }, + "node_modules/markmap-html-parser/node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmmirror.com/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/markmap-html-parser/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/markmap-html-parser/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/markmap-html-parser/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/markmap-html-parser/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/markmap-html-parser/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/markmap-html-parser/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/markmap-html-parser/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "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.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/markmap-html-parser/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/markmap-html-parser/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/markmap-html-parser/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/markmap-lib": { + "version": "0.16.1", + "resolved": "https://registry.npmmirror.com/markmap-lib/-/markmap-lib-0.16.1.tgz", + "integrity": "sha512-jD8VsB67m677IRehGSwwVJDlC6PS+xzDKsJOwdvjZ+ndfXrHa1lyqfvR6mIwvGGUIciF86YEITSKL9hQTHE4Rw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.6", + "highlight.js": "^11.8.0", + "js-yaml": "^4.1.0", + "katex": "^0.16.8", + "markmap-html-parser": "0.16.1", + "markmap-view": "0.16.0", + "prismjs": "^1.29.0", + "remarkable": "^2.0.1", + "remarkable-katex": "^1.2.1" + }, + "peerDependencies": { + "markmap-common": "*" + } + }, + "node_modules/markmap-toolbar": { + "version": "0.17.2", + "resolved": "https://registry.npmmirror.com/markmap-toolbar/-/markmap-toolbar-0.17.2.tgz", + "integrity": "sha512-WQ05P2xvQmZT0ybRUE0uRzrs30aXlJ6/yEUsA6A9nYEwm8T9jSwBxIM/5zYlkH/XzUcsRRxtCa4k1IWR74gkpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.6", + "@gera2ld/jsx-dom": "^2.2.2" + }, + "peerDependencies": { + "markmap-common": "*" + } + }, + "node_modules/markmap-view": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/markmap-view/-/markmap-view-0.16.0.tgz", + "integrity": "sha512-JOiSEThs8B4bAP9E6rcCWOz2SsMwCBFaR76wLARRVb04C/qLiLmvrm675kNPq4lRBAwtugHCYvjG0otpSlB4Cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.6", + "@gera2ld/jsx-dom": "^2.2.2", + "@types/d3": "^7.4.0", + "d3": "^7.8.5", + "d3-flextree": "^2.1.2" + }, + "peerDependencies": { + "markmap-common": "*" + } + }, + "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/mathjax-full": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/mathjax-full/-/mathjax-full-3.2.1.tgz", + "integrity": "sha512-aUz9o16MGZdeiIBwZjAfUBTiJb7LRqzZEl1YOZ8zQMGYIyh1/nxRebxKxjDe9L+xcZCr2OHdzoFBMcd6VnLv9Q==", + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, + "node_modules/md-editor-v3": { + "version": "2.11.3", + "resolved": "https://registry.npmmirror.com/md-editor-v3/-/md-editor-v3-2.11.3.tgz", + "integrity": "sha512-SCfS4qMy0HldFdplcIGUMCpSv8qkNWkYShSdv2gTHeViKduA34zV89BOrWcqls2EZSlvt2n3G7nHRzYUvJjDKw==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.47" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, + "node_modules/memfs": { + "version": "4.51.1", + "resolved": "https://registry.npmmirror.com/memfs/-/memfs-4.51.1.tgz", + "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "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/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "license": "Apache-2.0" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "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": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "license": "Apache-2.0" + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/npm2url": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/npm2url/-/npm2url-0.2.4.tgz", + "integrity": "sha512-arzGp/hQz0Ey+ZGhF64XVH7Xqwd+1Q/po5uGiBbzph8ebX6T0uvt3N7c1nBHQNsQVykQgHhqoRTX7JFcHecGuw==", + "license": "MIT" + }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "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": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/remarkable": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/remarkable/-/remarkable-2.0.1.tgz", + "integrity": "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.10", + "autolinker": "^3.11.0" + }, + "bin": { + "remarkable": "bin/remarkable.js" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/remarkable-katex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/remarkable-katex/-/remarkable-katex-1.2.1.tgz", + "integrity": "sha512-Y1VquJBZnaVsfsVcKW2hmjT+pDL7mp8l5WAVlvuvViltrdok2m1AIKmJv8SsH+mBY84PoMw67t3kTWw1dIm8+g==", + "license": "MIT" + }, + "node_modules/remarkable/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "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.95.1", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.95.1.tgz", + "integrity": "sha512-uPoDh5NIEZV4Dp5GBodkmNY9tSQfXY02pmCcUo+FR1P+x953HGkpw+vV28D4IqYB6f8webZtwoSaZaiPtpTeMg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded/-/sass-embedded-1.95.1.tgz", + "integrity": "sha512-l086+s40Z0qP7ckj4T+rI/7tZcwAfcKCG9ah9A808yINWOxZFv0kO0u/UHhR4G9Aimeyax/JNvqh8RE7z1wngg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "buffer-builder": "^0.2.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.0.2", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.95.1", + "sass-embedded-android-arm": "1.95.1", + "sass-embedded-android-arm64": "1.95.1", + "sass-embedded-android-riscv64": "1.95.1", + "sass-embedded-android-x64": "1.95.1", + "sass-embedded-darwin-arm64": "1.95.1", + "sass-embedded-darwin-x64": "1.95.1", + "sass-embedded-linux-arm": "1.95.1", + "sass-embedded-linux-arm64": "1.95.1", + "sass-embedded-linux-musl-arm": "1.95.1", + "sass-embedded-linux-musl-arm64": "1.95.1", + "sass-embedded-linux-musl-riscv64": "1.95.1", + "sass-embedded-linux-musl-x64": "1.95.1", + "sass-embedded-linux-riscv64": "1.95.1", + "sass-embedded-linux-x64": "1.95.1", + "sass-embedded-unknown-all": "1.95.1", + "sass-embedded-win32-arm64": "1.95.1", + "sass-embedded-win32-x64": "1.95.1" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.95.1.tgz", + "integrity": "sha512-ObGM3xSHEK2fu89GusvAdk1hId3D1R03CyQ6/AVTFSrcBFav1a3aWUmBWtImzf5LsVzliRnlAPPS6+rT/Ghb1A==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "sass": "1.95.1" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.95.1.tgz", + "integrity": "sha512-siaN1TVEjhBP4QJ5UlDBRhyKmMbFhbdcyHj0B4hIuNcinuVprP6tH1NT0NkHvkXh2egBmTvjzZgJ1ySsCB32JA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.95.1.tgz", + "integrity": "sha512-E+3vZXhUOVHFiSITH2g53/ynxTG4zz8vTVrXGAKkZQwSe6aCO22uc1Pah23F3jOrDNF/YLrsyp82T/CIIczK3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.95.1.tgz", + "integrity": "sha512-UcPcr5JXVtInD+/XE+2DhwPsALUdRAHyippnnAP6MtdaT3+AnqqvzSVy9Gb6SKyeqEk4YxPmIlQpZCVODDT4eA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.95.1.tgz", + "integrity": "sha512-sW/TO+B0Wq9VDTa7YiO74DW4iF9jEYds+9yslaHtc69r/Ch+Zj+ZB6HeJysfmen91zn5CLJDGrnTSrIk+/COfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.95.1.tgz", + "integrity": "sha512-SWTCwszlBzjin35T2OiGZSDRbC/sqg5Mjepih18lelELrz14eB9LcFTZeiqDfdnwx6qQqPWj2VufCpExr8jElA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.95.1.tgz", + "integrity": "sha512-0GZEgkE1e8E2h97lUtwgZbKHrJYmRE/KhWQBHv6ZueAto8DJcAFNFrIQiQoRJjraE6QTaw6ahSvc1YJ7gL4OQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.95.1.tgz", + "integrity": "sha512-zUAm/rztm5Uyy+DSs408VJg404siVgUuZyqId4tFwkPNC5WRKu25Z8bFMriyGaE4YfEqbNwFV07C16mJoGeVOA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.95.1.tgz", + "integrity": "sha512-MQxa+qVX7Os2rMpJ/AvhWup+1cS0JieQgCfi9cz1Zckn4zaUhg35+m2FQhfKvzv4afeW5bubTMOQeTRMQujbXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.95.1.tgz", + "integrity": "sha512-gNdaGmM3nZ0jkFNmyXWyNlXZPdaMP+7n5Mk3yGFGShqRt/6T/bHh5SkyNnU2ZdP1z7R9poPItJhULrZJ42ETeA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.95.1.tgz", + "integrity": "sha512-8lD5vHGzBjBRCMIr9CXCyjmy8Q1q+H4ygcYCIm/aPNYhrm9uPOzJfs8hv9kDRgRAASFkcPGlFw8tDH4QqiJ5wg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.95.1.tgz", + "integrity": "sha512-WjKfHxnFc/jOL5QtmgYuiWCc4616V15DkpE+7z41JWEawRXku6w++w7AR+Zx/jbz93FZ/AsZp27IS3XUt80u3Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.95.1.tgz", + "integrity": "sha512-3U6994SRUUmC8mPvSG/vNLUo2ZcGv3jHuPoBywTbJhGQI8gq0hef1MY8TU5mvtj9DhQYlah6MYktM4YrOQgqcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.95.1.tgz", + "integrity": "sha512-CJ0tEEQnfpJEMCQrdubLsmuVc/c66EgaCAO0ZgSJ/KpxBKF3O1lHN6e1UErRf6VO0rh8ExAOh75po12Vu849Og==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.95.1.tgz", + "integrity": "sha512-nGnzrEpZZOsGOwrRVyX4t15M8ijZWhc4e4lLpOqaPm+lv23HFncfY05WxU5bRj0KAknrkeTM2IX/6veP2aeUdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.95.1.tgz", + "integrity": "sha512-bhywAcadVQoCotD4gVmyMBi2SENPvyLFPrXf33VK5mY487Nf/g5SgGCUuGmfTsbns4NBwbwR7PA/1fnJmeMtdA==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.95.1" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.95.1.tgz", + "integrity": "sha512-RWWODCthWdMVODoq98lyIk9R56mgGJ4TFUjD9LSCe7fAYD/tiTkUabE4AUzkZqknQSYr0n0Q2uy7POSDIKvhVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.95.1", + "resolved": "https://registry.npmmirror.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.95.1.tgz", + "integrity": "sha512-jotHgOQnCb1XdjK0fhsyuhsfox7Y5EkrOc4h2caEpRcNCnsPTBZHqhuc8Lnw8HbKIhwKYkqWhexkjgz62MShhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "license": "ISC" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "license": "MIT (http://mootools.net/license.txt)", + "engines": { + "node": "*" + } + }, + "node_modules/sortablejs": { + "version": "1.15.6", + "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.6.tgz", + "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speech-rule-engine": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/speech-rule-engine/-/speech-rule-engine-4.1.2.tgz", + "integrity": "sha512-S6ji+flMEga+1QU79NDbwZ8Ivf0S/MpupQQiIC0rTpU/ZTKgcajijJJb1OcByBQDjrXCN1/DJtGz4ZJeBMPGJw==", + "license": "Apache-2.0", + "dependencies": { + "@xmldom/xmldom": "0.9.8", + "commander": "13.1.0", + "wicked-good-xpath": "1.3.0" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/stylus": { + "version": "0.58.1", + "resolved": "https://registry.npmmirror.com/stylus/-/stylus-0.58.1.tgz", + "integrity": "sha512-AYiCHm5ogczdCPMfe9aeQa4NklB2gcf4D/IhzYPddJjTgPc+k4D/EVE0yfQbZD43MHP3lPy+8NZ9fcFxkrgs/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "css": "^3.0.0", + "debug": "^4.3.2", + "glob": "^7.1.6", + "sax": "~1.2.4", + "source-map": "^0.7.3" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + } + }, + "node_modules/stylus-loader": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/stylus-loader/-/stylus-loader-7.1.3.tgz", + "integrity": "sha512-TY0SKwiY7D2kMd3UxaWKSf3xHF0FFN/FAfsSqfrhxRT/koXTwffq2cgEWDkLQz7VojMu7qEEHt5TlMjkPx9UDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.12", + "normalize-path": "^3.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "stylus": ">=0.52.4", + "webpack": "^5.0.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/sync-message-port/-/sync-message-port-1.1.3.tgz", + "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.18", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.15", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz", + "integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/three": { + "version": "0.160.1", + "resolved": "https://registry.npmmirror.com/three/-/three-0.160.1.tgz", + "integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==", + "license": "MIT" + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/unimport": { + "version": "3.14.6", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" + } + }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "license": "MIT" + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/unimport/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.18.6", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.18.6.tgz", + "integrity": "sha512-LMFzX5DtkTj/3wZuyG5bgKBoJ7WSgzqSGJ8ppDRdlvPh45mx6t6w3OcbExQi53n3xF5MYkNGPNR/HYOL95KL2A==", + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.3", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.1", + "magic-string": "^0.30.14", + "minimatch": "^9.0.5", + "unimport": "^3.13.4", + "unplugin": "^1.16.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-auto-import/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/unplugin-auto-import/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/vant": { + "version": "4.9.21", + "resolved": "https://registry.npmmirror.com/vant/-/vant-4.9.21.tgz", + "integrity": "sha512-hXUoZMrLLjykimFRLDlGNd+K2iYSRh9YwLMKnsVdVZ+9inUKxpqnjhOqlZwocbnYkvJlS+febf9u9aJpDol4Pw==", + "license": "MIT", + "dependencies": { + "@vant/popperjs": "^1.3.0", + "@vant/use": "^1.6.0", + "@vue/shared": "^3.5.17" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "dev": true, + "license": "MIT" + }, + "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", + "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/vue": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz", + "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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.3", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.3.tgz", + "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-waterfall-plugin-next": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/vue-waterfall-plugin-next/-/vue-waterfall-plugin-next-2.6.9.tgz", + "integrity": "sha512-b6DGrH39RHPebRIVvj3kxBxJABbQx288YXrhBTpdzeA4aHQmHtqpvH8xwOHO+mkmz4WoXrTA5TapzMXjkVZKfQ==", + "license": "UNLICENSED" + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.103.0", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/web/src/App.vue b/web/src/App.vue index 6576e1e8..dcf36acf 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -8,7 +8,7 @@ import { checkSession, getSystemInfo } from '@/store/cache' import { useSharedStore } from '@/store/sharedata' import { showMessageInfo } from '@/utils/dialog' -import { isChrome, isMobile } from '@/utils/libs' +import { initThumbTemplate, isChrome, isMobile } from '@/utils/libs' import { ElConfigProvider } from 'element-plus' import { onMounted } from 'vue' @@ -34,6 +34,9 @@ window.ResizeObserver = class ResizeObserver extends _ResizeObserver { const store = useSharedStore() onMounted(() => { + // 初始化缩略图模板(从后端加载并缓存到localStorage) + initThumbTemplate() + // 获取系统参数 getSystemInfo().then((res) => { const link = document.createElement('link') @@ -53,10 +56,8 @@ onMounted(() => { // 设置主题 document.documentElement.setAttribute('data-theme', store.theme) -}) -// 打印 banner -const banner = ` + const banner = ` .oooooo. oooo .o. ooooo d8P' 'Y8b 888 .888. 888 888 .ooooo. .ooooo. 888 oooo .8"888. 888 @@ -65,17 +66,18 @@ const banner = ` '88. .88' 888 .o 888 .o 888 88b. .8' 888. 888 Y8bood8P' Y8bod8P' Y8bod8P' o888o o888o o88o o8888o o888o ` -console.log('%c' + banner + '', 'color: purple;font-size: 18px;') + console.log('%c' + banner + '', 'color: purple;font-size: 18px;') -console.log( - '%c感谢大家为 GeekAI 做出的卓越贡献!', - "color: green;font-size: 40px;font-family: '微软雅黑';" -) -console.log( - '%c项目源码:https://github.com/yangjian102621/geekai %c 您的 star 对我们非常重要!', - "color: green;font-size: 20px;font-family: '微软雅黑';", - "color: red;font-size: 20px;font-family: '微软雅黑';" -) + console.log( + '%c感谢大家为 GeekAI 做出的卓越贡献!', + "color: green;font-size: 40px;font-family: '微软雅黑';" + ) + console.log( + '%c项目源码:https://github.com/yangjian102621/geekai %c 您的 star 对我们非常重要!', + "color: green;font-size: 20px;font-family: '微软雅黑';", + "color: red;font-size: 20px;font-family: '微软雅黑';" + ) +}) ")}catch(l){console&&console.log(l)}}h=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),h()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(i=h,o=a.document,z=!1,v(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,p())})}function p(){z||(z=!0,i())}function v(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(v,50)}p()}})(window); \ No newline at end of file +window._iconfont_svg_string_4125778='',(a=>{var l=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var h,i,t,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(l&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}h=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(i=function(){document.removeEventListener("DOMContentLoaded",i,!1),h()},document.addEventListener("DOMContentLoaded",i,!1)):document.attachEvent&&(t=h,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,v())})}function v(){z||(z=!0,t())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}v()}})(window); \ No newline at end of file diff --git a/web/src/assets/iconfont/iconfont.json b/web/src/assets/iconfont/iconfont.json index bba1e767..9f8edc8f 100644 --- a/web/src/assets/iconfont/iconfont.json +++ b/web/src/assets/iconfont/iconfont.json @@ -5,6 +5,216 @@ "css_prefix_text": "icon-", "description": "", "glyphs": [ + { + "icon_id": "6233087", + "name": "PPT图标", + "font_class": "ppt", + "unicode": "e67d", + "unicode_decimal": 59005 + }, + { + "icon_id": "703362", + "name": "pin", + "font_class": "pin", + "unicode": "e9f2", + "unicode_decimal": 59890 + }, + { + "icon_id": "35252746", + "name": "取消固定", + "font_class": "unpin", + "unicode": "e67c", + "unicode_decimal": 59004 + }, + { + "icon_id": "17548043", + "name": "智能地址", + "font_class": "rebot", + "unicode": "e6bf", + "unicode_decimal": 59071 + }, + { + "icon_id": "22095008", + "name": "VR小助手", + "font_class": "rebot2", + "unicode": "e67b", + "unicode_decimal": 59003 + }, + { + "icon_id": "44204797", + "name": "豆包", + "font_class": "doubao", + "unicode": "e67a", + "unicode_decimal": 59002 + }, + { + "icon_id": "46195143", + "name": "doubao", + "font_class": "seedance", + "unicode": "e902", + "unicode_decimal": 59650 + }, + { + "icon_id": "6151242", + "name": "folder", + "font_class": "folder", + "unicode": "e806", + "unicode_decimal": 59398 + }, + { + "icon_id": "6151276", + "name": "calendar-alt", + "font_class": "calendar", + "unicode": "e81a", + "unicode_decimal": 59418 + }, + { + "icon_id": "21096843", + "name": "timer", + "font_class": "timer", + "unicode": "ece1", + "unicode_decimal": 60641 + }, + { + "icon_id": "40481666", + "name": "ai", + "font_class": "aigc", + "unicode": "e676", + "unicode_decimal": 58998 + }, + { + "icon_id": "37654849", + "name": "通义万相", + "font_class": "wan", + "unicode": "e673", + "unicode_decimal": 58995 + }, + { + "icon_id": "43413883", + "name": "MiniMax", + "font_class": "minimax", + "unicode": "e7df", + "unicode_decimal": 59359 + }, + { + "icon_id": "43756975", + "name": "gemini-ai", + "font_class": "gemini", + "unicode": "e674", + "unicode_decimal": 58996 + }, + { + "icon_id": "46182385", + "name": "Sora", + "font_class": "sora", + "unicode": "e675", + "unicode_decimal": 58997 + }, + { + "icon_id": "7057916", + "name": "workflow", + "font_class": "workflow", + "unicode": "e68b", + "unicode_decimal": 59019 + }, + { + "icon_id": "45175337", + "name": "AI_Agent", + "font_class": "ai-agent", + "unicode": "eba3", + "unicode_decimal": 60323 + }, + { + "icon_id": "4577005", + "name": "渠道", + "font_class": "channel2", + "unicode": "e66d", + "unicode_decimal": 58989 + }, + { + "icon_id": "6692801", + "name": "channel", + "font_class": "channel", + "unicode": "e738", + "unicode_decimal": 59192 + }, + { + "icon_id": "8759958", + "name": "warning", + "font_class": "warning", + "unicode": "e668", + "unicode_decimal": 58984 + }, + { + "icon_id": "21161469", + "name": "info", + "font_class": "info", + "unicode": "e66c", + "unicode_decimal": 58988 + }, + { + "icon_id": "44336762", + "name": "9_16", + "font_class": "aspect_9_16", + "unicode": "e65f", + "unicode_decimal": 58975 + }, + { + "icon_id": "44336765", + "name": "2_3", + "font_class": "aspect_2_3", + "unicode": "e660", + "unicode_decimal": 58976 + }, + { + "icon_id": "44336767", + "name": "4_5", + "font_class": "aspect_4_5", + "unicode": "e662", + "unicode_decimal": 58978 + }, + { + "icon_id": "44336769", + "name": "3_4", + "font_class": "aspect_3_4", + "unicode": "e663", + "unicode_decimal": 58979 + }, + { + "icon_id": "44336774", + "name": "4_3", + "font_class": "aspect_4_3", + "unicode": "e664", + "unicode_decimal": 58980 + }, + { + "icon_id": "44336777", + "name": "3_2", + "font_class": "aspect_3_2", + "unicode": "e665", + "unicode_decimal": 58981 + }, + { + "icon_id": "44336781", + "name": "1_1", + "font_class": "aspect_1_1", + "unicode": "e667", + "unicode_decimal": 58983 + }, + { + "icon_id": "44336784", + "name": "5_4", + "font_class": "aspect_5_4", + "unicode": "e669", + "unicode_decimal": 58985 + }, + { + "icon_id": "44336786", + "name": "16_9", + "font_class": "aspect_16_9", + "unicode": "e66b", + "unicode_decimal": 58987 + }, { "icon_id": "10564356", "name": "resize", @@ -558,13 +768,6 @@ "unicode": "e8e9", "unicode_decimal": 59625 }, - { - "icon_id": "35446270", - "name": "AI对话_激活", - "font_class": "AIduihua_jihuo", - "unicode": "e6bb", - "unicode_decimal": 59067 - }, { "icon_id": "39584617", "name": "MidJourney", @@ -579,13 +782,6 @@ "unicode": "e60f", "unicode_decimal": 58895 }, - { - "icon_id": "1227734", - "name": "info", - "font_class": "info", - "unicode": "e6a0", - "unicode_decimal": 59040 - }, { "icon_id": "159969", "name": "more", @@ -721,7 +917,7 @@ }, { "icon_id": "41645421", - "name": "luma-logo", + "name": "luma", "font_class": "luma", "unicode": "e704", "unicode_decimal": 59140 @@ -1048,13 +1244,6 @@ "unicode": "e6c3", "unicode_decimal": 59075 }, - { - "icon_id": "4628503", - "name": "ppt", - "font_class": "ppt", - "unicode": "e642", - "unicode_decimal": 58946 - }, { "icon_id": "6233095", "name": "txt", diff --git a/web/src/assets/iconfont/iconfont.ttf b/web/src/assets/iconfont/iconfont.ttf index 4026bd2d..4041fda3 100644 Binary files a/web/src/assets/iconfont/iconfont.ttf and b/web/src/assets/iconfont/iconfont.ttf differ diff --git a/web/src/assets/iconfont/iconfont.woff b/web/src/assets/iconfont/iconfont.woff index 63c9e1b6..157734bf 100644 Binary files a/web/src/assets/iconfont/iconfont.woff and b/web/src/assets/iconfont/iconfont.woff differ diff --git a/web/src/assets/iconfont/iconfont.woff2 b/web/src/assets/iconfont/iconfont.woff2 index b74db5f0..8a0f3156 100644 Binary files a/web/src/assets/iconfont/iconfont.woff2 and b/web/src/assets/iconfont/iconfont.woff2 differ diff --git a/web/src/components/AgentManagePage.vue b/web/src/components/AgentManagePage.vue new file mode 100644 index 00000000..d18cd4b6 --- /dev/null +++ b/web/src/components/AgentManagePage.vue @@ -0,0 +1,540 @@ + + + + + diff --git a/web/src/components/AttachmentList.vue b/web/src/components/AttachmentList.vue index 701839aa..012f8a03 100644 --- a/web/src/components/AttachmentList.vue +++ b/web/src/components/AttachmentList.vue @@ -73,6 +73,7 @@ import { FormatFileSize, GetFileIcon } from '@/store/system' import { computed } from 'vue' import { httpDownload } from '@/utils/http' import { replaceImg } from '@/utils/libs' +import { showMessageError } from '@/utils/dialog' const props = defineProps({ files: { diff --git a/web/src/components/BindEmail.vue b/web/src/components/BindEmail.vue index 174668cd..3f44c2bc 100644 --- a/web/src/components/BindEmail.vue +++ b/web/src/components/BindEmail.vue @@ -6,17 +6,15 @@ - - - - - +
+ + - - + +
- @@ -25,13 +28,13 @@ @@ -42,21 +45,23 @@ +
+ 粘贴截图 +
+ +
+
在此区域按 Ctrl+V 粘贴截图
+ 关闭 +
+ 1) { return Array.isArray(props.modelValue) ? props.modelValue : [] } else { + if (Array.isArray(props.modelValue)) { + return props.modelValue.length > 0 && props.modelValue[0] ? [props.modelValue[0]] : [] + } return props.modelValue && props.modelValue.length > 0 ? [props.modelValue] : [] } }, @@ -247,21 +283,97 @@ const previewImage = (index) => { previewImageSrc.value = imageList.value[index] previewVisible.value = true } + +const openPasteZone = () => { + showPasteZone.value = true + nextTick(() => { + if (pasteZoneRef.value) { + pasteZoneRef.value.focus() + } + }) +} + +const closePasteZone = () => { + showPasteZone.value = false +} + +const onPaste = (e) => { + const items = e.clipboardData && e.clipboardData.items + if (!items || !items.length) return + + for (let i = 0; i < items.length; i++) { + const item = items[i] + if (item.kind === 'file' && item.type && item.type.startsWith('image/')) { + const file = item.getAsFile() + if (file) { + handleUpload({ file }) + } + break + } + } +} + +// 拖拽排序:同步新顺序到 v-model +const handleSortEnd = (evt) => { + const { oldIndex, newIndex } = evt + if (oldIndex === newIndex) return + const list = [...imageList.value] + const [removed] = list.splice(oldIndex, 1) + list.splice(newIndex, 0, removed) + imageList.value = list + // 调试:拖拽后打印图片列表 + console.log('[ImageUpload] 拖拽排序', { oldIndex, newIndex, imageList: list }) +} + +const initSortable = () => { + if (!uploadListRef.value || sortableInstance) return + sortableInstance = Sortable.create(uploadListRef.value, { + animation: 150, + ghostClass: 'upload-item--sort-ghost', + onEnd: handleSortEnd, + }) +} + +const destroySortable = () => { + if (sortableInstance) { + sortableInstance.destroy() + sortableInstance = null + } +} + +watch( + () => [props.maxCount, imageList.value.length], + () => { + if (props.maxCount > 1 && imageList.value.length > 0) { + nextTick(() => initSortable()) + } else { + destroySortable() + } + }, + { immediate: true } +) + +onBeforeUnmount(destroySortable) diff --git a/web/src/components/ReferenceInput.vue b/web/src/components/ReferenceInput.vue new file mode 100644 index 00000000..a4792fb4 --- /dev/null +++ b/web/src/components/ReferenceInput.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/web/src/components/ResetPass.vue b/web/src/components/ResetPass.vue index a615c0e8..886cfc8e 100644 --- a/web/src/components/ResetPass.vue +++ b/web/src/components/ResetPass.vue @@ -17,9 +17,11 @@ -
- - +
+ + + +
@@ -28,9 +30,11 @@ -
- - +
+ + + +
@@ -46,8 +50,8 @@
@@ -112,10 +116,6 @@ const close = function () { diff --git a/web/src/components/UserInfoDialog.vue b/web/src/components/UserInfoDialog.vue deleted file mode 100644 index e4c4b88d..00000000 --- a/web/src/components/UserInfoDialog.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - - - diff --git a/web/src/components/UserProfile.vue b/web/src/components/UserProfile.vue index 822a7307..5a8899bd 100644 --- a/web/src/components/UserProfile.vue +++ b/web/src/components/UserProfile.vue @@ -43,9 +43,9 @@ 签到
- + 保存 diff --git a/web/src/components/Welcome.vue b/web/src/components/Welcome.vue deleted file mode 100644 index f7e2cbcf..00000000 --- a/web/src/components/Welcome.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - diff --git a/web/src/components/admin/AdminHeader.vue b/web/src/components/admin/AdminHeader.vue index 98041cc9..bcfbfdbf 100644 --- a/web/src/components/admin/AdminHeader.vue +++ b/web/src/components/admin/AdminHeader.vue @@ -158,8 +158,12 @@ const logout = function () { overflow: hidden; height: 50px; font-size: 22px; - background-color: var(--chat-content-bg); + background: var(--admin-header-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-bottom: 1px solid var(--admin-header-border); color: var(--theme-text-color-primary); + transition: background 0.2s ease, border-color 0.2s ease; .collapse-btn { display: flex; @@ -167,11 +171,13 @@ const logout = function () { align-items: center; height: 100%; float: left; - padding: 0 10px; + padding: 0 14px; cursor: pointer; + transition: background 0.2s ease; + border-radius: 0; &:hover { - background-color: #eaecef; + background: var(--admin-header-hover); } } @@ -180,6 +186,16 @@ const logout = function () { display: flex; align-items: center; height: 50px; + + :deep(.el-breadcrumb__item) { + .el-breadcrumb__inner { + color: var(--theme-text-color-secondary); + transition: color 0.2s ease; + } + &:last-child .el-breadcrumb__inner { + color: var(--theme-text-color-primary); + } + } } .header-right { @@ -219,20 +235,31 @@ const logout = function () { .user-name { margin-left: 10px; + cursor: pointer; .el-icon { - color: #303133; + color: var(--theme-text-color-secondary); + transition: color 0.2s ease; + } + + &:hover .el-icon { + color: var(--theme-text-color-primary); } } .user-avatar { + transition: opacity 0.2s ease; + } + + .user-name:hover .user-avatar { + opacity: 0.9; } } } } .el-dropdown-link { - color: #fff; + color: var(--theme-text-color-primary); cursor: pointer; display: flex; align-items: center; @@ -261,7 +288,4 @@ const logout = function () { } } } - -.admin-header { -} diff --git a/web/src/components/admin/AdminSidebar.vue b/web/src/components/admin/AdminSidebar.vue index 5028ddab..faaf8c4b 100644 --- a/web/src/components/admin/AdminSidebar.vue +++ b/web/src/components/admin/AdminSidebar.vue @@ -9,9 +9,9 @@ class="sidebar-el-menu" :default-active="onRoutes" :collapse="sidebar.collapse" - background-color="#324157" - text-color="#bfcbd9" - active-text-color="#20a0ff" + background-color="transparent" + text-color="var(--admin-sidebar-text-muted)" + active-text-color="var(--admin-sidebar-text)" unique-opened router > @@ -33,7 +33,7 @@ {{ threeItem.title }} - + {{ subItem.title }} @@ -156,9 +156,10 @@ const items = [ }, { icon: 'mp3', - index: '/admin/medias', - title: '音视频记录', + index: '/admin/records/suno', + title: 'Suno音乐', }, + ], }, { @@ -179,6 +180,42 @@ const items = [ }, ], }, + { + icon: 'video', + title: '视频生成', + index: '/admin/video', + subs: [ + { + icon: 'config', + index: '/admin/video/config', + title: '生成配置', + }, + + { + icon: 'list', + index: '/admin/records/videos', + title: '生成记录', + }, + + ], + }, + { + icon: 'ppt', + title: 'AIPPT', + index: '/admin/ppt', + subs: [ + { + icon: 'config', + index: '/admin/ppt/config', + title: 'PPT 生成配置', + }, + { + icon: 'list', + index: '/admin/ppt/jobs', + title: 'PPT 任务列表', + }, + ], + }, { icon: 'moderation', @@ -249,6 +286,11 @@ const items = [ index: '/admin/config/plugin', title: '插件配置', }, + { + index: '/admin/config/wechat', + title: '微信配置', + icon: 'wechat', + }, ], }, { @@ -288,29 +330,6 @@ const items = [ index: '/admin/loginLog', title: '用户登录日志', }, - // { - // icon: 'menu', - // index: '1', - // title: '常用模板页面', - // subs: [ - // { - // index: '/admin/demo/form', - // title: '表单页面', - // }, - // { - // index: '/admin/demo/table', - // title: '常用表格', - // }, - // { - // index: '/admin/demo/import', - // title: '导入Excel', - // }, - // { - // index: '/admin/demo/editor', - // title: '富文本编辑器', - // }, - // ], - // }, ] const route = useRoute() @@ -330,34 +349,43 @@ setMenuItems(items) top: 0; bottom: 0; overflow-y: scroll; - background-color: #324157; + background: var(--admin-sidebar-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-right: 1px solid var(--admin-sidebar-border); + transition: background 0.2s ease, border-color 0.2s ease; .logo { display: flex; padding: 6px 15px; cursor: pointer; - background-color: #324157; + background: transparent; + transition: background 0.2s ease; + + &:hover { + background: var(--admin-header-hover); + } img { height: 36px; - padding-top: 5px; + padding: 2px; border-radius: 100%; background: #fff; border: 2px solid #754ff6; - padding: 2px; } .text { - color: #ffffff; + color: var(--admin-sidebar-text); font-weight: bold; padding: 12px 0 12px 10px; - transition: width 2s ease; + transition: width 2s ease, color 0.2s ease; } } ul { height: auto; min-height: 100%; + background: transparent; .el-menu-item, .el-sub-menu { @@ -367,42 +395,37 @@ setMenuItems(items) } } - .el-menu-item.is-active { - background-color: rgb(40, 52, 70); + .el-menu-item { + transition: background 0.2s ease, color 0.2s ease; + margin: 2px 8px; + border-radius: 8px; + + &:hover { + background: var(--admin-header-hover); + } + + &.is-active { + background: var(--admin-sidebar-active-bg); + color: var(--admin-sidebar-text); + box-shadow: inset 3px 0 0 var(--admin-sidebar-active-border); + } } + + .el-sub-menu__title { + transition: background 0.2s ease; + margin: 2px 8px; + border-radius: 8px; + + &:hover { + background: var(--admin-header-hover); + } + } + } - .sidebar-el-menu:not(.el-menu--collapse) { - width: 250px; - } -} - -.sidebar::-webkit-scrollbar { - width: 0; -} - -.sidebar.dark { - border-right: 1px solid var(--el-border-color-dark); - - .logo { - background: var(--el-bg-color); - border-right: 1px solid var(--el-border-color); - - .text { - color: var(--el-text-color-regular); - } - } - - ul { - background: var(--el-bg-color); - - .el-menu-item.is-active { - background-color: var(--el-menu-bg-color-dark); - } - - .el-menu-item:hover { - background-color: var(--el-menu-bg-color-darker); - } + .sidebar-el-menu { + border-right: none; + background: transparent; } .sidebar-el-menu:not(.el-menu--collapse) { @@ -410,7 +433,11 @@ setMenuItems(items) } .el-menu { - border-color: var(--el-border-color); + border-color: transparent; } } + +.sidebar::-webkit-scrollbar { + width: 0; +} diff --git a/web/src/components/admin/AdminTags.vue b/web/src/components/admin/AdminTags.vue index 86b17ed7..7472937f 100644 --- a/web/src/components/admin/AdminTags.vue +++ b/web/src/components/admin/AdminTags.vue @@ -123,10 +123,13 @@ const handleTags = (command) => { position: relative; height: 30px; overflow: hidden; - background: #fff; + background: var(--admin-tags-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); padding: 5px 120px 5px 10px; - -webkit-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); - box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); + border-bottom: 1px solid var(--admin-tags-border); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + transition: background 0.2s ease, border-color 0.2s ease; ul { box-sizing: border-box; @@ -138,21 +141,28 @@ const handleTags = (command) => { align-items: center; float: left; margin: 3px 5px 2px 3px; - border-radius: 3px; + border-radius: 6px; font-size: 12px; overflow: hidden; cursor: pointer; height: 23px; - border: 1px solid var(--el-border-color); - background: var(--el-bg-color); + border: 1px solid var(--admin-tags-border); + background: var(--admin-tags-item-bg); padding: 0 5px 0 12px; - color: var(--el-text-color); - -webkit-transition: all 0.3s ease-in; - -moz-transition: all 0.3s ease-in; - transition: all 0.3s ease-in; + color: var(--theme-text-color-secondary); + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease; &:hover { - background: var(--el-menu-bg-color-dark); + background: var(--admin-header-hover); + } + } + + .tags-li.active { + background: var(--admin-tags-active-bg); + border-color: rgba(107, 85, 255, 0.4); + .tags-li-title { + color: var(--el-color-primary); + font-weight: 500; } } @@ -163,11 +173,8 @@ const handleTags = (command) => { white-space: nowrap; text-overflow: ellipsis; margin-right: 5px; - color: #666; - } - - .tags-li.active .tags-li-title { - color: var(--el-color-primary); + color: var(--theme-text-color-secondary); + transition: color 0.2s ease; } } @@ -180,12 +187,14 @@ const handleTags = (command) => { text-align: center; width: 110px; height: 30px; - background: var(--el-bg-color); + background: transparent; z-index: 10; } } .tags.dark { - border-bottom: 1px solid var(--el-border-color); + .tags-li.active { + border-color: rgba(107, 85, 255, 0.5); + } } diff --git a/web/src/components/mobile/ParamBuilderMobile.vue b/web/src/components/mobile/ParamBuilderMobile.vue index 2043cea7..4c2d6603 100644 --- a/web/src/components/mobile/ParamBuilderMobile.vue +++ b/web/src/components/mobile/ParamBuilderMobile.vue @@ -45,12 +45,32 @@
+ +
+
+

+ + {{ selectedModel.name }} +

+

+ {{ selectedModel.label }} +

+
+
+