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

Reviewed-on: #35
This commit is contained in:
李寻欢 2024-04-12 11:38:10 +08:00
commit 3478f4f9e2
19 changed files with 289 additions and 22 deletions

View File

@ -90,6 +90,29 @@ func ChangeEnableGroupRankStatus(ctx *gin.Context) {
ctx.String(http.StatusOK, "操作成功") ctx.String(http.StatusOK, "操作成功")
} }
// ChangeEnableSummaryStatus
// @description: 修改是否开启聊天记录总结
// @param ctx
func ChangeEnableSummaryStatus(ctx *gin.Context) {
var p changeStatusParam
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("`enable_summary`", gorm.Expr(" !`enable_summary`")).Error
if err != nil {
log.Printf("修改开启聊天记录总结失败:%s", err)
ctx.String(http.StatusInternalServerError, "操作失败: %s", err)
return
}
ctx.String(http.StatusOK, "操作成功")
}
// ChangeEnableWelcomeStatus // ChangeEnableWelcomeStatus
// @description: 修改是否开启迎新 // @description: 修改是否开启迎新
// @param ctx // @param ctx

View File

@ -3,7 +3,7 @@ wechat:
# 微信HOOK接口地址 # 微信HOOK接口地址
host: 10.0.0.73:19088 host: 10.0.0.73:19088
# 微信容器映射出来的vnc页面地址没有就不填 # 微信容器映射出来的vnc页面地址没有就不填
# vncUrl: http://192.168.1.175:19087/vnc_lite.html # vncUrl: http://192.168.1.175:19087/vnc_lite.html
# 是否在启动的时候自动设置hook服务的回调 # 是否在启动的时候自动设置hook服务的回调
autoSetCallback: false autoSetCallback: false
# 回调IP如果是Docker运行本参数必填(填auto表示自动不适用于 docker 环境)如果Docker修改了映射格式为 ip:port # 回调IP如果是Docker运行本参数必填(填auto表示自动不适用于 docker 环境)如果Docker修改了映射格式为 ip:port
@ -23,17 +23,20 @@ mysql:
schema: public # postgres 专用 schema: public # postgres 专用
task: task:
enable: false enable: true
syncFriends: syncFriends:
enable: false enable: false
cron: '*/5 * * * *' # 五分钟一次 cron: '*/5 * * * *' # 五分钟一次
groupSummary:
enable: true
cron: '30 0 * * *' # 每天0:30
waterGroup: waterGroup:
enable: true enable: false
cron: cron:
yesterday: '30 9 * * *' # 每天9:30 yesterday: '30 9 * * *' # 每天9:30
week: '30 9 * * 1' # 每周一9:30 week: '30 9 * * 1' # 每周一9:30
month: '30 9 1 * *' # 每月1号9:30 month: '30 9 1 * *' # 每月1号9:30
year: '0 9 1 1 *' # 每年1月1号9:30 year: '0 9 1 1 *' # 每年1月1号9:30
# MQ配置 # MQ配置
mq: mq:
@ -53,6 +56,8 @@ ai:
enable: false enable: false
# 模型不填默认gpt-3.5-turbo-0613 # 模型不填默认gpt-3.5-turbo-0613
model: gpt-3.5-turbo-0613 model: gpt-3.5-turbo-0613
# 群聊总结模型
summaryModel: gpt-4-0613
# OpenAI Api key # OpenAI Api key
apiKey: sk-xxxx apiKey: sk-xxxx
# 接口代理域名不填默认ChatGPT官方地址 # 接口代理域名不填默认ChatGPT官方地址

View File

@ -3,12 +3,13 @@ package config
// ai // ai
// @description: AI配置 // @description: AI配置
type ai struct { type ai struct {
Enable bool `json:"enable" yaml:"enable"` // 是否启用AI Enable bool `json:"enable" yaml:"enable"` // 是否启用AI
Model string `json:"model" yaml:"model"` // 模型 Model string `json:"model" yaml:"model"` // 模型
ApiKey string `json:"apiKey" yaml:"apiKey"` // API Key SummaryModel string `json:"summaryModel" yaml:"summaryModel"` // 总结模型
BaseUrl string `json:"baseUrl" yaml:"baseUrl"` // API地址 ApiKey string `json:"apiKey" yaml:"apiKey"` // API Key
Personality string `json:"personality" yaml:"personality"` // 人设 BaseUrl string `json:"baseUrl" yaml:"baseUrl"` // API地址
Models []aiModel `json:"models" yaml:"models"` // 模型列表 Personality string `json:"personality" yaml:"personality"` // 人设
Models []aiModel `json:"models" yaml:"models"` // 模型列表
} }
// aiModel // aiModel

