Compare commits

...

26 Commits

Author SHA1 Message Date
李寻欢
45109b02ef Merge pull request '🎨 优化消息总结查询SQL,支持更多消息类型' (#43) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/43
2024-04-29 10:13:34 +08:00
李寻欢
e8523c1e3e 🎨 优化消息总结查询SQL,支持更多消息类型 2024-04-29 10:12:50 +08:00
李寻欢
765e0ead7f Merge pull request 'hotfix' (#42) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/42
2024-04-24 17:24:26 +08:00
李寻欢
e7117d227b 🎨 好友列表页显示效果优化 2024-04-24 17:23:59 +08:00
李寻欢
448ac83514 🎨 首页显示效果优化 2024-04-24 15:49:17 +08:00
李寻欢
5024fb4b05 Merge pull request '🔥 重构管理页面,长得更好看一些了' (#41) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/41
2024-04-23 10:45:38 +08:00
李寻欢
1fca021b38 🔥 重构管理页面,长得更好看一些了 2024-04-23 10:42:08 +08:00
李寻欢
70fc7add78 Merge pull request '🎨 优化AI对话角色提示词' (#40) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/40
2024-04-19 09:55:27 +08:00
李寻欢
ab5a8092f7 🎨 优化AI对话角色提示词 2024-04-19 09:54:50 +08:00
李寻欢
21a6f75ed3 Merge pull request '🎨 优化AI对话角色设置,支持为群或者好友单独设置角色(需手动修改数据库)' (#39) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/39
2024-04-16 17:19:31 +08:00
李寻欢
daa36f308b 🎨 优化AI对话角色设置,支持为群或者好友单独设置角色(需手动修改数据库) 2024-04-16 17:19:08 +08:00
李寻欢
e78f6a6d6e Merge pull request '🎨 优化群聊总结返回消息内容' (#38) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/38
2024-04-15 16:22:43 +08:00
李寻欢
bb92c67069 🎨 优化群聊总结返回消息内容 2024-04-15 16:22:17 +08:00
李寻欢
542a2d5ef6 Merge pull request '🎨 优化消息总结提示词' (#37) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/37
2024-04-15 15:10:32 +08:00
李寻欢
c13771be22 🎨 优化消息总结提示词 2024-04-15 15:09:40 +08:00
李寻欢
982ddef0da Merge pull request '🎨 总结群聊时,不足100条记录的不总结' (#36) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/36
2024-04-12 11:40:51 +08:00
李寻欢
99447d332d 🎨 总结群聊时,不足100条记录的不总结 2024-04-12 11:40:40 +08:00
李寻欢
3478f4f9e2 Merge pull request 'hotfix' (#35) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/35
2024-04-12 11:38:10 +08:00
李寻欢
b3ed0fcc6f 🆕 新增群聊对话记录总结 2024-04-12 11:37:21 +08:00
李寻欢
3da8b327d0 chore(go-wxhelper): add AI command functionality 🤖
Add AI command functionality to handle AI commands in the WeChat plugin. Includes options to enable or disable AI features.
2024-04-12 10:48:46 +08:00
李寻欢
0262fdd9f7 Merge pull request 'hotfix' (#34) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/34
2024-04-09 14:43:20 +08:00
李寻欢
f775f1d67d Merge remote-tracking branch 'origin/hotfix' into hotfix 2024-04-09 14:42:59 +08:00
李寻欢
43f9d590b7 🎨 脚本优化 2024-04-09 14:42:47 +08:00
李寻欢
22c7d3683b Merge pull request 'hotfix' (#33) from hotfix into main
Reviewed-on: https://gitee.ltd/lxh/go-wxhelper/pulls/33
2024-03-22 23:07:45 +08:00
李寻欢
d589a9615e Merge remote-tracking branch 'origin/hotfix' into hotfix 2024-03-22 23:07:00 +08:00
李寻欢
13205e445f 🎨 新增 MQ 开关 2024-03-22 23:06:54 +08:00
39 changed files with 1476 additions and 293 deletions

21
.editorconfig Normal file
View File

@@ -0,0 +1,21 @@
root = true
[*]
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
indent_style = tab
indent_size = 4
[*.md]
indent_size = 4
trim_trailing_whitespace = false
eclint_indent_style = unset
[Dockerfile]
indent_size = 4

View File

@@ -90,6 +90,29 @@ func ChangeEnableGroupRankStatus(ctx *gin.Context) {
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
// @description: 修改是否开启迎新
// @param ctx

View File

@@ -1,29 +0,0 @@
package app
import (
"fmt"
"github.com/gin-gonic/gin"
"go-wechat/config"
"go-wechat/service"
"net/http"
)
// Index
// @description: 首页
// @param ctx
func Index(ctx *gin.Context) {
var result = gin.H{
"msg": "success",
}
// 取出所有好友列表
friends, groups, err := service.GetAllFriend()
if err != nil {
result["msg"] = fmt.Sprintf("数据获取失败: %s", err.Error())
}
result["friends"] = friends
result["groups"] = groups
result["vnc"] = config.Conf.Wechat.VncUrl
result["aiModels"] = config.Conf.Ai.Models
// 渲染页面
ctx.HTML(http.StatusOK, "index.html", result)
}

99
app/pages.go Normal file
View File

@@ -0,0 +1,99 @@
package app
import (
"fmt"
"github.com/gin-gonic/gin"
"go-wechat/config"
"go-wechat/service"
"net/http"
)
// Index
// @description: 首页
// @param ctx
func Index(ctx *gin.Context) {
var result = gin.H{
"msg": "success",
}
// 取出所有好友列表
friends, groups, err := service.GetAllFriend()
if err != nil {
result["msg"] = fmt.Sprintf("数据获取失败: %s", err.Error())
}
var in, notIn int
for _, d := range friends {
if d.IsOk {
in++
} else {
notIn++
}
}
result["friendCount"] = in
result["friendWithoutCount"] = notIn
var gin, gnotIn int
for _, d := range groups {
if d.IsOk {
gin++
} else {
gnotIn++
}
}
result["groupCount"] = gin
result["groupWithoutCount"] = gnotIn
result["vnc"] = config.Conf.Wechat.VncUrl
result["isVnc"] = config.Conf.Wechat.VncUrl != ""
result["aiModels"] = config.Conf.Ai.Models
// 渲染页面
ctx.HTML(http.StatusOK, "index.html", result)
}
// Friend
// @description: 好友列表
// @param ctx
func Friend(ctx *gin.Context) {
var result = gin.H{
"msg": "success",
}
// 取出所有好友列表
friends, _, err := service.GetAllFriend()
if err != nil {
result["msg"] = fmt.Sprintf("数据获取失败: %s", err.Error())
}
result["friends"] = friends
result["vnc"] = config.Conf.Wechat.VncUrl
result["aiModels"] = config.Conf.Ai.Models
// 渲染页面
ctx.HTML(http.StatusOK, "friend.html", result)
}
// Group
// @description: 群组列表
// @param ctx
func Group(ctx *gin.Context) {
var result = gin.H{
"msg": "success",
}
// 取出所有好友列表
_, groups, err := service.GetAllFriend()
if err != nil {
result["msg"] = fmt.Sprintf("数据获取失败: %s", err.Error())
}
result["groups"] = groups
result["vnc"] = config.Conf.Wechat.VncUrl
result["aiModels"] = config.Conf.Ai.Models
// 渲染页面
ctx.HTML(http.StatusOK, "group.html", result)
}
// PageNotFound
// @description: 404页面
// @param ctx
func PageNotFound(ctx *gin.Context) {
// 渲染页面
ctx.HTML(http.StatusOK, "404.html", nil)
}

View File

@@ -27,8 +27,11 @@ task:
syncFriends:
enable: false
cron: '*/5 * * * *' # 五分钟一次
waterGroup:
groupSummary:
enable: true
cron: '30 0 * * *' # 每天0:30
waterGroup:
enable: false
cron:
yesterday: '30 9 * * *' # 每天9:30
week: '30 9 * * 1' # 每周一9:30
@@ -37,6 +40,8 @@ task:
# MQ配置
mq:
# 是否启用
enable: false
# RabbitMQ配置
rabbitmq:
host: 10.0.0.247
@@ -51,6 +56,8 @@ ai:
enable: false
# 模型不填默认gpt-3.5-turbo-0613
model: gpt-3.5-turbo-0613
# 群聊总结模型
summaryModel: gpt-4-0613
# OpenAI Api key
apiKey: sk-xxxx
# 接口代理域名不填默认ChatGPT官方地址
@@ -59,17 +66,19 @@ ai:
personality: 你的名字叫张三,你是一个百科机器人,你的爱好是看电影,你的性格是开朗的,你的专长是讲故事,你的梦想是当一名童话故事作家。你对政治没有一点儿兴趣,也不会讨论任何与政治相关的话题,你甚至可以拒绝回答这一类话题。
models:
- name: ChatGPT-4
model: gpt-4-0613
- name: 讯飞星火v3
model: SparkDesk3
- name: 讯飞星火随机
model: SparkDesk
model: gpt-4
- name: 讯飞星火v3.1
model: SparkDesk-v3.1
- name: 讯飞星火v3.5
model: SparkDesk-v3.5
- name: 月之暗面-8k
model: moonshot-v1-8k
- name: 月之暗面-32k
model: moonshot-v1-32k
- name: 月之暗面-128k
model: moonshot-v1-128k
- name: 跃问
model: StepChat
# 资源配置
# map[k]v结构k 会变成全小写,所以这儿不能用大写字母

View File

@@ -5,6 +5,7 @@ package config
type ai struct {
Enable bool `json:"enable" yaml:"enable"` // 是否启用AI
Model string `json:"model" yaml:"model"` // 模型
SummaryModel string `json:"summaryModel" yaml:"summaryModel"` // 总结模型
ApiKey string `json:"apiKey" yaml:"apiKey"` // API Key
BaseUrl string `json:"baseUrl" yaml:"baseUrl"` // API地址
Personality string `json:"personality" yaml:"personality"` // 人设

View File

@@ -5,6 +5,7 @@ import "fmt"
// mq
// @description: MQ配置
type mq struct {
Enable bool `json:"enable" yaml:"enable"` // 是否启用
RabbitMQ rabbitMq `json:"rabbitmq" yaml:"rabbitmq"` // RabbitMQ配置
}

View File

@@ -6,6 +6,7 @@ type task struct {
Enable bool `json:"enable" yaml:"enable"` // 是否启用
SyncFriends syncFriends `json:"syncFriends" yaml:"syncFriends"` // 同步好友
WaterGroup waterGroup `json:"waterGroup" yaml:"waterGroup"` // 水群排行榜
GroupSummary syncFriends `json:"groupSummary" yaml:"groupSummary"` // 群聊总结
}
// syncFriends

View File

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

View File

@@ -15,8 +15,10 @@ type Friend struct {
LastActive time.Time `json:"lastActive"` // 最后活跃时间
EnableAi bool `json:"enableAI" gorm:"type:tinyint(1) default 0 not null"` // 是否使用AI
AiModel string `json:"aiModel"` // AI模型
Prompt string `json:"prompt"` // 提示词
EnableChatRank bool `json:"enableChatRank" 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"` // 是否正常
}

22
main.go
View File

@@ -40,11 +40,23 @@ func main() {
// 自定义模板引擎函数
app.SetFuncMap(template.FuncMap{
"checkSwap": func(flag bool) string {
if flag {
return "swap-active"
"codeToChinese": func(code string) string {
switch code {
case "friend":
return "好友列表"
case "group":
return "群组列表"
case "index":
return "首页"
default:
return "其他页面"
}
return ""
},
"boolToChinese": func(flag bool) string {
if flag {
return "是"
}
return "否"
},
})
@@ -58,7 +70,7 @@ func main() {
return
}
// 404直接跳转到首页
ctx.Redirect(302, "/index.html")
ctx.Redirect(302, "/404.html")
})
app.NoMethod(func(ctx *gin.Context) {
ctx.String(http.StatusMethodNotAllowed, "不支持的请求方式")

View File

@@ -16,6 +16,10 @@ const exchangeName = "wechat-message"
// Init
// @description: 初始化MQ
func Init() {
if !config.Conf.Mq.Enable {
log.Println("未启用MQ")
return
}
// 读取MQ连接配置
mqUrl := config.Conf.Mq.RabbitMQ.GetURL()
if mqUrl == "" {

View File

@@ -46,13 +46,19 @@ func AI(m *plugin.MessageContext) {
m.Content = strings.Replace(m.Content, matches[0], "", 1)
}
// 处理预设角色,默认是配置文件里的,如果数据库配置不为空,则使用数据库配置
prompt := config.Conf.Ai.Personality
if friendInfo.Prompt != "" {
prompt = friendInfo.Prompt
}
// 组装消息体
messages := make([]openai.ChatCompletionMessage, 0)
if config.Conf.Ai.Personality != "" {
// 填充人设
messages = append(messages, openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleSystem,
Content: config.Conf.Ai.Personality,
Content: prompt,
})
}

View File

@@ -42,6 +42,8 @@ func Command(m *plugin.MessageContext) {
command.LeiGodCmd(m.FromUser, msgArray[1], msgArray[2:]...)
case "/肯德基", "/kfc":
command.KfcCrazyThursdayCmd(m.FromUser)
case "/ai":
command.AiCmd(m.FromUser, m.GroupUser, msgArray[1])
default:
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. 肯德基疯狂星期四文案
/kfc、/肯德基
#3. AI助手
/ai option
option: 指令选项,可选值:
启用: '启用'、'打开'、'enable'
停用: '停用'、'禁用'、'关闭'、'disable'
`
utils.SendMessage(m.FromUser, m.GroupUser, str, 0)

View File

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

View File

@@ -15,9 +15,10 @@ func Init(g *gin.Engine) {
})
g.GET("/index.html", app.Index) // 首页
g.GET("/test.html", func(ctx *gin.Context) {
ctx.HTML(200, "test.html", nil)
})
g.GET("/friend.html", app.Friend) // 好友列表
g.GET("/group.html", app.Group) // 群组列表
g.GET("/404.html", app.PageNotFound) // 群组列表
// 接口
api := g.Group("/api")
@@ -28,4 +29,5 @@ func Init(g *gin.Engine) {
api.PUT("/grouprank/status", app.ChangeEnableGroupRankStatus) // 修改是否开启水群排行榜状态
api.PUT("/grouprank/skip", app.ChangeSkipGroupRankStatus) // 修改是否跳过水群排行榜状态
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
}
// 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
// @description: 检查用户是否启用了指令
// @param userId

View File

@@ -3,6 +3,7 @@ package service
import (
"go-wechat/client"
"go-wechat/entity"
"go-wechat/vo"
"log"
"os"
"strconv"
@@ -39,3 +40,41 @@ func SaveMessage(msg entity.Message) {
go updateLastActive(msg)
}
}
// GetTextMessagesById
// @description: 根据群id或者用户Id获取消息
// @param id
// @return records
// @return err
func GetTextMessagesById(id string) (records []vo.TextMessageItem, err error) {
// APP消息类型
appMsgList := []string{"57", "4", "5", "6"}
// 这个查询子句抽出来写,方便后续扩展
selectStr := `CASE
WHEN tm.type = 49 THEN
CASE
WHEN EXTRACTVALUE ( tm.content, "/msg/appmsg/type" ) = '57' THEN
EXTRACTVALUE ( tm.content, "/msg/appmsg/title" )
WHEN EXTRACTVALUE ( tm.content, "/msg/appmsg/type" ) = '5' THEN
CONCAT("网页分享消息,标题: ", EXTRACTVALUE (tm.content, "/msg/appmsg/title"), ",描述:", EXTRACTVALUE (tm.content, "/msg/appmsg/des"))
WHEN EXTRACTVALUE ( tm.content, "/msg/appmsg/type" ) = '4' THEN
CONCAT("网页分享消息,标题: ", EXTRACTVALUE (tm.content, "/msg/appmsg/title"), ",描述:", EXTRACTVALUE (tm.content, "/msg/appmsg/des"))
WHEN EXTRACTVALUE ( tm.content, "/msg/appmsg/type" ) = '6' THEN
CONCAT("文件消息,文件名: ", EXTRACTVALUE (tm.content, "/msg/appmsg/title"))
ELSE EXTRACTVALUE ( tm.content, "/msg/appmsg/des" )
END ELSE tm.content
END`
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", selectStr+" AS message").
Where("tm.`from_user` = ?", id).
Where(`(tm.type = 1 OR ( tm.type = 49 AND EXTRACTVALUE ( tm.content, "/msg/appmsg/type" ) IN (?) ))`, appMsgList).
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/entity"
"go-wechat/model"
"go-wechat/utils"
"gorm.io/gorm"
"log"
"slices"
@@ -72,6 +73,8 @@ func Sync() {
log.Printf("新增好友失败: %s", err.Error())
continue
}
// 发送一条新消息
utils.SendMessage(friend.Wxid, "", "大家好我是一个AI机器人可以直接@我询问你想问的问题。该功能默认未启用,请群主艾特我并发送 /ai enable 指令启用", 0)
} else {
pm := map[string]any{
"nickname": friend.Nickname,

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

@@ -0,0 +1,87 @@
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"}--end--`, record.Nickname, strings.ReplaceAll(record.Message, "\n", "。。")))
}
msgTmp := `请帮我总结一下一下的群聊内容的梗概,生成的梗概需要尽可能详细,需要带上一些聊天关键信息,并且带上群友名字。
注意,他们可能是多个话题,请仔细甄别。
每一行代表一个人的发言,每一行的的格式为: {"{nickname}": "{content}"}--end--
聊天记录如下:
%s
`
msg := fmt.Sprintf(msgTmp, 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\n%s", resp.Choices[0].Message.Content)
//log.Printf("群[%s]对话记录总结成功,总结内容: %s", group.Wxid, replyMsg)
utils.SendMessage(group.Wxid, "", replyMsg, 0)
}
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/go-co-op/gocron"
"go-wechat/config"
"go-wechat/tasks/friends"
"go-wechat/tasks/summary"
"go-wechat/tasks/watergroup"
"log"
"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 {
log.Printf("更新好友列表任务已启用,执行表达式: %s", config.Conf.Task.SyncFriends.Cron)

78
views/404.html Normal file
View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>看起来你好像迷路了~</title>
<link href="https://fonts.googleapis.com/css?family=Concert+One" rel="stylesheet">
<link rel="stylesheet" href="assets/css/404.css">
</head>
<body>
<!-- partial:index.partial.html -->
<div class="text">
<p>404</p>
</div>
<div class="container">
<!-- caveman left -->
<div class="caveman">
<div class="leg">
<div class="foot">
<div class="fingers"></div>
</div>
</div>
<div class="leg">
<div class="foot">
<div class="fingers"></div>
</div>
</div>
<div class="shape">
<div class="circle"></div>
<div class="circle"></div>
</div>
<div class="head">
<div class="eye">
<div class="nose"></div>
</div>
<div class="mouth"></div>
</div>
<div class="arm-right">
<div class="club"></div>
</div>
</div>
<!-- caveman right -->
<div class="caveman">
<div class="leg">
<div class="foot">
<div class="fingers"></div>
</div>
</div>
<div class="leg">
<div class="foot">
<div class="fingers"></div>
</div>
</div>
<div class="shape">
<div class="circle"></div>
<div class="circle"></div>
</div>
<div class="head">
<div class="eye">
<div class="nose"></div>
</div>
<div class="mouth"></div>
</div>
<div class="arm-right">
<div class="club"></div>
</div>
</div>
</div>
<!-- //////////////// CREDIT //////////////// -->
<!-- partial -->
<script src='https://use.fontawesome.com/releases/v5.0.7/js/all.js'></script>
<script src="assets/js/script.js"></script>
</body>
</html>

138
views/components.html Normal file
View File

@@ -0,0 +1,138 @@
<!-- 公共模块 -->
<!-- AI设置 -->
{{define "ai"}}
<button type="button"
class="{{ if eq .EnableAi 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="changeAiEnableStatus({{.Wxid}})">
<span
class="{{ if eq .EnableAi true }}translate-x-5{{ else }}translate-x-0{{ end }} pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out">
<span
class="{{ if eq .EnableAi true }}opacity-0 duration-100 ease-out{{ else }}opacity-100 duration-200 ease-in{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12">
<path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
</span>
<span
class="{{ if eq .EnableAi true }}opacity-100 duration-200 ease-in{{ else }}opacity-0 duration-100 ease-out{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"/>
</svg>
</span>
</span>
</button>
{{end}}
<!-- 水群排行榜 -->
{{define "chatRank"}}
<button type="button"
class="{{ if eq .EnableChatRank 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="changeGroupRankEnableStatus({{.Wxid}})">
<span
class="{{ if eq .EnableChatRank true }}translate-x-5{{ else }}translate-x-0{{ end }} pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out">
<span
class="{{ if eq .EnableChatRank true }}opacity-0 duration-100 ease-out{{ else }}opacity-100 duration-200 ease-in{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12">
<path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
</span>
<span
class="{{ if eq .EnableChatRank true }}opacity-100 duration-200 ease-in{{ else }}opacity-0 duration-100 ease-out{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"/>
</svg>
</span>
</span>
</button>
{{end}}
<!-- 消息总结 -->
{{define "summary"}}
<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"
role="switch" aria-checked="false" onclick="changeSummaryEnableStatus({{.Wxid}})">
<span
class="{{ if eq .EnableSummary true }}translate-x-5{{ else }}translate-x-0{{ end }} pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out">
<span
class="{{ if eq .EnableSummary true }}opacity-0 duration-100 ease-out{{ else }}opacity-100 duration-200 ease-in{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12">
<path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
</span>
<span
class="{{ if eq .EnableSummary true }}opacity-100 duration-200 ease-in{{ else }}opacity-0 duration-100 ease-out{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"/>
</svg>
</span>
</span>
</button>
{{end}}
<!-- 欢迎新成员 -->
{{define "welcome"}}
<button type="button"
class="{{ if eq .EnableWelcome 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="changeWelcomeEnableStatus({{.Wxid}})">
<span
class="{{ if eq .EnableWelcome true }}translate-x-5{{ else }}translate-x-0{{ end }} pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out">
<span
class="{{ if eq .EnableWelcome true }}opacity-0 duration-100 ease-out{{ else }}opacity-100 duration-200 ease-in{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12">
<path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
</span>
<span
class="{{ if eq .EnableWelcome true }}opacity-100 duration-200 ease-in{{ else }}opacity-0 duration-100 ease-out{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"/>
</svg>
</span>
</span>
</button>
{{end}}
<!-- 指令设置 -->
{{define "command"}}
<button type="button"
class="{{ if eq .EnableCommand 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="changeCommandEnableStatus({{.Wxid}})">
<span
class="{{ if eq .EnableCommand true }}translate-x-5{{ else }}translate-x-0{{ end }} pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out">
<span
class="{{ if eq .EnableCommand true }}opacity-0 duration-100 ease-out{{ else }}opacity-100 duration-200 ease-in{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12">
<path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
</span>
<span
class="{{ if eq .EnableCommand true }}opacity-100 duration-200 ease-in{{ else }}opacity-0 duration-100 ease-out{{ end }} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
aria-hidden="true">
<svg class="h-3 w-3 text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"/>
</svg>
</span>
</span>
</button>
{{end}}

10
views/footer.html Normal file
View File

@@ -0,0 +1,10 @@
<footer>
<div class="mx-auto max-w-3xl px-4 sm:px-6 lg:max-w-7xl lg:px-8">
<div class="border-t border-gray-200 py-8 text-center text-sm text-gray-500 sm:text-left">
<span class="block sm:inline">本项目完全开源,开源地址: </span>
<span class="block sm:inline text-red-500">
<a target="_blank" href="https://gitee.ltd/lxh/go-wxhelper">https://gitee.ltd</a>
</span>
</div>
</div>
</footer>

105
views/friend.html Normal file
View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<title>好友列表</title>
<link href="assets/css/daisyui-4.4.14-full.min.css" rel="stylesheet" type="text/css"/>
<link href="assets/css/index.css" rel="stylesheet" type="text/css"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.5.0/axios.min.js"></script>
<script src="assets/js/index.js"></script>
</head>
<body class="h-full">
<div class="min-h-full">
{{ template "head.html" "friend" }}
<main class="-mt-32">
<div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
<div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6 text-2xl">
<ul role="list" class="grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 xl:gap-x-8">
{{ range .friends }}
<li class="overflow-hidden rounded-xl border border-gray-200">
<div class="flex items-center gap-x-4 border-b border-gray-900/5 bg-gray-50 p-6">
<img src="assets/img/status-{{ if eq .IsOk true }}ok{{else}}fail{{end}}.png" alt="Tuple" class="h-12 w-12 flex-none rounded-lg bg-white object-cover ring-1 ring-gray-900/10">
<div class="flex-1">
<div class="text-sm font-medium leading-6 text-gray-900">{{ .Nickname }}</div>
{{ if eq .IsOk true }}
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">正常</span>
{{ else }}
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20">已删除</span>
{{ end }}
</div>
</div>
<dl class="-my-3 divide-y divide-gray-100 px-6 py-4 text-sm leading-6">
<div class="flex justify-between gap-x-4 py-3">
<dt class="text-gray-500">原始微信Id<br/>微信号</dt>
<dd>
<div class="text-gray-700">{{ .Wxid }}</div>
{{ if not (eq .CustomAccount "") }}
<div class="truncate text-gray-500">{{ .CustomAccount }}</div>
{{end}}
</dd>
</div>
<div class="flex justify-between gap-x-4 py-3">
<dt class="text-gray-500">最后活跃时间</dt>
<dd class="flex items-start gap-x-2">
<div class="font-medium text-gray-900">
{{ if eq .LastActive.IsNil true }}
无数据
{{ else }}
<time datetime="{{ .LastActive }}">{{ .LastActive }}</time>
{{ end }}
</div>
</dd>
</div>
<div class="flex justify-between gap-x-4 py-3">
<dt class="text-gray-500">AI</dt>
<dd class="flex items-start gap-x-2">
{{ template "ai" . }}
</dd>
</div>
{{ if eq .EnableAi true }}
<div class="flex justify-between gap-x-4 py-3">
<dt class="text-gray-500 mt-2">AI模型</dt>
<dd class="flex items-start gap-x-2">
<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}})">
{{$useModel := .AiModel}}
{{ range $.aiModels }}
<option value="{{.Model}}" {{ if eq $useModel .Model}}selected{{ end }}>
{{.Name}}
</option>
{{ end }}
</select>
</label>
</dd>
</div>
{{ end }}
<div class="flex justify-between gap-x-4 py-3">
<dt class="text-gray-500">指令</dt>
<dd class="flex items-start gap-x-2">
{{ template "command" . }}
</dd>
</div>
</dl>
</li>
{{ end }}
</ul>
</div>
</div>
</main>
{{ template "footer.html" }}
</div>
</body>
</html>

118
views/group.html Normal file
View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en" class="h-full bg-gray-100">
<head>
<meta charset="UTF-8">
<title>水群助手</title>
<link href="assets/css/daisyui-4.4.14-full.min.css" rel="stylesheet" type="text/css"/>
<link href="assets/css/index.css" rel="stylesheet" type="text/css"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.5.0/axios.min.js"></script>
<script src="assets/js/index.js"></script>
</head>
<body class="h-full">
<div class="min-h-full">
{{ template "head.html" "group" }}
<main class="-mt-32">
<div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
<div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6 text-2xl">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">群名称
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
最后活跃时间
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
是否在通讯录
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
是否启用AI
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
是否启用水群排行榜
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
是否启用聊天记录总结
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
是否启用迎新
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
是否启用指令
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{{ range .groups }}
<tr class="even:bg-gray-50">
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<div class="font-medium text-gray-900">{{ .Nickname }}</div>
<div class="mt-1 truncate text-gray-500">{{ .Wxid }}</div>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ if eq .LastActive.IsNil true }}
无数据
{{ else }}
{{ .LastActive }}
{{ end }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ if eq .IsOk true }}
<span
class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20"></span>
{{ else }}
<span
class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20"></span>
{{ end }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ template "ai" . }}
<!-- 使用的模型 -->
{{ if eq .EnableAi true }}
<select id="location" name="location"
class="mt-2 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}})">
<option value="" {{ if eq .AiModel
"" }}selected{{ end }}>默认(gpt-3.5-turbo-0613)
</option>
{{$useModel := .AiModel}}
{{ range $.aiModels }}
<option value="{{.Model}}" {{ if eq $useModel .Model}}selected{{ end }}>
{{.Name}}({{.Model}})
</option>
{{ end }}
</select>
{{ end }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ template "chatRank" . }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ template "summary" . }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ template "welcome" . }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ template "command" . }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</main>
{{ template "footer.html" }}
</div>
</body>

27
views/head.html Normal file
View File

@@ -0,0 +1,27 @@
<div class="bg-green-600 pb-32">
<nav class="border-b border-green-300 border-opacity-25 bg-green-600 lg:border-none">
<div class="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8">
<div
class="relative flex h-16 items-center justify-between lg:border-b lg:border-green-400 lg:border-opacity-25">
<div class="flex items-center px-2 lg:px-0">
<div class="flex-shrink-0">
<img class="block h-8 w-8" src="assets/img/logo.png" alt="">
</div>
<div class="hidden lg:ml-10 lg:block">
<div class="flex space-x-4">
<a href="/index.html" class="{{ if eq . "index" }}bg-green-700{{ else }}hover:bg-green-500 hover:bg-opacity-75{{ end }} text-white rounded-md py-2 px-3 text-sm font-medium" aria-current="page">首页</a>
<a href="/friend.html" class="{{ if eq . "friend" }}bg-green-700{{ else }}hover:bg-green-500 hover:bg-opacity-75{{ end }} text-white rounded-md py-2 px-3 text-sm font-medium" aria-current="page">好友</a>
<a href="/group.html" class="{{ if eq . "group" }}bg-green-700{{ else }}hover:bg-green-500 hover:bg-opacity-75{{ end }} text-white rounded-md py-2 px-3 text-sm font-medium">群组</a>
<!-- <a href="/index.html" class="{{ if eq . "vnc" }}bg-green-700{{ else }}hover:bg-green-500 hover:bg-opacity-75{{ end }} text-white rounded-md py-2 px-3 text-sm font-medium">VNC</a>-->
</div>
</div>
</div>
</div>
</div>
</nav>
<header class="py-10">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold tracking-tight text-white">{{ codeToChinese . }}</h1>
</div>
</header>
</div>

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="h-full bg-gray-100">
<head>
<meta charset="UTF-8">
<title>水群助手</title>
@@ -7,231 +8,40 @@
<link href="assets/css/daisyui-4.4.14-full.min.css" rel="stylesheet" type="text/css"/>
<link href="assets/css/index.css" rel="stylesheet" type="text/css"/>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.5.0/axios.min.js"></script>
<script src="assets/js/index.js"></script>
</head>
<body>
<div class="p-5 space-y-5">
<!-- 如果msg不等于success显示alert -->
{{ if ne .msg "success" }}
<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>{{ .msg }}</span>
<body class="h-full">
<div class="min-h-full">
{{ template "head.html" "index"}}
<main class="-mt-32">
<div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
<div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6 text-2xl">
<dl class="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div class="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
<dt class="truncate text-sm font-medium text-gray-500">好友数量</dt>
<dd class="mt-1 text-2xl font-semibold tracking-tight text-gray-900">{{ .friendCount }}/{{ .friendWithoutCount }}</dd>
<dd class="mt-1 text-xs font-semibold tracking-tight text-red-300">通讯录/已删除</dd>
</div>
{{ end }}
<div role="tablist" class="tabs tabs-bordered">
<input type="radio" name="friend_tab" role="tab" class="tab" aria-label="好友列表" checked />
<div role="tabpanel" class="tab-content p-6">
<!-- 循环好友列表 -->
<table class="table table-zebra">
<!-- head -->
<thead>
<tr>
<th>微信Id</th>
<th>微信号</th>
<th>昵称</th>
<th>最后活跃时间</th>
<th>是否在通讯录</th>
<th>是否启用AI</th>
<th>是否启用指令</th>
</tr>
</thead>
<tbody>
{{ range .friends }}
<tr>
<td>{{ .Wxid }}</td>
<td>{{ .CustomAccount }}</td>
<td>{{ .Nickname }}</td>
<td>
{{ if eq .LastActive.IsNil true }}
无数据
{{ else }}
{{ .LastActive }}
{{ end }}
</td>
<td>
{{ if eq .IsOk true }}
<div class="badge badge-info gap-2">
<div class="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
<dt class="truncate text-sm font-medium text-gray-500">群组数量</dt>
<dd class="mt-1 text-3xl font-semibold tracking-tight text-gray-900">{{ .groupCount }}/{{ .groupWithoutCount }}</dd>
<dd class="mt-1 text-xs font-semibold tracking-tight text-red-300">在群聊/已退出</dd>
</div>
{{ else }}
<div class="badge badge-error gap-2">
<div class="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6">
<dt class="truncate text-sm font-medium text-gray-500">是否配置VNC</dt>
<dd class="mt-1 text-3xl font-semibold tracking-tight text-gray-900">{{ boolToChinese .isVnc }}</dd>
</div>
{{ end }}
</td>
<td>
<label class="swap swap-flip {{ checkSwap .EnableAi }}">
<input type="checkbox" onclick="changeAiEnableStatus({{.Wxid}})"/>
<div class="swap-on">✔️已启用</div>
<div class="swap-off">❌已禁用</div>
</label>
{{ if .EnableAi }}
<br />
<select class="select select-success select-xs w-1/2 max-w-xs" onchange="aiModelChange(event, {{.Wxid}})">
<option value="" {{ if eq .AiModel ""}}selected{{ end }}>默认(gpt-3.5-turbo-0613)</option>
{{$useModel := .AiModel}}
{{ range $.aiModels }}
<option value="{{.Model}}" {{ if eq $useModel .Model}}selected{{ end }}>{{.Name}}({{.Model}})</option>
{{ end }}
</select>
{{ end }}
</td>
<td>
<label class="swap swap-flip {{ checkSwap .EnableCommand }}">
<input type="checkbox" onclick="changeCommandEnableStatus({{.Wxid}})"/>
<div class="swap-on">✔️已启用</div>
<div class="swap-off">❌已禁用</div>
</label>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<input type="radio" name="friend_tab" role="tab" class="tab" aria-label="群列表" />
<div role="tabpanel" class="tab-content p-6">
<!-- 循环群列表 -->
<table class="table table-zebra">
<!-- head -->
<thead>
<tr>
<th>群Id</th>
<th>昵称</th>
<th>最后活跃时间</th>
<th>是否在通讯录</th>
<th>是否启用AI</th>
<th>是否启用水群排行榜</th>
<th>是否启用迎新</th>
<th>是否启用指令</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{{ range .groups }}
<tr>
<td>{{ .Wxid }}</td>
<td>{{ .Nickname }}</td>
<td>
{{ if eq .LastActive.IsNil true }}
无数据
{{ else }}
{{ .LastActive }}
{{ end }}
</td>
<td>
{{ if eq .IsOk true }}
<div class="badge badge-info gap-2">
</div>
{{ else }}
<div class="badge badge-error gap-2">
</div>
{{ end }}
</td>
<td>
<!-- EnableAi -->
<label class="swap swap-flip {{ checkSwap .EnableAi }}">
<input type="checkbox" onclick="changeAiEnableStatus({{.Wxid}})"/>
<div class="swap-on">✔️已启用</div>
<div class="swap-off">❌已禁用</div>
</label>
{{ if .EnableAi }}
<br />
<select class="select select-success select-xs w-1/2 max-w-xs" onchange="aiModelChange(event, {{.Wxid}})">
<option value="" {{ if eq .AiModel ""}}selected{{ end }}>默认(gpt-3.5-turbo-0613)</option>
{{$useModel := .AiModel}}
{{ range $.aiModels }}
<option value="{{.Model}}" {{ if eq $useModel .Model}}selected{{ end }}>{{.Name}}({{.Model}})</option>
{{ end }}
</select>
{{ end }}
</td>
<td>
<!-- EnableChatRank -->
<label class="swap swap-flip {{ checkSwap .EnableChatRank }}">
<input type="checkbox" onclick="changeGroupRankEnableStatus({{.Wxid}})"/>
<div class="swap-on">✔️已启用</div>
<div class="swap-off">❌已禁用</div>
</label>
</td>
<td>
<label class="swap swap-flip {{ checkSwap .EnableWelcome }}">
<input type="checkbox" onclick="changeWelcomeEnableStatus({{.Wxid}})"/>
<div class="swap-on">✔️已启用</div>
<div class="swap-off">❌已禁用</div>
</label>
</td>
<td>
<label class="swap swap-flip {{ checkSwap .EnableCommand }}">
<input type="checkbox" onclick="changeCommandEnableStatus({{.Wxid}})"/>
<div class="swap-on">✔️已启用</div>
<div class="swap-off">❌已禁用</div>
</label>
</td>
<td>
<button class="btn btn-link" onclick="getGroupUsers({{.Wxid}}, {{.Nickname}})">查看群成员</button>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ if ne .vnc "" }}
<input type="radio" name="friend_tab" role="tab" class="tab" aria-label="运行状态"/>
<div role="tabpanel" class="tab-content p-6">
<div style="height: 747px;width: 1280px;overflow: hidden !important;">
<iframe src="{{ .vnc }}" frameborder="0" style="width: 100%;height: 100%;pointer-events: none;"></iframe>
</dl>
</div>
</div>
{{ end }}
</main>
{{ template "footer.html" }}
</div>
</div>
<dialog id="groupUserModal" class="modal">
<div class="modal-box w-11/12 max-w-7xl">
<!-- <form method="dialog">-->
<!-- <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>-->
<!-- </form>-->
<h3 class="font-bold text-lg" id="groupUserModalName">我是群名称</h3>
<!-- 加载动画 -->
<div class="divider divider-warning">成员列表</div>
<!-- 好友列表 -->
<table class="table table-zebra">
<!-- head -->
<thead>
<tr>
<th>微信Id</th>
<th>昵称</th>
<th>是否群成员</th>
<th>是否群主</th>
<th>加群时间</th>
<th>最后活跃时间</th>
<th>退群时间</th>
<th>是否跳过水群排行榜</th>
</tr>
</thead>
<tbody id="groupUsers"></tbody>
</table>
</div>
</dialog>
</body>
</html>

53
views/notifications.html Normal file
View File

@@ -0,0 +1,53 @@
<!-- Global notification live region, render this permanently at the end of the document -->
<div aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
<div class="flex w-full flex-col items-center space-y-4 sm:items-end" id="notification">
<!--
Notification panel, dynamically insert this into the live region when it needs to be displayed
Entering: "transform ease-out duration-300 transition"
From: "translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
To: "translate-y-0 opacity-100 sm:translate-x-0"
Leaving: "transition ease-in duration-100"
From: "opacity-100"
To: "opacity-0"
-->
<div
class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium text-gray-900">通知标题</p>
<p class="mt-1 text-sm text-gray-500">
通知内容,通知内容,通知内容,通知内容,通知内容,通知内容,通知内容,通知内容</p>
</div>
<div class="ml-4 flex flex-shrink-0">
<button type="button"
class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onclick="closeAlert()">
<span class="sr-only">关闭</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function closeAlert() {
const element = document.getElementById("notification");
element.parentNode.parentNode.removeChild(element.parentNode);
}
</script>

429
views/static/css/404.css Normal file
View File

@@ -0,0 +1,429 @@
body {
background-color: #FF7F2E;
font-family: "Concert One", cursive;
margin: 0;
overflow: hidden;
padding: 0;
}
/*/////////////////// rules */
/*/////////////////// scene */
.text {
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
color: rgba(19, 36, 44, 0.1);
font-size: 30em;
text-align: center;
top: 40%;
}
.container {
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
height: 300px;
width: 500px;
}
.container:after {
position: absolute;
content: "";
background-color: rgba(19, 36, 44, 0.1);
border-radius: 12px;
bottom: 40px;
height: 12px;
left: 80px;
width: 350px;
z-index: -1;
}
/*/////////////////// caveman */
.caveman {
height: 300px;
position: absolute;
width: 250px;
}
.caveman:nth-child(1) {
right: 20px;
}
.caveman:nth-child(2) {
left: 20px;
transform: rotateY(180deg);
}
.head {
position: absolute;
background-color: #13242C;
border-radius: 50px;
height: 140px;
left: 60px;
top: 25px;
width: 65px;
}
.head:after, .head:before {
content: "";
position: absolute;
background-color: #13242C;
border-radius: 10px;
height: 20px;
width: 7px;
}
.head:after {
left: 35px;
top: -8px;
transform: rotate(20deg);
}
.head:before {
left: 30px;
top: -8px;
transform: rotate(-20deg);
}
.head .eye {
left: 50%;
position: absolute;
transform: translateX(-50%);
background-color: #EAB08C;
border-radius: 50px;
height: 16px;
left: 45%;
top: 40px;
width: 48px;
}
.head .eye:after, .head .eye:before {
content: "";
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: #13242C;
border-radius: 50%;
height: 5px;
width: 5px;
}
.head .eye:after {
left: 5px;
}
.head .eye:before {
right: 9px;
}
.head .eye .nose {
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
background-color: #D9766C;
border-left: 8px solid rgba(19, 36, 44, 0.1);
border-radius: 10px;
box-sizing: border-box;
height: 35px;
left: 45%;
top: 12px;
width: 15px;
}
.shape {
left: 50%;
position: absolute;
transform: translateX(-50%);
border-radius: 50%;
height: 140px;
overflow: hidden;
top: 70px;
width: 140px;
}
.shape .circle {
position: absolute;
border-radius: 50%;
height: 60px;
width: 60px;
}
.shape .circle:after, .shape .circle:before {
content: "";
position: absolute;
border-radius: 50%;
height: 20px;
width: 20px;
}
.shape .circle:after {
left: 50px;
top: 10px;
}
.shape .circle:before {
left: 60px;
top: 45px;
}
.shape .circle:nth-child(1) {
left: -12px;
top: 80px;
}
.shape .circle:nth-child(2) {
right: 10px;
top: 0px;
transform: rotate(90deg);
}
.shape .circle:nth-child(2):after {
left: 65px;
top: 10px;
}
.shape .circle:nth-child(2):before {
display: none;
}
.caveman:nth-child(1) .shape {
background-color: #D13433;
}
.caveman:nth-child(1) .shape .circle {
background-color: #932422;
}
.caveman:nth-child(1) .shape .circle:after, .caveman:nth-child(1) .shape .circle:before {
background-color: #932422;
}
.caveman:nth-child(2) .shape {
background-color: #932422;
}
.caveman:nth-child(2) .shape .circle {
background-color: #D13433;
}
.caveman:nth-child(2) .shape .circle:after, .caveman:nth-child(2) .shape .circle:before {
background-color: #D13433;
}
.arm-right {
position: absolute;
background-color: #EAB08C;
border-left: 8px solid rgba(19, 36, 44, 0.1);
border-radius: 50px;
box-sizing: border-box;
height: 180px;
left: 135px;
top: 80px;
transform-origin: 30px 30px;
width: 60px;
z-index: 1;
}
.arm-right .club {
position: absolute;
border-bottom: 110px solid #601513;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
height: 0;
left: -60px;
top: 120px;
transform: rotate(70deg);
width: 20px;
}
.arm-right .club:after, .arm-right .club:before {
position: absolute;
content: "";
background-color: #601513;
border-radius: 50%;
left: 0;
}
.arm-right .club:after {
height: 20px;
width: 20px;
top: -10px;
}
.arm-right .club:before {
height: 40px;
width: 40px;
left: -10px;
top: 90px;
}
.leg {
position: absolute;
border-radius: 10px;
height: 55px;
top: 200px;
width: 10px;
}
.leg:after {
position: absolute;
content: "";
border-radius: 50%;
height: 10px;
left: -5px;
top: 15px;
width: 10px;
}
.leg .foot {
position: absolute;
border-radius: 25px 25px 0 0;
height: 25px;
left: -38px;
top: 30px;
width: 50px;
}
.leg .foot:after, .leg .foot:before, .leg .foot .fingers, .leg .foot .fingers:after {
position: absolute;
background-color: #EAB08C;
border-radius: 50%;
bottom: 0px;
height: 15px;
transform-origin: bottom;
width: 15px;
}
.leg .foot:after {
left: -6px;
content: "";
}
.leg .foot:before {
left: 8px;
transform: scale(0.6);
content: "";
}
.leg .foot .fingers {
left: 15px;
transform: scale(0.6);
}
.leg .foot .fingers:after {
left: 11px;
content: "";
}
.leg:nth-child(1) {
background-color: #B2524D;
left: 95px;
}
.leg:nth-child(1):after {
background-color: #B2524D;
}
.leg:nth-child(1) .foot {
background-color: #B2524D;
}
.leg:nth-child(1) .foot:after {
background-color: #B2524D;
}
.leg:nth-child(1) .foot:before {
display: none;
}
.leg:nth-child(2) {
background-color: #D9766C;
left: 115px;
}
.leg:nth-child(2):after {
background-color: #D9766C;
}
.leg:nth-child(2) .foot {
background-color: #D9766C;
}
/*/////////////////// animation */
.caveman:nth-child(1) .arm-right {
animation: arm-anima 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
}
.caveman:nth-child(2) .arm-right {
animation: arm-anima 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
animation-delay: 0.6s;
}
@keyframes arm-anima {
0% {
transform: rotate(0);
}
100% {
transform: rotate(-360deg);
}
}
.caveman:nth-child(2) .head {
animation: head-anima 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
}
.caveman:nth-child(1) .head {
animation: head-anima 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
animation-delay: 0.6s;
}
@keyframes head-anima {
0% {
top: 25px;
}
42% {
top: 25px;
}
45% {
top: 50px;
}
100% {
top: 25px;
}
}
.caveman:nth-child(2) .eye:after,
.caveman:nth-child(2) .eye:before {
animation: eye-anima 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
}
.caveman:nth-child(1) .eye:after,
.caveman:nth-child(1) .eye:before {
animation: eye-anima 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
animation-delay: 0.6s;
}
@keyframes eye-anima {
0% {
height: 5px;
}
42% {
height: 5px;
}
45% {
height: 1px;
}
100% {
height: 5px;
}
}
.container:after {
animation: shadow-anima 1.2s infinite cubic-bezier(0.55, 0.01, 0.16, 1.34);
animation-delay: 0.1s;
}
@keyframes shadow-anima {
0% {
width: 350px;
left: 80px;
}
25% {
width: 450px;
left: 80px;
}
50% {
width: 350px;
left: 80px;
}
75% {
width: 450px;
left: 0px;
}
100% {
width: 350px;
left: 80px;
}
}
/*/////////////////////// credit ////*/
#link {
bottom: 20px;
color: #000;
opacity: 0.2;
display: flex;
align-items: center;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
#link p {
margin: 0;
margin-left: 5px;
}
#link:hover {
opacity: 1;
}

BIN
views/static/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

1
views/static/js/404.js Normal file
View File

@@ -0,0 +1 @@
// no JS this time !

View File

@@ -12,9 +12,12 @@ function changeAiEnableStatus(wxId) {
}
}).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) {
console.log(`错误信息: ${error}`);
alert("修改失败")
}).finally(function () {
window.location.reload();
})
}
@@ -29,9 +32,32 @@ function changeGroupRankEnableStatus(wxId) {
}
}).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) {
console.log(`错误信息: ${error}`);
alert("修改失败")
}).finally(function () {
window.location.reload();
})
}
// 修改水群排行榜状态
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) {
console.log(`错误信息: ${error}`);
alert("修改失败")
}).finally(function () {
window.location.reload();
})
}
@@ -45,9 +71,12 @@ function changeWelcomeEnableStatus(wxId) {
}
}).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) {
console.log(`错误信息: ${error}`);
alert("修改失败")
}).finally(function () {
window.location.reload();
})
}
@@ -61,9 +90,12 @@ function changeCommandEnableStatus(wxId) {
}
}).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) {
console.log(`错误信息: ${error}`);
alert("修改失败")
}).finally(function () {
window.location.reload();
})
}
@@ -79,9 +111,12 @@ function changeUserGroupRankSkipStatus(groupId, userId) {
}
}).then(function (response) {
console.log(`返回结果: ${JSON.stringify(response)}`);
alert(`${response.data}`)
}).catch(function (error) {
console.log(`错误信息: ${error}`);
alert("修改失败")
}).finally(function () {
window.location.reload();
})
}
@@ -154,5 +189,7 @@ function aiModelChange(event, wxid) {
}).catch(function (error) {
console.log(`错误信息: ${error}`);
alert("修改失败")
}).finally(function () {
window.location.reload();
})
}

View File

@@ -18,6 +18,7 @@ type FriendItem struct {
EnableChatRank bool // 是否使用聊天排行
EnableWelcome bool // 是否使用迎新
EnableCommand bool // 是否启用指令
EnableSummary 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"`
}