feat: add funcitons manger page

This commit is contained in:
RockYang 2023-12-21 08:58:24 +08:00
parent bf19120c27
commit 7c4dfe96ee
12 changed files with 216 additions and 190 deletions

View File

@ -0,0 +1,52 @@
package admin
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/handler"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type FunctionHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewFunctionHandler(app *core.AppServer, db *gorm.DB) *FunctionHandler {
h := FunctionHandler{db: db}
h.App = app
return &h
}
func (h *FunctionHandler) Save(c *gin.Context) {
var data vo.Function
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
logger.Info(data)
resp.SUCCESS(c)
}
func (h *FunctionHandler) List(c *gin.Context) {
resp.SUCCESS(c)
}
func (h *FunctionHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
if id > 0 {
res := h.db.Delete(&model.Function{Id: uint(id)})
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
}
resp.SUCCESS(c)
}

View File

@ -146,6 +146,9 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
Prompt: fmt.Sprintf("%s %s", taskId, prompt), Prompt: fmt.Sprintf("%s %s", taskId, prompt),
UserId: userId, UserId: userId,
}) })
// update user's img calls
h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
resp.SUCCESS(c) resp.SUCCESS(c)
} }
@ -246,6 +249,9 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
MessageId: data.MessageId, MessageId: data.MessageId,
MessageHash: data.MessageHash, MessageHash: data.MessageHash,
}) })
// update user's img calls
h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
resp.SUCCESS(c) resp.SUCCESS(c)
} }

View File