View File

@ -3,9 +3,10 @@ package config
// task // task
// @description: 定时任务 // @description: 定时任务
type task struct { type task struct {
Enable bool `json:"enable" yaml:"enable"` // 是否启用 Enable bool `json:"enable" yaml:"enable"` // 是否启用
SyncFriends syncFriends `json:"syncFriends" yaml:"syncFriends"` // 同步好友 SyncFriends syncFriends `json:"syncFriends" yaml:"syncFriends"` // 同步好友
WaterGroup waterGroup `json:"waterGroup" yaml:"waterGroup"` // 水群排行榜 WaterGroup waterGroup `json:"waterGroup" yaml:"waterGroup"` // 水群排行榜
GroupSummary syncFriends `json:"groupSummary" yaml:"groupSummary"` // 群聊总结
} }
// syncFriends // syncFriends

View File

@ -17,6 +17,7 @@ type Friend struct {
AiModel string `json:"aiModel"` // AI模型 AiModel string `json:"aiModel"` // AI模型
EnableChatRank bool `json:"enableChatRank" gorm:"type:tinyint(1) default 0 not null"` // 是否使用聊天排行 EnableChatRank bool `json:"enableChatRank" gorm:"type:tinyint(1) default 0 not null"` // 是否使用聊天排行
EnableWelcome bool `json:"enableWelcome" gorm:"type:tinyint(1) default 0 not null"` // 是否启用迎新 EnableWelcome bool `json:"enableWelcome" gorm:"type:tinyint(1) default 0 not null"` // 是否启用迎新
EnableSummary bool `json:"enableSummary" gorm:"type:tinyint(1) default 0 not null"` // 是否启用总结
IsOk bool `json:"isOk" gorm:"type:tinyint(1) default 0 not null"` // 是否正常 IsOk bool `json:"isOk" gorm:"type:tinyint(1) default 0 not null"` // 是否正常
} }

View File

@ -42,6 +42,8 @@ func Command(m *plugin.MessageContext) {
command.LeiGodCmd(m.FromUser, msgArray[1], msgArray[2:]...) command.LeiGodCmd(m.FromUser, msgArray[1], msgArray[2:]...)
case "/肯德基", "/kfc": case "/肯德基", "/kfc":
command.KfcCrazyThursdayCmd(m.FromUser) command.KfcCrazyThursdayCmd(m.FromUser)
case "/ai":
command.AiCmd(m.FromUser, m.GroupUser, msgArray[1])
default: default:
utils.SendMessage(m.FromUser, m.GroupUser, "指令错误", 0) utils.SendMessage(m.FromUser, m.GroupUser, "指令错误", 0)
} }

View File

@ -0,0 +1,65 @@
package command
import (
"fmt"
"go-wechat/client"
"go-wechat/entity"
"go-wechat/utils"
"log"
"strings"
)
// AiCmd
// @description: AI指令
// @param userId
// @param groupUserId
// @param cmd
func AiCmd(userId, groupUserId, cmd string) {
// 判断发信人是不是群主
can := false
if strings.Contains(userId, "@chatroom") {
// 判断是不是群主
err := client.MySQL.Model(&entity.GroupUser{}).
Where("group_id = ?", userId).
Where("wxid = ?", groupUserId).
Pluck("is_admin", &can).Error
if err != nil {
log.Printf("查询群主失败: %v", err)
return
}
}
if !can {
utils.SendMessage(userId, groupUserId, "您不是群主,无法使用指令", 0)
return
}
var err error
replyMsg := "操作成功"
switch cmd {
case "enable", "启用", "打开":
err = setAiEnable(userId, true)
case "disable", "停用", "禁用", "关闭":
err = setAiEnable(userId, false)
default:
replyMsg = "指令错误"
}
if err != nil {
log.Printf("AI指令执行失败: %v", err)
replyMsg = fmt.Sprintf("指令执行错误: %v", err)
}
utils.SendMessage(userId, groupUserId, replyMsg, 0)
}
// setAiEnable
// @description: 设置AI启用状态
// @param userId
// @param enable
// @return err
func setAiEnable(userId string, enable bool) (err error) {
// 更新
err = client.MySQL.Model(&entity.Friend{}).
Where("wxid = ?", userId).
Update("enable_ai", enable).Error
return
}

