Compare commits

..

89 Commits

Author SHA1 Message Date
wangxiaoji e93de2b103 首页新增联系我们模块:邮箱联系、在线客服、商务合作 2026-07-28 09:57:30 +08:00
wangxiaoji 2bbdae1bf5 首页定价预览改为实际模型:DeepSeek-V4/Flash + MiniMax-M2.1/M2.5 2026-07-28 09:46:38 +08:00
wangxiaoji c2b00265e3 固定蓝色主题:移除暗黑模式切换、隐藏多语言选择器、默认亮色主题 2026-07-28 09:33:21 +08:00
wangxiaoji 5a68d88fa9 定价页重构:SiliconFlow风格卡片布局 + 炫酷动画效果
- 移除侧边栏,改为全宽布局,顶部Hero区域(渐变标题+搜索框+热门标签)
- 新增横向筛选pill按钮(模型类型/供应商/服务商)
- 卡片网格3列响应式布局,悬停发光+上浮+微缩放效果
- 新增浮动粒子背景、卡片入场逐个动画、搜索框光晕脉冲
- 新增流光扫过效果、价格数字脉冲发光
- 新增DeepSeek-V4/MiniMax模型默认倍率
- 更新setup.ps1支持DeepSeek+Minimax双供应商配置
2026-07-28 09:29:57 +08:00
wangxiaoji 8df9865ff5 隐藏多语言切换功能并固定默认语言为简体中文 2026-07-20 15:17:04 +08:00
wangxiaoji 6836012ab4 修复 react-icons 5.7.0 中 SiSlack 被移除导致的编译错误
- render.jsx: 用内联 SVG 替代已移除的 SiSlack 图标
- vite.config.js: 移除 force: true 并添加 date-fns/locale 预构建,修复 semi-ui locale 模块优化崩溃
- package.json: 添加 antd 为直接依赖(@lobehub/ui 的必要 peer 依赖)
2026-07-02 14:41:00 +08:00
wangxiaoji c8d76437cb 移除全局搜索和模型对比功能 2026-07-01 16:30:53 +08:00
wangxiaoji 04c4e17dcc 新增全局搜索、新用户引导、模型对比工具
- 顶部搜索框:/ 键唤起,搜索模型名称/描述/标签,点击跳转到定价页
- 新用户引导:首次登录弹出三步向导(获取 Key → 选择模型 → 首次调用)
- 模型对比:并排对比最多4个模型的价格、支持功能、标签等属性
2026-07-01 16:24:26 +08:00
wangxiaoji 42070a5300 RPM/TPM 卡片样式统一:移除蓝色字体与 monospace,与其他统计卡片保持一致 2026-07-01 16:07:24 +08:00
wangxiaoji 01900f1a3a 操作日志中文化与功能优化
- 后端操作描述统一使用中文冒号(:)替换英文冒号(:)
- 前端资源名称列 ID 显示改为中文冒号
- 清理日志改为弹窗内独立日期选择器,不再依赖筛选条件
- 侧边栏操作记录移至渠道管理上方
- RPM/TPM 卡片标签改为中文显示
2026-07-01 16:03:03 +08:00
wangxiaoji 006e723ac5 新增功能:用量可视化、操作日志页面优化、CSV导出
- RechargeCard 增加额度使用进度条与实时 RPM/TPM 展示(5列网格布局)
- 统一操作日志页面布局风格,与其他控制台页面一致
- 充值账单与使用日志均支持 CSV 导出
- 新增 helpers/export.js 导出工具函数
2026-07-01 15:43:41 +08:00
wangxiaoji 6a210b9667 统一控制台页面布局:替换 mt-[60px] px-2 为统一 min-h-full pb-16 pt-20 + max-w-[1400px] 居中容器
涉及页面:Channel、Token、Redemption、Log、Task、Midjourney、Model、ModelHeat、ModelDeployment、Subscription、User、Setting、Supplier Dashboard/Apply/Channel/PricingSettings、SupplierAdmin list/application、About、DistributorAdmin/Center/Apply、DeploymentAccessGuard、PersonalSetting、Dashboard 等全部控制台页面
2026-07-01 15:18:35 +08:00
wangxiaoji 001ed4c17f 缩小API测速页面字体,与Dashboard风格统一 2026-07-01 14:22:21 +08:00
wangxiaoji ac1120fc33 重构充值页面和统一控制台页面风格
- 充值页面:全新设计余额概览、预设金额卡片、支付方式、兑换码区域
- 控制台主页:移除灰色背景,统一页面边距和宽度
- API测速页:移至控制台下,调整字体大小和间距
- 侧边栏:新增API测速菜单入口
- 修复个人设置页TwoFASetting组件缺少IconShield导入
- 多个页面统一顶部间距和背景样式
2026-07-01 14:13:15 +08:00
wangxiaoji 44356571a6 修复 Token 显示为 0:API 返回 prompt_tokens+completion_tokens,非 token_used 2026-06-30 14:50:49 +08:00
wangxiaoji 65c28251f9 时间范围联动数据:Dashboard 管理 rangeKey 状态,useDashboardData 按天数生成 dailyStats 2026-06-30 14:35:03 +08:00
wangxiaoji d2913ff260 Token 使用趋势全面升级:悬停提示 + 模型明细 + 视觉优化 2026-06-30 14:24:25 +08:00
wangxiaoji 8a9e0226d0 修复柱状图不显示:w-full 导致 flex 溢出,改为 flex-1 均分空间 2026-06-30 14:07:16 +08:00
wangxiaoji 31f4d247fe 修复柱状图与日期标签对齐:flex 布局统一使用 justify-between,移除峰值加粗 2026-06-30 14:07:00 +08:00
wangxiaoji 66e61ef51b Token 使用趋势折线图替换为柱状图 2026-06-30 14:06:47 +08:00
wangxiaoji 1ff561c32c 时间范围改为下拉菜单:预设选项 + 自定义日期范围 + 应用按钮 2026-06-30 14:06:37 +08:00
wangxiaoji 6b73f5b6b9 Token 使用趋势添加时间范围控制栏:粒度选择器与刷新按钮 2026-06-30 14:06:05 +08:00
wangxiaoji dd5474f500 修复 Token 使用趋势日期标签与迷你图对齐:日期均匀分布匹配数据点 2026-06-30 14:05:22 +08:00
wangxiaoji 715487fda7 Token 使用趋势日期标签可读性优化:非峰值日期颜色从浅灰改为中灰,确保可见 2026-06-30 10:04:32 +08:00
wangxiaoji 847c2bd9d3 Token 使用趋势 X 轴标签从星期改为具体日期(月/日 格式) 2026-06-30 09:59:15 +08:00
wangxiaoji 6a99f229e3 修复控制台最近调用显示失败的问题:isSuccess 将 LogTypeConsume(2) 和 LogTypeTopup(1) 也识别为成功 2026-06-30 09:30:34 +08:00
wangxiaoji b14a09af73 解决合并冲突:保留 getSystemName() 动态读取品牌名,统一为 TuringToken 2026-06-29 17:22:17 +08:00
wangxiaoji 8e8f19fc10 将控制台页面和首页底部的 TokenFactory 改为从 getSystemName() 动态读取,统一品牌名称为 TuringToken 2026-06-29 17:21:31 +08:00
wangxiaoji 5c8a66a82b ui: 控制台页面品牌名 TokenFactory → Turing Token
- 顶部 TopBar 标题
- 页脚版权

保持控制台页内品牌一致性。
2026-06-29 17:14:11 +08:00
wangxiaoji dd1b881cf1 重新设计登录页面:采用分栏布局,左侧品牌展示区(深色背景+Logo+标语+色彩装饰),右侧登录表单区 2026-06-29 17:05:08 +08:00
wangxiaoji dc1ba114e5 修改:默认 logo 路径改为 /logo.jpg 2026-06-29 16:53:30 +08:00
wangxiaoji af2abaee09 修改:系统名称改为 TuringToken 2026-06-29 16:43:53 +08:00
wangxiaoji f32c114f1a 修复:添加缺失的 logo.png 文件 2026-06-29 15:43:33 +08:00
wangxiaoji 8ed621ed76 修复:补全缺失的前端文件并取消忽略 vite.config.js 2026-06-29 15:20:20 +08:00
wangxiaoji 3fd1881e81 build: 批量降级 gin-contrib 依赖以兼容 Go 1.23
- cors: v1.7.7 → v1.5.0
- gzip: v1.2.6 → v1.1.0
- static: v1.1.6 → v1.1.3

新版本都要求 Go 1.25+,逐个降级省事。
2026-06-29 12:19:46 +08:00
wangxiaoji 18e915e373 build: 调整依赖版本以兼容 Go 1.23
- go.mod: go 1.26.2 → go 1.23
- gin-gonic/gin: v1.12.0 → v1.10.0
- gin-contrib/sessions: v1.1.0 → v1.0.1

新版 Gin/gin-contrib 都要求 Go 1.25+,但生产服务器
go 1.23.4 升不上来(连不上 proxy.golang.org),先降到
兼容版本保证能编过。
2026-06-29 12:08:54 +08:00
wangxiaoji 3fb6326f4a feat: 重构控制台 Dashboard + 新增 API 快速测试页
- 新增 Bento 风格 Dashboard 组件:TopBar、Hero、BentoStats、BentoTrend、MonthlyForecast、RecentActivity、Sparkline
- 新增 /console/api-tester API 快速测试页:支持 OpenAI/Anthropic 协议切换、多家国产模型预设、Key 清洗、流式/非流式切换、cURL 一键复制
- 注释和提示语统一中文化
- App.jsx 新增 /console/api-tester 路由(PrivateRoute 守卫)
2026-06-26 16:16:30 +08:00
xiezhouwei 1e3ce2c0d2 feat: remove channel ID and username search fields from usage log filters 2026-06-23 14:11:36 +08:00
xiezhouwei b6293434e9 feat: hide channel, user, and retry columns in usage log table by default\n\n- Set CHANNEL/USERNAME/RETRY default visibility to false for all users\n- Remove admin-only override logic (no longer differentiate by role)\n- Exclude these 3 columns from 'Select All' operation 2026-06-23 14:06:26 +08:00
xiezhouwei a9d2fc2abe fix: billing total display now consistently uses 2 decimals, floor 0.01 for tiny positive amounts\n\n- Previous 6-decimal fallback caused visual inconsistency with list (shows 0.01)\n- Now: raw>0 and toFixed(2)>0 -> normal 2-decimal display\n- Now: raw>0 but toFixed(2)==0 -> floor to 0.01 (minimum visible unit)\n- Matches list column display format, no more '= 0.000125' in detail 2026-06-23 11:49:21 +08:00
xiezhouwei 4d5dd30668 fix: prevent billing process total from rounding to 0 for very small amounts (e.g. low-ratio channel-only models)
- Add formatBillingTotalDisplay: auto-scales from 2 to 6 decimal places when amount < 0.005
- Replace formatBillingDisplayPrice with formatBillingTotalDisplay in the billing total line
- Prevents '= \' display when actual cost is a tiny positive value (e.g. 0.00002 USD)
2026-06-23 11:39:18 +08:00
xiezhouwei 789f3b43e4 fix: prevent float-to-int truncation turning small billing amounts (e.g. 0.01) into 0 in use-price paths\n\n- service/quota.go: replace IntPart() with Round(0).IntPart() + safety net\n- relay/helper/price.go: replace int(float64) with int(math.Round()) + safety net\n- controller/channel-test.go: same fix for test billing calculation\n- All use-price paths now round instead of truncate, and guarantee minimum 1 quota when effective pricing > 0 2026-06-23 11:26:00 +08:00
xiezhouwei 648c9917ab fix: hide cache prices on model card, show only input and output prices 2026-06-23 11:15:24 +08:00
xiezhouwei a93c04ce28 fix: show all configured channel prices (output/cache/createCache) on model card surface, not just input 2026-06-23 11:09:12 +08:00
xiezhouwei d676e2200f fix: show output/cache/create_cache prices when channel-level pricing provides the ratio, not only when global pricing has it 2026-06-23 11:00:44 +08:00
xiezhouwei 4a7cb9e0f0 feat: extend ModelHasConfiguredPricing to include channel-level pricing (ChannelModelRatio/ChannelModelPrice) via prebuilt index set 2026-06-23 10:51:46 +08:00
xiezhouwei a7759c5767 refine: show only avatar circle on homepage hero when logged in, remove username text 2026-06-22 11:58:07 +08:00
xiezhouwei c44cebeb18 fix: apply admin theme/language toggle to homepage hero section as well 2026-06-22 11:53:12 +08:00
xiezhouwei 09d6a7305e feat: add admin-controlled toggles for header theme/language display in system settings 2026-06-22 11:46:31 +08:00
xiezhouwei 3e79e956f0 Revert "fix: restrict theme toggle and language selector to console routes only, hide on public pages"
This reverts commit ff522958de.
2026-06-22 11:41:24 +08:00
xiezhouwei ff522958de fix: restrict theme toggle and language selector to console routes only, hide on public pages 2026-06-22 11:35:22 +08:00
xiezhouwei 7d30717cd1 fix: unify homepage hero button styles - rounded login button, console-style user avatar with dynamic color 2026-06-22 11:27:28 +08:00
xiezhouwei 4f85cad331 fix: show user info on homepage hero when logged in instead of always showing login button 2026-06-22 10:59:30 +08:00
xiezhouwei 0ab319143b feat: restructure docs page to left-center-right 3-column layout with page TOC sidebar 2026-06-22 10:08:09 +08:00
xiezhouwei e028f6f93a fix: adjust docs page scroll offset to prevent fixed header occlusion 2026-06-22 09:56:23 +08:00
xiezhouwei 76ac1a1b31 fix: clear default external docs_link, add migration to use in-app /docs page 2026-06-22 09:51:45 +08:00
xiezhouwei 64a4a68a6f feat: add in-app docs page with sidebar navigation and quickstart guide, replace /docs redirect with built-in page 2026-06-22 09:30:31 +08:00
xiezhouwei dfa7ccbd97 Revert "fix: remove '文档' nav link from homepage hero section"
This reverts commit 4429de2448.
2026-06-22 09:23:57 +08:00
xiezhouwei 4429de2448 fix: remove '文档' nav link from homepage hero section 2026-06-22 09:21:09 +08:00
xiezhouwei 6a880c08ba fix: add /docs redirect middleware to forward docs paths to configured docs_link 2026-06-22 09:16:31 +08:00
xiezhouwei 9f47782442 fix: homepage '文档' nav link now uses dynamic same-origin docs URL instead of hardcoded docs.new-api.cc 2026-06-22 09:09:02 +08:00
xiezhouwei 59cc502e6e fix: homepage '开始使用' button now links to API Key (/console/token) instead of register 2026-06-21 19:00:13 +08:00
xiezhouwei f2da0cdb24 fix: vendor icons missing .Color suffix (8 entries) + add Ollama entry 2026-06-21 18:50:49 +08:00
xiezhouwei 86bb4ee0cf rename: 体验馆 → 体验 (menu, settings, i18n across 11 locales) 2026-06-21 18:26:24 +08:00
xiezhouwei eb045c9a46 fix: add Chinese vendor name keywords for doubao/seedance in model_meta_infer vendorKeywordAliases 2026-06-21 17:26:30 +08:00
xiezhouwei 4a5fa8aa3b debug: add diagnostic logging to getOrCreateVendor and initDefaultVendorMapping for seedance/minimax 2026-06-21 17:26:14 +08:00
xiezhouwei 20de5cb147 fix: add minimax keyword to defaultVendorRules so MiniMax-M1/M3/T1 series get vendor assignment 2026-06-21 16:59:20 +08:00
xiezhouwei 07a6e29c1e fix: allow vendor_id=0 to be passed as model_type in experience button jump 2026-06-21 16:47:42 +08:00
xiezhouwei 4f8cde37d8 refactor: replace api.tokendance.space with api.turingtoken.ai on homepage 2026-06-21 16:40:30 +08:00
xiezhouwei 6eec903060 refactor: remove about link and GitHub display from homepage DarkFooter 2026-06-21 16:37:23 +08:00
xiezhouwei 0d798c2fbc fix: convert model_type URL param to number so Select matches optionList value type 2026-06-19 10:37:41 +08:00
xiezhouwei 2165430d87 feat: pass model_type (vendor_id) from model detail to playground settings panel 2026-06-19 10:30:32 +08:00
xiezhouwei 977e9e8bf5 feat: add experience button in model detail side sheet to jump to playground with model preset 2026-06-19 10:17:00 +08:00
xiezhouwei 189f314734 fix: add DeepSeek-V3.2 default model ratio so it appears in pricing list 2026-06-18 21:13:15 +08:00
xiezhouwei 4e8d2b8057 fix: strip spaces in FormatMatchingModelName to match model name variants like Seedance 2.0 2026-06-18 20:48:07 +08:00
xiezhouwei 06aa707141 fix: add seedance vendor rule and improve vendor_id auto-mapping for existing records 2026-06-18 18:07:47 +08:00
xiezhouwei 46819578de fix: model page tab filter not working and sidebar tag order 2026-06-18 17:01:50 +08:00
xiezhouwei 77b885b190 refactor: streamline model card to show only logo, name, description, and IO prices 2026-06-18 16:50:02 +08:00
xiezhouwei 4fbdafee1c fix: remove supplier filter from model page sidebar 2026-06-18 16:36:52 +08:00
xiezhouwei 825954e43f fix: 模型页面右侧列表被固定导航栏遮挡\n\n根因:PageLayout.jsx 第 325 行 <Header> 为 position: fixed; top: 0\n(h-14, 56px),在 /pricing 路径下也显示。但 models-page-layout\n使用 height: calc(100vh - 60px) 而非 padding-top 偏移,导致\n布局从视口顶部(y=0)开始,内容被固定头部遮盖。\n\n修改:\n- models-page-layout: padding: 56px 24px 0 + height: 100vh +\n box-sizing: border-box,使内容从固定头部下方开始\n- models-sidebar: 移除顶部 padding(padding: 0 16px 24px 0),\n 避免与外层 padding-top 叠加 2026-06-17 16:28:04 +08:00
xiezhouwei 61b0d902b6 refactor: 模型页面布局调整为左右结构(类 tokendance.models)
新增:
- ModelsSidebar.jsx 左侧筛选面板(作者/服务商/输入模态/供应商)
- ModelsContent.jsx 右侧内容区(全部/文本/图像/音频/视频 Tab + 搜索)
- index.css models-* 样式类

修改:
- PricingPage.jsx 布局改为 flex 左右结构替代 Semi UI Layout
- PricingCardView.jsx 新增 gridCols 参数支持 2 列网格布局
  当 gridCols=2 时:grid gap-4 + 卡片 !w-full

布局: 左 280px 筛选栏 + 右自适应内容区
Tab: 全部/文本/图像/音频/视频 (映射到 filterEndpointType)
卡片: 每行 2 个模型卡片
2026-06-17 14:33:53 +08:00
xiezhouwei 5fa5f87e1e fix: 补全主页独立导航栏顺序 - 模型→评测→文档→控制台
Home/index.jsx 第 153 行有自己的固定导航栏(fixed top-0 z-50),
完全独立于 HeaderBar 组件。其导航项数组仍是旧顺序
模型→控制台→评测→文档,现调整为:模型→评测→文档→控制台。

至此三处导航栏全部修正:
1. useNavigation.js(桌面 HeaderBar 导航)
2. MobileSiteNavDropdown.jsx(移动端下拉导航)
3. Home/index.jsx(主页独立固定导航栏)
2026-06-17 14:08:02 +08:00
xiezhouwei d33f55ad55 fix: 补全移动端导航栏顺序同步 - 模型→评测→文档→控制台
之前只修改了 useNavigation.js(桌面导航),漏掉了
MobileSiteNavDropdown.jsx 第34行硬编码的 PRIMARY_NAV_KEYS
数组,导致移动端下拉导航顺序仍为旧顺序。
2026-06-17 14:04:33 +08:00
xiezhouwei 3cb9c85ea8 feat: 调整首页导航栏顺序为 模型→评测→文档→控制台 2026-06-17 14:00:16 +08:00
xiezhouwei 5b5d7a95f3 fix: 评测页面顶部被固定导航栏遮挡 - 添加 pt-14 偏移\n\n根因:PageLayout.jsx 中 <Header> 使用 position: fixed; top: 0(h-14, 56px),但\n非 /console 路径的 <Content> padding 为 0,评测页面顶部内容被固定头部遮盖。\n\n修改:在外容器和 loading 容器添加 pt-14,精确匹配头部高度。 2026-06-17 11:47:48 +08:00
xiezhouwei d455641b86 fix: 评测页面浅色模式样式异常 - 切换为 shadcn/ui 主题变量系统
根因:Benchmarks 页面所有 Tailwind 样式使用硬编码暗色类
(bg-gray-950/text-white/bg-white/5/border-white/10 等),
导致浅色模式下显示为深色背景+白字,完全不可见。

修改:将所有 hardcoded dark-mode 类替换为 shadcn/ui CSS 变量类:
- bg-gray-950 → bg-background
- text-white/text-gray-* → text-foreground/text-muted-foreground
- border-white/* → border-border
- bg-white/* → bg-muted/* 和 bg-primary/*
- 卡片:添加上下模式 gradient(light:from-purple-50 dark:from-purple-950/40)
- 筛选/搜索/进度条/标签等全部适配 dual-theme
- 额外优化:分类图标更新、加载动画主题化
2026-06-17 11:39:59 +08:00
xiezhouwei e29f61aa68 fix: 评测页面 401 拦截器导致未登录用户被重定向到登录页
根因:Benchmarks 页面加载时调用 API.get('/api/option/'),
未登录用户返回 401 → axios 拦截器 → showError() → window.location.href = '/login'

修复:添加 skipErrorHandler: true 参数,跳过全局错误拦截,
API 失败时静默回退到离线默认数据
2026-06-17 11:25:47 +08:00
xiezhouwei 281c0de751 feat: 评测基准页面支持离线默认数据 + 路由公开访问
- 确认 /benchmarks 路由为公开(无 PrivateRoute)
- 嵌入 32 款主流 AI 模型完整评测数据作为默认数据
- 默认数据即时渲染,后端 API 查询改为后台异步更新(无等待状态)
- 数据覆盖 OpenAI/Anthropic/Google/DeepSeek/Meta/国内模型等
- 评分维度:智能指数、速度、价格、延迟、上下文窗口
2026-06-17 10:59:09 +08:00
xiezhouwei e00999b88c fix: 首页导航栏样式对齐 tokendance.space + 主题切换修复
- 首页导航字体/样式对齐 tokendance.space(text-[14px]、gap-1.5、SVG 图标)
- 全局 Navigation.jsx 和 MobileSiteNavDropdown.jsx 添加 SVG 图标
- 首页 HeroSection header 固定定位(sticky→fixed),导航链接靠右
- PageLayout 首页时隐藏全局 HeaderBar,避免导航重叠/文字不清
- 修复 HomeThemeToggle 主题切换失效(使用正确的 localStorage key 和 DOM 操作)
- 首页文字 TokenFactory→TuringToken,logo 靠左对齐
- i18n 多语言文件同步更新
2026-06-17 10:23:39 +08:00
187 changed files with 49025 additions and 5155 deletions

1
$null
View File

@ -1 +0,0 @@
ERROR: The process "main.exe" not found.

1
.gitignore vendored
View File

@ -10,7 +10,6 @@ upload
build build
*.db-journal *.db-journal
logs logs
web/vite.config.js
web/dist web/dist
.env .env
one-api one-api

509
app_backup.jsx Normal file
View File

@ -0,0 +1,509 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { lazy, Suspense, useContext, useMemo } from 'react';
import { Route, Routes, useLocation, useParams } from 'react-router-dom';
import Loading from './components/common/ui/Loading';
import User from './pages/User';
import { AuthRedirect, PrivateRoute, AdminRoute } from './helpers';
import RegisterForm from './components/auth/RegisterForm';
import LoginForm from './components/auth/LoginForm';
import NotFound from './pages/NotFound';
import Forbidden from './pages/Forbidden';
import Setting from './pages/Setting';
import { StatusContext } from './context/Status';
import PasswordResetForm from './components/auth/PasswordResetForm';
import PasswordResetConfirm from './components/auth/PasswordResetConfirm';
import Channel from './pages/Channel';
import Token from './pages/Token';
import Redemption from './pages/Redemption';
import TopUp from './pages/TopUp';
import Log from './pages/Log';
import Chat from './pages/Chat';
import Chat2Link from './pages/Chat2Link';
import Midjourney from './pages/Midjourney';
import Pricing from './pages/Pricing';
import Task from './pages/Task';
import ModelPage from './pages/Model';
import ModelDeploymentPage from './pages/ModelDeployment';
import ModelHeatPage from './pages/ModelHeat';
import Playground from './pages/Playground';
import Subscription from './pages/Subscription';
import OAuth2Callback from './components/auth/OAuth2Callback';
import PersonalSetting from './components/settings/PersonalSetting';
import SupplierApplyPage from './pages/Supplier/Apply';
import SupplierChannelPage from './pages/Supplier/Channel';
import PricingSettingsPage from './pages/Supplier/PricingSettings';
import SupplierDashboardPage from './pages/Supplier/Dashboard';
import SupplierApplication from './pages/SupplierAdmin/application';
import Suppliers from './pages/SupplierAdmin/list';
import Setup from './pages/Setup';
import SetupCheck from './components/layout/SetupCheck';
import OperationLog from './pages/OperationLog';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const DistributorApply = lazy(() => import('./pages/DistributorApply'));
const DistributorCenter = lazy(() => import('./pages/DistributorCenter'));
const DistributorAdmin = lazy(() => import('./pages/DistributorAdmin'));
const InviteRedirect = lazy(() => import('./pages/InviteRedirect'));
const About = lazy(() => import('./pages/About'));
const UserAgreement = lazy(() => import('./pages/UserAgreement'));
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
function DynamicOAuth2Callback() {
const { provider } = useParams();
return <OAuth2Callback type={provider} />;
}
function App() {
const location = useLocation();
const [statusState] = useContext(StatusContext);
// 广
const pricingRequireAuth = useMemo(() => {
const headerNavModulesConfig = statusState?.status?.HeaderNavModules;
if (headerNavModulesConfig) {
try {
const modules = JSON.parse(headerNavModulesConfig);
// pricingboolean
if (typeof modules.pricing === 'boolean') {
return false; //
}
// 使requireAuth
return modules.pricing?.requireAuth === true;
} catch (error) {
console.error('解析顶栏模块配置失败:', error);
return false; //
}
}
return false; //
}, [statusState?.status?.HeaderNavModules]);
return (
<SetupCheck>
<Routes>
<Route
path='/'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Home />
</Suspense>
}
/>
<Route
path='/setup'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Setup />
</Suspense>
}
/>
<Route path='/forbidden' element={<Forbidden />} />
<Route
path='/console/models'
element={
<AdminRoute>
<ModelPage />
</AdminRoute>
}
/>
<Route
path='/console/deployment'
element={
<AdminRoute>
<ModelDeploymentPage />
</AdminRoute>
}
/>
<Route
path='/console/model-heat'
element={
<AdminRoute>
<ModelHeatPage />
</AdminRoute>
}
/>
<Route
path='/console/subscription'
element={
<AdminRoute>
<Subscription />
</AdminRoute>
}
/>
<Route
path='/console/channel'
element={
<AdminRoute>
<Channel />
</AdminRoute>
}
/>
<Route
path='/console/token'
element={
<PrivateRoute>
<Token />
</PrivateRoute>
}
/>
<Route
path='/console/playground'
element={
<PrivateRoute>
<Playground />
</PrivateRoute>
}
/>
<Route
path='/console/redemption'
element={
<AdminRoute>
<Redemption />
</AdminRoute>
}
/>
<Route
path='/console/user'
element={
<AdminRoute>
<User />
</AdminRoute>
}
/>
<Route
path='/console/supplier-application'
element={
<AdminRoute>
<SupplierApplication />
</AdminRoute>
}
/>
<Route
path='/console/suppliers'
element={
<AdminRoute>
<Suppliers />
</AdminRoute>
}
/>
<Route
path='/user/reset'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PasswordResetConfirm />
</Suspense>
}
/>
<Route
path='/login'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<AuthRedirect>
<LoginForm />
</AuthRedirect>
</Suspense>
}
/>
<Route
path='/register'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<AuthRedirect>
<RegisterForm />
</AuthRedirect>
</Suspense>
}
/>
<Route
path='/r/:aff'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<InviteRedirect />
</Suspense>
}
/>
<Route
path='/reset'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PasswordResetForm />
</Suspense>
}
/>
<Route
path='/oauth/github'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<OAuth2Callback type='github'></OAuth2Callback>
</Suspense>
}
/>
<Route
path='/oauth/discord'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<OAuth2Callback type='discord'></OAuth2Callback>
</Suspense>
}
/>
<Route
path='/oauth/oidc'
element={
<Suspense fallback={<Loading></Loading>}>
<OAuth2Callback type='oidc'></OAuth2Callback>
</Suspense>
}
/>
<Route
path='/oauth/linuxdo'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<OAuth2Callback type='linuxdo'></OAuth2Callback>
</Suspense>
}
/>
<Route
path='/oauth/:provider'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<DynamicOAuth2Callback />
</Suspense>
}
/>
<Route
path='/console/setting'
element={
<AdminRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Setting />
</Suspense>
</AdminRoute>
}
/>
<Route
path='/console/personal'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PersonalSetting />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/console/supplier'
element={
<PrivateRoute>
<SupplierApplyPage />
</PrivateRoute>
}
/>
<Route
path='/console/supplier/apply'
element={
<PrivateRoute>
<SupplierApplyPage />
</PrivateRoute>
}
/>
<Route
path='/console/supplier/channel'
element={
<PrivateRoute>
<SupplierChannelPage />
</PrivateRoute>
}
/>
<Route
path='/console/supplier/pricing-settings'
element={
<PrivateRoute>
<PricingSettingsPage />
</PrivateRoute>
}
/>
<Route
path='/console/supplier/dashboard'
element={
<PrivateRoute>
<SupplierDashboardPage />
</PrivateRoute>
}
/>
<Route
path='/console/distributor/apply'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<DistributorApply />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/console/distributor/center'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<DistributorCenter />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/console/distributor/admin'
element={
<AdminRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<DistributorAdmin />
</Suspense>
</AdminRoute>
}
/>
<Route
path='/console/topup'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<TopUp />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/console/log'
element={
<PrivateRoute>
<Log />
</PrivateRoute>
}
/>
<Route
path='/console/operation-log'
element={
<AdminRoute>
<OperationLog />
</AdminRoute>
}
/>
<Route
path='/console'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Dashboard />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/console/midjourney'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Midjourney />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/console/task'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Task />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/pricing'
element={
pricingRequireAuth ? (
<PrivateRoute>
<Suspense
fallback={<Loading></Loading>}
key={location.pathname}
>
<Pricing />
</Suspense>
</PrivateRoute>
) : (
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Pricing />
</Suspense>
)
}
/>
<Route
path='/about'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<About />
</Suspense>
}
/>
<Route
path='/user-agreement'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<UserAgreement />
</Suspense>
}
/>
<Route
path='/privacy-policy'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PrivacyPolicy />
</Suspense>
}
/>
<Route
path='/console/chat/:id?'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Chat />
</Suspense>
}
/>
{/* 方便使用chat2link直接跳转聊天... */}
<Route
path='/chat2link'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Chat2Link />
</Suspense>
</PrivateRoute>
}
/>
<Route path='*' element={<NotFound />} />
</Routes>
</SetupCheck>
);
}
export default App;

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -12,7 +12,7 @@ import (
var StartTime = time.Now().Unix() // unit: second var StartTime = time.Now().Unix() // unit: second
var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change
var SystemName = "TokenFactory" var SystemName = "TuringToken"
var Footer = "" var Footer = ""
var Logo = "" var Logo = ""
var TopUpLink = "" var TopUpLink = ""

View File

@ -645,7 +645,11 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
quota = 1 quota = 1
} }
} else { } else {
quota = int(priceData.ModelPrice * common.QuotaPerUnit) quota = int(math.Round(priceData.ModelPrice * common.QuotaPerUnit))
// 有有效定价但舍入为 0 时保底为 1
if quota == 0 && priceData.ModelPrice > 0 {
quota = 1
}
} }
tok := time.Now() tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds() milliseconds := tok.Sub(tik).Milliseconds()

View File

@ -1348,7 +1348,7 @@ func AddChannel(c *gin.Context) {
if addChannelRequest.Channel != nil { if addChannelRequest.Channel != nil {
channelName = addChannelRequest.Channel.Name channelName = addChannelRequest.Channel.Name
} }
service.RecordCreateOperation(c, "channel", 0, channelName, "创建渠道: "+channelName, "") service.RecordCreateOperation(c, "channel", 0, channelName, "创建渠道"+channelName, "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
@ -1366,7 +1366,7 @@ func DeleteChannel(c *gin.Context) {
} }
model.InitChannelCache() model.InitChannelCache()
// 记录操作日志 // 记录操作日志
service.RecordDeleteOperation(c, "channel", id, "", fmt.Sprintf("删除渠道 (ID: %d)", id), "") service.RecordDeleteOperation(c, "channel", id, "", fmt.Sprintf("删除渠道 (ID%d)", id), "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
@ -1721,7 +1721,7 @@ func UpdateChannel(c *gin.Context) {
channel.Key = "" channel.Key = ""
clearChannelInfo(&channel.Channel) clearChannelInfo(&channel.Channel)
// 记录操作日志 // 记录操作日志
service.RecordUpdateOperation(c, "channel", channel.Id, channel.Name, "更新渠道: "+channel.Name, "") service.RecordUpdateOperation(c, "channel", channel.Id, channel.Name, "更新渠道"+channel.Name, "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",

View File

@ -117,6 +117,8 @@ func GetStatus(c *gin.Context) {
// 模块管理配置 // 模块管理配置
"HeaderNavModules": common.OptionMap["HeaderNavModules"], "HeaderNavModules": common.OptionMap["HeaderNavModules"],
"HeaderShowThemeToggle": common.OptionMap["HeaderShowThemeToggle"] != "false",
"HeaderShowLanguageSelector": common.OptionMap["HeaderShowLanguageSelector"] != "false",
"SidebarModulesByRole": common.OptionMap["SidebarModulesByRole"], "SidebarModulesByRole": common.OptionMap["SidebarModulesByRole"],
"oidc_enabled": system_setting.GetOIDCSettings().Enabled, "oidc_enabled": system_setting.GetOIDCSettings().Enabled,

View File

@ -270,7 +270,7 @@ func CreateModelMeta(c *gin.Context) {
return return
} }
model.RefreshPricing() model.RefreshPricing()
service.RecordCreateOperation(c, "model", m.Id, m.ModelName, "创建模型: "+m.ModelName, "") service.RecordCreateOperation(c, "model", m.Id, m.ModelName, "创建模型"+m.ModelName, "")
common.ApiSuccess(c, &m) common.ApiSuccess(c, &m)
} }
@ -319,7 +319,7 @@ func UpdateModelMeta(c *gin.Context) {
} }
} }
model.RefreshPricing() model.RefreshPricing()
service.RecordUpdateOperation(c, "model", m.Id, m.ModelName, "更新模型: "+m.ModelName, "") service.RecordUpdateOperation(c, "model", m.Id, m.ModelName, "更新模型"+m.ModelName, "")
common.ApiSuccess(c, &m) common.ApiSuccess(c, &m)
} }
@ -336,7 +336,7 @@ func DeleteModelMeta(c *gin.Context) {
return return
} }
model.RefreshPricing() model.RefreshPricing()
service.RecordDeleteOperation(c, "model", id, "", fmt.Sprintf("删除模型 (ID: %d)", id), "") service.RecordDeleteOperation(c, "model", id, "", fmt.Sprintf("删除模型 (ID%d)", id), "")
common.ApiSuccess(c, nil) common.ApiSuccess(c, nil)
} }

View File

@ -690,10 +690,10 @@ func UpdateOption(c *gin.Context) {
if modelPricingOptionKeys[option.Key] { if modelPricingOptionKeys[option.Key] {
changes := computeModelPricingDiff(oldVal, valStr) changes := computeModelPricingDiff(oldVal, valStr)
if len(changes) > 0 { if len(changes) > 0 {
service.RecordUpdateWithDiff(c, "setting", 0, option.Key, "更新系统设置: "+option.Key, changes, "") service.RecordUpdateWithDiff(c, "setting", 0, option.Key, "更新系统设置"+option.Key, changes, "")
} }
} else { } else {
service.RecordUpdateOperation(c, "setting", 0, option.Key, "更新系统设置: "+option.Key, "") service.RecordUpdateOperation(c, "setting", 0, option.Key, "更新系统设置"+option.Key, "")
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{

View File

@ -106,7 +106,7 @@ func AddRedemption(c *gin.Context) {
} }
keys = append(keys, key) keys = append(keys, key)
} }
service.RecordCreateOperation(c, "redemption", 0, redemption.Name, "创建兑换码: "+redemption.Name, "") service.RecordCreateOperation(c, "redemption", 0, redemption.Name, "创建兑换码"+redemption.Name, "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
@ -122,7 +122,7 @@ func DeleteRedemption(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
service.RecordDeleteOperation(c, "redemption", id, "", fmt.Sprintf("删除兑换码 (ID: %d)", id), "") service.RecordDeleteOperation(c, "redemption", id, "", fmt.Sprintf("删除兑换码 (ID%d)", id), "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
@ -161,7 +161,7 @@ func UpdateRedemption(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
service.RecordUpdateOperation(c, "redemption", cleanRedemption.Id, cleanRedemption.Name, "更新兑换码: "+cleanRedemption.Name, "") service.RecordUpdateOperation(c, "redemption", cleanRedemption.Id, cleanRedemption.Name, "更新兑换码"+cleanRedemption.Name, "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",

View File

@ -228,7 +228,7 @@ func AddToken(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
service.RecordCreateOperation(c, "token", cleanToken.Id, cleanToken.Name, "创建令牌: "+cleanToken.Name, "") service.RecordCreateOperation(c, "token", cleanToken.Id, cleanToken.Name, "创建令牌"+cleanToken.Name, "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
@ -243,7 +243,7 @@ func DeleteToken(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
service.RecordDeleteOperation(c, "token", id, "", fmt.Sprintf("删除令牌 (ID: %d)", id), "") service.RecordDeleteOperation(c, "token", id, "", fmt.Sprintf("删除令牌 (ID%d)", id), "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
@ -308,7 +308,7 @@ func UpdateToken(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
service.RecordUpdateOperation(c, "token", cleanToken.Id, cleanToken.Name, "更新令牌: "+cleanToken.Name, "") service.RecordUpdateOperation(c, "token", cleanToken.Id, cleanToken.Name, "更新令牌"+cleanToken.Name, "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",

View File

@ -1206,7 +1206,7 @@ func UpdateUser(c *gin.Context) {
if originUser.DistributorCommissionBps != updatedUser.DistributorCommissionBps { if originUser.DistributorCommissionBps != updatedUser.DistributorCommissionBps {
changes = append(changes, service.FieldChange{Field: "分销佣金比例", OldValue: fmt.Sprintf("%d‱", originUser.DistributorCommissionBps), NewValue: fmt.Sprintf("%d‱", updatedUser.DistributorCommissionBps)}) changes = append(changes, service.FieldChange{Field: "分销佣金比例", OldValue: fmt.Sprintf("%d‱", originUser.DistributorCommissionBps), NewValue: fmt.Sprintf("%d‱", updatedUser.DistributorCommissionBps)})
} }
service.RecordUpdateWithDiff(c, "user", updatedUser.Id, updatedUser.Username, "更新用户: "+updatedUser.Username, changes, "") service.RecordUpdateWithDiff(c, "user", updatedUser.Id, updatedUser.Username, "更新用户"+updatedUser.Username, changes, "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
@ -1246,7 +1246,7 @@ func AdminClearUserBinding(c *gin.Context) {
} }
model.RecordLog(user.Id, model.LogTypeManage, fmt.Sprintf("admin cleared %s binding for user %s", bindingType, user.Username)) model.RecordLog(user.Id, model.LogTypeManage, fmt.Sprintf("admin cleared %s binding for user %s", bindingType, user.Username))
service.RecordDeleteOperation(c, "user_binding", user.Id, user.Username, "清除用户绑定["+bindingType+"]: "+user.Username, "") service.RecordDeleteOperation(c, "user_binding", user.Id, user.Username, "清除用户绑定["+bindingType+"]"+user.Username, "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
@ -1421,7 +1421,7 @@ func DeleteUser(c *gin.Context) {
}) })
return return
} }
service.RecordDeleteOperation(c, "user", id, originUser.Username, "删除用户: "+originUser.Username, "") service.RecordDeleteOperation(c, "user", id, originUser.Username, "删除用户"+originUser.Username, "")
} }
func DeleteSelf(c *gin.Context) { func DeleteSelf(c *gin.Context) {
@ -1501,7 +1501,7 @@ func CreateUser(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
service.RecordCreateOperation(c, "user", cleanUser.Id, cleanUser.Username, "创建用户: "+cleanUser.Username, "") service.RecordCreateOperation(c, "user", cleanUser.Id, cleanUser.Username, "创建用户"+cleanUser.Username, "")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
@ -1781,7 +1781,7 @@ func ManageUser(c *gin.Context) {
} }
changes = append(changes, service.FieldChange{Field: "学员", OldValue: oldLabel, NewValue: newLabel}) changes = append(changes, service.FieldChange{Field: "学员", OldValue: oldLabel, NewValue: newLabel})
} }
service.RecordUpdateWithDiff(c, "user", user.Id, user.Username, label+": "+user.Username, changes, "") service.RecordUpdateWithDiff(c, "user", user.Id, user.Username, label+""+user.Username, changes, "")
} }
switch req.Action { switch req.Action {

2
create_pages.py Normal file
View File

@ -0,0 +1,2 @@
import sys
print(" hello\)

4
fix2.py Normal file
View File

@ -0,0 +1,4 @@
import sys
c=open(sys.argv[1,'r',encoding='utf-8').read()
o=c
print(len(c))

14
fix3.py Normal file
View File

@ -0,0 +1,14 @@
import sys, re
c = open(sys.argv[1], chr(39)+chr(114)+chr(39)+chr(44)+chr(101)+chr(110)+chr(99)+chr(111)+chr(100)+chr(105)+chr(110)+chr(103)+chr(61)+chr(39)+chr(117)+chr(116)+chr(102)+chr(45)+chr(56)+chr(39)+chr(41)+chr(46)+chr(114)+chr(101)+chr(97)+chr(100)+chr(40)+chr(41)
o = c
c = c.replace(chr(60)+chr(100)+chr(105)+chr(118)+chr(32)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(78)+chr(97)+chr(109)+chr(101)+chr(61)+chr(34)+chr(103)+chr(114)+chr(105)+chr(100)+chr(32)+chr(109)+chr(100)+chr(58)+chr(103)+chr(114)+chr(105)+chr(100)+chr(45)+chr(99)+chr(111)+chr(108)+chr(115)+chr(45)+chr(51)+chr(32)+chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114)+chr(32)+chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114)+chr(45)+chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114)+chr(34)+chr(62), chr(60)+chr(100)+chr(105)+chr(118)+chr(32)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(78)+chr(97)+chr(109)+chr(101)+chr(61)+chr(34)+chr(103)+chr(114)+chr(105)+chr(100)+chr(32)+chr(115)+chr(109)+chr(58)+chr(103)+chr(114)+chr(105)+chr(100)+chr(45)+chr(99)+chr(111)+chr(108)+chr(115)+chr(45)+chr(50)+chr(32)+chr(108)+chr(103)+chr(58)+chr(103)+chr(114)+chr(105)+chr(100)+chr(45)+chr(99)+chr(111)+chr(108)+chr(115)+chr(45)+chr(51)+chr(34)+chr(62))
c = c.replace(chr(116)+chr(114)+chr(97)+chr(110)+chr(115)+chr(105)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(99)+chr(111)+chr(108)+chr(111)+chr(114)+chr(115)+chr(32)+chr(100)+chr(117)+chr(114)+chr(97)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(49)+chr(48)+chr(48)+chr(32)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(98)+chr(103)+chr(45)+chr(102)+chr(111)+chr(114)+chr(101)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(32)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(98)+chr(97)+chr(99)+chr(107)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100), chr(116)+chr(114)+chr(97)+chr(110)+chr(115)+chr(105)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(97)+chr(108)+chr(108)+chr(32)+chr(100)+chr(117)+chr(114)+chr(97)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(50)+chr(48)+chr(48)+chr(32)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(98)+chr(103)+chr(45)+chr(102)+chr(111)+chr(114)+chr(101)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(32)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(98)+chr(97)+chr(99)+chr(107)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(32)+chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114)+chr(45)+chr(98)+chr(32)+chr(115)+chr(109)+chr(58)+chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114)+chr(45)+chr(114)+chr(32)+chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114)+chr(45)+chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114))
c = c.replace(chr(98)+chr(103)+chr(45)+chr(109)+chr(117)+chr(116)+chr(101)+chr(100)+chr(32)+chr(112)+chr(120)+chr(45)+chr(50)+chr(32)+chr(112)+chr(121)+chr(45)+chr(49)+chr(32)+chr(119)+chr(45)+chr(102)+chr(105)+chr(116)+chr(32)+chr(103)+chr(114)+chr(111)+chr(117)+chr(112)+chr(45)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(98)+chr(103)+chr(45)+chr(119)+chr(104)+chr(105)+chr(116)+chr(101)+chr(47)+chr(49)+chr(48)+chr(32)+chr(103)+chr(114)+chr(111)+chr(117)+chr(112)+chr(45)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(98)+chr(97)+chr(99)+chr(107)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(32)+chr(116)+chr(114)+chr(97)+chr(110)+chr(115)+chr(105)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(99)+chr(111)+chr(108)+chr(111)+chr(114)+chr(115)+chr(32)+chr(100)+chr(117)+chr(114)+chr(97)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(49)+chr(48)+chr(48), chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114)+chr(32)+chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114)+chr(45)+chr(102)+chr(111)+chr(114)+chr(101)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(47)+chr(49)+chr(48)+chr(32)+chr(112)+chr(120)+chr(45)+chr(50)+chr(32)+chr(112)+chr(121)+chr(45)+chr(49)+chr(32)+chr(119)+chr(45)+chr(102)+chr(105)+chr(116)+chr(32)+chr(103)+chr(114)+chr(111)+chr(117)+chr(112)+chr(45)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(98)+chr(111)+chr(114)+chr(100)+chr(101)+chr(114)+chr(45)+chr(98)+chr(97)+chr(99)+chr(107)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(47)+chr(51)+chr(48)+chr(32)+chr(103)+chr(114)+chr(111)+chr(117)+chr(112)+chr(45)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(98)+chr(97)+chr(99)+chr(107)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(32)+chr(116)+chr(114)+chr(97)+chr(110)+chr(115)+chr(105)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(97)+chr(108)+chr(108)+chr(32)+chr(100)+chr(117)+chr(114)+chr(97)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(50)+chr(48)+chr(48))
c = c.replace(chr(116)+chr(114)+chr(97)+chr(110)+chr(115)+chr(105)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(99)+chr(111)+chr(108)+chr(111)+chr(114)+chr(115)+chr(32)+chr(100)+chr(117)+chr(114)+chr(97)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(49)+chr(48)+chr(48), chr(116)+chr(114)+chr(97)+chr(110)+chr(115)+chr(105)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(97)+chr(108)+chr(108)+chr(32)+chr(100)+chr(117)+chr(114)+chr(97)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(50)+chr(48)+chr(48))
c = c.replace(chr(102)+chr(111)+chr(110)+chr(116)+chr(45)+chr(104)+chr(101)+chr(97)+chr(100)+chr(105)+chr(110)+chr(103)+chr(32)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(108)+chr(103)+chr(32)+chr(102)+chr(111)+chr(110)+chr(116)+chr(45)+chr(98)+chr(111)+chr(108)+chr(100)+chr(32)+chr(109)+chr(98)+chr(45)+chr(50)+chr(32)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(102)+chr(111)+chr(114)+chr(101)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(32)+chr(103)+chr(114)+chr(111)+chr(117)+chr(112)+chr(45)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(98)+chr(97)+chr(99)+chr(107)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100), chr(102)+chr(111)+chr(110)+chr(116)+chr(45)+chr(104)+chr(101)+chr(97)+chr(100)+chr(105)+chr(110)+chr(103)+chr(32)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(108)+chr(103)+chr(32)+chr(102)+chr(111)+chr(110)+chr(116)+chr(45)+chr(98)+chr(111)+chr(108)+chr(100)+chr(32)+chr(109)+chr(98)+chr(45)+chr(50)+chr(32)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(102)+chr(111)+chr(114)+chr(101)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(32)+chr(103)+chr(114)+chr(111)+chr(117)+chr(112)+chr(45)+chr(104)+chr(111)+chr(118)+chr(101)+chr(114)+chr(58)+chr(116)+chr(101)+chr(120)+chr(116)+chr(45)+chr(98)+chr(97)+chr(99)+chr(107)+chr(103)+chr(114)+chr(111)+chr(117)+chr(110)+chr(100)+chr(32)+chr(116)+chr(114)+chr(97)+chr(110)+chr(115)+chr(105)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(97)+chr(108)+chr(108)+chr(32)+chr(100)+chr(117)+chr(114)+chr(97)+chr(116)+chr(105)+chr(111)+chr(110)+chr(45)+chr(50)+chr(48)+chr(48))
import re
c = re.sub(chr(92)+chr(36)+chr(92)+chr(123)+chr(91)+chr(94)+chr(125)+chr(93)+chr(43)+chr(92)+chr(125), '', c)
c = c.replace(chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(78)+chr(97)+chr(109)+chr(101)+chr(61)+chr(96)+chr(103)+chr(114)+chr(111)+chr(117)+chr(112)+chr(32), chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(78)+chr(97)+chr(109)+chr(101)+chr(61)+chr(34)+chr(103)+chr(114)+chr(111)+chr(117)+chr(112)+chr(32))
print(chr(67)+chr(72)+chr(65)+chr(78)+chr(71)+chr(69)+chr(68) if c != o else chr(78)+chr(79)+chr(67)+chr(72)+chr(65)+chr(78)+chr(71)+chr(69))
if c != o:
open(sys.argv[1], chr(119)+chr(44)+chr(101)+chr(110)+chr(99)+chr(111)+chr(100)+chr(105)+chr(110)+chr(103)+chr(61)+chr(39)+chr(117)+chr(116)+chr(102)+chr(45)+chr(56)+chr(39)+chr(41)+chr(46)+chr(119)+chr(114)+chr(105)+chr(116)+chr(101)+chr(40)+chr(99)

1
fix_all.py Normal file
View File

@ -0,0 +1 @@
print(" not using template\)

1
fix_hero.py Normal file
View File

@ -0,0 +1 @@
#

1
fix_home.py Normal file
View File

@ -0,0 +1 @@
# test

26
go.mod
View File

@ -14,11 +14,11 @@ require (
github.com/aws/smithy-go v1.24.3 github.com/aws/smithy-go v1.24.3
github.com/bytedance/gopkg v0.1.4 github.com/bytedance/gopkg v0.1.4
github.com/fyinfor/router-engine v0.1.0 github.com/fyinfor/router-engine v0.1.0
github.com/gin-contrib/cors v1.7.7 github.com/gin-contrib/cors v1.5.0
github.com/gin-contrib/gzip v1.2.6 github.com/gin-contrib/gzip v1.1.0
github.com/gin-contrib/sessions v1.1.0 github.com/gin-contrib/sessions v1.0.1
github.com/gin-contrib/static v1.1.6 github.com/gin-contrib/static v1.1.3
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-audio/aiff v1.1.0 github.com/go-audio/aiff v1.1.0
github.com/go-audio/wav v1.1.0 github.com/go-audio/wav v1.1.0
@ -42,6 +42,9 @@ require (
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/stripe/stripe-go/v81 v81.4.0 github.com/stripe/stripe-go/v81 v81.4.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
github.com/thanhpk/randstr v1.0.6 github.com/thanhpk/randstr v1.0.6
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
@ -77,7 +80,6 @@ require (
github.com/bytedance/sonic/loader v0.5.1 // indirect github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
@ -99,7 +101,6 @@ require (
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.3 // indirect github.com/go-webauthn/x v0.2.3 // indirect
github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tpm v0.9.8 // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
@ -134,15 +135,8 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/procfs v0.20.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/samber/go-singleflightx v0.3.2 // indirect github.com/samber/go-singleflightx v0.3.2 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/swaggo/gin-swagger v1.6.1 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tinylib/msgp v1.6.3 // indirect github.com/tinylib/msgp v1.6.3 // indirect
@ -150,13 +144,10 @@ require (
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/arch v0.26.0 // indirect golang.org/x/arch v0.26.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.35.0 // indirect golang.org/x/mod v0.35.0 // indirect
golang.org/x/tools v0.44.0 // indirect golang.org/x/tools v0.44.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
@ -165,5 +156,4 @@ require (
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.48.2 // indirect modernc.org/sqlite v1.48.2 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
) )

229
go.sum
View File

@ -1,6 +1,5 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A= github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
@ -13,48 +12,26 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/abema/go-mp4 v1.4.1 h1:YoS4VRqd+pAmddRPLFf8vMk74kuGl6ULSjzhsIqwr6M=
github.com/abema/go-mp4 v1.4.1/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/abema/go-mp4 v1.5.0 h1:aJnu723gFuNswIiM08h4kO28pUZr0QXNAJGoZWT96AU= github.com/abema/go-mp4 v1.5.0 h1:aJnu723gFuNswIiM08h4kO28pUZr0QXNAJGoZWT96AU=
github.com/abema/go-mp4 v1.5.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws= github.com/abema/go-mp4 v1.5.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k=
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs= github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI= github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI= github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8= github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI= github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w= github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0 h1:TDKR8ACRw7G+GFaQlhoy6biu+8q6ZtSddQCy9avMdMI=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0/go.mod h1:XlhOh5Ax/lesqN4aZCUgj9vVJed5VoXYHHFYGAlJEwU=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4 h1:W6tKfa/s37faUnwJ71pGqsBO7/wfUX1L7tVprupQGo4= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4 h1:W6tKfa/s37faUnwJ71pGqsBO7/wfUX1L7tVprupQGo4=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4/go.mod h1:BZ+9thH0QOTDUwE8KAv/ZwUzsNC7CSMJXj/wtnZMs5k= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4/go.mod h1:BZ+9thH0QOTDUwE8KAv/ZwUzsNC7CSMJXj/wtnZMs5k=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/smithy-go v1.24.3 h1:XgOAaUgx+HhVBoP4v8n6HCQoTRDhoMghKqw4LNHsDNg= github.com/aws/smithy-go v1.24.3 h1:XgOAaUgx+HhVBoP4v8n6HCQoTRDhoMghKqw4LNHsDNg=
github.com/aws/smithy-go v1.24.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aws/smithy-go v1.24.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -62,24 +39,16 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -94,46 +63,26 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fyinfor/router-engine v0.1.0 h1:53Z8Ca28EpfSocnKTABaht79toE4bkZd6C3UaU5r1Uw= github.com/fyinfor/router-engine v0.1.0 h1:53Z8Ca28EpfSocnKTABaht79toE4bkZd6C3UaU5r1Uw=
github.com/fyinfor/router-engine v0.1.0/go.mod h1:7qMxp7aoYsSDZ7dJgPEGXzORyJU+xwuQ2FuB7NgRs8E= github.com/fyinfor/router-engine v0.1.0/go.mod h1:7qMxp7aoYsSDZ7dJgPEGXzORyJU+xwuQ2FuB7NgRs8E=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q= github.com/gin-contrib/gzip v1.1.0 h1:kVw7Nr9M+Z6Ch4qo7aGMbiqxDeyQFru+07MgAcUF62M=
github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE= github.com/gin-contrib/gzip v1.1.0/go.mod h1:iHJXCup4CWiKyPUEl+GwkHjchl+YyYuMKbOCiXujPIA=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg=
github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sessions v1.1.0 h1:00mhHfNEGF5sP2fwxa98aRqj1FOJdL6IkR86n2hOiBo=
github.com/gin-contrib/sessions v1.1.0/go.mod h1:TyYZDIs6qCQg2SOoYPgMT9pAkmZceVNEJMcv5qbIy60=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U= github.com/gin-contrib/static v1.1.3 h1:WLOpkBtMDJ3gATFZgNJyVibFMio/UHonnueqJsQ0w4U=
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs= github.com/gin-contrib/static v1.1.3/go.mod h1:zejpJ/YWp8cZj/6EpiL5f/+skv5daQTNwRx1E8Pci30=
github.com/gin-contrib/static v1.1.6 h1:4/OIJI9PxO2jsUezNulpVbzI8ORMmdPlJ4P9QGwWgME= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-contrib/static v1.1.6/go.mod h1:e9qkj8wAlsxE6mSFGVL/flqGfVibw5amjNEUa4idmHc= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-audio/aiff v1.1.0 h1:m2LYgu/2BarpF2yZnFPWtY3Tp41k0A4y51gDRZZsEuU= github.com/go-audio/aiff v1.1.0 h1:m2LYgu/2BarpF2yZnFPWtY3Tp41k0A4y51gDRZZsEuU=
@ -145,7 +94,6 @@ github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38r
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@ -159,86 +107,58 @@ github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q= github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q=
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4= github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4=
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA= github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk= github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M= github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M=
github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0= github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0=
github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
@ -247,8 +167,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
@ -261,25 +179,19 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -287,8 +199,6 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -296,16 +206,10 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s= github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mewkiz/flac v1.0.13 h1:6wF8rRQKBFW159Daqx6Ro7K5ZnlVhHUKfS5aTsC4oXs= github.com/mewkiz/flac v1.0.13 h1:6wF8rRQKBFW159Daqx6Ro7K5ZnlVhHUKfS5aTsC4oXs=
github.com/mewkiz/flac v1.0.13/go.mod h1:HfPYDA+oxjyuqMu2V+cyKcxF51KM6incpw5eZXmfA6k= github.com/mewkiz/flac v1.0.13/go.mod h1:HfPYDA+oxjyuqMu2V+cyKcxF51KM6incpw5eZXmfA6k=
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d h1:IL2tii4jXLdhCeQN69HNzYYW1kl0meSG0wt5+sLwszU=
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d/go.mod h1:SIpumAnUWSy0q9RzKD3pyH3g1t5vdawUAPcW5tQrUtI=
github.com/mewkiz/pkg v0.0.0-20260331151047-10214ccde7de h1:tVseKKgTIPOo8L0gFK4qX+kqyINtWncwER/t1n2im1A= github.com/mewkiz/pkg v0.0.0-20260331151047-10214ccde7de h1:tVseKKgTIPOo8L0gFK4qX+kqyINtWncwER/t1n2im1A=
github.com/mewkiz/pkg v0.0.0-20260331151047-10214ccde7de/go.mod h1:omNJr4dHOKbrBeoY/idmLDFw8OIdDUUZvj3uB1cJxwA= github.com/mewkiz/pkg v0.0.0-20260331151047-10214ccde7de/go.mod h1:omNJr4dHOKbrBeoY/idmLDFw8OIdDUUZvj3uB1cJxwA=
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 h1:h8O1byDZ1uk6RUXMhj1QJU3VXFKXHDZxr4TXRPGeBa8= github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 h1:h8O1byDZ1uk6RUXMhj1QJU3VXFKXHDZxr4TXRPGeBa8=
@ -315,13 +219,10 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
@ -335,14 +236,10 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -350,50 +247,28 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/go-singleflightx v0.3.2 h1:jXbUU0fvis8Fdv4HGONboX5WdEZcYLoBEcKiE+ITCyQ= github.com/samber/go-singleflightx v0.3.2 h1:jXbUU0fvis8Fdv4HGONboX5WdEZcYLoBEcKiE+ITCyQ=
github.com/samber/go-singleflightx v0.3.2/go.mod h1:X2BR+oheHIYc73PvxRMlcASg6KYYTQyUYpdVU7t/ux4= github.com/samber/go-singleflightx v0.3.2/go.mod h1:X2BR+oheHIYc73PvxRMlcASg6KYYTQyUYpdVU7t/ux4=
github.com/samber/hot v0.11.0 h1:JhV9hk8SmZIqB0To8OyCzPubvszkuoSXWx/7FCEGO+Q=
github.com/samber/hot v0.11.0/go.mod h1:NB9v5U4NfDx7jmlrP+zHuqCuLUsywgAtCH7XOAkOxAg=
github.com/samber/hot v0.13.0 h1:4/OyD5xNfhmdHqyKWHHSisiytp93gA3knkWZLAvld2w= github.com/samber/hot v0.13.0 h1:4/OyD5xNfhmdHqyKWHHSisiytp93gA3knkWZLAvld2w=
github.com/samber/hot v0.13.0/go.mod h1:NB9v5U4NfDx7jmlrP+zHuqCuLUsywgAtCH7XOAkOxAg= github.com/samber/hot v0.13.0/go.mod h1:NB9v5U4NfDx7jmlrP+zHuqCuLUsywgAtCH7XOAkOxAg=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -405,7 +280,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@ -426,42 +300,26 @@ github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZu
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tiktoken-go/tokenizer v0.6.2 h1:t0GN2DvcUZSFWT/62YOgoqb10y7gSXBGs0A+4VCQK+g=
github.com/tiktoken-go/tokenizer v0.6.2/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w=
github.com/tiktoken-go/tokenizer v0.7.0 h1:VMu6MPT0bXFDHr7UPh9uii7CNItVt3X9K90omxL54vw= github.com/tiktoken-go/tokenizer v0.7.0 h1:VMu6MPT0bXFDHr7UPh9uii7CNItVt3X9K90omxL54vw=
github.com/tiktoken-go/tokenizer v0.7.0/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w= github.com/tiktoken-go/tokenizer v0.7.0/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/waffo-com/waffo-go v1.3.1 h1:NCYD3oQ59DTJj1bwS5T/659LI4h8PuAIW4Qj/w7fKPw= github.com/waffo-com/waffo-go v1.3.1 h1:NCYD3oQ59DTJj1bwS5T/659LI4h8PuAIW4Qj/w7fKPw=
github.com/waffo-com/waffo-go v1.3.1/go.mod h1:IaXVYq6mmYtrLFFsLxPslNwuIZx0mIadWWjhe+eWb0g= github.com/waffo-com/waffo-go v1.3.1/go.mod h1:IaXVYq6mmYtrLFFsLxPslNwuIZx0mIadWWjhe+eWb0g=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@ -471,39 +329,25 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c h1:xA2TJS9Hu/ivzaZIrDcwvpJ3Fnpsk5fDOJ4iSnL6J0w= github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c h1:xA2TJS9Hu/ivzaZIrDcwvpJ3Fnpsk5fDOJ4iSnL6J0w=
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc= github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4= golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -512,8 +356,6 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -523,27 +365,20 @@ golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -553,14 +388,9 @@ golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -568,45 +398,36 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
@ -617,13 +438,9 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

BIN
heroscreenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

1
idx2.py Normal file
View File

@ -0,0 +1 @@
test

View File

@ -222,7 +222,7 @@ func commonActionLabel(action string, targetType string, targetName string) stri
} }
if targetName != "" { if targetName != "" {
return actionLabel + typeLabel + ": " + targetName return actionLabel + typeLabel + "" + targetName
} }
return actionLabel + typeLabel return actionLabel + typeLabel
} }

View File

@ -272,6 +272,8 @@ func migrateDB() error {
if err := migrateTokenModelLimitsToText(); err != nil { if err := migrateTokenModelLimitsToText(); err != nil {
return err return err
} }
// Clear legacy external docs_link so the in-app /docs page is used by default
migrateDocsLinkToDefault()
// GORM AutoMigrate + Postgres: MigrateColumnUnique drops NamingStrategy.UniqueName(table, col) // GORM AutoMigrate + Postgres: MigrateColumnUnique drops NamingStrategy.UniqueName(table, col)
// when the column is UNIQUE in information_schema but the model uses uniqueIndex (field.Unique=false). // when the column is UNIQUE in information_schema but the model uses uniqueIndex (field.Unique=false).
// If the live constraint was created with another name (e.g. Postgres default), DROP uni_* fails (SQLSTATE 42704). // If the live constraint was created with another name (e.g. Postgres default), DROP uni_* fails (SQLSTATE 42704).
@ -583,6 +585,28 @@ func migrateTokenModelLimitsToText() error {
return nil return nil
} }
// migrateDocsLinkToDefault clears any external docs_link saved in DB so the in-app /docs page is used.
// Admins can still re-configure an external link via Settings UI if desired.
func migrateDocsLinkToDefault() {
if DB == nil {
return
}
key := "general_setting.docs_link"
var opt Option
if err := DB.Where("`key` = ?", key).First(&opt).Error; err != nil {
// No existing record or table doesn't exist — nothing to migrate
return
}
if opt.Value == "" {
return
}
if err := DB.Model(&Option{}).Where("`key` = ?", key).Update("value", "").Error; err != nil {
common.SysLog(fmt.Sprintf("migrateDocsLinkToDefault: failed to clear %s: %v", key, err))
return
}
common.SysLog("migrateDocsLinkToDefault: cleared legacy docs_link, now using in-app /docs page")
}
// migrateSubscriptionPlanPriceAmount migrates price_amount column from float/double to decimal(10,6) // migrateSubscriptionPlanPriceAmount migrates price_amount column from float/double to decimal(10,6)
// This is safe to run multiple times - it checks the column type first // This is safe to run multiple times - it checks the column type first
func migrateSubscriptionPlanPriceAmount() { func migrateSubscriptionPlanPriceAmount() {

View File

@ -290,6 +290,9 @@ func InitOptionMap() {
common.OptionMap[k] = v common.OptionMap[k] = v
} }
common.OptionMap["HeaderShowThemeToggle"] = strconv.FormatBool(true)
common.OptionMap["HeaderShowLanguageSelector"] = strconv.FormatBool(true)
common.OptionMapRWMutex.Unlock() common.OptionMapRWMutex.Unlock()
loadOptionsFromDatabase() loadOptionsFromDatabase()
} }

View File

@ -1,7 +1,10 @@
package model package model
import ( import (
"fmt"
"strings" "strings"
"github.com/QuantumNous/new-api/common"
) )
// 简化的供应商映射规则 // 简化的供应商映射规则
@ -20,6 +23,7 @@ var defaultVendorRules = map[string]string{
"qwen": "阿里巴巴", "qwen": "阿里巴巴",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
"abab": "MiniMax", "abab": "MiniMax",
"minimax": "MiniMax",
"ernie": "百度", "ernie": "百度",
"spark": "讯飞", "spark": "讯飞",
"hunyuan": "腾讯", "hunyuan": "腾讯",
@ -32,6 +36,7 @@ var defaultVendorRules = map[string]string{
"grok": "xAI", "grok": "xAI",
"llama": "Meta", "llama": "Meta",
"doubao": "字节跳动", "doubao": "字节跳动",
"seedance": "字节跳动",
"kling": "快手", "kling": "快手",
"jimeng": "即梦", "jimeng": "即梦",
"vidu": "Vidu", "vidu": "Vidu",
@ -39,10 +44,10 @@ var defaultVendorRules = map[string]string{
// 供应商默认图标映射 // 供应商默认图标映射
var defaultVendorIcons = map[string]string{ var defaultVendorIcons = map[string]string{
"OpenAI": "OpenAI", "OpenAI": "OpenAI.Color",
"Anthropic": "Claude.Color", "Anthropic": "Claude.Color",
"Google": "Gemini.Color", "Google": "Gemini.Color",
"Moonshot": "Moonshot", "Moonshot": "Moonshot.Color",
"智谱": "Zhipu.Color", "智谱": "Zhipu.Color",
"阿里巴巴": "Qwen.Color", "阿里巴巴": "Qwen.Color",
"DeepSeek": "DeepSeek.Color", "DeepSeek": "DeepSeek.Color",
@ -54,25 +59,30 @@ var defaultVendorIcons = map[string]string{
"Cloudflare": "Cloudflare.Color", "Cloudflare": "Cloudflare.Color",
"360": "Ai360.Color", "360": "Ai360.Color",
"零一万物": "Yi.Color", "零一万物": "Yi.Color",
"Jina": "Jina", "Jina": "Jina.Color",
"Mistral": "Mistral.Color", "Mistral": "Mistral.Color",
"xAI": "XAI", "xAI": "XAI.Color",
"Meta": "Ollama", "Ollama": "Ollama.Color",
"Meta": "Meta.Color",
"字节跳动": "Doubao.Color", "字节跳动": "Doubao.Color",
"快手": "Kling.Color", "快手": "Kling.Color",
"即梦": "Jimeng.Color", "即梦": "Jimeng.Color",
"Vidu": "Vidu", "Vidu": "Vidu.Color",
"微软": "AzureAI", "微软": "AzureAI.Color",
"Microsoft": "AzureAI", "Microsoft": "AzureAI.Color",
"Azure": "AzureAI", "Azure": "AzureAI.Color",
} }
// initDefaultVendorMapping 简化的默认供应商映射 // initDefaultVendorMapping 简化的默认供应商映射
func initDefaultVendorMapping(metaMap map[string]*Model, vendorMap map[int]*Vendor, enableAbilities []AbilityWithChannel) { func initDefaultVendorMapping(metaMap map[string]*Model, vendorMap map[int]*Vendor, enableAbilities []AbilityWithChannel) {
for _, ability := range enableAbilities { for _, ability := range enableAbilities {
modelName := ability.Model modelName := ability.Model
if _, exists := metaMap[modelName]; exists { if existing, exists := metaMap[modelName]; exists {
continue // 已有记录但 vendor_id 已赋值 → 跳过
if existing.VendorID != 0 {
continue
}
// vendor_id=0 → 继续尝试匹配(修复已有 DB 记录但缺供应商的场景)
} }
// 匹配供应商 // 匹配供应商
@ -85,12 +95,24 @@ func initDefaultVendorMapping(metaMap map[string]*Model, vendorMap map[int]*Vend
} }
} }
// 创建模型元数据 if vendorID == 0 {
metaMap[modelName] = &Model{ // 仍未匹配到供应商 → 跳过(不给未匹配的模型写入 vendor_id=0 覆盖已有记录)
ModelName: modelName, if strings.Contains(modelLower, "seedance") || strings.Contains(modelLower, "minimax") {
VendorID: vendorID, common.SysLog(fmt.Sprintf("initDefaultVendorMapping: model %q matched pattern but vendorID=0 (getOrCreateVendor failed)", modelName))
Status: 1, }
NameRule: NameRuleExact, continue
}
// 写入或更新供应商 ID
if existing, exists := metaMap[modelName]; exists {
existing.VendorID = vendorID
} else {
metaMap[modelName] = &Model{
ModelName: modelName,
VendorID: vendorID,
Status: 1,
NameRule: NameRuleExact,
}
} }
} }
} }
@ -112,9 +134,11 @@ func getOrCreateVendor(vendorName string, vendorMap map[int]*Vendor) int {
} }
if err := newVendor.Insert(); err != nil { if err := newVendor.Insert(); err != nil {
common.SysLog(fmt.Sprintf("getOrCreateVendor: insert vendor %q failed: %v", vendorName, err))
return 0 return 0
} }
common.SysLog(fmt.Sprintf("getOrCreateVendor: created new vendor %q with id=%d", vendorName, newVendor.Id))
vendorMap[newVendor.Id] = newVendor vendorMap[newVendor.Id] = newVendor
return newVendor.Id return newVendor.Id
} }

View File

@ -172,7 +172,11 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
effModelPrice = effModelPrice * meta.ImagePriceRatio effModelPrice = effModelPrice * meta.ImagePriceRatio
modelPrice = modelPrice * meta.ImagePriceRatio // 保持 ModelPrice 字段与 imagePriceRatio 一致(供日志展示) modelPrice = modelPrice * meta.ImagePriceRatio // 保持 ModelPrice 字段与 imagePriceRatio 一致(供日志展示)
} }
preConsumedQuota = int(effModelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio) preConsumedQuota = int(math.Round(effModelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio))
// 有有效定价但舍入为 0 时保底为 1避免计费为 0
if preConsumedQuota == 0 && effModelPrice > 0 && groupRatioInfo.GroupRatio > 0 {
preConsumedQuota = 1
}
} }
// check if free model pre-consume is disabled // check if free model pre-consume is disabled
@ -276,7 +280,11 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
markupDisc := effectiveMarkupDiscountPercent(c, info, channelID, info.OriginModelName) markupDisc := effectiveMarkupDiscountPercent(c, info, channelID, info.OriginModelName)
globalPrice, _ := ratio_setting.GetModelPrice(info.OriginModelName, false) globalPrice, _ := ratio_setting.GetModelPrice(info.OriginModelName, false)
effModelPrice := model.EffectiveModelPrice(modelPrice, globalPrice, chDisc, markupDisc) effModelPrice := model.EffectiveModelPrice(modelPrice, globalPrice, chDisc, markupDisc)
quota := int(effModelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio) quota := int(math.Round(effModelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio))
// 有有效定价但舍入为 0 时保底为 1避免计费为 0
if quota == 0 && effModelPrice > 0 && groupRatioInfo.GroupRatio > 0 {
quota = 1
}
// 免费模型检测(与 ModelPriceHelper 对齐) // 免费模型检测(与 ModelPriceHelper 对齐)
freeModel := false freeModel := false

View File