@ -133,6 +133,9 @@ func (h *SdJobHandler) Image(c *gin.Context) {
UserId: userId, UserId: userId,
}) })
// update user's img calls
h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
resp.SUCCESS(c) resp.SUCCESS(c)
} }
@ -177,8 +180,8 @@ func (h *SdJobHandler) JobList(c *gin.Context) {
} }
if item.Progress < 100 { if item.Progress < 100 {
// 10 分钟还没完成的任务直接删除 // 5 分钟还没完成的任务直接删除
if time.Now().Sub(item.CreatedAt) > time.Minute*10 { if time.Now().Sub(item.CreatedAt) > time.Minute*5 {
h.db.Delete(&item) h.db.Delete(&item)
continue continue
} }

View File

@ -341,6 +341,14 @@ func main() {
group.POST("translate", h.Translate) group.POST("translate", h.Translate)
}), }),
fx.Provide(admin.NewFunctionHandler),
fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) {
group := s.Engine.Group("/api/admin/function/")
group.POST("save", h.Save)
group.GET("list", h.List)
group.GET("remove", h.Remove)
}),
fx.Provide(handler.NewTestHandler), fx.Provide(handler.NewTestHandler),
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) { fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
group := s.Engine.Group("/test/") group := s.Engine.Group("/test/")

View File

@ -1,24 +1,16 @@
{ {
"data": [ "data": [
"task(vpu09bi42w01k5f)", "task(cxvkpawy8onnfti)",
"A beautiful Chinese girl strolls along the beach", "a cute girl",
"", "",
[], [],
30, 20,
"DPM++ 2M SDE Karras", "DPM++ 2M Karras",
false,
false,
1, 1,
1, 1,
7, 7,
-1, 512,
-1, 512,
0,
0,
0,
false,
1024,
1024,
false, false,
0.7, 0.7,
2, 2,
@ -26,11 +18,24 @@
0, 0,
0, 0,
0, 0,
"Use same checkpoint",
"Use same sampler", "Use same sampler",
"", "",
"", "",
[], [],
"None", "None",
false,
"",
0.8,
-1,
false,
-1,
0,
0,
0,
null,
null,
null,
null, null,
false, false,
false, false,
@ -54,46 +59,22 @@
false, false,
false, false,
0, 0,
"Not set", null,
true, null,
true,
"",
"",
"",
"",
"",
1.3,
"Not set",
"Not set",
1.3,
"Not set",
1.3,
"Not set",
1.3,
1.3,
"Not set",
1.3,
"Not set",
1.3,
"Not set",
1.3,
"Not set",
1.3,
"Not set",
1.3,
"Not set",
false, false,
"None", null,
null,
false,
null,
null, null,
false, false,
50, 50,
[ [],
], "",
"{\"prompt\": \"A beautiful Chinese girl strolls along the beach\", \"all_prompts\": [\"A beautiful Chinese girl strolls along the beach\"], \"negative_prompt\": \"\", \"all_negative_prompts\": [\"\"], \"seed\": 291934632, \"all_seeds\": [291934632], \"subseed\": 2486830045, \"all_subseeds\": [2486830045], \"subseed_strength\": 0, \"width\": 1024, \"height\": 1024, \"sampler_name\": \"DPM++ 2M SDE Karras\", \"cfg_scale\": 7, \"steps\": 30, \"batch_size\": 1, \"restore_faces\": false, \"face_restoration_model\": null, \"sd_model_hash\": \"2a4411ef93\", \"seed_resize_from_w\": 0, \"seed_resize_from_h\": 0, \"denoising_strength\": null, \"extra_generation_params\": {}, \"index_of_first_image\": 0, \"infotexts\": [\"A beautiful Chinese girl strolls along the beach\\nSteps: 30, Sampler: DPM++ 2M SDE Karras, CFG scale: 7, Seed: 291934632, Size: 1024x1024, Model hash: 2a4411ef93, Model: sdxlUnstableDiffusers_v7ElectricMind, Version: v1.5.2\"], \"styles\": [], \"job_timestamp\": \"20231215222321\", \"clip_skip\": 1, \"is_using_inpainting_conditioning\": false}", "",
"<p>A beautiful Chinese girl strolls along the beach<br>\nSteps: 30, Sampler: DPM++ 2M SDE Karras, CFG scale: 7, Seed: 291934632, Size: 1024x1024, Model hash: 2a4411ef93, Model: sdxlUnstableDiffusers_v7ElectricMind, Version: v1.5.2</p>", ""
"<p class='comments'></p><div class='performance'><p class='time'>Time taken: <wbr><span class='measurement'>11.2 sec.</span></p><p class='vram'><abbr title='Active: peak amount of video memory used during generation (excluding cached data)'>A</abbr>: <span class='measurement'>10.26 GB</span>, <wbr><abbr title='Reserved: total amout of video memory allocated by the Torch library '>R</abbr>: <span class='measurement'>13.34 GB</span>, <wbr><abbr title='System: peak amout of video memory allocated by all running programs, out of total capacity'>Sys</abbr>: <span class='measurement'>13.8/23.6904 GB</span> (58.1%)</p></div>"
], ],
"event_data": null, "event_data": null,
"fn_index": 232, "fn_index": 446,
"session_hash": "sstp2i5ytoj" "session_hash": "nk5noh1rz1o"
} }

View File

@ -54,7 +54,6 @@ func (s *Service) Run() {
err := s.taskQueue.LPop(&task) err := s.taskQueue.LPop(&task)
if err != nil { if err != nil {
logger.Errorf("taking task with error: %v", err) logger.Errorf("taking task with error: %v", err)
s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumn("progress", -1)
continue continue
} }
@ -83,6 +82,8 @@ func (s *Service) Run() {
logger.Error("绘画任务执行失败:", err) logger.Error("绘画任务执行失败:", err)
// update the task progress // update the task progress
s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumn("progress", -1) s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumn("progress", -1)
// restore img_call quota
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
continue continue
} }
@ -157,8 +158,6 @@ func (s *Service) Notify(data CBReq) {
} }
if data.Status == Finished { if data.Status == Finished {
// update user's img calls
s.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
// release lock task // release lock task
atomic.AddInt32(&s.handledTaskNum, -1) atomic.AddInt32(&s.handledTaskNum, -1)
} }

View File

