🆕 新增手动发送AI群聊消息总结

This commit is contained in:
李寻欢 2024-08-21 10:11:29 +08:00
parent 0251291e4b
commit 06f97f26a0
9 changed files with 232 additions and 54 deletions

56
app/other.go Normal file
View File

@ -0,0 +1,56 @@
package app
import (
"fmt"
"github.com/gin-gonic/gin"
"go-wechat/model/vo"
"go-wechat/service"
"go-wechat/utils"
"log"
"net/http"
)
// SendAiSummary
// @description: 发送AI摘要
// @param ctx
func SendAiSummary(ctx *gin.Context) {
// 获取群Id
groupId := ctx.Query("id")
if groupId == "" {
ctx.String(http.StatusForbidden, "群Id不能为空")
return
}
// 取出群名称
groupInfo, err := service.GetFriendInfoById(groupId)
if err != nil {
ctx.String(http.StatusInternalServerError, "获取群信息失败")
return
}
// 获取对话记录
var records []vo.TextMessageItem
if records, err = service.GetTextMessagesById(groupId); err != nil {
log.Printf("获取群[%s]对话记录失败, 错误信息: %v", groupId, err)
ctx.String(http.StatusInternalServerError, "获取群对话记录失败")
return
}
if len(records) < 10 {
ctx.String(http.StatusForbidden, "群对话记录不足10条建议自己看")
return
}
// 组装对话记录为字符串
var replyMsg string
replyMsg, err = utils.GetAiSummary(groupInfo.Nickname, records)
if err != nil {
log.Printf("群聊记录总结失败: %v", err.Error())
ctx.String(http.StatusInternalServerError, "群聊消息总结失败,错误信息: "+err.Error())
return
}
replyMsg = fmt.Sprintf("#昨日消息总结\n又是一天过去了让我们一起来看看昨儿群友们都聊了什么有趣的话题吧~\n\n%s", replyMsg)
log.Printf("群[%s]对话记录总结成功,总结内容: %s", groupInfo.Nickname, replyMsg)
_ = utils.SendMessage(groupId, "", replyMsg, 0)
ctx.String(http.StatusOK, "操作完成")
}

View File

@ -12,6 +12,8 @@ import (
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"os"
"strconv"
"strings" "strings"
"time" "time"
) )
@ -22,6 +24,13 @@ func init() {
initialization.Plugin() // 注册插件 initialization.Plugin() // 注册插件
tasks.InitTasks() // 初始化定时任务 tasks.InitTasks() // 初始化定时任务
mq.Init() // 初始化MQ mq.Init() // 初始化MQ
if flag, _ := strconv.ParseBool(os.Getenv("DONT_SEND")); flag {
log.Printf("已设置环境变量DONT_SEND不发送消息")
}
if flag, _ := strconv.ParseBool(os.Getenv("DONT_SAVE")); flag {
log.Printf("已设置环境变量DONT_SAVE不保存消息")
}
} }
func main() { func main() {

View File

@ -28,6 +28,7 @@ func Init(g *gin.Engine) {
api.POST("/ai/model", app.ChangeUseAiModel) // 修改使用的AI模型 api.POST("/ai/model", app.ChangeUseAiModel) // 修改使用的AI模型
api.POST("/ai/free", app.ChangeAiFreeLimit) // 修改AI免费次数 api.POST("/ai/free", app.ChangeAiFreeLimit) // 修改AI免费次数
api.POST("/ai/assistant", app.ChangeUseAiAssistant) // 修改使用的AI助手 api.POST("/ai/assistant", app.ChangeUseAiAssistant) // 修改使用的AI助手
api.POST("/ai/summary", app.SendAiSummary) // 手动发送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) // 修改是否开启指令状态
api.PUT("/news/status", app.ChangeEnableNewsStatus) // 修改是否开启早报状态 api.PUT("/news/status", app.ChangeEnableNewsStatus) // 修改是否开启早报状态

View File

@ -1,15 +1,11 @@
package summary package summary
import ( import (
"context"
"fmt" "fmt"
"github.com/sashabaranov/go-openai"
"go-wechat/config"
"go-wechat/model/vo" "go-wechat/model/vo"
"go-wechat/service" "go-wechat/service"
"go-wechat/utils" "go-wechat/utils"
"log" "log"
"strings"
"time" "time"
) )
@ -37,63 +33,21 @@ func AiSummary() {
continue continue
} }
// 组装对话记录为字符串 // 组装对话记录为字符串
var content []string var replyMsg string
for _, record := range records { replyMsg, err = utils.GetAiSummary(group.Nickname, records)
content = append(content, fmt.Sprintf(`{"%s": "%s"}--end--`, record.Nickname, strings.ReplaceAll(record.Message, "\n", "。。")))
}
msgTmp := `请帮我总结一下一下的群聊内容的梗概生成的梗概需要尽可能详细需要带上一些聊天关键信息并且带上群友名字
注意他们可能是多个话题请仔细甄别
每一行代表一个人的发言每一行的的格式为 {"{nickname}": "{content}"}--end--
群名称: %s
聊天记录如下:
%s
`
msg := fmt.Sprintf(msgTmp, group.Nickname, 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 { if err != nil {
log.Printf("群聊记录总结失败: %v", err.Error()) log.Printf("群聊记录总结失败: %v", err.Error())
utils.SendMessage(group.Wxid, "", "#昨日消息总结\n\n群聊消息总结失败错误信息: "+err.Error(), 0) _ = utils.SendMessage(group.Wxid, "", "#昨日消息总结\n\n群聊消息总结失败错误信息: "+err.Error(), 0)
continue continue
} }
// 返回消息为空 replyMsg = fmt.Sprintf("#昨日消息总结\n又是一天过去了让我们一起来看看昨儿群友们都聊了什么有趣的话题吧~\n\n%s", replyMsg)
if resp.Choices[0].Message.Content == "" {
utils.SendMessage(group.Wxid, "", "#昨日消息总结\n\n群聊消息总结失败AI返回结果为空", 0)
continue
}
replyMsg := fmt.Sprintf("#昨日消息总结\n又是一天过去了让我们一起来看看昨儿群友们都聊了什么有趣的话题吧~\n\n%s", resp.Choices[0].Message.Content)
//log.Printf("群[%s]对话记录总结成功,总结内容: %s", group.Wxid, replyMsg) //log.Printf("群[%s]对话记录总结成功,总结内容: %s", group.Wxid, replyMsg)
utils.SendMessage(group.Wxid, "", replyMsg, 0) _ = utils.SendMessage(group.Wxid, "", replyMsg, 0)
// 判断耗时是否达到15秒,不足就等待 // 判断耗时是否达到一分钟,不足就等待
if used := time.Now().Sub(start); used < 15*time.Second { if used := time.Now().Sub(start); used < time.Minute {
time.Sleep(15*time.Second - used) time.Sleep(time.Minute - used)
} }
} }
} }