View File

@ -23,6 +23,13 @@ option: 指令选项,可选值:
#2. 肯德基疯狂星期四文案 #2. 肯德基疯狂星期四文案
/kfc/肯德基 /kfc/肯德基
#3. AI助手
/ai option
option: 指令选项可选值:
启用: '启用'、'打开'、'enable'
停用: '停用'、'禁用'、'关闭'、'disable'
` `
utils.SendMessage(m.FromUser, m.GroupUser, str, 0) utils.SendMessage(m.FromUser, m.GroupUser, str, 0)

View File

@ -70,9 +70,6 @@ services:
image: mysql:8 image: mysql:8
container_name: gw-db container_name: gw-db
restart: unless-stopped restart: unless-stopped
depends_on:
wechat:
condition: service_healthy
environment: environment:
- MYSQL_ROOT_PASSWORD=wechat - MYSQL_ROOT_PASSWORD=wechat
- MYSQL_USER=wechat - MYSQL_USER=wechat
@ -88,6 +85,7 @@ services:
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- mysql - mysql
- wechat
volumes: volumes:
# 配置文件请参阅项目根目录的config.yaml文件 # 配置文件请参阅项目根目录的config.yaml文件
- ./config/config.yaml:/app/config.yaml - ./config/config.yaml:/app/config.yaml

View File

@ -28,4 +28,5 @@ func Init(g *gin.Engine) {
api.PUT("/grouprank/status", app.ChangeEnableGroupRankStatus) // 修改是否开启水群排行榜状态 api.PUT("/grouprank/status", app.ChangeEnableGroupRankStatus) // 修改是否开启水群排行榜状态
api.PUT("/grouprank/skip", app.ChangeSkipGroupRankStatus) // 修改是否跳过水群排行榜状态 api.PUT("/grouprank/skip", app.ChangeSkipGroupRankStatus) // 修改是否跳过水群排行榜状态
api.GET("/group/users", app.GetGroupUsers) // 获取群成员列表 api.GET("/group/users", app.GetGroupUsers) // 获取群成员列表
api.PUT("/summary/status", app.ChangeEnableSummaryStatus) // 修改是否开启群聊总结状态
} }

View File

@ -53,6 +53,15 @@ func GetAllEnableChatRank() (records []entity.Friend, err error) {
return return
} }
// GetAllEnableSummary
// @description: 取出所有启用了总结的群组
// @return records
// @return err
func GetAllEnableSummary() (records []entity.Friend, err error) {
err = client.MySQL.Where("enable_summary = ?", 1).Where("wxid LIKE '%@chatroom'").Find(&records).Error
return
}
// CheckIsEnableCommand // CheckIsEnableCommand
// @description: 检查用户是否启用了指令 // @description: 检查用户是否启用了指令
// @param userId // @param userId

View File

@ -3,6 +3,7 @@ package service
import ( import (
"go-wechat/client" "go-wechat/client"
"go-wechat/entity" "go-wechat/entity"
"go-wechat/vo"
"log" "log"
"os" "os"
"strconv" "strconv"
@ -39,3 +40,22 @@ func SaveMessage(msg entity.Message) {
go updateLastActive(msg) go updateLastActive(msg)
} }
} }
// GetTextMessagesById
// @description: 根据群id或者用户Id获取消息
// @param id
// @return records
// @return err
func GetTextMessagesById(id string) (records []vo.TextMessageItem, err error) {
tx := client.MySQL.
Table("`t_message` AS tm").
Joins("LEFT JOIN t_group_user AS tgu ON tm.group_user = tgu.wxid AND tgu.group_id = tm.from_user").
Select("tgu.nickname", "IF( tm.type = 49, EXTRACTVALUE ( tm.content, \"/msg/appmsg/title\" ), tm.content ) AS message").
Where("tm.`from_user` = ?", id).
Where(`(tm.type = 1 OR ( tm.type = 49 AND EXTRACTVALUE ( tm.content, "/msg/appmsg/type" ) = '57' ))`).
Where("DATE ( tm.create_at ) = DATE ( CURDATE() - INTERVAL 1 DAY )").
Order("tm.create_at ASC")
err = tx.Find(&records).Error
return
}

View File

@ -8,6 +8,7 @@ import (
"go-wechat/config" "go-wechat/config"
"go-wechat/entity" "go-wechat/entity"
"go-wechat/model" "go-wechat/model"
"go-wechat/utils"
"gorm.io/gorm" "gorm.io/gorm"
"log" "log"
"slices" "slices"
@ -72,6 +73,8 @@ func Sync() {
log.Printf("新增好友失败: %s", err.Error()) log.Printf("新增好友失败: %s", err.Error())
continue continue
} }
// 发送一条新消息
utils.SendMessage(friend.Wxid, "", "大家好我是一个AI机器人可以直接@我询问你想问的问题。该功能默认未启用,请群主艾特我并发送 /ai enable 指令启用", 0)
} else { } else {
pm := map[string]any{ pm := map[string]any{
"nickname": friend.Nickname, "nickname": friend.Nickname,

81
tasks/summary/summary.go Normal file
View File

@ -0,0 +1,81 @@
package summary
import (
"context"
"fmt"
"github.com/sashabaranov/go-openai"
"go-wechat/config"
"go-wechat/service"
"go-wechat/utils"
"go-wechat/vo"
"log"
"strings"
)
// AiSummary
// @description: AI总结群聊记录
func AiSummary() {
groups, err := service.GetAllEnableSummary()
if err != nil {
log.Printf("获取启用了聊天排行榜的群组失败, 错误信息: %v", err)
return
}
for _, group := range groups {
// 获取对话记录
var records []vo.TextMessageItem
if records, err = service.GetTextMessagesById(group.Wxid); err != nil {
log.Printf("获取群[%s]对话记录失败, 错误信息: %v", group.Wxid, err)
continue
}
//if len(records) < 100 {
// log.Printf("群[%s]对话记录不足100条跳过总结", group.Wxid)
// continue
//}
// 组装对话记录为字符串
var content []string
for _, record := range records {
content = append(content, fmt.Sprintf("%s: %s\n-----end-----", record.Nickname, record.Message))
}
msg := fmt.Sprintf("请帮我总结一下一下的群聊内容的梗概(内容尽可能详细一些)。\n"+
"注意,他们可能是多个话题,请仔细甄别。\n"+
"每一行代表一个人的发言,每一行的的格式为: \n{nickname}: {content}\n-----end-----"+
"\n\n聊天记录如下: \n%s", strings.Join(content, "\n"))
// AI总结
messages := []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: msg,
},
}
// 默认使用AI回复
conf := openai.DefaultConfig(config.Conf.Ai.ApiKey)
if config.Conf.Ai.BaseUrl != "" {
conf.BaseURL = fmt.Sprintf("%s/v1", config.Conf.Ai.BaseUrl)
}
ai := openai.NewClientWithConfig(conf)
var resp openai.ChatCompletionResponse
resp, err = ai.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: config.Conf.Ai.SummaryModel,
Messages: messages,
},
)
if err != nil {
log.Printf("群聊记录总结失败: %v", err.Error())
continue
}
// 返回消息为空
if resp.Choices[0].Message.Content == "" {
continue
}
replyMsg := fmt.Sprintf("#昨日消息总结\n\n%s", resp.Choices[0].Message.Content)
utils.SendMessage(group.Wxid, "", replyMsg, 0)
}
}

View File