@ -26,7 +26,7 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
// create sd service // create sd service
name := fmt.Sprintf("StableDifffusion Service-%d", k) name := fmt.Sprintf("StableDifffusion Service-%d", k)
service := NewService(name, 4, 600, config, queue, db, manager) service := NewService(name, 1, 300, config, queue, db, manager)
// run sd service // run sd service
go func() { go func() {
service.Run() service.Run()

View File

@ -69,6 +69,8 @@ func (s *Service) Run() {
logger.Error("绘画任务执行失败:", err) logger.Error("绘画任务执行失败:", err)
// update the task progress // update the task progress
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", -1) s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", -1)
// restore img_call quota
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
// release task num // release task num
atomic.AddInt32(&s.handledTaskNum, -1) atomic.AddInt32(&s.handledTaskNum, -1)
continue continue

View File

@ -32,14 +32,14 @@ var ParamKeys = map[string]int{
"negative_prompt": 2, "negative_prompt": 2,
"steps": 4, "steps": 4,
"sampler": 5, "sampler": 5,
"face_fix": 6, // 面部修复 "face_fix": 7, // 面部修复
"cfg_scale": 10, "cfg_scale": 8,
"seed": 11, "seed": 27,
"height": 17, "height": 10,
"width": 18, "width": 9,
"hd_fix": 19, "hd_fix": 11,
"hd_redraw_rate": 20, //高清修复重绘幅度 "hd_redraw_rate": 12, //高清修复重绘幅度
"hd_scale": 21, // 高清修复放大倍数 "hd_scale": 13, // 高清修复放大倍数
"hd_scale_alg": 22, // 高清修复放大算法 "hd_scale_alg": 14, // 高清修复放大算法
"hd_sample_num": 23, // 高清修复采样次数 "hd_sample_num": 15, // 高清修复采样次数
} }

View File

@ -17,3 +17,5 @@ ALTER TABLE `chatgpt_functions` MODIFY `id` int NOT NULL AUTO_INCREMENT;
ALTER TABLE `chatgpt_functions` ADD UNIQUE(`name`); ALTER TABLE `chatgpt_functions` ADD UNIQUE(`name`);
ALTER TABLE `chatgpt_functions` ADD `enabled` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '是否启用' AFTER `action`; ALTER TABLE `chatgpt_functions` ADD `enabled` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '是否启用' AFTER `action`;
ALTER TABLE `chatgpt_functions` ADD `lebal` VARCHAR(30) NULL COMMENT '函数标签' AFTER `name`;

View File

@ -533,10 +533,10 @@ const params = ref({
cfg_scale: 7, cfg_scale: 7,
face_fix: false, face_fix: false,
hd_fix: false, hd_fix: false,
hd_redraw_rate: 0.7, hd_redraw_rate: 0.5,
hd_scale: 2, hd_scale: 2,
hd_scale_alg: scaleAlg[0], hd_scale_alg: scaleAlg[0],
hd_steps: 10, hd_steps: 15,
prompt: "", prompt: "",
negative_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet", negative_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
}) })

View File