View File

@ -8,6 +8,8 @@ import (
"go-wechat/common/current" "go-wechat/common/current"
"go-wechat/config" "go-wechat/config"
"log" "log"
"os"
"strconv"
"time" "time"
) )
@ -17,6 +19,10 @@ import (
// @param atId // @param atId
// @param msg // @param msg
func SendMessage(toId, atId, msg string, retryCount int) (err error) { func SendMessage(toId, atId, msg string, retryCount int) (err error) {
if flag, _ := strconv.ParseBool(os.Getenv("DONT_SEND")); flag {
return
}
if retryCount > 5 { if retryCount > 5 {
log.Printf("重试五次失败,停止发送") log.Printf("重试五次失败,停止发送")
err = errors.New("重试五次失败,停止发送") err = errors.New("重试五次失败,停止发送")
@ -62,6 +68,10 @@ func SendMessage(toId, atId, msg string, retryCount int) (err error) {
// @param imgPath string 图片路径 // @param imgPath string 图片路径
// @param retryCount int 重试次数 // @param retryCount int 重试次数
func SendImage(toId, imgPath string, retryCount int) { func SendImage(toId, imgPath string, retryCount int) {
if flag, _ := strconv.ParseBool(os.Getenv("DONT_SEND")); flag {
return
}
if retryCount > 5 { if retryCount > 5 {
log.Printf("重试五次失败,停止发送") log.Printf("重试五次失败,停止发送")
return return
@ -94,6 +104,10 @@ func SendImage(toId, imgPath string, retryCount int) {
// @param emotionHash string 表情包hash(md5值) // @param emotionHash string 表情包hash(md5值)
// @param retryCount int 重试次数 // @param retryCount int 重试次数
func SendEmotion(toId, emotionHash string, retryCount int) { func SendEmotion(toId, emotionHash string, retryCount int) {
if flag, _ := strconv.ParseBool(os.Getenv("DONT_SEND")); flag {
return
}
if retryCount > 5 { if retryCount > 5 {
log.Printf("重试五次失败,停止发送") log.Printf("重试五次失败,停止发送")
return return
@ -200,6 +214,10 @@ func QuitChatroom(chatRoomId string, retryCount int) {
// @param url string 链接 // @param url string 链接
// @param retryCount int 重试次数 // @param retryCount int 重试次数
func SendPublicMsg(wxId, title, digest, url string, retryCount int) { func SendPublicMsg(wxId, title, digest, url string, retryCount int) {
if flag, _ := strconv.ParseBool(os.Getenv("DONT_SEND")); flag {
return
}
if retryCount > 5 { if retryCount > 5 {
log.Printf("重试五次失败,停止发送") log.Printf("重试五次失败,停止发送")
return return

108
utils/summary.go Normal file
View File

@ -0,0 +1,108 @@
package utils
import (
"context"
"errors"
"fmt"
"github.com/sashabaranov/go-openai"
"go-wechat/config"
"go-wechat/model/vo"
"log"
"strings"
)
// GetAiSummary
// @description: AI总结群聊记录
// @param groupName
// @param records
// @return replyMsg
// @return err
func GetAiSummary(groupName string, records []vo.TextMessageItem) (replyMsg string, err error) {
// 组装对话记录为字符串
var content []string
for _, record := range records {
content = append(content, fmt.Sprintf(`{"%s": "%s"}--end--`, record.Nickname, strings.ReplaceAll(record.Message, "\n", "。。")))
}
prompt := `# Role: 数据分析师和信息整理专家
# Background: 用户需要从群聊记录中提取有价值的信息包括技术话题讨论工具分享网站或资源链接
# Profile: 你是一位专业的数据分析师擅长从大量文本中提取关键信息并能够高效地整理和总结内容
# Skills: 数据分析信息提取内容总结网络资源评估
# Goals: 设计一个流程帮助用户识别群聊中的技术话题共享的工具和资源并高效地总结每个话题的要点
# Constrains: 该流程需要简洁明了易于用户理解和操作同时确保不遗漏任何重要信息
# OutputFormat: 结构化的文本总结包含关键讨论点结论等
# Workflow:
## 1. 快速浏览群聊记录
- 通过快速浏览聊天记录初步识别出主要话题
## 2. 识别和分类主要话题
- 识别群聊中的主要话题分类讨论内容
- 每个话题独立提取避免混淆
- 话题识别尽可能准确确保不遗漏重要信息
## 3. 编写总结概括
- 对每个话题进行总结包含以下内容
- 关键讨论点主要的讨论内容和观点
- 参与成员参与讨论的成员的昵称
- 结论得出的结论或共识内容要尽可能详细
## 4. 组织信息
- 以逻辑清晰易于理解的方式组织信息
- 使用结构化文本使总结内容易于阅读
## 5. 生成最终总结
- 根据上述步骤生成每个话题的详细总结
- 确保每个总结完整且有条理
- 返回数据格式为结构化文本方便用户查阅
## 6. 群聊记录消息格式
格式: {"{nickname}": "{content}"}--end--
# Examples:
- 话题技术讨论 - AI在医疗领域的应用
- 关键讨论点讨论了AI在医疗诊断中的应用前景和现有挑战
- 成员张三李四王五
- 结论AI技术有望提高医疗诊断的准确性和效率但数据隐私和模型解释性仍需解决
- 话题美食讨论 - 杭州到底有没有美食
- 关键讨论点讨论了杭州的美食有哪些分布地区评价如何
- 成员张三李四王五
- 结论杭州好像没有美食
`
msg := fmt.Sprintf("群名称: %s\n聊天记录如下:\n%s", groupName, strings.Join(content, "\n"))
// AI总结
messages := []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: prompt,
},
{
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())
return
}
// 返回消息为空
if resp.Choices[0].Message.Content == "" {
err = errors.New("AI返回结果为空")
return
}
replyMsg = resp.Choices[0].Message.Content
return
}

View File

@ -57,6 +57,9 @@
<!-- 消息总结 --> <!-- 消息总结 -->
{{define "summary"}} {{define "summary"}}
{{ if eq .EnableSummary true }}
<button type="button" class="btn-link float-end text-blue-600" onclick="sendAiSummary({{.Wxid}})">手动发送总结</button>
{{ end }}
<button type="button" <button type="button"
class="{{ if eq .EnableSummary true }}bg-green-600{{ else }}bg-gray-200{{ end }} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-offset-2" class="{{ if eq .EnableSummary true }}bg-green-600{{ else }}bg-gray-200{{ end }} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-offset-2"
role="switch" aria-checked="false" onclick="changeSummaryEnableStatus({{.Wxid}})"> role="switch" aria-checked="false" onclick="changeSummaryEnableStatus({{.Wxid}})">

View File

@ -56,6 +56,7 @@
</div> </div>
{{ if eq .EnableAi true }} {{ if eq .EnableAi true }}
<div class="float-end"> <div class="float-end">
<!-- 模型 -->
<div> <div>
<label> <label>
<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" onchange="aiModelChange(event, {{.Wxid}})"> <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" onchange="aiModelChange(event, {{.Wxid}})">
@ -68,6 +69,7 @@
</select> </select>
</label> </label>
</div> </div>
<!-- AI角色 -->
<div class="float-end mt-1"> <div class="float-end mt-1">
<label> <label>
<select <select

View File

@ -317,3 +317,30 @@ function changeAiFreeLimit(wxid, limitNumber) {
window.location.reload(); window.location.reload();
}) })
} }
// 发送AI群聊总结
function sendAiSummary(wxid) {
if (wxid === '') {
alert("wxid不能为空")
return
}
if (!confirm("确定发送AI群聊总结吗?")) {
return
}
// 请求接口
axios({
method: 'post',
url: '/api/ai/summary',
params: {
id: wxid,
}
}).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) {
console.log(`错误信息: ${error}`);
alert(`${response.data}`)
}).finally(function () {
window.location.reload();
})
}