@ -4,6 +4,7 @@ import (
"github.com/go-co-op/gocron" "github.com/go-co-op/gocron"
"go-wechat/config" "go-wechat/config"
"go-wechat/tasks/friends" "go-wechat/tasks/friends"
"go-wechat/tasks/summary"
"go-wechat/tasks/watergroup" "go-wechat/tasks/watergroup"
"log" "log"
"time" "time"
@ -37,6 +38,13 @@ func InitTasks() {
} }
} }
// 群聊总结
if config.Conf.Task.GroupSummary.Enable {
log.Printf("群聊总结任务已启用,执行表达式: %s", config.Conf.Task.GroupSummary.Cron)
_, _ = s.Cron(config.Conf.Task.GroupSummary.Cron).Do(summary.AiSummary)
}
// 更新好友列表 // 更新好友列表
if config.Conf.Task.SyncFriends.Enable { if config.Conf.Task.SyncFriends.Enable {
log.Printf("更新好友列表任务已启用,执行表达式: %s", config.Conf.Task.SyncFriends.Cron) log.Printf("更新好友列表任务已启用,执行表达式: %s", config.Conf.Task.SyncFriends.Cron)

View File

@ -108,6 +108,7 @@
<th>是否在通讯录</th> <th>是否在通讯录</th>
<th>是否启用AI</th> <th>是否启用AI</th>
<th>是否启用水群排行榜</th> <th>是否启用水群排行榜</th>
<th>是否启用聊天记录总结</th>
<th>是否启用迎新</th> <th>是否启用迎新</th>
<th>是否启用指令</th> <th>是否启用指令</th>
<th>操作</th> <th>操作</th>
@ -164,6 +165,15 @@
<div class="swap-off">❌已禁用</div> <div class="swap-off">❌已禁用</div>
</label> </label>
</td> </td>
<td>
<!-- EnableSummary -->
<label class="swap swap-flip {{ checkSwap .EnableSummary }}">
<input type="checkbox" onclick="changeSummaryEnableStatus({{.Wxid}})"/>
<div class="swap-on">✔️已启用</div>
<div class="swap-off">❌已禁用</div>
</label>
</td>
<td> <td>
<label class="swap swap-flip {{ checkSwap .EnableWelcome }}"> <label class="swap swap-flip {{ checkSwap .EnableWelcome }}">
<input type="checkbox" onclick="changeWelcomeEnableStatus({{.Wxid}})"/> <input type="checkbox" onclick="changeWelcomeEnableStatus({{.Wxid}})"/>

View File

@ -12,6 +12,7 @@ function changeAiEnableStatus(wxId) {
} }
}).then(function (response) { }).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`); console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) { }).catch(function (error) {
console.log(`错误信息: ${error}`); console.log(`错误信息: ${error}`);
alert("修改失败") alert("修改失败")
@ -29,6 +30,25 @@ function changeGroupRankEnableStatus(wxId) {
} }
}).then(function (response) { }).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`); console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) {
console.log(`错误信息: ${error}`);
alert("修改失败")
})
}
// 修改水群排行榜状态
function changeSummaryEnableStatus(wxId) {
// console.log("修改聊天记录总结开启状态: ", wxId)
axios({
method: 'put',
url: '/api/summary/status',
data: {
wxId: wxId
}
}).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) { }).catch(function (error) {
console.log(`错误信息: ${error}`); console.log(`错误信息: ${error}`);
alert("修改失败") alert("修改失败")
@ -45,6 +65,7 @@ function changeWelcomeEnableStatus(wxId) {
} }
}).then(function (response) { }).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`); console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) { }).catch(function (error) {
console.log(`错误信息: ${error}`); console.log(`错误信息: ${error}`);
alert("修改失败") alert("修改失败")
@ -61,6 +82,7 @@ function changeCommandEnableStatus(wxId) {
} }
}).then(function (response) { }).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`); console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) { }).catch(function (error) {
console.log(`错误信息: ${error}`); console.log(`错误信息: ${error}`);
alert("修改失败") alert("修改失败")
@ -79,6 +101,7 @@ function changeUserGroupRankSkipStatus(groupId, userId) {
} }
}).then(function (response) { }).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`); console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) { }).catch(function (error) {
console.log(`错误信息: ${error}`); console.log(`错误信息: ${error}`);
alert("修改失败") alert("修改失败")

View File

@ -18,6 +18,7 @@ type FriendItem struct {
EnableChatRank bool // 是否使用聊天排行 EnableChatRank bool // 是否使用聊天排行
EnableWelcome bool // 是否使用迎新 EnableWelcome bool // 是否使用迎新
EnableCommand bool // 是否启用指令 EnableCommand bool // 是否启用指令
EnableSummary bool // 是否启用总结
IsOk bool // 是否还在通讯库(群聊是要还在群里也算) IsOk bool // 是否还在通讯库(群聊是要还在群里也算)
} }

8
vo/message.go Normal file
View File

@ -0,0 +1,8 @@
package vo
// TextMessageItem
// @description: 文字消息
type TextMessageItem struct {
Nickname string `json:"nickname"`
Message string `json:"message"`
}