@ -21,7 +21,7 @@
<el-table-column label="操作" width="150" align="right"> <el-table-column label="操作" width="150" align="right">
<template #default="scope"> <template #default="scope">
<el-button size="small" type="primary" @click="rowEdit(scope.$index, scope.row)">编辑</el-button> <el-button size="small" type="primary" @click="rowEdit(scope.$index, scope.row)">编辑</el-button>
<el-popconfirm title="确定要删除当前函数吗?" @confirm="removeRole(scope.row)"> <el-popconfirm title="确定要删除当前函数吗?" @confirm="remove(scope.row)">
<template #reference> <template #reference>
<el-button size="small" type="danger">删除</el-button> <el-button size="small" type="danger">删除</el-button>
</template> </template>
@ -33,80 +33,89 @@
<el-dialog <el-dialog
v-model="showDialog" v-model="showDialog"
title="编辑角色" :title="title"
width="50%" width="50%"
> >
<el-form :model="role" label-width="120px" ref="formRef" label-position="left" :rules="rules"> <el-form :model="item" label-width="120px" ref="formRef" label-position="left" :rules="rules">
<el-form-item label="角色名称:" prop="name"> <el-form-item label="函数名称:" prop="name">
<el-input <el-input
v-model="role.name" v-model="item.name"
autocomplete="off" autocomplete="off"
/> />
</el-form-item> </el-form-item>
<el-form-item label="角色标志:" prop="key"> <el-form-item label="函数标签:" prop="label">
<el-input <el-input
v-model="role.key" v-model="item.label"
placeholder="函数的中文名称"
autocomplete="off" autocomplete="off"
/> />
</el-form-item> </el-form-item>
<el-form-item label="角色图标:" prop="icon"> <el-form-item label="功能描述:" prop="description">
<el-input <el-input
v-model="role.icon" v-model="item.description"
autocomplete="off" autocomplete="off"
/> />
</el-form-item> </el-form-item>
<el-form-item label="打招呼信息:" prop="hello_msg"> <el-form-item label="函数参数:" prop="parameters">
<el-input
v-model="role.hello_msg"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="上下文信息:" prop="context">
<template #default> <template #default>
<el-table :data="role.context" :border="childBorder" size="small"> <el-table :data="params" :border="childBorder" size="small">
<el-table-column label="对话角色" width="120"> <el-table-column label="参数名称" width="120">
<template #default="scope"> <template #default="scope">
<el-input <el-input
v-model="scope.row.role" v-model="scope.row.name"
autocomplete="off" autocomplete="off"
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="对话内容"> <el-table-column label="参数类型" width="120">
<template #header> <template #default="scope">
<div class="context-msg-key"> <el-select v-model="scope.row.type" placeholder="参数类型">
<span>对话内容</span> <el-option v-for="pt in paramsType" :value="pt" :key="pt">{{ pt }}</el-option>
<span class="fr"> </el-select>
<el-button type="primary" @click="addContext" size="small"> </template>
<el-icon> </el-table-column>
<Plus/> <el-table-column label="参数描述">
</el-icon>
增加一行
</el-button>
</span>
</div>
</template>
<template #default="scope"> <template #default="scope">
<div class="context-msg-content">
<el-input <el-input
v-model="scope.row.content" v-model="scope.row.desc"
autocomplete="off" autocomplete="off"
/> />
<span><el-icon @click="removeContext(scope.$index)"><RemoveFilled/></el-icon></span> </template>
</el-table-column>
<el-table-column label="必填参数" width="80">
<template #default="scope">
<div class="param-opt">
<el-checkbox v-model="scope.row.required"/>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="scope">
<div class="param-opt">
<el-button type="danger" :icon="Delete" circle @click="removeParam(scope.$index)" size="small"/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="param-line">
<el-button type="primary" @click="addParam" size="small">
<el-icon>
<Plus/>
</el-icon>
增加参数
</el-button>
</div>
</template> </template>
</el-form-item> </el-form-item>
<el-form-item label="启用状态"> <el-form-item label="启用状态">
<el-switch v-model="role.enable"/> <el-switch v-model="item.enabled"/>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -122,7 +131,7 @@
<script setup> <script setup>
import {Plus, RemoveFilled} from "@element-plus/icons-vue"; import {Delete, Plus, RemoveFilled} from "@element-plus/icons-vue";
import {onMounted, reactive, ref} from "vue"; import {onMounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
@ -133,74 +142,31 @@ const showDialog = ref(false)
const parentBorder = ref(true) const parentBorder = ref(true)
const childBorder = ref(true) const childBorder = ref(true)
const tableData = ref([]) const tableData = ref([])
const sortedTableData = ref([]) const item = ref({parameters: []})
const role = ref({context: []}) const params = ref([])
const formRef = ref(null) const formRef = ref(null)
const editRow = ref({}) const editRow = ref({})
const loading = ref(true) const loading = ref(true)
const title = ref("新增函数")
const rules = reactive({ const rules = reactive({
name: [{required: true, message: '请输入用户名', trigger: 'blur',}], name: [{required: true, message: '请输入函数名称', trigger: 'blur',}],
key: [{required: true, message: '请输入角色标识', trigger: 'blur',}], label: [{required: true, message: '请输入函数标签', trigger: 'blur',}],
icon: [{required: true, message: '请输入角色图标', trigger: 'blur',}], description: [{required: true, message: '请输入函数功能描述', trigger: 'blur',}],
sort: [
{required: true, message: '请输入排序数字', trigger: 'blur'},
{type: 'number', message: '请输入有效数字'},
],
hello_msg: [{required: true, message: '请输入打招呼信息', trigger: 'change',}]
}) })
const paramsType = ref(["string", "number"])
//
httpGet('/api/admin/role/list').then((res) => {
tableData.value = res.data
sortedTableData.value = copyObj(tableData.value)
loading.value = false
}).catch(() => {
ElMessage.error("获取聊天角色失败");
})
onMounted(() => { onMounted(() => {
const drawBodyWrapper = document.querySelector('.el-table__body tbody') fetch()
//
Sortable.create(drawBodyWrapper, {
sort: true,
animation: 500,
onEnd({newIndex, oldIndex, from}) {
if (oldIndex === newIndex) {
return
}
const sortedData = Array.from(from.children).map(row => row.querySelector('.sort').getAttribute('data-id'));
const ids = []
const sorts = []
sortedData.forEach((id, index) => {
ids.push(parseInt(id))
sorts.push(index)
})
httpPost("/api/admin/role/sort", {ids: ids, sorts: sorts}).catch(e => {
ElMessage.error("排序失败:" + e.message)
})
}
})
}) })
const editSort = function (event, row) { const fetch = () => {
event.stopPropagation() httpGet('/api/admin/function/list').then((res) => {
editRow.value.id = row.id tableData.value = res.data
editRow.value.sort = row.sort loading.value = false
}
const updateSort = function (row) {
if (row.sort === editRow.value.sort) {
editRow.value.id = 0
return
}
httpPost('/api/admin/role/sort', {"id": row.id, "sort": row.sort}).then(() => {
editRow.value.id = 0
}).catch(() => { }).catch(() => {
ElMessage.error("更新失败!") ElMessage.error("获取数据失败");
}) })
} }
@ -208,12 +174,12 @@ const updateSort = function (row) {
const curIndex = ref(0) const curIndex = ref(0)
const rowEdit = function (index, row) { const rowEdit = function (index, row) {
curIndex.value = index curIndex.value = index
role.value = copyObj(row) item.value = copyObj(row)
showDialog.value = true showDialog.value = true
} }
const addRole = function () { const addRole = function () {
role.value = {context: []} item.value = {parameters: []}
showDialog.value = true showDialog.value = true
} }
@ -221,14 +187,24 @@ const save = function () {
formRef.value.validate((valid) => { formRef.value.validate((valid) => {
if (valid) { if (valid) {
showDialog.value = false showDialog.value = false
httpPost('/api/admin/role/save', role.value).then((res) => { const properties = {}
const required = []
// process params
for (let i = 0; i < params.value.length; i++) {
properties[params.value[i].name] = {"type": params.value[i].type, "description": params.value[i].desc}
if (params.value[i].required) {
required.push(params.value[i].name)
}
}
item.value.parameters = {type: "object", "properties": properties, "required": required}
httpPost('/api/admin/function/save', item.value).then((res) => {
ElMessage.success('操作成功') ElMessage.success('操作成功')
// //
if (role.value.id) { // if (item.value.id) {
tableData.value[curIndex.value] = role.value // tableData.value[curIndex.value] = item.value
} else { // } else {
tableData.value.push(res.data) // tableData.value.push(res.data)
} // }
}).catch((e) => { }).catch((e) => {
ElMessage.error('操作失败,' + e.message) ElMessage.error('操作失败,' + e.message)
}) })
@ -236,26 +212,24 @@ const save = function () {
}) })
} }
const removeRole = function (row) { const remove = function (row) {
httpGet('/api/admin/role/remove?id=' + row.id).then(() => { httpGet('/api/admin/role/remove?id=' + row.id).then(() => {
ElMessage.success("删除成功!") ElMessage.success("删除成功!")
tableData.value = removeArrayItem(tableData.value, row, (v1, v2) => { fetch()
return v1.id === v2.id
})
}).catch(() => { }).catch(() => {
ElMessage.error("删除失败!") ElMessage.error("删除失败!")
}) })
} }
const addContext = function () { const addParam = function () {
if (!role.value.context) { if (!params.value) {
role.value.context = [] item.value = []
} }
role.value.context.push({role: '', content: ''}) params.value.push({name: "", type: "", desc: "", required: false})
} }
const removeContext = function (index) { const removeParam = function (index) {
role.value.context.splice(index, 1); params.value.splice(index, 1);
} }
</script> </script>
@ -272,18 +246,17 @@ const removeContext = function (index) {
} }
} }
.context-msg-key { .param-line {
.fr { padding 5px 0
float right
.el-icon { .el-icon {
margin-right 5px margin-right 5px;
}
} }
} }
.context-msg-content { .param-opt {
display flex display flex
justify-content center
.el-icon { .el-icon {
font-size: 20px; font-size: 20px;