@ -18,6 +18,7 @@ func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
SetDashboardRouter(router) SetDashboardRouter(router)
SetRelayRouter(router) SetRelayRouter(router)
SetVideoRouter(router) SetVideoRouter(router)
frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL") frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL")
if common.IsMasterNode && frontendBaseUrl != "" { if common.IsMasterNode && frontendBaseUrl != "" {
frontendBaseUrl = "" frontendBaseUrl = ""

1
script.b64 Normal file
View File

@ -0,0 +1 @@
aW1wb3J0IHN5cwpwcmludCgndGVzdCcp

View File

@ -323,7 +323,8 @@ var vendorKeywordAliases = []struct {
{"qwen", []string{"alibaba", "qwen", "tongyi", "aliyun"}}, {"qwen", []string{"alibaba", "qwen", "tongyi", "aliyun"}},
{"moonshot", []string{"moonshot"}}, {"moonshot", []string{"moonshot"}},
{"kimi", []string{"moonshot"}}, {"kimi", []string{"moonshot"}},
{"doubao", []string{"bytedance", "volcengine", "volcano"}}, {"doubao", []string{"bytedance", "volcengine", "volcano", "字节跳动", "字节"}},
{"seedance", []string{"bytedance", "volcengine", "volcano", "doubao", "字节跳动", "字节"}},
{"ernie", []string{"baidu"}}, {"ernie", []string{"baidu"}},
{"wenxin", []string{"baidu"}}, {"wenxin", []string{"baidu"}},
{"hunyuan", []string{"tencent"}}, {"hunyuan", []string{"tencent"}},

View File

@ -66,7 +66,12 @@ func calculateAudioQuota(info QuotaInfo) int {
// 新公式:固定价格 = 渠道固定价 * 成本折扣率% + 全局固定价 * 加价折扣率% // 新公式:固定价格 = 渠道固定价 * 成本折扣率% + 全局固定价 * 加价折扣率%
effModelPrice := model.EffectiveModelPrice(info.ModelPrice, info.GlobalModelPrice, costDisc, markupDisc) effModelPrice := model.EffectiveModelPrice(info.ModelPrice, info.GlobalModelPrice, costDisc, markupDisc)
quota := decimal.NewFromFloat(effModelPrice).Mul(quotaPerUnit).Mul(groupRatio) quota := decimal.NewFromFloat(effModelPrice).Mul(quotaPerUnit).Mul(groupRatio)
return int(quota.IntPart()) quotaInt := int(quota.Round(0).IntPart())
// 有有效定价但舍入为 0 时保底为 1避免计费为 0
if info.UsePrice && quotaInt == 0 && quota.GreaterThan(decimal.Zero) {
quotaInt = 1
}
return quotaInt
} }
completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(info.ModelName)) completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(info.ModelName))
@ -101,7 +106,12 @@ func calculateAudioQuota(info QuotaInfo) int {
quota = decimal.NewFromInt(1) quota = decimal.NewFromInt(1)
} }
return int(quota.Round(0).IntPart()) quotaInt := int(quota.Round(0).IntPart())
// 有有效输入倍率但舍入为 0 时保底为 1如 0.01 经 Round 后为 0
if quotaInt == 0 && effInputRate > 0 && quota.GreaterThan(decimal.Zero) {
quotaInt = 1
}
return quotaInt
} }
func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage) error { func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage) error {

View File

@ -51,7 +51,7 @@ type GeneralSetting struct {
// 默认配置 // 默认配置
var generalSetting = GeneralSetting{ var generalSetting = GeneralSetting{
DocsLink: "https://docs.newapi.pro", DocsLink: "",
DefaultSiteLanguage: "zh-CN", DefaultSiteLanguage: "zh-CN",
PingIntervalEnabled: false, PingIntervalEnabled: false,
PingIntervalSeconds: 60, PingIntervalSeconds: 60,

View File

@ -0,0 +1,56 @@
package rankings_setting
import "github.com/QuantumNous/new-api/setting/config"
// ModelBenchmark 单个模型的评测数据
type ModelBenchmark struct {
Name string `json:"name"`
Provider string `json:"provider"`
Intelligence float64 `json:"intelligence"` // 智能指数
Speed float64 `json:"speed"` // 输出速度 (tokens/s)
Price float64 `json:"price"` // 每百万 token 价格 ($)
Latency float64 `json:"latency"` // 首 token 延迟 (秒)
ContextWindow string `json:"context_window"` // 上下文窗口大小描述
Type string `json:"type"` // reasoning / non-reasoning
Openness string `json:"openness"` // proprietary / open-weights
Category string `json:"category"` // language / image / video
}
// BenchmarkSetting 评测数据配置
type BenchmarkSetting struct {
Models []ModelBenchmark `json:"models"`
}
// 默认评测数据
var benchmarkSetting = BenchmarkSetting{
Models: []ModelBenchmark{
{Name: "Claude Fable 5", Provider: "Anthropic", Intelligence: 60, Speed: 42.0, Price: 15.00, Latency: 0.45, ContextWindow: "200K", Type: "reasoning", Openness: "proprietary", Category: "language"},
{Name: "Claude Opus 4.8", Provider: "Anthropic", Intelligence: 56, Speed: 38.0, Price: 15.00, Latency: 0.50, ContextWindow: "200K", Type: "reasoning", Openness: "proprietary", Category: "language"},
{Name: "GPT-5.5", Provider: "OpenAI", Intelligence: 55, Speed: 85.0, Price: 10.00, Latency: 0.35, ContextWindow: "128K", Type: "reasoning", Openness: "proprietary", Category: "language"},
{Name: "Claude Opus 4.7", Provider: "Anthropic", Intelligence: 54, Speed: 40.0, Price: 15.00, Latency: 0.48, ContextWindow: "200K", Type: "reasoning", Openness: "proprietary", Category: "language"},
{Name: "MiniMax-M3", Provider: "MiniMax", Intelligence: 44, Speed: 120.0, Price: 2.00, Latency: 0.30, ContextWindow: "128K", Type: "reasoning", Openness: "open-weights", Category: "language"},
{Name: "DeepSeek V4 Pro", Provider: "DeepSeek", Intelligence: 44, Speed: 95.0, Price: 1.50, Latency: 0.32, ContextWindow: "128K", Type: "reasoning", Openness: "open-weights", Category: "language"},
{Name: "Kimi K2.6", Provider: "Kimi", Intelligence: 43, Speed: 88.0, Price: 2.50, Latency: 0.38, ContextWindow: "128K", Type: "reasoning", Openness: "proprietary", Category: "language"},
{Name: "Qwen3.5 0.8B", Provider: "Alibaba", Intelligence: 18, Speed: 250.0, Price: 0.01, Latency: 0.25, ContextWindow: "32K", Type: "non-reasoning", Openness: "open-weights", Category: "language"},
{Name: "Gemini 2.5 Flash", Provider: "Google", Intelligence: 38, Speed: 180.0, Price: 0.15, Latency: 0.37, ContextWindow: "1M", Type: "non-reasoning", Openness: "proprietary", Category: "language"},
{Name: "Llama 4 Scout", Provider: "Meta", Intelligence: 35, Speed: 150.0, Price: 0.10, Latency: 0.40, ContextWindow: "10M", Type: "non-reasoning", Openness: "open-weights", Category: "language"},
{Name: "Mercury 2", Provider: "Inception Labs", Intelligence: 22, Speed: 839.3, Price: 0.50, Latency: 0.28, ContextWindow: "32K", Type: "non-reasoning", Openness: "proprietary", Category: "language"},
{Name: "GPT-4.1", Provider: "OpenAI", Intelligence: 42, Speed: 72.0, Price: 2.00, Latency: 0.42, ContextWindow: "128K", Type: "non-reasoning", Openness: "proprietary", Category: "language"},
{Name: "Claude Sonnet 4.6", Provider: "Anthropic", Intelligence: 45, Speed: 68.0, Price: 3.00, Latency: 0.44, ContextWindow: "200K", Type: "non-reasoning", Openness: "proprietary", Category: "language"},
{Name: "Gemma 3n E4B", Provider: "Google", Intelligence: 20, Speed: 300.0, Price: 0.02, Latency: 0.22, ContextWindow: "32K", Type: "non-reasoning", Openness: "open-weights", Category: "language"},
{Name: "Command A+", Provider: "Cohere", Intelligence: 32, Speed: 130.0, Price: 0.80, Latency: 0.39, ContextWindow: "128K", Type: "non-reasoning", Openness: "proprietary", Category: "language"},
{Name: "Grok 4", Provider: "xAI", Intelligence: 46, Speed: 75.0, Price: 5.00, Latency: 0.40, ContextWindow: "128K", Type: "reasoning", Openness: "proprietary", Category: "language"},
{Name: "North Mini Code", Provider: "North AI", Intelligence: 28, Speed: 200.0, Price: 0.25, Latency: 0.31, ContextWindow: "64K", Type: "non-reasoning", Openness: "proprietary", Category: "language"},
{Name: "LFM2 1.2B", Provider: "Liquid", Intelligence: 10, Speed: 542.6, Price: 0.05, Latency: 0.18, ContextWindow: "16K", Type: "non-reasoning", Openness: "open-weights", Category: "language"},
{Name: "Gemini 2.0 Pro", Provider: "Google", Intelligence: 48, Speed: 55.0, Price: 2.50, Latency: 0.50, ContextWindow: "2M", Type: "reasoning", Openness: "proprietary", Category: "language"},
},
}
func init() {
config.GlobalConfig.Register("benchmark_data", &benchmarkSetting)
}
// GetBenchmarkSetting 获取评测数据配置
func GetBenchmarkSetting() *BenchmarkSetting {
return &benchmarkSetting
}

View File

@ -198,7 +198,11 @@ func ChannelModelPrice2JSONString() string {
} }
func UpdateChannelModelPriceByJSONString(jsonStr string) error { func UpdateChannelModelPriceByJSONString(jsonStr string) error {
return types.LoadFromJsonString(channelModelPriceMap, jsonStr) err := types.LoadFromJsonString(channelModelPriceMap, jsonStr)
if err == nil {
rebuildChannelPricedModelSet()
}
return err
} }
func GetChannelModelPriceCopy() map[string]map[string]float64 { func GetChannelModelPriceCopy() map[string]map[string]float64 {
@ -227,7 +231,11 @@ func ChannelModelRatio2JSONString() string {
} }
func UpdateChannelModelRatioByJSONString(jsonStr string) error { func UpdateChannelModelRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonString(channelModelRatioMap, jsonStr) err := types.LoadFromJsonString(channelModelRatioMap, jsonStr)
if err == nil {
rebuildChannelPricedModelSet()
}
return err
} }
func GetChannelModelRatioCopy() map[string]map[string]float64 { func GetChannelModelRatioCopy() map[string]map[string]float64 {

View File

@ -2,6 +2,7 @@ package ratio_setting
import ( import (
"strings" "strings"
"sync"
"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/setting/operation_setting" "github.com/QuantumNous/new-api/setting/operation_setting"
@ -272,6 +273,15 @@ var defaultModelRatio = map[string]float64{
"deepseek-ai/DeepSeek-R1": 0.8, "deepseek-ai/DeepSeek-R1": 0.8,
"deepseek-ai/DeepSeek-V3-0324": 0.8, "deepseek-ai/DeepSeek-V3-0324": 0.8,
"deepseek-ai/DeepSeek-V3.1": 0.8, "deepseek-ai/DeepSeek-V3.1": 0.8,
"DeepSeek-V3.2": 0.8,
"deepseek-V4": 1.0,
"deepseek-v4-Flash": 0.2,
// MiniMax
"MiniMax-M2.1": 1.0,
"MiniMax-M2.1-highspeed": 0.5,
"MiniMax-M2": 0.8,
"MiniMax-M2.5": 1.2,
"MiniMax-M2.5-highspeed": 0.6,
} }
var defaultModelPrice = map[string]float64{ var defaultModelPrice = map[string]float64{
@ -341,6 +351,11 @@ var modelPriceMap = types.NewRWMap[string, float64]()
var modelRatioMap = types.NewRWMap[string, float64]() var modelRatioMap = types.NewRWMap[string, float64]()
var completionRatioMap = types.NewRWMap[string, float64]() var completionRatioMap = types.NewRWMap[string, float64]()
// channelPricedModelSet 缓存所有在渠道级定价ChannelModelRatio / ChannelModelPrice中配置了价格的模型名已 FormatMatchingModelName
// 用于 ModelHasConfiguredPricing 快速判断,避免每次遍历 channelModelRatioMap / channelModelPriceMap。
var channelPricedModelSetMu sync.RWMutex
var channelPricedModelSet = make(map[string]bool)
var defaultCompletionRatio = map[string]float64{ var defaultCompletionRatio = map[string]float64{
"gpt-4-gizmo-*": 2, "gpt-4-gizmo-*": 2,
"gpt-4o-gizmo-*": 3, "gpt-4o-gizmo-*": 3,
@ -364,6 +379,25 @@ func InitRatioSettings() {
imagePriceMap.AddAll(defaultImagePrice) imagePriceMap.AddAll(defaultImagePrice)
} }
// rebuildChannelPricedModelSet 从 channelModelRatioMap 和 channelModelPriceMap 重建 channelPricedModelSet。
// 在渠道级定价数据变更DB Option 同步 / 管理后台更新)后调用,确保 ModelHasConfiguredPricing 能感知渠道级定价。
func rebuildChannelPricedModelSet() {
set := make(map[string]bool)
for _, ratios := range channelModelRatioMap.ReadAll() {
for model := range ratios {
set[FormatMatchingModelName(model)] = true
}
}
for _, prices := range channelModelPriceMap.ReadAll() {
for model := range prices {
set[FormatMatchingModelName(model)] = true
}
}
channelPricedModelSetMu.Lock()
channelPricedModelSet = set
channelPricedModelSetMu.Unlock()
}
func GetModelPriceMap() map[string]float64 { func GetModelPriceMap() map[string]float64 {
return modelPriceMap.ReadAll() return modelPriceMap.ReadAll()
} }
@ -432,7 +466,7 @@ func GetModelRatio(name string) (float64, bool, string) {
return ratio, true, name return ratio, true, name
} }
// ModelHasConfiguredPricing 表示模型在价格表或倍率表中存在显式配置(含 compact 通配)。 // ModelHasConfiguredPricing 表示模型在价格表或倍率表中存在显式配置(含 compact 通配、渠道级定价)。
// 未命中表键时 GetModelRatio 不再提供可用倍率(非自用为 success=false自用为占位倍率此类模型不应出现在定价接口。 // 未命中表键时 GetModelRatio 不再提供可用倍率(非自用为 success=false自用为占位倍率此类模型不应出现在定价接口。
func ModelHasConfiguredPricing(model string) bool { func ModelHasConfiguredPricing(model string) bool {
if _, ok := GetModelPrice(model, false); ok { if _, ok := GetModelPrice(model, false); ok {
@ -447,7 +481,10 @@ func ModelHasConfiguredPricing(model string) bool {
return true return true
} }
} }
return false channelPricedModelSetMu.RLock()
ok := channelPricedModelSet[name]
channelPricedModelSetMu.RUnlock()
return ok
} }
func DefaultModelRatio2JSONString() string { func DefaultModelRatio2JSONString() string {
@ -870,6 +907,8 @@ func GetCompletionRatioCopy() map[string]float64 {
// 转换模型名,减少渠道必须配置各种带参数模型 // 转换模型名,减少渠道必须配置各种带参数模型
func FormatMatchingModelName(name string) string { func FormatMatchingModelName(name string) string {
// 去除空格,使 "Seedance 2.0" 与 "Seedance2.0" 等变体能匹配
name = strings.ReplaceAll(name, " ", "")
if strings.HasPrefix(name, "gemini-2.5-flash-lite") { if strings.HasPrefix(name, "gemini-2.5-flash-lite") {
name = handleThinkingBudgetModel(name, "gemini-2.5-flash-lite", "gemini-2.5-flash-lite-thinking-*") name = handleThinkingBudgetModel(name, "gemini-2.5-flash-lite", "gemini-2.5-flash-lite-thinking-*")

56
setup.ps1 Normal file
View File

@ -0,0 +1,56 @@
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# Login
$loginResp = Invoke-WebRequest -Uri "http://localhost:3000/api/user/login" -Method POST -Body '{"username":"admin","password":"admin12345"}' -ContentType "application/json; charset=utf-8" -UseBasicParsing -SessionVariable ws
Write-Host "Login OK"
$hdr = @{"New-Api-User"="1"}
# Create channels
$ch1 = '{"mode":"single","channel":{"name":"DeepSeek","type":43,"base_url":"https://api.deepseek.com","key":"sk-ds-test","models":"deepseek-V4,deepseek-v4-Flash","status":1,"supplier_type":"\u516c\u6709\u4e91","other":"{}","setting":"{}"}}'
$ch2 = '{"mode":"single","channel":{"name":"Minimax","type":35,"base_url":"https://api.minimax.chat","key":"sk-mm-test","models":"MiniMax-M2.1,MiniMax-M2.1-highspeed,MiniMax-M2,MiniMax-M2.5,MiniMax-M2.5-highspeed","status":1,"supplier_type":"\u516c\u6709\u4e91","other":"{}","setting":"{}"}}'
foreach ($ch in @($ch1,$ch2)) {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($ch)
$r = Invoke-WebRequest -Uri "http://localhost:3000/api/channel/" -Method POST -Body $bytes -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr
Write-Host "Channel created"
}
# Sync abilities
$m1 = [System.Text.Encoding]::UTF8.GetBytes('{"models":["deepseek-V4","deepseek-v4-Flash"]}')
$m2 = [System.Text.Encoding]::UTF8.GetBytes('{"models":["MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2","MiniMax-M2.5","MiniMax-M2.5-highspeed"]}')
Invoke-WebRequest -Uri "http://localhost:3000/api/channel/1/models" -Method PATCH -Body $m1 -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr | Out-Null
Invoke-WebRequest -Uri "http://localhost:3000/api/channel/2/models" -Method PATCH -Body $m2 -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr | Out-Null
Write-Host "Abilities synced"
# Create model metas
$names = @("deepseek-V4","deepseek-v4-Flash","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2","MiniMax-M2.5","MiniMax-M2.5-highspeed")
foreach ($n in $names) {
$body = [System.Text.Encoding]::UTF8.GetBytes("{`"model_name`":`"$n`",`"status`":1}")
Invoke-WebRequest -Uri "http://localhost:3000/api/models/" -Method POST -Body $body -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr | Out-Null
Write-Host "Meta: $n"
}
# Seed test results (required for pricing page to display data)
$testModels = @(
@{ch=1; ms=@("deepseek-V4","deepseek-v4-Flash"); msrt=200},
@{ch=2; ms=@("MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2","MiniMax-M2.5","MiniMax-M2.5-highspeed"); msrt=300}
)
foreach ($g in $testModels) {
foreach ($m in $g.ms) {
$body = [System.Text.Encoding]::UTF8.GetBytes("{`"channel_id`":$($g.ch),`"model_name`":`"$m`",`"manual_display_response_time`":$($g.msrt),`"manual_stability_grade`":5}")
Invoke-WebRequest -Uri "http://localhost:3000/api/channel/model-test-result-display" -Method PUT -Body $body -ContentType "application/json; charset=utf-8" -UseBasicParsing -WebSession $ws -Headers $hdr | Out-Null
Write-Host "Test result: ch=$($g.ch) $m"
}
}
# Reset ratios
$rr = Invoke-WebRequest -Uri "http://localhost:3000/api/option/rest_model_ratio" -Method POST -UseBasicParsing -WebSession $ws -Headers $hdr
Write-Host "Ratio reset: $($rr.Content)"
# Check
$resp = Invoke-WebRequest -Uri "http://localhost:3000/api/pricing" -UseBasicParsing -WebSession $ws -Headers $hdr
$data = $resp.Content | ConvertFrom-Json
Write-Host "`nPricing data count: $($data.data.Count)"
$data.data | ForEach-Object { Write-Host " $($_.model_name)" }

1
test_out.txt Normal file
View File

@ -0,0 +1 @@
hello

234
tokendance.yaml Normal file
View File

@ -0,0 +1,234 @@
- generic [ref=e2]:
- generic [ref=e5]:
- banner [ref=e6]:
- generic [ref=e7]:
- link "TokenDance 词元跳动 BETA" [ref=e8] [cursor=pointer]:
- /url: /
- img "TokenDance 词元跳动" [ref=e9]
- generic [ref=e10]: BETA
- generic [ref=e12]:
- img [ref=e13]
- searchbox "搜索模型..." [ref=e16]
- navigation [ref=e17]:
- link "模型" [ref=e18] [cursor=pointer]:
- /url: /models
- img [ref=e19]
- generic [ref=e22]: 模型
- link "体验" [ref=e23] [cursor=pointer]:
- /url: /playground
- img [ref=e24]
- generic [ref=e26]: 体验
- generic [ref=e27]:
- link "评测" [ref=e28] [cursor=pointer]:
- /url: /benchmarks
- img [ref=e29]
- generic [ref=e35]: 评测
- link "数据" [ref=e36] [cursor=pointer]:
- /url: /rankings
- img [ref=e37]
- generic [ref=e39]: 数据
- link "文档" [ref=e40] [cursor=pointer]:
- /url: /docs/quickstart
- img [ref=e41]
- generic [ref=e44]: 文档
- link "登录" [ref=e46] [cursor=pointer]:
- /url: /login
- generic [ref=e47]: 登录
- generic [ref=e48]:
- generic [ref=e50]:
- generic [ref=e51]:
- generic [ref=e52]:
- paragraph [ref=e53]: // unified model API gateway
- heading "让每位 AI 创造者, 少走一步弯路" [level=1] [ref=e55]:
- text: 让每位 AI 创造者,
- text: 少走一步弯路
- paragraph [ref=e56]: 兼容 OpenAI / Claude / Gemini 等协议,覆盖文本、图像、视频、语音,智能路由,统一计费。
- generic [ref=e57]:
- link "免费开始" [ref=e58] [cursor=pointer]:
- /url: /keys
- link "查看文档" [ref=e59] [cursor=pointer]:
- /url: /docs/quickstart
- link "战略合作方 无问芯穹" [ref=e61] [cursor=pointer]:
- /url: https://cloud.infini-ai.com
- generic [ref=e62]: 战略合作方
- img "无问芯穹" [ref=e63]
- generic [ref=e65]:
- generic [ref=e70]: terminal
- button "Copy" [ref=e71] [cursor=pointer]:
- img [ref=e72]
- code [ref=e76]: "$ curl https://tokendance.space/gateway/v1/chat/completions \\ -H \"Content-Type: application/json\" \\ -H \"Authorization: Bearer YOUR_KEY\" \\ -d '{ \"model\": \"MODEL_ID\", \"messages\": [{ \"role\": \"user\", \"content\": \"Hello World!\" }] }'"
- paragraph [ref=e78]: 200 OK · latency 312ms · tokens 42
- generic [ref=e81]:
- generic [ref=e82]: TOKEN
- generic [ref=e83]: DANCE
- generic [ref=e85]:
- generic [ref=e86]: OpenAI 协议
- generic [ref=e87]: Claude 协议
- generic [ref=e88]: Gemini 协议
- generic [ref=e89]: 智能路由
- generic [ref=e90]: 统一计费
- generic [ref=e91]: 模型丰富
- generic [ref=e92]: 安全可靠
- generic [ref=e93]: 开箱即用
- generic [ref=e94]: OpenAI 协议
- generic [ref=e95]: Claude 协议
- generic [ref=e96]: Gemini 协议
- generic [ref=e97]: 智能路由
- generic [ref=e98]: 统一计费
- generic [ref=e99]: 模型丰富
- generic [ref=e100]: 安全可靠
- generic [ref=e101]: 开箱即用
- generic [ref=e102]: OpenAI 协议
- generic [ref=e103]: Claude 协议
- generic [ref=e104]: Gemini 协议
- generic [ref=e105]: 智能路由
- generic [ref=e106]: 统一计费
- generic [ref=e107]: 模型丰富
- generic [ref=e108]: 安全可靠
- generic [ref=e109]: 开箱即用
- generic [ref=e110]: OpenAI 协议
- generic [ref=e111]: Claude 协议
- generic [ref=e112]: Gemini 协议
- generic [ref=e113]: 智能路由
- generic [ref=e114]: 统一计费
- generic [ref=e115]: 模型丰富
- generic [ref=e116]: 安全可靠
- generic [ref=e117]: 开箱即用
- generic [ref=e119]:
- generic [ref=e120]: Features
- heading "为接入 AI 模型的开发者而造。" [level=2] [ref=e121]
- generic [ref=e122]:
- generic [ref=e123]:
- code [ref=e124]: "baseURL: \"tokendance.space\""
- heading "多协议兼容" [level=3] [ref=e125]
- paragraph [ref=e126]: 原生支持 OpenAI、Claude、Gemini 文本协议,覆盖图像 / 视频 / 文本转语音生成。无需修改代码,切换 Base URL 即可接入。
- generic [ref=e127]:
- code [ref=e128]: route(model) → provider
- heading "智能路由" [level=3] [ref=e129]
- paragraph [ref=e130]: 根据模型名称自动路由至对应供应商。一个入口,无需关心底层调度。
- generic [ref=e131]:
- code [ref=e132]: billing.unified()
- heading "统一计费" [level=3] [ref=e133]
- paragraph [ref=e134]: 跨供应商统一 Token 消耗统计与账单。告别多平台分别充值的混乱。
- generic [ref=e135]:
- code [ref=e136]: "fallback: model[] → provider[]"
- heading "容错降级" [level=3] [ref=e137]
- paragraph [ref=e138]: 同一模型支持多供应商端点自动切换;单次请求可指定多个候选模型,逐级降级,保障服务持续可用。
- generic [ref=e139]:
- code [ref=e140]: models.list() → 8+
- heading "模型丰富" [level=3] [ref=e141]
- paragraph [ref=e142]: 接入 MiniMax、通义千问、Kimi、智谱、DeepSeek 等国内头部模型。持续扩展中。
- generic [ref=e143]:
- code [ref=e144]: import OpenAI from "openai"
- heading "开箱即用" [level=3] [ref=e145]
- paragraph [ref=e146]: Watcha 一键登录,分钟级接入。兼容现有 SDK零迁移成本。
- generic [ref=e148]:
- generic [ref=e149]:
- generic [ref=e150]: // how it works
- heading "三步接入,分钟级上线" [level=2] [ref=e151]
- link "01 注册账号 通过 Watcha 一键登录,即刻开始使用。 →" [ref=e152] [cursor=pointer]:
- /url: /login
- generic [ref=e153]:
- generic [ref=e155]: "01"
- generic [ref=e156]:
- heading "注册账号" [level=3] [ref=e157]
- paragraph [ref=e158]: 通过 Watcha 一键登录,即刻开始使用。
- generic [ref=e159]:
- link "02 创建 API Key 在控制台创建密钥,支持多 Key 管理与权限控制。 →" [ref=e160] [cursor=pointer]:
- /url: /keys
- generic [ref=e161]:
- generic [ref=e163]: "02"
- generic [ref=e164]:
- heading "创建 API Key" [level=3] [ref=e165]
- paragraph [ref=e166]: 在控制台创建密钥,支持多 Key 管理与权限控制。
- generic [ref=e167]:
- link "03 发起请求 使用你熟悉的 SDK 调用任意模型,完全兼容原生协议。 →" [ref=e168] [cursor=pointer]:
- /url: /docs/quickstart
- generic [ref=e169]:
- generic [ref=e171]: "03"
- generic [ref=e172]:
- heading "发起请求" [level=3] [ref=e173]
- paragraph [ref=e174]: 使用你熟悉的 SDK 调用任意模型,完全兼容原生协议。
- generic [ref=e175]:
- generic [ref=e178]:
- paragraph [ref=e179]: Quick Start
- heading "快速开始" [level=2] [ref=e180]
- generic [ref=e181]:
- button "OpenAI" [ref=e182] [cursor=pointer]
- button "Claude" [ref=e183] [cursor=pointer]
- button "Gemini" [ref=e184] [cursor=pointer]
- generic [ref=e185]:
- generic [ref=e186]:
- generic [ref=e187]:
- button "cURL" [ref=e188] [cursor=pointer]
- button "Python" [ref=e189] [cursor=pointer]
- button "Node.js" [ref=e190] [cursor=pointer]
- button "Copy" [ref=e191] [cursor=pointer]:
- img [ref=e192]
- code [ref=e197]:
- generic [ref=e198]: curl https://tokendance.space/gateway/v1/chat/completions \
- generic [ref=e199]: "-H \"Authorization: Bearer YOUR_API_KEY\" \\"
- generic [ref=e200]: "-H \"Content-Type: application/json\" \\"
- generic [ref=e201]: "-d '{"
- generic [ref=e202]: "\"model\": \"MODEL_ID\","
- generic [ref=e203]: "\"messages\": [{\"role\": \"user\", \"content\": \"Hello!\"}]"
- generic [ref=e204]: "}'"
- link "Documentation 阅读完整文档,了解更多用法 →" [ref=e205] [cursor=pointer]:
- /url: /docs/quickstart
- generic [ref=e206]:
- paragraph [ref=e207]: Documentation
- paragraph [ref=e208]: 阅读完整文档,了解更多用法
- generic [ref=e209]:
- generic [ref=e211]:
- generic [ref=e212]: // pricing
- heading "获取 Token 额度" [level=2] [ref=e213]
- generic [ref=e214]:
- generic [ref=e215]:
- generic [ref=e216]: 6/30 截止
- generic [ref=e217]: 注册即送 10 元额度
- generic [ref=e218]: 新用户注册即赠 10 元 Token7 天有效期
- link "百亿 Token 补贴计划 AI 时代Token 正在成为硬通货。当 Agent 开始吞噬软件,一个人也可以像一支队伍。我们在内测期间开启百亿 Token 补贴计划,赋能 AI 时代的超级个体 —— 无论你正在验证场景、打磨产品,还是准备扩大规模,都欢迎申请。 查看详情 →" [ref=e219] [cursor=pointer]:
- /url: https://mp.weixin.qq.com/s/D_ZmohSbk1RiR1W1W4EWDQ
- heading "百亿 Token 补贴计划" [level=3] [ref=e220]
- paragraph [ref=e221]: AI 时代Token 正在成为硬通货。当 Agent 开始吞噬软件,一个人也可以像一支队伍。我们在内测期间开启百亿 Token 补贴计划,赋能 AI 时代的超级个体 —— 无论你正在验证场景、打磨产品,还是准备扩大规模,都欢迎申请。
- generic [ref=e222]: 查看详情 →
- generic [ref=e223]:
- generic [ref=e224]:
- generic [ref=e226]: 限量 500 张
- heading "浦发观猹联名卡" [level=3] [ref=e227]
- generic [ref=e228]: 「浦耳猹」
- paragraph [ref=e229]: 前 200 位办理用户,赠送近千万 Token折合 RMB 100 元)
- generic [ref=e230]:
- link "立即申请 →" [ref=e231] [cursor=pointer]:
- /url: https://mp.weixin.qq.com/s/kWmrIU4IYxuCIDzLFQAvXg
- link "Token 申请问卷 →" [ref=e232] [cursor=pointer]:
- /url: https://agentuniverse.feishu.cn/share/base/form/shrcnvZvZq6hK2xgBvVGIuKpR5e
- generic [ref=e233]:
- heading "观猹开发者计划" [level=3] [ref=e234]
- paragraph [ref=e235]: 加入开发者计划,赠送超值 Token 额度。获取技术支持与优先体验新模型的机会。
- link "了解详情 →" [ref=e236] [cursor=pointer]:
- /url: https://agentuniverse.feishu.cn/wiki/J2FPwJp7zi6D6wklFbrcPbVNnNe
- paragraph [ref=e241]: 免费注册,分钟级接入。兼容你现有的 SDK 和工作流。
- contentinfo [ref=e242]:
- generic [ref=e243]:
- generic [ref=e244]:
- generic [ref=e245]:
- link "文档" [ref=e246] [cursor=pointer]:
- /url: /docs/quickstart
- link "模型" [ref=e247] [cursor=pointer]:
- /url: /models
- link "控制台" [ref=e248] [cursor=pointer]:
- /url: /keys
- link "战略合作方 无问芯穹" [ref=e249] [cursor=pointer]:
- /url: https://cloud.infini-ai.com
- generic [ref=e250]: 战略合作方
- img "无问芯穹" [ref=e251]
- generic [ref=e252]:
- generic [ref=e253]: © 2026 TokenDance
- generic [ref=e254]:
- link "浙ICP备2024107375号-10" [ref=e255] [cursor=pointer]:
- /url: https://beian.miit.gov.cn/
- link "浙公网安备33019202002952号" [ref=e256] [cursor=pointer]:
- /url: https://beian.mps.gov.cn/#/query/webSearch?code=33019202002952
- region "Notifications (F8)":
- list

234
tokendance.yml Normal file
View File

@ -0,0 +1,234 @@
- generic [ref=e2]:
- generic [ref=e5]:
- banner [ref=e6]:
- generic [ref=e7]:
- link "TokenDance 词元跳动 BETA" [ref=e8] [cursor=pointer]:
- /url: /
- img "TokenDance 词元跳动" [ref=e9]
- generic [ref=e10]: BETA
- generic [ref=e12]:
- img [ref=e13]
- searchbox "搜索模型..." [ref=e16]
- navigation [ref=e17]:
- link "模型" [ref=e18] [cursor=pointer]:
- /url: /models
- img [ref=e19]
- generic [ref=e22]: 模型
- link "体验" [ref=e23] [cursor=pointer]:
- /url: /playground
- img [ref=e24]
- generic [ref=e26]: 体验
- generic [ref=e27]:
- link "评测" [ref=e28] [cursor=pointer]:
- /url: /benchmarks
- img [ref=e29]
- generic [ref=e35]: 评测
- link "数据" [ref=e36] [cursor=pointer]:
- /url: /rankings
- img [ref=e37]
- generic [ref=e39]: 数据
- link "文档" [ref=e40] [cursor=pointer]:
- /url: /docs/quickstart
- img [ref=e41]
- generic [ref=e44]: 文档
- link "登录" [ref=e46] [cursor=pointer]:
- /url: /login
- generic [ref=e47]: 登录
- generic [ref=e48]:
- generic [ref=e50]:
- generic [ref=e51]:
- generic [ref=e52]:
- paragraph [ref=e53]: // unified model API gateway
- heading "让每位 AI 创造者, 少走一步弯路" [level=1] [ref=e55]:
- text: 让每位 AI 创造者,
- text: 少走一步弯路
- paragraph [ref=e56]: 兼容 OpenAI / Claude / Gemini 等协议,覆盖文本、图像、视频、语音,智能路由,统一计费。
- generic [ref=e57]:
- link "免费开始" [ref=e58] [cursor=pointer]:
- /url: /keys
- link "查看文档" [ref=e59] [cursor=pointer]:
- /url: /docs/quickstart
- link "战略合作方 无问芯穹" [ref=e61] [cursor=pointer]:
- /url: https://cloud.infini-ai.com
- generic [ref=e62]: 战略合作方
- img "无问芯穹" [ref=e63]
- generic [ref=e65]:
- generic [ref=e70]: terminal
- button "Copy" [ref=e71] [cursor=pointer]:
- img [ref=e72]
- code [ref=e76]: "$ curl https://tokendance.space/gateway/v1/chat/completions \\ -H \"Content-Type: application/json\" \\ -H \"Authorization: Bearer YOUR_KEY\" \\ -d '{ \"model\": \"MODEL_ID\", \"messages\": [{ \"role\": \"user\", \"content\": \"Hello World!\" }] }'"
- paragraph [ref=e78]: 200 OK · latency 312ms · tokens 42
- generic [ref=e81]:
- generic [ref=e82]: TOKEN
- generic [ref=e83]: DANCE
- generic [ref=e85]:
- generic [ref=e86]: OpenAI 协议
- generic [ref=e87]: Claude 协议
- generic [ref=e88]: Gemini 协议
- generic [ref=e89]: 智能路由
- generic [ref=e90]: 统一计费
- generic [ref=e91]: 模型丰富
- generic [ref=e92]: 安全可靠
- generic [ref=e93]: 开箱即用
- generic [ref=e94]: OpenAI 协议
- generic [ref=e95]: Claude 协议
- generic [ref=e96]: Gemini 协议
- generic [ref=e97]: 智能路由
- generic [ref=e98]: 统一计费
- generic [ref=e99]: 模型丰富
- generic [ref=e100]: 安全可靠
- generic [ref=e101]: 开箱即用
- generic [ref=e102]: OpenAI 协议
- generic [ref=e103]: Claude 协议
- generic [ref=e104]: Gemini 协议
- generic [ref=e105]: 智能路由
- generic [ref=e106]: 统一计费
- generic [ref=e107]: 模型丰富
- generic [ref=e108]: 安全可靠
- generic [ref=e109]: 开箱即用
- generic [ref=e110]: OpenAI 协议
- generic [ref=e111]: Claude 协议
- generic [ref=e112]: Gemini 协议
- generic [ref=e113]: 智能路由
- generic [ref=e114]: 统一计费
- generic [ref=e115]: 模型丰富
- generic [ref=e116]: 安全可靠
- generic [ref=e117]: 开箱即用
- generic [ref=e119]:
- generic [ref=e120]: Features
- heading "为接入 AI 模型的开发者而造。" [level=2] [ref=e121]
- generic [ref=e122]:
- generic [ref=e123]:
- code [ref=e124]: "baseURL: \"tokendance.space\""
- heading "多协议兼容" [level=3] [ref=e125]
- paragraph [ref=e126]: 原生支持 OpenAI、Claude、Gemini 文本协议,覆盖图像 / 视频 / 文本转语音生成。无需修改代码,切换 Base URL 即可接入。
- generic [ref=e127]:
- code [ref=e128]: route(model) → provider
- heading "智能路由" [level=3] [ref=e129]
- paragraph [ref=e130]: 根据模型名称自动路由至对应供应商。一个入口,无需关心底层调度。
- generic [ref=e131]:
- code [ref=e132]: billing.unified()
- heading "统一计费" [level=3] [ref=e133]
- paragraph [ref=e134]: 跨供应商统一 Token 消耗统计与账单。告别多平台分别充值的混乱。
- generic [ref=e135]:
- code [ref=e136]: "fallback: model[] → provider[]"
- heading "容错降级" [level=3] [ref=e137]
- paragraph [ref=e138]: 同一模型支持多供应商端点自动切换;单次请求可指定多个候选模型,逐级降级,保障服务持续可用。
- generic [ref=e139]:
- code [ref=e140]: models.list() → 8+
- heading "模型丰富" [level=3] [ref=e141]
- paragraph [ref=e142]: 接入 MiniMax、通义千问、Kimi、智谱、DeepSeek 等国内头部模型。持续扩展中。
- generic [ref=e143]:
- code [ref=e144]: import OpenAI from "openai"
- heading "开箱即用" [level=3] [ref=e145]
- paragraph [ref=e146]: Watcha 一键登录,分钟级接入。兼容现有 SDK零迁移成本。
- generic [ref=e148]:
- generic [ref=e149]:
- generic [ref=e150]: // how it works
- heading "三步接入,分钟级上线" [level=2] [ref=e151]
- link "01 注册账号 通过 Watcha 一键登录,即刻开始使用。 →" [ref=e152] [cursor=pointer]:
- /url: /login
- generic [ref=e153]:
- generic [ref=e155]: "01"
- generic [ref=e156]:
- heading "注册账号" [level=3] [ref=e157]
- paragraph [ref=e158]: 通过 Watcha 一键登录,即刻开始使用。
- generic [ref=e159]:
- link "02 创建 API Key 在控制台创建密钥,支持多 Key 管理与权限控制。 →" [ref=e160] [cursor=pointer]:
- /url: /keys
- generic [ref=e161]:
- generic [ref=e163]: "02"
- generic [ref=e164]:
- heading "创建 API Key" [level=3] [ref=e165]
- paragraph [ref=e166]: 在控制台创建密钥,支持多 Key 管理与权限控制。
- generic [ref=e167]:
- link "03 发起请求 使用你熟悉的 SDK 调用任意模型,完全兼容原生协议。 →" [ref=e168] [cursor=pointer]:
- /url: /docs/quickstart
- generic [ref=e169]:
- generic [ref=e171]: "03"
- generic [ref=e172]:
- heading "发起请求" [level=3] [ref=e173]
- paragraph [ref=e174]: 使用你熟悉的 SDK 调用任意模型,完全兼容原生协议。
- generic [ref=e175]:
- generic [ref=e178]:
- paragraph [ref=e179]: Quick Start
- heading "快速开始" [level=2] [ref=e180]
- generic [ref=e181]:
- button "OpenAI" [ref=e182] [cursor=pointer]
- button "Claude" [ref=e183] [cursor=pointer]
- button "Gemini" [ref=e184] [cursor=pointer]
- generic [ref=e185]:
- generic [ref=e186]:
- generic [ref=e187]:
- button "cURL" [ref=e188] [cursor=pointer]
- button "Python" [ref=e189] [cursor=pointer]
- button "Node.js" [ref=e190] [cursor=pointer]
- button "Copy" [ref=e191] [cursor=pointer]:
- img [ref=e192]
- code [ref=e197]:
- generic [ref=e198]: curl https://tokendance.space/gateway/v1/chat/completions \
- generic [ref=e199]: "-H \"Authorization: Bearer YOUR_API_KEY\" \\"
- generic [ref=e200]: "-H \"Content-Type: application/json\" \\"
- generic [ref=e201]: "-d '{"
- generic [ref=e202]: "\"model\": \"MODEL_ID\","
- generic [ref=e203]: "\"messages\": [{\"role\": \"user\", \"content\": \"Hello!\"}]"
- generic [ref=e204]: "}'"
- link "Documentation 阅读完整文档,了解更多用法 →" [ref=e205] [cursor=pointer]:
- /url: /docs/quickstart
- generic [ref=e206]:
- paragraph [ref=e207]: Documentation
- paragraph [ref=e208]: 阅读完整文档,了解更多用法
- generic [ref=e209]:
- generic [ref=e211]:
- generic [ref=e212]: // pricing
- heading "获取 Token 额度" [level=2] [ref=e213]
- generic [ref=e214]:
- generic [ref=e215]:
- generic [ref=e216]: 6/16 截止
- generic [ref=e217]: 注册即送 10 元额度
- generic [ref=e218]: 新用户注册即赠 10 元 Token7 天有效期
- link "百亿 Token 补贴计划 AI 时代Token 正在成为硬通货。当 Agent 开始吞噬软件,一个人也可以像一支队伍。我们在内测期间开启百亿 Token 补贴计划,赋能 AI 时代的超级个体 —— 无论你正在验证场景、打磨产品,还是准备扩大规模,都欢迎申请。 查看详情 →" [ref=e219] [cursor=pointer]:
- /url: https://mp.weixin.qq.com/s/D_ZmohSbk1RiR1W1W4EWDQ
- heading "百亿 Token 补贴计划" [level=3] [ref=e220]
- paragraph [ref=e221]: AI 时代Token 正在成为硬通货。当 Agent 开始吞噬软件,一个人也可以像一支队伍。我们在内测期间开启百亿 Token 补贴计划,赋能 AI 时代的超级个体 —— 无论你正在验证场景、打磨产品,还是准备扩大规模,都欢迎申请。
- generic [ref=e222]: 查看详情 →
- generic [ref=e223]:
- generic [ref=e224]:
- generic [ref=e226]: 限量 500 张
- heading "浦发观猹联名卡" [level=3] [ref=e227]
- generic [ref=e228]: 「浦耳猹」
- paragraph [ref=e229]: 前 200 位办理用户,赠送近千万 Token折合 RMB 100 元)
- generic [ref=e230]:
- link "立即申请 →" [ref=e231] [cursor=pointer]:
- /url: https://mp.weixin.qq.com/s/kWmrIU4IYxuCIDzLFQAvXg
- link "Token 申请问卷 →" [ref=e232] [cursor=pointer]:
- /url: https://agentuniverse.feishu.cn/share/base/form/shrcnvZvZq6hK2xgBvVGIuKpR5e
- generic [ref=e233]:
- heading "观猹开发者计划" [level=3] [ref=e234]
- paragraph [ref=e235]: 加入开发者计划,赠送超值 Token 额度。获取技术支持与优先体验新模型的机会。
- link "了解详情 →" [ref=e236] [cursor=pointer]:
- /url: https://agentuniverse.feishu.cn/wiki/J2FPwJp7zi6D6wklFbrcPbVNnNe
- paragraph [ref=e241]: 免费注册,分钟级接入。兼容你现有的 SDK 和工作流。
- contentinfo [ref=e242]:
- generic [ref=e243]:
- generic [ref=e244]:
- generic [ref=e245]:
- link "文档" [ref=e246] [cursor=pointer]:
- /url: /docs/quickstart
- link "模型" [ref=e247] [cursor=pointer]:
- /url: /models
- link "控制台" [ref=e248] [cursor=pointer]:
- /url: /keys
- link "战略合作方 无问芯穹" [ref=e249] [cursor=pointer]:
- /url: https://cloud.infini-ai.com
- generic [ref=e250]: 战略合作方
- img "无问芯穹" [ref=e251]
- generic [ref=e252]:
- generic [ref=e253]: © 2026 TokenDance
- generic [ref=e254]:
- link "浙ICP备2024107375号-10" [ref=e255] [cursor=pointer]:
- /url: https://beian.miit.gov.cn/
- link "浙公网安备33019202002952号" [ref=e256] [cursor=pointer]:
- /url: https://beian.mps.gov.cn/#/query/webSearch?code=33019202002952
- region "Notifications (F8)":
- list

1
w Normal file
View File

@ -0,0 +1 @@
x=1

5
web/bun.lock vendored
View File

@ -6,6 +6,7 @@
"name": "react-template", "name": "react-template",
"dependencies": { "dependencies": {
"@douyinfe/semi-icons": "^2.63.1", "@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-illustrations": "^2.100.0",
"@douyinfe/semi-ui": "^2.69.1", "@douyinfe/semi-ui": "^2.69.1",
"@lobehub/icons": "^2.0.0", "@lobehub/icons": "^2.0.0",
"@visactor/react-vchart": "~1.8.8", "@visactor/react-vchart": "~1.8.8",
@ -171,7 +172,7 @@
"@douyinfe/semi-icons": ["@douyinfe/semi-icons@2.94.0", "https://registry.npmmirror.com/@douyinfe/semi-icons/-/semi-icons-2.94.0.tgz", { "dependencies": { "classnames": "^2.2.6" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-Hn6Bm8umITAxw5xZlslrFXf+UBUWU5OD9YSNDN46H+OKnZFwbnYV628DBWSewBrvCItxUGMXHnLal4h1ZIb6qQ=="], "@douyinfe/semi-icons": ["@douyinfe/semi-icons@2.94.0", "https://registry.npmmirror.com/@douyinfe/semi-icons/-/semi-icons-2.94.0.tgz", { "dependencies": { "classnames": "^2.2.6" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-Hn6Bm8umITAxw5xZlslrFXf+UBUWU5OD9YSNDN46H+OKnZFwbnYV628DBWSewBrvCItxUGMXHnLal4h1ZIb6qQ=="],
"@douyinfe/semi-illustrations": ["@douyinfe/semi-illustrations@2.94.0", "https://registry.npmmirror.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.94.0.tgz", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-nXi4bysh4GsO/DxoNUmCd5NlRYeYZb7pqXcGZFj+PpxbPoG7Qn4jqhrUUFrJNppvven9nS0SgabDAHnT3o+jaQ=="], "@douyinfe/semi-illustrations": ["@douyinfe/semi-illustrations@2.100.0", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-SN7plpE328WGBohLHOVpYe6FwWSO6RLS7Xf6LhqEdtarwK52ircr4C/b+OyRqIwcLOzRYMgIoqcWnAQGmowcUw=="],
"@douyinfe/semi-json-viewer-core": ["@douyinfe/semi-json-viewer-core@2.94.0", "https://registry.npmmirror.com/@douyinfe/semi-json-viewer-core/-/semi-json-viewer-core-2.94.0.tgz", { "dependencies": { "jsonc-parser": "^3.3.1" } }, "sha512-pi4+Oi2zvsc0S3smWz58RNbfB73NgfO68fntKsVbLV1lTwNFuDxSswT8n8SXDZhtKoYgPVMmE3eMTc2nkXnvng=="], "@douyinfe/semi-json-viewer-core": ["@douyinfe/semi-json-viewer-core@2.94.0", "https://registry.npmmirror.com/@douyinfe/semi-json-viewer-core/-/semi-json-viewer-core-2.94.0.tgz", { "dependencies": { "jsonc-parser": "^3.3.1" } }, "sha512-pi4+Oi2zvsc0S3smWz58RNbfB73NgfO68fntKsVbLV1lTwNFuDxSswT8n8SXDZhtKoYgPVMmE3eMTc2nkXnvng=="],
@ -2261,6 +2262,8 @@
"@ant-design/cssinjs-utils/@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="], "@ant-design/cssinjs-utils/@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="],
"@douyinfe/semi-ui/@douyinfe/semi-illustrations": ["@douyinfe/semi-illustrations@2.94.0", "https://registry.npmmirror.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.94.0.tgz", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-nXi4bysh4GsO/DxoNUmCd5NlRYeYZb7pqXcGZFj+PpxbPoG7Qn4jqhrUUFrJNppvven9nS0SgabDAHnT3o+jaQ=="],
"@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], "@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="],
"@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],

76
web/deploy.py vendored Normal file
View File

@ -0,0 +1,76 @@
import paramiko
import os
import sys
host = "26.0.12.13"
user = "root"
password = "root@1234"
local_dist = r"C:\Users\wangxj\tokenFactory\web\dist"
remote_dist = "/root/tokenFactory/web/dist"
print("Connecting...")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(host, username=user, password=password)
# Clean old dist files AND rebuild Go
print("Cleaning old dist and rebuilding...")
stdin, stdout, stderr = ssh.exec_command(
"rm -rf /root/tokenFactory/web/dist && mkdir -p /root/tokenFactory/web/dist"
)
stdout.read()
sftp = ssh.open_sftp()
uploaded = 0
for root, dirs, files in os.walk(local_dist):
rel_path = os.path.relpath(root, local_dist)
if rel_path == ".":
remote_dir = remote_dist
else:
remote_dir = remote_dist + "/" + rel_path.replace("\\", "/")
stdin, stdout, stderr = ssh.exec_command(f"mkdir -p {remote_dir}")
stdout.read()
for f in files:
local_file = os.path.join(root, f)
remote_file = (remote_dir + "/" + f).replace("\\", "/")
try:
sftp.put(local_file, remote_file)
uploaded += 1
if uploaded % 30 == 0:
print(f" Uploaded {uploaded} files...")
except Exception as e:
print(f" FAILED: {f}: {e}")
sftp.close()
print(f"Uploaded {uploaded} files.")
# Rebuild Go
print("Building Go...")
stdin, stdout, stderr = ssh.exec_command(
"export PATH=$PATH:/usr/local/go/bin && cd /root/tokenFactory && go build -o tf . 2>&1"
)
out = stdout.read().decode()
err = stderr.read().decode()
if out.strip():
print("stdout:", out[-300:].strip())
if err.strip():
print("stderr:", err[-300:].strip())
# Replace binary file (not in a subdirectory — Docker mounts ./token-factory as a file)
print("Replacing binary...")
stdin, stdout, stderr = ssh.exec_command(
"rm -rf /root/tokenFactory/token-factory && cp /root/tokenFactory/tf /root/tokenFactory/token-factory && ls -la /root/tokenFactory/token-factory"
)
print(stdout.read().decode().strip())
# Docker restart
print("Restarting Docker...")
stdin, stdout, stderr = ssh.exec_command(
"cd /root/tokenFactory && docker compose down 2>&1 && docker compose up -d 2>&1"
)
print(stdout.read().decode()[-500:].strip())
ssh.close()
print("Done!")

27
web/index.html vendored
View File

@ -1,22 +1,18 @@
<!doctype html> <!doctype html>
<html lang="zh"> <html lang="zh">
<head> <head>
<!-- TokenDance Design System: Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400;1,700&family=Source+Serif+4:ital,wght@0,400;0,600;0,700;1,400&family=Noto+Serif+SC:wght@400;600;700;900&family=JetBrains+Mono:wght@400;500;700&family=Bebas+Neue&display=swap" rel="stylesheet" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="/logo.png" /> <link rel="icon" href="/logo.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<meta <meta name="description" content="Unified AI model gateway supporting OpenAI, Claude, Gemini and more." />
name="description"
lang="zh"
content="统一的 AI 模型聚合与分发网关,支持将各类大语言模型跨格式转换为 OpenAI、Claude、Gemini 兼容接口,为个人与企业提供集中式模型管理与网关服务。"
/>
<meta
name="description"
lang="en"
content="A unified AI model hub for aggregation & distribution. It supports cross-converting various LLMs into OpenAI-compatible, Claude-compatible, or Gemini-compatible formats. A centralized gateway for personal and enterprise model management."
/>
<meta name="generator" content="toekn-factory" /> <meta name="generator" content="toekn-factory" />
<!-- 标题由 /api/status 写入 localStorage 后由前端同步;不在此处读 localStorage以免旧缓存如改名前默认值抢先显示 --> <!-- 鏍囬鐢?/api/status 鍐欏叆 localStorage 鍚庣敱鍓嶇鍚屾锛涗笉鍦ㄦ澶勮 localStorage锛屼互鍏嶆棫缂撳瓨锛堝鏀瑰悕鍓嶉粯璁ゅ€硷級鎶㈠厛鏄剧ず -->
<title></title> <title></title>
<!--umami--> <!--umami-->
<!--Google Analytics--> <!--Google Analytics-->
@ -28,3 +24,8 @@
<script type="module" src="/src/index.jsx"></script> <script type="module" src="/src/index.jsx"></script>
</body> </body>
</html> </html>

3
web/package.json vendored
View File

@ -5,11 +5,14 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@douyinfe/semi-icons": "^2.63.1", "@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-illustrations": "^2.100.0",
"@douyinfe/semi-theme-default": "^2.101.0",
"@douyinfe/semi-ui": "^2.69.1", "@douyinfe/semi-ui": "^2.69.1",
"@lobehub/icons": "^2.0.0", "@lobehub/icons": "^2.0.0",
"@visactor/react-vchart": "~1.8.8", "@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8", "@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8",
"antd": "^5.29.3",
"axios": "1.13.5", "axios": "1.13.5",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",

10952
web/pnpm-lock.yaml vendored Normal file

File diff suppressed because it is too large Load Diff

4
web/pnpm-workspace.yaml vendored Normal file
View File

@ -0,0 +1,4 @@
allowBuilds:
'@parcel/watcher': set this to true or false
'@swc/core': set this to true or false
esbuild: set this to true or false

BIN
web/public/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 KiB

101
web/src/App.jsx vendored
View File

@ -1,4 +1,4 @@
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -18,6 +18,38 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { lazy, Suspense, useContext, useMemo } from 'react'; import React, { lazy, Suspense, useContext, useMemo } from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, info: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
this.setState({ info });
console.error('ErrorBoundary caught:', error, info);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: 40, color: 'red', fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
<h2>页面崩溃: {this.state.error?.message}</h2>
<details>
<summary>Stack</summary>
<pre>{this.state.error?.stack}</pre>
</details>
<details>
<summary>Component Stack</summary>
<pre>{this.state.info?.componentStack}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
import { Route, Routes, useLocation, useParams } from 'react-router-dom'; import { Route, Routes, useLocation, useParams } from 'react-router-dom';
import Loading from './components/common/ui/Loading'; import Loading from './components/common/ui/Loading';
import User from './pages/User'; import User from './pages/User';
@ -37,6 +69,7 @@ import Redemption from './pages/Redemption';
import TopUp from './pages/TopUp'; import TopUp from './pages/TopUp';
import Log from './pages/Log'; import Log from './pages/Log';
import Chat from './pages/Chat'; import Chat from './pages/Chat';
import Benchmarks from './pages/Benchmarks';
import Chat2Link from './pages/Chat2Link'; import Chat2Link from './pages/Chat2Link';
import Midjourney from './pages/Midjourney'; import Midjourney from './pages/Midjourney';
import Pricing from './pages/Pricing'; import Pricing from './pages/Pricing';
@ -45,6 +78,7 @@ import ModelPage from './pages/Model';
import ModelDeploymentPage from './pages/ModelDeployment'; import ModelDeploymentPage from './pages/ModelDeployment';
import ModelHeatPage from './pages/ModelHeat'; import ModelHeatPage from './pages/ModelHeat';
import Playground from './pages/Playground'; import Playground from './pages/Playground';
import ApiTester from './pages/ApiTester';
import Subscription from './pages/Subscription'; import Subscription from './pages/Subscription';
import OAuth2Callback from './components/auth/OAuth2Callback'; import OAuth2Callback from './components/auth/OAuth2Callback';
import PersonalSetting from './components/settings/PersonalSetting'; import PersonalSetting from './components/settings/PersonalSetting';
@ -57,6 +91,10 @@ import Suppliers from './pages/SupplierAdmin/list';
import Setup from './pages/Setup'; import Setup from './pages/Setup';
import SetupCheck from './components/layout/SetupCheck'; import SetupCheck from './components/layout/SetupCheck';
import OperationLog from './pages/OperationLog'; import OperationLog from './pages/OperationLog';
import LuckyBag from './pages/LuckyBag';
import Drawing from './pages/Drawing';
import ProfitDashboard from './pages/ProfitDashboard';
import SettingsAntiAbuse from './pages/Setting/Operation/SettingsAntiAbuse';
const Home = lazy(() => import('./pages/Home')); const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard')); const Dashboard = lazy(() => import('./pages/Dashboard'));
@ -67,6 +105,7 @@ const InviteRedirect = lazy(() => import('./pages/InviteRedirect'));
const About = lazy(() => import('./pages/About')); const About = lazy(() => import('./pages/About'));
const UserAgreement = lazy(() => import('./pages/UserAgreement')); const UserAgreement = lazy(() => import('./pages/UserAgreement'));
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy')); const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
const Docs = lazy(() => import('./pages/Docs'));
function DynamicOAuth2Callback() { function DynamicOAuth2Callback() {
const { provider } = useParams(); const { provider } = useParams();
@ -175,6 +214,14 @@ function App() {
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route
path='/console/api-tester'
element={
<PrivateRoute>
<ApiTester />
</PrivateRoute>
}
/>
<Route <Route
path='/console/redemption' path='/console/redemption'
element={ element={
@ -191,6 +238,38 @@ function App() {
</AdminRoute> </AdminRoute>
} }
/> />
<Route
path='/console/lucky-bag'
element={
<PrivateRoute>
<LuckyBag />
</PrivateRoute>
}
/>
<Route
path='/console/drawing'
element={
<PrivateRoute>
<Drawing />
</PrivateRoute>
}
/>
<Route
path='/console/profit'
element={
<PrivateRoute>
<ProfitDashboard />
</PrivateRoute>
}
/>
<Route
path='/console/anti-abuse'
element={
<PrivateRoute>
<SettingsAntiAbuse />
</PrivateRoute>
}
/>
<Route <Route
path='/console/supplier-application' path='/console/supplier-application'
element={ element={
@ -305,9 +384,11 @@ function App() {
path='/console/personal' path='/console/personal'
element={ element={
<PrivateRoute> <PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}> <ErrorBoundary>
<PersonalSetting /> <Suspense fallback={<Loading></Loading>} key={location.pathname}>
</Suspense> <PersonalSetting />
</Suspense>
</ErrorBoundary>
</PrivateRoute> </PrivateRoute>
} }
/> />
@ -464,6 +545,14 @@ function App() {
</Suspense> </Suspense>
} }
/> />
<Route
path='/docs'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Docs />
</Suspense>
}
/>
<Route <Route
path='/user-agreement' path='/user-agreement'
element={ element={
@ -499,10 +588,12 @@ function App() {
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route path='*' element={<NotFound />} /> <Route path='/benchmarks' element={<Benchmarks />} />
<Route path='*' element={<NotFound />} />
</Routes> </Routes>
</SetupCheck> </SetupCheck>
); );
} }
export default App; export default App;

View File

@ -1077,34 +1077,58 @@ const LoginForm = () => {
); );
}; };
return ( const brandTagline = t('统一 AI 模型网关,聚合全球顶尖大模型,一站接入即刻使用');
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */}
<div
className='blur-ball blur-ball-indigo'
style={{ top: '-80px', right: '-80px', transform: 'none' }}
/>
<div
className='blur-ball blur-ball-teal'
style={{ top: '50%', left: '-120px' }}
/>
<div className='w-full max-w-sm mt-[60px]'>
{showEmailLogin || !hasOAuthLoginOptions
? renderEmailLoginForm()
: renderOAuthOptions()}
{renderWeChatLoginModal()}
{render2FAModal()}
{turnstileEnabled && ( return (
<div className='flex justify-center mt-6'> <div className='flex min-h-screen'>
<Turnstile {/* 左侧品牌展示区 */}
sitekey={turnstileSiteKey} <div className='hidden lg:flex lg:w-1/2 relative overflow-hidden bg-zinc-900 flex-col items-center justify-center px-12'>
onVerify={(token) => { <div
setTurnstileToken(token); className='blur-ball blur-ball-indigo'
}} style={{ top: '-120px', right: '-120px', width: '400px', height: '400px' }}
/> />
<div
className='blur-ball blur-ball-teal'
style={{ bottom: '-80px', left: '-80px', width: '350px', height: '350px' }}
/>
<div className='relative z-10 text-center max-w-md'>
<img src={logo} alt='Logo' className='w-20 h-20 rounded-2xl mx-auto mb-8 shadow-2xl' />
<h1 className='text-4xl font-bold text-white mb-4 tracking-tight'>{systemName}</h1>
<p className='text-lg text-zinc-400 leading-relaxed'>{brandTagline}</p>
<div className='mt-12 flex gap-3 justify-center'>
<div className='w-12 h-1 rounded-full bg-emerald-400' />
<div className='w-8 h-1 rounded-full bg-blue-400' />
<div className='w-8 h-1 rounded-full bg-violet-400' />
<div className='w-6 h-1 rounded-full bg-amber-400' />
</div> </div>
)} </div>
</div>
{/* 右侧登录表单区 */}
<div className='flex-1 flex items-center justify-center bg-white dark:bg-zinc-950 px-4 sm:px-6 lg:px-8 py-12'>
<div className='w-full max-w-sm'>
{/* 移动端显示 logo */}
<div className='lg:hidden flex items-center justify-center gap-2 mb-8'>
<img src={logo} alt='Logo' className='w-8 h-8 rounded-full' />
<span className='text-lg font-semibold'>{systemName}</span>
</div>
{showEmailLogin || !hasOAuthLoginOptions
? renderEmailLoginForm()
: renderOAuthOptions()}
{renderWeChatLoginModal()}
{render2FAModal()}
{turnstileEnabled && (
<div className='flex justify-center mt-6'>
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
</div>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,61 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import clsx from 'clsx';
const variants = {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
};
const sizes = {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
};
const Button = React.forwardRef(
({ className, variant = 'default', size = 'default', children, ...props }, ref) => {
return (
<button
ref={ref}
className={clsx(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
variants[variant],
sizes[size],
className,
)}
{...props}
>
{children}
</button>
);
}
);
Button.displayName = 'Button';
export default Button;

View File

@ -0,0 +1,98 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import clsx from 'clsx';
const Card = React.forwardRef(({ className, children, ...props }, ref) => (
<div
ref={ref}
className={clsx(
'rounded-lg border border-border bg-card text-card-foreground shadow-sm',
className,
)}
{...props}
>
{children}
</div>
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef(({ className, children, ...props }, ref) => (
<div
ref={ref}
className={clsx('flex flex-col space-y-1.5 p-6', className)}
{...props}
>
{children}
</div>
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef(({ className, children, ...props }, ref) => (
<h3
ref={ref}
className={clsx(
'text-2xl font-semibold leading-none tracking-tight',
className,
)}
{...props}
>
{children}
</h3>
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef(
({ className, children, ...props }, ref) => (
<p
ref={ref}
className={clsx('text-sm text-muted-foreground', className)}
{...props}
>
{children}
</p>
)
);
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef(
({ className, children, ...props }, ref) => (
<div ref={ref} className={clsx('p-6 pt-0', className)} {...props}>
{children}
</div>
)
);
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={clsx('flex items-center p-6 pt-0', className)}
{...props}
>
{children}
</div>
)
);
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
export default Card;

View File

@ -0,0 +1,5 @@
// shadcn/ui style base components
// These are minimal wrappers around raw elements using CSS variable tokens
export { default as Button } from './Button';
export { default as Card } from './Card';

View File

@ -0,0 +1,28 @@
import React from 'react';
const BarChart = ({ data = [], height = 80, barW = 24 }) => {
if (!data || data.length === 0) return null;
const max = Math.max(...data, 1);
return (
<div className='flex w-full items-end justify-between' style={{ height }}>
{data.map((v, i) => {
const pct = Math.max(2, Math.round((v / max) * 100));
return (
<div
key={i}
className='flex flex-1 justify-center'
>
<div
className='rounded-sm bg-blue-500'
style={{ height: `${pct}%`, width: barW }}
/>
</div>
);
})}
</div>
);
};
export default BarChart;

View File

@ -0,0 +1,143 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Activity, CreditCard, Hash, Layers, Gauge } from 'lucide-react';
import { renderQuota } from '../../helpers';
const formatNumber = (n) => {
if (n == null || isNaN(n)) return '—';
const v = Number(n);
if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + 'M';
if (v >= 1_000) return (v / 1_000).toFixed(1) + 'K';
return v.toLocaleString();
};
const formatLatency = (v) => {
if (v == null) return '—';
if (v < 1) return `${Math.round(v * 1000)}ms`;
return `${v.toFixed(2)}s`;
};
const BentoStat = ({ label, value, unit, sub, delta, icon: Icon, className = '' }) => {
return (
<div
className={`relative flex flex-col gap-3 rounded-2xl border border-slate-200 bg-white p-5 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20 ${className}`}
>
<div className='flex items-center gap-3'>
<div className='flex h-9 w-9 items-center justify-center rounded-lg bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70'>
<Icon size={16} />
</div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>{label}</div>
</div>
<div className='flex flex-1 flex-col justify-center'>
<div className='flex items-baseline gap-2'>
<span className='font-mono text-3xl font-bold tabular-nums leading-none text-slate-900 md:text-4xl dark:text-white'>
{value}
</span>
{unit && <span className='text-xs text-slate-400 dark:text-white/40'>{unit}</span>}
</div>
</div>
<div className='space-y-1.5'>
{delta != null && (
<div
className={`inline-flex items-center gap-1.5 text-xs tabular-nums ${
delta < 0
? 'text-emerald-600 dark:text-emerald-400'
: delta > 0
? 'text-red-500 dark:text-red-400'
: 'text-slate-500 dark:text-white/50'
}`}
>
<span className='font-semibold'>
{delta > 0 ? '↑' : delta < 0 ? '↓' : '·'} {Math.abs(delta).toFixed(0)}%
</span>
<span className='font-normal text-slate-400 dark:text-white/40'>对比上月</span>
</div>
)}
{sub && (
<div className='text-[11px] text-slate-500 tabular-nums dark:text-white/40'>{sub}</div>
)}
</div>
</div>
);
};
const BentoStats = ({ metrics, monthDelta }) => {
return (
<>
<BentoStat
label='今日请求'
value={metrics.today.requests}
unit='次'
sub={
<span>
<span className='font-semibold text-slate-900 dark:text-white'>
{formatNumber(metrics.today.tokens)}
</span>{' '}
tokens
<span className='mx-1.5 text-slate-300 dark:text-white/30'>·</span>
{renderQuota(metrics.today.cost)}
</span>
}
icon={Activity}
/>
<BentoStat
label='今日 token'
value={formatNumber(metrics.today.tokens)}
unit='tokens'
sub={`今日花费 ${renderQuota(metrics.today.cost)}`}
icon={Hash}
/>
<BentoStat
label='平均响应'
value={formatLatency(metrics.today.avgLatency)}
sub='今日成功调用'
icon={Gauge}
/>
<BentoStat
label='本月消费'
value={metrics.month.cost ? renderQuota(metrics.month.cost) : '—'}
sub={
<span>
<span className='font-semibold tabular-nums text-slate-900 dark:text-white'>
{metrics.month.requests}
</span>{' '}
次调用
<span className='mx-1.5 text-slate-300 dark:text-white/30'>·</span>
<span className='tabular-nums'>{formatNumber(metrics.month.tokens)}</span> tokens
</span>
}
delta={monthDelta}
icon={CreditCard}
/>
<BentoStat
label='累计 token'
value={formatNumber(metrics.month.tokens)}
unit='本月'
sub='输入+输出'
icon={Layers}
/>
</>
);
};
export default BentoStats;

View File

@ -0,0 +1,329 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useMemo, useState, useRef, useEffect, useCallback } from 'react';
import { TrendingUp, RefreshCw, ChevronDown } from 'lucide-react';
import Sparkline from './Sparkline';
import { renderQuota, renderNumber } from '../../helpers';
const formatMD = (d) => {
const m = d.getMonth() + 1;
const day = d.getDate();
return `${m}/${day}`;
};
const ACCENT_COLORS = ['#10b981', '#3b82f6', '#8b5cf6', '#f59e0b'];
const DOT_COLOR = '#3b82f6';
const RANGE_OPTIONS = [
{ key: 'today', label: '今天' },
{ key: 'yesterday', label: '昨天' },
{ key: '24h', label: '近 24 小时' },
{ key: '7d', label: '近 7 天' },
{ key: '14d', label: '近 14 天' },
{ key: '30d', label: '近 30 天' },
{ key: 'thisMonth', label: '本月' },
{ key: 'lastMonth', label: '上月' },
];
const BentoTrend = ({
dailyStats = [],
topModels = [],
className = '',
onRefresh,
loading = false,
rangeKey = '7d',
onRangeChange,
}) => {
const [rangeOpen, setRangeOpen] = useState(false);
const [customStart, setCustomStart] = useState('');
const [customEnd, setCustomEnd] = useState('');
const [tooltip, setTooltip] = useState(null); // { dayIdx, x, y } or null
const rangeRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
const handler = (e) => {
if (rangeRef.current && !rangeRef.current.contains(e.target)) {
setRangeOpen(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
const selectedLabel = RANGE_OPTIONS.find((r) => r.key === rangeKey)?.label || '近 7 天';
const handleApply = () => {
if (customStart && customEnd) {
onRangeChange('custom');
}
setRangeOpen(false);
};
const handleHover = useCallback((idx, cx, cy) => {
if (idx < 0 || idx >= dailyStats.length) {
setTooltip(null);
return;
}
const rect = chartRef.current?.getBoundingClientRect();
setTooltip({
dayIdx: idx,
x: rect ? cx - rect.left : cx,
y: rect ? cy - rect.top - 120 : cy - 120,
});
}, [dailyStats.length]);
const counts = useMemo(() => dailyStats.map((d) => d.count), [dailyStats]);
const costs = useMemo(() => dailyStats.map((d) => d.cost), [dailyStats]);
const totalCalls = counts.reduce((s, v) => s + v, 0);
const totalCost = costs.reduce((s, v) => s + v, 0);
const avg = totalCalls > 0 ? (totalCalls / Math.max(counts.length, 1)).toFixed(1) : '0.0';
const peak = counts.length > 0 ? Math.max(...counts) : 0;
const topMax = topModels[0]?.count || 0;
const hoverDay = tooltip ? dailyStats[tooltip.dayIdx] : null;
const hoverModels = hoverDay ? Object.entries(hoverDay.models || {}).sort((a, b) => b[1] - a[1]) : [];
return (
<div
className={`relative flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20 ${className}`}
>
{/* Header */}
<div className='flex items-center justify-between gap-3'>
<div className='flex items-center gap-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
<TrendingUp size={18} />
</div>
<div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>Token 使用趋势</div>
<div className='mt-0.5 text-[11px] text-slate-400 tabular-nums dark:text-white/40'>
{selectedLabel}
<span className='font-semibold text-slate-900 dark:text-white'> {totalCalls}</span>
· 日均{' '}
<span className='font-semibold text-slate-900 dark:text-white'>{avg}</span>
</div>
</div>
</div>
<div className='hidden text-right md:block'>
<div className='text-[10px] uppercase tracking-wider text-slate-400 font-mono dark:text-white/30'>
花费
</div>
<div className='font-mono text-sm font-semibold tabular-nums text-emerald-600 dark:text-emerald-400'>
{totalCost > 0 ? renderQuota(totalCost) : '—'}
</div>
</div>
</div>
{/* Controls */}
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2' ref={rangeRef}>
<div className='relative'>
<button
onClick={() => setRangeOpen(!rangeOpen)}
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition-colors hover:border-slate-300 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:bg-white/5'
>
{selectedLabel}
<ChevronDown size={12} className='text-slate-400 dark:text-white/40' />
</button>
{rangeOpen && (
<div className='absolute left-0 top-full z-50 mt-1 w-64 rounded-xl border border-slate-200 bg-white p-3 shadow-lg dark:border-white/10 dark:bg-neutral-900'>
<div className='space-y-0.5'>
{RANGE_OPTIONS.map((opt) => (
<button
key={opt.key}
onClick={() => {
onRangeChange(opt.key);
setCustomStart('');
setCustomEnd('');
setRangeOpen(false);
}}
className={`w-full rounded-lg px-3 py-1.5 text-left text-xs transition-colors ${
rangeKey === opt.key
? 'bg-blue-50 font-medium text-blue-700 dark:bg-blue-500/10 dark:text-blue-400'
: 'text-slate-600 hover:bg-slate-50 dark:text-white/60 dark:hover:bg-white/5'
}`}
>
{opt.label}
</button>
))}
</div>
<div className='my-2 border-t border-slate-100 dark:border-white/5' />
<div className='space-y-2'>
<div className='flex items-center gap-2'>
<input
type='date'
value={customStart}
onChange={(e) => setCustomStart(e.target.value)}
className='flex-1 rounded-lg border border-slate-200 bg-white px-2 py-1 text-xs text-slate-700 outline-none focus:border-blue-400 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70'
placeholder='开始日期'
/>
<span className='text-xs text-slate-400'></span>
<input
type='date'
value={customEnd}
onChange={(e) => setCustomEnd(e.target.value)}
className='flex-1 rounded-lg border border-slate-200 bg-white px-2 py-1 text-xs text-slate-700 outline-none focus:border-blue-400 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70'
placeholder='结束日期'
/>
</div>
<button
onClick={handleApply}
disabled={!customStart || !customEnd}
className='w-full rounded-lg bg-blue-600 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-600'
>
应用
</button>
</div>
</div>
)}
</div>
<button
onClick={onRefresh}
disabled={loading}
className='flex h-7 w-7 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-colors hover:border-slate-300 hover:bg-slate-50 hover:text-slate-700 disabled:opacity-50 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/60 dark:hover:bg-white/5 dark:hover:text-white'
title='刷新'
>
<RefreshCw size={12} className={loading ? 'animate-spin' : ''} />
</button>
</div>
{/* Peak / total indicators */}
<div className='hidden items-center gap-3 sm:flex'>
<div className='flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-white/40'>
<span className='inline-block h-1.5 w-1.5 rounded-full bg-blue-500' />
峰值 <span className='font-semibold text-slate-700 dark:text-white/80'>{peak}</span>
</div>
<div className='flex items-center gap-1.5 text-[11px] text-slate-400 dark:text-white/40'>
合计 <span className='font-semibold text-slate-700 dark:text-white/80'>{totalCalls}</span>
</div>
</div>
</div>
{/* Chart area */}
<div ref={chartRef} className='relative flex min-h-[80px] flex-1 items-end text-blue-500'>
<Sparkline data={counts} height={80} stroke={DOT_COLOR} onHover={handleHover} />
{/* Tooltip */}
{tooltip && hoverDay && (
<div
className='pointer-events-none absolute z-50 min-w-[180px] rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-lg dark:border-white/10 dark:bg-neutral-900'
style={{ left: tooltip.x, top: tooltip.y, transform: 'translate(-50%, -100%)' }}
>
<div className='mb-1 text-xs font-semibold text-slate-900 dark:text-white'>
{hoverDay.date.getMonth() + 1}{hoverDay.date.getDate()}
</div>
<div className='mb-2 flex items-center gap-3 text-[11px] text-slate-500 dark:text-white/50'>
<span>调用 <b className='text-slate-900 dark:text-white'>{hoverDay.count}</b> </span>
<span>花费 <b className='text-slate-900 dark:text-white'>{renderQuota(hoverDay.cost)}</b></span>
</div>
{hoverModels.length > 0 && (
<>
<div className='mb-1.5 h-px bg-slate-100 dark:bg-white/5' />
<div className='space-y-1'>
{hoverModels.slice(0, 5).map(([name, count], i) => (
<div key={name} className='flex items-center gap-2 text-[11px]'>
<span
className='inline-block h-1.5 w-1.5 rounded-full shrink-0'
style={{ backgroundColor: ACCENT_COLORS[i % ACCENT_COLORS.length] }}
/>
<span className='min-w-0 flex-1 truncate text-slate-600 dark:text-white/70'>
{name}
</span>
<span className='shrink-0 font-mono tabular-nums text-slate-400 dark:text-white/40'>
{count}
</span>
</div>
))}
</div>
</>
)}
</div>
)}
</div>
{/* Date labels */}
<div className='flex items-center justify-between text-[11px]'>
<div className='flex w-full items-center justify-between tabular-nums'>
{dailyStats.map((d, i) => {
const isHovered = tooltip?.dayIdx === i;
return (
<span
key={i}
className={`text-center text-xs transition-colors ${
isHovered
? 'font-semibold text-blue-600 dark:text-blue-400'
: 'text-slate-500 dark:text-white/50'
}`}
>
{formatMD(d.date)}
</span>
);
})}
</div>
</div>
{/* Top 5 Models */}
{topModels.length > 0 && (
<div className='border-t border-slate-100 pt-4 dark:border-white/5'>
<div className='mb-2 flex items-center justify-between text-[11px] uppercase tracking-wider text-slate-400 dark:text-white/40'>
<span>Top 5 模型</span>
<span className='font-mono normal-case tracking-normal'> 30 </span>
</div>
<ul className='space-y-1.5'>
{topModels.map((m, i) => {
const pct = topMax > 0 ? (m.count / topMax) * 100 : 0;
const color = ACCENT_COLORS[i % ACCENT_COLORS.length];
return (
<li
key={m.name + i}
className='flex items-center gap-3 text-xs'
title={m.name}
>
<span className='w-3 shrink-0 text-right font-mono text-[10px] tabular-nums' style={{ color }}>
{i + 1}
</span>
<span className='min-w-0 flex-1 truncate text-slate-700 dark:text-white/80'>
{m.name}
</span>
<div className='hidden h-1 w-24 overflow-hidden rounded-full bg-slate-100 sm:block dark:bg-white/5'>
<div
className='h-full rounded-full'
style={{ width: `${pct}%`, backgroundColor: color }}
/>
</div>
<span className='w-12 shrink-0 text-right font-mono tabular-nums text-slate-500 dark:text-white/60'>
{m.count}
</span>
</li>
);
})}
</ul>
</div>
)}
</div>
);
};
export default BentoTrend;

View File

@ -0,0 +1,56 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useMemo } from 'react';
const getGreeting = () => {
const h = new Date().getHours();
if (h < 5) return '深夜好';
if (h < 12) return '早上好';
if (h < 14) return '中午好';
if (h < 18) return '下午好';
return '晚上好';
};
const Hero = ({ user }) => {
const dateStr = useMemo(() => {
const now = new Date();
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}日 · ${weekdays[now.getDay()]}`;
}, []);
return (
<div className='flex flex-wrap items-center justify-between gap-4'>
<div className='min-w-0'>
<div className='mb-1.5 flex items-center gap-2'>
<span className='flex h-1.5 w-1.5 rounded-full bg-emerald-500' />
<span className='text-xs text-slate-500 dark:text-white/50'>{dateStr}</span>
</div>
<h1 className='text-2xl font-semibold leading-tight tracking-tight md:text-3xl'>
<span className='text-slate-500 dark:text-white/60'>{getGreeting()}</span>
<span className='text-slate-900 dark:text-white'>
{user?.username || '访客'}
</span>
</h1>
</div>
</div>
);
};
export default Hero;

View File

@ -0,0 +1,130 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Receipt, TrendingUp, TrendingDown, Minus } from 'lucide-react';
import { renderQuota } from '../../helpers';
const SubStat = ({ label, value, hint }) => (
<div className='min-w-0'>
<div className='text-[11px] uppercase tracking-wider text-slate-400 dark:text-white/40'>
{label}
</div>
<div className='mt-1 font-mono text-base font-semibold tabular-nums text-slate-900 dark:text-white'>
{value}
</div>
{hint && (
<div className='mt-0.5 text-[11px] text-slate-400 tabular-nums dark:text-white/40'>
{hint}
</div>
)}
</div>
);
const MonthlyForecast = ({ forecast = {}, className = '' }) => {
const {
spent = 0,
balance = 0,
dailyAvg = 0,
daysInMonth = 0,
daysElapsed = 0,
projected = 0,
monthDelta = null,
} = forecast;
const daysRemaining = Math.max(0, daysInMonth - daysElapsed);
const hasData = spent > 0;
const overBudget = hasData && balance > 0 && projected > balance;
const DeltaIcon =
monthDelta == null ? Minus : monthDelta > 0 ? TrendingUp : TrendingDown;
const deltaTone =
monthDelta == null
? 'text-slate-500 dark:text-white/50'
: monthDelta > 5
? 'text-red-500 dark:text-red-400'
: monthDelta < -5
? 'text-emerald-600 dark:text-emerald-400'
: 'text-slate-500 dark:text-white/50';
return (
<div
className={`relative flex flex-col gap-5 rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20 ${className}`}
>
<div className='flex items-center gap-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70'>
<Receipt size={18} />
</div>
<div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>
本月账单预测
</div>
<div className='mt-0.5 text-[11px] text-slate-400 tabular-nums dark:text-white/40'>
已过 {daysElapsed} / {daysInMonth} · {daysRemaining}
</div>
</div>
</div>
<div className='border-t border-slate-100 pt-4 dark:border-white/5'>
<div className='text-[11px] uppercase tracking-wider text-slate-400 dark:text-white/40'>
月底预计
</div>
<div
className={`mt-1.5 font-mono text-3xl font-bold tabular-nums leading-none md:text-4xl ${
overBudget
? 'text-red-500 dark:text-red-400'
: 'text-slate-900 dark:text-white'
}`}
>
{hasData ? renderQuota(projected) : '—'}
</div>
{monthDelta != null && (
<div className={`mt-2 inline-flex items-center gap-1 text-xs tabular-nums ${deltaTone}`}>
<DeltaIcon size={12} />
<span className='font-semibold'>
{monthDelta > 0 ? '+' : ''}
{monthDelta.toFixed(0)}%
</span>
<span className='text-slate-400 dark:text-white/40'>较上月</span>
</div>
)}
</div>
<div className='grid grid-cols-3 gap-4 border-t border-slate-100 pt-4 dark:border-white/5'>
<SubStat
label='已花费'
value={hasData ? renderQuota(spent) : '—'}
hint={`${daysElapsed} 天累计`}
/>
<SubStat
label='日均'
value={hasData ? renderQuota(dailyAvg) : '—'}
hint='本月至今日'
/>
<SubStat
label='账户余额'
value={balance > 0 ? renderQuota(balance) : '—'}
hint={overBudget ? '预计不足' : '可继续使用'}
/>
</div>
</div>
);
};
export default MonthlyForecast;

View File

@ -0,0 +1,137 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useMemo } from 'react';
import { ArrowUpRight, Clock, Inbox } from 'lucide-react';
import { renderQuota, renderNumber, timestamp2string } from '../../helpers';
const isSuccess = (l) =>
l?.type === 0 || l?.type === 1 || l?.type === 2 || l?.status === 0 || l?.type === 'success' || l?.type === undefined;
const RecentActivity = ({ logs = [], className = '' }) => {
const recent = useMemo(() => logs.slice(0, 10), [logs]);
return (
<div
className={`relative overflow-hidden rounded-2xl border border-slate-200 bg-white transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20 ${className}`}
>
<div className='flex items-center justify-between p-6 pb-4'>
<div className='flex items-center gap-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70'>
<Clock size={18} />
</div>
<div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>最近调用</div>
<div className='mt-0.5 text-[11px] text-slate-400 tabular-nums dark:text-white/40'>
{' '}
<span className='font-semibold text-slate-900 dark:text-white'>
{recent.length}
</span>{' '}
条记录
</div>
</div>
</div>
<a
href='/console/log'
className='flex items-center gap-1 text-xs text-slate-500 transition-colors hover:text-slate-900 dark:text-white/60 dark:hover:text-white'
>
查看全部 <ArrowUpRight size={11} />
</a>
</div>
{recent.length === 0 ? (
<div className='flex flex-col items-center justify-center px-6 py-14'>
<div className='mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-slate-100 dark:bg-white/5'>
<Inbox size={28} className='text-slate-400 dark:text-white/30' />
</div>
<h3 className='text-sm font-semibold text-slate-900 dark:text-white'>暂无使用记录</h3>
<p className='mt-1 text-xs text-slate-500 dark:text-white/40'>
开始使用 API 您的使用历史将显示在这里
</p>
</div>
) : (
<div className='overflow-hidden border-t border-slate-100 dark:border-white/5'>
<table className='w-full text-sm'>
<thead>
<tr className='border-b border-slate-100 bg-slate-50/50 text-slate-500 dark:border-white/5 dark:bg-white/[0.02] dark:text-white/40'>
<th className='w-[140px] px-6 py-2.5 text-left font-mono text-[10px] font-normal uppercase tracking-wider'>
time
</th>
<th className='px-2 py-2.5 text-left font-mono text-[10px] font-normal uppercase tracking-wider'>
model
</th>
<th className='w-[120px] px-2 py-2.5 text-right font-mono text-[10px] font-normal uppercase tracking-wider'>
tokens
</th>
<th className='w-[120px] px-2 py-2.5 text-right font-mono text-[10px] font-normal uppercase tracking-wider'>
cost
</th>
<th className='w-[120px] px-6 py-2.5 text-right font-mono text-[10px] font-normal uppercase tracking-wider'>
status
</th>
</tr>
</thead>
<tbody>
{recent.map((l) => {
const ok = isSuccess(l);
return (
<tr
key={l.id}
className='border-b border-slate-100 transition-colors last:border-b-0 hover:bg-slate-50/50 dark:border-white/5 dark:hover:bg-white/[0.02]'
>
<td className='whitespace-nowrap px-6 py-3 font-mono text-xs tabular-nums text-slate-500 dark:text-white/50'>
{timestamp2string(l.created_at).slice(5, 16)}
</td>
<td className='max-w-[300px] truncate px-2 py-3 text-slate-700 dark:text-white/90'>
{l.model_name || '—'}
</td>
<td className='px-2 py-3 text-right font-mono text-xs tabular-nums text-slate-500 dark:text-white/70'>
{renderNumber(l.token_used || 0)}
</td>
<td className='px-2 py-3 text-right font-mono text-xs tabular-nums text-slate-500 dark:text-white/70'>
{renderQuota(l.quota || 0)}
</td>
<td className='whitespace-nowrap px-6 py-3 text-right'>
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 ${
ok
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400'
: 'bg-red-50 text-red-600 dark:bg-red-500/10 dark:text-red-400'
}`}
>
<span
className={`h-1.5 w-1.5 rounded-full ${
ok ? 'bg-emerald-500' : 'bg-red-500'
}`}
/>
<span className='text-xs font-medium'>{ok ? '成功' : '失败'}</span>
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
};
export default RecentActivity;

View File

@ -0,0 +1,96 @@
import React, { useState, useCallback } from 'react';
const Sparkline = ({ data = [], height = 80, stroke = 'currentColor', onHover }) => {
const [hoverIdx, setHoverIdx] = useState(null);
if (!data || data.length === 0) return null;
const max = Math.max(...data, 1);
const n = data.length;
const w = 400;
const xs = data.map((_, i) => (n > 1 ? (i / (n - 1)) * w : w / 2));
const ys = data.map((v) => height - (v / max) * (height - 8) - 4);
const points = xs.map((x, i) => `${x.toFixed(1)},${ys[i].toFixed(1)}`).join(' ');
const areaPoints = `0,${height} ${points} ${w},${height}`;
const handleMouseMove = useCallback((e) => {
const svg = e.currentTarget;
const rect = svg.getBoundingClientRect();
const mx = ((e.clientX - rect.left) / rect.width) * w;
const dxs = xs.map((x) => Math.abs(x - mx));
const idx = dxs.indexOf(Math.min(...dxs));
setHoverIdx(idx);
onHover?.(idx, e.clientX, e.clientY);
}, [xs, w, onHover]);
const handleMouseLeave = useCallback(() => {
setHoverIdx(null);
onHover?.(-1);
}, [onHover]);
return (
<svg
width='100%'
height={height}
viewBox={`0 0 ${w} ${height}`}
preserveAspectRatio='none'
className='cursor-crosshair'
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<defs>
<linearGradient id='sg' x1='0%' y1='0%' x2='0%' y2='100%'>
<stop offset='0%' stopColor={stroke} stopOpacity='0.12' />
<stop offset='100%' stopColor={stroke} stopOpacity='0' />
</linearGradient>
</defs>
{/* hover vertical line */}
{hoverIdx != null && (
<line
x1={xs[hoverIdx].toFixed(1)}
y1={0}
x2={xs[hoverIdx].toFixed(1)}
y2={height}
stroke={stroke}
strokeWidth='1'
strokeDasharray='3 3'
opacity='0.3'
vectorEffect='non-scaling-stroke'
/>
)}
<polygon points={areaPoints} fill='url(#sg)' />
<polyline
points={points}
fill='none'
stroke={stroke}
strokeWidth='2.5'
strokeLinecap='round'
strokeLinejoin='round'
vectorEffect='non-scaling-stroke'
/>
{data.map((v, i) => {
const active = hoverIdx === i;
return (
<circle
key={i}
cx={xs[i].toFixed(1)}
cy={ys[i].toFixed(1)}
r={active ? 5 : 3}
fill={active ? stroke : 'white'}
stroke={stroke}
strokeWidth={active ? 3 : 2}
vectorEffect='non-scaling-stroke'
style={{ transition: 'r 0.15s, fill 0.15s' }}
/>
);
})}
</svg>
);
};
export default Sparkline;

View File

@ -0,0 +1,75 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Search, RefreshCw, Wallet } from 'lucide-react';
import { renderQuota, getSystemName } from '../../helpers';
const TopBar = ({ onSearch, onRefresh, loading, user, unreadCount = 0 }) => {
const initial = (user?.username || '?').charAt(0).toUpperCase();
const balance = Number(user?.quota || 0);
return (
<div className='flex items-center justify-between border-b border-slate-200 py-5 dark:border-white/10'>
<div className='flex items-center gap-3'>
<span className='text-base font-semibold tracking-tight text-slate-900 dark:text-white'>
{getSystemName()}
</span>
</div>
<div className='flex items-center gap-2'>
<button
onClick={onSearch}
className='flex items-center gap-2 rounded-full border border-slate-200 bg-white px-4 py-1.5 text-xs text-slate-500 transition-colors hover:border-slate-300 hover:bg-slate-50 hover:text-slate-700 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/60 dark:hover:bg-white/5 dark:hover:text-white'
title='搜索'
>
<Search size={13} className='text-slate-400 dark:text-white/40' />
<span>搜索</span>
</button>
<button
onClick={onRefresh}
disabled={loading}
className='flex h-8 w-8 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-colors hover:border-slate-300 hover:bg-slate-50 hover:text-slate-700 disabled:opacity-50 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/60 dark:hover:bg-white/5 dark:hover:text-white'
title='刷新'
>
<RefreshCw size={13} className={loading ? 'animate-spin' : ''} />
</button>
<a
href='/console/topup'
className='hidden items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-sm transition-colors hover:border-slate-300 hover:bg-slate-50 sm:inline-flex dark:border-white/10 dark:bg-white/[0.02] dark:hover:bg-white/5'
title='账户余额'
>
<Wallet size={13} className='text-slate-400 dark:text-white/40' />
<span className='font-mono font-semibold tabular-nums text-emerald-600 dark:text-emerald-400'>
{renderQuota(balance)}
</span>
</a>
<div className='relative ml-1 flex h-9 w-9 items-center justify-center rounded-full border border-slate-200 bg-slate-100 text-xs font-semibold text-slate-700 dark:border-white/10 dark:bg-white/5 dark:text-white/80'>
{initial}
{unreadCount > 0 && (
<span className='absolute -right-0.5 -top-0.5 flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-red-500 px-1 text-[9px] font-mono text-white'>
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</div>
</div>
</div>
);
};
export default TopBar;

View File

@ -17,258 +17,85 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useContext, useEffect } from 'react'; import React, { useContext, useState } from 'react';
import { getRelativeTime, userIsDistributorUser } from '../../helpers';
import { UserContext } from '../../context/User'; import { UserContext } from '../../context/User';
import { StatusContext } from '../../context/Status'; import { useUserMessageUnreadCount } from '../../hooks/common/useUserMessageUnreadCount';
import DashboardHeader from './DashboardHeader';
import StatsCards from './StatsCards';
import ChartsPanel from './ChartsPanel';
import ApiInfoPanel from './ApiInfoPanel';
import AnnouncementsPanel from './AnnouncementsPanel';
import FaqPanel from './FaqPanel';
import UptimePanel from './UptimePanel';
import SearchModal from './modals/SearchModal';
import DistributorAnalyticsBoard from '../distributor/DistributorAnalyticsBoard';
import { useDashboardData } from '../../hooks/dashboard/useDashboardData'; import { useDashboardData } from '../../hooks/dashboard/useDashboardData';
import { useDashboardStats } from '../../hooks/dashboard/useDashboardStats'; import { getSystemName } from '../../helpers';
import { useDashboardCharts } from '../../hooks/dashboard/useDashboardCharts';
import { import TopBar from './TopBar';
CHART_CONFIG, import Hero from './Hero';
CARD_PROPS, import BentoStats from './BentoStats';
FLEX_CENTER_GAP2, import BentoTrend from './BentoTrend';
ILLUSTRATION_SIZE, import MonthlyForecast from './MonthlyForecast';
ANNOUNCEMENT_LEGEND_DATA, import RecentActivity from './RecentActivity';
UPTIME_STATUS_MAP,
} from '../../constants/dashboard.constants'; const RANGE_DAYS = {
import { today: 1,
getTrendSpec, yesterday: 1,
handleCopyUrl, '24h': 1,
handleSpeedTest, '7d': 7,
getUptimeStatusColor, '14d': 14,
getUptimeStatusText, '30d': 30,
renderMonitorList, };
} from '../../helpers/dashboard';
const Dashboard = () => { const Dashboard = () => {
// ========== Context ========== const [userState] = useContext(UserContext);
const [userState, userDispatch] = useContext(UserContext); const user = userState?.user;
const [statusState, statusDispatch] = useContext(StatusContext); const [rangeKey, setRangeKey] = useState('7d');
// ========== ========== const rangeDays = RANGE_DAYS[rangeKey] || 7;
const dashboardData = useDashboardData(userState, userDispatch, statusState);
// ========== ========== const dashboard = useDashboardData(userState, rangeDays);
const dashboardCharts = useDashboardCharts( const { unreadCount } = useUserMessageUnreadCount(user);
dashboardData.dataExportDefaultTime,
dashboardData.setTrendData,
dashboardData.setConsumeQuota,
dashboardData.setTimes,
dashboardData.setConsumeTokens,
dashboardData.setPieData,
dashboardData.setLineData,
dashboardData.setModelColors,
dashboardData.t,
);
// ========== ==========
const { groupedStatsData } = useDashboardStats(
userState,
dashboardData.consumeQuota,
dashboardData.consumeTokens,
dashboardData.times,
dashboardData.trendData,
dashboardData.performanceMetrics,
dashboardData.navigate,
dashboardData.t,
);
// ========== ==========
const initChart = async () => {
await dashboardData.loadQuotaData().then((data) => {
if (data && data.length > 0) {
dashboardCharts.updateChartData(data);
}
});
await dashboardData.loadUptimeData();
};
const handleRefresh = async () => {
const data = await dashboardData.refresh();
if (data && data.length > 0) {
dashboardCharts.updateChartData(data);
}
};
const handleSearchConfirm = async () => {
await dashboardData.handleSearchConfirm(dashboardCharts.updateChartData);
};
// ========== ==========
const apiInfoData = statusState?.status?.api_info || [];
const announcementData = (statusState?.status?.announcements || []).map(
(item) => {
const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
const absoluteTime =
pubDate && !isNaN(pubDate.getTime())
? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
: item?.publishDate || '';
const relativeTime = getRelativeTime(item.publishDate);
return {
...item,
time: absoluteTime,
relative: relativeTime,
};
},
);
const faqData = statusState?.status?.faq || [];
const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(
([status, info]) => ({
status: Number(status),
color: info.color,
label: dashboardData.t(info.label),
}),
);
// ========== Effects ==========
useEffect(() => {
initChart();
}, []);
return ( return (
<div className='h-full flex flex-col gap-6'> <div className='flex flex-col gap-6 pb-8'>
<DashboardHeader <TopBar
getGreeting={dashboardData.getGreeting} onSearch={() => {}}
greetingVisible={dashboardData.greetingVisible} onRefresh={dashboard.refresh}
showSearchModal={dashboardData.showSearchModal} loading={dashboard.loading}
refresh={handleRefresh} user={user}
loading={dashboardData.loading} unreadCount={unreadCount}
t={dashboardData.t} />
/>
<SearchModal <div className='space-y-6 pb-8'>
searchModalVisible={dashboardData.searchModalVisible} <Hero user={user} />
handleSearchConfirm={handleSearchConfirm}
handleCloseModal={dashboardData.handleCloseModal}
isMobile={dashboardData.isMobile}
isAdminUser={dashboardData.isAdminUser}
inputs={dashboardData.inputs}
dataExportDefaultTime={dashboardData.dataExportDefaultTime}
timeOptions={dashboardData.timeOptions}
handleInputChange={dashboardData.handleInputChange}
t={dashboardData.t}
/>
<StatsCards {/* 第一行5 个核心指标卡 */}
groupedStatsData={groupedStatsData} <div className='grid grid-cols-2 gap-5 md:grid-cols-3 xl:grid-cols-5'>
loading={dashboardData.loading} <BentoStats
getTrendSpec={getTrendSpec} metrics={dashboard.metrics}
CARD_PROPS={CARD_PROPS} monthDelta={dashboard.monthDelta}
CHART_CONFIG={CHART_CONFIG}
/>
{userIsDistributorUser(userState?.user) ? (
<DistributorAnalyticsBoard />
) : null}
{/* API信息和图表面板 */}
<div>
<div
className={`grid grid-cols-1 gap-6 ${dashboardData.hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}
>
<ChartsPanel
activeChartTab={dashboardData.activeChartTab}
setActiveChartTab={dashboardData.setActiveChartTab}
spec_line={dashboardCharts.spec_line}
spec_model_line={dashboardCharts.spec_model_line}
spec_pie={dashboardCharts.spec_pie}
spec_rank_bar={dashboardCharts.spec_rank_bar}
CARD_PROPS={CARD_PROPS}
CHART_CONFIG={CHART_CONFIG}
FLEX_CENTER_GAP2={FLEX_CENTER_GAP2}
hasApiInfoPanel={dashboardData.hasApiInfoPanel}
t={dashboardData.t}
/>
{dashboardData.hasApiInfoPanel && (
<ApiInfoPanel
apiInfoData={apiInfoData}
handleCopyUrl={(url) => handleCopyUrl(url, dashboardData.t)}
handleSpeedTest={handleSpeedTest}
CARD_PROPS={CARD_PROPS}
FLEX_CENTER_GAP2={FLEX_CENTER_GAP2}
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
t={dashboardData.t}
/> />
)}
</div>
</div>
{/* 系统公告和常见问答卡片 */}
{dashboardData.hasInfoPanels && (
<div>
<div className='grid grid-cols-1 lg:grid-cols-4 gap-6'>
{/* 公告卡片 */}
{dashboardData.announcementsEnabled && (
<AnnouncementsPanel
announcementData={announcementData}
announcementLegendData={ANNOUNCEMENT_LEGEND_DATA.map(
(item) => ({
...item,
label: dashboardData.t(item.label),
}),
)}
CARD_PROPS={CARD_PROPS}
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
t={dashboardData.t}
/>
)}
{/* 常见问答卡片 */}
{dashboardData.faqEnabled && (
<FaqPanel
faqData={faqData}
CARD_PROPS={CARD_PROPS}
FLEX_CENTER_GAP2={FLEX_CENTER_GAP2}
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
t={dashboardData.t}
/>
)}
{/* 大模型部署定制服务 / 可用性监控卡片 */}
{dashboardData.uptimeEnabled && (
<UptimePanel
uptimeData={dashboardData.uptimeData}
uptimeLoading={dashboardData.uptimeLoading}
activeUptimeTab={dashboardData.activeUptimeTab}
setActiveUptimeTab={dashboardData.setActiveUptimeTab}
loadUptimeData={dashboardData.loadUptimeData}
uptimeLegendData={uptimeLegendData}
renderMonitorList={(monitors) =>
renderMonitorList(
monitors,
(status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP),
(status) =>
getUptimeStatusText(
status,
UPTIME_STATUS_MAP,
dashboardData.t,
),
dashboardData.t,
)
}
CARD_PROPS={CARD_PROPS}
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
t={dashboardData.t}
/>
)}
</div> </div>
{/* 第二行:七日用量趋势 + 本月账单预测 */}
<div className='grid grid-cols-1 gap-5 md:grid-cols-4'>
<BentoTrend
dailyStats={dashboard.dailyStats}
topModels={dashboard.topModels}
onRefresh={dashboard.refresh}
loading={dashboard.loading}
rangeKey={rangeKey}
onRangeChange={setRangeKey}
className='md:col-span-2'
/>
<MonthlyForecast
forecast={dashboard.monthlyForecast}
className='md:col-span-2'
/>
</div>
{/* 第三行:最近调用 */}
<RecentActivity
logs={dashboard.recentLogs}
/>
</div>
<div className='flex items-center justify-center border-t border-slate-200 py-6 text-xs text-slate-400 dark:border-white/10 dark:text-white/30'>
<span>{getSystemName()} · 2026</span>
</div> </div>
)}
</div> </div>
); );
}; };

View File

@ -378,7 +378,6 @@ const HomeModelList = () => {
<PricingCardView <PricingCardView
filteredModels={pricingData.filteredModels} filteredModels={pricingData.filteredModels}
loading={pricingData.loading} loading={pricingData.loading}
rowSelection={null}
pageSize={pricingData.pageSize} pageSize={pricingData.pageSize}
setPageSize={pricingData.setPageSize} setPageSize={pricingData.setPageSize}
currentPage={pricingData.currentPage} currentPage={pricingData.currentPage}
@ -387,9 +386,6 @@ const HomeModelList = () => {
groupRatio={pricingData.groupRatio} groupRatio={pricingData.groupRatio}
groupModelPrice={pricingData.groupModelPrice} groupModelPrice={pricingData.groupModelPrice}
groupModelRatio={pricingData.groupModelRatio} groupModelRatio={pricingData.groupModelRatio}
copyText={pricingData.copyText}
setModalImageUrl={pricingData.setModalImageUrl}
setIsModalOpenurl={pricingData.setIsModalOpenurl}
currency={pricingData.currency} currency={pricingData.currency}
siteDisplayType={pricingData.siteDisplayType} siteDisplayType={pricingData.siteDisplayType}
tokenUnit={pricingData.tokenUnit} tokenUnit={pricingData.tokenUnit}
@ -399,10 +395,7 @@ const HomeModelList = () => {
pricingData.channelVideoCompletionRatio pricingData.channelVideoCompletionRatio
} }
channelVideoPrice={pricingData.channelVideoPrice} channelVideoPrice={pricingData.channelVideoPrice}
showRatio={false}
t={pricingData.t} t={pricingData.t}
selectedRowKeys={[]}
setSelectedRowKeys={() => {}}
openModelDetail={pricingData.openModelDetail} openModelDetail={pricingData.openModelDetail}
showSizeChanger={false} showSizeChanger={false}
blurPricing={blurPricing} blurPricing={blurPricing}

View File

@ -23,6 +23,7 @@ import { IconClose } from '@douyinfe/semi-icons';
import SiderBar from './SiderBar'; import SiderBar from './SiderBar';
import App from '../../App'; import App from '../../App';
import FooterBar from './Footer'; import FooterBar from './Footer';
import OnboardingWizard from '../onboarding/OnboardingWizard';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useIsMobile } from '../../hooks/common/useIsMobile'; import { useIsMobile } from '../../hooks/common/useIsMobile';
@ -322,22 +323,24 @@ const PageLayout = () => {
</div> </div>
</div> </div>
) : null} ) : null}
<Header {location.pathname !== '/' && (
style={{ <Header
padding: 0, style={{
height: 'auto', padding: 0,
lineHeight: 'normal', height: 'auto',
position: 'fixed', lineHeight: 'normal',
width: '100%', position: 'fixed',
top: 0, width: '100%',
zIndex: 100, top: 0,
}} zIndex: 100,
> }}
<HeaderBar >
onMobileMenuToggle={() => setDrawerOpen((prev) => !prev)} <HeaderBar
drawerOpen={drawerOpen} onMobileMenuToggle={() => setDrawerOpen((prev) => !prev)}
/> drawerOpen={drawerOpen}
</Header> />
</Header>
)}
<Layout <Layout
style={{ style={{
overflow: isMobile ? 'visible' : 'auto', overflow: isMobile ? 'visible' : 'auto',
@ -400,6 +403,7 @@ const PageLayout = () => {
)} )}
</Layout> </Layout>
</Layout> </Layout>
<OnboardingWizard />
<ToastContainer /> <ToastContainer />
</Layout> </Layout>
); );

View File

@ -51,6 +51,8 @@ const routerMap = {
deployment: '/console/deployment', deployment: '/console/deployment',
'model-heat': '/console/model-heat', 'model-heat': '/console/model-heat',
playground: '/console/playground', playground: '/console/playground',
benchmarks: '/benchmarks',
'api-tester': '/console/api-tester',
personal: '/console/personal', personal: '/console/personal',
supplier: null, supplier: null,
distributor: '/console/distributor/admin', distributor: '/console/distributor/admin',
@ -84,6 +86,16 @@ const SiderBar = ({ onNavigate = () => {} }) => {
const workspaceItems = useMemo(() => { const workspaceItems = useMemo(() => {
const items = [ const items = [
{
text: t('评测'),
itemKey: 'benchmarks',
to: '/benchmarks',
},
{
text: t('API 测速'),
itemKey: 'api-tester',
to: '/api-tester',
},
{ {
text: t('数据看板'), text: t('数据看板'),
itemKey: 'detail', itemKey: 'detail',
@ -94,7 +106,7 @@ const SiderBar = ({ onNavigate = () => {} }) => {
: 'tableHiddle', : 'tableHiddle',
}, },
{ {
text: t('令牌管理'), text: t('API Key'),
itemKey: 'token', itemKey: 'token',
to: '/token', to: '/token',
}, },
@ -195,6 +207,12 @@ const SiderBar = ({ onNavigate = () => {} }) => {
const adminItems = useMemo(() => { const adminItems = useMemo(() => {
const items = [ const items = [
{
text: t('操作记录'),
itemKey: 'operation-log',
to: '/console/operation-log',
className: isAdmin() ? '' : 'tableHiddle',
},
{ {
text: t('渠道管理'), text: t('渠道管理'),
itemKey: 'channel', itemKey: 'channel',
@ -237,12 +255,6 @@ const SiderBar = ({ onNavigate = () => {} }) => {
to: '/user', to: '/user',
className: isAdmin() ? '' : 'tableHiddle', className: isAdmin() ? '' : 'tableHiddle',
}, },
{
text: t('操作记录'),
itemKey: 'operation-log',
to: '/console/operation-log',
className: isAdmin() ? '' : 'tableHiddle',
},
{ {
text: t('代理管理'), text: t('代理管理'),
itemKey: 'distributor', itemKey: 'distributor',
@ -303,7 +315,7 @@ const SiderBar = ({ onNavigate = () => {} }) => {
const chatMenuItems = useMemo(() => { const chatMenuItems = useMemo(() => {
const items = [ const items = [
{ {
text: t('操练场'), text: t('体验'),
itemKey: 'playground', itemKey: 'playground',
to: '/playground', to: '/playground',
}, },

View File

@ -1,4 +1,4 @@
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -18,7 +18,6 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from 'react';
import NewYearButton from './NewYearButton';
import NotificationButton from './NotificationButton'; import NotificationButton from './NotificationButton';
import ThemeToggle from './ThemeToggle'; import ThemeToggle from './ThemeToggle';
import LanguageSelector from './LanguageSelector'; import LanguageSelector from './LanguageSelector';
@ -36,6 +35,9 @@ const ActionButtons = ({
isLoading, isLoading,
isMobile, isMobile,
isSelfUseMode, isSelfUseMode,
showThemeToggle = true,
//
showLanguageSelector = false,
logout, logout,
navigate, navigate,
t, t,
@ -43,8 +45,7 @@ const ActionButtons = ({
const shouldShowNoticeButton = Boolean(userState?.user?.id); const shouldShowNoticeButton = Boolean(userState?.user?.id);
return ( return (
<div className='flex items-center gap-2 md:gap-3'> <div className="flex items-center gap-1 md:gap-1.5">
{/* <NewYearButton isNewYear={isNewYear} /> */}
{shouldShowNoticeButton && ( {shouldShowNoticeButton && (
<NotificationButton <NotificationButton
unreadCount={unreadCount} unreadCount={unreadCount}
@ -53,12 +54,16 @@ const ActionButtons = ({
/> />
)} )}
<ThemeToggle theme={theme} onThemeToggle={onThemeToggle} t={t} /> {showThemeToggle && (
<ThemeToggle theme={theme} onThemeToggle={onThemeToggle} t={t} />
)}
<LanguageSelector {showLanguageSelector && (
currentLang={currentLang} <LanguageSelector
onLanguageChange={onLanguageChange} currentLang={currentLang}
/> onLanguageChange={onLanguageChange}
/>
)}
<UserArea <UserArea
userState={userState} userState={userState}

View File

@ -1,4 +1,4 @@
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -17,23 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useCallback, useContext } from 'react'; import React from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Typography, Tag } from '@douyinfe/semi-ui'; import { Tag } from '@douyinfe/semi-ui';
import SkeletonWrapper from '../components/SkeletonWrapper'; import SkeletonWrapper from '../components/SkeletonWrapper';
import { userIsSupplierUser, showInfo, isAdmin } from '../../../helpers';
import { StatusContext } from '../../../context/Status';
/**
* 顶栏申请入口浅色用 #409EFF
* 注意tailwind.config 覆盖了默认 colors blue/zinc 等键暗色必须用任意值或 semi-color-*否则 dark: 不会产出 CSS
*/
const APPLY_BTN_BASE =
'flex-shrink-0 inline-flex items-center justify-center text-sm font-semibold transition-all duration-200 ease-in-out rounded-lg px-3.5 py-2 min-h-[2.25rem] border shadow-sm dark:shadow-none';
const APPLY_BTN_IDLE =
'border-[#b3d8ff] bg-[#ecf5ff] text-[#409EFF] hover:bg-[#d9ecff] hover:border-[#409EFF] active:bg-[#c6e2ff] dark:border-[rgba(96,165,250,0.35)] dark:bg-[rgba(59,130,246,0.1)] dark:text-[rgba(147,197,253,0.92)] dark:hover:bg-[rgba(59,130,246,0.15)] dark:hover:border-[rgba(96,165,250,0.48)] dark:active:bg-[rgba(59,130,246,0.12)]';
const APPLY_BTN_ACTIVE =
'border-[#409EFF] bg-[#d9ecff] text-[#337ecc] shadow-md dark:border-[rgba(96,165,250,0.55)] dark:bg-[rgba(59,130,246,0.18)] dark:text-[rgba(191,219,254,0.95)] dark:shadow-none dark:ring-1 dark:ring-[rgba(96,165,250,0.28)]';
const HeaderLogo = ({ const HeaderLogo = ({
isMobile, isMobile,
@ -44,93 +31,43 @@ const HeaderLogo = ({
systemName, systemName,
isSelfUseMode, isSelfUseMode,
isDemoSiteMode, isDemoSiteMode,
userState,
t, t,
}) => { }) => {
const location = useLocation();
const navigate = useNavigate();
const user = userState?.user;
const [statusState] = useContext(StatusContext);
const isSupplierApplyEnabled = React.useMemo(() => {
try {
const roleModulesRaw = statusState?.status?.SidebarModulesByRole;
if (!roleModulesRaw) return true;
const roleConfig = JSON.parse(roleModulesRaw);
const userRole = String(user?.role ?? 0);
const config = roleConfig[userRole];
if (!config) return true;
const consoleSection = config.console;
if (!consoleSection) return true;
if (consoleSection.enabled === false) return false;
return consoleSection.supplierApply !== false;
} catch {
return true;
}
}, [statusState?.status?.SidebarModulesByRole, user?.role]);
/** 未登录且已在登录页时:提示先登录,并同步 redirect 到目标申请页 */
const handleApplyEntryClick = useCallback(
(applyPath) => (e) => {
if (user) return;
if (location.pathname !== '/login') return;
e.preventDefault();
showInfo(t('请先登录'));
navigate(`/login?redirect=${encodeURIComponent(applyPath)}`, {
replace: true,
});
},
[user, location.pathname, navigate, t],
);
if (isMobile && isConsoleRoute) { if (isMobile && isConsoleRoute) {
return null; return null;
} }
const supplierApplyPath = '/console/supplier/apply';
/** 管理员不展示;已是供应商则隐藏「提供算力」 */
const hideApplyLinks = isAdmin();
const showSupplierApply = !hideApplyLinks && !userIsSupplierUser(user) && isSupplierApplyEnabled;
const supplierTo = user
? supplierApplyPath
: `/login?redirect=${encodeURIComponent(supplierApplyPath)}`;
const supplierActive = location.pathname.startsWith(supplierApplyPath);
return ( return (
<div className='flex items-center gap-2 md:gap-3 flex-shrink-0'> <div className="flex items-center gap-2 md:gap-3 flex-shrink-0">
<Link to='/' className='group flex items-center gap-2'> <Link to="/" className="group flex items-center gap-2">
<div className='relative w-8 h-8 md:w-8 md:h-8'> <div className="relative w-8 h-8 md:w-8 md:h-8">
<SkeletonWrapper loading={isLoading || !logoLoaded} type='image' /> <SkeletonWrapper loading={isLoading || !logoLoaded} type="image" />
<img <img
src={logo} src={logo}
alt='logo' alt="logo"
className={`absolute inset-0 w-full h-full transition-all duration-200 group-hover:scale-110 rounded-full ${!isLoading && logoLoaded ? 'opacity-100' : 'opacity-0'}`} className={`absolute inset-0 w-full h-full transition-all duration-200 group-hover:scale-110 rounded-full ${
!isLoading && logoLoaded ? "opacity-100" : "opacity-0"
}`}
/> />
</div> </div>
<div className='hidden md:flex items-center gap-2'> <div className="hidden md:flex items-center gap-2">
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
<SkeletonWrapper <SkeletonWrapper
loading={isLoading} loading={isLoading}
type='title' type="title"
width={120} width={120}
height={24} height={24}
> >
<Typography.Title <span className="text-lg font-semibold text-foreground">
heading={4}
className='!text-lg !font-semibold !mb-0'
>
{systemName} {systemName}
</Typography.Title> </span>
</SkeletonWrapper> </SkeletonWrapper>
{(isSelfUseMode || isDemoSiteMode) && !isLoading && ( {(isSelfUseMode || isDemoSiteMode) && !isLoading && (
<Tag <Tag
color={isSelfUseMode ? 'purple' : 'blue'} color={isSelfUseMode ? "purple" : "blue"}
className='text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm' size="small"
size='small' shape="circle"
shape='circle' className="text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm"
> >
{isSelfUseMode ? t('自用模式') : t('演示站点')} {isSelfUseMode ? t('自用模式') : t('演示站点')}
</Tag> </Tag>
@ -138,17 +75,6 @@ const HeaderLogo = ({
</div> </div>
</div> </div>
</Link> </Link>
{showSupplierApply && (
<div className='hidden sm:flex items-center gap-2 flex-shrink-0'>
<Link
to={supplierTo}
onClick={handleApplyEntryClick(supplierApplyPath)}
className={`${APPLY_BTN_BASE} ${supplierActive ? APPLY_BTN_ACTIVE : APPLY_BTN_IDLE}`}
>
{t('提供算力')}
</Link>
</div>
)}
</div> </div>
); );
}; };

View File

@ -1,4 +1,4 @@
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -17,60 +17,76 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React, { useState, useRef, useEffect } from "react";
import { Button, Dropdown } from '@douyinfe/semi-ui'; import { Languages } from "lucide-react";
import { Languages } from 'lucide-react'; import { useTranslation } from "react-i18next";
import { useTranslation } from 'react-i18next';
import { import {
normalizeLanguage, normalizeLanguage,
supportedLanguages, supportedLanguages,
LANGUAGE_NATIVE_LABELS, LANGUAGE_NATIVE_LABELS,
} from '../../../i18n/language'; } from "../../../i18n/language";
/** 语言在头部与下拉中一律显示其自称(如 English、简体中文不随界面语言翻译 */
const nativeLabel = (code) => LANGUAGE_NATIVE_LABELS[code] || code; const nativeLabel = (code) => LANGUAGE_NATIVE_LABELS[code] || code;
const itemClass = (active) =>
`!px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${
active
? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold'
: 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'
}`;
const LanguageSelector = ({ currentLang, onLanguageChange }) => { const LanguageSelector = ({ currentLang, onLanguageChange }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const normalized = normalizeLanguage(currentLang) || 'zh-CN'; const [open, setOpen] = useState(false);
const currentLabel = nativeLabel(normalized); const ref = useRef(null);
const normalized = normalizeLanguage(currentLang) || "zh-CN";
useEffect(() => {
if (!open) return;
const handler = (e) => {
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [open]);
return ( return (
<Dropdown <div className="relative" ref={ref}>
position='bottomRight' <button
render={ type="button"
<Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'> aria-label={`${t("切换语言")}: ${nativeLabel(normalized)}`}
{supportedLanguages.map((code) => ( onClick={() => setOpen(!open)}
<Dropdown.Item className="inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
key={code}
onClick={() => onLanguageChange(code)}
className={itemClass(normalized === code)}
>
{nativeLabel(code)}
</Dropdown.Item>
))}
</Dropdown.Menu>
}
>
<Button
icon={<Languages size={18} />}
aria-label={`${t('common.changeLanguage')}: ${currentLabel}`}
theme='borderless'
type='tertiary'
className='!px-2 !py-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2 !max-w-[11rem] sm:!max-w-[14rem]'
> >
<span className='truncate text-sm font-medium min-w-0'> <Languages size={16} />
{currentLabel} <span className="hidden sm:inline truncate max-w-[6rem]">
{nativeLabel(normalized)}
</span> </span>
</Button> </button>
</Dropdown>
{open && (
<div className="absolute right-0 top-full mt-1 z-50 min-w-[8rem] rounded-lg border border-border bg-popover p-1 shadow-md animate-fade-in">
{supportedLanguages.map((code) => {
const active = normalized === code;
return (
<button
key={code}
type="button"
onClick={() => {
onLanguageChange(code);
setOpen(false);
}}
className={`flex w-full items-center rounded-md px-2.5 py-1.5 text-sm transition-colors ${
active
? "bg-accent text-accent-foreground font-medium"
: "text-popover-foreground hover:bg-accent hover:text-accent-foreground"
}`}
>
{nativeLabel(code)}
{active && (
<span className="ml-auto flex h-3.5 w-3.5 items-center justify-center">
<span className="h-1.5 w-1.5 rounded-full bg-primary"></span>
</span>
)}
</button>
);
})}
</div>
)}
</div>
); );
}; };

View File

@ -1,4 +1,4 @@
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -17,9 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from "react";
import { Button } from '@douyinfe/semi-ui'; import { X, Menu } from "lucide-react";
import { IconClose, IconMenu } from '@douyinfe/semi-icons';
const MobileMenuButton = ({ const MobileMenuButton = ({
isConsoleRoute, isConsoleRoute,
@ -33,23 +32,17 @@ const MobileMenuButton = ({
return null; return null;
} }
const isOpen = isMobile ? drawerOpen : collapsed;
return ( return (
<Button <button
icon={ type="button"
(isMobile ? drawerOpen : collapsed) ? ( aria-label={isOpen ? t("关闭侧边栏") : t("打开侧边栏")}
<IconClose className='text-lg' />
) : (
<IconMenu className='text-lg' />
)
}
aria-label={
(isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏')
}
onClick={onToggle} onClick={onToggle}
theme='borderless' className="inline-flex items-center justify-center p-2 rounded-md text-foreground hover:bg-accent hover:text-accent-foreground transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
type='tertiary' >
className='!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700' {isOpen ? <X size={20} /> : <Menu size={20} />}
/> </button>
); );
}; };

View File

@ -20,19 +20,22 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { Button, Dropdown } from '@douyinfe/semi-ui'; import { Button, Dropdown } from '@douyinfe/semi-ui';
import { ChevronDown } from 'lucide-react'; import { ChevronDown, Box, LayoutDashboard, Trophy, BookOpen } from 'lucide-react';
const NAV_ICONS = {
pricing: Box,
console: LayoutDashboard,
benchmark: Trophy,
docs: BookOpen,
};
import SkeletonWrapper from '../components/SkeletonWrapper'; import SkeletonWrapper from '../components/SkeletonWrapper';
import { StatusContext } from '../../../context/Status';
import { isAdmin, userIsSupplierUser } from '../../../helpers';
/** 主站入口顺序(与桌面顶栏一致) */ /** 主站入口顺序(与桌面顶栏一致) */
const PRIMARY_NAV_KEYS = ['home', 'pricing', 'docs', 'about']; const PRIMARY_NAV_KEYS = ['pricing', 'benchmark', 'docs', 'console'];
const menuClass = const menuClass =
'!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'; '!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600';
const supplierApplyPath = '/console/supplier/apply';
const MobileSiteNavDropdown = ({ const MobileSiteNavDropdown = ({
mainNavLinks, mainNavLinks,
pricingRequireAuth, pricingRequireAuth,
@ -43,7 +46,6 @@ const MobileSiteNavDropdown = ({
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const user = userState?.user; const user = userState?.user;
const [statusState] = React.useContext(StatusContext);
const byNavKey = useMemo( const byNavKey = useMemo(
() => Object.fromEntries(mainNavLinks.map((l) => [l.itemKey, l])), () => Object.fromEntries(mainNavLinks.map((l) => [l.itemKey, l])),
@ -51,53 +53,16 @@ const MobileSiteNavDropdown = ({
); );
const primaryAndConsoleLinks = useMemo(() => { const primaryAndConsoleLinks = useMemo(() => {
const primary = PRIMARY_NAV_KEYS.map((k) => byNavKey[k]).filter(Boolean); return PRIMARY_NAV_KEYS.map((k) => byNavKey[k]).filter(Boolean);
const consoleLink = byNavKey.console;
if (consoleLink) {
return [...primary, consoleLink];
}
return primary;
}, [byNavKey]); }, [byNavKey]);
const applyEntries = useMemo(() => {
const hideApplyLinks = isAdmin();
if (!hideApplyLinks && !userIsSupplierUser(user)) {
try {
const roleModulesRaw = statusState?.status?.SidebarModulesByRole;
if (roleModulesRaw) {
const roleConfig = JSON.parse(roleModulesRaw);
const userRole = String(user?.role ?? 0);
const config = roleConfig[userRole];
if (config) {
const consoleSection = config.console;
if (consoleSection) {
if (consoleSection.enabled === false || consoleSection.supplierApply === false) {
return [];
}
}
}
}
} catch {}
return [
{
itemKey: 'supplier-apply',
text: t('提供算力'),
redirectPath: supplierApplyPath,
},
];
}
return [];
}, [user, t, statusState?.status?.SidebarModulesByRole]);
const currentPageLabel = useMemo(() => { const currentPageLabel = useMemo(() => {
const p = location.pathname; const p = location.pathname;
if (p === '/') return t('首页'); if (p === '/pricing' || p.startsWith('/pricing/')) return t('模型');
if (p === '/pricing' || p.startsWith('/pricing/')) return t('模型广场'); if (p.startsWith('/benchmarks')) return t('评测');
if (p === '/about' || p.startsWith('/about/')) return t('关于');
if (/\/[a-z]{2}\/docs\b/i.test(p) || p.includes('/docs')) { if (/\/[a-z]{2}\/docs\b/i.test(p) || p.includes('/docs')) {
return t('文档'); return t('文档');
} }
if (p.startsWith(supplierApplyPath)) return t('提供算力');
if (p.startsWith('/console')) return t('控制台'); if (p.startsWith('/console')) return t('控制台');
if (p === '/login') return t('登录'); if (p === '/login') return t('登录');
return t('页面导航'); return t('页面导航');
@ -148,20 +113,7 @@ const MobileSiteNavDropdown = ({
[navigate, resolveInternalTarget], [navigate, resolveInternalTarget],
); );
const handleSelectApply = useCallback( if (primaryAndConsoleLinks.length === 0) {
(entry) => {
window.setTimeout(() => {
if (user) {
navigate(entry.redirectPath);
return;
}
navigate(`/login?redirect=${encodeURIComponent(entry.redirectPath)}`);
}, 10);
},
[navigate, user],
);
if (primaryAndConsoleLinks.length === 0 && applyEntries.length === 0) {
return null; return null;
} }
@ -199,28 +151,14 @@ const MobileSiteNavDropdown = ({
onClick={() => handleSelectMain(link)} onClick={() => handleSelectMain(link)}
className={itemClass(active)} className={itemClass(active)}
> >
{link.text} <span className="inline-flex items-center gap-2">
</Dropdown.Item> {NAV_ICONS[link.itemKey] && React.createElement(NAV_ICONS[link.itemKey], { size: 16 })}
); {link.text}
})} </span>
{applyEntries.length > 0 && primaryAndConsoleLinks.length > 0 && (
<Dropdown.Divider />
)}
{applyEntries.map((entry) => {
const active = isActivePath(entry.redirectPath);
return (
<Dropdown.Item
key={entry.itemKey}
onMouseDown={(e) => {
e.stopPropagation();
}}
onClick={() => handleSelectApply(entry)}
className={itemClass(active)}
>
{entry.text}
</Dropdown.Item> </Dropdown.Item>
); );
})} })}
</Dropdown.Menu> </Dropdown.Menu>
} }
> >

View File

@ -1,4 +1,4 @@
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -19,8 +19,16 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react'; import React from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Box, LayoutDashboard, Trophy, BookOpen } from 'lucide-react';
import SkeletonWrapper from '../components/SkeletonWrapper'; import SkeletonWrapper from '../components/SkeletonWrapper';
const NAV_ICONS = {
pricing: Box,
console: LayoutDashboard,
benchmark: Trophy,
docs: BookOpen,
};
const Navigation = ({ const Navigation = ({
mainNavLinks, mainNavLinks,
isMobile, isMobile,
@ -33,21 +41,26 @@ const Navigation = ({
const isActive = (linkPath) => { const isActive = (linkPath) => {
if (!linkPath) return false; if (!linkPath) return false;
const currentPath = location.pathname; const currentPath = location.pathname;
if (linkPath === '/') { if (linkPath === "/") {
return currentPath === '/'; return currentPath === "/";
} }
return currentPath.startsWith(linkPath); return currentPath.startsWith(linkPath);
}; };
const renderNavLinks = () => { const renderNavLinks = () => {
const baseClasses = const baseClasses =
'flex-shrink-0 flex items-center text-sm font-medium transition-all duration-200 ease-in-out rounded-md'; "flex-shrink-0 inline-flex items-center gap-1.5 text-[14px] transition-colors duration-150 rounded-md px-3 py-1.5";
const spacingClasses = isMobile ? 'px-2 py-1' : 'px-3 py-2';
const hoverClasses = const hoverClasses =
'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700/50 hover:!text-semi-color-text-0 dark:hover:!text-white'; "hover:bg-accent hover:text-accent-foreground";
return mainNavLinks.map((link) => { return mainNavLinks.map((link) => {
const linkContent = <span>{link.text}</span>; const Icon = NAV_ICONS[link.itemKey];
const linkContent = (
<>
{Icon && <Icon size={16} className="shrink-0" />}
<span>{link.text}</span>
</>
);
if (link.isExternal) { if (link.isExternal) {
const openInNewTab = link.openInNewTab !== false; const openInNewTab = link.openInNewTab !== false;
@ -56,9 +69,9 @@ const Navigation = ({
key={link.itemKey} key={link.itemKey}
href={link.externalLink} href={link.externalLink}
{...(openInNewTab {...(openInNewTab
? { target: '_blank', rel: 'noopener noreferrer' } ? { target: "_blank", rel: "noopener noreferrer" }
: {})} : {})}
className={`${baseClasses} ${spacingClasses} ${hoverClasses} !text-semi-color-text-1 dark:!text-gray-300`} className={`${baseClasses} ${hoverClasses} text-muted-foreground`}
> >
{linkContent} {linkContent}
</a> </a>
@ -66,23 +79,23 @@ const Navigation = ({
} }
let targetPath = link.to; let targetPath = link.to;
if (link.itemKey === 'console' && !userState.user) { if (link.itemKey === "console" && !userState.user) {
targetPath = '/login'; targetPath = "/login";
} }
if (link.itemKey === 'pricing' && pricingRequireAuth && !userState.user) { if (link.itemKey === "pricing" && pricingRequireAuth && !userState.user) {
targetPath = '/login'; targetPath = "/login";
} }
const active = isActive(link.to); const active = isActive(link.to);
const activeClasses = active const activeClasses = active
? '!bg-semi-color-fill-2 dark:!bg-gray-700 !text-semi-color-text-0 dark:!text-white' ? "bg-accent text-accent-foreground font-semibold"
: '!text-semi-color-text-1 dark:!text-gray-300'; : "text-muted-foreground";
return ( return (
<Link <Link
key={link.itemKey} key={link.itemKey}
to={targetPath} to={targetPath}
className={`${baseClasses} ${spacingClasses} ${hoverClasses} ${activeClasses}`} className={`${baseClasses} ${hoverClasses} ${activeClasses}`}
> >
{linkContent} {linkContent}
</Link> </Link>
@ -91,10 +104,10 @@ const Navigation = ({
}; };
return ( return (
<nav className='hidden md:flex items-center gap-1 overflow-x-auto whitespace-nowrap scrollbar-hide'> <nav className="hidden md:flex items-center gap-1 overflow-x-auto whitespace-nowrap scrollbar-hide">
<SkeletonWrapper <SkeletonWrapper
loading={isLoading} loading={isLoading}
type='navigation' type="navigation"
count={4} count={4}
width={60} width={60}
height={16} height={16}

View File

@ -1,4 +1,4 @@
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -17,31 +17,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from "react";
import { Button, Badge } from '@douyinfe/semi-ui'; import { Bell } from "lucide-react";
import { Bell } from 'lucide-react';
// NotificationButton
const NotificationButton = ({ unreadCount, onNoticeOpen, t }) => { const NotificationButton = ({ unreadCount, onNoticeOpen, t }) => {
const buttonProps = { return (
icon: <Bell size={18} />, <button
'aria-label': t('站内消息'), type="button"
onClick: onNoticeOpen, aria-label={t("站内消息")}
theme: 'borderless', onClick={onNoticeOpen}
type: 'tertiary', className="relative inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className: >
'!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2', <Bell size={18} />
}; {unreadCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 flex h-4 min-w-[1rem] items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-bold text-destructive-foreground ring-2 ring-background">
if (unreadCount > 0) { {unreadCount > 99 ? "99+" : unreadCount}
return ( </span>
<Badge count={unreadCount} type='danger' overflowCount={99}> )}
<Button {...buttonProps} /> </button>
</Badge> );
);
}
return <Button {...buttonProps} />;
}; };
export default NotificationButton; export default NotificationButton;

View File

@ -17,86 +17,88 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Input, Dropdown, Typography } from '@douyinfe/semi-ui'; import { Input, Typography } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons'; import { IconSearch } from '@douyinfe/semi-icons';
import { API, isAdmin, showSuccess } from '../../../helpers';
import { useNavigate } from 'react-router-dom';
const mockSearchData = [ const { Text } = Typography;
{
month: '四月 2026', const SEARCH_DEBOUNCE_MS = 300;
items: [
{ id: 1, name: 'Google: Gemma 4 31B', icon: '⬥', color: 'text-blue-500' }, function debounce(fn, ms) {
{ let timer;
id: 2, return (...args) => {
name: 'Qwen: Qwen3.6 Plus (free)', clearTimeout(timer);
icon: '⬡', timer = setTimeout(() => fn(...args), ms);
color: 'text-purple-500', };
}, }
{
id: 3,
name: 'Z.ai: GLM 5V Turbo',
icon: '⬢',
color: 'text-gray-800 dark:text-white',
},
{
id: 4,
name: 'Arcee AI: Trinity Large Thinking',
icon: '⬢',
color: 'text-teal-500',
},
{
id: 5,
name: 'xAI: Grok 4.20 Multi-Agent',
icon: '⚡',
color: 'text-gray-800 dark:text-white',
},
{
id: 6,
name: 'xAI: Grok 4.20',
icon: '⚡',
color: 'text-gray-800 dark:text-white',
},
],
},
{
month: '三月 2026',
items: [
{
id: 7,
name: 'Google: Lyria 3 Pro Preview',
icon: '⬥',
color: 'text-blue-500',
},
],
},
];
const SearchDropdown = ({ isMobile }) => { const SearchDropdown = ({ isMobile }) => {
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [filteredData, setFilteredData] = useState(mockSearchData); const [models, setModels] = useState([]);
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
const inputRef = useRef(null); const inputRef = useRef(null);
const navigate = useNavigate();
//
useEffect(() => {
(async () => {
try {
const res = await API.get('/api/pricing', { disableDuplicate: true });
if (res?.data?.success && Array.isArray(res.data.data)) {
setModels(res.data.data);
}
} catch {}
})();
}, []);
const doSearch = useCallback(
(q) => {
const trimmed = q.trim();
if (!trimmed) {
setResults([]);
return;
}
setLoading(true);
const lower = trimmed.toLowerCase();
const matched = [];
for (const m of models) {
const name = (m.model_name || '').toLowerCase();
const desc = (m.description || '').toLowerCase();
const tags = (m.tags || '').toLowerCase();
if (name.includes(lower) || desc.includes(lower) || tags.includes(lower)) {
matched.push({
type: 'model',
label: m.model_name,
desc: m.description || '',
navigate: `/pricing?search=${encodeURIComponent(trimmed)}`,
});
}
if (matched.length >= 8) break;
}
setResults(matched);
setLoading(false);
},
[models],
);
const debouncedSearch = useCallback(
debounce(doSearch, SEARCH_DEBOUNCE_MS),
[doSearch],
);
useEffect(() => { useEffect(() => {
if (searchValue.trim() === '') { debouncedSearch(searchValue);
setFilteredData(mockSearchData); }, [searchValue, debouncedSearch]);
} else {
const filtered = mockSearchData
.map((group) => ({
...group,
items: group.items.filter((item) =>
item.name.toLowerCase().includes(searchValue.toLowerCase()),
),
}))
.filter((group) => group.items.length > 0);
setFilteredData(filtered);
}
}, [searchValue]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event) => { const handleKeyDown = (event) => {
if (event.key === '/') { if (event.key === '/' && document.activeElement === document.body) {
event.preventDefault(); event.preventDefault();
inputRef.current?.focus(); inputRef.current?.focus();
setVisible(true); setVisible(true);
@ -106,89 +108,110 @@ const SearchDropdown = ({ isMobile }) => {
inputRef.current?.blur(); inputRef.current?.blur();
} }
}; };
document.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown);
return () => { return () => document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, []); }, []);
const handleItemClick = (item) => { const handleItemClick = (item) => {
console.log('Selected:', item);
setVisible(false); setVisible(false);
setSearchValue(''); setSearchValue('');
setResults([]);
if (item.navigate) {
if (item.navigate.startsWith('/')) {
navigate(item.navigate);
} else {
window.open(item.navigate, '_blank');
}
}
};
const typeLabel = (type) => {
switch (type) {
case 'model': return '模型';
default: return '';
}
}; };
const renderDropdownContent = () => { const renderDropdownContent = () => {
if (!searchValue.trim()) {
return (
<div className='px-4 py-8 text-center'>
<Text className='!text-sm !text-semi-color-text-2 dark:!text-gray-400'>
输入关键词搜索模型
</Text>
</div>
);
}
if (loading) {
return (
<div className='px-4 py-8 text-center'>
<Text className='!text-sm !text-semi-color-text-2 dark:!text-gray-400'>
搜索中...
</Text>
</div>
);
}
if (results.length === 0) {
return (
<div className='px-4 py-8 text-center'>
<Text className='!text-sm !text-semi-color-text-2 dark:!text-gray-400'>
未找到匹配结果
</Text>
</div>
);
}
return ( return (
<div className='w-80 md:w-96 max-h-96 overflow-y-auto'> <div className='max-h-96 overflow-y-auto py-2'>
{filteredData.length > 0 ? ( {results.map((item, i) => (
filteredData.map((group) => ( <div
<div key={group.month} className='py-2'> key={`${item.type}-${i}`}
<Typography.Text className='!px-4 !py-2 !text-xs !font-semibold !text-semi-color-text-2 dark:!text-gray-400 uppercase tracking-wider block'> onClick={() => handleItemClick(item)}
{group.month} className='px-4 py-2.5 flex items-center gap-3 cursor-pointer hover:bg-semi-color-fill-1 dark:hover:bg-gray-700 transition-colors'
</Typography.Text> >
<div> <span className='shrink-0 rounded bg-blue-100 dark:bg-blue-900/40 px-1.5 py-0.5 text-xs text-blue-600 dark:text-blue-300'>
{group.items.map((item) => ( {typeLabel(item.type)}
<div </span>
key={item.id} <div className='min-w-0 flex-1'>
onClick={() => handleItemClick(item)} <Text className='!text-sm !font-medium !text-semi-color-text-0 dark:!text-gray-200 block truncate'>
className='px-4 py-2.5 flex items-center gap-3 cursor-pointer hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors' {item.label}
> </Text>
<span className={`text-lg ${item.color}`}>{item.icon}</span> {item.desc && (
<Typography.Text className='!text-sm !font-medium !text-semi-color-text-0 dark:!text-gray-200'> <Text className='!text-xs !text-semi-color-text-2 dark:!text-gray-400 block truncate'>
{item.name} {item.desc}
</Typography.Text> </Text>
</div> )}
))}
</div>
</div> </div>
))
) : (
<div className='px-4 py-8 text-center'>
<Typography.Text className='!text-sm !text-semi-color-text-2 dark:!text-gray-400'>
No results found
</Typography.Text>
</div> </div>
)} ))}
</div> </div>
); );
}; };
return ( return (
<div className='relative' ref={dropdownRef}> <div className='relative' ref={dropdownRef}>
<Dropdown <div className='relative'>
visible={visible} <Input
onVisibleChange={setVisible} ref={inputRef}
position='bottomLeft' placeholder='搜索模型...'
trigger='custom' prefix={<IconSearch className='text-semi-color-text-2 dark:text-gray-400' />}
getPopupContainer={() => dropdownRef.current} suffix={
render={ <kbd className='hidden sm:inline-block px-1.5 py-0.5 text-xs font-semibold text-semi-color-text-2 dark:text-gray-400 bg-semi-color-fill-0 dark:bg-gray-700 border border-semi-color-border dark:border-gray-600 rounded'>
<div className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-800 dark:!border-gray-600'> /
{renderDropdownContent()} </kbd>
</div> }
} value={searchValue}
> onChange={setSearchValue}
<div className='relative'> onFocus={() => { if (searchValue.trim()) setVisible(true); }}
<Input onBlur={() => setTimeout(() => setVisible(false), 150)}
ref={inputRef} className='!w-40 lg:!w-56 !h-9 !text-sm !bg-semi-color-fill-0 dark:!bg-gray-800/50 !border-semi-color-border dark:!border-gray-700 hover:!border-semi-color-primary dark:hover:!border-blue-400 focus:!border-semi-color-primary dark:focus:!border-blue-400'
placeholder='Search' style={{ borderRadius: '6px' }}
prefix={ />
<IconSearch className='text-semi-color-text-2 dark:text-gray-400' /> </div>
} {visible && (results.length > 0 || searchValue.trim()) && (
suffix={ <div className='absolute left-0 top-full mt-1 w-80 md:w-96 bg-semi-color-bg-overlay border border-semi-color-border shadow-lg rounded-lg dark:bg-gray-800 dark:border-gray-600 z-50'>
<kbd className='px-1.5 py-0.5 text-xs font-semibold !text-semi-color-text-2 dark:!text-gray-400 !bg-semi-color-fill-0 dark:!bg-gray-700 border !border-semi-color-border dark:!border-gray-600 rounded'> {renderDropdownContent()}
/
</kbd>
}
value={searchValue}
onChange={setSearchValue}
onFocus={() => setVisible(true)}
className='!w-48 lg:!w-64 !h-9 !text-sm !bg-semi-color-fill-0 dark:!bg-gray-800/50 !border-semi-color-border dark:!border-gray-700 hover:!border-semi-color-primary dark:hover:!border-blue-400 focus:!border-semi-color-primary dark:focus:!border-blue-400'
style={{ borderRadius: '6px', paddingRight: '10px' }}
/>
</div> </div>
</Dropdown> )}
</div> </div>
); );
}; };

View File

@ -1,4 +1,4 @@
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -17,101 +17,98 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useMemo } from 'react'; import React, { useMemo, useState, useRef, useEffect } from "react";
import { Dropdown } from '@douyinfe/semi-ui'; import { Sun, Moon, Monitor } from "lucide-react";
import { Sun, Moon, Monitor } from 'lucide-react'; import { useActualTheme } from "../../../context/Theme";
import { useActualTheme } from '../../../context/Theme';
const ThemeToggle = ({ theme, onThemeToggle, t }) => { const ThemeToggle = ({ theme, onThemeToggle, t }) => {
const actualTheme = useActualTheme(); const actualTheme = useActualTheme();
const [open, setOpen] = useState(false);
const ref = useRef(null);
const themeOptions = useMemo( const themeOptions = useMemo(
() => [ () => [
{ {
key: 'light', key: "light",
icon: <Sun size={18} />, icon: <Sun size={16} />,
buttonIcon: <Sun size={18} />, label: t("浅色模式"),
label: t('浅色模式'),
description: t('始终使用浅色主题'),
}, },
{ {
key: 'dark', key: "dark",
icon: <Moon size={18} />, icon: <Moon size={16} />,
buttonIcon: <Moon size={18} />, label: t("深色模式"),
label: t('深色模式'),
description: t('始终使用深色主题'),
}, },
{ {
key: 'auto', key: "auto",
icon: <Monitor size={18} />, icon: <Monitor size={16} />,
buttonIcon: <Monitor size={18} />, label: t("自动模式"),
label: t('自动模式'),
description: t('跟随系统主题设置'),
}, },
], ],
[t], [t],
); );
const getItemClassName = (isSelected) => const currentIcon = useMemo(() => {
isSelected const opt = themeOptions.find((o) => o.key === theme);
? '!bg-semi-color-primary-light-default !font-semibold' return opt?.icon || themeOptions[2].icon;
: 'hover:!bg-semi-color-fill-1';
const currentButtonIcon = useMemo(() => {
const currentOption = themeOptions.find((option) => option.key === theme);
return currentOption?.buttonIcon || themeOptions[2].buttonIcon;
}, [theme, themeOptions]); }, [theme, themeOptions]);
return ( // Close on outside click
<Dropdown useEffect(() => {
position='bottomRight' if (!open) return;
trigger='click' const handler = (e) => {
clickToHide if (ref.current && !ref.current.contains(e.target)) setOpen(false);
render={ };
<Dropdown.Menu> document.addEventListener("mousedown", handler);
{themeOptions.map((option) => ( return () => document.removeEventListener("mousedown", handler);
<Dropdown.Item }, [open]);
key={option.key}
icon={option.icon}
onClick={() => onThemeToggle(option.key)}
className={getItemClassName(theme === option.key)}
>
<div className='flex flex-col'>
<span>{option.label}</span>
<span className='text-xs text-semi-color-text-2'>
{option.description}
</span>
</div>
</Dropdown.Item>
))}
{theme === 'auto' && ( return (
<> <div className="relative" ref={ref}>
<Dropdown.Divider /> <button
<div className='px-3 py-2 text-xs text-semi-color-text-2'> type="button"
{t('当前跟随系统')} aria-label={t("切换主题")}
{actualTheme === 'dark' ? t('深色') : t('浅色')} onClick={() => setOpen(!open)}
</div> className="inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors"
</>
)}
</Dropdown.Menu>
}
>
<span
role='button'
tabIndex={0}
aria-label={t('切换主题')}
className='inline-flex items-center justify-center p-1.5 rounded-full cursor-pointer text-current bg-semi-color-fill-0 hover:bg-semi-color-fill-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-semi-color-primary'
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.currentTarget.click();
}
}}
> >
{currentButtonIcon} {currentIcon}
</span> </button>
</Dropdown>
{open && (
<div className="absolute right-0 top-full mt-1 z-50 min-w-[10rem] rounded-lg border border-border bg-popover p-1 shadow-md animate-fade-in">
{themeOptions.map((option) => {
const selected = theme === option.key;
return (
<button
key={option.key}
type="button"
onClick={() => {
onThemeToggle(option.key);
setOpen(false);
}}
className={`flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-sm transition-colors ${
selected
? "bg-accent text-accent-foreground font-medium"
: "text-popover-foreground hover:bg-accent hover:text-accent-foreground"
}`}
>
{option.icon}
<span>{option.label}</span>
{selected && (
<span className="ml-auto flex h-3.5 w-3.5 items-center justify-center">
<span className="h-1.5 w-1.5 rounded-full bg-primary"></span>
</span>
)}
</button>
);
})}
{theme === "auto" && (
<div className="border-t border-border mt-1 pt-1 px-2.5 py-1.5 text-xs text-muted-foreground">
{t("当前跟随系统")}{actualTheme === "dark" ? t("深色") : t("浅色")}
</div>
)}
</div>
)}
</div>
); );
}; };

View File

@ -84,7 +84,7 @@ const UserArea = ({
size='small' size='small'
className='text-gray-500 dark:text-gray-400' className='text-gray-500 dark:text-gray-400'
/> />
<span>{t('令牌管理')}</span> <span>{t('API Key')}</span>
</div> </div>
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item <Dropdown.Item

View File

@ -1,4 +1,4 @@
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -43,6 +43,8 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
docsNav, docsNav,
isDemoSiteMode, isDemoSiteMode,
isConsoleRoute, isConsoleRoute,
showThemeToggle,
showLanguageSelector,
theme, theme,
headerNavModules, headerNavModules,
pricingRequireAuth, pricingRequireAuth,
@ -57,11 +59,9 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
const [messageModalVisible, setMessageModalVisible] = useState(false); const [messageModalVisible, setMessageModalVisible] = useState(false);
const { unreadCount: messageUnreadCount, refreshUnreadCount } = const { unreadCount: messageUnreadCount, refreshUnreadCount } =
useUserMessageUnreadCount(userState?.user); useUserMessageUnreadCount(userState?.user);
// handleMessageModalOpen
const handleMessageModalOpen = useCallback(() => { const handleMessageModalOpen = useCallback(() => {
setMessageModalVisible(true); setMessageModalVisible(true);
}, []); }, []);
// handleMessageModalClose
const handleMessageModalClose = useCallback(async () => { const handleMessageModalClose = useCallback(async () => {
setMessageModalVisible(false); setMessageModalVisible(false);
await refreshUnreadCount(); await refreshUnreadCount();
@ -70,7 +70,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
const { mainNavLinks } = useNavigation(t, docsNav, headerNavModules); const { mainNavLinks } = useNavigation(t, docsNav, headerNavModules);
return ( return (
<header className='text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-[rgba(24,24,27,0.75)] backdrop-blur-lg border-b border-[#f5f5f5] dark:border-semi-color-border'> <header className="sticky top-0 z-50 w-full border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<UserMessageModal <UserMessageModal
visible={messageModalVisible} visible={messageModalVisible}
onClose={handleMessageModalClose} onClose={handleMessageModalClose}
@ -79,70 +79,69 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
t={t} t={t}
/> />
<div className='w-full px-4 md:px-6'> <div className="flex h-14 items-center px-4 md:px-6">
<div className='flex items-center justify-between h-14 gap-2'> <div className="flex items-center gap-2 md:gap-4">
<div className='flex items-center gap-2 md:gap-4 flex-1 min-w-0 md:flex-initial'> <MobileMenuButton
<MobileMenuButton isConsoleRoute={isConsoleRoute}
isConsoleRoute={isConsoleRoute} isMobile={isMobile}
isMobile={isMobile} drawerOpen={drawerOpen}
drawerOpen={drawerOpen} collapsed={collapsed}
collapsed={collapsed} onToggle={handleMobileMenuToggle}
onToggle={handleMobileMenuToggle} t={t}
t={t} />
/>
<HeaderLogo <HeaderLogo
isMobile={isMobile} isMobile={isMobile}
isConsoleRoute={isConsoleRoute} isConsoleRoute={isConsoleRoute}
logo={logo} logo={logo}
logoLoaded={logoLoaded} logoLoaded={logoLoaded}
isLoading={isLoading} isLoading={isLoading}
systemName={systemName} systemName={systemName}
isSelfUseMode={isSelfUseMode} isSelfUseMode={isSelfUseMode}
isDemoSiteMode={isDemoSiteMode} isDemoSiteMode={isDemoSiteMode}
userState={userState} t={t}
t={t} />
/> </div>
<div className='min-w-0 flex-1 overflow-hidden md:hidden'> <div className="min-w-0 overflow-hidden md:hidden">
<MobileSiteNavDropdown <MobileSiteNavDropdown
mainNavLinks={mainNavLinks} mainNavLinks={mainNavLinks}
pricingRequireAuth={pricingRequireAuth} pricingRequireAuth={pricingRequireAuth}
userState={userState} userState={userState}
isLoading={isLoading} isLoading={isLoading}
t={t} t={t}
/> />
</div> </div>
{/* {!isMobile && <SearchDropdown isMobile={isMobile} />} */} <div className="flex-1" />
</div>
<div className='flex flex-shrink-0 items-center gap-2 md:gap-6'> <div className="flex items-center gap-1 md:gap-2">
<Navigation <Navigation
mainNavLinks={mainNavLinks} mainNavLinks={mainNavLinks}
isMobile={isMobile} isMobile={isMobile}
isLoading={isLoading} isLoading={isLoading}
userState={userState} userState={userState}
pricingRequireAuth={pricingRequireAuth} pricingRequireAuth={pricingRequireAuth}
/> />
<ActionButtons <ActionButtons
isNewYear={isNewYear} isNewYear={isNewYear}
unreadCount={messageUnreadCount} unreadCount={messageUnreadCount}
onNoticeOpen={handleMessageModalOpen} onNoticeOpen={handleMessageModalOpen}
theme={theme} theme={theme}
onThemeToggle={handleThemeToggle} onThemeToggle={handleThemeToggle}
currentLang={currentLang} currentLang={currentLang}
onLanguageChange={handleLanguageChange} onLanguageChange={handleLanguageChange}
userState={userState} userState={userState}
isLoading={isLoading} isLoading={isLoading}
isMobile={isMobile} isMobile={isMobile}
isSelfUseMode={isSelfUseMode} isSelfUseMode={isSelfUseMode}
logout={logout} showThemeToggle={showThemeToggle}
navigate={navigate} showLanguageSelector={showLanguageSelector}
t={t} logout={logout}
/> navigate={navigate}
</div> t={t}
/>
</div> </div>
</div> </div>
</header> </header>

View File

@ -43,27 +43,30 @@ const DeploymentAccessGuard = ({
if (loading) { if (loading) {
return ( return (
<div className='mt-[60px] px-2'> <div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
<Card loading={true} style={{ minHeight: '400px' }}> <Card loading={true} style={{ minHeight: '400px' }}>
<div style={{ textAlign: 'center', padding: '50px 0' }}> <div style={{ textAlign: 'center', padding: '50px 0' }}>
<Text type='secondary'>{t('加载设置中...')}</Text> <Text type='secondary'>{t('加载设置中...')}</Text>
</div> </div>
</Card> </Card>
</div>
</div> </div>
); );
} }
if (!isEnabled) { if (!isEnabled) {
return ( return (
<div <div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
className='mt-[60px] px-4' <div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
style={{ <div
minHeight: 'calc(100vh - 60px)', style={{
display: 'flex', minHeight: 'calc(100vh - 160px)',
alignItems: 'center', display: 'flex',
justifyContent: 'center', alignItems: 'center',
}} justifyContent: 'center',
> }}
>
<div <div
style={{ style={{
maxWidth: '600px', maxWidth: '600px',
@ -276,18 +279,22 @@ const DeploymentAccessGuard = ({
</Text> </Text>
</Card> </Card>
</div> </div>
</div>
</div>
</div> </div>
); );
} }
if (connectionLoading || (connectionOk === null && !connectionError)) { if (connectionLoading || (connectionOk === null && !connectionError)) {
return ( return (
<div className='mt-[60px] px-2'> <div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
<Card loading={true} style={{ minHeight: '400px' }}> <Card loading={true} style={{ minHeight: '400px' }}>
<div style={{ textAlign: 'center', padding: '50px 0' }}> <div style={{ textAlign: 'center', padding: '50px 0' }}>
<Text type='secondary'>{t('正在检查 io.net 连接...')}</Text> <Text type='secondary'>{t('正在检查 io.net 连接...')}</Text>
</div> </div>
</Card> </Card>
</div>
</div> </div>
); );
} }
@ -301,15 +308,16 @@ const DeploymentAccessGuard = ({
const detail = connectionError?.message || ''; const detail = connectionError?.message || '';
return ( return (
<div <div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
className='mt-[60px] px-4' <div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
style={{ <div
minHeight: 'calc(100vh - 60px)', style={{
display: 'flex', minHeight: 'calc(100vh - 160px)',
alignItems: 'center', display: 'flex',
justifyContent: 'center', alignItems: 'center',
}} justifyContent: 'center',
> }}
>
<div <div
style={{ style={{
maxWidth: '600px', maxWidth: '600px',
@ -402,6 +410,8 @@ const DeploymentAccessGuard = ({
</div> </div>
</Card> </Card>
</div> </div>
</div>
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,198 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useEffect, useContext } from 'react';
import { Modal, Button, Steps, Typography, Space } from '@douyinfe/semi-ui';
import { IconCopy, IconLink } from '@douyinfe/semi-icons';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { UserContext } from '../../context/User';
import { copy, showSuccess } from '../../helpers';
const { Text, Title } = Typography;
const STORAGE_KEY = 'onboarding_completed';
const OnboardingWizard = () => {
const [visible, setVisible] = useState(false);
const [step, setStep] = useState(0);
const { t } = useTranslation();
const navigate = useNavigate();
const [userState] = useContext(UserContext);
useEffect(() => {
const completed = localStorage.getItem(STORAGE_KEY);
if (!completed && userState?.user) {
setVisible(true);
}
}, [userState?.user]);
const handleClose = () => {
localStorage.setItem(STORAGE_KEY, '1');
setVisible(false);
};
const getTokenStepExample = () => {
const baseUrl = window.location.origin;
const demoKey = 'sk-your-api-key-here';
return `curl ${baseUrl}/v1/chat/completions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer ${demoKey}" \\
-d '{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "Hello!"}]
}'`;
};
const steps = [
{
title: t('获取 API Key'),
content: (
<div className='space-y-4'>
<Text>{t('前往令牌管理页面创建您的第一个 API Key。')}</Text>
<div className='rounded-lg bg-semi-color-fill-0 dark:bg-gray-800 p-4'>
<Text className='!text-sm !text-semi-color-text-2'>
1. 点击下方按钮进入令牌页面<br />
2. 点击添加令牌创建新 Key<br />
3. 复制生成的 sk- 开头的 Key 妥善保存
</Text>
</div>
<Button
type='primary'
icon={<IconLink />}
onClick={() => {
handleClose();
navigate('/console/token');
}}
>
{t('前往令牌管理')}
</Button>
</div>
),
},
{
title: t('选择模型'),
content: (
<div className='space-y-4'>
<Text>{t('浏览模型市场,选择适合您需求的模型。')}</Text>
<div className='rounded-lg bg-semi-color-fill-0 dark:bg-gray-800 p-4'>
<Text className='!text-sm !text-semi-color-text-2'>
支持文本对话图片生成视频生成语音等多种模型<br />
可按价格供应商功能标签筛选
</Text>
</div>
<Button
type='primary'
icon={<IconLink />}
onClick={() => {
handleClose();
navigate('/pricing');
}}
>
{t('浏览模型市场')}
</Button>
</div>
),
},
{
title: t('首次调用'),
content: (
<div className='space-y-4'>
<Text>{t('使用以下 cURL 命令发送您的第一个 API 请求:')}</Text>
<div className='relative rounded-lg bg-gray-900 dark:bg-gray-950 p-4 group'>
<Button
theme='borderless'
type='tertiary'
size='small'
icon={<IconCopy />}
className='!absolute top-2 right-2 !text-gray-400 hover:!text-white'
onClick={() => {
copy(getTokenStepExample());
showSuccess(t('已复制'));
}}
/>
<pre className='text-xs text-green-400 overflow-x-auto whitespace-pre-wrap'>
{getTokenStepExample()}
</pre>
</div>
<Text className='!text-xs !text-semi-color-text-2'>
{t('将 {key} 替换为您上一步创建的 API Key将 model 替换为您选择的模型名称。', { key: 'sk-your-api-key-here' })}
</Text>
<Button
type='tertiary'
icon={<IconLink />}
onClick={() => {
handleClose();
navigate('/docs');
}}
>
{t('查看完整 API 文档')}
</Button>
</div>
),
},
];
return (
<Modal
title={
<div className='flex items-center gap-2'>
<span className='text-lg'>🚀</span>
<Title heading={5} className='!mb-0'>
{t('欢迎使用')}
</Title>
</div>
}
visible={visible}
onCancel={handleClose}
footer={
<Space>
{step > 0 && (
<Button onClick={() => setStep((s) => s - 1)}>
{t('上一步')}
</Button>
)}
{step < steps.length - 1 ? (
<Button
type='primary'
onClick={() => setStep((s) => s + 1)}
>
{t('下一步')}
</Button>
) : (
<Button type='primary' onClick={handleClose}>
{t('开始使用')}
</Button>
)}
</Space>
}
width={560}
maskClosable={false}
>
<Steps current={step} className='mb-6'>
{steps.map((s) => (
<Steps.Step key={s.title} title={s.title} />
))}
</Steps>
<div className='min-h-[160px]'>{steps[step].content}</div>
</Modal>
);
};
export default OnboardingWizard;

View File

@ -23,7 +23,7 @@ import React, { useEffect, useRef, useState } from 'react';
const ROOT_MARGIN = '280px 0px 280px 0px'; const ROOT_MARGIN = '280px 0px 280px 0px';
/** /**
* 操练场消息气泡进入或接近可视区域后再挂载子树减轻长对话下 DOM/媒体压力 * 体验消息气泡进入或接近可视区域后再挂载子树减轻长对话下 DOM/媒体压力
* variant=media更高占位避免图片/视频模式切换后列表高度不足导致滚不到底部 * variant=media更高占位避免图片/视频模式切换后列表高度不足导致滚不到底部
*/ */
const LazyVisibleMessage = ({ const LazyVisibleMessage = ({

View File

@ -38,7 +38,7 @@ import {
import ParameterControl from './ParameterControl'; import ParameterControl from './ParameterControl';
/** /**
* 操练场分组下拉与全局 renderGroupOption 一致但不展示倍率角标 * 体验分组下拉与全局 renderGroupOption 一致但不展示倍率角标
* @param {Record<string, unknown>} item Semi Select 传入的选项渲染参数 * @param {Record<string, unknown>} item Semi Select 传入的选项渲染参数
*/ */
const renderPlaygroundGroupOption = (item) => const renderPlaygroundGroupOption = (item) =>
@ -85,7 +85,7 @@ const SettingsPanel = ({
const mediaModeEnabled = isImageMode || isVideoMode; const mediaModeEnabled = isImageMode || isVideoMode;
const videoMediaHint = isVideoMode const videoMediaHint = isVideoMode
? t( ? t(
'操练场视频素材提示', '体验视频素材提示',
'图片地址:第 1 张为首帧2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。', '图片地址:第 1 张为首帧2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。',
) )
: ''; : '';

View File

@ -83,11 +83,11 @@ const VideoUrlInput = ({
{!enabled ? ( {!enabled ? (
<Typography.Text className='text-xs text-gray-500 mb-2 block'> <Typography.Text className='text-xs text-gray-500 mb-2 block'>
{t('操练场视频地址停用提示', '启用后可添加视频 URL视频生视频、视频编辑等')} {t('体验视频地址停用提示', '启用后可添加视频 URL视频生视频、视频编辑等')}
</Typography.Text> </Typography.Text>
) : list.length === 0 ? ( ) : list.length === 0 ? (
<Typography.Text className='text-xs text-gray-500 mb-2 block'> <Typography.Text className='text-xs text-gray-500 mb-2 block'>
{t('操练场视频地址空列表提示', '点击 + 添加 .mp4 / .mov 等可访问的视频链接')} {t('体验视频地址空列表提示', '点击 + 添加 .mp4 / .mov 等可访问的视频链接')}
</Typography.Text> </Typography.Text>
) : ( ) : (
<Typography.Text className='text-xs text-gray-500 mb-2 block'> <Typography.Text className='text-xs text-gray-500 mb-2 block'>

View File

@ -0,0 +1,379 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
Button,
Card,
Form,
Table,
Modal,
Typography,
Toast,
} from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../../helpers';
import { useTranslation } from 'react-i18next';
import { Plus, Pencil, Trash2, RefreshCw } from 'lucide-react';
const { Text } = Typography;
const EMPTY_MODEL = {
name: '',
provider: '',
intelligence: 0,
speed: 0,
price: 0,
latency: 0,
context_window: '',
type: 'non-reasoning',
openness: 'proprietary',
category: 'language',
};
export default function BenchmarkDataManager() {
const { t } = useTranslation();
const [models, setModels] = useState([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [editingIndex, setEditingIndex] = useState(null);
const [editForm, setEditForm] = useState({ ...EMPTY_MODEL });
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await API.get('/api/option/');
if (res.data.success && res.data.data) {
const option = res.data.data.find(
(o) => o.key === 'benchmark_data.models',
);
if (option && option.value) {
setModels(JSON.parse(option.value));
} else {
// try to fetch via the specific key
const fallbackRes = await API.get(
'/api/option/?key=benchmark_data.models',
);
if (fallbackRes.data?.data) {
setModels(JSON.parse(fallbackRes.data.data.value || '[]'));
}
}
}
} catch (error) {
showError(t('获取评测数据失败'));
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => {
fetchData();
}, [fetchData]);
const saveData = async (newModels) => {
setSaving(true);
try {
const res = await API.put('/api/option/', {
key: 'benchmark_data.models',
value: JSON.stringify(newModels),
});
if (res.data.success) {
showSuccess(t('保存成功'));
setModels(newModels);
} else {
showError(res.data.message || t('保存失败'));
}
} catch (error) {
showError(t('保存失败,请重试'));
} finally {
setSaving(false);
}
};
const openAddModal = () => {
setEditingIndex(null);
setEditForm({ ...EMPTY_MODEL });
setEditModalVisible(true);
};
const openEditModal = (index) => {
setEditingIndex(index);
setEditForm({ ...models[index] });
setEditModalVisible(true);
};
const handleSaveModel = async () => {
if (!editForm.name || !editForm.provider) {
showError(t('模型名称和供应商不能为空'));
return;
}
const newModels = [...models];
if (editingIndex !== null) {
newModels[editingIndex] = { ...editForm };
} else {
newModels.push({ ...editForm });
}
await saveData(newModels);
setEditModalVisible(false);
};
const handleDeleteModel = async (index) => {
const newModels = models.filter((_, i) => i !== index);
await saveData(newModels);
};
const handleFieldChange = (field, value) => {
setEditForm((prev) => ({
...prev,
[field]:
['intelligence', 'speed', 'price', 'latency'].includes(field)
? parseFloat(value) || 0
: value,
}));
};
const columns = [
{
title: t('模型名称'),
dataIndex: 'name',
width: 180,
render: (text, record) => (
<div>
<div className='font-medium'>{text}</div>
<Text type='secondary' size='small'>
{record.provider}
</Text>
</div>
),
},
{
title: t('智能指数'),
dataIndex: 'intelligence',
width: 100,
render: (val) => (
<span className='text-purple-400 font-mono'>{val}</span>
),
},
{
title: t('速度 (tok/s)'),
dataIndex: 'speed',
width: 120,
render: (val) => (
<span className='text-cyan-400 font-mono'>
{val?.toLocaleString()}
</span>
),
},
{
title: t('价格 ($/M)'),
dataIndex: 'price',
width: 100,
render: (val) => (
<span className='text-emerald-400 font-mono'>${val?.toFixed(2)}</span>
),
},
{
title: t('延迟 (s)'),
dataIndex: 'latency',
width: 100,
render: (val) => (
<span className='text-amber-400 font-mono'>{val?.toFixed(2)}s</span>
),
},
{
title: t('类型'),
dataIndex: 'type',
width: 80,
render: (val) => (
<span
className={`px-2 py-0.5 rounded text-xs ${
val === 'reasoning'
? 'bg-purple-900/50 text-purple-300'
: 'bg-blue-900/50 text-blue-300'
}`}
>
{val === 'reasoning' ? t('推理') : t('通用')}
</span>
),
},
{
title: t('操作'),
width: 120,
render: (_, __, index) => (
<div className='flex gap-2'>
<Button
size='small'
type='tertiary'
icon={<Pencil size={14} />}
onClick={() => openEditModal(index)}
/>
<Button
size='small'
type='danger'
icon={<Trash2 size={14} />}
onClick={() => handleDeleteModel(index)}
/>
</div>
),
},
];
return (
<Card
title={t('AI 模型评测数据管理')}
style={{ marginBottom: 16 }}
headerExtraContent={
<div className='flex gap-2'>
<Button
size='small'
type='tertiary'
icon={<RefreshCw size={14} />}
onClick={fetchData}
loading={loading}
>
{t('刷新')}
</Button>
<Button
size='small'
type='primary'
icon={<Plus size={14} />}
onClick={openAddModal}
>
{t('添加模型')}
</Button>
</div>
}
>
<Text type='secondary' size='small' style={{ marginBottom: 16, display: 'block' }}>
{t('在此管理 AI 模型评测数据,添加、编辑或删除模型条目。保存后前端评测页面将自动更新。')}
</Text>
<Table
columns={columns}
dataSource={models}
pagination={{ pageSize: 20 }}
loading={loading}
rowKey={(_, idx) => idx}
size='small'
bordered
/>
{/* Edit/Add Modal */}
<Modal
title={editingIndex !== null ? t('编辑模型') : t('添加模型')}
visible={editModalVisible}
onOk={handleSaveModel}
onCancel={() => setEditModalVisible(false)}
okText={t('保存')}
cancelText={t('取消')}
confirmLoading={saving}
style={{ width: 520 }}
>
<Form layout='vertical'>
<div className='grid grid-cols-2 gap-3'>
<Form.Input
field='name'
label={t('模型名称')}
value={editForm.name}
onChange={(v) => handleFieldChange('name', v)}
placeholder='e.g. Claude Opus 4.8'
/>
<Form.Input
field='provider'
label={t('供应商')}
value={editForm.provider}
onChange={(v) => handleFieldChange('provider', v)}
placeholder='e.g. Anthropic'
/>
<Form.InputNumber
field='intelligence'
label={t('智能指数')}
value={editForm.intelligence}
onChange={(v) => handleFieldChange('intelligence', v)}
min={0}
max={100}
/>
<Form.InputNumber
field='speed'
label={t('速度 (tokens/s)')}
value={editForm.speed}
onChange={(v) => handleFieldChange('speed', v)}
min={0}
/>
<Form.InputNumber
field='price'
label={t('价格 ($/M tokens)')}
value={editForm.price}
onChange={(v) => handleFieldChange('price', v)}
min={0}
step={0.01}
/>
<Form.InputNumber
field='latency'
label={t('延迟 (秒)')}
value={editForm.latency}
onChange={(v) => handleFieldChange('latency', v)}
min={0}
step={0.01}
/>
<Form.Input
field='context_window'
label={t('上下文窗口')}
value={editForm.context_window}
onChange={(v) => handleFieldChange('context_window', v)}
placeholder='e.g. 128K'
/>
<Form.Select
field='type'
label={t('模型类型')}
value={editForm.type}
onChange={(v) => handleFieldChange('type', v)}
>
<Form.Select.Option value='reasoning'>
{t('推理模型')}
</Form.Select.Option>
<Form.Select.Option value='non-reasoning'>
{t('通用模型')}
</Form.Select.Option>
</Form.Select>
<Form.Select
field='openness'
label={t('开放性')}
value={editForm.openness}
onChange={(v) => handleFieldChange('openness', v)}
>
<Form.Select.Option value='proprietary'>
{t('闭源')}
</Form.Select.Option>
<Form.Select.Option value='open-weights'>
{t('开放权重')}
</Form.Select.Option>
</Form.Select>
<Form.Select
field='category'
label={t('类别')}
value={editForm.category}
onChange={(v) => handleFieldChange('category', v)}
>
<Form.Select.Option value='language'>
{t('语言模型')}
</Form.Select.Option>
<Form.Select.Option value='image'>
{t('图像模型')}
</Form.Select.Option>
<Form.Select.Option value='video'>
{t('视频模型')}
</Form.Select.Option>
</Form.Select>
</div>
</Form>
</Modal>
</Card>
);
}

View File

@ -28,6 +28,7 @@ import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit'; import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit';
import SettingsDistributor from '../../pages/Setting/Operation/SettingsDistributor'; import SettingsDistributor from '../../pages/Setting/Operation/SettingsDistributor';
import SettingsCheckin from '../../pages/Setting/Operation/SettingsCheckin'; import SettingsCheckin from '../../pages/Setting/Operation/SettingsCheckin';
import SettingsAntiAbuse from '../../pages/Setting/Operation/SettingsAntiAbuse';
import { API, showError, toBoolean } from '../../helpers'; import { API, showError, toBoolean } from '../../helpers';
const OperationSetting = () => { const OperationSetting = () => {
@ -169,6 +170,10 @@ const OperationSetting = () => {
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<SettingsCheckin options={inputs} refresh={onRefresh} /> <SettingsCheckin options={inputs} refresh={onRefresh} />
</Card> </Card>
{/* 风控设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsAntiAbuse />
</Card>
</Spin> </Spin>
</> </>
); );

View File

@ -270,7 +270,7 @@ const OtherSetting = () => {
iconLink.rel = 'icon'; iconLink.rel = 'icon';
document.head.appendChild(iconLink); document.head.appendChild(iconLink);
} }
iconLink.href = inputs.Logo || '/logo.png'; iconLink.href = inputs.Logo || '/logo.jpg';
showSuccess('Logo 已更新'); showSuccess('Logo 已更新');
} catch (error) { } catch (error) {
console.error('Logo 更新失败', error); console.error('Logo 更新失败', error);

View File

@ -33,7 +33,7 @@ import {
} from '../../helpers'; } from '../../helpers';
import { normalizeSmsVerificationEnabled } from '../../helpers/data'; import { normalizeSmsVerificationEnabled } from '../../helpers/data';
import { UserContext } from '../../context/User'; import { UserContext } from '../../context/User';
import { Modal, Card, Button, Typography } from '@douyinfe/semi-ui'; import { Modal } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
// //
@ -555,26 +555,25 @@ const PersonalSetting = () => {
return ( return (
<div className='mt-[60px]'> <div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
<div className='flex justify-center'> <div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
<div className='w-full max-w-7xl mx-auto px-2'> {/* 顶部用户信息区域 */}
{/* 顶部用户信息区域 */} <UserInfoHeader t={t} userState={userState} />
<UserInfoHeader t={t} userState={userState} />
{/* 签到日历 - 仅在启用时显示 */} {/* 签到日历 - 仅在启用时显示 */}
{status?.checkin_enabled && ( {status?.checkin_enabled && (
<div className='mt-4 md:mt-6'> <div className='mt-4 md:mt-6'>
<CheckinCalendar <CheckinCalendar
t={t} t={t}
status={status} status={status}
turnstileEnabled={turnstileEnabled} turnstileEnabled={turnstileEnabled}
turnstileSiteKey={turnstileSiteKey} turnstileSiteKey={turnstileSiteKey}
/> />
</div> </div>
)} )}
{/* 账户管理和其他设置 */} {/* 账户管理和其他设置 */}
<div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'> <div className='mt-4 grid grid-cols-1 items-start gap-4 md:mt-6 md:gap-6 xl:grid-cols-2'>
{/* 左侧:账户管理设置 */} {/* 左侧:账户管理设置 */}
<div className='flex flex-col gap-4 md:gap-6'> <div className='flex flex-col gap-4 md:gap-6'>
<AccountManagement <AccountManagement
@ -598,8 +597,8 @@ const PersonalSetting = () => {
onPasskeyDelete={handleRemovePasskey} onPasskeyDelete={handleRemovePasskey}
/> />
{/* 偏好设置(语言等) */} {/* 偏好设置(语言等)- 已隐藏:多语言功能屏蔽,统一使用简体中文 */}
<PreferencesSettings t={t} /> {/* <PreferencesSettings t={t} /> */}
</div> </div>
{/* 右侧:其他设置 */} {/* 右侧:其他设置 */}
@ -609,8 +608,6 @@ const PersonalSetting = () => {
handleNotificationSettingChange={handleNotificationSettingChange} handleNotificationSettingChange={handleNotificationSettingChange}
saveNotificationSettings={saveNotificationSettings} saveNotificationSettings={saveNotificationSettings}
/> />
</div>
</div>
</div> </div>
{/* 模态框组件 */} {/* 模态框组件 */}
@ -680,6 +677,7 @@ const PersonalSetting = () => {
turnstileSiteKey={turnstileSiteKey} turnstileSiteKey={turnstileSiteKey}
setTurnstileToken={setTurnstileToken} setTurnstileToken={setTurnstileToken}
/> />
</div>
</div> </div>
); );
}; };

View File

@ -19,18 +19,15 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { import {
Card,
Calendar, Calendar,
Button, Button,
Typography,
Avatar,
Spin, Spin,
Tooltip, Tooltip,
Collapsible, Collapsible,
Modal, Modal,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
CalendarCheck, Sparkles,
Gift, Gift,
Check, Check,
ChevronDown, ChevronDown,
@ -213,7 +210,7 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
}; };
return ( return (
<Card className='!rounded-2xl'> <div className='rounded-2xl border border-slate-200 bg-white p-6 dark:border-white/5 dark:bg-white/[0.02]'>
<Modal <Modal
title='Security Check' title='Security Check'
visible={turnstileModalVisible} visible={turnstileModalVisible}
@ -241,24 +238,24 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
{/* 卡片头部 */} {/* 卡片头部 */}
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<div <div
className='flex items-center flex-1 cursor-pointer' className='flex flex-1 cursor-pointer items-center gap-3'
onClick={() => setIsCollapsed(!isCollapsed)} onClick={() => setIsCollapsed(!isCollapsed)}
> >
<Avatar size='small' color='green' className='mr-3 shadow-md'> <div className='flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400'>
<CalendarCheck size={16} /> <Sparkles size={18} />
</Avatar> </div>
<div className='flex-1'> <div className='min-w-0 flex-1'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Typography.Text className='text-lg font-medium'> <span className='text-lg font-semibold tracking-tight text-slate-900 dark:text-white'>
{t('每日签到')} {t('每日签到')}
</Typography.Text> </span>
{isCollapsed ? ( {isCollapsed ? (
<ChevronDown size={16} className='text-gray-400' /> <ChevronDown size={16} className='text-slate-400 dark:text-white/40' />
) : ( ) : (
<ChevronUp size={16} className='text-gray-400' /> <ChevronUp size={16} className='text-slate-400 dark:text-white/40' />
)} )}
</div> </div>
<div className='text-xs text-gray-500 dark:text-gray-400'> <div className='text-xs text-slate-400 dark:text-white/40'>
{!initialLoaded {!initialLoaded
? t('正在加载签到状态...') ? t('正在加载签到状态...')
: checkinData.stats?.checked_in_today : checkinData.stats?.checked_in_today
@ -276,7 +273,7 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
onClick={() => doCheckin()} onClick={() => doCheckin()}
loading={checkinLoading || !initialLoaded} loading={checkinLoading || !initialLoaded}
disabled={!initialLoaded || checkinData.stats?.checked_in_today} disabled={!initialLoaded || checkinData.stats?.checked_in_today}
className='!bg-green-600 hover:!bg-green-700' className='!rounded-lg'
> >
{!initialLoaded {!initialLoaded
? t('加载中...') ? t('加载中...')
@ -289,30 +286,36 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
{/* 可折叠内容 */} {/* 可折叠内容 */}
<Collapsible isOpen={isCollapsed === false} keepDOM> <Collapsible isOpen={isCollapsed === false} keepDOM>
{/* 签到统计 */} {/* 签到统计 */}
<div className='grid grid-cols-3 gap-3 mb-4 mt-4'> <div className='mt-4 mb-4 grid grid-cols-3 gap-3'>
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'> <div className='rounded-xl border border-slate-200 bg-slate-50/60 p-2.5 text-center dark:border-white/5 dark:bg-white/[0.02]'>
<div className='text-xl font-bold text-green-600'> <div className='text-xl font-bold tabular-nums text-emerald-600 dark:text-emerald-400'>
{checkinData.stats?.total_checkins || 0} {checkinData.stats?.total_checkins || 0}
</div> </div>
<div className='text-xs text-gray-500'>{t('累计签到')}</div> <div className='text-xs text-slate-400 dark:text-white/40'>
{t('累计签到')}
</div>
</div> </div>
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'> <div className='rounded-xl border border-slate-200 bg-slate-50/60 p-2.5 text-center dark:border-white/5 dark:bg-white/[0.02]'>
<div className='text-xl font-bold text-orange-600'> <div className='text-xl font-bold tabular-nums text-amber-600 dark:text-amber-400'>
{renderQuota(monthlyQuota)} {renderQuota(monthlyQuota)}
</div> </div>
<div className='text-xs text-gray-500'>{t('本月获得')}</div> <div className='text-xs text-slate-400 dark:text-white/40'>
{t('本月获得')}
</div>
</div> </div>
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'> <div className='rounded-xl border border-slate-200 bg-slate-50/60 p-2.5 text-center dark:border-white/5 dark:bg-white/[0.02]'>
<div className='text-xl font-bold text-blue-600'> <div className='text-xl font-bold tabular-nums text-blue-600 dark:text-blue-400'>
{renderQuota(checkinData.stats?.total_quota || 0)} {renderQuota(checkinData.stats?.total_quota || 0)}
</div> </div>
<div className='text-xs text-gray-500'>{t('累计获得')}</div> <div className='text-xs text-slate-400 dark:text-white/40'>
{t('累计获得')}
</div>
</div> </div>
</div> </div>
{/* 签到日历 - 使用更紧凑的样式 */} {/* 签到日历 - 使用更紧凑的样式 */}
<Spin spinning={loading}> <Spin spinning={loading}>
<div className='border rounded-lg overflow-hidden checkin-calendar'> <div className='checkin-calendar overflow-hidden rounded-xl border border-slate-200 dark:border-white/5'>
<style>{` <style>{`
.checkin-calendar .semi-calendar { .checkin-calendar .semi-calendar {
font-size: 13px; font-size: 13px;
@ -367,17 +370,15 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
</Spin> </Spin>
{/* 签到说明 */} {/* 签到说明 */}
<div className='mt-3 p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'> <div className='mt-3 rounded-xl border border-slate-200 bg-slate-50/60 p-2.5 dark:border-white/5 dark:bg-white/[0.02]'>
<Typography.Text type='tertiary' className='text-xs'> <ul className='list-disc space-y-0.5 pl-4 text-xs text-slate-400 dark:text-white/40'>
<ul className='list-disc list-inside space-y-0.5'> <li>{t('每日签到可获得随机额度奖励')}</li>
<li>{t('每日签到可获得随机额度奖励')}</li> <li>{t('签到奖励将直接添加到您的账户余额')}</li>
<li>{t('签到奖励将直接添加到您的账户余额')}</li> <li>{t('每日仅可签到一次,请勿重复签到')}</li>
<li>{t('每日仅可签到一次,请勿重复签到')}</li> </ul>
</ul>
</Typography.Text>
</div> </div>
</Collapsible> </Collapsible>
</Card> </div>
); );
}; };

View File

@ -21,8 +21,6 @@ import React, { useRef, useEffect, useState, useContext } from 'react';
import { import {
Button, Button,
Typography, Typography,
Card,
Avatar,
Form, Form,
Radio, Radio,
Toast, Toast,
@ -33,7 +31,13 @@ import {
Col, Col,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons'; import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react'; import {
BellRing,
Coins,
EyeOff,
LayoutPanelLeft,
Wrench,
} from 'lucide-react';
import { import {
renderQuotaWithPrompt, renderQuotaWithPrompt,
API, API,
@ -212,11 +216,11 @@ const NotificationSettings = ({
{ {
key: 'chat', key: 'chat',
title: t('聊天区域'), title: t('聊天区域'),
description: t('操练场和聊天功能'), description: t('体验和聊天功能'),
modules: [ modules: [
{ {
key: 'playground', key: 'playground',
title: t('操练场'), title: t('体验'),
description: t('AI模型测试环境'), description: t('AI模型测试环境'),
}, },
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') }, { key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
@ -228,7 +232,7 @@ const NotificationSettings = ({
description: t('数据管理和日志查看'), description: t('数据管理和日志查看'),
modules: [ modules: [
{ key: 'detail', title: t('数据看板'), description: t('系统数据统计') }, { key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
{ key: 'token', title: t('令牌管理'), description: t('API令牌管理') }, { key: 'token', title: t('API Key'), description: t('APIAPI Key') },
{ key: 'log', title: t('使用日志'), description: t('API使用记录') }, { key: 'log', title: t('使用日志'), description: t('API使用记录') },
{ {
key: 'midjourney', key: 'midjourney',
@ -317,48 +321,16 @@ const NotificationSettings = ({
}; };
return ( return (
<Card <div className='rounded-2xl border border-slate-200 bg-white p-6 dark:border-white/5 dark:bg-white/[0.02]'>
className='!rounded-2xl shadow-sm border-0' <div className='mb-5 flex items-center gap-3'>
footer={ <div className='flex h-10 w-10 items-center justify-center rounded-xl bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
<div className='flex justify-end gap-3'> <Wrench size={18} />
{activeTabKey === 'sidebar' ? (
//
<>
<Button
type='tertiary'
onClick={resetSidebarModules}
className='!rounded-lg'
>
{t('重置为默认')}
</Button>
<Button
type='primary'
onClick={saveSidebarSettings}
loading={sidebarLoading}
className='!rounded-lg'
>
{t('保存设置')}
</Button>
</>
) : (
//
<Button type='primary' onClick={handleSubmit}>
{t('保存设置')}
</Button>
)}
</div> </div>
}
>
{/* 卡片头部 */}
<div className='flex items-center mb-4'>
<Avatar size='small' color='blue' className='mr-3 shadow-md'>
<Bell size={16} />
</Avatar>
<div> <div>
<Typography.Text className='text-lg font-medium'> <div className='text-lg font-semibold tracking-tight text-slate-900 dark:text-white'>
{t('其他设置')} {t('其他设置')}
</Typography.Text> </div>
<div className='text-xs text-gray-600'> <div className='text-xs text-slate-400 dark:text-white/40'>
{t('通知、价格和隐私相关设置')} {t('通知、价格和隐私相关设置')}
</div> </div>
</div> </div>
@ -379,7 +351,7 @@ const NotificationSettings = ({
<TabPane <TabPane
tab={ tab={
<div className='flex items-center'> <div className='flex items-center'>
<Bell size={16} className='mr-2' /> <BellRing size={14} className='mr-2' />
{t('通知配置')} {t('通知配置')}
</div> </div>
} }
@ -719,7 +691,7 @@ const NotificationSettings = ({
<TabPane <TabPane
tab={ tab={
<div className='flex items-center'> <div className='flex items-center'>
<DollarSign size={16} className='mr-2' /> <Coins size={14} className='mr-2' />
{t('价格设置')} {t('价格设置')}
</div> </div>
} }
@ -745,7 +717,7 @@ const NotificationSettings = ({
<TabPane <TabPane
tab={ tab={
<div className='flex items-center'> <div className='flex items-center'>
<ShieldCheck size={16} className='mr-2' /> <EyeOff size={14} className='mr-2' />
{t('隐私设置')} {t('隐私设置')}
</div> </div>
} }
@ -770,7 +742,7 @@ const NotificationSettings = ({
<TabPane <TabPane
tab={ tab={
<div className='flex items-center'> <div className='flex items-center'>
<Settings size={16} className='mr-2' /> <LayoutPanelLeft size={14} className='mr-2' />
{t('边栏设置')} {t('边栏设置')}
</div> </div>
} }
@ -791,36 +763,19 @@ const NotificationSettings = ({
</Typography.Text> </Typography.Text>
</div> </div>
{/* 边栏设置功能区域容器 */} {/* 边栏设置功能区域容器 */}
<div <div className='rounded-2xl border border-slate-200 bg-slate-50/50 p-4 dark:border-white/5 dark:bg-white/[0.02]'>
className='border rounded-xl p-4'
style={{
borderColor: 'var(--semi-color-border)',
backgroundColor: 'var(--semi-color-bg-1)',
}}
>
{sectionConfigs.map((section) => ( {sectionConfigs.map((section) => (
<div key={section.key} className='mb-6'> <div key={section.key} className='mb-6 last:mb-0'>
{/* 区域标题和总开关 */} {/* 区域标题和总开关 */}
<div <div className='mb-4 flex items-center justify-between rounded-xl border border-slate-200 bg-white p-4 dark:border-white/5 dark:bg-white/[0.02]'>
className='flex justify-between items-center mb-4 p-4 rounded-lg' <div className='min-w-0'>
style={{ <div className='mb-1 text-base font-semibold text-slate-900 dark:text-white'>
backgroundColor: 'var(--semi-color-fill-0)',
border: '1px solid var(--semi-color-border-light)',
borderColor: 'var(--semi-color-fill-1)',
}}
>
<div>
<div className='font-semibold text-base text-gray-900 mb-1'>
{section.title} {section.title}
</div> </div>
<Typography.Text <Typography.Text
type='secondary' type='secondary'
size='small' size='small'
style={{ className='text-xs text-slate-400 dark:text-white/40'
fontSize: '12px',
lineHeight: '1.5',
color: 'var(--semi-color-text-2)',
}}
> >
{section.description} {section.description}
</Typography.Text> </Typography.Text>
@ -845,30 +800,23 @@ const NotificationSettings = ({
lg={8} lg={8}
xl={8} xl={8}
> >
<Card <div
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${ className={`rounded-xl border border-slate-200 bg-white p-4 transition-colors hover:border-blue-300 dark:border-white/5 dark:bg-white/[0.02] dark:hover:border-blue-400/40 ${
sidebarModulesUser[section.key]?.enabled !== sidebarModulesUser[section.key]?.enabled !==
false false
? '' ? ''
: 'opacity-50' : 'opacity-50'
}`} }`}
bodyStyle={{ padding: '16px' }}
> >
<div className='flex justify-between items-center h-full'> <div className='flex h-full items-center justify-between'>
<div className='flex-1 text-left'> <div className='min-w-0 flex-1 text-left'>
<div className='font-semibold text-sm text-gray-900 mb-1'> <div className='mb-1 text-sm font-semibold text-slate-900 dark:text-white'>
{module.title} {module.title}
</div> </div>
<Typography.Text <Typography.Text
type='secondary' type='secondary'
size='small' size='small'
className='block' className='mt-1 block text-xs text-slate-400 dark:text-white/40'
style={{
fontSize: '12px',
lineHeight: '1.5',
color: 'var(--semi-color-text-2)',
marginTop: '4px',
}}
> >
{module.description} {module.description}
</Typography.Text> </Typography.Text>
@ -892,7 +840,7 @@ const NotificationSettings = ({
/> />
</div> </div>
</div> </div>
</Card> </div>
</Col> </Col>
))} ))}
</Row> </Row>
@ -906,7 +854,41 @@ const NotificationSettings = ({
</Tabs> </Tabs>
)} )}
</Form> </Form>
</Card>
{/* Footer: 保存按钮 */}
<div className='mt-6 flex justify-end gap-3 border-t border-slate-100 pt-4 dark:border-white/5'>
{activeTabKey === 'sidebar' ? (
<>
<Button
type='tertiary'
theme='light'
onClick={resetSidebarModules}
className='!rounded-lg'
>
{t('重置为默认')}
</Button>
<Button
type='primary'
theme='solid'
onClick={saveSidebarSettings}
loading={sidebarLoading}
className='!rounded-lg'
>
{t('保存设置')}
</Button>
</>
) : (
<Button
type='primary'
theme='solid'
onClick={handleSubmit}
className='!rounded-lg'
>
{t('保存设置')}
</Button>
)}
</div>
</div>
); );
}; };

View File

@ -18,8 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { Card, Select, Typography, Avatar } from '@douyinfe/semi-ui'; import { Select } from '@douyinfe/semi-ui';
import { Languages } from 'lucide-react'; import { SlidersHorizontal, Globe } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { API, showSuccess, showError } from '../../../../helpers'; import { API, showSuccess, showError } from '../../../../helpers';
import { UserContext } from '../../../../context/User'; import { UserContext } from '../../../../context/User';
@ -124,62 +124,55 @@ const PreferencesSettings = ({ t }) => {
}; };
return ( return (
<Card className='!rounded-2xl shadow-sm border-0'> <div className='rounded-2xl border border-slate-200 bg-white p-6 dark:border-white/5 dark:bg-white/[0.02]'>
{/* Card Header */} {/* Card Header */}
<div className='flex items-center mb-4'> <div className='mb-5 flex items-center gap-3'>
<Avatar size='small' color='violet' className='mr-3 shadow-md'> <div className='flex h-10 w-10 items-center justify-center rounded-xl bg-violet-50 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400'>
<Languages size={16} /> <SlidersHorizontal size={18} />
</Avatar> </div>
<div> <div>
<Typography.Text className='text-lg font-medium'> <div className='text-lg font-semibold tracking-tight text-slate-900 dark:text-white'>
{t('偏好设置')} {t('偏好设置')}
</Typography.Text> </div>
<div className='text-xs text-gray-600 dark:text-gray-400'> <div className='text-xs text-slate-400 dark:text-white/40'>
{t('界面语言和其他个人偏好')} {t('界面语言和其他个人偏好')}
</div> </div>
</div> </div>
</div> </div>
{/* Language Setting Card */} {/* Language Setting Card */}
<Card className='!rounded-xl border dark:border-gray-700'> <div className='flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-5 sm:flex-row sm:items-center sm:justify-between dark:border-white/5 dark:bg-white/[0.02]'>
<div className='flex flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-4'> <div className='flex w-full items-start gap-3 sm:w-auto'>
<div className='flex items-start w-full sm:w-auto'> <div className='flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-violet-50 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400'>
<div className='w-12 h-12 rounded-full bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center mr-4 flex-shrink-0'> <Globe size={18} />
<Languages </div>
size={20} <div className='min-w-0'>
className='text-violet-600 dark:text-violet-400' <div className='text-sm font-semibold text-slate-900 dark:text-white'>
/> {t('语言偏好')}
</div> </div>
<div> <div className='mt-0.5 text-xs text-slate-400 dark:text-white/40'>
<Typography.Title heading={6} className='mb-1'> {t('选择您的首选界面语言,设置将自动保存并同步到所有设备')}
{t('语言偏好')}
</Typography.Title>
<Typography.Text type='tertiary' className='text-sm'>
{t('选择您的首选界面语言,设置将自动保存并同步到所有设备')}
</Typography.Text>
</div> </div>
</div> </div>
<Select
value={currentLanguage}
onChange={handleLanguagePreferenceChange}
style={{ width: 180 }}
loading={loading}
optionList={languageOptions.map((opt) => ({
value: opt.value,
label: opt.label,
}))}
/>
</div> </div>
</Card> <Select
value={currentLanguage}
onChange={handleLanguagePreferenceChange}
style={{ width: 180 }}
loading={loading}
optionList={languageOptions.map((opt) => ({
value: opt.value,
label: opt.label,
}))}
/>
</div>
{/* Additional info */} {/* Additional info */}
<div className='mt-4 text-xs text-gray-500 dark:text-gray-400'> <div className='mt-4 text-xs text-slate-400 dark:text-white/40'>
<Typography.Text type='tertiary'> {t(
{t( '提示语言偏好会同步到您登录的所有设备并影响API返回的错误消息语言。',
'提示语言偏好会同步到您登录的所有设备并影响API返回的错误消息语言。', )}
)}
</Typography.Text>
</div> </div>
</Card> </div>
); );
}; };

View File

@ -32,18 +32,19 @@ import {
Badge, Badge,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IconShield,
IconAlertTriangle, IconAlertTriangle,
IconRefresh, IconRefresh,
IconCopy, IconCopy,
IconShield,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { ScanFace } from 'lucide-react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const TwoFASetting = ({ t }) => { const TwoFASetting = ({ t, compact = false }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [status, setStatus] = useState({ const [status, setStatus] = useState({
enabled: false, enabled: false,
@ -366,20 +367,23 @@ const TwoFASetting = ({ t }) => {
return ( return (
<> <>
<Card className='!rounded-xl w-full'> <div
<div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'> className={
<div className='flex items-start w-full sm:w-auto'> compact
<div className='w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4 flex-shrink-0'> ? ''
<IconShield : 'rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/5 dark:bg-white/[0.02]'
size='large' }
className='text-slate-600 dark:text-slate-300' >
/> <div className='flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between'>
<div className='flex min-w-0 items-start gap-3'>
<div className='flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400'>
<ScanFace size={18} />
</div> </div>
<div className='flex-1'> <div className='min-w-0 flex-1'>
<div className='flex items-center gap-2 mb-1'> <div className='mb-1 flex items-center gap-2'>
<Typography.Title heading={6} className='mb-0'> <span className='text-sm font-semibold text-slate-900 dark:text-white'>
{t('两步验证设置')} {t('两步验证设置')}
</Typography.Title> </span>
{status.enabled ? ( {status.enabled ? (
<Tag color='green' shape='circle' size='small'> <Tag color='green' shape='circle' size='small'>
{t('已启用')} {t('已启用')}
@ -395,23 +399,24 @@ const TwoFASetting = ({ t }) => {
</Tag> </Tag>
)} )}
</div> </div>
<Typography.Text type='tertiary' className='text-sm'> <Typography.Text
type='tertiary'
className='text-xs text-slate-400 dark:text-white/40'
>
{t( {t(
'两步验证2FA为您的账户提供额外的安全保护。启用后登录时需要输入密码和验证器应用生成的验证码。', '两步验证2FA为您的账户提供额外的安全保护。启用后登录时需要输入密码和验证器应用生成的验证码。',
)} )}
</Typography.Text> </Typography.Text>
{status.enabled && ( {status.enabled && (
<div className='mt-2'> <div className='mt-2 text-xs text-slate-400 dark:text-white/40'>
<Text size='small' type='secondary'> {t('剩余备用码:')}
{t('剩余备用码:')} {status.backup_codes_remaining || 0}
{status.backup_codes_remaining || 0} {t('个')}
{t('个')}
</Text>
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className='flex flex-col space-y-2 w-full sm:w-auto'> <div className='flex w-full flex-col gap-2 sm:w-auto sm:self-center'>
{!status.enabled ? ( {!status.enabled ? (
<Button <Button
type='primary' type='primary'
@ -419,19 +424,19 @@ const TwoFASetting = ({ t }) => {
size='default' size='default'
onClick={handleSetup2FA} onClick={handleSetup2FA}
loading={loading} loading={loading}
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700' className='!rounded-lg'
icon={<IconShield />} icon={<ScanFace />}
> >
{t('启用验证')} {t('启用验证')}
</Button> </Button>
) : ( ) : (
<div className='flex flex-col space-y-2'> <div className='flex flex-col gap-2'>
<Button <Button
type='danger' type='danger'
theme='solid' theme='solid'
size='default' size='default'
onClick={() => setDisableModalVisible(true)} onClick={() => setDisableModalVisible(true)}
className='!rounded-lg !bg-slate-500 hover:!bg-slate-600' className='!rounded-lg'
icon={<IconAlertTriangle />} icon={<IconAlertTriangle />}
> >
{t('禁用两步验证')} {t('禁用两步验证')}
@ -450,7 +455,7 @@ const TwoFASetting = ({ t }) => {
)} )}
</div> </div>
</div> </div>
</Card> </div>
{/* 2FA设置模态框 */} {/* 2FA设置模态框 */}
<Modal <Modal

View File

@ -18,211 +18,154 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from 'react';
import { Avatar, Card, Tag, Divider, Typography } from '@douyinfe/semi-ui'; import { Tag } from '@douyinfe/semi-ui';
import {
Coins,
BarChart2,
Users,
Crown,
ShieldCheck,
User as UserIcon,
Store,
} from 'lucide-react';
import { import {
isRoot, isRoot,
isAdmin, isAdmin,
isDistributor, isDistributor,
renderQuota, renderQuota,
stringToColor,
} from '../../../../helpers'; } from '../../../../helpers';
import { Coins, BarChart2, Users } from 'lucide-react';
const iconWrap =
'flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70';
const roleAccent = {
root: 'bg-amber-50 text-amber-600 dark:bg-amber-500/10 dark:text-amber-400',
admin:
'bg-violet-50 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400',
distributor:
'bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400',
user: 'bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70',
};
const roleLabelKey = {
root: '超级管理员',
admin: '管理员',
distributor: '代理',
user: '普通用户',
};
const UserInfoHeader = ({ t, userState }) => { const UserInfoHeader = ({ t, userState }) => {
const getUsername = () => { const username = userState.user?.username || 'null';
if (userState.user) { const roleKey = isRoot()
return userState.user.username; ? 'root'
} else { : isAdmin()
return 'null'; ? 'admin'
} : isDistributor()
}; ? 'distributor'
: 'user';
const RoleIcon =
roleKey === 'root'
? Crown
: roleKey === 'admin'
? ShieldCheck
: roleKey === 'distributor'
? Store
: UserIcon;
const getAvatarText = () => { const stats = [
const username = getUsername(); {
if (username && username.length > 0) { key: 'quota',
return username.slice(0, 2).toUpperCase(); label: t('当前余额'),
} value: renderQuota(userState?.user?.quota),
return 'NA'; accent:
}; 'bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400',
icon: Coins,
},
{
key: 'used',
label: t('历史消耗'),
value: renderQuota(userState?.user?.used_quota),
accent:
'bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400',
icon: BarChart2,
},
{
key: 'group',
label: t('用户分组'),
value: userState?.user?.group || t('默认'),
accent:
'bg-violet-50 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400',
icon: Users,
},
];
return ( return (
<Card <div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
className='!rounded-2xl overflow-hidden' {/* 头部:头像 + 用户名 + 角色 */}
cover={ <div className='flex items-center gap-4'>
<div <div
className='relative h-32' className={`flex h-14 w-14 items-center justify-center rounded-2xl text-base font-semibold ${roleAccent[roleKey]}`}
style={{
'--palette-primary-darkerChannel': '0 75 80',
backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
> >
{/* 用户信息内容 */} {username.slice(0, 2).toUpperCase()}
<div className='relative z-10 h-full flex flex-col justify-end p-6'> </div>
<div className='flex items-center'> <div className='min-w-0 flex-1'>
<div className='flex items-stretch gap-3 sm:gap-4 flex-1 min-w-0'> <div className='flex flex-wrap items-center gap-2'>
<Avatar size='large' color={stringToColor(getUsername())}> <h1 className='truncate text-xl font-semibold tracking-tight text-slate-900 dark:text-white'>
{getAvatarText()} {username}
</Avatar> </h1>
<div className='flex-1 min-w-0 flex flex-col justify-between'> <Tag
<div shape='circle'
className='text-3xl font-bold truncate' size='small'
style={{ color: 'white' }} className='!rounded-full'
> style={{
{getUsername()} color: 'inherit',
</div> backgroundColor: 'transparent',
<div className='flex flex-wrap items-center gap-2'> border: 'none',
{isRoot() ? ( padding: 0,
<Tag }}
size='large' >
shape='circle' <span
style={{ color: 'white' }} className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs ${roleAccent[roleKey]}`}
> >
{t('超级管理员')} <RoleIcon size={12} />
</Tag> {t(roleLabelKey[roleKey])}
) : isAdmin() ? ( </span>
<Tag </Tag>
size='large' <span className='text-xs text-slate-400 dark:text-white/40 tabular-nums'>
shape='circle' ID: {userState?.user?.id}
style={{ color: 'white' }} </span>
> </div>
{t('管理员')} <p className='mt-1 text-xs text-slate-400 dark:text-white/40'>
</Tag> {t('账户概览与使用统计')}
) : isDistributor() ? ( </p>
<Tag </div>
size='large' </div>
shape='circle'
style={{ color: 'white' }} {/* 数据三宫格 */}
> <div className='mt-6 grid grid-cols-1 gap-3 border-t border-slate-100 pt-6 sm:grid-cols-3 dark:border-white/5'>
{t('代理')} {stats.map((s) => {
</Tag> const Icon = s.icon;
) : ( return (
<Tag <div
size='large' key={s.key}
shape='circle' className='flex items-center gap-3 rounded-xl border border-slate-100 bg-white px-4 py-3 dark:border-white/5 dark:bg-white/[0.02]'
style={{ color: 'white' }} >
> <div
{t('普通用户')} className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl ${s.accent}`}
</Tag> >
)} <Icon size={18} />
<Tag size='large' shape='circle' style={{ color: 'white' }}> </div>
ID: {userState?.user?.id} <div className='min-w-0'>
</Tag> <div className='text-xs text-slate-400 dark:text-white/40'>
</div> {s.label}
</div>
<div className='truncate text-base font-semibold tabular-nums text-slate-900 dark:text-white'>
{s.value}
</div> </div>
</div> </div>
</div> </div>
</div> );
</div> })}
}
>
{/* 当前余额和桌面版统计信息(勿用 Badge 展示长文案,会遮挡数字并挤压布局) */}
<div className='flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6'>
{/* 当前余额:标签在上、数字在下,避免与右侧统计区重叠 */}
<div className='min-w-0 flex-1'>
<Typography.Text
type='tertiary'
size='small'
className='!block mb-1.5'
>
{t('当前余额')}
</Typography.Text>
<div className='text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide tabular-nums break-words text-[var(--semi-color-text-0)]'>
{renderQuota(userState?.user?.quota)}
</div>
</div>
{/* 桌面版统计信息Semi UI 卡片) */}
<div className='hidden min-w-0 shrink-0 lg:block'>
<Card
size='small'
className='!rounded-xl max-w-full'
bodyStyle={{ padding: '12px 16px' }}
>
<div className='flex flex-wrap items-center gap-x-4 gap-y-2'>
<div className='flex items-center gap-2'>
<Coins size={16} />
<Typography.Text size='small' type='tertiary'>
{t('历史消耗')}
</Typography.Text>
<Typography.Text size='small' type='tertiary' strong>
{renderQuota(userState?.user?.used_quota)}
</Typography.Text>
</div>
<Divider layout='vertical' />
<div className='flex items-center gap-2'>
<BarChart2 size={16} />
<Typography.Text size='small' type='tertiary'>
{t('请求次数')}
</Typography.Text>
<Typography.Text size='small' type='tertiary' strong>
{userState.user?.request_count || 0}
</Typography.Text>
</div>
<Divider layout='vertical' />
<div className='flex items-center gap-2'>
<Users size={16} />
<Typography.Text size='small' type='tertiary'>
{t('用户分组')}
</Typography.Text>
<Typography.Text size='small' type='tertiary' strong>
{userState?.user?.group || t('默认')}
</Typography.Text>
</div>
</div>
</Card>
</div>
</div> </div>
</div>
{/* 移动端和中等屏幕统计信息卡片 */}
<div className='lg:hidden mt-2'>
<Card
size='small'
className='!rounded-xl'
bodyStyle={{ padding: '12px 16px' }}
>
<div className='space-y-3'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Coins size={16} />
<Typography.Text size='small' type='tertiary'>
{t('历史消耗')}
</Typography.Text>
</div>
<Typography.Text size='small' type='tertiary' strong>
{renderQuota(userState?.user?.used_quota)}
</Typography.Text>
</div>
<Divider margin='8px' />
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<BarChart2 size={16} />
<Typography.Text size='small' type='tertiary'>
{t('请求次数')}
</Typography.Text>
</div>
<Typography.Text size='small' type='tertiary' strong>
{userState.user?.request_count || 0}
</Typography.Text>
</div>
<Divider margin='8px' />
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Users size={16} />
<Typography.Text size='small' type='tertiary'>
{t('用户分组')}
</Typography.Text>
</div>
<Typography.Text size='small' type='tertiary' strong>
{userState?.user?.group || t('默认')}
</Typography.Text>
</div>
</div>
</Card>
</div>
</Card>
); );
}; };

View File

@ -296,7 +296,7 @@ export default function ChannelImportModal({ refresh }) {
/> />
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<Text type='quaternary' size='small'> <Text type='quaternary' size='small'>
{t('此密钥将作为所有建站渠道的 API Key用于访问上游平台。请在目标平台的令牌管理页面创建获取。')} {t('此密钥将作为所有建站渠道的 API Key用于访问上游平台。请在目标平台的API Key页面创建获取。')}
</Text> </Text>
</div> </div>
</div> </div>

View File

@ -0,0 +1,211 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Search } from 'lucide-react';
import PricingCardView from '../view/card/PricingCardView';
const TYPE_TABS = [
{ key: 'all', labelKey: '全部' },
{ key: 'text', labelKey: '文本' },
{ key: 'image', labelKey: '图片' },
{ key: 'audio', labelKey: '音频' },
{ key: 'video', labelKey: '视频' },
];
const TAB_TO_TAG = {
all: 'all',
text: '文本',
image: '图片',
audio: '音频',
video: '视频',
};
const ModelsContent = ({
filteredModels,
models,
filterTag, setFilterTag,
filterVendor, setFilterVendor,
filterSupplierType, setFilterSupplierType,
searchValue, setSearchValue,
loading,
isMobile,
blurPricing,
t,
...cardProps
}) => {
const activeTabKey = filterTag === 'all' ? 'all'
: (Object.entries(TAB_TO_TAG).find(([, v]) => v === filterTag)?.[0] || 'all');
// Get unique vendors for filter pills
const vendors = React.useMemo(() => {
if (!models) return [];
const names = [...new Set(models.map((m) => m.vendor_name).filter(Boolean))].sort();
return names;
}, [models]);
// Get unique supplier types
const supplierTypes = React.useMemo(() => {
if (!models) return [];
const types = new Set();
models.forEach((m) => {
(m.channel_list || []).forEach((ch) => {
if (ch.supplier_type) types.add(ch.supplier_type);
});
});
return [...types].sort();
}, [models]);
return (
<div className='sf-pricing-inner'>
{/* Floating Particles */}
<div className='sf-particles'>
{Array.from({ length: 8 }, (_, i) => (
<div key={i} className='sf-particle' />
))}
</div>
{/* Hero Section */}
<div className='sf-hero'>
<h1 className='sf-hero-title'>
{t('探索最适合的 AI 模型')}
</h1>
<p className='sf-hero-subtitle'>
{t('为您的应用找到最优质的模型与服务')}
</p>
{/* Search Bar */}
<div className='sf-search-bar'>
<Search size={16} className='sf-search-icon' />
<input
type='text'
placeholder={t('搜索模型名称、供应商、应用场景...')}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
className='sf-search-input'
/>
</div>
{/* Hot Models Quick Tags */}
{models && models.length > 0 && (
<div className='sf-hot-tags'>
<span className='sf-hot-label'>{t('热门模型')}</span>
{models.slice(0, isMobile ? 4 : 7).map((m) => (
<button
key={m.model_name}
className='sf-hot-tag'
onClick={() => setSearchValue(m.model_name)}
>
{m.model_name}
</button>
))}
</div>
)}
</div>
{/* Filter Section */}
<div className='sf-filter-section'>
{/* Type Filters */}
<div className='sf-filter-row'>
<span className='sf-filter-label'>{t('模型类型')}</span>
<div className='sf-filter-pills'>
{TYPE_TABS.map((tab) => (
<button
key={tab.key}
className={`sf-filter-pill${activeTabKey === tab.key ? ' active' : ''}`}
onClick={() => setFilterTag(TAB_TO_TAG[tab.key])}
>
{t(tab.labelKey)}
</button>
))}
</div>
</div>
{/* Vendor Filters */}
{vendors.length > 0 && (
<div className='sf-filter-row'>
<span className='sf-filter-label'>{t('供应商')}</span>
<div className='sf-filter-pills'>
<button
className={`sf-filter-pill${filterVendor === 'all' ? ' active' : ''}`}
onClick={() => setFilterVendor('all')}
>
{t('全部')}
</button>
{vendors.map((v) => (
<button
key={v}
className={`sf-filter-pill${filterVendor === v ? ' active' : ''}`}
onClick={() => setFilterVendor(filterVendor === v ? 'all' : v)}
>
{v}
</button>
))}
</div>
</div>
)}
{/* Supplier Type Filters */}
{supplierTypes.length > 0 && (
<div className='sf-filter-row'>
<span className='sf-filter-label'>{t('服务商')}</span>
<div className='sf-filter-pills'>
<button
className={`sf-filter-pill${filterSupplierType === 'all' ? ' active' : ''}`}
onClick={() => setFilterSupplierType('all')}
>
{t('全部')}
</button>
{supplierTypes.map((type) => (
<button
key={type}
className={`sf-filter-pill${filterSupplierType === type ? ' active' : ''}`}
onClick={() => setFilterSupplierType(filterSupplierType === type ? 'all' : type)}
>
{type}
</button>
))}
</div>
</div>
)}
</div>
{/* Results Header */}
<div className='sf-results-header'>
<h2 className='sf-results-title'>
{t('为您找到')} <span className='sf-results-count'>{filteredModels?.length || 0}</span> {t('个模型')}
</h2>
</div>
{/* Model Cards Grid */}
<div className='sf-cards-container'>
<PricingCardView
filteredModels={filteredModels}
loading={loading}
blurPricing={blurPricing}
t={t}
gridCols={isMobile ? 1 : 3}
{...cardProps}
/>
</div>
</div>
);
};
export default ModelsContent;

View File

@ -0,0 +1,151 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { usePricingFilterCounts } from '../../../../hooks/model-pricing/usePricingFilterCounts';
import { getLobeHubIcon } from '../../../../helpers';
const ModelsSidebar = ({
models,
filterVendor, setFilterVendor,
filterSupplierType, setFilterSupplierType,
filterTag, setFilterTag,
filterGroup, filterQuotaType, filterEndpointType, searchValue,
loading, t,
}) => {
const { vendorModels, tagModels, supplierTypeModels } = usePricingFilterCounts({
models,
filterGroup,
filterQuotaType,
filterEndpointType,
filterVendor,
filterTag,
filterSupplierType,
searchValue,
});
// (Vendors)
const vendors = React.useMemo(() => {
const names = [...new Set(models.map((m) => m.vendor_name).filter(Boolean))].sort();
return names.map((name) => {
const icon = models.find((m) => m.vendor_name === name)?.vendor_icon;
return {
name,
icon,
count: vendorModels.filter((m) => m.vendor_name === name).length,
};
});
}, [models, vendorModels]);
// (Supplier Types)
const supplierTypes = React.useMemo(() => {
const types = new Set();
models.forEach((m) => {
(m.channel_list || []).forEach((ch) => {
if (ch.supplier_type) types.add(ch.supplier_type);
});
});
return [...types].sort();
}, [models]);
// (Tags)
const TAG_ORDER = ['文本', '图片', '音频', '视频'];
const tags = React.useMemo(() => {
const allTags = new Set();
models.forEach((m) => {
if (m.tags) {
String(m.tags).split(/[,;|]/).map(s => s.trim()).filter(Boolean).forEach(t => allTags.add(t));
}
});
// 使
const ordered = TAG_ORDER.filter(t => allTags.has(t));
const rest = [...allTags].filter(t => !TAG_ORDER.includes(t));
return [...ordered, ...rest];
}, [models]);
const FilterItem = ({ label, count, active, onClick }) => (
<div
className={`models-filter-item${active ? ' active' : ''}`}
onClick={onClick}
>
<span>{label}</span>
<span className='count'>{count}</span>
</div>
);
return (
<div>
{/* 作者 */}
<div className='models-filter-section'>
<div className='models-filter-title'>{t('作者')}</div>
<FilterItem label={t('全部')} count={models.length} active={filterVendor === 'all'} onClick={() => setFilterVendor('all')} />
{vendors.map((v) => (
<FilterItem
key={v.name}
label={
<span className='flex items-center gap-2'>
{v.icon && (
<img
src={getLobeHubIcon(v.icon)}
alt=''
className='w-4 h-4 rounded-full'
onError={(e) => { e.target.style.display = 'none'; }}
/>
)}
<span>{v.name}</span>
</span>
}
count={v.count}
active={filterVendor === v.name}
onClick={() => setFilterVendor(filterVendor === v.name ? 'all' : v.name)}
/>
))}
</div>
{/* 服务商 */}
<div className='models-filter-section'>
<div className='models-filter-title'>{t('服务商')}</div>
<FilterItem label={t('全部')} count={models.length} active={filterSupplierType === 'all'} onClick={() => setFilterSupplierType('all')} />
{supplierTypes.map((type) => (
<FilterItem
key={type}
label={type}
count={supplierTypeModels.filter((m) =>
(m.channel_list || []).some((ch) => ch.supplier_type === type)
).length}
active={filterSupplierType === type}
onClick={() => setFilterSupplierType(filterSupplierType === type ? 'all' : type)}
/>
))}
</div>
{/* 输入模态 */}
<div className='models-filter-section'>
<div className='models-filter-title'>{t('输入模态')}</div>
<FilterItem label={t('全部')} count={models.length} active={filterTag === 'all'} onClick={() => setFilterTag('all')} />
{tags.map((tag) => (
<FilterItem
key={tag}
label={tag}
count={tagModels.filter((m) =>
m.tags && String(m.tags).split(/[,;|]/).map(s => s.trim()).includes(tag)
).length}
active={filterTag === tag}
onClick={() => setFilterTag(filterTag === tag ? 'all' : tag)}
/>
))}
</div>
</div>
);
};
export default ModelsSidebar;

View File

@ -18,9 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useContext, useMemo } from 'react'; import React, { useContext, useMemo } from 'react';
import { Layout, ImagePreview } from '@douyinfe/semi-ui'; import { ImagePreview } from '@douyinfe/semi-ui';
import PricingSidebar from './PricingSidebar'; import ModelsContent from './ModelsContent';
import PricingContent from './content/PricingContent';
import ModelDetailSideSheet from '../modal/ModelDetailSideSheet'; import ModelDetailSideSheet from '../modal/ModelDetailSideSheet';
import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData'; import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData';
import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { useIsMobile } from '../../../../hooks/common/useIsMobile';
@ -29,7 +28,6 @@ import { UserContext } from '../../../../context/User';
const PricingPage = () => { const PricingPage = () => {
const pricingData = useModelPricingData(); const pricingData = useModelPricingData();
const { Sider, Content } = Layout;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [showRatio, setShowRatio] = React.useState(false); const [showRatio, setShowRatio] = React.useState(false);
const [viewMode, setViewMode] = React.useState('card'); const [viewMode, setViewMode] = React.useState('card');
@ -60,22 +58,12 @@ const PricingPage = () => {
}; };
return ( return (
<div className='bg-white'> <div className='sf-pricing-page'>
<Layout className='pricing-layout'> <ModelsContent
{!isMobile && ( {...allProps}
<Sider className='pricing-scroll-hide pricing-sidebar'> isMobile={isMobile}
<PricingSidebar {...allProps} /> sidebarProps={allProps}
</Sider> />
)}
<Content className='pricing-scroll-hide pricing-content'>
<PricingContent
{...allProps}
isMobile={isMobile}
sidebarProps={allProps}
/>
</Content>
</Layout>
<ImagePreview <ImagePreview
src={pricingData.modalImageUrl} src={pricingData.modalImageUrl}

View File

@ -18,7 +18,6 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from 'react';
import { Button } from '@douyinfe/semi-ui';
import PricingGroups from '../filter/PricingGroups'; import PricingGroups from '../filter/PricingGroups';
import PricingQuotaTypes from '../filter/PricingQuotaTypes'; import PricingQuotaTypes from '../filter/PricingQuotaTypes';
import PricingEndpointTypes from '../filter/PricingEndpointTypes'; import PricingEndpointTypes from '../filter/PricingEndpointTypes';
@ -99,17 +98,22 @@ const PricingSidebar = ({
}); });
return ( return (
<div className='p-2'> <div className='px-4 py-5'>
<div className='flex items-center justify-between mb-6'> <div className='mb-5 flex items-center justify-between border-b border-slate-100 pb-4 dark:border-white/5'>
<div className='text-lg font-semibold text-gray-800'>{t('筛选')}</div> <div>
<Button <div className='text-sm font-semibold tracking-tight text-slate-900 dark:text-white'>
theme='outline' {t('筛选')}
type='tertiary' </div>
<div className='mt-0.5 text-[11px] text-slate-400 tabular-nums dark:text-white/40'>
调整左侧条件实时刷新结果
</div>
</div>
<button
onClick={handleResetFilters} onClick={handleResetFilters}
className='text-gray-500 hover:text-gray-700' className='inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-[11px] font-medium text-slate-500 transition-colors hover:border-slate-300 hover:text-slate-900 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/60 dark:hover:border-white/20 dark:hover:text-white'
> >
{t('重置')} {t('重置')}
</Button> </button>
</div> </div>
<PricingVendors <PricingVendors

View File

@ -18,49 +18,32 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useState, useEffect, useMemo, useCallback, memo } from 'react'; import React, { useState, useEffect, useMemo, useCallback, memo } from 'react';
import { import { Tooltip, Modal } from '@douyinfe/semi-ui';
Card, import { Layers } from 'lucide-react';
Tag,
Avatar,
Typography,
Tooltip,
Modal,
} from '@douyinfe/semi-ui';
import { getLobeHubIcon } from '../../../../../helpers'; import { getLobeHubIcon } from '../../../../../helpers';
import SearchActions from './SearchActions'; import SearchActions from './SearchActions';
const { Paragraph } = Typography;
const CONFIG = { const CONFIG = {
CAROUSEL_INTERVAL: 2000, CAROUSEL_INTERVAL: 2000,
ICON_SIZE: 40, ICON_SIZE: 32,
UNKNOWN_VENDOR: 'unknown', UNKNOWN_VENDOR: 'unknown',
}; };
const THEME_COLORS = { const THEME = {
allVendors: { allVendors: {
primary: '37 99 235', iconWrap:
background: 'rgba(59, 130, 246, 0.08)', 'bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70',
accent: 'text-slate-600 dark:text-slate-300',
badge: 'bg-slate-100 text-slate-700 dark:bg-white/5 dark:text-slate-300',
}, },
specific: { specific: {
primary: '16 185 129', iconWrap:
background: 'rgba(16, 185, 129, 0.1)', 'bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400',
accent: 'text-emerald-600 dark:text-emerald-400',
badge: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300',
}, },
}; };
const COMPONENT_STYLES = {
tag: {
backgroundColor: 'rgba(255,255,255,0.95)',
color: '#1f2937',
border: '1px solid rgba(255,255,255,0.8)',
fontWeight: '500',
},
avatarContainer:
'w-16 h-16 rounded-2xl bg-white/90 shadow-md backdrop-blur-sm flex items-center justify-center',
titleText: { color: 'white' },
descriptionText: { color: 'rgba(255,255,255,0.9)' },
};
const CONTENT_TEXTS = { const CONTENT_TEXTS = {
unknown: { unknown: {
displayName: (t) => t('未知模型类型'), displayName: (t) => t('未知模型类型'),
@ -85,35 +68,32 @@ const getVendorDisplayName = (vendorName, t) => {
}; };
const createDefaultAvatar = () => ( const createDefaultAvatar = () => (
<div className={COMPONENT_STYLES.avatarContainer}> <div className='flex h-12 w-12 items-center justify-center rounded-xl bg-slate-100 text-slate-500 dark:bg-white/5 dark:text-white/60'>
<Avatar size='large' color='transparent'> <Layers size={22} />
AI
</Avatar>
</div> </div>
); );
const getAvatarBackgroundColor = (isAllVendors) =>
isAllVendors
? THEME_COLORS.allVendors.background
: THEME_COLORS.specific.background;
const getAvatarText = (vendorName) => const getAvatarText = (vendorName) =>
vendorName === CONFIG.UNKNOWN_VENDOR vendorName === CONFIG.UNKNOWN_VENDOR
? '?' ? '?'
: (vendorName || '').charAt(0).toUpperCase() || '?'; : (vendorName || '').charAt(0).toUpperCase() || '?';
const createAvatarContent = (vendor, isAllVendors) => { const createAvatarContent = (vendor, iconWrapClass) => {
if (vendor.icon) { if (vendor.icon) {
return getLobeHubIcon(vendor.icon, CONFIG.ICON_SIZE); return (
<div
className={`flex h-12 w-12 items-center justify-center rounded-xl ${iconWrapClass}`}
>
{getLobeHubIcon(vendor.icon, CONFIG.ICON_SIZE)}
</div>
);
} }
return ( return (
<Avatar <div
size='large' className={`flex h-12 w-12 items-center justify-center rounded-xl font-mono text-lg font-semibold ${iconWrapClass}`}
style={{ backgroundColor: getAvatarBackgroundColor(isAllVendors) }}
> >
{getAvatarText(vendor.name)} {getAvatarText(vendor.name)}
</Avatar> </div>
); );
}; };
@ -123,11 +103,14 @@ const renderVendorAvatar = (vendor, t, isAllVendors = false) => {
} }
const displayName = getVendorDisplayName(vendor.name, t); const displayName = getVendorDisplayName(vendor.name, t);
const avatarContent = createAvatarContent(vendor, isAllVendors); const iconWrapClass = isAllVendors
? THEME.allVendors.iconWrap
: THEME.specific.iconWrap;
const avatarContent = createAvatarContent(vendor, iconWrapClass);
return ( return (
<Tooltip content={displayName} position='top'> <Tooltip content={displayName} position='top'>
<div className={COMPONENT_STYLES.avatarContainer}>{avatarContent}</div> {avatarContent}
</Tooltip> </Tooltip>
); );
}; };
@ -259,17 +242,6 @@ const PricingVendorIntro = memo(
[vendorInfo, t], [vendorInfo, t],
); );
const createCoverStyle = useCallback(
(primaryColor) => ({
'--palette-primary-darkerChannel': primaryColor,
backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}),
[],
);
const renderSearchActions = useCallback( const renderSearchActions = useCallback(
() => ( () => (
<SearchActions <SearchActions
@ -318,51 +290,41 @@ const PricingVendorIntro = memo(
); );
const renderHeaderCard = useCallback( const renderHeaderCard = useCallback(
({ title, count, description, rightContent, primaryDarkerChannel }) => ( ({ title, count, description, rightContent, theme }) => (
<Card <div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
className='!rounded-2xl shadow-sm border-0' <div className='flex items-start gap-4'>
cover={ <div className='min-w-0 flex-1'>
<div <div className='mb-1.5 flex flex-wrap items-center gap-2'>
className='relative h-full' <h2 className='truncate text-lg font-semibold tracking-tight text-slate-900 sm:text-xl dark:text-white'>
style={createCoverStyle(primaryDarkerChannel)} {title}
> </h2>
<div className='relative z-10 h-full flex items-center justify-between p-4'> <span
<div className='flex-1 min-w-0 mr-4'> className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-[11px] font-medium ${theme.badge}`}
<div className='flex flex-row flex-wrap items-center gap-2 sm:gap-3 mb-2'> >
<h2 {t('共 {{count}} 个模型', { count })}
className='text-lg sm:text-xl font-bold truncate' </span>
style={COMPONENT_STYLES.titleText}
>
{title}
</h2>
<Tag
style={COMPONENT_STYLES.tag}
shape='circle'
size='small'
className='self-center'
>
{t('共 {{count}} 个模型', { count })}
</Tag>
</div>
<Paragraph
className='text-xs sm:text-sm leading-relaxed !mb-0 cursor-pointer'
style={COMPONENT_STYLES.descriptionText}
ellipsis={{ rows: 2 }}
onClick={() => handleOpenDescModal(description)}
>
{description}
</Paragraph>
</div>
<div className='flex-shrink-0'>{rightContent}</div>
</div> </div>
<p
className='cursor-pointer text-xs leading-relaxed text-slate-500 sm:text-sm dark:text-white/50'
onClick={() => handleOpenDescModal(description)}
style={{
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{description}
</p>
</div> </div>
} <div className='flex shrink-0 items-center'>{rightContent}</div>
> </div>
{renderSearchActions()} <div className='mt-5 border-t border-slate-100 pt-4 dark:border-white/5'>
</Card> {renderSearchActions()}
</div>
</div>
), ),
[renderSearchActions, createCoverStyle, handleOpenDescModal, t], [renderSearchActions, handleOpenDescModal, t],
); );
const renderAllVendorsAvatar = useCallback(() => { const renderAllVendorsAvatar = useCallback(() => {
@ -379,7 +341,7 @@ const PricingVendorIntro = memo(
count: currentModelCount, count: currentModelCount,
description: getVendorDescription('all'), description: getVendorDescription('all'),
rightContent: renderAllVendorsAvatar(), rightContent: renderAllVendorsAvatar(),
primaryDarkerChannel: THEME_COLORS.allVendors.primary, theme: THEME.allVendors,
}); });
return ( return (
<> <>
@ -402,7 +364,7 @@ const PricingVendorIntro = memo(
description: description:
currentVendor.description || getVendorDescription(currentVendor.name), currentVendor.description || getVendorDescription(currentVendor.name),
rightContent: renderVendorAvatar(currentVendor, t, false), rightContent: renderVendorAvatar(currentVendor, t, false),
primaryDarkerChannel: THEME_COLORS.specific.primary, theme: THEME.specific,
}); });
return ( return (

View File

@ -65,8 +65,8 @@ const SearchActions = memo(
}, [tokenUnit, setTokenUnit]); }, [tokenUnit, setTokenUnit]);
return ( return (
<div className='flex items-center gap-2 w-full'> <div className='flex flex-wrap items-center gap-2 w-full'>
<div className='flex-1'> <div className='min-w-[180px] flex-1'>
<Input <Input
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('模糊搜索模型名称')} placeholder={t('模糊搜索模型名称')}
@ -80,11 +80,10 @@ const SearchActions = memo(
<Button <Button
theme='outline' theme='outline'
type='primary' type='tertiary'
icon={<IconCopy />} icon={<IconCopy />}
onClick={handleCopyClick} onClick={handleCopyClick}
disabled={selectedRowKeys.length === 0} disabled={selectedRowKeys.length === 0}
className='!bg-blue-500 hover:!bg-blue-600 !text-white disabled:!bg-gray-300 disabled:!text-gray-500'
> >
{t('复制')} {t('复制')}
</Button> </Button>
@ -96,7 +95,7 @@ const SearchActions = memo(
{/* 充值价格显示开关 */} {/* 充值价格显示开关 */}
{supportsCurrencyDisplay && ( {supportsCurrencyDisplay && (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<span className='text-sm text-gray-600'> <span className='text-sm text-slate-500 dark:text-white/50'>
{t('充值价格显示')} {t('充值价格显示')}
</span> </span>
<Switch <Switch
@ -121,7 +120,7 @@ const SearchActions = memo(
{/* 显示倍率开关 */} {/* 显示倍率开关 */}
{/* <div className='flex items-center gap-2'> {/* <div className='flex items-center gap-2'>
<span className='text-sm text-gray-600'>{t('倍率')}</span> <span className='text-sm text-slate-500 dark:text-white/50'>{t('倍率')}</span>
<Switch checked={showRatio} onChange={setShowRatio} /> <Switch checked={showRatio} onChange={setShowRatio} />
</div> */} </div> */}

Some files were not shown because too many files have changed in this diff Show More