From 6f6d818197be40ba1d5a2a9c0c4c9f0e1403fa59 Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Tue, 9 Jan 2024 23:52:52 +0800 Subject: [PATCH] feat release: admin model market feature --- admin/controller.go | 24 ++ admin/instance.go | 7 + admin/market.go | 57 +++ admin/router.go | 2 + app/src/api/v1.ts | 33 ++ app/src/assets/admin/dashboard.less | 21 +- app/src/assets/admin/market.less | 5 + app/src/assets/admin/menu.less | 29 +- app/src/assets/globals.less | 1 + app/src/components/admin/MenuBar.tsx | 16 +- app/src/components/app/AppProvider.tsx | 34 +- app/src/components/ui/tooltip.tsx | 1 + app/src/conf.ts | 514 +------------------------ app/src/routes/admin/Market.tsx | 55 ++- app/src/store/chat.ts | 41 +- app/src/utils/base.ts | 9 + app/src/utils/memory.ts | 4 + app/src/utils/storage.ts | 9 + connection/database.go | 3 + main.go | 1 + manager/relay.go | 5 + manager/router.go | 1 + 22 files changed, 293 insertions(+), 579 deletions(-) create mode 100644 admin/instance.go create mode 100644 admin/market.go create mode 100644 app/src/api/v1.ts diff --git a/admin/controller.go b/admin/controller.go index aa7c4b69..6f298c76 100644 --- a/admin/controller.go +++ b/admin/controller.go @@ -33,6 +33,30 @@ type UpdateRootPasswordForm struct { Password string `json:"password"` } +func UpdateMarketAPI(c *gin.Context) { + var form MarketModelList + if err := c.ShouldBindJSON(&form); err != nil { + c.JSON(http.StatusOK, gin.H{ + "status": false, + "error": err.Error(), + }) + return + } + + err := MarketInstance.SetModels(form) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "status": false, + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "status": true, + }) +} + func InfoAPI(c *gin.Context) { db := utils.GetDBFromContext(c) cache := utils.GetCacheFromContext(c) diff --git a/admin/instance.go b/admin/instance.go new file mode 100644 index 00000000..2a73e17a --- /dev/null +++ b/admin/instance.go @@ -0,0 +1,7 @@ +package admin + +var MarketInstance *Market + +func InitInstance() { + MarketInstance = NewMarket() +} diff --git a/admin/market.go b/admin/market.go new file mode 100644 index 00000000..70829fe7 --- /dev/null +++ b/admin/market.go @@ -0,0 +1,57 @@ +package admin + +import ( + "fmt" + "github.com/spf13/viper" +) + +type ModelTag []string +type MarketModel struct { + Id string `json:"id" mapstructure:"id" required:"true"` + Name string `json:"name" mapstructure:"name" required:"true"` + Description string `json:"description" mapstructure:"description"` + Default bool `json:"default" mapstructure:"default"` + HighContext bool `json:"high_context" mapstructure:"high_context"` + Avatar string `json:"avatar" mapstructure:"avatar"` + Tag ModelTag `json:"tag" mapstructure:"tag"` +} +type MarketModelList []MarketModel + +type Market struct { + Models MarketModelList `json:"models" mapstructure:"models"` +} + +func NewMarket() *Market { + var models MarketModelList + if err := viper.UnmarshalKey("market", &models); err != nil { + fmt.Println(fmt.Sprintf("[market] read config error: %s, use default config", err.Error())) + models = MarketModelList{} + } + + return &Market{ + Models: models, + } +} + +func (m *Market) GetModels() MarketModelList { + return m.Models +} + +func (m *Market) GetModel(id string) *MarketModel { + for _, model := range m.Models { + if model.Id == id { + return &model + } + } + return nil +} + +func (m *Market) SaveConfig() error { + viper.Set("market", m.Models) + return viper.WriteConfig() +} + +func (m *Market) SetModels(models MarketModelList) error { + m.Models = models + return m.SaveConfig() +} diff --git a/admin/router.go b/admin/router.go index 1255a80a..554414aa 100644 --- a/admin/router.go +++ b/admin/router.go @@ -24,4 +24,6 @@ func Register(app *gin.RouterGroup) { app.POST("/admin/user/quota", UserQuotaAPI) app.POST("/admin/user/subscription", UserSubscriptionAPI) app.POST("/admin/user/root", UpdateRootPasswordAPI) + + app.POST("/admin/market/update", UpdateMarketAPI) } diff --git a/app/src/api/v1.ts b/app/src/api/v1.ts new file mode 100644 index 00000000..5022da93 --- /dev/null +++ b/app/src/api/v1.ts @@ -0,0 +1,33 @@ +import axios from "axios"; +import { Model } from "@/api/types.ts"; +import { ChargeProps } from "@/admin/charge.ts"; + +export async function getApiModels(): Promise { + try { + const res = await axios.get("/v1/models"); + return res.data as string[]; + } catch (e) { + console.warn(e); + return []; + } +} + +export async function getApiMarket(): Promise { + try { + const res = await axios.get("/v1/market"); + return res.data as Model[]; + } catch (e) { + console.warn(e); + return []; + } +} + +export async function getApiCharge(): Promise { + try { + const res = await axios.get("/v1/charge"); + return res.data as ChargeProps[]; + } catch (e) { + console.warn(e); + return []; + } +} diff --git a/app/src/assets/admin/dashboard.less b/app/src/assets/admin/dashboard.less index b3a7df3a..c88fff5a 100644 --- a/app/src/assets/admin/dashboard.less +++ b/app/src/assets/admin/dashboard.less @@ -21,7 +21,7 @@ width: 100%; height: max-content; - padding: 1rem 2rem; + padding: 1rem 2rem 0; @media (max-width: 940px) { flex-direction: column; @@ -102,9 +102,18 @@ display: flex; flex-direction: row; flex-wrap: wrap; - padding: 1rem 1.5rem; + padding: 0 1.5rem 1rem; width: 100%; + @media (max-width: 940px) { + flex-direction: column; + padding: 1rem; + + .chart-box { + width: calc(100% - 1rem) !important; + } + } + .chart-box { width: calc(50% - 1rem); height: max-content; @@ -116,13 +125,5 @@ background: hsl(var(--background)); box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow); user-select: none; - - @media (max-width: 680px) { - width: 100%; - } - } - - @media (max-width: 680px) { - padding: 1rem; } } diff --git a/app/src/assets/admin/market.less b/app/src/assets/admin/market.less index 0d932bc8..d5d1eec3 100644 --- a/app/src/assets/admin/market.less +++ b/app/src/assets/admin/market.less @@ -43,6 +43,11 @@ height: max-content; align-items: center; background: hsl(var(--card)); + transition: 0.25s; + + &.error { + border-color: hsl(var(--error)); + } .market-tags { display: flex; diff --git a/app/src/assets/admin/menu.less b/app/src/assets/admin/menu.less index d5e58a20..8e13b601 100644 --- a/app/src/assets/admin/menu.less +++ b/app/src/assets/admin/menu.less @@ -28,7 +28,7 @@ height: max-content; padding: 0.75rem 1rem; align-items: center; - margin: 0 0.75rem; + margin: 0.15rem 0.75rem; user-select: none; cursor: pointer; transition: 0.2s ease-in-out; @@ -36,35 +36,13 @@ font-size: 16px; color: hsl(var(--text-secondary)); - &:before { - content: ''; - display: inline-block; - width: 4px; - height: 2rem; - background: hsl(var(--text)); - border-radius: var(--radius); - margin-right: 0; - opacity: 0; - transition: 0.25s; - transition-property: opacity, margin-right; - } - &:hover { background: hsl(var(--card-hover)); - - &:before { - opacity: .05; - margin-right: 0.75rem; - } } &.active { color: hsl(var(--text)); - - &:before { - opacity: 1; - margin-right: 0.75rem; - } + background: hsl(var(--card-hover)); } & > * { @@ -78,7 +56,10 @@ .menu-item-icon { width: 1.5rem; height: 1.5rem; + scale: 0.95; margin-right: 0.5rem; + margin-left: 0.5rem; + transform: translateY(1px); } } diff --git a/app/src/assets/globals.less b/app/src/assets/globals.less index c1f8932d..934eb906 100644 --- a/app/src/assets/globals.less +++ b/app/src/assets/globals.less @@ -60,6 +60,7 @@ --gold: 45 100% 50%; --link: 210 100% 63%; + --error: 20 80% 50%; } .dark { diff --git a/app/src/components/admin/MenuBar.tsx b/app/src/components/admin/MenuBar.tsx index b0803bd4..cdc85e30 100644 --- a/app/src/components/admin/MenuBar.tsx +++ b/app/src/components/admin/MenuBar.tsx @@ -3,9 +3,9 @@ import { closeMenu, selectMenu } from "@/store/menu.ts"; import React, { useMemo } from "react"; import { BookCopy, - CandlestickChart, + CloudCog, + Gauge, GitFork, - LayoutDashboard, Radio, Settings, Users, @@ -49,11 +49,7 @@ function MenuBar() { const open = useSelector(selectMenu); return (
- } - path={"/"} - /> + } path={"/"} /> } path={"/users"} /> } path={"/channel"} /> - } - path={"/charge"} - /> + } path={"/charge"} /> } diff --git a/app/src/components/app/AppProvider.tsx b/app/src/components/app/AppProvider.tsx index 5679d85d..03436725 100644 --- a/app/src/components/app/AppProvider.tsx +++ b/app/src/components/app/AppProvider.tsx @@ -3,14 +3,40 @@ import { ThemeProvider } from "@/components/ThemeProvider.tsx"; import DialogManager from "@/dialogs"; import Broadcast from "@/components/Broadcast.tsx"; import { useEffectAsync } from "@/utils/hook.ts"; -import { allModels } from "@/conf.ts"; -import axios from "axios"; +import { allModels, supportModels } from "@/conf.ts"; import { channelModels } from "@/admin/channel.ts"; +import { getApiCharge, getApiMarket, getApiModels } from "@/api/v1.ts"; +import { loadPreferenceModels } from "@/utils/storage.ts"; +import { resetJsArray } from "@/utils/base.ts"; +import { useDispatch } from "react-redux"; +import { initChatModels } from "@/store/chat.ts"; +import { Model } from "@/api/types.ts"; +import { ChargeProps, nonBilling } from "@/admin/charge.ts"; function AppProvider() { + const dispatch = useDispatch(); + useEffectAsync(async () => { - const res = await axios.get("/v1/models"); - res.data.forEach((model: string) => { + const market = await getApiMarket(); + const charge = await getApiCharge(); + + market.forEach((item: Model) => { + const obj = charge.find((i: ChargeProps) => i.models.includes(item.id)); + if (!obj) return; + + item.free = obj.type === nonBilling; + item.auth = item.free && !obj.anonymous; + }); + + resetJsArray(supportModels, loadPreferenceModels(market)); + resetJsArray( + allModels, + supportModels.map((model) => model.id), + ); + initChatModels(dispatch); + + const models = await getApiModels(); + models.forEach((model: string) => { if (!allModels.includes(model)) allModels.push(model); if (!channelModels.includes(model)) channelModels.push(model); }); diff --git a/app/src/components/ui/tooltip.tsx b/app/src/components/ui/tooltip.tsx index 3d2e11ef..4e11b23c 100644 --- a/app/src/components/ui/tooltip.tsx +++ b/app/src/components/ui/tooltip.tsx @@ -18,6 +18,7 @@ const TooltipContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + "max-w-[100vw]", className, )} {...props} diff --git a/app/src/conf.ts b/app/src/conf.ts index 3c52c09a..2a7c584c 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -12,7 +12,7 @@ import { getMemory } from "@/utils/memory.ts"; import { Compass, Image, Newspaper } from "lucide-react"; import React from "react"; import { syncSiteInfo } from "@/admin/api/info.ts"; -import { loadPreferenceModels } from "@/utils/storage.ts"; +import { getOfflineModels, loadPreferenceModels } from "@/utils/storage.ts"; export const version = "3.8.1"; export const dev: boolean = getDev(); @@ -21,517 +21,7 @@ export let rest_api: string = getRestApi(deploy); export let ws_api: string = getWebsocketApi(deploy); export const tokenField = getTokenField(deploy); -export let supportModels: Model[] = loadPreferenceModels([ - // openai models - { - id: "gpt-3.5-turbo-0613", - name: "GPT-3.5", - avatar: "gpt35turbo.png", - free: true, - auth: false, - high_context: false, - default: true, - tag: ["free", "official"], - }, - { - id: "gpt-3.5-turbo-16k-0613", - name: "GPT-3.5-16k", - avatar: "gpt35turbo16k.webp", - free: true, - auth: true, - high_context: true, - default: true, - tag: ["free", "official", "high-context"], - }, - { - id: "gpt-3.5-turbo-1106", - name: "GPT-3.5 1106", - avatar: "gpt35turbo16k.webp", - free: true, - auth: true, - high_context: true, - default: true, - tag: ["free", "official"], - }, - { - id: "gpt-3.5-turbo-fast", - name: "GPT-3.5 Fast", - avatar: "gpt35turbo16k.webp", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official"], - }, - { - id: "gpt-3.5-turbo-16k-fast", - name: "GPT-3.5 16K Fast", - avatar: "gpt35turbo16k.webp", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official"], - }, - { - id: "gpt-4-0613", - name: "GPT-4", - avatar: "gpt4.png", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "high-quality"], - }, - { - id: "gpt-4-1106-preview", - name: "GPT-4 Turbo 128k", - avatar: "gpt432k.webp", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "high-context", "unstable"], - }, - { - id: "gpt-4-vision-preview", - name: "GPT-4 Vision 128k", - avatar: "gpt4v.png", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "high-context", "multi-modal", "unstable"], - }, - { - id: "gpt-4-v", - name: "GPT-4 Vision", - avatar: "gpt4v.png", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "unstable", "multi-modal"], - }, - { - id: "gpt-4-dalle", - name: "GPT-4 DALLE", - avatar: "gpt4dalle.png", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "unstable", "image-generation"], - }, - - { - id: "azure-gpt-3.5-turbo", - name: "Azure GPT-3.5", - avatar: "gpt35turbo.png", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official"], - }, - { - id: "azure-gpt-3.5-turbo-16k", - name: "Azure GPT-3.5 16K", - avatar: "gpt35turbo16k.webp", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official"], - }, - { - id: "azure-gpt-4", - name: "Azure GPT-4", - avatar: "gpt4.png", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "high-quality"], - }, - { - id: "azure-gpt-4-1106-preview", - name: "Azure GPT-4 Turbo 128k", - avatar: "gpt432k.webp", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "high-context", "unstable"], - }, - { - id: "azure-gpt-4-vision-preview", - name: "Azure GPT-4 Vision 128k", - avatar: "gpt4v.png", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "high-context", "multi-modal"], - }, - { - id: "azure-gpt-4-32k", - name: "Azure GPT-4 32k", - avatar: "gpt432k.webp", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "multi-modal"], - }, - - // spark desk - { - id: "spark-desk-v3", - name: "讯飞星火 V3", - avatar: "sparkdesk.jpg", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official", "high-quality"], - }, - { - id: "spark-desk-v2", - name: "讯飞星火 V2", - avatar: "sparkdesk.jpg", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["official"], - }, - { - id: "spark-desk-v1.5", - name: "讯飞星火 V1.5", - avatar: "sparkdesk.jpg", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["official"], - }, - - // dashscope models - { - id: "qwen-plus-net", - name: "通义千问 Plus Net", - avatar: "tongyi.png", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official", "high-quality", "web"], - }, - { - id: "qwen-plus", - name: "通义千问 Plus", - avatar: "tongyi.png", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official", "high-quality"], - }, - { - id: "qwen-turbo-net", - name: "通义千问 Turbo Net", - avatar: "tongyi.png", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["official", "web"], - }, - { - id: "qwen-turbo", - name: "通义千问 Turbo", - avatar: "tongyi.png", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["official"], - }, - - // huyuan models - { - id: "hunyuan", - name: "腾讯混元 Pro", - avatar: "hunyuan.png", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official"], - }, - - // zhipu models - { - id: "zhipu-chatglm-turbo", - name: "ChatGLM Turbo", - avatar: "chatglm.png", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "open-source", "high-context"], - }, - - // baichuan models - { - id: "baichuan-53b", - name: "百川 Baichuan 53B", - avatar: "baichuan.png", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official", "open-source"], - }, - - // skylark models - { - id: "skylark-chat", - name: "抖音豆包 Skylark", - avatar: "skylark.jpg", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official"], - }, - - // 360 models - { - id: "360-gpt-v9", - name: "360 智脑", - avatar: "360gpt.png", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["official"], - }, - - { - id: "claude-1-100k", - name: "Claude", - avatar: "claude.png", - free: true, - auth: true, - high_context: true, - default: true, - tag: ["free", "unstable"], - }, - { - id: "claude-2", - name: "Claude 100k", - avatar: "claude100k.png", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "high-context"], - }, - { - id: "claude-2.1", - name: "Claude 200k", - avatar: "claude100k.png", - free: false, - auth: true, - high_context: true, - default: true, - tag: ["official", "high-context"], - }, - - // llama models - { - id: "llama-2-70b", - name: "LLaMa-2 70B", - avatar: "llama2.webp", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["open-source", "unstable"], - }, - { - id: "llama-2-13b", - name: "LLaMa-2 13B", - avatar: "llama2.webp", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["open-source", "unstable"], - }, - { - id: "llama-2-7b", - name: "LLaMa-2 7B", - avatar: "llama2.webp", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["open-source", "unstable"], - }, - - { - id: "code-llama-34b", - name: "Code LLaMa 34B", - avatar: "llamacode.webp", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["open-source", "unstable"], - }, - { - id: "code-llama-13b", - name: "Code LLaMa 13B", - avatar: "llamacode.webp", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["open-source", "unstable"], - }, - { - id: "code-llama-7b", - name: "Code LLaMa 7B", - avatar: "llamacode.webp", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["open-source", "unstable"], - }, - - // new bing - { - id: "bing-creative", - name: "New Bing", - avatar: "newbing.jpg", - free: true, - auth: true, - high_context: true, - default: true, - tag: ["free", "unstable", "web"], - }, - - // google palm2 - { - id: "chat-bison-001", - name: "Google PaLM2", - avatar: "palm2.webp", - free: true, - auth: true, - high_context: false, - default: false, - tag: ["free", "english-model"], - }, - - // gemini - { - id: "gemini-pro", - name: "Gemini Pro", - avatar: "gemini.jpeg", - free: true, - auth: true, - high_context: true, - default: true, - tag: ["free", "official"], - }, - { - id: "gemini-pro-vision", - name: "Gemini Pro Vision", - avatar: "gemini.jpeg", - free: true, - auth: true, - high_context: true, - default: true, - tag: ["free", "official", "multi-modal"], - }, - - // drawing models - { - id: "midjourney", - name: "Midjourney", - avatar: "midjourney.jpg", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official", "image-generation"], - }, - { - id: "midjourney-fast", - name: "Midjourney Fast", - avatar: "midjourney.jpg", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official", "fast", "image-generation"], - }, - { - id: "midjourney-turbo", - name: "Midjourney Turbo", - avatar: "midjourney.jpg", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official", "fast", "image-generation"], - }, - { - id: "stable-diffusion", - name: "Stable Diffusion XL", - avatar: "stablediffusion.jpeg", - free: false, - auth: true, - high_context: false, - default: false, - tag: ["open-source", "unstable", "image-generation"], - }, - { - id: "dall-e-2", - name: "DALLE 2", - avatar: "dalle.jpeg", - free: true, - auth: true, - high_context: false, - default: true, - tag: ["free", "official", "image-generation"], - }, - { - id: "dall-e-3", - name: "DALLE 3", - avatar: "dalle.jpeg", - free: false, - auth: true, - high_context: false, - default: true, - tag: ["official", "image-generation"], - }, - - { - id: "gpt-4-32k-0613", - name: "GPT-4-32k", - avatar: "gpt432k.webp", - free: false, - auth: true, - high_context: true, - default: false, - tag: ["official", "high-quality", "high-price"], - }, -]); +export let supportModels: Model[] = loadPreferenceModels(getOfflineModels()); export let allModels: string[] = supportModels.map((model) => model.id); diff --git a/app/src/routes/admin/Market.tsx b/app/src/routes/admin/Market.tsx index 42d1baa6..c4eb19cc 100644 --- a/app/src/routes/admin/Market.tsx +++ b/app/src/routes/admin/Market.tsx @@ -17,7 +17,7 @@ import { Model as RawModel } from "@/api/types.ts"; import { supportModels } from "@/conf.ts"; import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd"; import { Input } from "@/components/ui/input.tsx"; -import { GripVertical, HelpCircle } from "lucide-react"; +import { GripVertical, HelpCircle, Plus, Trash2 } from "lucide-react"; import { generateRandomChar, isUrl } from "@/utils/base.ts"; import Require from "@/components/Require.tsx"; import { Textarea } from "@/components/ui/textarea.tsx"; @@ -59,6 +59,22 @@ function reducer(state: MarketForm, action: any): MarketForm { seed: generateSeed(), }, ]; + case "new": + return [ + ...state, + { + id: "", + name: "", + free: false, + auth: false, + description: "", + high_context: false, + default: false, + tag: [], + avatar: modelImages[0], + seed: generateSeed(), + }, + ]; case "remove": let { idx } = action.payload; return [...state.slice(0, idx), ...state.slice(idx + 1)]; @@ -342,6 +358,14 @@ function Market() { ); }, [form]); + const doCheck = (index: number) => { + return useMemo((): boolean => { + const model = form[index]; + + return model.id.trim().length > 0 && model.name.trim().length > 0; + }, [form, index]); + }; + return (
@@ -349,7 +373,7 @@ function Market() { {t("admin.market.title")} + {index === form.length - 1 && ( + + )} +
)} diff --git a/app/src/store/chat.ts b/app/src/store/chat.ts index 9a43ecd0..e1515d2b 100644 --- a/app/src/store/chat.ts +++ b/app/src/store/chat.ts @@ -2,9 +2,16 @@ import { createSlice } from "@reduxjs/toolkit"; import { ConversationInstance, Model } from "@/api/types.ts"; import { Message } from "@/api/types.ts"; import { insertStart } from "@/utils/base.ts"; -import { RootState } from "./index.ts"; +import { AppDispatch, RootState } from "./index.ts"; import { planModels, supportModels } from "@/conf.ts"; -import { getBooleanMemory, getMemory, setMemory } from "@/utils/memory.ts"; +import { + getArrayMemory, + getBooleanMemory, + getMemory, + setArrayMemory, + setMemory, +} from "@/utils/memory.ts"; +import { setOfflineModels } from "@/utils/storage.ts"; type initialStateType = { history: ConversationInstance[]; @@ -31,17 +38,12 @@ export function getPlanModels(level: number): string[] { } export function getModel(model: string | undefined | null): string { + if (supportModels.length === 0) return ""; return model && inModel(model) ? model : supportModels[0].id; } -export function getModelList( - models: string | undefined | null, - select: string | undefined | null, -): string[] { - const list = - models && models.length - ? models.split(",").filter((item) => inModel(item)) - : []; +export function getModelList(models: string[], select: string): string[] { + const list = models.filter((item) => inModel(item)); const target = list.length ? list : supportModels.filter((item) => item.default).map((item) => item.id); @@ -58,11 +60,20 @@ const chatSlice = createSlice({ model: getModel(getMemory("model")), web: getBooleanMemory("web", false), current: -1, - model_list: getModelList(getMemory("model_list"), getMemory("model")), + model_list: getModelList(getArrayMemory("model_list"), getMemory("model")), market: false, mask: false, } as initialStateType, reducers: { + doInit: (state) => { + setOfflineModels(supportModels); + + state.model = getModel(getMemory("model")); + state.model_list = getModelList( + getArrayMemory("model_list"), + getMemory("model"), + ); + }, setHistory: (state, action) => { state.history = action.payload as ConversationInstance[]; }, @@ -109,20 +120,20 @@ const chatSlice = createSlice({ setModelList: (state, action) => { const models = action.payload as string[]; state.model_list = models.filter((item) => inModel(item)); - setMemory("model_list", models.join(",")); + setArrayMemory("model_list", models); }, addModelList: (state, action) => { const model = action.payload as string; if (inModel(model) && !state.model_list.includes(model)) { state.model_list.push(model); - setMemory("model_list", state.model_list.join(",")); + setArrayMemory("model_list", state.model_list); } }, removeModelList: (state, action) => { const model = action.payload as string; if (inModel(model) && state.model_list.includes(model)) { state.model_list = state.model_list.filter((item) => item !== model); - setMemory("model_list", state.model_list.join(",")); + setArrayMemory("model_list", state.model_list); } }, setMarket: (state, action) => { @@ -147,6 +158,7 @@ const chatSlice = createSlice({ }); export const { + doInit, setHistory, removeHistory, addHistory, @@ -178,5 +190,6 @@ export const selectModelList = (state: RootState): string[] => state.chat.model_list; export const selectMarket = (state: RootState): boolean => state.chat.market; export const selectMask = (state: RootState): boolean => state.chat.mask; +export const initChatModels = (dispatch: AppDispatch) => dispatch(doInit()); export default chatSlice.reducer; diff --git a/app/src/utils/base.ts b/app/src/utils/base.ts index 1a2ed49f..fb03788c 100644 --- a/app/src/utils/base.ts +++ b/app/src/utils/base.ts @@ -71,3 +71,12 @@ export function isUrl(value: string): boolean { return false; } } + +export function resetJsArray(arr: T[], target: T[]): T[] { + /** + * this function is used to reset an array to another array without changing the *pointer + */ + + arr.splice(0, arr.length, ...target); + return arr; +} diff --git a/app/src/utils/memory.ts b/app/src/utils/memory.ts index 9af05228..5149bd48 100644 --- a/app/src/utils/memory.ts +++ b/app/src/utils/memory.ts @@ -11,6 +11,10 @@ export function setNumberMemory(key: string, value: number) { setMemory(key, value.toString()); } +export function setArrayMemory(key: string, value: string[]) { + setMemory(key, value.join(",")); +} + export function getMemory(key: string): string { return (localStorage.getItem(key) || "").trim(); } diff --git a/app/src/utils/storage.ts b/app/src/utils/storage.ts index 120a91d1..c75ce7e6 100644 --- a/app/src/utils/storage.ts +++ b/app/src/utils/storage.ts @@ -25,3 +25,12 @@ export function loadPreferenceModels(models: Model[]): Model[] { return aIndex - bIndex; }); } + +export function setOfflineModels(models: Model[]): void { + setMemory("model_offline", JSON.stringify(models)); +} + +export function getOfflineModels(): Model[] { + const memory = getMemory("model_offline"); + return memory.length ? (JSON.parse(memory) as Model[]) : []; +} diff --git a/connection/database.go b/connection/database.go index eabc6e10..b42ca3d0 100644 --- a/connection/database.go +++ b/connection/database.go @@ -45,6 +45,9 @@ func ConnectMySQL() *sql.DB { log.Println(fmt.Sprintf("[connection] connected to mysql server (host: %s)", viper.GetString("mysql.host"))) } + db.SetMaxOpenConns(512) + db.SetMaxIdleConns(64) + CreateUserTable(db) CreateConversationTable(db) CreateSharingTable(db) diff --git a/main.go b/main.go index 53fc2c6f..97a85267 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ func registerApiRouter(engine *gin.Engine) { func main() { utils.ReadConf() + admin.InitInstance() channel.InitManager() if cli.Run() { diff --git a/manager/relay.go b/manager/relay.go index f4f1a8e3..31f369d0 100644 --- a/manager/relay.go +++ b/manager/relay.go @@ -1,6 +1,7 @@ package manager import ( + "chat/admin" "chat/channel" "github.com/gin-gonic/gin" "net/http" @@ -10,6 +11,10 @@ func ModelAPI(c *gin.Context) { c.JSON(http.StatusOK, channel.ConduitInstance.GetModels()) } +func MarketAPI(c *gin.Context) { + c.JSON(http.StatusOK, admin.MarketInstance.GetModels()) +} + func ChargeAPI(c *gin.Context) { c.JSON(http.StatusOK, channel.ChargeInstance.ListRules()) } diff --git a/manager/router.go b/manager/router.go index 9c0365a7..7ca74044 100644 --- a/manager/router.go +++ b/manager/router.go @@ -8,6 +8,7 @@ import ( func Register(app *gin.RouterGroup) { app.GET("/chat", ChatAPI) app.GET("/v1/models", ModelAPI) + app.GET("/v1/market", MarketAPI) app.GET("/v1/charge", ChargeAPI) app.GET("/dashboard/billing/usage", GetBillingUsage) app.GET("/dashboard/billing/subscription", GetSubscription)