From 9ccff4efbc6a0ae44b4f84210ca86c278f141c39 Mon Sep 17 00:00:00 2001 From: RockYang Date: Tue, 11 Aug 2026 14:51:20 +0800 Subject: [PATCH] feat(release): migrate GeekAI v4.3.0 to open source - Sync backend and frontend from GeekAI Plus v4.3.0 - Remove commercial License flows and update open-source deployment defaults - Preserve Docker Compose deployment and bump image tags to v4.3.0 BREAKING CHANGE: commercial License configuration and related endpoints are removed --- .cursor/commands/ui-ux-pro-max.md | 226 + .gitignore | 4 +- AGENTS.md | 29 + CHANGELOG.md | 37 +- CLAUDE.md | 2 +- PROMPT.md | 8 + README.md | 18 +- api/config.sample.toml | 101 +- api/core/config.go | 14 +- api/core/middleware/auth.go | 4 +- api/core/types/config.go | 42 +- api/core/types/geekai.go | 11 +- api/core/types/moderation.go | 3 +- api/core/types/oss.go | 60 +- api/core/types/ppt_config.go | 47 + api/core/types/sms.go | 17 +- api/core/types/task.go | 103 +- api/core/types/video_config.go | 37 + api/core/types/web.go | 5 +- api/go.mod | 38 +- api/go.sum | 73 +- api/handler/admin/admin_handler.go | 6 +- api/handler/admin/chat_app_handler.go | 15 +- api/handler/admin/config_handler.go | 61 +- api/handler/admin/dashboard_handler.go | 14 +- api/handler/admin/image_handler.go | 80 +- api/handler/admin/media_handler.go | 215 - api/handler/admin/moderation_handler.go | 8 +- api/handler/admin/ppt_handler.go | 284 + api/handler/admin/suno_handler.go | 137 + api/handler/admin/user_handler.go | 247 +- api/handler/admin/video_handler.go | 269 + api/handler/base_handler.go | 4 +- api/handler/chat_app_handler.go | 197 +- api/handler/chat_handler.go | 12 +- api/handler/config_handler.go | 43 +- api/handler/function_handler.go | 30 +- .../{dalle_handler.go => image_handler.go} | 65 +- api/handler/mj_handler.go | 122 +- api/handler/payment_handler.go | 8 +- api/handler/ppt_task_handler.go | 547 ++ api/handler/realtime_handler.go | 7 +- api/handler/sd_handler.go | 328 - api/handler/suno_handler.go | 40 +- api/handler/user_handler.go | 148 +- api/handler/video_handler.go | 282 +- api/handler/wxgzh_handler.go | 95 + api/{logger => log}/logger.go | 2 +- api/main.go | 72 +- .../image_service.go} | 58 +- .../jimeng/{service.go => jimemg_service.go} | 10 +- api/service/migration_service.go | 543 +- api/service/mj/client.go | 33 +- api/service/mj/service.go | 3 + api/service/moderation/moderation_manager.go | 4 - api/service/oss/tencent_oss.go | 167 + api/service/oss/uploader.go | 1 + api/service/oss/uploader_manager.go | 102 +- api/service/payment/alipay_service.go | 4 +- api/service/payment/wxpay_service.go | 14 +- api/service/ppt/embed/minimal.pptx | Bin 0 -> 10300 bytes api/service/ppt/export.go | 404 + api/service/ppt/image_adapter.go | 263 + api/service/ppt/llm_client.go | 373 + api/service/ppt/ppt_service.go | 884 ++ api/service/ppt/reference_image.go | 50 + api/service/ppt/slide_edit.go | 182 + api/service/sd/service.go | 299 - api/service/sms/bao.go | 14 +- api/service/sms/service.go | 1 + api/service/sms/sms_manager.go | 24 +- api/service/sms/tencent.go | 127 + api/service/smtp_sms_service.go | 2 +- api/service/sora/sora_service.go | 21 +- api/service/suno/service.go | 52 +- api/service/types.go | 4 +- api/service/video/adapters/adapter.go | 32 + api/service/video/adapters/doubao_adapter.go | 302 + api/service/video/adapters/keling_adapter.go | 267 + api/service/video/adapters/luma_adapter.go | 197 + api/service/video/adapters/minimax_adapter.go | 227 + api/service/video/adapters/sora_adapter.go | 347 + api/service/video/adapters/veo_adapter.go | 226 + api/service/video/adapters/wan_adapter.go | 220 + api/service/video/power_calculator.go | 77 + api/service/video/video.go | 663 -- api/service/video/video_service.go | 333 + api/service/wxgzh_service.go | 94 + api/store/model/chat_app.go | 24 +- api/store/model/dalle_job.go | 21 - api/store/model/image_job.go | 22 + api/store/model/ppt_job.go | 29 + api/store/model/sd_job.go | 23 - api/store/model/suno_job.go | 51 +- api/store/model/user.go | 3 +- api/store/model/video_job.go | 12 +- api/store/mysql.go | 16 +- api/store/vo/chat_app.go | 24 +- api/store/vo/dalle_job.go | 14 - api/store/vo/image_job.go | 15 + api/store/vo/ppt_job.go | 110 + api/store/vo/sd_job.go | 20 - api/store/vo/suno_job.go | 39 +- api/store/vo/user.go | 32 +- api/store/vo/video_job.go | 31 +- api/test/test.go | 18 + api/utils/common.go | 2 +- api/utils/file.go | 78 +- api/utils/net.go | 215 +- api/utils/openai.go | 68 +- database/archive/geekai_plus-v4.2.3.sql | 142 +- database/geekai_plus-v4.2.6.sql | 148 +- docker/docker-compose.yaml | 4 +- docs/ai3d.md | 510 -- docs/plans/ppt-resume-generation.md | 54 + docs/requirement.md | 122 + web/.env.development | 2 +- web/.env.production | 2 +- web/package-lock.json | 7446 +++++++++++++++++ web/src/App.vue | 30 +- web/src/assets/css/chat-plus.scss | 527 +- web/src/assets/css/home.scss | 29 +- web/src/assets/css/image-sd.css | 372 - web/src/assets/css/image-sd.scss | 235 - .../css/{image-dall.scss => image.scss} | 6 +- web/src/assets/css/images-wall.scss | 2 - web/src/assets/css/mobile/image-sd.css | 133 - web/src/assets/css/mobile/image-sd.scss | 205 - web/src/assets/css/sd-task-dialog.css | 63 - web/src/assets/css/sd-task-dialog.scss | 94 - web/src/assets/css/theme-dark.scss | 17 + web/src/assets/css/theme-light.scss | 17 + web/src/assets/css/video.scss | 16 +- web/src/assets/fonts/OPlusSans3-Medium.ttf | Bin 9039948 -> 0 bytes web/src/assets/fonts/OPlusSans3-Regular.ttf | Bin 9120176 -> 0 bytes web/src/assets/iconfont/iconfont.css | 138 +- web/src/assets/iconfont/iconfont.js | 2 +- web/src/assets/iconfont/iconfont.json | 233 +- web/src/assets/iconfont/iconfont.ttf | Bin 60972 -> 66600 bytes web/src/assets/iconfont/iconfont.woff | Bin 39776 -> 43208 bytes web/src/assets/iconfont/iconfont.woff2 | Bin 34476 -> 37164 bytes web/src/components/AgentManagePage.vue | 540 ++ web/src/components/AttachmentList.vue | 1 + web/src/components/BindEmail.vue | 22 +- web/src/components/BindMobile.vue | 22 +- web/src/components/ChatInputBox.vue | 497 ++ web/src/components/GemList.vue | 186 + web/src/components/ImageUpload.vue | 205 +- web/src/components/LoginDialog.vue | 41 + web/src/components/ParamBuilder.vue | 163 +- web/src/components/PasswordDialog.vue | 4 +- web/src/components/RedeemVerify.vue | 13 +- web/src/components/ReferenceInput.vue | 271 + web/src/components/ResetPass.vue | 24 +- web/src/components/SdTaskView.vue | 177 - web/src/components/UserInfoDialog.vue | 43 - web/src/components/UserProfile.vue | 4 +- web/src/components/Welcome.vue | 173 - web/src/components/admin/AdminHeader.vue | 40 +- web/src/components/admin/AdminSidebar.vue | 165 +- web/src/components/admin/AdminTags.vue | 45 +- .../components/mobile/ParamBuilderMobile.vue | 143 +- web/src/components/ppt/PPTPreviewDialog.vue | 527 ++ web/src/components/ui/CustomSlideTabs.vue | 124 + web/src/components/ui/ItemsInput.vue | 23 +- web/src/main.js | 1 + web/src/router.js | 68 +- web/src/store/data/jimeng_params.js | 141 +- web/src/store/data/video_params.js | 1253 +++ web/src/store/mobile/video.js | 33 +- web/src/store/suno.js | 3 - web/src/store/video.js | 1048 +-- web/src/utils/http.js | 41 + web/src/utils/libs.js | 74 + web/src/views/ChatApps.vue | 18 +- web/src/views/ChatExport.vue | 924 +- web/src/views/ChatPlus.vue | 820 +- web/src/views/{ImagesWall.vue => Gallery.vue} | 114 +- web/src/views/Home.vue | 153 +- web/src/views/{Dalle.vue => Image.vue} | 457 +- web/src/views/ImageMj.vue | 669 +- web/src/views/ImageSd.vue | 772 -- web/src/views/Invitation.vue | 2 +- web/src/views/Login.vue | 1 + web/src/views/MarkMap.vue | 8 +- web/src/views/Member.vue | 584 +- web/src/views/PPTCreate.vue | 748 ++ web/src/views/Suno.vue | 4 +- web/src/views/Video.vue | 1024 +-- web/src/views/admin/ApiKey.vue | 5 +- web/src/views/admin/Apps.vue | 177 +- web/src/views/admin/Manager.vue | 4 +- web/src/views/admin/Users.vue | 128 +- web/src/views/admin/jimeng/JimengJobs.vue | 95 +- web/src/views/admin/ppt/PPTJobs.vue | 371 + web/src/views/admin/records/ImageList.vue | 354 +- web/src/views/admin/records/Medias.vue | 482 -- web/src/views/admin/records/Suno.vue | 450 + web/src/views/admin/records/Videos.vue | 363 + web/src/views/admin/settings/BasicConfig.vue | 10 - web/src/views/admin/settings/PPTConfig.vue | 278 + web/src/views/admin/settings/PowerConfig.vue | 113 +- web/src/views/admin/settings/SmsConfig.vue | 63 +- .../views/admin/settings/StorageConfig.vue | 203 +- web/src/views/admin/settings/WechatConfig.vue | 98 + web/src/views/admin/video/VideoConfig.vue | 387 + web/src/views/mobile/Apps.vue | 20 +- web/src/views/mobile/Create.vue | 18 +- web/src/views/mobile/Discover.vue | 20 +- web/src/views/mobile/Image.vue | 16 +- web/src/views/mobile/Index.vue | 25 +- web/src/views/mobile/Member.vue | 91 +- web/src/views/mobile/SunoCreate.vue | 4 +- web/src/views/mobile/VideoCreate.vue | 705 +- .../mobile/pages/{ImgWall.vue => Gallery.vue} | 46 +- .../mobile/pages/{ImageDall.vue => Image.vue} | 209 +- web/src/views/mobile/pages/ImageMj.vue | 6 +- web/src/views/mobile/pages/ImageSd.vue | 594 -- web/vite.config.js | 9 + 219 files changed, 29212 insertions(+), 10802 deletions(-) create mode 100644 .cursor/commands/ui-ux-pro-max.md create mode 100644 AGENTS.md create mode 100644 PROMPT.md create mode 100644 api/core/types/ppt_config.go create mode 100644 api/core/types/video_config.go delete mode 100644 api/handler/admin/media_handler.go create mode 100644 api/handler/admin/ppt_handler.go create mode 100644 api/handler/admin/suno_handler.go create mode 100644 api/handler/admin/video_handler.go rename api/handler/{dalle_handler.go => image_handler.go} (79%) create mode 100644 api/handler/ppt_task_handler.go delete mode 100644 api/handler/sd_handler.go create mode 100644 api/handler/wxgzh_handler.go rename api/{logger => log}/logger.go (99%) rename api/service/{dalle/dalle_service.go => image/image_service.go} (83%) rename api/service/jimeng/{service.go => jimemg_service.go} (98%) create mode 100644 api/service/oss/tencent_oss.go create mode 100644 api/service/ppt/embed/minimal.pptx create mode 100644 api/service/ppt/export.go create mode 100644 api/service/ppt/image_adapter.go create mode 100644 api/service/ppt/llm_client.go create mode 100644 api/service/ppt/ppt_service.go create mode 100644 api/service/ppt/reference_image.go create mode 100644 api/service/ppt/slide_edit.go delete mode 100644 api/service/sd/service.go create mode 100644 api/service/sms/tencent.go create mode 100644 api/service/video/adapters/adapter.go create mode 100644 api/service/video/adapters/doubao_adapter.go create mode 100644 api/service/video/adapters/keling_adapter.go create mode 100644 api/service/video/adapters/luma_adapter.go create mode 100644 api/service/video/adapters/minimax_adapter.go create mode 100644 api/service/video/adapters/sora_adapter.go create mode 100644 api/service/video/adapters/veo_adapter.go create mode 100644 api/service/video/adapters/wan_adapter.go create mode 100644 api/service/video/power_calculator.go delete mode 100644 api/service/video/video.go create mode 100644 api/service/video/video_service.go create mode 100644 api/service/wxgzh_service.go delete mode 100644 api/store/model/dalle_job.go create mode 100644 api/store/model/image_job.go create mode 100644 api/store/model/ppt_job.go delete mode 100644 api/store/model/sd_job.go delete mode 100644 api/store/vo/dalle_job.go create mode 100644 api/store/vo/image_job.go create mode 100644 api/store/vo/ppt_job.go delete mode 100644 api/store/vo/sd_job.go create mode 100644 api/test/test.go delete mode 100644 docs/ai3d.md create mode 100644 docs/plans/ppt-resume-generation.md create mode 100644 docs/requirement.md create mode 100644 web/package-lock.json delete mode 100644 web/src/assets/css/image-sd.css delete mode 100644 web/src/assets/css/image-sd.scss rename web/src/assets/css/{image-dall.scss => image.scss} (98%) delete mode 100644 web/src/assets/css/mobile/image-sd.css delete mode 100644 web/src/assets/css/mobile/image-sd.scss delete mode 100644 web/src/assets/css/sd-task-dialog.css delete mode 100644 web/src/assets/css/sd-task-dialog.scss delete mode 100644 web/src/assets/fonts/OPlusSans3-Medium.ttf delete mode 100644 web/src/assets/fonts/OPlusSans3-Regular.ttf create mode 100644 web/src/components/AgentManagePage.vue create mode 100644 web/src/components/ChatInputBox.vue create mode 100644 web/src/components/GemList.vue create mode 100644 web/src/components/ReferenceInput.vue delete mode 100644 web/src/components/SdTaskView.vue delete mode 100644 web/src/components/UserInfoDialog.vue delete mode 100644 web/src/components/Welcome.vue create mode 100644 web/src/components/ppt/PPTPreviewDialog.vue create mode 100644 web/src/components/ui/CustomSlideTabs.vue create mode 100644 web/src/store/data/video_params.js rename web/src/views/{ImagesWall.vue => Gallery.vue} (71%) rename web/src/views/{Dalle.vue => Image.vue} (58%) delete mode 100644 web/src/views/ImageSd.vue create mode 100644 web/src/views/PPTCreate.vue create mode 100644 web/src/views/admin/ppt/PPTJobs.vue delete mode 100644 web/src/views/admin/records/Medias.vue create mode 100644 web/src/views/admin/records/Suno.vue create mode 100644 web/src/views/admin/records/Videos.vue create mode 100644 web/src/views/admin/settings/PPTConfig.vue create mode 100644 web/src/views/admin/settings/WechatConfig.vue create mode 100644 web/src/views/admin/video/VideoConfig.vue rename web/src/views/mobile/pages/{ImgWall.vue => Gallery.vue} (77%) rename web/src/views/mobile/pages/{ImageDall.vue => Image.vue} (73%) delete mode 100644 web/src/views/mobile/pages/ImageSd.vue 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 0000000000000000000000000000000000000000..e1095c2e8de7dfb227c385276a4f8c595b3cb1c3 GIT binary patch literal 10300 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", "_", "|", "_", + ) + 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 4026bd2dc95b965fdd8a792b4eafc60beeef9552..4041fda31bbc3ec8e5279249de37d266eb96203e 100644 GIT binary patch delta 10119 zcma)?31C#!xyQeA?_`!tGFxU(GMULFGh|<82}?G%0AX<@tP(;dpSnY{nGhgj|Q9(R8Co_p^( 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

