feat: implements image function replace Mj with DALL-E-3

This commit is contained in:
RockYang 2023-11-26 20:37:48 +08:00
parent a2c7a75705
commit 23b5ffa97d
17 changed files with 227 additions and 181 deletions

View File

@ -148,6 +148,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
c.Request.URL.Path == "/api/chat/detail" ||
c.Request.URL.Path == "/api/role/list" ||
c.Request.URL.Path == "/api/mj/jobs" ||
c.Request.URL.Path == "/api/invite/hits" ||
c.Request.URL.Path == "/api/sd/jobs" ||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||

View File

@ -145,7 +145,6 @@ type SystemConfig struct {
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
EnabledRegister bool `json:"enabled_register"` // 是否启用注册功能,关闭注册功能之后将无法注册
EnabledMsg bool `json:"enabled_msg"` // 是否启用短信验证码服务
EnabledDraw bool `json:"enabled_draw"` // 是否启用 AI 绘画功能
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能

View File

@ -23,10 +23,10 @@ type Property struct {
}
const (
FuncZaoBao = "zao_bao" // 每日早报
FuncHeadLine = "headline" // 今日头条
FuncWeibo = "weibo_hot" // 微博热搜
FuncMidJourney = "mid_journey" // MJ 绘画
FuncZaoBao = "zao_bao" // 每日早报
FuncHeadLine = "headline" // 今日头条
FuncWeibo = "weibo_hot" // 微博热搜
FuncImage = "draw_image" // AI 绘画
)
var InnerFunctions = []Function{
@ -76,14 +76,14 @@ var InnerFunctions = []Function{
},
{
Name: FuncMidJourney,
Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画",
Name: FuncImage,
Description: "AI 绘画工具,根据输入的绘图描述用 AI 工具进行绘画",
Parameters: Parameters{
Type: "object",
Properties: map[string]Property{
"prompt": {
Type: "string",
Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。提示词中的参数作为提示的一部分,不要删除",
Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。",
},
},
Required: []string{},

View File

@ -27,6 +27,7 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Platform string `json:"platform"`
Type string `json:"type"`
Value string `json:"value"`
}
if err := c.ShouldBindJSON(&data); err != nil {
@ -40,7 +41,8 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
}
apiKey.Platform = data.Platform
apiKey.Value = data.Value
res := h.db.Debug().Save(&apiKey)
apiKey.Type = data.Type
res := h.db.Save(&apiKey)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return

View File

@ -127,12 +127,12 @@ func (h *ChatHandler) sendAzureMessage(
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
// for creating image, check if the user's img_calls > 0
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
if functionName == types.FuncImage && userVo.ImgCalls <= 0 {
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
utils.ReplyMessage(ws, ErrImg)
} else {
f := h.App.Functions[functionName]
if functionName == types.FuncMidJourney {
if functionName == types.FuncImage {
params["user_id"] = userVo.Id
params["role_id"] = role.Id
params["chat_id"] = session.ChatId
@ -149,9 +149,8 @@ func (h *ChatHandler) sendAzureMessage(
contents = append(contents, msg)
} else {
content := data
if functionName == types.FuncMidJourney {
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
h.mjService.ChatClients.Put(session.SessionId, ws)
if functionName == types.FuncImage {
content = fmt.Sprintf("下面是根据您的描述创作的图片,他们描绘了 【%s】 的场景", params["prompt"])
// update user's img_calls
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
}

View File

@ -224,9 +224,6 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
if h.App.SysConfig.EnabledFunction {
var functions = make([]types.Function, 0)
for _, f := range types.InnerFunctions {
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
continue
}
functions = append(functions, f)
}
req.Functions = functions
@ -405,7 +402,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
}
if *apiKey == "" {
var key model.ApiKey
res := h.db.Where("platform = ?", platform).Order("last_used_at ASC").First(&key)
res := h.db.Where("platform = ? AND type = ?", platform, "chat").Order("last_used_at ASC").First(&key)
if res.Error != nil {
return nil, errors.New("no available key, please import key")
}

View File

@ -126,12 +126,12 @@ func (h *ChatHandler) sendOpenAiMessage(
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
// for creating image, check if the user's img_calls > 0
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
if functionName == types.FuncImage && userVo.ImgCalls <= 0 {
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
utils.ReplyMessage(ws, ErrImg)
} else {
f := h.App.Functions[functionName]
if functionName == types.FuncMidJourney {
if functionName == types.FuncImage {
params["user_id"] = userVo.Id
params["role_id"] = role.Id
params["chat_id"] = session.ChatId
@ -148,9 +148,8 @@ func (h *ChatHandler) sendOpenAiMessage(
contents = append(contents, msg)
} else {
content := data
if functionName == types.FuncMidJourney {
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
h.mjService.ChatClients.Put(session.SessionId, ws)
if functionName == types.FuncImage {
content = fmt.Sprintf("下面是根据您的描述创作的图片,他们描绘了 【%s】 的场景。%s", params["prompt"], data)
// update user's img_calls
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
}

View File

@ -69,7 +69,7 @@ func (h *ChatHandler) sendXunFeiMessage(
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
if apiKey == "" {
var key model.ApiKey
res := h.db.Where("platform = ?", session.Model.Platform).Order("last_used_at ASC").First(&key)
res := h.db.Where("platform = ? AND type = ?", session.Model.Platform, "chat").Order("last_used_at ASC").First(&key)
if res.Error != nil {
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
return nil

View File

@ -0,0 +1,92 @@
package fun
import (
"chatplus/core/types"
"chatplus/service/oss"
"chatplus/store/model"
"chatplus/utils"
"fmt"
"github.com/imroc/req/v3"
"gorm.io/gorm"
)
// AI 绘画函数
type FuncImage struct {
name string
apiURL string
db *gorm.DB
uploadManager *oss.UploaderManager
proxyURL string
}
func NewImageFunc(db *gorm.DB, manager *oss.UploaderManager, config *types.AppConfig) FuncImage {
return FuncImage{
db: db,
name: "DALL-E3 绘画",
uploadManager: manager,
proxyURL: config.ProxyURL,
apiURL: "https://api.openai.com/v1/images/generations",
}
}
type imgReq struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
N int `json:"n"`
Size string `json:"size"`
}
type imgRes struct {
Created int64 `json:"created"`
Data []struct {
RevisedPrompt string `json:"revised_prompt"`
Url string `json:"url"`
} `json:"data"`
}
type ErrRes struct {
Error struct {
Code interface{} `json:"code"`
Message string `json:"message"`
Param interface{} `json:"param"`
Type string `json:"type"`
} `json:"error"`
}
func (f FuncImage) Invoke(params map[string]interface{}) (string, error) {
logger.Infof("绘画参数:%+v", params)
prompt := utils.InterfaceToString(params["prompt"])
// 获取绘图 API KEY
var apiKey model.ApiKey
f.db.Where("platform = ? AND type = ?", types.OpenAI, "img").Order("last_used_at ASC").First(&apiKey)
var res imgRes
var errRes ErrRes
r, err := req.C().SetProxyURL(f.proxyURL).R().SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(imgReq{
Model: "dall-e-3",
Prompt: prompt,
N: 1,
Size: "1024x1024",
}).
SetErrorResult(&errRes).
SetSuccessResult(&res).Post(f.apiURL)
if err != nil || r.IsErrorState() {
return "", fmt.Errorf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message)
}
// 存储图片
imgURL, err := f.uploadManager.GetUploadHandler().PutImg(res.Data[0].Url, false)
if err != nil {
return "", fmt.Errorf("下载图片失败: %s", err.Error())
}
logger.Info(imgURL)
return fmt.Sprintf("\n\n![](%s)\n", imgURL), nil
}
func (f FuncImage) Name() string {
return f.name
}
var _ Function = &FuncImage{}

View File

@ -1,49 +0,0 @@
package fun
import (
"chatplus/core/types"
"chatplus/service/mj"
"chatplus/utils"
"errors"
)
// AI 绘画函数
type FuncMidJourney struct {
name string
service *mj.Service
config types.MidJourneyConfig
}
func NewMidJourneyFunc(mjService *mj.Service, config types.MidJourneyConfig) FuncMidJourney {
return FuncMidJourney{
name: "MidJourney AI 绘画",
config: config,
service: mjService}
}
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
if !f.config.Enabled {
return "", errors.New("MidJourney AI 绘画功能没有启用")
}
logger.Infof("MJ 绘画参数:%+v", params)
prompt := utils.InterfaceToString(params["prompt"])
f.service.PushTask(types.MjTask{
SessionId: utils.InterfaceToString(params["session_id"]),
Src: types.TaskSrcChat,
Type: types.TaskImage,
Prompt: prompt,
UserId: utils.IntValue(utils.InterfaceToString(params["user_id"]), 0),
RoleId: utils.IntValue(utils.InterfaceToString(params["role_id"]), 0),
Icon: utils.InterfaceToString(params["icon"]),
ChatId: utils.InterfaceToString(params["chat_id"]),
})
return prompt, nil
}
func (f FuncMidJourney) Name() string {
return f.name
}
var _ Function = &FuncMidJourney{}

View File

@ -3,7 +3,8 @@ package fun
import (
"chatplus/core/types"
logger2 "chatplus/logger"
"chatplus/service/mj"
"chatplus/service/oss"
"gorm.io/gorm"
)
type Function interface {
@ -29,11 +30,11 @@ type dataItem struct {
Remark string `json:"remark"`
}
func NewFunctions(config *types.AppConfig, mjService *mj.Service) map[string]Function {
func NewFunctions(config *types.AppConfig, db *gorm.DB, manager *oss.UploaderManager) map[string]Function {
return map[string]Function{
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
types.FuncMidJourney: NewMidJourneyFunc(mjService, config.MjConfig),
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
types.FuncImage: NewImageFunc(db, manager, config),
}
}

View File

@ -1,13 +1,23 @@
package main
import (
"chatplus/utils"
"fmt"
"reflect"
"time"
"os"
)
func main() {
r := time.Now()
f := reflect.ValueOf(r)
fmt.Println(f.Type().Kind())
imgURL := "https://oaidalleapiprodscus.blob.core.windows.net/private/org-UJimNEKhVm07E58nxnjx5FeG/user-e5UAcPVbkm2nwD8urggRRM8q/img-zFXWyrJ9Z1HppI36dZMXNEaA.png?st=2023-11-26T09%3A57%3A49Z&se=2023-11-26T11%3A57%3A49Z&sp=r&sv=2021-08-06&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2023-11-26T08%3A14%3A59Z&ske=2023-11-27T08%3A14%3A59Z&sks=b&skv=2021-08-06&sig=VmlU9didavbl02XYim2XuMmLMFJsLtCY/ULnzCjeO1g%3D"
imageData, err := utils.DownloadImage(imgURL, "")
if err != nil {
panic(err)
}
newImagePath := "newimage.png"
err = os.WriteFile(newImagePath, imageData, 0644)
if err != nil {
fmt.Println("Error writing image file:", err)
return
}
fmt.Println("图片保存成功!")
}

View File

@ -2,10 +2,7 @@
<div class="home">
<div class="navigator">
<div class="logo">
<el-link href="/">
<el-image :src="logo"/>
</el-link>
<el-image :src="logo"/>
<div class="divider"></div>
</div>
@ -37,8 +34,6 @@
<script setup>
import {useRouter} from "vue-router";
import {checkSession} from "@/action/session";
import {isMobile} from "@/utils/libs";
import {ref} from "vue";
const router = useRouter();

View File

@ -1,85 +1,88 @@
<template>
<div class="page-invitation">
<div class="inner">
<h2>会员推广计划</h2>
<div class="share-box">
<div class="info">
我们非常欢迎您把此应用分享给您身边的朋友分享成功注册后您将获得 <strong>{{ inviteChatCalls }}</strong> 次对话额度以及
<strong>{{ inviteImgCalls }}</strong> 次AI绘画额度作为奖励
你可以保存下面的二维码或者直接复制分享您的专属推广链接发送给微信好友
<div class="custom-scroll">
<div class="page-invitation" :style="{height: listBoxHeight + 'px'}">
<div class="inner">
<h2>会员推广计划</h2>
<div class="share-box">
<div class="info">
我们非常欢迎您把此应用分享给您身边的朋友分享成功注册后您将获得 <strong>{{ inviteChatCalls }}</strong>
次对话额度以及
<strong>{{ inviteImgCalls }}</strong> 次AI绘画额度作为奖励
你可以保存下面的二维码或者直接复制分享您的专属推广链接发送给微信好友
</div>
<div class="invite-qrcode">
<el-image :src="qrImg"/>
</div>
<div class="invite-url">
<span>{{ inviteURL }}</span>
<el-button type="primary" plain class="copy-link" :data-clipboard-text="inviteURL">复制链接</el-button>
</div>
</div>
<div class="invite-qrcode">
<el-image :src="qrImg"/>
<div class="invite-stats">
<el-row :gutter="20">
<el-col :span="8">
<div class="item-box yellow">
<el-row :gutter="10">
<el-col :span="10">
<div class="item-icon">
<i class="iconfont icon-role"></i>
</div>
</el-col>
<el-col :span="14">
<div class="item-info">
<div class="num">{{ hits }}</div>
<div class="text">点击量</div>
</div>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="8">
<div class="item-box blue">
<el-row :gutter="10">
<el-col :span="10">
<div class="item-icon">
<i class="iconfont icon-order"></i>
</div>
</el-col>
<el-col :span="14">
<div class="item-info">
<div class="num">{{ regNum }}</div>
<div class="text">注册量</div>
</div>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="8">
<div class="item-box green">
<el-row :gutter="10">
<el-col :span="10">
<div class="item-icon">
<i class="iconfont icon-chart"></i>
</div>
</el-col>
<el-col :span="14">
<div class="item-info">
<div class="num">{{ rate }}%</div>
<div class="text">转化率</div>
</div>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</div>
<div class="invite-url">
<span>{{ inviteURL }}</span>
<el-button type="primary" plain class="copy-link" :data-clipboard-text="inviteURL">复制链接</el-button>
<h2>您推荐用户</h2>
<div class="invite-logs">
<invite-list v-if="isLogin"/>
</div>
</div>
<div class="invite-stats">
<el-row :gutter="20">
<el-col :span="8">
<div class="item-box yellow">
<el-row :gutter="10">
<el-col :span="10">
<div class="item-icon">
<i class="iconfont icon-role"></i>
</div>
</el-col>
<el-col :span="14">
<div class="item-info">
<div class="num">{{ hits }}</div>
<div class="text">点击量</div>
</div>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="8">
<div class="item-box blue">
<el-row :gutter="10">
<el-col :span="10">
<div class="item-icon">
<i class="iconfont icon-order"></i>
</div>
</el-col>
<el-col :span="14">
<div class="item-info">
<div class="num">{{ regNum }}</div>
<div class="text">注册量</div>
</div>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="8">
<div class="item-box green">
<el-row :gutter="10">
<el-col :span="10">
<div class="item-icon">
<i class="iconfont icon-chart"></i>
</div>
</el-col>
<el-col :span="14">
<div class="item-info">
<div class="num">{{ rate }}%</div>
<div class="text">转化率</div>
</div>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</div>
<h2>您推荐用户</h2>
<div class="invite-logs">
<invite-list v-if="isLogin"/>
</div>
</div>
</div>
</template>
@ -94,11 +97,11 @@ import InviteList from "@/components/InviteList.vue";
import {checkSession} from "@/action/session";
import {useRouter} from "vue-router";
const listBoxHeight = window.innerHeight
const inviteURL = ref("")
const qrImg = ref("")
const inviteChatCalls = ref(0)
const inviteImgCalls = ref(0)
const users = ref([])
const hits = ref(0)
const regNum = ref(0)
const rate = ref(0)
@ -150,11 +153,14 @@ onMounted(() => {
</script>
<style lang="stylus" scoped>
@import "@/assets/css/custom-scroll.styl"
.page-invitation {
display: flex;
justify-content: center;
background-color: #282c34;
height 100vh
overflow-x hidden
overflow-y visible
.inner {
max-width 1000px

View File

@ -152,7 +152,6 @@ import RewardVerify from "@/components/RewardVerify.vue";
import {useRouter} from "vue-router";
import {removeUserToken} from "@/store/session";
import UserOrder from "@/components/UserOrder.vue";
import CountDown from "@/components/CountDown.vue";
const listBoxHeight = window.innerHeight - 97
const list = ref([])

View File

@ -124,9 +124,17 @@ import FooterBar from "@/components/FooterBar.vue";
import SendMsg from "@/components/SendMsg.vue";
import {validateMobile} from "@/utils/validate";
import {isMobile} from "@/utils/libs";
import SendMsgMobile from "@/components/SendMsg.vue";
import {setUserToken} from "@/store/session";
import {checkSession} from "@/action/session";
checkSession().then(() => {
if (isMobile()) {
router.push('/mobile')
} else {
router.push('/chat')
}
}).catch(() => {
})
const router = useRouter();
const title = ref('ChatGPT-PLUS 用户注册');
const formData = ref({

View File

@ -66,19 +66,6 @@
</el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="启用AI绘画" prop="enabled_draw">
<el-switch v-model="system['enabled_draw']"/>
<el-tooltip
effect="dark"
content="需要开启函数功能此配置才会生效"
raw-content
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="启用众筹功能" prop="enabled_reward">
<el-switch v-model="system['enabled_reward']"/>