Merge pull request 'hotfix' (#94) from hotfix into main
All checks were successful
BuildImage / build-image (push) Successful in 1m48s

Reviewed-on: #94
This commit is contained in:
李寻欢 2024-08-17 13:16:25 +08:00
commit afa519175b
16 changed files with 173 additions and 14 deletions

View File

@ -280,3 +280,26 @@ func AutoClearMembers(ctx *gin.Context) {
ctx.String(http.StatusOK, "操作成功") ctx.String(http.StatusOK, "操作成功")
} }
// ChangeAiFreeLimit
// @description: 修改AI免费次数
// @param ctx
func ChangeAiFreeLimit(ctx *gin.Context) {
var p autoClearMembers
if err := ctx.ShouldBindJSON(&p); err != nil {
ctx.String(http.StatusBadRequest, "参数错误")
return
}
log.Printf("待修改的Id%s", p.WxId)
err := client.MySQL.Model(&entity.Friend{}).
Where("wxid = ?", p.WxId).
Update("`ai_free_limit`", p.Days).Error
if err != nil {
log.Printf("修改AI免费次数失败%s", err)
ctx.String(http.StatusInternalServerError, "操作失败: %s", err)
return
}
ctx.String(http.StatusOK, "操作成功")
}

View File

@ -25,6 +25,8 @@ system:
news: true news: true
# 热榜 # 热榜
hotTop: true hotTop: true
# 每天免费 AI 次数限制
aiFreeLimit: 50
# 微信HOOK配置 # 微信HOOK配置
wechat: wechat:

View File

@ -16,10 +16,11 @@ type newFriendNotify struct {
// 默认规则 // 默认规则
type defaultRule struct { type defaultRule struct {
Ai bool `json:"ai" yaml:"ai"` // 是否启用AI Ai bool `json:"ai" yaml:"ai"` // 是否启用AI
ChatRank bool `json:"chatRank" yaml:"chatRank"` // 是否启用聊天排行榜 ChatRank bool `json:"chatRank" yaml:"chatRank"` // 是否启用聊天排行榜
Summary bool `json:"summary" yaml:"summary"` // 是否启用聊天总结 Summary bool `json:"summary" yaml:"summary"` // 是否启用聊天总结
Welcome bool `json:"welcome" yaml:"welcome"` // 是否启用欢迎新成员 Welcome bool `json:"welcome" yaml:"welcome"` // 是否启用欢迎新成员
News bool `json:"news" yaml:"news"` // 是否启用每日早报 News bool `json:"news" yaml:"news"` // 是否启用每日早报
HotTop bool `json:"hotTop" yaml:"hotTop"` // 是否启用热门话题 HotTop bool `json:"hotTop" yaml:"hotTop"` // 是否启用热门话题
AiFreeLimit int `json:"aiFreeLimit" yaml:"aiFreeLimit"` // AI免费次数
} }

View File

@ -25,6 +25,8 @@ type Friend struct {
ClearMember int `json:"clearMember"` // 清理成员配置(多少天未活跃的) ClearMember int `json:"clearMember"` // 清理成员配置(多少天未活跃的)
IsOk bool `json:"isOk" gorm:"type:tinyint(1) default 0 not null"` // 是否正常 IsOk bool `json:"isOk" gorm:"type:tinyint(1) default 0 not null"` // 是否正常
UsedTokens int `json:"usedTokens"` // 已使用的AI Token数量 UsedTokens int `json:"usedTokens"` // 已使用的AI Token数量
AiFreeLimit int `json:"aiFreeLimit"` // AI免费次数
AiUsedToday int `json:"aiUsedToday"` // 今日已使用的AI次数
} }
func (Friend) TableName() string { func (Friend) TableName() string {

View File

@ -24,6 +24,8 @@ type FriendItem struct {
EnableHotTop bool // 是否启用热搜 EnableHotTop bool // 是否启用热搜
ClearMember int // 清理成员配置(多少天未活跃的) ClearMember int // 清理成员配置(多少天未活跃的)
IsOk bool // 是否还在通讯库(群聊是要还在群里也算) IsOk bool // 是否还在通讯库(群聊是要还在群里也算)
AiFreeLimit int // AI免费次数
AiUsedToday int // 今日已使用的AI次数
} }
// GroupUserItem // GroupUserItem

View File

@ -19,6 +19,8 @@ import (
"time" "time"
) )
var notifyMap = make(map[string]bool)
// AI // AI
// @description: AI消息 // @description: AI消息
// @param m // @param m
@ -37,6 +39,23 @@ func AI(m *plugin.MessageContext) {
if !friendInfo.EnableAi { if !friendInfo.EnableAi {
return return
} }
if friendInfo.AiUsedToday > 0 && friendInfo.AiUsedToday >= friendInfo.AiFreeLimit {
if notifyMap[m.FromUser] {
return
}
_ = utils.SendMessage(m.FromUser, "", fmt.Sprintf("本群今天的免费次数已经用完啦,明天再来找我聊天吧~\n每天限制%d次0点自动重置", friendInfo.AiFreeLimit), 0)
notifyMap[m.FromUser] = true
return
} else {
notifyMap[m.FromUser] = false
}
var err error
defer func() {
if err == nil {
service.UpdateAiUsedToday(m.FromUser)
}
}()
// 预处理一下发送的消息,用正则去掉@机器人的内容 // 预处理一下发送的消息,用正则去掉@机器人的内容
re := regexp.MustCompile(`@([^| ]+)`) re := regexp.MustCompile(`@([^| ]+)`)
@ -132,17 +151,17 @@ func AI(m *plugin.MessageContext) {
if err != nil { if err != nil {
log.Printf("OpenAI聊天发起失败: %v", err.Error()) log.Printf("OpenAI聊天发起失败: %v", err.Error())
utils.SendMessage(m.FromUser, m.GroupUser, "AI聊天初始化失败我已经通知我主人来修啦请稍候一下下喔", 0) _ = utils.SendMessage(m.FromUser, m.GroupUser, "AI聊天初始化失败我已经通知我主人来修啦请稍候一下下喔", 0)
return return
} }
// 返回消息为空 // 返回消息为空
if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == "" { if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == "" {
utils.SendMessage(m.FromUser, m.GroupUser, "AI似乎抽风了没有告诉我你需要的回答~", 0) _ = utils.SendMessage(m.FromUser, m.GroupUser, "AI似乎抽风了没有告诉我你需要的回答~", 0)
return return
} }
// 异步更新一下已使用的AI次数 // 异步更新一下已使用的AI tokens
go service.UpdateUsedAiTokens(m.FromUser, resp.Usage.TotalTokens) go service.UpdateUsedAiTokens(m.FromUser, resp.Usage.TotalTokens)
// 保存一下AI 返回的消息,消息 Id 使用传入 Id 的负数 // 保存一下AI 返回的消息,消息 Id 使用传入 Id 的负数
@ -162,7 +181,7 @@ func AI(m *plugin.MessageContext) {
if m.GroupUser != "" { if m.GroupUser != "" {
replyMsg = "\n" + resp.Choices[0].Message.Content replyMsg = "\n" + resp.Choices[0].Message.Content
} }
utils.SendMessage(m.FromUser, m.GroupUser, replyMsg, 0) err = utils.SendMessage(m.FromUser, m.GroupUser, replyMsg, 0)
} }
// getGroupUserMessages // getGroupUserMessages

View File

@ -26,6 +26,7 @@ func Init(g *gin.Engine) {
api := g.Group("/api") api := g.Group("/api")
api.PUT("/ai/status", app.ChangeEnableAiStatus) // 修改是否开启AI状态 api.PUT("/ai/status", app.ChangeEnableAiStatus) // 修改是否开启AI状态
api.POST("/ai/model", app.ChangeUseAiModel) // 修改使用的AI模型 api.POST("/ai/model", app.ChangeUseAiModel) // 修改使用的AI模型
api.POST("/ai/free", app.ChangeAiFreeLimit) // 修改AI免费次数
api.POST("/ai/assistant", app.ChangeUseAiAssistant) // 修改使用的AI助手 api.POST("/ai/assistant", app.ChangeUseAiAssistant) // 修改使用的AI助手
api.PUT("/welcome/status", app.ChangeEnableWelcomeStatus) // 修改是否开启迎新状态 api.PUT("/welcome/status", app.ChangeEnableWelcomeStatus) // 修改是否开启迎新状态
api.PUT("/command/status", app.ChangeEnableCommandStatus) // 修改是否开启指令状态 api.PUT("/command/status", app.ChangeEnableCommandStatus) // 修改是否开启指令状态

View File

@ -152,3 +152,25 @@ func UpdateUsedAiTokens(wxId string, tokens int) {
log.Printf("更新AI使用次数失败, 错误信息: %v", err) log.Printf("更新AI使用次数失败, 错误信息: %v", err)
} }
} }
// UpdateAiUsedToday
// @description: 更新AI今日使用次数
// @param wxId
func UpdateAiUsedToday(wxId string) {
err := client.MySQL.Model(&entity.Friend{}).
Where("wxid = ?", wxId).
Update("`ai_used_today`", gorm.Expr(" `ai_used_today` + 1")).Error
if err != nil {
log.Printf("更新AI今日使用次数失败, 错误信息: %v", err)
}
}
// ClearAiUsedToday
// @description: 清空AI今日使用次数
func ClearAiUsedToday() {
err := client.MySQL.Model(&entity.Friend{}).
Update("`ai_used_today`", 0).Error
if err != nil {
log.Printf("清空AI今日使用次数失败, 错误信息: %v", err)
}
}

9
tasks/friends/ai.go Normal file
View File

@ -0,0 +1,9 @@
package friends
import "go-wechat/service"
// ClearAiUsedToday
// @description: 清空AI日使用次数
func ClearAiUsedToday() {
service.ClearAiUsedToday()
}

View File

@ -85,6 +85,7 @@ func Sync() {
EnableWelcome: config.Conf.System.DefaultRule.Welcome, EnableWelcome: config.Conf.System.DefaultRule.Welcome,
EnableNews: config.Conf.System.DefaultRule.News, EnableNews: config.Conf.System.DefaultRule.News,
EnableHotTop: config.Conf.System.DefaultRule.HotTop, EnableHotTop: config.Conf.System.DefaultRule.HotTop,
AiFreeLimit: config.Conf.System.DefaultRule.AiFreeLimit,
ClearMember: 0, ClearMember: 0,
LastActive: types.DateTime(time.Now().Local()), LastActive: types.DateTime(time.Now().Local()),
}).Error }).Error

View File

@ -65,6 +65,8 @@ func InitTasks() {
// 每天0点检查一次处理清理群成员 // 每天0点检查一次处理清理群成员
_, _ = s.Cron("0 0 * * *").Do(cleargroupuser.ClearGroupUser) _, _ = s.Cron("0 0 * * *").Do(cleargroupuser.ClearGroupUser)
// 每天0点清空AI日使用次数
_, _ = s.Cron("0 0 * * *").Do(friends.ClearAiUsedToday)
// 开启定时任务 // 开启定时任务
s.StartAsync() s.StartAsync()

View File

@ -2,6 +2,7 @@ package utils
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"go-wechat/common/current" "go-wechat/common/current"
@ -15,9 +16,10 @@ import (
// @param toId // @param toId
// @param atId // @param atId
// @param msg // @param msg
func SendMessage(toId, atId, msg string, retryCount int) { func SendMessage(toId, atId, msg string, retryCount int) (err error) {
if retryCount > 5 { if retryCount > 5 {
log.Printf("重试五次失败,停止发送") log.Printf("重试五次失败,停止发送")
err = errors.New("重试五次失败,停止发送")
return return
} }
@ -48,9 +50,10 @@ func SendMessage(toId, atId, msg string, retryCount int) {
log.Printf("发送文本消息失败: %s", err.Error()) log.Printf("发送文本消息失败: %s", err.Error())
// 休眠五秒后重新发送 // 休眠五秒后重新发送
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
SendMessage(toId, atId, msg, retryCount+1) return SendMessage(toId, atId, msg, retryCount+1)
} }
log.Printf("发送文本消息结果: %s", resp.String()) log.Printf("发送文本消息结果: %s", resp.String())
return
} }
// SendImage // SendImage

View File

@ -94,6 +94,26 @@
</label> </label>
</dd> </dd>
</div> </div>
<!-- 每日 AI 免费次数限制 -->
<div class="flex justify-between gap-x-4 py-3 items-center">
<dt class="text-gray-500">
每日 AI 免费次数
</dt>
<dd class="flex items-start gap-x-2 items-center">
<div class="relative rounded-md">
<label>
<input type="number" id="auto-ai-{{ .Wxid }}" min="0" class="block w-1/2 float-end rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" placeholder="每日 AI 免费次数限制"
value="{{.AiFreeLimit}}"
onblur="changeAiFreeLimit({{.Wxid}}, this.value)"
>
</label>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span class="text-gray-500 sm:text-sm" id="price-currency"></span>
</div>
</div>
</dd>
</div>
{{ end }} {{ end }}
<div class="flex justify-between gap-x-4 py-3"> <div class="flex justify-between gap-x-4 py-3">

View File

@ -73,8 +73,7 @@
<select <select
class="block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-green-600 sm:text-sm sm:leading-6" class="block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-green-600 sm:text-sm sm:leading-6"
onchange="aiAssistantChange(event, {{.Wxid}})"> onchange="aiAssistantChange(event, {{.Wxid}})">
<option value="" {{ if eq .Prompt <option value="" {{ if eq .Prompt "" }}selected{{ end }}>默认</option>
"" }}selected{{ end }}>默认</option>
{{$usePrompt := .Prompt}} {{$usePrompt := .Prompt}}
{{ range $.assistant }} {{ range $.assistant }}
@ -89,6 +88,26 @@
{{ end }} {{ end }}
</dd> </dd>
</div> </div>
<!-- 每日 AI 免费次数限制 -->
<div class="flex justify-between gap-x-4 py-3 items-center">
<dt class="text-gray-500">
每日 AI 免费次数限制
<span class="text-red-300">(0表示不限)</span>
</dt>
<dd class="flex items-start gap-x-2 items-center">
<div class="relative rounded-md">
<label>
<input type="number" id="auto-ai-{{ .Wxid }}" min="0" class="block w-1/2 float-end rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" placeholder="每日 AI 免费次数限制"
value="{{.AiFreeLimit}}"
onblur="changeAiFreeLimit({{.Wxid}}, this.value)"
>
</label>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span class="text-gray-500 sm:text-sm" id="price-currency"></span>
</div>
</div>
</dd>
</div>
<!-- 水群排行榜 --> <!-- 水群排行榜 -->
<div class="flex justify-between gap-x-4 py-3 items-center"> <div class="flex justify-between gap-x-4 py-3 items-center">
<dt class="text-gray-500">水群排行榜</dt> <dt class="text-gray-500">水群排行榜</dt>

View File

@ -81,6 +81,13 @@
{{ end }} {{ end }}
</dd> </dd>
</div> </div>
<!-- 今日 AI 对话已使用次数 -->
<div class="flex justify-between gap-x-4 py-3 items-center">
<dt class="text-gray-500">今日 AI 对话已使用次数</dt>
<dd class="flex items-start gap-x-2">
{{ .info.AiUsedToday }}次(限制{{ .info.AiFreeLimit }}次)
</dd>
</div>
{{$isGroup := checkIsGroup .info.Wxid}} {{$isGroup := checkIsGroup .info.Wxid}}
{{ if eq $isGroup true }} {{ if eq $isGroup true }}

View File

@ -291,3 +291,29 @@ function changeClearMember(wxid, oldVal, newVal) {
window.location.reload(); window.location.reload();
}) })
} }
// 修改每日免费 AI 对话次数
function changeAiFreeLimit(wxid, limitNumber) {
limitNumber = Number(limitNumber)
if (limitNumber < 0) {
alert('值不能小于0')
}
// 请求接口
axios({
method: 'post',
url: '/api/ai/free',
data: {
wxid: wxid,
days: Number(limitNumber)
}
}).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) {
console.log(`错误信息: ${error}`);
alert("修改失败")
}).finally(function () {
window.location.reload();
})
}