G^|s_QW>Ss^ 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 D2x6PK diff --git a/web/src/assets/iconfont/iconfont.woff b/web/src/assets/iconfont/iconfont.woff index 63c9e1b6f524245b074a731fe883047fc3bcbd70..157734bf9dc2cbe3460f64d20a2b451ecb08ce9b 100644 GIT binary patch delta 42971 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

1JoT9NmB1wOQG^t2VWyx@&87bo9$O4_!g8 zeU~?Q9&x?{FC)Gk=R~*IG=pAnt~Us}(q;R#<`Dw#3ci*9Ts3yuJE^!%~O zr8j?FZNGf=O-s>}?UZuUp>uzC?xCBMQ%m07Yz`MNM=qGdv3&~1KTAemB4>_K>{h6ZgwqHZSyKhwp7mIuv`M)G&_eoJBPRUQ`L!oW8 zu5_ zw5vok2aLrQxfRg%LD!leYSg3lX;!V3QSy}W<-eU-|6 zyZ4_5BwV|suP+5Y+LwALiJLu?>a+ho7Q0~iL(49V#^ccomp!!nf>?hHEM2H~wQ%Eq zjlP5;n11SWO}chb3j(S?ptoFr>G)O~I-R{qo@{sLc7$C|G>^6qQV&+xmv%(cyR1-?frBAWTFM?i# zpJEjk5>*Sj;g1i%W0uvzd7lbXy|6k8tvW@5!yh~~|0U3U1RAD32I~ztQ(V;_5Bh_l znH4LBIyp&_kd-KO<~75_luyVb$gIe@F^eS#4CBpq_H>(}YEMS8{4B!>3Ib=mmw_BCm_{OVdK;bNR1d3Wb=o8ba=u|b~<=noY5}k=>vyAFfeU^fzdRHC3 z^ziEL?$tP2)%%(^=9c~ao@6h|n5#6&!>CccmaFIZ+jcrB(&{Z`2rFg-p=fB-&p5I~j>Cv*jq^MBR2;PYT7c=5XD z!TKdlKk6ml@Sop*d>H)-Eus3%YlwzbcJZ@h7iSE|I|$hn$4elwe`yDFyJ z%k=ArLpE!%V3m;$c|4(XAw{ATc*@9-L?-X#Pec+bU{KY|U!1~zJ zZ8IvY!(l{03}H^5X}lT9=lk<{nCO}53!0hUpfBAUZa(cx7sKG;-h5ZG`GH7R2Cs6e z+pf4(5i3-?zbZ$9dc0WgOF>6JY7Tu5-HhoLSC;5F)$@P=Svr83&eB?M0CT|;G}@jR z)a`;-0pCcTz1r;Ev8_|F%huRL+2`&Vod|(nqz0FN3?`FJB12+1lq-L`ABW zNs=v_c$S)`jXDzrmv?d?_qGgwT01>FHuC7+1N(R1f3EHgdX^u$d~l1~&vB-ki*=Q( z{cBf$?^9LyP1f)R-IkZ*ymHW++{^mB0V63H1`dD4(#XktX`!Jz;7vl;femX8s-8&b zk_UF|K6`l2sHO#w;+J4|PoNOw`SAGYTm`M1TNyh_&@zvXiz zA^27r4#d7=1ZuU}V1LWPj|x*sS4?{4Yq_G>9tqPIgz_lWTTV`Bf{I|s6z*(6muZ!Q zP(YEZEGuB}40IxAE`EPqFEa6kP`uvW6 zn4XB5sdrmCSyCh<-VsLEe*_WqsF}WK&H}O+@<=?9D)>fli%WAIA`|lR_-T#x;l)K^ z5M+@*DoQ{K#=|SOZ{9MzyeFH2=K%->H!ljZ6bKV@VejU{2mW!E0S11m0%Ej~A|i>A zl76oesV;f%03z0^P(ZA#yA>JAjF|L)>z<*Q-!s-_=)lb|9AX#Sv-2vFv9hXU|{Lyb!!>}$!KJ3V)M-Ou9ecG zcwUc1@p{2^ru5XJ0um zGj;g9V|zCq+Zg&RJ_F12;AhW4)$nr`aC`mu1|bdx3=d>@zC|Nq0_X%h>GHVz^Hy448gaZ73>7Dq&+yLKb zem+yJE~{2EZakT28a!zz%{giTfWzjy3lDt;nD%e>0}&znKORBoKJf9Ar2Vq$C8T{o zYtyvXq6;vsCcAtTd-^yz-&(~%sWL14ytUwIU1t4oF&K((@nL^= z%AZ`-&FTJ3(B45EuzX-}SNw-01)sZ|M3dxj3BR4gN7=H9ELJ3cNkz%a?QM4L9ZT9fjMrGKJu6(GJx; zdUl9IK=j9w-93p0rzxCNH)6ObUl=)@=!mBhkbLp6-9rIxs#+fAbTeJfSO&jrcyx`J zG2avGY{`Lt{}p{1{uqD7_!>9Kes z5wm}mw@<`UiNxrw>$ACaGduR~{rfeAj;;de>PCVavZ}G{_xaAw{2KV+SakK~+325= zAQ)R!=vtK+os7pPht{lFQ`mR@zP$Zs{BQ`fGWJq`ZvO@Qb8A}P?{nw{wAMA?8gtFK zcDRn>v&FB7<5wMjyM!pJRZ~%_!A=}e^};-T@;HYlG)LGq3Tw0_zB0@w)bO>UGoq-S z3DgYUpm*nTk{H!YRn>bF+0n^Vt-}{|3&>D(zm~401&iw%P5AB!ha;hl8on#*uOz-L zh_{P>-ye1m*A8EKc>C5$tybB({l#_y+^%WTJKeds8BKV6K18-$*vU$v9F}io{h^@j zwp61d8)W)bMc2zHHEER)&LEipnFL%8ZEyMYPjE>@i{GJ^4)E7@X3Qf3Lc6q1J zU5KGO4`Z%X_--C{9mDsNc$LUG4y&CIlMeWQ8*@VZ(KxjPYlu)KxE@ndq$NRI#3g`g zp0o?_m+E&0%Hap&3^QNHFB*k|KZM^!=(`X-0ZHOBoX!AvZK50d#99Mq zRY-NWDT751<}7FUL%pH->*5p;Lwaw2h-WyX#Yq;tqN-SCOjqDkakrT2$E!hqN{kmL z;;GKrfka|pwlfu$Ql4CqmgXjlSF>54sxe3+2gCfP!0?=eP{@~hM{z2e=)p(B2kl8jr;0OwGCtbo z6$43mq&WG5^N8!~pAM_Kk?Jbw5ydC~PfMwUAlVTI z9(2X;op*Xqlwe!)U#0GNS|MzIS`?7QjXWt#2w=*Ah+^{fTpsh4WM|V~$7{dktI$Qt z1AL`QU&$fu2_#3OUUC3xs$GC;u24gAj?QU3NAdsD4)iJ7bJpg+I$*^54Pzp+mu1C) zZDtrU-rNcy5P%#D+(LLqSA)l-gZpr!$N-S&AHJC5I(D2Dj*j(Ws`hYyJ-{y+O7#j} zA)p)a=%%U}Djxlt-3P{cf&l@FU-ain;q(YqdPonatDu<3lz=~Hrb^9QhB1krQ8a`X z7O-7b{5T8N0><=hOA!sIoEl!{$(gKYHWb$kFd6dc3TLreP~qKcJ*ZEN^LzCnD~=<iptpEx%<$P#*VQ8ub1ymjg6Tb zvR#OU-D43o9gY;cdjVoE8A(cA*@N(Zy2+JU|GB3E}8PAHK%!W0_FUVql+L!=(EGn<|wCr1+ zMIS+9E(6oM(~B(F7n!V5&m)e(paZNxS4sSVQrg(DKWwcG4M*m_Y@SlQ5lPG@2gHG;hlcr*RA~!SVGT=?)=R%!&3%Dk+T%`BI+0oeEQ5 z5%PU`{!WsVD=b=;l!<1Mn;~ z%yO#ei^hDS%CTdZdS*d3gal0Su}~IZI0G<)Ck)Y6ai`UY$Raiqg18a-O`yXNQ+>VR zqP^MM>9rpfOcviINhhR?2`(}-(z6o& zWz0GD#>|-a8L#(Q?`YP3Z)QwC;q@-_2f@QPxVdM45PF7l+jqLT69}#7E-txzt>1n* zEF?a`$&kmiY*df{C!$9giA7L!wR~aTjsT_|2Jjn0&AF0#(ws*DeJiq1u0gSfTAolV z$ri#8y1d3P&0FAQ41*J1r8%m2y$XCr@v_?OjA(y_F#BLoWNt^dvS<`uHcATilSWR{ z{fPa4ucP=`&a$W<;gCT`p|Hq5q~j0RMUY_h*55dqJ2f}|H$K;#n}e=77ogw5*YUS7 zO7;QqyN{@iELj*DU>@ML1(0kK8iv|P5-%*}HkdNeO(OpK4B6n&%h0W{uPYH1a+|KZ z>BH~5_`*kueS?jCI*cH<$nrPonkeo)_|A8Kkn)-4gRJnhB(WgOFrv)dCScyZ|Ll5y zE_>7F?fcH#vvY}Ta@+yTpUBK|5ruNu8@FuVd+e~i5;ARF&i{kLe6 z6^KzXpydmK*lC^)fT1pep}-gGuv7G-YH1m_jSBFMa2(#Yb-o?I#PN10!GadQ76$)FJ6X*!!cGdbZ8@Goc7-71Z94lu+?g;Wm`^GAh$v#;!0C2nGQIBeR zWd1-v)C~Q*9tB$QPOn(V70g)7R5j6Hd~RLKMT3?aVyuw6nD69)2YQ3yT&Aax>F$^= zgtcf)3l*k2I&)KNd$WL9!t(-8EY2B!P1Xc0Dr;Vg<$~FC%!2rkywc^9t|=(8!UI*e zi1+fSd5MWv2Nv)UQ!0w!MY@M5l+btU8FVw6bmd*E>Ha}3Pryg1OGgr@Fb-ONS!1y5 zyc9W*YMa>-&a;T`H8tBC!}r$#{ipsc9aWo#BmHs2KJ)&o&g{nujSug-kb#PSE_~oD z!QjunTfkF4bMEGPGL?*9^xg;Fdr>@*wLft7x;4GU!`nDcO3UtdDX)~%zV$FHp?iS^*S%W~fmFrCP(UBz*0J0Pp* z+^6up-d&y<8`)K<>>3%HsqM3WSLSo$Q>)h9ngNJ2#rKAhxMBqdnBvEHMdJNy&mCMf zHJ;0Z03LW;z|@sEHYPyTFN|w`PXPagi7iza*9O{nUdJ&FZMZrqf(XH+rY>3NwgIFZ zM&4mhMu|BCDTyPNc-1y4W*TM2La|1su&Iz~#6n{`wjj1+^?C1XR9AO@FlZ}ix?TdC z>_;~}`sgJaqJ^i6>xVU6*M`>@pDx5U_%Fr_#M5I(HWvfd<|AWI3k-Add0_b_hPZKZ zc+#inT+{99a}B!IxwaEai#+u?GH=34VO*WXaS>*SsXiz%A^W};n+Q&gVtTXZKrUw2DWaK$1myVIoY zi!(%5uz~*o(O>9)0t7W-Uc^^Q$lt?hQ>EYV%b*q6#C5ZxSDWxA|(gH{2+IKQA=DksL2# z>~*3?$ccu1^EmYr_#H@*yXeTyC})aUtrp5A1psoE73gv-QG3>prEoO7eC^b1R|(CP zx@M==E(deeIyW`8zUTiocCE2(8+V*{CqDHcMN*<9N|bC$q-0xuNwQ*Fw&hooxQSh- zOOp+A>Y&Yk%d?jv%i0FRj4a5yA#H&s!MXxP*CELU>`RBOz~&7Y)-UUdp%}IS8<1en zB0%<~9|jD?uqDLqjc%@uGU-U&$sGT+ftRCH8N66 zg`n0yv@&yPxK>E)#7-?GY2Rq+^M{oV_P%iS3Me&ys|(ZX(+kxmD3wV;qoo`hH)939gq1zOjM8h(a%QTsB=22#eIe}#ryHm%^ zx1Zbb8u$bn`~UI{H%n}iA~9ge9+Vu8$k|5n&M(M)CC$Lai(sv@*H8A|CC>wC`XBfX zbN=UlgRlTb!7fo3z=8xcQ}W*PekG{9_v1*2m%ySH_0AH0@KRL6!^{HUwdlRK$noHE zOuP4eD9>P)VL|}E#w<1PB3QtWSubVzJ!a{JS1)pZe3KwE^ogbpCGYkdcvQN>n~@le zg}olSidGl_U%M>6c5}=VG`GzUpdzwoVCY4EpFs0Lpz+*H5E=%Eio-Vp5y_&t@`lTG z0;MtK(d?k(3}(Uia)zAA$VTpK0pW92k`R*CYijc7wG}M4%chzZjNB`g@1FkoGsoV3 z9z4A`TR8KCyLx?P<@)MpuOEfyjGR@?WxbD-bS9rkV`b%m{eQy4uYS7P10f0lmfQP( z<+C?duN~RpCySB-x&uiRqbKBC(oC6CxW2*9K^%X{fM`CKP>rojf{$5J63{5 z^kSX3n`AywN74?38s2VioZo2ow?J{C2)2q7HUM3tUcGkc_@SEj0{Cdi_V(C#KD5D> zJp{bp=7y5t%GgL@S6NPlS$x@llq~oYIM&{1+$90%TW zG}<*pCf-J2WULZS4&}56s!WjQni!JMgg@V#8azgKXxN$gX z-i!VZ2Jn~nn=7YRnzVtv>zq4mjK45$3_IL9D;Ty0^O|i4x3IPYWtLSDECV!+MEn62 zfYeY(h9Gnl0mP%t5Zc6lD**q+51pv$pl^Pz$BTcEP$e}kh%y-Id9R4IkcWN;e$)&) z(pz4>TOjr)>{^L0Yp;qx;C@$_Fz& zHD}VGK*z0@d>b|4_H{_=M51=3gntP3KF1_9e8`-x0J!8n$hTh*zyh}A>4h{GMqx6(^!3fwc17S38Qd*R9S=m3vk)14>`H#h~Pmt8ZiI?{t$33z8idiFn;kt1vRoeXy)n8vh-$rK` z3u`PR)Zfz)g>oRk`l@wct|@+2Fqv>#*EVbns+OJExi{x$+_F91U22zL1vfL^ofDZt zZfJZsXPxm*(@Y?6_sMKqq|O6-6XAG~ivyI8Fwa+S&7u(=C@5tc!R`3;49b& z*V@>=2JhsU?DlYC?FITqf%JxO{qLWX)Toj z(PElcjcWaX-A-FmIe?H9(ewF|z@prgm1^6M*ZT~WkN2fEBe9Tm;RjD`uAgj8{TC6T ziZ=iN0C=2ZU}Rum0AjCs>+RzCZN4&avoL_b(?`+gVf6oR|MPjjGfx0=IT)Bgq5yIO zhz@L%(UKj1eE`3o2&#CTV_{%mU}5Q1t#K zBFz30rw(hF5$9h6sQK@R4IfxoqN~Ru#`6Csl6$^?;Wd{PtRE6q4M^skA>=Ng{1YIJ zkw-A~0RR(ER1yFH00000f&iWX`~iRhNCOrFJOfw*q67K_P6X-&^adseJO|zgFbJFp z6bWhx+6n#&A__(dY6^}DvI^D;`U@TlN(*)im6b==>7MvFF7eE)37(^JT8E6^=8x9+89IzdV9mF0O9%dgJAABFSAQm9PAq*jw zB4#4=Bq}7FB}yfvCe|k|C$=dbDk>_@D-bJYE1oPiETAnyEo?3bE<`TuFJ3RQFc>hL zF=jFtGUzjuGsZLcG+;FBHC#2$HXt@~Ht0BiS~$cw@HtXBfH~Ycj5}02hC8@C4m?0S zoIP+o^gebz%0LuARzUtj96~TcoI@BxdPBrS2t+_cltns4q($aN97bS9rbgaJKu3~C zwnyYhI7nznv`HFCo=O%tWx4r-c$%whE(`fidGnZ zR)AMFSB6*QSXfxlSzcM_T5MXnTTWa2TuxldT`pa6UI<^zU?gDNVX$HZVya_aWEy2g zWvph%XBcNPXOd_}X#iDeha1L;)aW;6IV_;-p zV7Sj@!XVE80!%>61%wO?|G|6)04LxA0!y1g68MeD+W@ct)X6A1HlwM7oZ~H_2ebJn;1@|6MHa0g- zKI4u5|G5gAh!7(|iVQhM7-I|DID-ptAuhr-a7|nb*T!{lU0e^>#|>~p+z2k4( z3^&Jr#kd7-iCf{;xD9TL+u`=O1MY}Bp}?JS7u*#)*u@_9aTe!rH{2cPaSz-R_rkq# zAKVxB!~O99JP;4UgYghN6c5A0aS0xQN8(aE3XjHP@K{`i$KmmK0-lH`;mLRko{Fd8 z>39a7i3y$sfhkHFO1uWI#q02Tya8{-oA7451#iXM@OHcd@5H8ws@N4`Azs2wHd;9@^#GmkI`~`o--|%<* z1OLRo@NZm&|7=8E=c7*Qq}O4zG&MdQ#@)0s{ZyFT5miE$l+KK*$mXR`q$^<)FI%#) zkaZa?gw9&2Wh<6xO##KuSdlM`ozIkiS>!?%q9*P0vJqO7+RfG=TL`PAu4AcZW=pt^ zNNzM%~*{atETv;kgFOALu3H+MbUWbBn*YwBIl8(!MN|6_Tb>@j_ z(mt6AS@q-AFk2!l9Zf|!FQ!c2)__WBOGRG z)cPd}(@ZLqWDXK?ql;7XG^Dz0EnYI{nKXu*jgMrvABLSU?1o`~Z<`k$k#&PK zW5JUbMP(K`KY@LhF;RR-v%P)}%5kCHD%Z3q0uL!IJE5zrG|Dl^4|{UMc9+WP64P6j z;395hs;H>sjA`fJE^LjZqA^w;8|{V4n2)**LN&zOm*GG)|Nv&b)f53dCu-Dm?Rr9U|Fmm^MzSc5fYaS`(`W-c~4vGXTgC?hDnLB ze4S*x`iQ$>+UJzpkVzZ#;;TlbY#|QF)?qLCj$!v%pRI|ErhOeOYpf53DS~0f>qK*j zYzNJGXSFy_RXio7;>NU07|)A1cB4>r!TqYO0iF0X38(us^kko4lbNhD(aB;?tC&xv nQ6*e6HN0ou3Fr8qK^1d$tCY7iy-zvaO+Ve(_%FTww!;7bd3XQ9 delta 39512 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�QGbttRjqmZXpQl!4@aUIjnW{)DtGOy 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 diff --git a/web/src/assets/iconfont/iconfont.woff2 b/web/src/assets/iconfont/iconfont.woff2 index b74db5f0ba2e21c4d532f6797a8ba11e34735d60..8a0f31569774e912fd746efb73d2d432a9f63aaa 100644 GIT binary patch literal 37164 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 U?~fL7M-7~P(b9(cS#%!PZ_TdOq5uE@ 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 }} +

+
+
+