hotfix #94
@ -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, "操作成功")
|
||||||
|
}
|
||||||
|
@ -25,6 +25,8 @@ system:
|
|||||||
news: true
|
news: true
|
||||||
# 热榜
|
# 热榜
|
||||||
hotTop: true
|
hotTop: true
|
||||||
|
# 每天免费 AI 次数限制
|
||||||
|
aiFreeLimit: 50
|
||||||
|
|
||||||
# 微信HOOK配置
|
# 微信HOOK配置
|
||||||
wechat:
|
wechat:
|
||||||
|
@ -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免费次数
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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) // 修改是否开启指令状态
|
||||||
|
@ -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
9
tasks/friends/ai.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package friends
|
||||||
|
|
||||||
|
import "go-wechat/service"
|
||||||
|
|
||||||
|
// ClearAiUsedToday
|
||||||
|
// @description: 清空AI日使用次数
|
||||||
|
func ClearAiUsedToday() {
|
||||||
|
service.ClearAiUsedToday()
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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 }}
|
||||||
|
@ -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();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user