Compare commits
No commits in common. "c5884ac6337b040a5203ec3b6dda2aea4c7831e8" and "14749171637a52e9be1530ae2962c3041c052cca" have entirely different histories.
c5884ac633
...
1474917163
|
|
@ -10,6 +10,7 @@ upload
|
|||
build
|
||||
*.db-journal
|
||||
logs
|
||||
web/vite.config.js
|
||||
web/dist
|
||||
.env
|
||||
one-api
|
||||
|
|
|
|||
509
app_backup.jsx
509
app_backup.jsx
|
|
@ -1,509 +0,0 @@
|
|||
/*
|
||||
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);
|
||||
|
||||
// 处理向后兼容性:如果pricing是boolean,默认不需要登录
|
||||
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.
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
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 SystemName = "TuringToken"
|
||||
var SystemName = "TokenFactory"
|
||||
var Footer = ""
|
||||
var Logo = ""
|
||||
var TopUpLink = ""
|
||||
|
|
|
|||
|
|
@ -645,11 +645,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
|||
quota = 1
|
||||
}
|
||||
} else {
|
||||
quota = int(math.Round(priceData.ModelPrice * common.QuotaPerUnit))
|
||||
// 有有效定价但舍入为 0 时保底为 1
|
||||
if quota == 0 && priceData.ModelPrice > 0 {
|
||||
quota = 1
|
||||
}
|
||||
quota = int(priceData.ModelPrice * common.QuotaPerUnit)
|
||||
}
|
||||
tok := time.Now()
|
||||
milliseconds := tok.Sub(tik).Milliseconds()
|
||||
|
|
|
|||
|
|
@ -1348,7 +1348,7 @@ func AddChannel(c *gin.Context) {
|
|||
if addChannelRequest.Channel != nil {
|
||||
channelName = addChannelRequest.Channel.Name
|
||||
}
|
||||
service.RecordCreateOperation(c, "channel", 0, channelName, "创建渠道:"+channelName, "")
|
||||
service.RecordCreateOperation(c, "channel", 0, channelName, "创建渠道: "+channelName, "")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
|
@ -1366,7 +1366,7 @@ func DeleteChannel(c *gin.Context) {
|
|||
}
|
||||
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{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
|
@ -1721,7 +1721,7 @@ func UpdateChannel(c *gin.Context) {
|
|||
channel.Key = ""
|
||||
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{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
|
|
|||
|
|
@ -117,8 +117,6 @@ func GetStatus(c *gin.Context) {
|
|||
|
||||
// 模块管理配置
|
||||
"HeaderNavModules": common.OptionMap["HeaderNavModules"],
|
||||
"HeaderShowThemeToggle": common.OptionMap["HeaderShowThemeToggle"] != "false",
|
||||
"HeaderShowLanguageSelector": common.OptionMap["HeaderShowLanguageSelector"] != "false",
|
||||
"SidebarModulesByRole": common.OptionMap["SidebarModulesByRole"],
|
||||
|
||||
"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ func CreateModelMeta(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -319,7 +319,7 @@ func UpdateModelMeta(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -336,7 +336,7 @@ func DeleteModelMeta(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -690,10 +690,10 @@ func UpdateOption(c *gin.Context) {
|
|||
if modelPricingOptionKeys[option.Key] {
|
||||
changes := computeModelPricingDiff(oldVal, valStr)
|
||||
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 {
|
||||
service.RecordUpdateOperation(c, "setting", 0, option.Key, "更新系统设置:"+option.Key, "")
|
||||
service.RecordUpdateOperation(c, "setting", 0, option.Key, "更新系统设置: "+option.Key, "")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func AddRedemption(c *gin.Context) {
|
|||
}
|
||||
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{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
|
@ -122,7 +122,7 @@ func DeleteRedemption(c *gin.Context) {
|
|||
common.ApiError(c, err)
|
||||
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{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
|
@ -161,7 +161,7 @@ func UpdateRedemption(c *gin.Context) {
|
|||
common.ApiError(c, err)
|
||||
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{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ func AddToken(c *gin.Context) {
|
|||
common.ApiError(c, err)
|
||||
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{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
|
@ -243,7 +243,7 @@ func DeleteToken(c *gin.Context) {
|
|||
common.ApiError(c, err)
|
||||
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{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
|
@ -308,7 +308,7 @@ func UpdateToken(c *gin.Context) {
|
|||
common.ApiError(c, err)
|
||||
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{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
|
|
|||
|
|
@ -1206,7 +1206,7 @@ func UpdateUser(c *gin.Context) {
|
|||
if originUser.DistributorCommissionBps != 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{
|
||||
"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))
|
||||
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{
|
||||
"success": true,
|
||||
|
|
@ -1421,7 +1421,7 @@ func DeleteUser(c *gin.Context) {
|
|||
})
|
||||
return
|
||||
}
|
||||
service.RecordDeleteOperation(c, "user", id, originUser.Username, "删除用户:"+originUser.Username, "")
|
||||
service.RecordDeleteOperation(c, "user", id, originUser.Username, "删除用户: "+originUser.Username, "")
|
||||
}
|
||||
|
||||
func DeleteSelf(c *gin.Context) {
|
||||
|
|
@ -1501,7 +1501,7 @@ func CreateUser(c *gin.Context) {
|
|||
common.ApiError(c, err)
|
||||
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{
|
||||
"success": true,
|
||||
|
|
@ -1781,7 +1781,7 @@ func ManageUser(c *gin.Context) {
|
|||
}
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
import sys
|
||||
print(" hello\)
|
||||
4
fix2.py
4
fix2.py
|
|
@ -1,4 +0,0 @@
|
|||
import sys
|
||||
c=open(sys.argv[1,'r',encoding='utf-8').read()
|
||||
o=c
|
||||
print(len(c))
|
||||
14
fix3.py
14
fix3.py
|
|
@ -1,14 +0,0 @@
|
|||
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 +0,0 @@
|
|||
print(" not using template\)
|
||||
|
|
@ -1 +0,0 @@
|
|||
#
|
||||
|
|
@ -1 +0,0 @@
|
|||
# test
|
||||
26
go.mod
26
go.mod
|
|
@ -14,11 +14,11 @@ require (
|
|||
github.com/aws/smithy-go v1.24.3
|
||||
github.com/bytedance/gopkg v0.1.4
|
||||
github.com/fyinfor/router-engine v0.1.0
|
||||
github.com/gin-contrib/cors v1.5.0
|
||||
github.com/gin-contrib/gzip v1.1.0
|
||||
github.com/gin-contrib/sessions v1.0.1
|
||||
github.com/gin-contrib/static v1.1.3
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/gin-contrib/cors v1.7.7
|
||||
github.com/gin-contrib/gzip v1.2.6
|
||||
github.com/gin-contrib/sessions v1.1.0
|
||||
github.com/gin-contrib/static v1.1.6
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-audio/aiff v1.1.0
|
||||
github.com/go-audio/wav v1.1.0
|
||||
|
|
@ -42,9 +42,6 @@ require (
|
|||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
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/thanhpk/randstr v1.0.6
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
|
|
@ -80,6 +77,7 @@ require (
|
|||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
|
|
@ -101,6 +99,7 @@ require (
|
|||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.3 // 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/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
|
|
@ -135,8 +134,15 @@ require (
|
|||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // 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/russross/blackfriday/v2 v2.0.1 // 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/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
|
|
@ -144,10 +150,13 @@ require (
|
|||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.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/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
|
||||
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/tools v0.44.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
|
|
@ -156,4 +165,5 @@ require (
|
|||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.48.2 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
|
|
|||
229
go.sum
229
go.sum
|
|
@ -1,5 +1,6 @@
|
|||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
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/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
|
||||
|
|
@ -12,26 +13,48 @@ 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/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/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/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/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/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/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/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/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/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/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/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/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/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
|
|
@ -39,16 +62,24 @@ 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.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
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/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/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/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
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.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -63,26 +94,46 @@ 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/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
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/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/go.mod h1:7qMxp7aoYsSDZ7dJgPEGXzORyJU+xwuQ2FuB7NgRs8E=
|
||||
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/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
|
||||
github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
|
||||
github.com/gin-contrib/gzip v1.1.0 h1:kVw7Nr9M+Z6Ch4qo7aGMbiqxDeyQFru+07MgAcUF62M=
|
||||
github.com/gin-contrib/gzip v1.1.0/go.mod h1:iHJXCup4CWiKyPUEl+GwkHjchl+YyYuMKbOCiXujPIA=
|
||||
github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
|
||||
github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
|
||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q=
|
||||
github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
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/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||
github.com/gin-contrib/static v1.1.3 h1:WLOpkBtMDJ3gATFZgNJyVibFMio/UHonnueqJsQ0w4U=
|
||||
github.com/gin-contrib/static v1.1.3/go.mod h1:zejpJ/YWp8cZj/6EpiL5f/+skv5daQTNwRx1E8Pci30=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
||||
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
||||
github.com/gin-contrib/static v1.1.6 h1:4/OIJI9PxO2jsUezNulpVbzI8ORMmdPlJ4P9QGwWgME=
|
||||
github.com/gin-contrib/static v1.1.6/go.mod h1:e9qkj8wAlsxE6mSFGVL/flqGfVibw5amjNEUa4idmHc=
|
||||
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/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/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-audio/aiff v1.1.0 h1:m2LYgu/2BarpF2yZnFPWtY3Tp41k0A4y51gDRZZsEuU=
|
||||
|
|
@ -94,6 +145,7 @@ 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.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
|
||||
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.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
|
|
@ -107,58 +159,86 @@ 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.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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.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/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
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/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/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/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/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/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/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/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/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
|
||||
|
|
@ -167,6 +247,8 @@ 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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
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/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
|
|
@ -179,19 +261,25 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
|||
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/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/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
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/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/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/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
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/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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
|
|
@ -199,6 +287,8 @@ 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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
|
|
@ -206,10 +296,16 @@ 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/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
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/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
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/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/go.mod h1:omNJr4dHOKbrBeoY/idmLDFw8OIdDUUZvj3uB1cJxwA=
|
||||
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 h1:h8O1byDZ1uk6RUXMhj1QJU3VXFKXHDZxr4TXRPGeBa8=
|
||||
|
|
@ -219,10 +315,13 @@ 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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
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/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/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/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||
|
|
@ -236,10 +335,14 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
|||
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/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/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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/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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
|
@ -247,28 +350,50 @@ 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/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
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/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/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/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/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/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/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/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/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/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/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
|
@ -280,6 +405,7 @@ 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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
|
|
@ -300,26 +426,42 @@ 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.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
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.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
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.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
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/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/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
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/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/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/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/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/go.mod h1:IaXVYq6mmYtrLFFsLxPslNwuIZx0mIadWWjhe+eWb0g=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
|
|
@ -329,25 +471,39 @@ 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/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
|
||||
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/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
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/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/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
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/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-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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
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/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.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/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
@ -356,6 +512,8 @@ 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-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.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/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -365,20 +523,27 @@ 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-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-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-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-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-20220722155257-8c9f86f7a55f/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.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/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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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.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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
|
@ -388,9 +553,14 @@ 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-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.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/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-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/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
@ -398,36 +568,45 @@ 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
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/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/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
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-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/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/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
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/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/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/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
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/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
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/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
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/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/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
|
|
@ -438,9 +617,13 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||
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/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/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
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/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=
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 507 KiB |
|
|
@ -222,7 +222,7 @@ func commonActionLabel(action string, targetType string, targetName string) stri
|
|||
}
|
||||
|
||||
if targetName != "" {
|
||||
return actionLabel + typeLabel + ":" + targetName
|
||||
return actionLabel + typeLabel + ": " + targetName
|
||||
}
|
||||
return actionLabel + typeLabel
|
||||
}
|
||||
|
|
|
|||
|
|
@ -272,8 +272,6 @@ func migrateDB() error {
|
|||
if err := migrateTokenModelLimitsToText(); err != nil {
|
||||
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)
|
||||
// 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).
|
||||
|
|
@ -585,28 +583,6 @@ func migrateTokenModelLimitsToText() error {
|
|||
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)
|
||||
// This is safe to run multiple times - it checks the column type first
|
||||
func migrateSubscriptionPlanPriceAmount() {
|
||||
|
|
|
|||
|
|
@ -290,9 +290,6 @@ func InitOptionMap() {
|
|||
common.OptionMap[k] = v
|
||||
}
|
||||
|
||||
common.OptionMap["HeaderShowThemeToggle"] = strconv.FormatBool(true)
|
||||
common.OptionMap["HeaderShowLanguageSelector"] = strconv.FormatBool(true)
|
||||
|
||||
common.OptionMapRWMutex.Unlock()
|
||||
loadOptionsFromDatabase()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
)
|
||||
|
||||
// 简化的供应商映射规则
|
||||
|
|
@ -23,7 +20,6 @@ var defaultVendorRules = map[string]string{
|
|||
"qwen": "阿里巴巴",
|
||||
"deepseek": "DeepSeek",
|
||||
"abab": "MiniMax",
|
||||
"minimax": "MiniMax",
|
||||
"ernie": "百度",
|
||||
"spark": "讯飞",
|
||||
"hunyuan": "腾讯",
|
||||
|
|
@ -36,7 +32,6 @@ var defaultVendorRules = map[string]string{
|
|||
"grok": "xAI",
|
||||
"llama": "Meta",
|
||||
"doubao": "字节跳动",
|
||||
"seedance": "字节跳动",
|
||||
"kling": "快手",
|
||||
"jimeng": "即梦",
|
||||
"vidu": "Vidu",
|
||||
|
|
@ -44,10 +39,10 @@ var defaultVendorRules = map[string]string{
|
|||
|
||||
// 供应商默认图标映射
|
||||
var defaultVendorIcons = map[string]string{
|
||||
"OpenAI": "OpenAI.Color",
|
||||
"OpenAI": "OpenAI",
|
||||
"Anthropic": "Claude.Color",
|
||||
"Google": "Gemini.Color",
|
||||
"Moonshot": "Moonshot.Color",
|
||||
"Moonshot": "Moonshot",
|
||||
"智谱": "Zhipu.Color",
|
||||
"阿里巴巴": "Qwen.Color",
|
||||
"DeepSeek": "DeepSeek.Color",
|
||||
|
|
@ -59,30 +54,25 @@ var defaultVendorIcons = map[string]string{
|
|||
"Cloudflare": "Cloudflare.Color",
|
||||
"360": "Ai360.Color",
|
||||
"零一万物": "Yi.Color",
|
||||
"Jina": "Jina.Color",
|
||||
"Jina": "Jina",
|
||||
"Mistral": "Mistral.Color",
|
||||
"xAI": "XAI.Color",
|
||||
"Ollama": "Ollama.Color",
|
||||
"Meta": "Meta.Color",
|
||||
"xAI": "XAI",
|
||||
"Meta": "Ollama",
|
||||
"字节跳动": "Doubao.Color",
|
||||
"快手": "Kling.Color",
|
||||
"即梦": "Jimeng.Color",
|
||||
"Vidu": "Vidu.Color",
|
||||
"微软": "AzureAI.Color",
|
||||
"Microsoft": "AzureAI.Color",
|
||||
"Azure": "AzureAI.Color",
|
||||
"Vidu": "Vidu",
|
||||
"微软": "AzureAI",
|
||||
"Microsoft": "AzureAI",
|
||||
"Azure": "AzureAI",
|
||||
}
|
||||
|
||||
// initDefaultVendorMapping 简化的默认供应商映射
|
||||
func initDefaultVendorMapping(metaMap map[string]*Model, vendorMap map[int]*Vendor, enableAbilities []AbilityWithChannel) {
|
||||
for _, ability := range enableAbilities {
|
||||
modelName := ability.Model
|
||||
if existing, exists := metaMap[modelName]; exists {
|
||||
// 已有记录但 vendor_id 已赋值 → 跳过
|
||||
if existing.VendorID != 0 {
|
||||
continue
|
||||
}
|
||||
// vendor_id=0 → 继续尝试匹配(修复已有 DB 记录但缺供应商的场景)
|
||||
if _, exists := metaMap[modelName]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配供应商
|
||||
|
|
@ -95,24 +85,12 @@ func initDefaultVendorMapping(metaMap map[string]*Model, vendorMap map[int]*Vend
|
|||
}
|
||||
}
|
||||
|
||||
if vendorID == 0 {
|
||||
// 仍未匹配到供应商 → 跳过(不给未匹配的模型写入 vendor_id=0 覆盖已有记录)
|
||||
if strings.Contains(modelLower, "seedance") || strings.Contains(modelLower, "minimax") {
|
||||
common.SysLog(fmt.Sprintf("initDefaultVendorMapping: model %q matched pattern but vendorID=0 (getOrCreateVendor failed)", modelName))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 写入或更新供应商 ID
|
||||
if existing, exists := metaMap[modelName]; exists {
|
||||
existing.VendorID = vendorID
|
||||
} else {
|
||||
metaMap[modelName] = &Model{
|
||||
ModelName: modelName,
|
||||
VendorID: vendorID,
|
||||
Status: 1,
|
||||
NameRule: NameRuleExact,
|
||||
}
|
||||
// 创建模型元数据
|
||||
metaMap[modelName] = &Model{
|
||||
ModelName: modelName,
|
||||
VendorID: vendorID,
|
||||
Status: 1,
|
||||
NameRule: NameRuleExact,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -134,11 +112,9 @@ func getOrCreateVendor(vendorName string, vendorMap map[int]*Vendor) int {
|
|||
}
|
||||
|
||||
if err := newVendor.Insert(); err != nil {
|
||||
common.SysLog(fmt.Sprintf("getOrCreateVendor: insert vendor %q failed: %v", vendorName, err))
|
||||
return 0
|
||||
}
|
||||
|
||||
common.SysLog(fmt.Sprintf("getOrCreateVendor: created new vendor %q with id=%d", vendorName, newVendor.Id))
|
||||
vendorMap[newVendor.Id] = newVendor
|
||||
return newVendor.Id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,11 +172,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
|||
effModelPrice = effModelPrice * meta.ImagePriceRatio
|
||||
modelPrice = modelPrice * meta.ImagePriceRatio // 保持 ModelPrice 字段与 imagePriceRatio 一致(供日志展示)
|
||||
}
|
||||
preConsumedQuota = int(math.Round(effModelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio))
|
||||
// 有有效定价但舍入为 0 时保底为 1,避免计费为 0
|
||||
if preConsumedQuota == 0 && effModelPrice > 0 && groupRatioInfo.GroupRatio > 0 {
|
||||
preConsumedQuota = 1
|
||||
}
|
||||
preConsumedQuota = int(effModelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
|
||||
}
|
||||
|
||||
// check if free model pre-consume is disabled
|
||||
|
|
@ -280,11 +276,7 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
|
|||
markupDisc := effectiveMarkupDiscountPercent(c, info, channelID, info.OriginModelName)
|
||||
globalPrice, _ := ratio_setting.GetModelPrice(info.OriginModelName, false)
|
||||
effModelPrice := model.EffectiveModelPrice(modelPrice, globalPrice, chDisc, markupDisc)
|
||||
quota := int(math.Round(effModelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio))
|
||||
// 有有效定价但舍入为 0 时保底为 1,避免计费为 0
|
||||
if quota == 0 && effModelPrice > 0 && groupRatioInfo.GroupRatio > 0 {
|
||||
quota = 1
|
||||
}
|
||||
quota := int(effModelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
|
||||
|
||||
// 免费模型检测(与 ModelPriceHelper 对齐)
|
||||
freeModel := false
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
|||
SetDashboardRouter(router)
|
||||
SetRelayRouter(router)
|
||||
SetVideoRouter(router)
|
||||
|
||||
frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL")
|
||||
if common.IsMasterNode && frontendBaseUrl != "" {
|
||||
frontendBaseUrl = ""
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
aW1wb3J0IHN5cwpwcmludCgndGVzdCcp
|
||||
|
|
@ -323,8 +323,7 @@ var vendorKeywordAliases = []struct {
|
|||
{"qwen", []string{"alibaba", "qwen", "tongyi", "aliyun"}},
|
||||
{"moonshot", []string{"moonshot"}},
|
||||
{"kimi", []string{"moonshot"}},
|
||||
{"doubao", []string{"bytedance", "volcengine", "volcano", "字节跳动", "字节"}},
|
||||
{"seedance", []string{"bytedance", "volcengine", "volcano", "doubao", "字节跳动", "字节"}},
|
||||
{"doubao", []string{"bytedance", "volcengine", "volcano"}},
|
||||
{"ernie", []string{"baidu"}},
|
||||
{"wenxin", []string{"baidu"}},
|
||||
{"hunyuan", []string{"tencent"}},
|
||||
|
|
|
|||
|
|
@ -66,12 +66,7 @@ func calculateAudioQuota(info QuotaInfo) int {
|
|||
// 新公式:固定价格 = 渠道固定价 * 成本折扣率% + 全局固定价 * 加价折扣率%
|
||||
effModelPrice := model.EffectiveModelPrice(info.ModelPrice, info.GlobalModelPrice, costDisc, markupDisc)
|
||||
quota := decimal.NewFromFloat(effModelPrice).Mul(quotaPerUnit).Mul(groupRatio)
|
||||
quotaInt := int(quota.Round(0).IntPart())
|
||||
// 有有效定价但舍入为 0 时保底为 1,避免计费为 0
|
||||
if info.UsePrice && quotaInt == 0 && quota.GreaterThan(decimal.Zero) {
|
||||
quotaInt = 1
|
||||
}
|
||||
return quotaInt
|
||||
return int(quota.IntPart())
|
||||
}
|
||||
|
||||
completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(info.ModelName))
|
||||
|
|
@ -106,12 +101,7 @@ func calculateAudioQuota(info QuotaInfo) int {
|
|||
quota = decimal.NewFromInt(1)
|
||||
}
|
||||
|
||||
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
|
||||
return int(quota.Round(0).IntPart())
|
||||
}
|
||||
|
||||
func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage) error {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ type GeneralSetting struct {
|
|||
|
||||
// 默认配置
|
||||
var generalSetting = GeneralSetting{
|
||||
DocsLink: "",
|
||||
DocsLink: "https://docs.newapi.pro",
|
||||
DefaultSiteLanguage: "zh-CN",
|
||||
PingIntervalEnabled: false,
|
||||
PingIntervalSeconds: 60,
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -198,11 +198,7 @@ func ChannelModelPrice2JSONString() string {
|
|||
}
|
||||
|
||||
func UpdateChannelModelPriceByJSONString(jsonStr string) error {
|
||||
err := types.LoadFromJsonString(channelModelPriceMap, jsonStr)
|
||||
if err == nil {
|
||||
rebuildChannelPricedModelSet()
|
||||
}
|
||||
return err
|
||||
return types.LoadFromJsonString(channelModelPriceMap, jsonStr)
|
||||
}
|
||||
|
||||
func GetChannelModelPriceCopy() map[string]map[string]float64 {
|
||||
|
|
@ -231,11 +227,7 @@ func ChannelModelRatio2JSONString() string {
|
|||
}
|
||||
|
||||
func UpdateChannelModelRatioByJSONString(jsonStr string) error {
|
||||
err := types.LoadFromJsonString(channelModelRatioMap, jsonStr)
|
||||
if err == nil {
|
||||
rebuildChannelPricedModelSet()
|
||||
}
|
||||
return err
|
||||
return types.LoadFromJsonString(channelModelRatioMap, jsonStr)
|
||||
}
|
||||
|
||||
func GetChannelModelRatioCopy() map[string]map[string]float64 {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package ratio_setting
|
|||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
|
|
@ -273,15 +272,6 @@ var defaultModelRatio = map[string]float64{
|
|||
"deepseek-ai/DeepSeek-R1": 0.8,
|
||||
"deepseek-ai/DeepSeek-V3-0324": 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{
|
||||
|
|
@ -351,11 +341,6 @@ var modelPriceMap = types.NewRWMap[string, float64]()
|
|||
var modelRatioMap = 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{
|
||||
"gpt-4-gizmo-*": 2,
|
||||
"gpt-4o-gizmo-*": 3,
|
||||
|
|
@ -379,25 +364,6 @@ func InitRatioSettings() {
|
|||
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 {
|
||||
return modelPriceMap.ReadAll()
|
||||
}
|
||||
|
|
@ -466,7 +432,7 @@ func GetModelRatio(name string) (float64, bool, string) {
|
|||
return ratio, true, name
|
||||
}
|
||||
|
||||
// ModelHasConfiguredPricing 表示模型在价格表或倍率表中存在显式配置(含 compact 通配、渠道级定价)。
|
||||
// ModelHasConfiguredPricing 表示模型在价格表或倍率表中存在显式配置(含 compact 通配)。
|
||||
// 未命中表键时 GetModelRatio 不再提供可用倍率(非自用为 success=false;自用为占位倍率),此类模型不应出现在定价接口。
|
||||
func ModelHasConfiguredPricing(model string) bool {
|
||||
if _, ok := GetModelPrice(model, false); ok {
|
||||
|
|
@ -481,10 +447,7 @@ func ModelHasConfiguredPricing(model string) bool {
|
|||
return true
|
||||
}
|
||||
}
|
||||
channelPricedModelSetMu.RLock()
|
||||
ok := channelPricedModelSet[name]
|
||||
channelPricedModelSetMu.RUnlock()
|
||||
return ok
|
||||
return false
|
||||
}
|
||||
|
||||
func DefaultModelRatio2JSONString() string {
|
||||
|
|
@ -907,8 +870,6 @@ func GetCompletionRatioCopy() map[string]float64 {
|
|||
|
||||
// 转换模型名,减少渠道必须配置各种带参数模型
|
||||
func FormatMatchingModelName(name string) string {
|
||||
// 去除空格,使 "Seedance 2.0" 与 "Seedance2.0" 等变体能匹配
|
||||
name = strings.ReplaceAll(name, " ", "")
|
||||
|
||||
if strings.HasPrefix(name, "gemini-2.5-flash-lite") {
|
||||
name = handleThinkingBudgetModel(name, "gemini-2.5-flash-lite", "gemini-2.5-flash-lite-thinking-*")
|
||||
|
|
|
|||
56
setup.ps1
56
setup.ps1
|
|
@ -1,56 +0,0 @@
|
|||
$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 +0,0 @@
|
|||
hello
|
||||
234
tokendance.yaml
234
tokendance.yaml
|
|
@ -1,234 +0,0 @@
|
|||
- 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 元 Token,7 天有效期
|
||||
- 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
234
tokendance.yml
|
|
@ -1,234 +0,0 @@
|
|||
- 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 元 Token,7 天有效期
|
||||
- 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
|
||||
|
|
@ -6,7 +6,6 @@
|
|||
"name": "react-template",
|
||||
"dependencies": {
|
||||
"@douyinfe/semi-icons": "^2.63.1",
|
||||
"@douyinfe/semi-illustrations": "^2.100.0",
|
||||
"@douyinfe/semi-ui": "^2.69.1",
|
||||
"@lobehub/icons": "^2.0.0",
|
||||
"@visactor/react-vchart": "~1.8.8",
|
||||
|
|
@ -172,7 +171,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-illustrations": ["@douyinfe/semi-illustrations@2.100.0", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-SN7plpE328WGBohLHOVpYe6FwWSO6RLS7Xf6LhqEdtarwK52ircr4C/b+OyRqIwcLOzRYMgIoqcWnAQGmowcUw=="],
|
||||
"@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-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=="],
|
||||
|
||||
|
|
@ -2262,8 +2261,6 @@
|
|||
|
||||
"@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/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=="],
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
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!")
|
||||
|
|
@ -1,18 +1,22 @@
|
|||
<!doctype html>
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<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" />
|
||||
<link rel="icon" href="/logo.jpg" />
|
||||
<link rel="icon" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="description" content="Unified AI model gateway supporting OpenAI, Claude, Gemini and more." />
|
||||
<meta
|
||||
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" />
|
||||
<!-- 鏍囬鐢?/api/status 鍐欏叆 localStorage 鍚庣敱鍓嶇鍚屾锛涗笉鍦ㄦ澶勮 localStorage锛屼互鍏嶆棫缂撳瓨锛堝鏀瑰悕鍓嶉粯璁ゅ€硷級鎶㈠厛鏄剧ず -->
|
||||
<!-- 标题由 /api/status 写入 localStorage 后由前端同步;不在此处读 localStorage,以免旧缓存(如改名前默认值)抢先显示 -->
|
||||
<title></title>
|
||||
<!--umami-->
|
||||
<!--Google Analytics-->
|
||||
|
|
@ -24,8 +28,3 @@
|
|||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,11 @@
|
|||
"type": "module",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@lobehub/icons": "^2.0.0",
|
||||
"@visactor/react-vchart": "~1.8.8",
|
||||
"@visactor/vchart": "~1.8.8",
|
||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||
"antd": "^5.29.3",
|
||||
"axios": "1.13.5",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.11",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,4 +0,0 @@
|
|||
allowBuilds:
|
||||
'@parcel/watcher': set this to true or false
|
||||
'@swc/core': set this to true or false
|
||||
esbuild: set this to true or false
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 882 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -18,38 +18,6 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
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 Loading from './components/common/ui/Loading';
|
||||
import User from './pages/User';
|
||||
|
|
@ -69,7 +37,6 @@ import Redemption from './pages/Redemption';
|
|||
import TopUp from './pages/TopUp';
|
||||
import Log from './pages/Log';
|
||||
import Chat from './pages/Chat';
|
||||
import Benchmarks from './pages/Benchmarks';
|
||||
import Chat2Link from './pages/Chat2Link';
|
||||
import Midjourney from './pages/Midjourney';
|
||||
import Pricing from './pages/Pricing';
|
||||
|
|
@ -78,7 +45,6 @@ import ModelPage from './pages/Model';
|
|||
import ModelDeploymentPage from './pages/ModelDeployment';
|
||||
import ModelHeatPage from './pages/ModelHeat';
|
||||
import Playground from './pages/Playground';
|
||||
import ApiTester from './pages/ApiTester';
|
||||
import Subscription from './pages/Subscription';
|
||||
import OAuth2Callback from './components/auth/OAuth2Callback';
|
||||
import PersonalSetting from './components/settings/PersonalSetting';
|
||||
|
|
@ -91,10 +57,6 @@ import Suppliers from './pages/SupplierAdmin/list';
|
|||
import Setup from './pages/Setup';
|
||||
import SetupCheck from './components/layout/SetupCheck';
|
||||
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 Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
|
|
@ -105,7 +67,6 @@ const InviteRedirect = lazy(() => import('./pages/InviteRedirect'));
|
|||
const About = lazy(() => import('./pages/About'));
|
||||
const UserAgreement = lazy(() => import('./pages/UserAgreement'));
|
||||
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
|
||||
const Docs = lazy(() => import('./pages/Docs'));
|
||||
|
||||
function DynamicOAuth2Callback() {
|
||||
const { provider } = useParams();
|
||||
|
|
@ -214,14 +175,6 @@ function App() {
|
|||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/console/api-tester'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ApiTester />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/console/redemption'
|
||||
element={
|
||||
|
|
@ -238,38 +191,6 @@ function App() {
|
|||
</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
|
||||
path='/console/supplier-application'
|
||||
element={
|
||||
|
|
@ -384,11 +305,9 @@ function App() {
|
|||
path='/console/personal'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||
<PersonalSetting />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||
<PersonalSetting />
|
||||
</Suspense>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
|
@ -545,14 +464,6 @@ function App() {
|
|||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/docs'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||
<Docs />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user-agreement'
|
||||
element={
|
||||
|
|
@ -588,12 +499,10 @@ function App() {
|
|||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route path='/benchmarks' element={<Benchmarks />} />
|
||||
<Route path='*' element={<NotFound />} />
|
||||
<Route path='*' element={<NotFound />} />
|
||||
</Routes>
|
||||
</SetupCheck>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
|
|
|
|||
|
|
@ -1077,58 +1077,34 @@ const LoginForm = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const brandTagline = t('统一 AI 模型网关,聚合全球顶尖大模型,一站接入即刻使用');
|
||||
|
||||
return (
|
||||
<div className='flex min-h-screen'>
|
||||
{/* 左侧品牌展示区 */}
|
||||
<div className='hidden lg:flex lg:w-1/2 relative overflow-hidden bg-zinc-900 flex-col items-center justify-center px-12'>
|
||||
<div
|
||||
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 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()}
|
||||
|
||||
{/* 右侧登录表单区 */}
|
||||
<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>
|
||||
{turnstileEnabled && (
|
||||
<div className='flex justify-center mt-6'>
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// 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';
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -17,85 +17,258 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { getRelativeTime, userIsDistributorUser } from '../../helpers';
|
||||
import { UserContext } from '../../context/User';
|
||||
import { useUserMessageUnreadCount } from '../../hooks/common/useUserMessageUnreadCount';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
|
||||
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 { getSystemName } from '../../helpers';
|
||||
import { useDashboardStats } from '../../hooks/dashboard/useDashboardStats';
|
||||
import { useDashboardCharts } from '../../hooks/dashboard/useDashboardCharts';
|
||||
|
||||
import TopBar from './TopBar';
|
||||
import Hero from './Hero';
|
||||
import BentoStats from './BentoStats';
|
||||
import BentoTrend from './BentoTrend';
|
||||
import MonthlyForecast from './MonthlyForecast';
|
||||
import RecentActivity from './RecentActivity';
|
||||
|
||||
const RANGE_DAYS = {
|
||||
today: 1,
|
||||
yesterday: 1,
|
||||
'24h': 1,
|
||||
'7d': 7,
|
||||
'14d': 14,
|
||||
'30d': 30,
|
||||
};
|
||||
import {
|
||||
CHART_CONFIG,
|
||||
CARD_PROPS,
|
||||
FLEX_CENTER_GAP2,
|
||||
ILLUSTRATION_SIZE,
|
||||
ANNOUNCEMENT_LEGEND_DATA,
|
||||
UPTIME_STATUS_MAP,
|
||||
} from '../../constants/dashboard.constants';
|
||||
import {
|
||||
getTrendSpec,
|
||||
handleCopyUrl,
|
||||
handleSpeedTest,
|
||||
getUptimeStatusColor,
|
||||
getUptimeStatusText,
|
||||
renderMonitorList,
|
||||
} from '../../helpers/dashboard';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [userState] = useContext(UserContext);
|
||||
const user = userState?.user;
|
||||
const [rangeKey, setRangeKey] = useState('7d');
|
||||
// ========== Context ==========
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
|
||||
const rangeDays = RANGE_DAYS[rangeKey] || 7;
|
||||
// ========== 主要数据管理 ==========
|
||||
const dashboardData = useDashboardData(userState, userDispatch, statusState);
|
||||
|
||||
const dashboard = useDashboardData(userState, rangeDays);
|
||||
const { unreadCount } = useUserMessageUnreadCount(user);
|
||||
// ========== 图表管理 ==========
|
||||
const dashboardCharts = useDashboardCharts(
|
||||
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 (
|
||||
<div className='flex flex-col gap-6 pb-8'>
|
||||
<TopBar
|
||||
onSearch={() => {}}
|
||||
onRefresh={dashboard.refresh}
|
||||
loading={dashboard.loading}
|
||||
user={user}
|
||||
unreadCount={unreadCount}
|
||||
/>
|
||||
<div className='h-full flex flex-col gap-6'>
|
||||
<DashboardHeader
|
||||
getGreeting={dashboardData.getGreeting}
|
||||
greetingVisible={dashboardData.greetingVisible}
|
||||
showSearchModal={dashboardData.showSearchModal}
|
||||
refresh={handleRefresh}
|
||||
loading={dashboardData.loading}
|
||||
t={dashboardData.t}
|
||||
/>
|
||||
|
||||
<div className='space-y-6 pb-8'>
|
||||
<Hero user={user} />
|
||||
<SearchModal
|
||||
searchModalVisible={dashboardData.searchModalVisible}
|
||||
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}
|
||||
/>
|
||||
|
||||
{/* 第一行:5 个核心指标卡 */}
|
||||
<div className='grid grid-cols-2 gap-5 md:grid-cols-3 xl:grid-cols-5'>
|
||||
<BentoStats
|
||||
metrics={dashboard.metrics}
|
||||
monthDelta={dashboard.monthDelta}
|
||||
/>
|
||||
</div>
|
||||
<StatsCards
|
||||
groupedStatsData={groupedStatsData}
|
||||
loading={dashboardData.loading}
|
||||
getTrendSpec={getTrendSpec}
|
||||
CARD_PROPS={CARD_PROPS}
|
||||
CHART_CONFIG={CHART_CONFIG}
|
||||
/>
|
||||
|
||||
{/* 第二行:七日用量趋势 + 本月账单预测 */}
|
||||
<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>
|
||||
{userIsDistributorUser(userState?.user) ? (
|
||||
<DistributorAnalyticsBoard />
|
||||
) : null}
|
||||
|
||||
{/* 第三行:最近调用 */}
|
||||
<RecentActivity
|
||||
logs={dashboard.recentLogs}
|
||||
{/* 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}
|
||||
/>
|
||||
</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>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -378,6 +378,7 @@ const HomeModelList = () => {
|
|||
<PricingCardView
|
||||
filteredModels={pricingData.filteredModels}
|
||||
loading={pricingData.loading}
|
||||
rowSelection={null}
|
||||
pageSize={pricingData.pageSize}
|
||||
setPageSize={pricingData.setPageSize}
|
||||
currentPage={pricingData.currentPage}
|
||||
|
|
@ -386,6 +387,9 @@ const HomeModelList = () => {
|
|||
groupRatio={pricingData.groupRatio}
|
||||
groupModelPrice={pricingData.groupModelPrice}
|
||||
groupModelRatio={pricingData.groupModelRatio}
|
||||
copyText={pricingData.copyText}
|
||||
setModalImageUrl={pricingData.setModalImageUrl}
|
||||
setIsModalOpenurl={pricingData.setIsModalOpenurl}
|
||||
currency={pricingData.currency}
|
||||
siteDisplayType={pricingData.siteDisplayType}
|
||||
tokenUnit={pricingData.tokenUnit}
|
||||
|
|
@ -395,7 +399,10 @@ const HomeModelList = () => {
|
|||
pricingData.channelVideoCompletionRatio
|
||||
}
|
||||
channelVideoPrice={pricingData.channelVideoPrice}
|
||||
showRatio={false}
|
||||
t={pricingData.t}
|
||||
selectedRowKeys={[]}
|
||||
setSelectedRowKeys={() => {}}
|
||||
openModelDetail={pricingData.openModelDetail}
|
||||
showSizeChanger={false}
|
||||
blurPricing={blurPricing}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { IconClose } from '@douyinfe/semi-icons';
|
|||
import SiderBar from './SiderBar';
|
||||
import App from '../../App';
|
||||
import FooterBar from './Footer';
|
||||
import OnboardingWizard from '../onboarding/OnboardingWizard';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useIsMobile } from '../../hooks/common/useIsMobile';
|
||||
|
|
@ -323,24 +322,22 @@ const PageLayout = () => {
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{location.pathname !== '/' && (
|
||||
<Header
|
||||
style={{
|
||||
padding: 0,
|
||||
height: 'auto',
|
||||
lineHeight: 'normal',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<HeaderBar
|
||||
onMobileMenuToggle={() => setDrawerOpen((prev) => !prev)}
|
||||
drawerOpen={drawerOpen}
|
||||
/>
|
||||
</Header>
|
||||
)}
|
||||
<Header
|
||||
style={{
|
||||
padding: 0,
|
||||
height: 'auto',
|
||||
lineHeight: 'normal',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<HeaderBar
|
||||
onMobileMenuToggle={() => setDrawerOpen((prev) => !prev)}
|
||||
drawerOpen={drawerOpen}
|
||||
/>
|
||||
</Header>
|
||||
<Layout
|
||||
style={{
|
||||
overflow: isMobile ? 'visible' : 'auto',
|
||||
|
|
@ -403,7 +400,6 @@ const PageLayout = () => {
|
|||
)}
|
||||
</Layout>
|
||||
</Layout>
|
||||
<OnboardingWizard />
|
||||
<ToastContainer />
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -51,8 +51,6 @@ const routerMap = {
|
|||
deployment: '/console/deployment',
|
||||
'model-heat': '/console/model-heat',
|
||||
playground: '/console/playground',
|
||||
benchmarks: '/benchmarks',
|
||||
'api-tester': '/console/api-tester',
|
||||
personal: '/console/personal',
|
||||
supplier: null,
|
||||
distributor: '/console/distributor/admin',
|
||||
|
|
@ -86,16 +84,6 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
|||
|
||||
const workspaceItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
text: t('评测'),
|
||||
itemKey: 'benchmarks',
|
||||
to: '/benchmarks',
|
||||
},
|
||||
{
|
||||
text: t('API 测速'),
|
||||
itemKey: 'api-tester',
|
||||
to: '/api-tester',
|
||||
},
|
||||
{
|
||||
text: t('数据看板'),
|
||||
itemKey: 'detail',
|
||||
|
|
@ -106,7 +94,7 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
|||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('API Key'),
|
||||
text: t('令牌管理'),
|
||||
itemKey: 'token',
|
||||
to: '/token',
|
||||
},
|
||||
|
|
@ -207,12 +195,6 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
|||
|
||||
const adminItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
text: t('操作记录'),
|
||||
itemKey: 'operation-log',
|
||||
to: '/console/operation-log',
|
||||
className: isAdmin() ? '' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('渠道管理'),
|
||||
itemKey: 'channel',
|
||||
|
|
@ -255,6 +237,12 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
|||
to: '/user',
|
||||
className: isAdmin() ? '' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('操作记录'),
|
||||
itemKey: 'operation-log',
|
||||
to: '/console/operation-log',
|
||||
className: isAdmin() ? '' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('代理管理'),
|
||||
itemKey: 'distributor',
|
||||
|
|
@ -315,7 +303,7 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
|||
const chatMenuItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
text: t('体验'),
|
||||
text: t('操练场'),
|
||||
itemKey: 'playground',
|
||||
to: '/playground',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import NewYearButton from './NewYearButton';
|
||||
import NotificationButton from './NotificationButton';
|
||||
import ThemeToggle from './ThemeToggle';
|
||||
import LanguageSelector from './LanguageSelector';
|
||||
|
|
@ -35,9 +36,6 @@ const ActionButtons = ({
|
|||
isLoading,
|
||||
isMobile,
|
||||
isSelfUseMode,
|
||||
showThemeToggle = true,
|
||||
// 默认隐藏顶栏语言选择器,仅保留简体中文
|
||||
showLanguageSelector = false,
|
||||
logout,
|
||||
navigate,
|
||||
t,
|
||||
|
|
@ -45,7 +43,8 @@ const ActionButtons = ({
|
|||
const shouldShowNoticeButton = Boolean(userState?.user?.id);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 md:gap-1.5">
|
||||
<div className='flex items-center gap-2 md:gap-3'>
|
||||
{/* <NewYearButton isNewYear={isNewYear} /> */}
|
||||
{shouldShowNoticeButton && (
|
||||
<NotificationButton
|
||||
unreadCount={unreadCount}
|
||||
|
|
@ -54,16 +53,12 @@ const ActionButtons = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{showThemeToggle && (
|
||||
<ThemeToggle theme={theme} onThemeToggle={onThemeToggle} t={t} />
|
||||
)}
|
||||
<ThemeToggle theme={theme} onThemeToggle={onThemeToggle} t={t} />
|
||||
|
||||
{showLanguageSelector && (
|
||||
<LanguageSelector
|
||||
currentLang={currentLang}
|
||||
onLanguageChange={onLanguageChange}
|
||||
/>
|
||||
)}
|
||||
<LanguageSelector
|
||||
currentLang={currentLang}
|
||||
onLanguageChange={onLanguageChange}
|
||||
/>
|
||||
|
||||
<UserArea
|
||||
userState={userState}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -17,10 +17,23 @@ 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 { Link } from 'react-router-dom';
|
||||
import { Tag } from '@douyinfe/semi-ui';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Typography, Tag } from '@douyinfe/semi-ui';
|
||||
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 = ({
|
||||
isMobile,
|
||||
|
|
@ -31,43 +44,93 @@ const HeaderLogo = ({
|
|||
systemName,
|
||||
isSelfUseMode,
|
||||
isDemoSiteMode,
|
||||
userState,
|
||||
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) {
|
||||
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 (
|
||||
<div className="flex items-center gap-2 md:gap-3 flex-shrink-0">
|
||||
<Link to="/" className="group flex items-center gap-2">
|
||||
<div className="relative w-8 h-8 md:w-8 md:h-8">
|
||||
<SkeletonWrapper loading={isLoading || !logoLoaded} type="image" />
|
||||
<div className='flex items-center gap-2 md:gap-3 flex-shrink-0'>
|
||||
<Link to='/' className='group flex items-center gap-2'>
|
||||
<div className='relative w-8 h-8 md:w-8 md:h-8'>
|
||||
<SkeletonWrapper loading={isLoading || !logoLoaded} type='image' />
|
||||
<img
|
||||
src={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"
|
||||
}`}
|
||||
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'}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='hidden md:flex items-center gap-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<SkeletonWrapper
|
||||
loading={isLoading}
|
||||
type="title"
|
||||
type='title'
|
||||
width={120}
|
||||
height={24}
|
||||
>
|
||||
<span className="text-lg font-semibold text-foreground">
|
||||
<Typography.Title
|
||||
heading={4}
|
||||
className='!text-lg !font-semibold !mb-0'
|
||||
>
|
||||
{systemName}
|
||||
</span>
|
||||
</Typography.Title>
|
||||
</SkeletonWrapper>
|
||||
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
|
||||
<Tag
|
||||
color={isSelfUseMode ? "purple" : "blue"}
|
||||
size="small"
|
||||
shape="circle"
|
||||
className="text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm"
|
||||
color={isSelfUseMode ? 'purple' : 'blue'}
|
||||
className='text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm'
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
{isSelfUseMode ? t('自用模式') : t('演示站点')}
|
||||
</Tag>
|
||||
|
|
@ -75,6 +138,17 @@ const HeaderLogo = ({
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -17,76 +17,60 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { Languages } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React from 'react';
|
||||
import { Button, Dropdown } from '@douyinfe/semi-ui';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
normalizeLanguage,
|
||||
supportedLanguages,
|
||||
LANGUAGE_NATIVE_LABELS,
|
||||
} from "../../../i18n/language";
|
||||
} from '../../../i18n/language';
|
||||
|
||||
/** 语言在头部与下拉中一律显示其自称(如 English、简体中文),不随界面语言翻译 */
|
||||
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 { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
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]);
|
||||
const normalized = normalizeLanguage(currentLang) || 'zh-CN';
|
||||
const currentLabel = nativeLabel(normalized);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${t("切换语言")}: ${nativeLabel(normalized)}`}
|
||||
onClick={() => setOpen(!open)}
|
||||
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"
|
||||
<Dropdown
|
||||
position='bottomRight'
|
||||
render={
|
||||
<Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
|
||||
{supportedLanguages.map((code) => (
|
||||
<Dropdown.Item
|
||||
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]'
|
||||
>
|
||||
<Languages size={16} />
|
||||
<span className="hidden sm:inline truncate max-w-[6rem]">
|
||||
{nativeLabel(normalized)}
|
||||
<span className='truncate text-sm font-medium min-w-0'>
|
||||
{currentLabel}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{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>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -17,8 +17,9 @@ 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 { X, Menu } from "lucide-react";
|
||||
import React from 'react';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { IconClose, IconMenu } from '@douyinfe/semi-icons';
|
||||
|
||||
const MobileMenuButton = ({
|
||||
isConsoleRoute,
|
||||
|
|
@ -32,17 +33,23 @@ const MobileMenuButton = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
const isOpen = isMobile ? drawerOpen : collapsed;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isOpen ? t("关闭侧边栏") : t("打开侧边栏")}
|
||||
<Button
|
||||
icon={
|
||||
(isMobile ? drawerOpen : collapsed) ? (
|
||||
<IconClose className='text-lg' />
|
||||
) : (
|
||||
<IconMenu className='text-lg' />
|
||||
)
|
||||
}
|
||||
aria-label={
|
||||
(isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏')
|
||||
}
|
||||
onClick={onToggle}
|
||||
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"
|
||||
>
|
||||
{isOpen ? <X size={20} /> : <Menu size={20} />}
|
||||
</button>
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
className='!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,22 +20,19 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
import React, { useMemo, useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Button, Dropdown } from '@douyinfe/semi-ui';
|
||||
import { ChevronDown, Box, LayoutDashboard, Trophy, BookOpen } from 'lucide-react';
|
||||
|
||||
const NAV_ICONS = {
|
||||
pricing: Box,
|
||||
console: LayoutDashboard,
|
||||
benchmark: Trophy,
|
||||
docs: BookOpen,
|
||||
};
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import SkeletonWrapper from '../components/SkeletonWrapper';
|
||||
import { StatusContext } from '../../../context/Status';
|
||||
import { isAdmin, userIsSupplierUser } from '../../../helpers';
|
||||
|
||||
/** 主站入口顺序(与桌面顶栏一致) */
|
||||
const PRIMARY_NAV_KEYS = ['pricing', 'benchmark', 'docs', 'console'];
|
||||
const PRIMARY_NAV_KEYS = ['home', 'pricing', 'docs', 'about'];
|
||||
|
||||
const menuClass =
|
||||
'!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 = ({
|
||||
mainNavLinks,
|
||||
pricingRequireAuth,
|
||||
|
|
@ -46,6 +43,7 @@ const MobileSiteNavDropdown = ({
|
|||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const user = userState?.user;
|
||||
const [statusState] = React.useContext(StatusContext);
|
||||
|
||||
const byNavKey = useMemo(
|
||||
() => Object.fromEntries(mainNavLinks.map((l) => [l.itemKey, l])),
|
||||
|
|
@ -53,16 +51,53 @@ const MobileSiteNavDropdown = ({
|
|||
);
|
||||
|
||||
const primaryAndConsoleLinks = useMemo(() => {
|
||||
return PRIMARY_NAV_KEYS.map((k) => byNavKey[k]).filter(Boolean);
|
||||
const primary = PRIMARY_NAV_KEYS.map((k) => byNavKey[k]).filter(Boolean);
|
||||
const consoleLink = byNavKey.console;
|
||||
if (consoleLink) {
|
||||
return [...primary, consoleLink];
|
||||
}
|
||||
return primary;
|
||||
}, [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 p = location.pathname;
|
||||
if (p === '/pricing' || p.startsWith('/pricing/')) return t('模型');
|
||||
if (p.startsWith('/benchmarks')) return t('评测');
|
||||
if (p === '/') return t('首页');
|
||||
if (p === '/pricing' || p.startsWith('/pricing/')) return t('模型广场');
|
||||
if (p === '/about' || p.startsWith('/about/')) return t('关于');
|
||||
if (/\/[a-z]{2}\/docs\b/i.test(p) || p.includes('/docs')) {
|
||||
return t('文档');
|
||||
}
|
||||
if (p.startsWith(supplierApplyPath)) return t('提供算力');
|
||||
if (p.startsWith('/console')) return t('控制台');
|
||||
if (p === '/login') return t('登录');
|
||||
return t('页面导航');
|
||||
|
|
@ -113,7 +148,20 @@ const MobileSiteNavDropdown = ({
|
|||
[navigate, resolveInternalTarget],
|
||||
);
|
||||
|
||||
if (primaryAndConsoleLinks.length === 0) {
|
||||
const handleSelectApply = useCallback(
|
||||
(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;
|
||||
}
|
||||
|
||||
|
|
@ -151,14 +199,28 @@ const MobileSiteNavDropdown = ({
|
|||
onClick={() => handleSelectMain(link)}
|
||||
className={itemClass(active)}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{NAV_ICONS[link.itemKey] && React.createElement(NAV_ICONS[link.itemKey], { size: 16 })}
|
||||
{link.text}
|
||||
</span>
|
||||
{link.text}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
{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.Menu>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -19,16 +19,8 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Box, LayoutDashboard, Trophy, BookOpen } from 'lucide-react';
|
||||
import SkeletonWrapper from '../components/SkeletonWrapper';
|
||||
|
||||
const NAV_ICONS = {
|
||||
pricing: Box,
|
||||
console: LayoutDashboard,
|
||||
benchmark: Trophy,
|
||||
docs: BookOpen,
|
||||
};
|
||||
|
||||
const Navigation = ({
|
||||
mainNavLinks,
|
||||
isMobile,
|
||||
|
|
@ -41,26 +33,21 @@ const Navigation = ({
|
|||
const isActive = (linkPath) => {
|
||||
if (!linkPath) return false;
|
||||
const currentPath = location.pathname;
|
||||
if (linkPath === "/") {
|
||||
return currentPath === "/";
|
||||
if (linkPath === '/') {
|
||||
return currentPath === '/';
|
||||
}
|
||||
return currentPath.startsWith(linkPath);
|
||||
};
|
||||
|
||||
const renderNavLinks = () => {
|
||||
const baseClasses =
|
||||
"flex-shrink-0 inline-flex items-center gap-1.5 text-[14px] transition-colors duration-150 rounded-md px-3 py-1.5";
|
||||
'flex-shrink-0 flex items-center text-sm font-medium transition-all duration-200 ease-in-out rounded-md';
|
||||
const spacingClasses = isMobile ? 'px-2 py-1' : 'px-3 py-2';
|
||||
const hoverClasses =
|
||||
"hover:bg-accent hover:text-accent-foreground";
|
||||
'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700/50 hover:!text-semi-color-text-0 dark:hover:!text-white';
|
||||
|
||||
return mainNavLinks.map((link) => {
|
||||
const Icon = NAV_ICONS[link.itemKey];
|
||||
const linkContent = (
|
||||
<>
|
||||
{Icon && <Icon size={16} className="shrink-0" />}
|
||||
<span>{link.text}</span>
|
||||
</>
|
||||
);
|
||||
const linkContent = <span>{link.text}</span>;
|
||||
|
||||
if (link.isExternal) {
|
||||
const openInNewTab = link.openInNewTab !== false;
|
||||
|
|
@ -69,9 +56,9 @@ const Navigation = ({
|
|||
key={link.itemKey}
|
||||
href={link.externalLink}
|
||||
{...(openInNewTab
|
||||
? { target: "_blank", rel: "noopener noreferrer" }
|
||||
? { target: '_blank', rel: 'noopener noreferrer' }
|
||||
: {})}
|
||||
className={`${baseClasses} ${hoverClasses} text-muted-foreground`}
|
||||
className={`${baseClasses} ${spacingClasses} ${hoverClasses} !text-semi-color-text-1 dark:!text-gray-300`}
|
||||
>
|
||||
{linkContent}
|
||||
</a>
|
||||
|
|
@ -79,23 +66,23 @@ const Navigation = ({
|
|||
}
|
||||
|
||||
let targetPath = link.to;
|
||||
if (link.itemKey === "console" && !userState.user) {
|
||||
targetPath = "/login";
|
||||
if (link.itemKey === 'console' && !userState.user) {
|
||||
targetPath = '/login';
|
||||
}
|
||||
if (link.itemKey === "pricing" && pricingRequireAuth && !userState.user) {
|
||||
targetPath = "/login";
|
||||
if (link.itemKey === 'pricing' && pricingRequireAuth && !userState.user) {
|
||||
targetPath = '/login';
|
||||
}
|
||||
|
||||
const active = isActive(link.to);
|
||||
const activeClasses = active
|
||||
? "bg-accent text-accent-foreground font-semibold"
|
||||
: "text-muted-foreground";
|
||||
? '!bg-semi-color-fill-2 dark:!bg-gray-700 !text-semi-color-text-0 dark:!text-white'
|
||||
: '!text-semi-color-text-1 dark:!text-gray-300';
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.itemKey}
|
||||
to={targetPath}
|
||||
className={`${baseClasses} ${hoverClasses} ${activeClasses}`}
|
||||
className={`${baseClasses} ${spacingClasses} ${hoverClasses} ${activeClasses}`}
|
||||
>
|
||||
{linkContent}
|
||||
</Link>
|
||||
|
|
@ -104,10 +91,10 @@ const Navigation = ({
|
|||
};
|
||||
|
||||
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
|
||||
loading={isLoading}
|
||||
type="navigation"
|
||||
type='navigation'
|
||||
count={4}
|
||||
width={60}
|
||||
height={16}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -17,25 +17,31 @@ 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 { Bell } from "lucide-react";
|
||||
import React from 'react';
|
||||
import { Button, Badge } from '@douyinfe/semi-ui';
|
||||
import { Bell } from 'lucide-react';
|
||||
|
||||
// NotificationButton 展示站内消息铃铛与未读角标。
|
||||
const NotificationButton = ({ unreadCount, onNoticeOpen, t }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("站内消息")}
|
||||
onClick={onNoticeOpen}
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
{unreadCount > 99 ? "99+" : unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
const buttonProps = {
|
||||
icon: <Bell size={18} />,
|
||||
'aria-label': t('站内消息'),
|
||||
onClick: onNoticeOpen,
|
||||
theme: 'borderless',
|
||||
type: 'tertiary',
|
||||
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',
|
||||
};
|
||||
|
||||
if (unreadCount > 0) {
|
||||
return (
|
||||
<Badge count={unreadCount} type='danger' overflowCount={99}>
|
||||
<Button {...buttonProps} />
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return <Button {...buttonProps} />;
|
||||
};
|
||||
|
||||
export default NotificationButton;
|
||||
|
|
|
|||
|
|
@ -17,88 +17,86 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { Input, Typography } from '@douyinfe/semi-ui';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Input, Dropdown, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import { API, isAdmin, showSuccess } from '../../../helpers';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
function debounce(fn, ms) {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), ms);
|
||||
};
|
||||
}
|
||||
const mockSearchData = [
|
||||
{
|
||||
month: '四月 2026',
|
||||
items: [
|
||||
{ id: 1, name: 'Google: Gemma 4 31B', icon: '⬥', color: 'text-blue-500' },
|
||||
{
|
||||
id: 2,
|
||||
name: 'Qwen: Qwen3.6 Plus (free)',
|
||||
icon: '⬡',
|
||||
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 [searchValue, setSearchValue] = useState('');
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [models, setModels] = useState([]);
|
||||
const [results, setResults] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filteredData, setFilteredData] = useState(mockSearchData);
|
||||
const dropdownRef = 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(() => {
|
||||
debouncedSearch(searchValue);
|
||||
}, [searchValue, debouncedSearch]);
|
||||
if (searchValue.trim() === '') {
|
||||
setFilteredData(mockSearchData);
|
||||
} 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(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === '/' && document.activeElement === document.body) {
|
||||
if (event.key === '/') {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
setVisible(true);
|
||||
|
|
@ -108,110 +106,89 @@ const SearchDropdown = ({ isMobile }) => {
|
|||
inputRef.current?.blur();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleItemClick = (item) => {
|
||||
console.log('Selected:', item);
|
||||
setVisible(false);
|
||||
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 = () => {
|
||||
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 (
|
||||
<div className='max-h-96 overflow-y-auto py-2'>
|
||||
{results.map((item, i) => (
|
||||
<div
|
||||
key={`${item.type}-${i}`}
|
||||
onClick={() => handleItemClick(item)}
|
||||
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'
|
||||
>
|
||||
<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'>
|
||||
{typeLabel(item.type)}
|
||||
</span>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<Text className='!text-sm !font-medium !text-semi-color-text-0 dark:!text-gray-200 block truncate'>
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.desc && (
|
||||
<Text className='!text-xs !text-semi-color-text-2 dark:!text-gray-400 block truncate'>
|
||||
{item.desc}
|
||||
</Text>
|
||||
)}
|
||||
<div className='w-80 md:w-96 max-h-96 overflow-y-auto'>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((group) => (
|
||||
<div key={group.month} className='py-2'>
|
||||
<Typography.Text className='!px-4 !py-2 !text-xs !font-semibold !text-semi-color-text-2 dark:!text-gray-400 uppercase tracking-wider block'>
|
||||
{group.month}
|
||||
</Typography.Text>
|
||||
<div>
|
||||
{group.items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleItemClick(item)}
|
||||
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'
|
||||
>
|
||||
<span className={`text-lg ${item.color}`}>{item.icon}</span>
|
||||
<Typography.Text className='!text-sm !font-medium !text-semi-color-text-0 dark:!text-gray-200'>
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative' ref={dropdownRef}>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder='搜索模型...'
|
||||
prefix={<IconSearch className='text-semi-color-text-2 dark:text-gray-400' />}
|
||||
suffix={
|
||||
<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'>
|
||||
/
|
||||
</kbd>
|
||||
}
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onFocus={() => { if (searchValue.trim()) setVisible(true); }}
|
||||
onBlur={() => setTimeout(() => setVisible(false), 150)}
|
||||
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'
|
||||
style={{ borderRadius: '6px' }}
|
||||
/>
|
||||
</div>
|
||||
{visible && (results.length > 0 || searchValue.trim()) && (
|
||||
<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'>
|
||||
{renderDropdownContent()}
|
||||
<Dropdown
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
position='bottomLeft'
|
||||
trigger='custom'
|
||||
getPopupContainer={() => dropdownRef.current}
|
||||
render={
|
||||
<div className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-800 dark:!border-gray-600'>
|
||||
{renderDropdownContent()}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder='Search'
|
||||
prefix={
|
||||
<IconSearch className='text-semi-color-text-2 dark:text-gray-400' />
|
||||
}
|
||||
suffix={
|
||||
<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'>
|
||||
/
|
||||
</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>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -17,98 +17,101 @@ 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 } from "react";
|
||||
import { Sun, Moon, Monitor } from "lucide-react";
|
||||
import { useActualTheme } from "../../../context/Theme";
|
||||
import React, { useMemo } from 'react';
|
||||
import { Dropdown } from '@douyinfe/semi-ui';
|
||||
import { Sun, Moon, Monitor } from 'lucide-react';
|
||||
import { useActualTheme } from '../../../context/Theme';
|
||||
|
||||
const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
||||
const actualTheme = useActualTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
const themeOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "light",
|
||||
icon: <Sun size={16} />,
|
||||
label: t("浅色模式"),
|
||||
key: 'light',
|
||||
icon: <Sun size={18} />,
|
||||
buttonIcon: <Sun size={18} />,
|
||||
label: t('浅色模式'),
|
||||
description: t('始终使用浅色主题'),
|
||||
},
|
||||
{
|
||||
key: "dark",
|
||||
icon: <Moon size={16} />,
|
||||
label: t("深色模式"),
|
||||
key: 'dark',
|
||||
icon: <Moon size={18} />,
|
||||
buttonIcon: <Moon size={18} />,
|
||||
label: t('深色模式'),
|
||||
description: t('始终使用深色主题'),
|
||||
},
|
||||
{
|
||||
key: "auto",
|
||||
icon: <Monitor size={16} />,
|
||||
label: t("自动模式"),
|
||||
key: 'auto',
|
||||
icon: <Monitor size={18} />,
|
||||
buttonIcon: <Monitor size={18} />,
|
||||
label: t('自动模式'),
|
||||
description: t('跟随系统主题设置'),
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const currentIcon = useMemo(() => {
|
||||
const opt = themeOptions.find((o) => o.key === theme);
|
||||
return opt?.icon || themeOptions[2].icon;
|
||||
const getItemClassName = (isSelected) =>
|
||||
isSelected
|
||||
? '!bg-semi-color-primary-light-default !font-semibold'
|
||||
: '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]);
|
||||
|
||||
// Close on outside click
|
||||
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 (
|
||||
<div className="relative" ref={ref}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("切换主题")}
|
||||
onClick={() => setOpen(!open)}
|
||||
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"
|
||||
>
|
||||
{currentIcon}
|
||||
</button>
|
||||
|
||||
{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}
|
||||
<Dropdown
|
||||
position='bottomRight'
|
||||
trigger='click'
|
||||
clickToHide
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
{themeOptions.map((option) => (
|
||||
<Dropdown.Item
|
||||
key={option.key}
|
||||
icon={option.icon}
|
||||
onClick={() => onThemeToggle(option.key)}
|
||||
className={getItemClassName(theme === option.key)}
|
||||
>
|
||||
<div className='flex flex-col'>
|
||||
<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>
|
||||
<span className='text-xs text-semi-color-text-2'>
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
|
||||
{theme === 'auto' && (
|
||||
<>
|
||||
<Dropdown.Divider />
|
||||
<div className='px-3 py-2 text-xs text-semi-color-text-2'>
|
||||
{t('当前跟随系统')}:
|
||||
{actualTheme === 'dark' ? t('深色') : t('浅色')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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}
|
||||
</span>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ const UserArea = ({
|
|||
size='small'
|
||||
className='text-gray-500 dark:text-gray-400'
|
||||
/>
|
||||
<span>{t('API Key')}</span>
|
||||
<span>{t('令牌管理')}</span>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -43,8 +43,6 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||
docsNav,
|
||||
isDemoSiteMode,
|
||||
isConsoleRoute,
|
||||
showThemeToggle,
|
||||
showLanguageSelector,
|
||||
theme,
|
||||
headerNavModules,
|
||||
pricingRequireAuth,
|
||||
|
|
@ -59,9 +57,11 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||
const [messageModalVisible, setMessageModalVisible] = useState(false);
|
||||
const { unreadCount: messageUnreadCount, refreshUnreadCount } =
|
||||
useUserMessageUnreadCount(userState?.user);
|
||||
// handleMessageModalOpen 打开站内消息弹窗。
|
||||
const handleMessageModalOpen = useCallback(() => {
|
||||
setMessageModalVisible(true);
|
||||
}, []);
|
||||
// handleMessageModalClose 关闭站内消息弹窗并刷新未读计数。
|
||||
const handleMessageModalClose = useCallback(async () => {
|
||||
setMessageModalVisible(false);
|
||||
await refreshUnreadCount();
|
||||
|
|
@ -70,7 +70,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||
const { mainNavLinks } = useNavigation(t, docsNav, headerNavModules);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<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'>
|
||||
<UserMessageModal
|
||||
visible={messageModalVisible}
|
||||
onClose={handleMessageModalClose}
|
||||
|
|
@ -79,69 +79,70 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||
t={t}
|
||||
/>
|
||||
|
||||
<div className="flex h-14 items-center px-4 md:px-6">
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
<MobileMenuButton
|
||||
isConsoleRoute={isConsoleRoute}
|
||||
isMobile={isMobile}
|
||||
drawerOpen={drawerOpen}
|
||||
collapsed={collapsed}
|
||||
onToggle={handleMobileMenuToggle}
|
||||
t={t}
|
||||
/>
|
||||
<div className='w-full 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 flex-1 min-w-0 md:flex-initial'>
|
||||
<MobileMenuButton
|
||||
isConsoleRoute={isConsoleRoute}
|
||||
isMobile={isMobile}
|
||||
drawerOpen={drawerOpen}
|
||||
collapsed={collapsed}
|
||||
onToggle={handleMobileMenuToggle}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<HeaderLogo
|
||||
isMobile={isMobile}
|
||||
isConsoleRoute={isConsoleRoute}
|
||||
logo={logo}
|
||||
logoLoaded={logoLoaded}
|
||||
isLoading={isLoading}
|
||||
systemName={systemName}
|
||||
isSelfUseMode={isSelfUseMode}
|
||||
isDemoSiteMode={isDemoSiteMode}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
<HeaderLogo
|
||||
isMobile={isMobile}
|
||||
isConsoleRoute={isConsoleRoute}
|
||||
logo={logo}
|
||||
logoLoaded={logoLoaded}
|
||||
isLoading={isLoading}
|
||||
systemName={systemName}
|
||||
isSelfUseMode={isSelfUseMode}
|
||||
isDemoSiteMode={isDemoSiteMode}
|
||||
userState={userState}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<div className="min-w-0 overflow-hidden md:hidden">
|
||||
<MobileSiteNavDropdown
|
||||
mainNavLinks={mainNavLinks}
|
||||
pricingRequireAuth={pricingRequireAuth}
|
||||
userState={userState}
|
||||
isLoading={isLoading}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
<div className='min-w-0 flex-1 overflow-hidden md:hidden'>
|
||||
<MobileSiteNavDropdown
|
||||
mainNavLinks={mainNavLinks}
|
||||
pricingRequireAuth={pricingRequireAuth}
|
||||
userState={userState}
|
||||
isLoading={isLoading}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
{/* {!isMobile && <SearchDropdown isMobile={isMobile} />} */}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 md:gap-2">
|
||||
<Navigation
|
||||
mainNavLinks={mainNavLinks}
|
||||
isMobile={isMobile}
|
||||
isLoading={isLoading}
|
||||
userState={userState}
|
||||
pricingRequireAuth={pricingRequireAuth}
|
||||
/>
|
||||
<div className='flex flex-shrink-0 items-center gap-2 md:gap-6'>
|
||||
<Navigation
|
||||
mainNavLinks={mainNavLinks}
|
||||
isMobile={isMobile}
|
||||
isLoading={isLoading}
|
||||
userState={userState}
|
||||
pricingRequireAuth={pricingRequireAuth}
|
||||
/>
|
||||
|
||||
<ActionButtons
|
||||
isNewYear={isNewYear}
|
||||
unreadCount={messageUnreadCount}
|
||||
onNoticeOpen={handleMessageModalOpen}
|
||||
theme={theme}
|
||||
onThemeToggle={handleThemeToggle}
|
||||
currentLang={currentLang}
|
||||
onLanguageChange={handleLanguageChange}
|
||||
userState={userState}
|
||||
isLoading={isLoading}
|
||||
isMobile={isMobile}
|
||||
isSelfUseMode={isSelfUseMode}
|
||||
showThemeToggle={showThemeToggle}
|
||||
showLanguageSelector={showLanguageSelector}
|
||||
logout={logout}
|
||||
navigate={navigate}
|
||||
t={t}
|
||||
/>
|
||||
<ActionButtons
|
||||
isNewYear={isNewYear}
|
||||
unreadCount={messageUnreadCount}
|
||||
onNoticeOpen={handleMessageModalOpen}
|
||||
theme={theme}
|
||||
onThemeToggle={handleThemeToggle}
|
||||
currentLang={currentLang}
|
||||
onLanguageChange={handleLanguageChange}
|
||||
userState={userState}
|
||||
isLoading={isLoading}
|
||||
isMobile={isMobile}
|
||||
isSelfUseMode={isSelfUseMode}
|
||||
logout={logout}
|
||||
navigate={navigate}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -43,30 +43,27 @@ const DeploymentAccessGuard = ({
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<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'>
|
||||
<div className='mt-[60px] px-2'>
|
||||
<Card loading={true} style={{ minHeight: '400px' }}>
|
||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||
<Text type='secondary'>{t('加载设置中...')}</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isEnabled) {
|
||||
return (
|
||||
<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'>
|
||||
<div
|
||||
style={{
|
||||
minHeight: 'calc(100vh - 160px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='mt-[60px] px-4'
|
||||
style={{
|
||||
minHeight: 'calc(100vh - 60px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '600px',
|
||||
|
|
@ -279,22 +276,18 @@ const DeploymentAccessGuard = ({
|
|||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (connectionLoading || (connectionOk === null && !connectionError)) {
|
||||
return (
|
||||
<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'>
|
||||
<div className='mt-[60px] px-2'>
|
||||
<Card loading={true} style={{ minHeight: '400px' }}>
|
||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||
<Text type='secondary'>{t('正在检查 io.net 连接...')}</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -308,16 +301,15 @@ const DeploymentAccessGuard = ({
|
|||
const detail = connectionError?.message || '';
|
||||
|
||||
return (
|
||||
<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'>
|
||||
<div
|
||||
style={{
|
||||
minHeight: 'calc(100vh - 160px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='mt-[60px] px-4'
|
||||
style={{
|
||||
minHeight: 'calc(100vh - 60px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '600px',
|
||||
|
|
@ -410,8 +402,6 @@ const DeploymentAccessGuard = ({
|
|||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -23,7 +23,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||
const ROOT_MARGIN = '280px 0px 280px 0px';
|
||||
|
||||
/**
|
||||
* 体验消息气泡:进入(或接近)可视区域后再挂载子树,减轻长对话下 DOM/媒体压力。
|
||||
* 操练场消息气泡:进入(或接近)可视区域后再挂载子树,减轻长对话下 DOM/媒体压力。
|
||||
* variant=media:更高占位,避免图片/视频模式切换后列表高度不足导致滚不到底部。
|
||||
*/
|
||||
const LazyVisibleMessage = ({
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import {
|
|||
import ParameterControl from './ParameterControl';
|
||||
|
||||
/**
|
||||
* 体验分组下拉:与全局 renderGroupOption 一致,但不展示倍率角标。
|
||||
* 操练场分组下拉:与全局 renderGroupOption 一致,但不展示倍率角标。
|
||||
* @param {Record<string, unknown>} item Semi Select 传入的选项渲染参数
|
||||
*/
|
||||
const renderPlaygroundGroupOption = (item) =>
|
||||
|
|
@ -85,7 +85,7 @@ const SettingsPanel = ({
|
|||
const mediaModeEnabled = isImageMode || isVideoMode;
|
||||
const videoMediaHint = isVideoMode
|
||||
? t(
|
||||
'体验视频素材提示',
|
||||
'操练场视频素材提示',
|
||||
'图片地址:第 1 张为首帧,2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。',
|
||||
)
|
||||
: '';
|
||||
|
|
|
|||
|
|
@ -83,11 +83,11 @@ const VideoUrlInput = ({
|
|||
|
||||
{!enabled ? (
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||
{t('体验视频地址停用提示', '启用后可添加视频 URL(视频生视频、视频编辑等)')}
|
||||
{t('操练场视频地址停用提示', '启用后可添加视频 URL(视频生视频、视频编辑等)')}
|
||||
</Typography.Text>
|
||||
) : list.length === 0 ? (
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||
{t('体验视频地址空列表提示', '点击 + 添加 .mp4 / .mov 等可访问的视频链接')}
|
||||
{t('操练场视频地址空列表提示', '点击 + 添加 .mp4 / .mov 等可访问的视频链接')}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||
|
|
|
|||
|
|
@ -1,379 +0,0 @@
|
|||
/*
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -28,7 +28,6 @@ import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring
|
|||
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit';
|
||||
import SettingsDistributor from '../../pages/Setting/Operation/SettingsDistributor';
|
||||
import SettingsCheckin from '../../pages/Setting/Operation/SettingsCheckin';
|
||||
import SettingsAntiAbuse from '../../pages/Setting/Operation/SettingsAntiAbuse';
|
||||
import { API, showError, toBoolean } from '../../helpers';
|
||||
|
||||
const OperationSetting = () => {
|
||||
|
|
@ -170,10 +169,6 @@ const OperationSetting = () => {
|
|||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsCheckin options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 风控设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsAntiAbuse />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ const OtherSetting = () => {
|
|||
iconLink.rel = 'icon';
|
||||
document.head.appendChild(iconLink);
|
||||
}
|
||||
iconLink.href = inputs.Logo || '/logo.jpg';
|
||||
iconLink.href = inputs.Logo || '/logo.png';
|
||||
showSuccess('Logo 已更新');
|
||||
} catch (error) {
|
||||
console.error('Logo 更新失败', error);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import {
|
|||
} from '../../helpers';
|
||||
import { normalizeSmsVerificationEnabled } from '../../helpers/data';
|
||||
import { UserContext } from '../../context/User';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
import { Modal, Card, Button, Typography } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 导入子组件
|
||||
|
|
@ -555,25 +555,26 @@ const PersonalSetting = () => {
|
|||
|
||||
|
||||
return (
|
||||
<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'>
|
||||
{/* 顶部用户信息区域 */}
|
||||
<UserInfoHeader t={t} userState={userState} />
|
||||
<div className='mt-[60px]'>
|
||||
<div className='flex justify-center'>
|
||||
<div className='w-full max-w-7xl mx-auto px-2'>
|
||||
{/* 顶部用户信息区域 */}
|
||||
<UserInfoHeader t={t} userState={userState} />
|
||||
|
||||
{/* 签到日历 - 仅在启用时显示 */}
|
||||
{status?.checkin_enabled && (
|
||||
<div className='mt-4 md:mt-6'>
|
||||
<CheckinCalendar
|
||||
t={t}
|
||||
status={status}
|
||||
turnstileEnabled={turnstileEnabled}
|
||||
turnstileSiteKey={turnstileSiteKey}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 签到日历 - 仅在启用时显示 */}
|
||||
{status?.checkin_enabled && (
|
||||
<div className='mt-4 md:mt-6'>
|
||||
<CheckinCalendar
|
||||
t={t}
|
||||
status={status}
|
||||
turnstileEnabled={turnstileEnabled}
|
||||
turnstileSiteKey={turnstileSiteKey}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 账户管理和其他设置 */}
|
||||
<div className='mt-4 grid grid-cols-1 items-start gap-4 md:mt-6 md:gap-6 xl:grid-cols-2'>
|
||||
{/* 账户管理和其他设置 */}
|
||||
<div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'>
|
||||
{/* 左侧:账户管理设置 */}
|
||||
<div className='flex flex-col gap-4 md:gap-6'>
|
||||
<AccountManagement
|
||||
|
|
@ -597,8 +598,8 @@ const PersonalSetting = () => {
|
|||
onPasskeyDelete={handleRemovePasskey}
|
||||
/>
|
||||
|
||||
{/* 偏好设置(语言等)- 已隐藏:多语言功能屏蔽,统一使用简体中文 */}
|
||||
{/* <PreferencesSettings t={t} /> */}
|
||||
{/* 偏好设置(语言等) */}
|
||||
<PreferencesSettings t={t} />
|
||||
</div>
|
||||
|
||||
{/* 右侧:其他设置 */}
|
||||
|
|
@ -608,6 +609,8 @@ const PersonalSetting = () => {
|
|||
handleNotificationSettingChange={handleNotificationSettingChange}
|
||||
saveNotificationSettings={saveNotificationSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模态框组件 */}
|
||||
|
|
@ -677,7 +680,6 @@ const PersonalSetting = () => {
|
|||
turnstileSiteKey={turnstileSiteKey}
|
||||
setTurnstileToken={setTurnstileToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -19,15 +19,18 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Calendar,
|
||||
Button,
|
||||
Typography,
|
||||
Avatar,
|
||||
Spin,
|
||||
Tooltip,
|
||||
Collapsible,
|
||||
Modal,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Sparkles,
|
||||
CalendarCheck,
|
||||
Gift,
|
||||
Check,
|
||||
ChevronDown,
|
||||
|
|
@ -210,7 +213,7 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='rounded-2xl border border-slate-200 bg-white p-6 dark:border-white/5 dark:bg-white/[0.02]'>
|
||||
<Card className='!rounded-2xl'>
|
||||
<Modal
|
||||
title='Security Check'
|
||||
visible={turnstileModalVisible}
|
||||
|
|
@ -238,24 +241,24 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
|
|||
{/* 卡片头部 */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div
|
||||
className='flex flex-1 cursor-pointer items-center gap-3'
|
||||
className='flex items-center flex-1 cursor-pointer'
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
<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'>
|
||||
<Sparkles size={18} />
|
||||
</div>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<Avatar size='small' color='green' className='mr-3 shadow-md'>
|
||||
<CalendarCheck size={16} />
|
||||
</Avatar>
|
||||
<div className='flex-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-lg font-semibold tracking-tight text-slate-900 dark:text-white'>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('每日签到')}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
{isCollapsed ? (
|
||||
<ChevronDown size={16} className='text-slate-400 dark:text-white/40' />
|
||||
<ChevronDown size={16} className='text-gray-400' />
|
||||
) : (
|
||||
<ChevronUp size={16} className='text-slate-400 dark:text-white/40' />
|
||||
<ChevronUp size={16} className='text-gray-400' />
|
||||
)}
|
||||
</div>
|
||||
<div className='text-xs text-slate-400 dark:text-white/40'>
|
||||
<div className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
{!initialLoaded
|
||||
? t('正在加载签到状态...')
|
||||
: checkinData.stats?.checked_in_today
|
||||
|
|
@ -273,7 +276,7 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
|
|||
onClick={() => doCheckin()}
|
||||
loading={checkinLoading || !initialLoaded}
|
||||
disabled={!initialLoaded || checkinData.stats?.checked_in_today}
|
||||
className='!rounded-lg'
|
||||
className='!bg-green-600 hover:!bg-green-700'
|
||||
>
|
||||
{!initialLoaded
|
||||
? t('加载中...')
|
||||
|
|
@ -286,36 +289,30 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
|
|||
{/* 可折叠内容 */}
|
||||
<Collapsible isOpen={isCollapsed === false} keepDOM>
|
||||
{/* 签到统计 */}
|
||||
<div className='mt-4 mb-4 grid grid-cols-3 gap-3'>
|
||||
<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 tabular-nums text-emerald-600 dark:text-emerald-400'>
|
||||
<div className='grid grid-cols-3 gap-3 mb-4 mt-4'>
|
||||
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'>
|
||||
<div className='text-xl font-bold text-green-600'>
|
||||
{checkinData.stats?.total_checkins || 0}
|
||||
</div>
|
||||
<div className='text-xs text-slate-400 dark:text-white/40'>
|
||||
{t('累计签到')}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{t('累计签到')}</div>
|
||||
</div>
|
||||
<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 tabular-nums text-amber-600 dark:text-amber-400'>
|
||||
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'>
|
||||
<div className='text-xl font-bold text-orange-600'>
|
||||
{renderQuota(monthlyQuota)}
|
||||
</div>
|
||||
<div className='text-xs text-slate-400 dark:text-white/40'>
|
||||
{t('本月获得')}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{t('本月获得')}</div>
|
||||
</div>
|
||||
<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 tabular-nums text-blue-600 dark:text-blue-400'>
|
||||
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'>
|
||||
<div className='text-xl font-bold text-blue-600'>
|
||||
{renderQuota(checkinData.stats?.total_quota || 0)}
|
||||
</div>
|
||||
<div className='text-xs text-slate-400 dark:text-white/40'>
|
||||
{t('累计获得')}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{t('累计获得')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 签到日历 - 使用更紧凑的样式 */}
|
||||
<Spin spinning={loading}>
|
||||
<div className='checkin-calendar overflow-hidden rounded-xl border border-slate-200 dark:border-white/5'>
|
||||
<div className='border rounded-lg overflow-hidden checkin-calendar'>
|
||||
<style>{`
|
||||
.checkin-calendar .semi-calendar {
|
||||
font-size: 13px;
|
||||
|
|
@ -370,15 +367,17 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
|
|||
</Spin>
|
||||
|
||||
{/* 签到说明 */}
|
||||
<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]'>
|
||||
<ul className='list-disc space-y-0.5 pl-4 text-xs text-slate-400 dark:text-white/40'>
|
||||
<li>{t('每日签到可获得随机额度奖励')}</li>
|
||||
<li>{t('签到奖励将直接添加到您的账户余额')}</li>
|
||||
<li>{t('每日仅可签到一次,请勿重复签到')}</li>
|
||||
</ul>
|
||||
<div className='mt-3 p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'>
|
||||
<Typography.Text type='tertiary' className='text-xs'>
|
||||
<ul className='list-disc list-inside space-y-0.5'>
|
||||
<li>{t('每日签到可获得随机额度奖励')}</li>
|
||||
<li>{t('签到奖励将直接添加到您的账户余额')}</li>
|
||||
<li>{t('每日仅可签到一次,请勿重复签到')}</li>
|
||||
</ul>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import React, { useRef, useEffect, useState, useContext } from 'react';
|
|||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Card,
|
||||
Avatar,
|
||||
Form,
|
||||
Radio,
|
||||
Toast,
|
||||
|
|
@ -31,13 +33,7 @@ import {
|
|||
Col,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
BellRing,
|
||||
Coins,
|
||||
EyeOff,
|
||||
LayoutPanelLeft,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react';
|
||||
import {
|
||||
renderQuotaWithPrompt,
|
||||
API,
|
||||
|
|
@ -216,11 +212,11 @@ const NotificationSettings = ({
|
|||
{
|
||||
key: 'chat',
|
||||
title: t('聊天区域'),
|
||||
description: t('体验和聊天功能'),
|
||||
description: t('操练场和聊天功能'),
|
||||
modules: [
|
||||
{
|
||||
key: 'playground',
|
||||
title: t('体验'),
|
||||
title: t('操练场'),
|
||||
description: t('AI模型测试环境'),
|
||||
},
|
||||
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
|
||||
|
|
@ -232,7 +228,7 @@ const NotificationSettings = ({
|
|||
description: t('数据管理和日志查看'),
|
||||
modules: [
|
||||
{ key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
|
||||
{ key: 'token', title: t('API Key'), description: t('APIAPI Key') },
|
||||
{ key: 'token', title: t('令牌管理'), description: t('API令牌管理') },
|
||||
{ key: 'log', title: t('使用日志'), description: t('API使用记录') },
|
||||
{
|
||||
key: 'midjourney',
|
||||
|
|
@ -321,16 +317,48 @@ const NotificationSettings = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='rounded-2xl border border-slate-200 bg-white p-6 dark:border-white/5 dark:bg-white/[0.02]'>
|
||||
<div className='mb-5 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'>
|
||||
<Wrench size={18} />
|
||||
<Card
|
||||
className='!rounded-2xl shadow-sm border-0'
|
||||
footer={
|
||||
<div className='flex justify-end gap-3'>
|
||||
{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 className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='blue' className='mr-3 shadow-md'>
|
||||
<Bell size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className='text-lg font-semibold tracking-tight text-slate-900 dark:text-white'>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('其他设置')}
|
||||
</div>
|
||||
<div className='text-xs text-slate-400 dark:text-white/40'>
|
||||
</Typography.Text>
|
||||
<div className='text-xs text-gray-600'>
|
||||
{t('通知、价格和隐私相关设置')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -351,7 +379,7 @@ const NotificationSettings = ({
|
|||
<TabPane
|
||||
tab={
|
||||
<div className='flex items-center'>
|
||||
<BellRing size={14} className='mr-2' />
|
||||
<Bell size={16} className='mr-2' />
|
||||
{t('通知配置')}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -691,7 +719,7 @@ const NotificationSettings = ({
|
|||
<TabPane
|
||||
tab={
|
||||
<div className='flex items-center'>
|
||||
<Coins size={14} className='mr-2' />
|
||||
<DollarSign size={16} className='mr-2' />
|
||||
{t('价格设置')}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -717,7 +745,7 @@ const NotificationSettings = ({
|
|||
<TabPane
|
||||
tab={
|
||||
<div className='flex items-center'>
|
||||
<EyeOff size={14} className='mr-2' />
|
||||
<ShieldCheck size={16} className='mr-2' />
|
||||
{t('隐私设置')}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -742,7 +770,7 @@ const NotificationSettings = ({
|
|||
<TabPane
|
||||
tab={
|
||||
<div className='flex items-center'>
|
||||
<LayoutPanelLeft size={14} className='mr-2' />
|
||||
<Settings size={16} className='mr-2' />
|
||||
{t('边栏设置')}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -763,19 +791,36 @@ const NotificationSettings = ({
|
|||
</Typography.Text>
|
||||
</div>
|
||||
{/* 边栏设置功能区域容器 */}
|
||||
<div className='rounded-2xl border border-slate-200 bg-slate-50/50 p-4 dark:border-white/5 dark:bg-white/[0.02]'>
|
||||
<div
|
||||
className='border rounded-xl p-4'
|
||||
style={{
|
||||
borderColor: 'var(--semi-color-border)',
|
||||
backgroundColor: 'var(--semi-color-bg-1)',
|
||||
}}
|
||||
>
|
||||
{sectionConfigs.map((section) => (
|
||||
<div key={section.key} className='mb-6 last:mb-0'>
|
||||
<div key={section.key} className='mb-6'>
|
||||
{/* 区域标题和总开关 */}
|
||||
<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]'>
|
||||
<div className='min-w-0'>
|
||||
<div className='mb-1 text-base font-semibold text-slate-900 dark:text-white'>
|
||||
<div
|
||||
className='flex justify-between items-center mb-4 p-4 rounded-lg'
|
||||
style={{
|
||||
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}
|
||||
</div>
|
||||
<Typography.Text
|
||||
type='secondary'
|
||||
size='small'
|
||||
className='text-xs text-slate-400 dark:text-white/40'
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
color: 'var(--semi-color-text-2)',
|
||||
}}
|
||||
>
|
||||
{section.description}
|
||||
</Typography.Text>
|
||||
|
|
@ -800,23 +845,30 @@ const NotificationSettings = ({
|
|||
lg={8}
|
||||
xl={8}
|
||||
>
|
||||
<div
|
||||
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 ${
|
||||
<Card
|
||||
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
|
||||
sidebarModulesUser[section.key]?.enabled !==
|
||||
false
|
||||
? ''
|
||||
: 'opacity-50'
|
||||
}`}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
<div className='flex h-full items-center justify-between'>
|
||||
<div className='min-w-0 flex-1 text-left'>
|
||||
<div className='mb-1 text-sm font-semibold text-slate-900 dark:text-white'>
|
||||
<div className='flex justify-between items-center h-full'>
|
||||
<div className='flex-1 text-left'>
|
||||
<div className='font-semibold text-sm text-gray-900 mb-1'>
|
||||
{module.title}
|
||||
</div>
|
||||
<Typography.Text
|
||||
type='secondary'
|
||||
size='small'
|
||||
className='mt-1 block text-xs text-slate-400 dark:text-white/40'
|
||||
className='block'
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
color: 'var(--semi-color-text-2)',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
{module.description}
|
||||
</Typography.Text>
|
||||
|
|
@ -840,7 +892,7 @@ const NotificationSettings = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
|
@ -854,41 +906,7 @@ const NotificationSettings = ({
|
|||
</Tabs>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
{/* 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>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Select } from '@douyinfe/semi-ui';
|
||||
import { SlidersHorizontal, Globe } from 'lucide-react';
|
||||
import { Card, Select, Typography, Avatar } from '@douyinfe/semi-ui';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, showSuccess, showError } from '../../../../helpers';
|
||||
import { UserContext } from '../../../../context/User';
|
||||
|
|
@ -124,55 +124,62 @@ const PreferencesSettings = ({ t }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='rounded-2xl border border-slate-200 bg-white p-6 dark:border-white/5 dark:bg-white/[0.02]'>
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
{/* Card Header */}
|
||||
<div className='mb-5 flex items-center gap-3'>
|
||||
<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'>
|
||||
<SlidersHorizontal size={18} />
|
||||
</div>
|
||||
<div className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='violet' className='mr-3 shadow-md'>
|
||||
<Languages size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className='text-lg font-semibold tracking-tight text-slate-900 dark:text-white'>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('偏好设置')}
|
||||
</div>
|
||||
<div className='text-xs text-slate-400 dark:text-white/40'>
|
||||
</Typography.Text>
|
||||
<div className='text-xs text-gray-600 dark:text-gray-400'>
|
||||
{t('界面语言和其他个人偏好')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Language Setting Card */}
|
||||
<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 w-full items-start gap-3 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'>
|
||||
<Globe size={18} />
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
<div className='text-sm font-semibold text-slate-900 dark:text-white'>
|
||||
{t('语言偏好')}
|
||||
<Card className='!rounded-xl border dark:border-gray-700'>
|
||||
<div className='flex flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-4'>
|
||||
<div className='flex items-start w-full sm:w-auto'>
|
||||
<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'>
|
||||
<Languages
|
||||
size={20}
|
||||
className='text-violet-600 dark:text-violet-400'
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-0.5 text-xs text-slate-400 dark:text-white/40'>
|
||||
{t('选择您的首选界面语言,设置将自动保存并同步到所有设备')}
|
||||
<div>
|
||||
<Typography.Title heading={6} className='mb-1'>
|
||||
{t('语言偏好')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type='tertiary' className='text-sm'>
|
||||
{t('选择您的首选界面语言,设置将自动保存并同步到所有设备')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onChange={handleLanguagePreferenceChange}
|
||||
style={{ width: 180 }}
|
||||
loading={loading}
|
||||
optionList={languageOptions.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onChange={handleLanguagePreferenceChange}
|
||||
style={{ width: 180 }}
|
||||
loading={loading}
|
||||
optionList={languageOptions.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Additional info */}
|
||||
<div className='mt-4 text-xs text-slate-400 dark:text-white/40'>
|
||||
{t(
|
||||
'提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。',
|
||||
)}
|
||||
<div className='mt-4 text-xs text-gray-500 dark:text-gray-400'>
|
||||
<Typography.Text type='tertiary'>
|
||||
{t(
|
||||
'提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。',
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -32,19 +32,18 @@ import {
|
|||
Badge,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconShield,
|
||||
IconAlertTriangle,
|
||||
IconRefresh,
|
||||
IconCopy,
|
||||
IconShield,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { ScanFace } from 'lucide-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
const TwoFASetting = ({ t, compact = false }) => {
|
||||
const TwoFASetting = ({ t }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [status, setStatus] = useState({
|
||||
enabled: false,
|
||||
|
|
@ -367,23 +366,20 @@ const TwoFASetting = ({ t, compact = false }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
compact
|
||||
? ''
|
||||
: 'rounded-2xl border border-slate-200 bg-white p-5 dark:border-white/5 dark:bg-white/[0.02]'
|
||||
}
|
||||
>
|
||||
<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} />
|
||||
<Card className='!rounded-xl w-full'>
|
||||
<div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||
<div className='flex items-start w-full sm:w-auto'>
|
||||
<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
|
||||
size='large'
|
||||
className='text-slate-600 dark:text-slate-300'
|
||||
/>
|
||||
</div>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='mb-1 flex items-center gap-2'>
|
||||
<span className='text-sm font-semibold text-slate-900 dark:text-white'>
|
||||
<div className='flex-1'>
|
||||
<div className='flex items-center gap-2 mb-1'>
|
||||
<Typography.Title heading={6} className='mb-0'>
|
||||
{t('两步验证设置')}
|
||||
</span>
|
||||
</Typography.Title>
|
||||
{status.enabled ? (
|
||||
<Tag color='green' shape='circle' size='small'>
|
||||
{t('已启用')}
|
||||
|
|
@ -399,24 +395,23 @@ const TwoFASetting = ({ t, compact = false }) => {
|
|||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
<Typography.Text
|
||||
type='tertiary'
|
||||
className='text-xs text-slate-400 dark:text-white/40'
|
||||
>
|
||||
<Typography.Text type='tertiary' className='text-sm'>
|
||||
{t(
|
||||
'两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。',
|
||||
)}
|
||||
</Typography.Text>
|
||||
{status.enabled && (
|
||||
<div className='mt-2 text-xs text-slate-400 dark:text-white/40'>
|
||||
{t('剩余备用码:')}
|
||||
{status.backup_codes_remaining || 0}
|
||||
{t('个')}
|
||||
<div className='mt-2'>
|
||||
<Text size='small' type='secondary'>
|
||||
{t('剩余备用码:')}
|
||||
{status.backup_codes_remaining || 0}
|
||||
{t('个')}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex w-full flex-col gap-2 sm:w-auto sm:self-center'>
|
||||
<div className='flex flex-col space-y-2 w-full sm:w-auto'>
|
||||
{!status.enabled ? (
|
||||
<Button
|
||||
type='primary'
|
||||
|
|
@ -424,19 +419,19 @@ const TwoFASetting = ({ t, compact = false }) => {
|
|||
size='default'
|
||||
onClick={handleSetup2FA}
|
||||
loading={loading}
|
||||
className='!rounded-lg'
|
||||
icon={<ScanFace />}
|
||||
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||
icon={<IconShield />}
|
||||
>
|
||||
{t('启用验证')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex flex-col space-y-2'>
|
||||
<Button
|
||||
type='danger'
|
||||
theme='solid'
|
||||
size='default'
|
||||
onClick={() => setDisableModalVisible(true)}
|
||||
className='!rounded-lg'
|
||||
className='!rounded-lg !bg-slate-500 hover:!bg-slate-600'
|
||||
icon={<IconAlertTriangle />}
|
||||
>
|
||||
{t('禁用两步验证')}
|
||||
|
|
@ -455,7 +450,7 @@ const TwoFASetting = ({ t, compact = false }) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 2FA设置模态框 */}
|
||||
<Modal
|
||||
|
|
|
|||
|
|
@ -18,154 +18,211 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Tag } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Coins,
|
||||
BarChart2,
|
||||
Users,
|
||||
Crown,
|
||||
ShieldCheck,
|
||||
User as UserIcon,
|
||||
Store,
|
||||
} from 'lucide-react';
|
||||
import { Avatar, Card, Tag, Divider, Typography } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
isRoot,
|
||||
isAdmin,
|
||||
isDistributor,
|
||||
renderQuota,
|
||||
stringToColor,
|
||||
} from '../../../../helpers';
|
||||
|
||||
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: '普通用户',
|
||||
};
|
||||
import { Coins, BarChart2, Users } from 'lucide-react';
|
||||
|
||||
const UserInfoHeader = ({ t, userState }) => {
|
||||
const username = userState.user?.username || 'null';
|
||||
const roleKey = isRoot()
|
||||
? 'root'
|
||||
: isAdmin()
|
||||
? 'admin'
|
||||
: isDistributor()
|
||||
? 'distributor'
|
||||
: 'user';
|
||||
const RoleIcon =
|
||||
roleKey === 'root'
|
||||
? Crown
|
||||
: roleKey === 'admin'
|
||||
? ShieldCheck
|
||||
: roleKey === 'distributor'
|
||||
? Store
|
||||
: UserIcon;
|
||||
const getUsername = () => {
|
||||
if (userState.user) {
|
||||
return userState.user.username;
|
||||
} else {
|
||||
return 'null';
|
||||
}
|
||||
};
|
||||
|
||||
const stats = [
|
||||
{
|
||||
key: 'quota',
|
||||
label: t('当前余额'),
|
||||
value: renderQuota(userState?.user?.quota),
|
||||
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,
|
||||
},
|
||||
];
|
||||
const getAvatarText = () => {
|
||||
const username = getUsername();
|
||||
if (username && username.length > 0) {
|
||||
return username.slice(0, 2).toUpperCase();
|
||||
}
|
||||
return 'NA';
|
||||
};
|
||||
|
||||
return (
|
||||
<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'>
|
||||
{/* 头部:头像 + 用户名 + 角色 */}
|
||||
<div className='flex items-center gap-4'>
|
||||
<Card
|
||||
className='!rounded-2xl overflow-hidden'
|
||||
cover={
|
||||
<div
|
||||
className={`flex h-14 w-14 items-center justify-center rounded-2xl text-base font-semibold ${roleAccent[roleKey]}`}
|
||||
className='relative h-32'
|
||||
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>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<h1 className='truncate text-xl font-semibold tracking-tight text-slate-900 dark:text-white'>
|
||||
{username}
|
||||
</h1>
|
||||
<Tag
|
||||
shape='circle'
|
||||
size='small'
|
||||
className='!rounded-full'
|
||||
style={{
|
||||
color: 'inherit',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs ${roleAccent[roleKey]}`}
|
||||
>
|
||||
<RoleIcon size={12} />
|
||||
{t(roleLabelKey[roleKey])}
|
||||
</span>
|
||||
</Tag>
|
||||
<span className='text-xs text-slate-400 dark:text-white/40 tabular-nums'>
|
||||
ID: {userState?.user?.id}
|
||||
</span>
|
||||
</div>
|
||||
<p className='mt-1 text-xs text-slate-400 dark:text-white/40'>
|
||||
{t('账户概览与使用统计')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据三宫格 */}
|
||||
<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'>
|
||||
{stats.map((s) => {
|
||||
const Icon = s.icon;
|
||||
return (
|
||||
<div
|
||||
key={s.key}
|
||||
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]'
|
||||
>
|
||||
<div
|
||||
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl ${s.accent}`}
|
||||
>
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
<div className='text-xs text-slate-400 dark:text-white/40'>
|
||||
{s.label}
|
||||
</div>
|
||||
<div className='truncate text-base font-semibold tabular-nums text-slate-900 dark:text-white'>
|
||||
{s.value}
|
||||
{/* 用户信息内容 */}
|
||||
<div className='relative z-10 h-full flex flex-col justify-end p-6'>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex items-stretch gap-3 sm:gap-4 flex-1 min-w-0'>
|
||||
<Avatar size='large' color={stringToColor(getUsername())}>
|
||||
{getAvatarText()}
|
||||
</Avatar>
|
||||
<div className='flex-1 min-w-0 flex flex-col justify-between'>
|
||||
<div
|
||||
className='text-3xl font-bold truncate'
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
{getUsername()}
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
{isRoot() ? (
|
||||
<Tag
|
||||
size='large'
|
||||
shape='circle'
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
{t('超级管理员')}
|
||||
</Tag>
|
||||
) : isAdmin() ? (
|
||||
<Tag
|
||||
size='large'
|
||||
shape='circle'
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
{t('管理员')}
|
||||
</Tag>
|
||||
) : isDistributor() ? (
|
||||
<Tag
|
||||
size='large'
|
||||
shape='circle'
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
{t('代理')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag
|
||||
size='large'
|
||||
shape='circle'
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
{t('普通用户')}
|
||||
</Tag>
|
||||
)}
|
||||
<Tag size='large' shape='circle' style={{ color: 'white' }}>
|
||||
ID: {userState?.user?.id}
|
||||
</Tag>
|
||||
</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 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ export default function ChannelImportModal({ refresh }) {
|
|||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type='quaternary' size='small'>
|
||||
{t('此密钥将作为所有建站渠道的 API Key,用于访问上游平台。请在目标平台的API Key页面创建获取。')}
|
||||
{t('此密钥将作为所有建站渠道的 API Key,用于访问上游平台。请在目标平台的令牌管理页面创建获取。')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,211 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
/*
|
||||
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;
|
||||
|
|
@ -18,8 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { ImagePreview } from '@douyinfe/semi-ui';
|
||||
import ModelsContent from './ModelsContent';
|
||||
import { Layout, ImagePreview } from '@douyinfe/semi-ui';
|
||||
import PricingSidebar from './PricingSidebar';
|
||||
import PricingContent from './content/PricingContent';
|
||||
import ModelDetailSideSheet from '../modal/ModelDetailSideSheet';
|
||||
import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
|
|
@ -28,6 +29,7 @@ import { UserContext } from '../../../../context/User';
|
|||
|
||||
const PricingPage = () => {
|
||||
const pricingData = useModelPricingData();
|
||||
const { Sider, Content } = Layout;
|
||||
const isMobile = useIsMobile();
|
||||
const [showRatio, setShowRatio] = React.useState(false);
|
||||
const [viewMode, setViewMode] = React.useState('card');
|
||||
|
|
@ -58,12 +60,22 @@ const PricingPage = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='sf-pricing-page'>
|
||||
<ModelsContent
|
||||
{...allProps}
|
||||
isMobile={isMobile}
|
||||
sidebarProps={allProps}
|
||||
/>
|
||||
<div className='bg-white'>
|
||||
<Layout className='pricing-layout'>
|
||||
{!isMobile && (
|
||||
<Sider className='pricing-scroll-hide pricing-sidebar'>
|
||||
<PricingSidebar {...allProps} />
|
||||
</Sider>
|
||||
)}
|
||||
|
||||
<Content className='pricing-scroll-hide pricing-content'>
|
||||
<PricingContent
|
||||
{...allProps}
|
||||
isMobile={isMobile}
|
||||
sidebarProps={allProps}
|
||||
/>
|
||||
</Content>
|
||||
</Layout>
|
||||
|
||||
<ImagePreview
|
||||
src={pricingData.modalImageUrl}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import PricingGroups from '../filter/PricingGroups';
|
||||
import PricingQuotaTypes from '../filter/PricingQuotaTypes';
|
||||
import PricingEndpointTypes from '../filter/PricingEndpointTypes';
|
||||
|
|
@ -98,22 +99,17 @@ const PricingSidebar = ({
|
|||
});
|
||||
|
||||
return (
|
||||
<div className='px-4 py-5'>
|
||||
<div className='mb-5 flex items-center justify-between border-b border-slate-100 pb-4 dark:border-white/5'>
|
||||
<div>
|
||||
<div className='text-sm font-semibold tracking-tight text-slate-900 dark:text-white'>
|
||||
{t('筛选')}
|
||||
</div>
|
||||
<div className='mt-0.5 text-[11px] text-slate-400 tabular-nums dark:text-white/40'>
|
||||
调整左侧条件实时刷新结果
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<div className='p-2'>
|
||||
<div className='flex items-center justify-between mb-6'>
|
||||
<div className='text-lg font-semibold text-gray-800'>{t('筛选')}</div>
|
||||
<Button
|
||||
theme='outline'
|
||||
type='tertiary'
|
||||
onClick={handleResetFilters}
|
||||
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'
|
||||
className='text-gray-500 hover:text-gray-700'
|
||||
>
|
||||
{t('重置')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PricingVendors
|
||||
|
|
|
|||
|
|
@ -18,32 +18,49 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback, memo } from 'react';
|
||||
import { Tooltip, Modal } from '@douyinfe/semi-ui';
|
||||
import { Layers } from 'lucide-react';
|
||||
import {
|
||||
Card,
|
||||
Tag,
|
||||
Avatar,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Modal,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { getLobeHubIcon } from '../../../../../helpers';
|
||||
import SearchActions from './SearchActions';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
const CONFIG = {
|
||||
CAROUSEL_INTERVAL: 2000,
|
||||
ICON_SIZE: 32,
|
||||
ICON_SIZE: 40,
|
||||
UNKNOWN_VENDOR: 'unknown',
|
||||
};
|
||||
|
||||
const THEME = {
|
||||
const THEME_COLORS = {
|
||||
allVendors: {
|
||||
iconWrap:
|
||||
'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',
|
||||
primary: '37 99 235',
|
||||
background: 'rgba(59, 130, 246, 0.08)',
|
||||
},
|
||||
specific: {
|
||||
iconWrap:
|
||||
'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',
|
||||
primary: '16 185 129',
|
||||
background: 'rgba(16, 185, 129, 0.1)',
|
||||
},
|
||||
};
|
||||
|
||||
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 = {
|
||||
unknown: {
|
||||
displayName: (t) => t('未知模型类型'),
|
||||
|
|
@ -68,32 +85,35 @@ const getVendorDisplayName = (vendorName, t) => {
|
|||
};
|
||||
|
||||
const createDefaultAvatar = () => (
|
||||
<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'>
|
||||
<Layers size={22} />
|
||||
<div className={COMPONENT_STYLES.avatarContainer}>
|
||||
<Avatar size='large' color='transparent'>
|
||||
AI
|
||||
</Avatar>
|
||||
</div>
|
||||
);
|
||||
|
||||
const getAvatarBackgroundColor = (isAllVendors) =>
|
||||
isAllVendors
|
||||
? THEME_COLORS.allVendors.background
|
||||
: THEME_COLORS.specific.background;
|
||||
|
||||
const getAvatarText = (vendorName) =>
|
||||
vendorName === CONFIG.UNKNOWN_VENDOR
|
||||
? '?'
|
||||
: (vendorName || '').charAt(0).toUpperCase() || '?';
|
||||
|
||||
const createAvatarContent = (vendor, iconWrapClass) => {
|
||||
const createAvatarContent = (vendor, isAllVendors) => {
|
||||
if (vendor.icon) {
|
||||
return (
|
||||
<div
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-xl ${iconWrapClass}`}
|
||||
>
|
||||
{getLobeHubIcon(vendor.icon, CONFIG.ICON_SIZE)}
|
||||
</div>
|
||||
);
|
||||
return getLobeHubIcon(vendor.icon, CONFIG.ICON_SIZE);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-xl font-mono text-lg font-semibold ${iconWrapClass}`}
|
||||
<Avatar
|
||||
size='large'
|
||||
style={{ backgroundColor: getAvatarBackgroundColor(isAllVendors) }}
|
||||
>
|
||||
{getAvatarText(vendor.name)}
|
||||
</div>
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -103,14 +123,11 @@ const renderVendorAvatar = (vendor, t, isAllVendors = false) => {
|
|||
}
|
||||
|
||||
const displayName = getVendorDisplayName(vendor.name, t);
|
||||
const iconWrapClass = isAllVendors
|
||||
? THEME.allVendors.iconWrap
|
||||
: THEME.specific.iconWrap;
|
||||
const avatarContent = createAvatarContent(vendor, iconWrapClass);
|
||||
const avatarContent = createAvatarContent(vendor, isAllVendors);
|
||||
|
||||
return (
|
||||
<Tooltip content={displayName} position='top'>
|
||||
{avatarContent}
|
||||
<div className={COMPONENT_STYLES.avatarContainer}>{avatarContent}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
@ -242,6 +259,17 @@ const PricingVendorIntro = memo(
|
|||
[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(
|
||||
() => (
|
||||
<SearchActions
|
||||
|
|
@ -290,41 +318,51 @@ const PricingVendorIntro = memo(
|
|||
);
|
||||
|
||||
const renderHeaderCard = useCallback(
|
||||
({ title, count, description, rightContent, theme }) => (
|
||||
<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'>
|
||||
<div className='flex items-start gap-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='mb-1.5 flex flex-wrap items-center gap-2'>
|
||||
<h2 className='truncate text-lg font-semibold tracking-tight text-slate-900 sm:text-xl dark:text-white'>
|
||||
{title}
|
||||
</h2>
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-[11px] font-medium ${theme.badge}`}
|
||||
>
|
||||
{t('共 {{count}} 个模型', { count })}
|
||||
</span>
|
||||
({ title, count, description, rightContent, primaryDarkerChannel }) => (
|
||||
<Card
|
||||
className='!rounded-2xl shadow-sm border-0'
|
||||
cover={
|
||||
<div
|
||||
className='relative h-full'
|
||||
style={createCoverStyle(primaryDarkerChannel)}
|
||||
>
|
||||
<div className='relative z-10 h-full flex items-center justify-between p-4'>
|
||||
<div className='flex-1 min-w-0 mr-4'>
|
||||
<div className='flex flex-row flex-wrap items-center gap-2 sm:gap-3 mb-2'>
|
||||
<h2
|
||||
className='text-lg sm:text-xl font-bold truncate'
|
||||
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>
|
||||
<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 className='flex shrink-0 items-center'>{rightContent}</div>
|
||||
</div>
|
||||
<div className='mt-5 border-t border-slate-100 pt-4 dark:border-white/5'>
|
||||
{renderSearchActions()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{renderSearchActions()}
|
||||
</Card>
|
||||
),
|
||||
[renderSearchActions, handleOpenDescModal, t],
|
||||
[renderSearchActions, createCoverStyle, handleOpenDescModal, t],
|
||||
);
|
||||
|
||||
const renderAllVendorsAvatar = useCallback(() => {
|
||||
|
|
@ -341,7 +379,7 @@ const PricingVendorIntro = memo(
|
|||
count: currentModelCount,
|
||||
description: getVendorDescription('all'),
|
||||
rightContent: renderAllVendorsAvatar(),
|
||||
theme: THEME.allVendors,
|
||||
primaryDarkerChannel: THEME_COLORS.allVendors.primary,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
|
|
@ -364,7 +402,7 @@ const PricingVendorIntro = memo(
|
|||
description:
|
||||
currentVendor.description || getVendorDescription(currentVendor.name),
|
||||
rightContent: renderVendorAvatar(currentVendor, t, false),
|
||||
theme: THEME.specific,
|
||||
primaryDarkerChannel: THEME_COLORS.specific.primary,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ const SearchActions = memo(
|
|||
}, [tokenUnit, setTokenUnit]);
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap items-center gap-2 w-full'>
|
||||
<div className='min-w-[180px] flex-1'>
|
||||
<div className='flex items-center gap-2 w-full'>
|
||||
<div className='flex-1'>
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('模糊搜索模型名称')}
|
||||
|
|
@ -80,10 +80,11 @@ const SearchActions = memo(
|
|||
|
||||
<Button
|
||||
theme='outline'
|
||||
type='tertiary'
|
||||
type='primary'
|
||||
icon={<IconCopy />}
|
||||
onClick={handleCopyClick}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
className='!bg-blue-500 hover:!bg-blue-600 !text-white disabled:!bg-gray-300 disabled:!text-gray-500'
|
||||
>
|
||||
{t('复制')}
|
||||
</Button>
|
||||
|
|
@ -95,7 +96,7 @@ const SearchActions = memo(
|
|||
{/* 充值价格显示开关 */}
|
||||
{supportsCurrencyDisplay && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-sm text-slate-500 dark:text-white/50'>
|
||||
<span className='text-sm text-gray-600'>
|
||||
{t('充值价格显示')}
|
||||
</span>
|
||||
<Switch
|
||||
|
|
@ -120,7 +121,7 @@ const SearchActions = memo(
|
|||
|
||||
{/* 显示倍率开关 */}
|
||||
{/* <div className='flex items-center gap-2'>
|
||||
<span className='text-sm text-slate-500 dark:text-white/50'>{t('倍率')}</span>
|
||||
<span className='text-sm text-gray-600'>{t('倍率')}</span>
|
||||
<Switch checked={showRatio} onChange={setShowRatio} />
|
||||
</div> */}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue