Compare commits

...

17 Commits
pr_3 ... v3.2.1

Author SHA1 Message Date
RockYang
a2a6081027 opt: remove default value for stable-diffusion page 2023-12-12 09:59:20 +08:00
RockYang
5a10ed37a7 docs: update readme file 2023-12-12 09:52:28 +08:00
RockYang
1a9dd9de0b docs: update build config.toml 2023-12-12 07:25:36 +08:00
RockYang
0dae5bef71 docs: update changelog file 2023-12-11 17:17:17 +08:00
RockYang
b4413ed726 add translate api for midjourney 2023-12-11 17:01:02 +08:00
RockYang
5e1fe88b8b feat: add prompt translate handler 2023-12-11 06:56:00 +08:00
RockYang
91ed41b536 feat: add system config item for dall e3 generate image num 2023-12-10 17:13:25 +08:00
RockYang
024c0032eb chore: change default params for stable diffusion 2023-12-10 14:45:22 +08:00
RockYang
4a9f7e3bce feat: add HuPiPay payment support 2023-12-08 19:43:13 +08:00
RockYang
cf4dcc34ec feat: add image generation API URL in chat configurations 2023-12-07 16:31:32 +08:00
RockYang
4d612c15af docs: add arm64 build script 2023-12-07 15:44:20 +08:00
RockYang
8aec87cc02 fix: fixed bug for prompt code format, prevent xss attacks 2023-12-07 14:02:13 +08:00
RockYang
442e411cde opt: save chat ID when the chat websocket disconnect 2023-12-07 11:07:08 +08:00
RockYang
acec0194de feat: adjust task list component styles 2023-12-06 19:05:51 +08:00
RockYang
8557f5b94a Merge branch 'pr_3' into dev 2023-12-06 18:54:45 +08:00
RockYang
efd4ab46f5 docs: update readme file 2023-12-06 14:44:06 +08:00
RockYang
f0994ba457 docs: update comments 2023-11-30 17:35:56 +08:00
35 changed files with 848 additions and 310 deletions

View File

@@ -1,4 +1,12 @@
# 更新日志
## v3.2.1
* 功能优化:切换角色和模型的时候自动创建新的对话
* Bug修复修复文件上传失败No such file bug
* 功能新增MidJourney 绘画页面新增提示词翻译功能,新增多个绘画参数
* Bug修复[PC端对话在刷新后异常](https://github.com/yangjian102621/chatgpt-plus/issues/59)
* 功能新增:增加 arm64 架构打包脚本
* 功能新增:支持 dall-e3 绘图的 API 地址自定义配置
* 功能新增:新增虎皮椒支付功能接入,支持微信和支付宝通道
## v3.2.0
* 功能新增:新增邀请注册功能

View File

@@ -13,6 +13,11 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
绘画函数插件。
## 关于部署镜像申明
由于目前部署人数越来越多,本人的阿里云镜像仓库流量不够支撑大家使用了。所以从 v3.2.0 版本开始,一键部署脚本和部署镜像将只提供给 **[付费技术交流群]** 内用户使用。
代码依旧是全部开源的,大家可自行编译打包镜像。
## 功能截图
### PC 端聊天界面
@@ -90,22 +95,16 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
## 项目文档
chatgpt-plus v3.2.0 一键部署脚本来了,真的只需运行一条命令,就可以完成部署:
```shell
bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.0-24e4849229.sh)"
```
目前只支持 Ubuntu 系统,推荐 Ubuntu 22.04 LTS
详细的部署和开发文档请参考 [**ChatGPT-Plus 文档**](https://ai.r9it.com/docs/)。
详细部署文档请参考 [ChatGPT-Plus 文档](https://ai.r9it.com/docs/)。
加微信进入微信讨论群可获取 **一键部署脚本添加好友时请注明来自Github!!!)。**
![微信名片](docs/imgs/wx.png)
## 参与贡献
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。
![微信名片](docs/imgs/wx.png)
#### 特此声明:由于个人时间有限,不接受在微信或者微信群给开发者提 Bug有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合
### Commit 类型

View File

@@ -1,19 +1,14 @@
SHELL=/usr/bin/env bash
NAME := chatgpt-plus
all: window linux darwin
all: amd64 arm64
amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-linux main.go
.PHONY: amd64
window:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/$(NAME)-amd64.exe main.go
.PHONY: window
linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-amd64-linux main.go
.PHONY: linux
darwin:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
.PHONY: darwin
arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build -o bin/$(NAME)-linux main.go
.PHONY: arm64
clean:
rm -rf bin/$(NAME)-*

View File

@@ -156,6 +156,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
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, "/test/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/payment/") ||
@@ -229,35 +230,37 @@ func parameterHandlerMiddleware() gin.HandlerFunc {
params[key][i] = strings.TrimSpace(value)
}
}
// 更新参数
// update get parameters
c.Request.URL.RawQuery = params.Encode()
// skip file upload requests
contentType := c.Request.Header.Get("Content-Type")
if strings.Contains(contentType, "multipart/form-data") {
c.Next()
return
}
// POST JSON 参数处理
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Next()
return
}
if strings.Contains(contentType, "application/json") {
// process POST JSON request body
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Next()
return
}
// 还原请求体
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// 将请求体解析为 JSON
var jsonData map[string]interface{}
if err := c.ShouldBindJSON(&jsonData); err != nil {
c.Next()
return
}
// 还原请求体
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// 将请求体解析为 JSON
var jsonData map[string]interface{}
if err := c.ShouldBindJSON(&jsonData); err != nil {
c.Next()
return
}
// 对 JSON 数据中的字符串值去除两端空格
trimJSONStrings(jsonData)
// 更新请求体
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
// 对 JSON 数据中的字符串值去除两端空格
trimJSONStrings(jsonData)
// 更新请求体
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
}
c.Next()
}

View File

@@ -22,8 +22,9 @@ type AppConfig struct {
WeChatBot bool // 是否启用微信机器人
SdConfig StableDiffusionConfig // sd 绘画配置
XXLConfig XXLConfig
AlipayConfig AlipayConfig
XXLConfig XXLConfig
AlipayConfig AlipayConfig
HuPiPayConfig HuPiPayConfig
}
type ChatPlusApiConfig struct {
@@ -40,10 +41,6 @@ type MidJourneyConfig struct {
ChanelId string // Chanel ID
}
type WeChatConfig struct {
Enabled bool
}
type StableDiffusionConfig struct {
Enabled bool
ApiURL string
@@ -61,7 +58,7 @@ type AliYunSmsConfig struct {
}
type AlipayConfig struct {
Enabled bool // 是否启用该服务
Enabled bool // 是否启用该支付通道
SandBox bool // 是否沙盒环境
AppId string // 应用 ID
UserId string // 支付宝用户 ID
@@ -72,6 +69,15 @@ type AlipayConfig struct {
NotifyURL string // 异步通知回调
}
type HuPiPayConfig struct { //虎皮椒第四方支付配置
Enabled bool // 是否启用该支付通道
Name string // 支付名称wechat/alipay
AppId string // App ID
AppSecret string // app 密钥
NotifyURL string // 异步通知回调
PayURL string // 支付网关
}
type XXLConfig struct { // XXL 任务调度配置
Enabled bool
ServerAddr string
@@ -106,9 +112,11 @@ type ChatConfig struct {
Baidu ModelAPIConfig `json:"baidu"`
XunFei ModelAPIConfig `json:"xun_fei"`
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
ContextDeep int `json:"context_deep"` // 上下文深度
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
ContextDeep int `json:"context_deep"` // 上下文深度
DallApiURL string `json:"dall_api_url"` // dall-e3 绘图 API 地址
DallImgNum int `json:"dall_img_num"` // dall-e3 出图数量
}
type Platform string

View File

@@ -44,6 +44,8 @@ func (h *OrderHandler) List(c *gin.Context) {
end := utils.Str2stamp(data.PayTime[1] + " 00:00:00")
session = session.Where("pay_time >= ? AND pay_time <= ?", start, end)
}
session = session.Where("status = ?", types.OrderPaidSuccess)
var total int64
session.Model(&model.Order{}).Count(&total)
var items []model.Order

View File

@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"gorm.io/gorm"
"html/template"
"io"
"strings"
"time"
@@ -200,7 +201,7 @@ func (h *ChatHandler) sendAzureMessage(
RoleId: role.Id,
Type: types.PromptMsg,
Icon: userVo.Avatar,
Content: prompt,
Content: template.HTMLEscapeString(prompt),
Tokens: promptToken,
UseContext: useContext,
}

View File

@@ -9,6 +9,7 @@ import (
"context"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"strings"
@@ -156,7 +157,7 @@ func (h *ChatHandler) sendBaiduMessage(
RoleId: role.Id,
Type: types.PromptMsg,
Icon: userVo.Avatar,
Content: prompt,
Content: template.HTMLEscapeString(prompt),
Tokens: promptToken,
UseContext: true,
}

View File

@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"github.com/golang-jwt/jwt/v5"
"html/template"
"io"
"strings"
"time"
@@ -135,7 +136,7 @@ func (h *ChatHandler) sendChatGLMMessage(
RoleId: role.Id,
Type: types.PromptMsg,
Icon: userVo.Avatar,
Content: prompt,
Content: template.HTMLEscapeString(prompt),
Tokens: promptToken,
UseContext: true,
}

View File

@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"gorm.io/gorm"
"html/template"
"io"
"strings"
"time"
@@ -135,7 +136,6 @@ func (h *ChatHandler) sendOpenAiMessage(
params["user_id"] = userVo.Id
params["role_id"] = role.Id
params["chat_id"] = session.ChatId
params["icon"] = "/images/avatar/mid_journey.png"
params["session_id"] = session.SessionId
}
data, err := f.Invoke(params)
@@ -199,7 +199,7 @@ func (h *ChatHandler) sendOpenAiMessage(
RoleId: role.Id,
Type: types.PromptMsg,
Icon: userVo.Avatar,
Content: prompt,
Content: template.HTMLEscapeString(prompt),
Tokens: promptToken,
UseContext: useContext,
}

View File

@@ -12,6 +12,7 @@ import (
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"html/template"
"io"
"net/http"
"net/url"
@@ -198,7 +199,7 @@ func (h *ChatHandler) sendXunFeiMessage(
RoleId: role.Id,
Type: types.PromptMsg,
Icon: userVo.Avatar,
Content: prompt,
Content: template.HTMLEscapeString(prompt),
Tokens: promptToken,
UseContext: true,
}

View File

@@ -21,31 +21,37 @@ import (
const (
PayWayAlipay = "支付宝"
PayWayWechat = "微信支付"
PayWayXunHu = "虎皮椒"
)
// PaymentHandler 支付服务回调 handler
type PaymentHandler struct {
BaseHandler
alipayService *payment.AlipayService
snowflake *service.Snowflake
db *gorm.DB
fs embed.FS
lock sync.Mutex
alipayService *payment.AlipayService
huPiPayService *payment.HuPiPayService
snowflake *service.Snowflake
db *gorm.DB
fs embed.FS
lock sync.Mutex
}
func NewPaymentHandler(server *core.AppServer, alipayService *payment.AlipayService, snowflake *service.Snowflake, db *gorm.DB, fs embed.FS) *PaymentHandler {
h := PaymentHandler{lock: sync.Mutex{}}
func NewPaymentHandler(server *core.AppServer, alipayService *payment.AlipayService, huPiPayService *payment.HuPiPayService, snowflake *service.Snowflake, db *gorm.DB, fs embed.FS) *PaymentHandler {
h := PaymentHandler{
alipayService: alipayService,
huPiPayService: huPiPayService,
snowflake: snowflake,
fs: fs,
db: db,
lock: sync.Mutex{},
}
h.App = server
h.alipayService = alipayService
h.snowflake = snowflake
h.db = db
h.fs = fs
return &h
}
func (h *PaymentHandler) Alipay(c *gin.Context) {
func (h *PaymentHandler) DoPay(c *gin.Context) {
orderNo := h.GetTrim(c, "order_no")
payWay := h.GetTrim(c, "pay_way")
if orderNo == "" {
resp.ERROR(c, types.InvalidArgs)
return
@@ -60,21 +66,61 @@ func (h *PaymentHandler) Alipay(c *gin.Context) {
// 更新扫码状态
h.db.Model(&order).UpdateColumn("status", types.OrderScanned)
// 生成支付链接
notifyURL := h.App.Config.AlipayConfig.NotifyURL
returnURL := "" // 关闭同步回跳
amount := fmt.Sprintf("%.2f", order.Amount)
if payWay == "alipay" { // 支付宝
// 生成支付链接
notifyURL := h.App.Config.AlipayConfig.NotifyURL
returnURL := "" // 关闭同步回跳
amount := fmt.Sprintf("%.2f", order.Amount)
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject)
if err != nil {
resp.ERROR(c, "error with generate pay url: "+err.Error())
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject)
if err != nil {
resp.ERROR(c, "error with generate pay url: "+err.Error())
return
}
c.Redirect(302, uri)
return
}
} else if payWay == "hupi" { // 虎皮椒支付
params := map[string]string{
"version": "1.1",
"trade_order_id": orderNo,
"total_fee": fmt.Sprintf("%f", order.Amount),
"title": order.Subject,
"notify_url": h.App.Config.HuPiPayConfig.NotifyURL,
"return_url": "",
"wap_name": "极客学长",
"callback_url": "",
}
c.Redirect(302, uri)
res, err := h.huPiPayService.Pay(params)
if err != nil {
resp.ERROR(c, "error with generate pay url: "+err.Error())
return
}
var r struct {
Openid int64 `json:"openid"`
UrlQrcode string `json:"url_qrcode"`
URL string `json:"url"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
err = utils.JsonDecode(res, &r)
if err != nil {
resp.ERROR(c, "error with decode payment result: "+err.Error())
return
}
if r.ErrCode != 0 {
resp.ERROR(c, "error with generate pay url: "+r.ErrMsg)
return
}
c.Redirect(302, r.URL)
}
resp.ERROR(c, "Invalid operations")
}
// OrderQuery 单状态查询
// OrderQuery 查询订单状态
func (h *PaymentHandler) OrderQuery(c *gin.Context) {
var data struct {
OrderNo string `json:"order_no"`
@@ -111,16 +157,12 @@ func (h *PaymentHandler) OrderQuery(c *gin.Context) {
resp.SUCCESS(c, gin.H{"status": order.Status})
}
// AlipayQrcode 生成支付宝支付 URL 二维码
func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
if !h.App.SysConfig.EnabledAlipay || h.alipayService == nil {
resp.ERROR(c, "当前支付通道已经关闭,请联系管理员开通!")
return
}
// PayQrcode 生成支付 URL 二维码
func (h *PaymentHandler) PayQrcode(c *gin.Context) {
var data struct {
ProductId uint `json:"product_id"`
UserId int `json:"user_id"`
PayWay string `json:"pay_way"` // 支付方式
ProductId uint `json:"product_id"`
UserId int `json:"user_id"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
@@ -146,6 +188,10 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
return
}
payWay := PayWayAlipay
if data.PayWay == "hupi" {
payWay = PayWayXunHu
}
// 创建订单
remark := types.OrderRemark{
Days: product.Days,
@@ -162,7 +208,7 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
Subject: product.Name,
Amount: product.Price - product.Discount,
Status: types.OrderNotPaid,
PayWay: PayWayAlipay,
PayWay: payWay,
Remark: utils.JsonEncode(remark),
}
res = h.db.Create(&order)
@@ -171,19 +217,30 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
return
}
// 生成二维码图片
file, err := h.fs.Open("res/img/alipay.jpg")
var logo string
if data.PayWay == "alipay" {
logo = "res/img/alipay.jpg"
} else if data.PayWay == "hupi" {
if h.App.Config.HuPiPayConfig.Name == "wechat" {
logo = "res/img/wechat-pay.jpg"
} else {
logo = "res/img/alipay.jpg"
}
}
file, err := h.fs.Open(logo)
if err != nil {
resp.ERROR(c, err.Error())
resp.ERROR(c, "error with open qrcode log file: "+err.Error())
return
}
parse, err := url.Parse(h.App.Config.AlipayConfig.NotifyURL)
if err != nil {
resp.ERROR(c, err.Error())
return
}
imageURL := fmt.Sprintf("%s://%s/api/payment/alipay?order_no=%s", parse.Scheme, parse.Host, orderNo)
imageURL := fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s", parse.Scheme, parse.Host, orderNo, data.PayWay)
imgData, err := utils.GenQrcode(imageURL, 400, file)
if err != nil {
resp.ERROR(c, err.Error())
@@ -193,6 +250,7 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL})
}
// AlipayNotify 支付宝支付回调
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
err := c.Request.ParseForm()
if err != nil {
@@ -212,27 +270,46 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
h.lock.Lock()
defer h.lock.Unlock()
var order model.Order
res := h.db.Where("order_no = ?", r.OutTradeNo).First(&order)
if res.Error != nil {
logger.Error(res.Error)
err = h.notify(r.OutTradeNo)
if err != nil {
c.String(http.StatusOK, "fail")
return
}
c.String(http.StatusOK, "success")
}
// 异步通知回调公共逻辑
func (h *PaymentHandler) notify(orderNo string) error {
var order model.Order
res := h.db.Where("order_no = ?", orderNo).First(&order)
if res.Error != nil {
err := fmt.Errorf("error with fetch order: %v", res.Error)
logger.Error(err)
return err
}
// 已支付订单,直接返回
if order.Status == types.OrderPaidSuccess {
return nil
}
var user model.User
res = h.db.First(&user, order.UserId)
if res.Error != nil {
logger.Error(res.Error)
c.String(http.StatusOK, "fail")
return
err := fmt.Errorf("error with fetch user info: %v", res.Error)
logger.Error(err)
return err
}
var remark types.OrderRemark
err = utils.JsonDecode(order.Remark, &remark)
err := utils.JsonDecode(order.Remark, &remark)
if err != nil {
logger.Error(res.Error)
c.String(http.StatusOK, "fail")
return
err := fmt.Errorf("error with decode order remark: %v", err)
logger.Error(err)
return err
}
// 1. 点卡days == 0, calls > 0
// 2. vip 套餐days > 0, calls == 0
if remark.Days > 0 {
@@ -256,18 +333,57 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
// 更新用户信息
res = h.db.Updates(&user)
if res.Error != nil {
logger.Error(res.Error)
c.String(http.StatusOK, "fail")
return
err := fmt.Errorf("error with update user info: %v", res.Error)
logger.Error(err)
return err
}
// 更新订单状态
order.PayTime = time.Now().Unix()
order.Status = types.OrderPaidSuccess
h.db.Updates(&order)
res = h.db.Updates(&order)
if res.Error != nil {
err := fmt.Errorf("error with update order info: %v", res.Error)
logger.Error(err)
return err
}
// 更新产品销量
h.db.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
return nil
}
// GetPayWays 获取支付方式
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
data := gin.H{}
if h.App.Config.AlipayConfig.Enabled {
data["alipay"] = gin.H{"name": "alipay"}
}
if h.App.Config.HuPiPayConfig.Enabled {
data["hupi"] = gin.H{"name": h.App.Config.HuPiPayConfig.Name}
}
resp.SUCCESS(c, data)
}
// HuPiPayNotify 虎皮椒支付异步回调
func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
err := c.Request.ParseForm()
if err != nil {
c.String(http.StatusOK, "fail")
return
}
orderNo := c.Request.Form.Get("trade_order_id")
logger.Infof("收到订单支付回调,订单 NO%s", orderNo)
// TODO 是否要保存订单交易流水号
h.lock.Lock()
defer h.lock.Unlock()
err = h.notify(orderNo)
if err != nil {
c.String(http.StatusOK, "fail")
return
}
c.String(http.StatusOK, "success")
}

View File

@@ -0,0 +1,91 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/utils/resp"
"fmt"
"github.com/imroc/req/v3"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
const translatePromptTemplate = "Please rewrite the following text into AI painting prompt words, and please try to add detailed description of the picture, painting style, scene, rendering effect, picture light and other elements. Please output directly in English without any explanation, within 150 words. The text to be rewritten is: [%s]"
type PromptHandler struct {
BaseHandler
db *gorm.DB
}
func NewPromptHandler(app *core.AppServer, db *gorm.DB) *PromptHandler {
h := &PromptHandler{db: db}
h.App = app
return h
}
type apiRes struct {
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
type apiErrRes struct {
Error struct {
Code interface{} `json:"code"`
Message string `json:"message"`
Param interface{} `json:"param"`
Type string `json:"type"`
} `json:"error"`
}
func (h *PromptHandler) Translate(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 获取 OpenAI 的 API KEY
var apiKey model.ApiKey
res := h.db.Where("platform = ?", types.OpenAI).First(&apiKey)
if res.Error != nil {
resp.ERROR(c, "找不到可用 OpenAI API KEY")
return
}
messages := make([]interface{}, 1)
messages[0] = types.Message{
Role: "user",
Content: fmt.Sprintf(translatePromptTemplate, data.Prompt),
}
var response apiRes
var errRes apiErrRes
r, err := req.C().SetProxyURL(h.App.Config.ProxyURL).R().SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(types.ApiRequest{
Model: "gpt-3.5-turbo",
Temperature: 0.9,
MaxTokens: 1024,
Stream: false,
Messages: messages,
}).
SetErrorResult(&errRes).
SetSuccessResult(&response).Post(h.App.ChatConfig.OpenAI.ApiURL)
if err != nil || r.IsErrorState() {
resp.ERROR(c, fmt.Sprintf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message))
return
}
resp.SUCCESS(c, response.Choices[0].Message.Content)
}

View File

@@ -0,0 +1,40 @@
package handler
import (
"chatplus/service"
"github.com/gin-gonic/gin"
)
type TestHandler struct {
snowflake *service.Snowflake
}
func NewTestHandler(snowflake *service.Snowflake) *TestHandler {
return &TestHandler{snowflake: snowflake}
}
func (h *TestHandler) TestPay(c *gin.Context) {
//appId := "" //Appid
//appSecret := "" //密钥
//var host = "https://api.xunhupay.com/payment/do.html" //跳转支付页接口URL
//client := payment.NewXunHuPay(appId, appSecret) //初始化调用
//
////支付参数appid、time、nonce_str和hash这四个参数不用传调用的时候执行方法内部已经处理
//orderNo, _ := h.snowflake.Next()
//params := map[string]string{
// "version": "1.1",
// "trade_order_id": orderNo,
// "total_fee": "0.1",
// "title": "测试支付",
// "notify_url": "http://xxxxxxx.com",
// "return_url": "http://localhost:8888",
// "wap_name": "极客学长",
// "callback_url": "",
//}
//
//execute, err := client.Execute(host, params) //执行支付操作
//if err != nil {
// logger.Error(err)
//}
//resp.SUCCESS(c, execute)
}

View File

@@ -17,7 +17,6 @@ import (
"chatplus/store"
"context"
"embed"
"github.com/go-redis/redis/v8"
"io"
"log"
"os"
@@ -26,6 +25,8 @@ import (
"syscall"
"time"
"github.com/go-redis/redis/v8"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"go.uber.org/fx"
"gorm.io/gorm"
@@ -192,6 +193,7 @@ func main() {
}),
fx.Provide(payment.NewAlipayService),
fx.Provide(payment.NewHuPiPay),
fx.Provide(service.NewSnowflake),
fx.Provide(service.NewXXLJobExecutor),
fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
@@ -318,10 +320,12 @@ func main() {
}),
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
group := s.Engine.Group("/api/payment/")
group.GET("alipay", h.Alipay)
group.GET("doPay", h.DoPay)
group.GET("payWays", h.GetPayWays)
group.POST("query", h.OrderQuery)
group.POST("alipay/qrcode", h.AlipayQrcode)
group.POST("qrcode", h.PayQrcode)
group.POST("alipay/notify", h.AlipayNotify)
group.POST("hupipay/notify", h.HuPiPayNotify)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
group := s.Engine.Group("/api/admin/product/")
@@ -353,6 +357,17 @@ func main() {
group.GET("hits", h.Hits)
}),
fx.Provide(handler.NewPromptHandler),
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
group := s.Engine.Group("/api/prompt/")
group.POST("translate", h.Translate)
}),
fx.Provide(handler.NewTestHandler),
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
group := s.Engine.Group("/test/")
group.GET("pay", h.TestPay)
}),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
err := s.Run(db)
if err != nil {

BIN
api/res/img/wechat-pay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -1,20 +1,20 @@
{
"data": [
"task(m1wpaa4v60zedj8)",
"a cute cat",
"task(s95jqt5jr8yppcp)",
"A beautiful Chinese girl in a garden",
"",
[],
20,
"DPM++ 2M Karras",
30,
"Euler a",
1,
1,
7,
512,
384,
512,
true,
0.7,
2,
"ESRGAN_4x",
"Latent",
10,
0,
0,
@@ -55,13 +55,13 @@
false,
false,
0,
false,
[],
[
],
"",
"",
""
],
"event_data": null,
"fn_index": 96,
"session_hash": "kmb0ojjfhdj"
"fn_index": 95,
"session_hash": "eqwumnt3rov"
}

View File

@@ -6,6 +6,7 @@ import (
"chatplus/store/model"
"chatplus/utils"
"fmt"
"github.com/imroc/req/v3"
"gorm.io/gorm"
)
@@ -14,7 +15,6 @@ import (
type FuncImage struct {
name string
apiURL string
db *gorm.DB
uploadManager *oss.UploaderManager
proxyURL string
@@ -26,7 +26,6 @@ func NewImageFunc(db *gorm.DB, manager *oss.UploaderManager, config *types.AppCo
name: "DALL-E3 绘画",
uploadManager: manager,
proxyURL: config.ProxyURL,
apiURL: "https://api.openai.com/v1/images/generations",
}
}
@@ -57,9 +56,34 @@ type ErrRes struct {
func (f FuncImage) Invoke(params map[string]interface{}) (string, error) {
logger.Infof("绘画参数:%+v", params)
prompt := utils.InterfaceToString(params["prompt"])
// 获取绘图 API KEY
// get image generation API KEY
var apiKey model.ApiKey
f.db.Where("platform = ? AND type = ?", types.OpenAI, "img").Order("last_used_at ASC").First(&apiKey)
tx := f.db.Where("platform = ? AND type = ?", types.OpenAI, "img").Order("last_used_at ASC").First(&apiKey)
if tx.Error != nil {
return "", fmt.Errorf("error with get generation API KEY: %v", tx.Error)
}
// get image generation api URL
var conf model.Config
var chatConfig types.ChatConfig
tx = f.db.Where("marker", "chat").First(&conf)
if tx.Error != nil {
return "", fmt.Errorf("error with get chat configs: %v", tx.Error)
}
err := utils.JsonDecode(conf.Config, &chatConfig)
if err != nil {
return "", fmt.Errorf("error with decode chat config: %v", err)
}
apiURL := chatConfig.DallApiURL
if utils.IsEmptyValue(apiURL) {
apiURL = "https://api.openai.com/v1/images/generations"
}
imgNum := chatConfig.DallImgNum
if imgNum <= 0 {
imgNum = 1
}
var res imgRes
var errRes ErrRes
r, err := req.C().SetProxyURL(f.proxyURL).R().SetHeader("Content-Type", "application/json").
@@ -67,11 +91,11 @@ func (f FuncImage) Invoke(params map[string]interface{}) (string, error) {
SetBody(imgReq{
Model: "dall-e-3",
Prompt: prompt,
N: 1,
N: imgNum,
Size: "1024x1024",
}).
SetErrorResult(&errRes).
SetSuccessResult(&res).Post(f.apiURL)
SetSuccessResult(&res).Post(apiURL)
if err != nil || r.IsErrorState() {
return "", fmt.Errorf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message)
}

View File

@@ -0,0 +1,72 @@
package payment
import (
"chatplus/core/types"
"crypto/md5"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
type HuPiPayService struct {
appId string
appSecret string
host string
}
func NewHuPiPay(config *types.AppConfig) *HuPiPayService {
return &HuPiPayService{
appId: config.HuPiPayConfig.AppId,
appSecret: config.HuPiPayConfig.AppSecret,
host: config.HuPiPayConfig.PayURL,
}
}
// Pay 执行支付请求操作
func (s *HuPiPayService) Pay(params map[string]string) (string, error) {
data := url.Values{}
simple := strconv.FormatInt(time.Now().Unix(), 10)
params["appid"] = s.appId
params["time"] = simple
params["nonce_str"] = simple
for k, v := range params {
data.Add(k, v)
}
data.Add("hash", s.Sign(params))
resp, err := http.PostForm(s.host, data)
if err != nil {
return "error", err
}
defer resp.Body.Close()
all, err := io.ReadAll(resp.Body)
if err != nil {
return "error", err
}
return string(all), err
}
// Sign 签名方法
func (s *HuPiPayService) Sign(params map[string]string) string {
var data string
keys := make([]string, 0, 0)
params["appid"] = s.appId
for key, _ := range params {
keys = append(keys, key)
}
sort.Strings(keys)
//拼接
for _, k := range keys {
data = fmt.Sprintf("%s%s=%s&", data, k, params[k])
}
data = strings.Trim(data, "&")
data = fmt.Sprintf("%s%s", data, s.appSecret)
m := md5.New()
m.Write([]byte(data))
sign := fmt.Sprintf("%x", m.Sum(nil))
return sign
}

View File

@@ -135,7 +135,6 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
"fn_index": taskInfo.FnIndex,
"session_hash": taskInfo.SessionHash,
}
logger.Debug(utils.JsonEncode(body))
var result = make(chan CBReq)
go func() {
var res struct {
@@ -231,7 +230,6 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
cbReq.ImageData = progressRes.LivePreview
cbReq.Progress = int(progressRes.Progress * 100)
logger.Debug(cbReq)
s.callback(cbReq)
time.Sleep(time.Second)
}
@@ -287,8 +285,13 @@ func (s *Service) callback(data CBReq) {
if data.Progress < 100 && data.ImageData != "" {
jobVo.ImgURL = data.ImageData
}
logger.Infof("绘图进度:%d", data.Progress)
// 扣减绘图次数
s.db.Model(&model.User{}).Where("id = ?", jobVo.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
if data.Progress == 100 {
s.db.Model(&model.User{}).Where("id = ? AND img_calls > 0", jobVo.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
}
// 推送任务到前端
if client != nil {
utils.ReplyChunkMessage(client, jobVo)

View File

@@ -1,9 +1,11 @@
#!/bin/bash
version=$1
# build go api
arch=${2:-amd64}
# build go api program
cd ../api
make clean linux
make clean $arch
# build web app
cd ../web
@@ -12,15 +14,15 @@ npm run build
cd ../build
# remove docker image if exists
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version-$arch
# build docker image for chatgpt-plus-go
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version -f dockerfile-api-go ../
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version-$arch -f dockerfile-api-go ../
# build docker image for chatgpt-plus-vue
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version
docker build --platform linux/amd64 -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version -f dockerfile-vue ../
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version-$arch
docker build --platform linux/amd64 -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version-$arch -f dockerfile-vue ../
if [ "$2" = "push" ];then
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version
if [ "$3" = "push" ];then
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version-$arch
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version-$arch
fi

View File

@@ -4,9 +4,9 @@ FROM alpine:3.18.2
MAINTAINER yangjian<yangjian102621@163.com>
WORKDIR /var/www/app
COPY ./api/bin/chatgpt-plus-amd64-linux /var/www/app
COPY ./api/bin/chatgpt-plus-linux /var/www/app
EXPOSE 5678
# 容器启动时执行的命令
CMD ["./chatgpt-plus-amd64-linux"]
CMD ["./chatgpt-plus-linux"]

View File

@@ -37,7 +37,7 @@ WeChatBot = false
Active = "local" # 默认使用本地文件存储引擎
[OSS.Local]
BasePath = "./static/upload" # 本地文件上传根路径
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
BaseURL = "/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
[OSS.Minio]
Endpoint = "" # 如 172.22.11.200:9000
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
@@ -82,4 +82,12 @@ WeChatBot = false
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
NotifyURL = "http://ai.r9it.com/api/payment/alipay/notify" # 支付异步回调地址
[HuPiPayConfig] # 虎皮椒支付配置
Enabled = false
Name = "wechat"
AppId = ""
AppSecret = ""
PayURL = "https://api.xunhupay.com/payment/do.html"
NotifyURL = "http://ai.r9it.com/api/payment/hupipay/notify"

View File

@@ -1,115 +1,186 @@
.task-list-box {
width: 100%;
padding: 10px;
color: #fff;
overflow-x: hidden;
width: 100%;
padding: 10px;
color: #fff;
overflow-x: hidden;
}
.task-list-box .task-list-inner .el-tabs {
--el-tabs-header-height: 55px;
}
.task-list-box .task-list-inner .el-tabs__item {
color: #fff;
font-size: 18px;
}
.task-list-box .task-list-inner .title-tabs .el-tabs__item.is-active {
color: #47fff1;
font-size: 18px;
}
.task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
background-color: #47fff1;
}
.task-list-box .task-list-inner .title-tabs .el-tabs__content {
padding: 10px 0;
}
.task-list-box .task-list-inner .el-textarea {
--el-input-focus-border-color: #47fff1;
}
.task-list-box .task-list-inner .el-textarea__inner {
background: transparent;
color: #fff;
}
.task-list-box .task-list-inner .el-input__wrapper {
background: transparent;
padding: 5px;
}
.task-list-box .task-list-inner .text {
margin-bottom: 10px;
color: #6b778c;
font-size: 15px;
}
.task-list-box .task-list-inner .param-line.pt {
padding-top: 5px;
padding-bottom: 5px;
}
.task-list-box .task-list-inner .form-item-inner {
display: flex;
align-items: center;
}
.task-list-box .task-list-inner .form-item-inner .el-icon {
margin-left: 10px;
}
.task-list-box .task-list-inner .el-form-item__label {
color: #fff;
}
.task-list-box .task-list-inner .img-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 300px;
transition: var(--el-transition-duration-fast);
margin-bottom: 20px;
}
.task-list-box .task-list-inner .img-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.task-list-box .task-list-inner .img-uploader .el-upload .el-icon.uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100%;
height: 120px;
text-align: center;
}
.task-list-box .task-list-inner .submit-btn {
display: flex;
margin: 20px 0;
}
.task-list-box .task-list-inner .submit-btn .el-button {
width: 200px;
}
.task-list-box .task-list-inner .submit-btn .text-info {
width: 100%;
display: flex;
justify-content: right;
align-items: center;
}
.task-list-box .running-job-list .job-item {
width: 100%;
padding: 2px;
background-color: #555;
width: 100%;
padding: 2px;
background-color: #555;
}
.task-list-box .running-job-list .job-item .job-item-inner {
position: relative;
height: 100%;
overflow: hidden;
position: relative;
height: 100%;
overflow: hidden;
}
.task-list-box .running-job-list .job-item .job-item-inner .progress {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.task-list-box .running-job-list .job-item .job-item-inner .progress span {
font-size: 20px;
color: #fff;
font-size: 20px;
color: #fff;
}
.task-list-box .finish-job-list .job-item {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
border: 1px solid #666;
padding: 6px;
overflow: hidden;
border-radius: 6px;
transition: all 0.3s ease; /* 添加过渡效果 */
}
.task-list-box .finish-job-list .job-item .opt .opt-line {
margin: 6px 0;
margin: 6px 0;
}
.task-list-box .finish-job-list .job-item .opt .opt-line ul {
display: flex;
flex-flow: row;
display: flex;
flex-flow: row;
}
.task-list-box .finish-job-list .job-item .opt .opt-line ul li {
margin-right: 10px;
margin-right: 6px;
}
.task-list-box .finish-job-list .job-item .opt .opt-line ul li a {
padding: 3px 0;
width: 44px;
text-align: center;
border-radius: 5px;
display: block;
cursor: pointer;
background-color: #4e5058;
color: #fff;
padding: 3px 0;
width: 40px;
text-align: center;
border-radius: 5px;
display: block;
cursor: pointer;
background-color: #4e5058;
color: #fff;
}
.task-list-box .finish-job-list .job-item .opt .opt-line ul li a:hover {
background-color: #6d6f78;
background-color: #6d6f78;
}
.task-list-box .finish-job-list .job-item .opt .opt-line ul .show-prompt {
font-size: 20px;
cursor: pointer;
font-size: 20px;
cursor: pointer;
}
.task-list-box .finish-job-list .animate:hover {
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */
transform: translateY(-10px); /* 向上移动10像素 */
}
.task-list-box .el-image {
width: 100%;
height: 100%;
max-height: 240px;
width: 100%;
height: 100%;
overflow: visible;
}
.task-list-box .el-image img {
height: 240px;
height: 240px;
}
.task-list-box .el-image .el-image-viewer__wrapper img {
width: auto;
height: auto;
width: auto;
height: auto;
}
.task-list-box .el-image .image-slot {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
height: 100%;
min-height: 200px;
color: #fff;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
height: 100%;
min-height: 200px;
color: #fff;
height: 240px;
}
.task-list-box .el-image .image-slot .iconfont {
font-size: 50px;
margin-bottom: 10px;
font-size: 50px;
margin-bottom: 10px;
}
.task-list-box .el-image.upscale {
max-height: 312px;
max-height: 310px;
}
.task-list-box .el-image.upscale img {
height: 312px;
height: 310px;
}
.task-list-box .el-image.upscale .el-image-viewer__wrapper img {
width: auto;
height: auto;
width: auto;
height: auto;
}

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1697164072791') format('woff2'),
url('iconfont.woff?t=1697164072791') format('woff'),
url('iconfont.ttf?t=1697164072791') format('truetype');
src: url('iconfont.woff2?t=1702024026523') format('woff2'),
url('iconfont.woff?t=1702024026523') format('woff'),
url('iconfont.ttf?t=1702024026523') format('truetype');
}
.iconfont {
@@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-alipay:before {
content: "\e634";
}
.icon-face:before {
content: "\e64b";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,13 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "1486848",
"name": "支付宝支付",
"font_class": "alipay",
"unicode": "e634",
"unicode_decimal": 58932
},
{
"icon_id": "845789",
"name": "笑脸",

Binary file not shown.

View File

@@ -105,12 +105,12 @@
:value="item.id"
/>
</el-select>
<!-- <el-button type="primary" @click="newChat">-->
<!-- <el-icon>-->
<!-- <Plus/>-->
<!-- </el-icon>-->
<!-- 新建-->
<!-- </el-button>-->
<el-button type="primary" @click="newChat">
<el-icon>
<Plus/>
</el-icon>
新建
</el-button>
<el-button type="success" @click="exportChat" plain>
<i class="iconfont icon-export"></i>
@@ -237,7 +237,7 @@ import {
Check,
Close,
Delete,
Edit,
Edit, Plus,
Promotion,
RefreshRight,
Search,
@@ -245,7 +245,7 @@ import {
VideoPause
} from '@element-plus/icons-vue'
import 'highlight.js/styles/a11y-dark.css'
import {dateFormat, isMobile, randString, removeArrayItem, renderInputText, UUID} from "@/utils/libs";
import {dateFormat, isMobile, randString, removeArrayItem, UUID} from "@/utils/libs";
import {ElMessage, ElMessageBox} from "element-plus";
import hl from "highlight.js";
import {getSessionId, getUserToken, removeUserToken} from "@/store/session";
@@ -624,6 +624,7 @@ const connect = function (chat_id, role_id) {
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
localStorage.setItem("chat_id", chat_id)
})
};
}
@@ -689,13 +690,12 @@ const sendMessage = function () {
if (prompt.value.trim().length === 0 || canSend.value === false) {
return false;
}
// 追加消息
chatData.value.push({
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: renderInputText(prompt.value),
content: md.render(prompt.value),
created_at: new Date().getTime(),
});
@@ -760,10 +760,7 @@ const loadChatHistory = function (chatId) {
}
showHello.value = false
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
chatData.value.push(data[i]);
continue;
} else if (data[i].type === "mj") {
if (data[i].type === "mj") {
data[i].content = JSON.parse(data[i].content)
data[i].content.html = md.render(data[i].content?.content)
chatData.value.push(data[i]);
@@ -801,7 +798,7 @@ const reGenerate = function () {
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: renderInputText(text)
content: md.render(text)
});
socket.value.send(text);
}

View File

@@ -233,29 +233,31 @@
</el-tab-pane>
</el-tabs>
<div class="param-line pt">
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>提示词</span>
<el-tooltip effect="light" content="输入你想要的内容,用逗号分割" placement="right">
<el-icon>
<InfoFilled/>
<div v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.8)">
<div class="param-line pt">
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>提示词</span>
<el-tooltip effect="light" content="输入你想要的内容,用逗号分割" placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
<el-button type="success" @click="translatePrompt">
<el-icon style="margin-right: 6px;font-size: 18px;">
<Refresh/>
</el-icon>
</el-tooltip>
翻译
</el-button>
</div>
<el-button type="success">
<el-icon style="margin-right: 6px;font-size: 18px;">
<Refresh/>
</el-icon>
翻译
</el-button>
</div>
</div>
<div class="param-line pt">
<el-input v-model="params.prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
ref="promptRef"
placeholder="这里输入你的英文咒语例如A chinese girl walking in the middle of a cobblestone street"/>
<div class="param-line pt">
<el-input v-model="params.prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
ref="promptRef"
placeholder="这里输入你的英文咒语例如A chinese girl walking in the middle of a cobblestone street"/>
</div>
</div>
<div class="param-line pt">
@@ -268,12 +270,12 @@
</el-icon>
</el-tooltip>
</div>
<el-button type="success">
<el-icon style="margin-right: 6px;font-size: 18px;">
<Refresh/>
</el-icon>
翻译
</el-button>
<!-- <el-button type="success">-->
<!-- <el-icon style="margin-right: 6px;font-size: 18px;">-->
<!-- <Refresh/>-->
<!-- </el-icon>-->
<!-- 翻译-->
<!-- </el-button>-->
</div>
</div>
@@ -286,7 +288,7 @@
<div class="submit-btn">
<el-button color="#47fff1" :dark="false" @click="generate" round>立即生成</el-button>
<div class="text-info">
<el-tag type="success">可用额度{{ imgCalls }}</el-tag>
<el-tag type="success">绘图可用额度{{ imgCalls }}</el-tag>
</div>
</div>
</el-form>
@@ -336,7 +338,7 @@
<h2>创作记录</h2>
<div class="finish-job-list">
<ItemList :items="finishedJobs" v-if="finishedJobs.length > 0" width="240" :gap="16">
<ItemList :items="finishedJobs" v-if="finishedJobs.length > 0" :width="240" :gap="16">
<template #default="scope">
<div class="job-item">
<el-image
@@ -500,6 +502,7 @@ const router = useRouter()
const socket = ref(null)
const imgCalls = ref(0)
const loading = ref(false)
const connect = () => {
let host = process.env.VUE_APP_WS_HOST
@@ -568,6 +571,16 @@ const connect = () => {
});
}
const translatePrompt = () => {
loading.value = true
httpPost("/api/prompt/translate", {"prompt": params.value.prompt}).then(res => {
params.value.prompt = res.data
loading.value = false
}).then(e => {
ElMessage.error("翻译失败:" + e.message)
})
}
onMounted(() => {
checkSession().then(user => {
imgCalls.value = user['img_calls']

View File

@@ -500,21 +500,21 @@ window.onresize = () => {
}
const samplers = ["Euler a", "Euler", "DPM2 a Karras", "DPM++ 2S a Karras", "DPM++ 2M Karras", "DPM++ SDE Karras", "DPM2", "DPM2 a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM fast", "DPM adaptive",
"LMS Karras", "DPM2 Karras", "DDIM", "PLMS", "UniPC", "LMS", "Heun",]
const scaleAlg = ["ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
const params = ref({
width: 1024,
height: 1024,
sampler: samplers[0],
seed: -1,
steps: 20,
steps: 30,
cfg_scale: 7,
face_fix: false,
hd_fix: false,
hd_redraw_rate: 0.3,
hd_redraw_rate: 0.7,
hd_scale: 2,
hd_scale_alg: scaleAlg[0],
hd_steps: 0,
prompt: "A beautiful Chinese girl riding on a tiger",
hd_steps: 10,
prompt: "",
negative_prompt: "nsfw, paintings, cartoon, anime, sketches, low quality,easynegative,ng_deepnegative _v1 75t,(worst quality:2),(low quality:2),(normalquality:2),lowres,bad anatomy,bad hands,normal quality,((monochrome)),((grayscale)),((watermark))",
})

View File

@@ -41,7 +41,7 @@
<ItemList :items="list" v-if="list.length > 0" :gap="30" :width="240">
<template #default="scope">
<div class="product-item" :style="{width: scope.width+'px'}" @click="orderPay(scope.item)">
<div class="product-item" :style="{width: scope.width+'px'}">
<div class="image-container">
<el-image :src="vipImg" fit="cover"/>
</div>
@@ -62,6 +62,16 @@
<span class="expire" v-if="scope.item.days > 0">{{ scope.item.days }}</span>
<span class="expire" v-else>当月有效</span>
</div>
<div class="pay-way">
<el-button type="primary" @click="alipay(scope.item)" size="small" v-if="payWays['alipay']">
<i class="iconfont icon-alipay"></i> 支付宝
</el-button>
<el-button type="success" @click="huPiPay(scope.item)" size="small" v-if="payWays['hupi']">
<span v-if="payWays['hupi']['name'] === 'wechat'"><i class="iconfont icon-wechat-pay"></i> 微信</span>
<span v-else><i class="iconfont icon-alipay"></i> 支付宝</span>
</el-button>
</div>
</div>
</div>
</template>
@@ -113,7 +123,7 @@
title="充值订单支付">
<div class="pay-container">
<div class="count-down">
<count-down :second="orderTimeout" @timeout="orderPay" ref="countDown"/>
<count-down :second="orderTimeout" @timeout="refreshPayCode" ref="countDownRef"/>
</div>
<div class="pay-qrcode" v-loading="loading">
@@ -130,9 +140,10 @@
<el-icon>
<InfoFilled/>
</el-icon>
<span class="text">请打开手机支付宝扫码支付</span>
<span class="text">请打开手机{{ payName }}扫码支付</span>
</div>
</div>
</el-dialog>
</div>
</template>
@@ -152,6 +163,7 @@ 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([])
@@ -171,11 +183,15 @@ const isLogin = ref(false)
const router = useRouter()
const curPayProduct = ref(null)
const activeOrderNo = ref("")
const countDown = ref(null)
const countDownRef = ref(null)
const orderTimeout = ref(1800)
const loading = ref(true)
const orderPayInfoText = ref("")
const payWays = ref({})
const payName = ref("支付宝")
const curPay = ref("alipay") // 当前支付方式
onMounted(() => {
checkSession().then(_user => {
@@ -200,9 +216,48 @@ onMounted(() => {
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
httpGet("/api/payment/payWays").then(res => {
payWays.value = res.data
}).catch(e => {
ElMessage.error("获取支付方式失败:" + e.message)
})
})
const orderPay = (row) => {
// refresh payment qrcode
const refreshPayCode = () => {
if (curPay.value === 'alipay') {
alipay()
} else if (curPay.value === 'hupi') {
huPiPay()
}
}
const genPayQrcode = () => {
loading.value = true
text.value = ""
httpPost("/api/payment/qrcode", {
pay_way: curPay.value,
product_id: curPayProduct.value.id,
user_id: user.value.id
}).then(res => {
showPayDialog.value = true
qrcode.value = res.data['image']
activeOrderNo.value = res.data['order_no']
queryOrder(activeOrderNo.value)
loading.value = false
// 重置计数器
if (countDownRef.value) {
countDownRef.value.resetTimer()
}
}).catch(e => {
ElMessage.error("生成支付订单失败:" + e.message)
})
}
const alipay = (row) => {
payName.value = "支付宝"
curPay.value = "alipay"
if (!user.value.id) {
showLoginDialog.value = true
return
@@ -210,21 +265,22 @@ const orderPay = (row) => {
if (row) {
curPayProduct.value = row
}
loading.value = true
text.value = ""
httpPost("/api/payment/alipay/qrcode", {product_id: curPayProduct.value.id, user_id: user.value.id}).then(res => {
showPayDialog.value = true
qrcode.value = res.data['image']
activeOrderNo.value = res.data['order_no']
queryOrder(activeOrderNo.value)
loading.value = false
// 重置计数器
if (countDown.value) {
countDown.value.resetTimer()
}
}).catch(e => {
ElMessage.error("生成支付订单失败:" + e.message)
})
genPayQrcode()
}
// 虎皮椒支付
const huPiPay = (row) => {
payName.value = payWays.value["hupi"]["name"] === "wechat" ? '微信' : '支付宝'
curPay.value = "hupi"
if (!user.value.id) {
showLoginDialog.value = true
return
}
if (row) {
curPayProduct.value = row
}
genPayQrcode()
}
const queryOrder = (orderNo) => {
@@ -416,6 +472,16 @@ const logout = function () {
}
}
.pay-way {
padding 10px 0
display flex
justify-content: space-between
.iconfont {
margin-right 5px
}
}
}
&:hover {

View File

@@ -249,6 +249,13 @@
<el-input v-model.number="chat['xun_fei']['max_tokens']" placeholder="回复的最大字数最大4096"/>
</el-form-item>
<el-divider content-position="center">AI绘图</el-divider>
<el-form-item label="DALL-E3 API地址">
<el-input v-model="chat['dall_api_url']" placeholder="OpenAI官方API需要配合代理使用"/>
</el-form-item>
<el-form-item label="默认出图数量">
<el-input v-model.number="chat['dall_img_num']" placeholder="调用 DALL E3 API 传入的出图数量"/>
</el-form-item>
<el-form-item style="text-align: right">
<el-button type="primary" @click="save('chat')">保存</el-button>
</el-form-item>
@@ -274,6 +281,7 @@ const chat = ref({
context_deep: 0,
enable_context: true,
enable_history: true,
dall_api_url: "",
})
const loading = ref(true)
const systemFormRef = ref(null)
@@ -290,25 +298,7 @@ onMounted(() => {
// 加载聊天配置
httpGet('/api/admin/config/get?key=chat').then(res => {
// chat.value = res.data
if (res.data.open_ai) {
chat.value.open_ai = res.data.open_ai
}
if (res.data.azure) {
chat.value.azure = res.data.azure
}
if (res.data.chat_gml) {
chat.value.chat_gml = res.data.chat_gml
}
if (res.data.baidu) {
chat.value.baidu = res.data.baidu
}
if (res.data.xun_fei) {
chat.value.xun_fei = res.data.xun_fei
}
chat.value.context_deep = res.data.context_deep
chat.value.enable_context = res.data.enable_context
chat.value.enable_history = res.data.enable_history
chat.value = res.data
loading.value = false
}).catch(e => {
ElMessage.error("加载聊天配置失败: " + e.message)