From 06d2d756c4f4ac3b9bb8ce5e113c7322c6187266 Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Mon, 18 Dec 2023 00:08:26 +0800 Subject: [PATCH] feat: system config settings --- adapter/midjourney/api.go | 7 +- addition/web/duckduckgo.go | 11 ++- admin/analysis.go | 2 +- app/src/admin/api/system.ts | 65 ++++++++++++++++++ app/src/conf.ts | 9 --- app/src/routes/admin/System.tsx | 117 +++++++++++++++++--------------- app/src/utils/dev.ts | 7 ++ app/src/utils/form.ts | 17 ++++- channel/controller.go | 38 +++++++++-- channel/manager.go | 8 ++- channel/router.go | 3 + channel/system.go | 85 +++++++++++++++++++++++ channel/worker.go | 4 +- main.go | 12 +++- manager/transhipment.go | 2 +- 15 files changed, 300 insertions(+), 87 deletions(-) create mode 100644 app/src/admin/api/system.ts create mode 100644 app/src/utils/dev.ts diff --git a/adapter/midjourney/api.go b/adapter/midjourney/api.go index b2e906e1..62940f00 100644 --- a/adapter/midjourney/api.go +++ b/adapter/midjourney/api.go @@ -19,8 +19,11 @@ func (c *ChatInstance) CreateImagineRequest(prompt string) (*ImagineResponse, er "mj-api-secret": c.GetApiSecret(), }, ImagineRequest{ - NotifyHook: fmt.Sprintf("%s/mj/notify", viper.GetString("system.domain")), - Prompt: prompt, + NotifyHook: fmt.Sprintf( + "%s/mj/notify", + viper.GetString("system.general.backend"), + ), + Prompt: prompt, }, ) diff --git a/addition/web/duckduckgo.go b/addition/web/duckduckgo.go index d2b6a737..187cf3d5 100644 --- a/addition/web/duckduckgo.go +++ b/addition/web/duckduckgo.go @@ -1,9 +1,9 @@ package web import ( + "chat/channel" "chat/utils" "fmt" - "github.com/spf13/viper" "net/url" "strings" ) @@ -30,8 +30,13 @@ func formatResponse(data *DDGResponse) string { } func CallDuckDuckGoAPI(query string) *DDGResponse { - query = url.QueryEscape(query) - data, err := utils.Get(fmt.Sprintf("%s/search?q=%s&max_results=%d", viper.GetString("system.ddg"), query, viper.GetInt("system.ddg_max_results")), nil) + data, err := utils.Get(fmt.Sprintf( + "%s/search?q=%s&max_results=%d", + channel.SystemInstance.GetSearchEndpoint(), + url.QueryEscape(query), + channel.SystemInstance.GetSearchQuery(), + ), nil) + if err != nil { return nil } diff --git a/admin/analysis.go b/admin/analysis.go index cb6de955..b2757009 100644 --- a/admin/analysis.go +++ b/admin/analysis.go @@ -43,7 +43,7 @@ func GetModelData(cache *redis.Client) ModelChartForm { return ModelChartForm{ Date: getDates(dates), - Value: utils.EachNotNil[string, ModelData](channel.ManagerInstance.GetModels(), func(model string) *ModelData { + Value: utils.EachNotNil[string, ModelData](channel.ConduitInstance.GetModels(), func(model string) *ModelData { data := ModelData{ Model: model, Data: utils.Each[time.Time, int64](dates, func(date time.Time) int64 { diff --git a/app/src/admin/api/system.ts b/app/src/admin/api/system.ts new file mode 100644 index 00000000..9b0930a1 --- /dev/null +++ b/app/src/admin/api/system.ts @@ -0,0 +1,65 @@ +import { CommonResponse } from "@/admin/utils.ts"; +import { getErrorMessage } from "@/utils/base.ts"; +import axios from "axios"; + +export type GeneralState = { + backend: string; +}; + +export type MailState = { + host: string; + port: number; + username: string; + password: string; + from: string; +}; + +export type SearchState = { + endpoint: string; + query: number; +}; + +export type SystemProps = { + general: GeneralState; + mail: MailState; + search: SearchState; +}; + +export type SystemResponse = CommonResponse & { + data?: SystemProps; +}; + +export async function getConfig(): Promise { + try { + const response = await axios.get("/admin/config/view"); + return response.data as SystemResponse; + } catch (e) { + return { status: false, error: getErrorMessage(e) }; + } +} + +export async function setConfig(config: SystemProps): Promise { + try { + const response = await axios.post(`/admin/config/update`, config); + return response.data as CommonResponse; + } catch (e) { + return { status: false, error: getErrorMessage(e) }; + } +} + +export const initialSystemState: SystemProps = { + general: { + backend: "", + }, + mail: { + host: "", + port: 465, + username: "", + password: "", + from: "", + }, + search: { + endpoint: "https://duckduckgo-api.vercel.app", + query: 5, + }, +}; diff --git a/app/src/conf.ts b/app/src/conf.ts index 8126092d..a03379c2 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -39,13 +39,6 @@ export const supportModels: Model[] = [ auth: true, tag: ["free", "official"], }, - { - id: "gpt-4-free", - name: "GPT-4 Free", - free: true, - auth: true, - tag: ["free", "unstable", "high-quality"], - }, { id: "gpt-4-0613", name: "GPT-4", @@ -321,7 +314,6 @@ export const supportModels: Model[] = [ export const defaultModels = [ "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", - "gpt-4-free", "gpt-4-0613", "gpt-4-1106-preview", @@ -380,7 +372,6 @@ export const modelAvatars: Record = { "gpt-3.5-turbo-16k-0613": "gpt35turbo16k.webp", "gpt-3.5-turbo-1106": "gpt35turbo16k.webp", "gpt-4-0613": "gpt4.png", - "gpt-4-free": "gpt4.png", "gpt-4-1106-preview": "gpt432k.webp", "gpt-4-vision-preview": "gpt4v.png", "gpt-4-all": "gpt4.png", diff --git a/app/src/routes/admin/System.tsx b/app/src/routes/admin/System.tsx index 245e19dc..c17d77e1 100644 --- a/app/src/routes/admin/System.tsx +++ b/app/src/routes/admin/System.tsx @@ -16,16 +16,27 @@ import { Input } from "@/components/ui/input.tsx"; import { useReducer } from "react"; import { formReducer } from "@/utils/form.ts"; import { NumberInput } from "@/components/ui/number-input.tsx"; +import { + GeneralState, + getConfig, + initialSystemState, + MailState, + SearchState, + setConfig, + SystemProps, +} from "@/admin/api/system.ts"; +import { useEffectAsync } from "@/utils/hook.ts"; +import { toastState } from "@/admin/utils.ts"; +import { useToast } from "@/components/ui/use-toast.ts"; -type GeneralState = { - backend: string; +type CompProps = { + data: T; + dispatch: (action: any) => void; + onChange: () => void; }; -function General() { +function General({ data, dispatch, onChange }: CompProps) { const { t } = useTranslation(); - const [general, generalDispatch] = useReducer(formReducer(), { - backend: "", - } as GeneralState); return ( - generalDispatch({ - type: "update:backend", + dispatch({ + type: "update:general.backend", value: e.target.value, }) } @@ -51,7 +62,7 @@ function General() {
- @@ -59,21 +70,8 @@ function General() { ); } -type MailState = { - host: string; - port: number; - username: string; - password: string; -}; - -function Mail() { +function Mail({ data, dispatch, onChange }: CompProps) { const { t } = useTranslation(); - const [mail, mailDispatch] = useReducer(formReducer(), { - host: "", - port: 465, - username: "", - password: "", - } as MailState); return ( - mailDispatch({ - type: "update:host", + dispatch({ + type: "update:mail.host", value: e.target.value, }) } @@ -97,9 +95,9 @@ function Mail() { - mailDispatch({ type: "update:port", value }) + dispatch({ type: "update:mail.port", value }) } placeholder={`465`} min={0} @@ -109,10 +107,10 @@ function Mail() { - mailDispatch({ - type: "update:username", + dispatch({ + type: "update:mail.username", value: e.target.value, }) } @@ -122,10 +120,10 @@ function Mail() { - mailDispatch({ - type: "update:password", + dispatch({ + type: "update:mail.password", value: e.target.value, }) } @@ -134,7 +132,7 @@ function Mail() {
- @@ -142,17 +140,8 @@ function Mail() { ); } -type SearchState = { - endpoint: string; - query: number; -}; - -function Search() { +function Search({ data, dispatch, onChange }: CompProps) { const { t } = useTranslation(); - const [search, searchDispatch] = useReducer(formReducer(), { - endpoint: "https://duckduckgo-api.vercel.app", - query: 5, - } as SearchState); return ( - searchDispatch({ - type: "update:endpoint", + dispatch({ + type: "update:search.endpoint", value: e.target.value, }) } @@ -176,9 +165,9 @@ function Search() { - searchDispatch({ type: "update:query", value }) + dispatch({ type: "update:search.query", value }) } placeholder={`5`} min={0} @@ -188,7 +177,7 @@ function Search() { {t("admin.system.searchTip")}
- @@ -198,6 +187,24 @@ function Search() { function System() { const { t } = useTranslation(); + const { toast } = useToast(); + const [data, setData] = useReducer( + formReducer(), + initialSystemState, + ); + + const save = async () => { + const res = await setConfig(data); + toastState(toast, t, res, true); + }; + + useEffectAsync(async () => { + const res = await getConfig(); + toastState(toast, t, res); + if (res.status) { + setData({ type: "set", value: res.data }); + } + }, []); return (
@@ -206,9 +213,9 @@ function System() { {t("admin.settings")} - - - + + +
diff --git a/app/src/utils/dev.ts b/app/src/utils/dev.ts new file mode 100644 index 00000000..f8d68203 --- /dev/null +++ b/app/src/utils/dev.ts @@ -0,0 +1,7 @@ +export function inWaiting(duration: number): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, duration); + }); +} diff --git a/app/src/utils/form.ts b/app/src/utils/form.ts index f73d22c9..13856094 100644 --- a/app/src/utils/form.ts +++ b/app/src/utils/form.ts @@ -1,3 +1,16 @@ +export function setKey(state: T, key: string, value: any): T { + const segment = key.split("."); + if (segment.length === 1) { + return { ...state, [key]: value }; + } else if (segment.length > 1) { + const [k, ...v] = segment; + return { ...state, [k]: setKey(state[k as keyof T], v.join("."), value) }; + } + + // segment.length is zero + throw new Error("invalid key"); +} + export const formReducer = () => { return (state: T, action: any) => { action.payload = action.payload ?? action.value; @@ -7,10 +20,12 @@ export const formReducer = () => { return { ...state, ...action.payload }; case "reset": return { ...action.payload }; + case "set": + return action.payload; default: if (action.type.startsWith("update:")) { const key = action.type.slice(7); - return { ...state, [key]: action.payload }; + return setKey(state, key, action.payload); } } }; diff --git a/channel/controller.go b/channel/controller.go index 8dd5fb8c..981d9dae 100644 --- a/channel/controller.go +++ b/channel/controller.go @@ -8,7 +8,7 @@ import ( func DeleteChannel(c *gin.Context) { id := c.Param("id") - state := ManagerInstance.DeleteChannel(utils.ParseInt(id)) + state := ConduitInstance.DeleteChannel(utils.ParseInt(id)) c.JSON(http.StatusOK, gin.H{ "status": state == nil, @@ -18,7 +18,7 @@ func DeleteChannel(c *gin.Context) { func ActivateChannel(c *gin.Context) { id := c.Param("id") - state := ManagerInstance.ActivateChannel(utils.ParseInt(id)) + state := ConduitInstance.ActivateChannel(utils.ParseInt(id)) c.JSON(http.StatusOK, gin.H{ "status": state == nil, @@ -28,7 +28,7 @@ func ActivateChannel(c *gin.Context) { func DeactivateChannel(c *gin.Context) { id := c.Param("id") - state := ManagerInstance.DeactivateChannel(utils.ParseInt(id)) + state := ConduitInstance.DeactivateChannel(utils.ParseInt(id)) c.JSON(http.StatusOK, gin.H{ "status": state == nil, @@ -39,13 +39,13 @@ func DeactivateChannel(c *gin.Context) { func GetChannelList(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": true, - "data": ManagerInstance.Sequence, + "data": ConduitInstance.Sequence, }) } func GetChannel(c *gin.Context) { id := c.Param("id") - channel := ManagerInstance.Sequence.GetChannelById(utils.ParseInt(id)) + channel := ConduitInstance.Sequence.GetChannelById(utils.ParseInt(id)) c.JSON(http.StatusOK, gin.H{ "status": channel != nil, @@ -63,7 +63,7 @@ func CreateChannel(c *gin.Context) { return } - state := ManagerInstance.CreateChannel(&channel) + state := ConduitInstance.CreateChannel(&channel) c.JSON(http.StatusOK, gin.H{ "status": state == nil, "error": utils.GetError(state), @@ -83,7 +83,7 @@ func UpdateChannel(c *gin.Context) { id := c.Param("id") channel.Id = utils.ParseInt(id) - state := ManagerInstance.UpdateChannel(channel.Id, &channel) + state := ConduitInstance.UpdateChannel(channel.Id, &channel) c.JSON(http.StatusOK, gin.H{ "status": state == nil, "error": utils.GetError(state), @@ -123,3 +123,27 @@ func DeleteCharge(c *gin.Context) { "error": utils.GetError(state), }) } + +func GetConfig(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": true, + "data": SystemInstance, + }) +} + +func UpdateConfig(c *gin.Context) { + var config SystemConfig + if err := c.ShouldBindJSON(&config); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "status": false, + "error": err.Error(), + }) + return + } + + state := SystemInstance.UpdateConfig(&config) + c.JSON(http.StatusOK, gin.H{ + "status": state == nil, + "error": utils.GetError(state), + }) +} diff --git a/channel/manager.go b/channel/manager.go index af92aaef..68f4ebb8 100644 --- a/channel/manager.go +++ b/channel/manager.go @@ -6,15 +6,17 @@ import ( "github.com/spf13/viper" ) -var ManagerInstance *Manager +var ConduitInstance *Manager var ChargeInstance *ChargeManager +var SystemInstance *SystemConfig func InitManager() { - ManagerInstance = NewManager() + ConduitInstance = NewChannelManager() ChargeInstance = NewChargeManager() + SystemInstance = NewSystemConfig() } -func NewManager() *Manager { +func NewChannelManager() *Manager { var seq Sequence if err := viper.UnmarshalKey("channel", &seq); err != nil { panic(err) diff --git a/channel/router.go b/channel/router.go index 0166d4a8..6df2028c 100644 --- a/channel/router.go +++ b/channel/router.go @@ -14,4 +14,7 @@ func Register(app *gin.Engine) { app.GET("/admin/charge/list", GetChargeList) app.POST("/admin/charge/set", SetCharge) app.GET("/admin/charge/delete/:id", DeleteCharge) + + app.GET("/admin/config/view", GetConfig) + app.POST("/admin/config/update", UpdateConfig) } diff --git a/channel/system.go b/channel/system.go index 9c86d4ad..9765c064 100644 --- a/channel/system.go +++ b/channel/system.go @@ -1 +1,86 @@ package channel + +import ( + "chat/utils" + "github.com/spf13/viper" +) + +type generalState struct { + Backend string `json:"backend" mapstructure:"backend"` +} + +type mailState struct { + Host string `json:"host" mapstructure:"host"` + Port int `json:"port" mapstructure:"port"` + Username string `json:"username" mapstructure:"username"` + Password string `json:"password" mapstructure:"password"` + From string `json:"from" mapstructure:"from"` +} + +type searchState struct { + Endpoint string `json:"endpoint" mapstructure:"endpoint"` + Query int `json:"query" mapstructure:"query"` +} + +type SystemConfig struct { + General generalState `json:"general" mapstructure:"general"` + Mail mailState `json:"mail" mapstructure:"mail"` + Search searchState `json:"search" mapstructure:"search"` +} + +func NewSystemConfig() *SystemConfig { + conf := &SystemConfig{} + if err := viper.UnmarshalKey("system", conf); err != nil { + panic(err) + } + + return conf +} + +func (c *SystemConfig) SaveConfig() error { + viper.Set("system", c) + + // fix: import cycle not allowed + { + viper.Set("system.general.backend", c.GetBackend()) + } + return viper.WriteConfig() +} + +func (c *SystemConfig) UpdateConfig(data *SystemConfig) error { + c.General = data.General + c.Mail = data.Mail + c.Search = data.Search + + return c.SaveConfig() +} + +func (c *SystemConfig) GetBackend() string { + return c.General.Backend +} + +func (c *SystemConfig) GetMail() *utils.SmtpPoster { + return utils.NewSmtpPoster( + c.Mail.Host, + c.Mail.Port, + c.Mail.Username, + c.Mail.Password, + c.Mail.From, + ) +} + +func (c *SystemConfig) GetSearchEndpoint() string { + if len(c.Search.Endpoint) == 0 { + return "https://duckduckgo-api.vercel.app" + } + + return c.Search.Endpoint +} + +func (c *SystemConfig) GetSearchQuery() int { + if c.Search.Query <= 0 { + return 5 + } + + return c.Search.Query +} diff --git a/channel/worker.go b/channel/worker.go index 1bb16ffa..74b698e2 100644 --- a/channel/worker.go +++ b/channel/worker.go @@ -9,11 +9,11 @@ import ( ) func NewChatRequest(props *adapter.ChatProps, hook globals.Hook) error { - if !ManagerInstance.HasChannel(props.Model) { + if !ConduitInstance.HasChannel(props.Model) { return fmt.Errorf("cannot find channel for model %s", props.Model) } - ticker := ManagerInstance.GetTicker(props.Model) + ticker := ConduitInstance.GetTicker(props.Model) debug := viper.GetBool("debug") diff --git a/main.go b/main.go index afd077f5..7b301635 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "chat/manager" "chat/manager/conversation" "chat/middleware" - "chat/utils" "fmt" "github.com/gin-gonic/gin" "github.com/spf13/viper" @@ -26,7 +25,7 @@ func main() { } channel.InitManager() - app := gin.Default() + app := gin.New() middleware.RegisterMiddleware(app) { @@ -38,7 +37,14 @@ func main() { conversation.Register(app) } - gin.SetMode(utils.Multi[string](viper.GetBool("debug"), gin.DebugMode, gin.ReleaseMode)) + if viper.GetBool("debug") { + app.Use(gin.Logger()) + } else { + gin.SetMode(gin.ReleaseMode) + } + + app.Use(gin.Recovery()) + if err := app.Run(fmt.Sprintf(":%s", viper.GetString("server.port"))); err != nil { panic(err) } diff --git a/manager/transhipment.go b/manager/transhipment.go index daa33730..7fc7771b 100644 --- a/manager/transhipment.go +++ b/manager/transhipment.go @@ -81,7 +81,7 @@ type TranshipmentError struct { } func ModelAPI(c *gin.Context) { - c.JSON(http.StatusOK, channel.ManagerInstance.GetModels()) + c.JSON(http.StatusOK, channel.ConduitInstance.GetModels()) } func sendErrorResponse(c *gin.Context, err error, types ...string) {