From 617d4f50a4c51c657d289113754dd40ca13a45dd Mon Sep 17 00:00:00 2001
From: himawari <54976075+guohuiyuan@users.noreply.github.com>
Date: Sun, 20 Jul 2025 13:31:06 +0800
Subject: [PATCH] feat: airecord (#1180)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: 源文雨 <41315874+fumiama@users.noreply.github.com>
---
README.md | 20 ++++-
go.mod | 4 +-
go.sum | 8 +-
main.go | 2 +
plugin/aichat/cfg.go | 47 ++++++++++-
plugin/aichat/main.go | 56 ++++++++++---
plugin/airecord/record.go | 134 ++++++++++++++++++++++++++++++
plugin/bilibili/bilibili_parse.go | 7 +-
plugin/bilibili/card2msg_test.go | 2 +-
plugin/manager/manager.go | 7 ++
10 files changed, 267 insertions(+), 20 deletions(-)
create mode 100644 plugin/airecord/record.go
diff --git a/README.md b/README.md
index 5279927c..988beba6 100644
--- a/README.md
+++ b/README.md
@@ -255,6 +255,8 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w]
- [x] 翻牌
- [x] 赞我
+
+ - [x] 群签到
- [x] [开启 | 关闭]入群验证
@@ -276,6 +278,20 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w]
- 设置欢迎语可选添加参数说明:{at}可在发送时艾特被欢迎者 {nickname}是被欢迎者名字 {avatar}是被欢迎者头像 {uid}是被欢迎者QQ号 {gid}是当前群群号 {groupname} 是当前群群名
+
+
+ 群应用:AI声聊
+
+ `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/airecord"`
+
+ - [x] 设置AI语音群号1048452984(tips:机器人任意所在群聊即可)
+
+ - [x] 设置AI语音模型
+
+ - [x] 查看AI语音配置
+
+ - [x] 发送AI语音xxx
+
定时指令触发器
@@ -1584,7 +1600,7 @@ print("run[CQ:image,file="+j["img"]+"]")
- [x] 设置AI聊天温度80
- [x] 设置AI聊天接口类型[OpenAI|OLLaMA|GenAI]
- [x] 设置AI聊天(不)支持系统提示词
- - [x] 设置AI聊天接口地址https://xxx
+ - [x] 设置AI聊天接口地址https://api.deepseek.com/chat/completions
- [x] 设置AI聊天密钥xxx
- [x] 设置AI聊天模型名xxx
- [x] 查看AI聊天系统提示词
@@ -1594,6 +1610,8 @@ print("run[CQ:image,file="+j["img"]+"]")
- [x] 设置AI聊天(不)响应AT
- [x] 设置AI聊天最大长度4096
- [x] 设置AI聊天TopP 0.9
+ - [x] 设置AI聊天(不)以AI语音输出
+ - [x] 查看AI聊天配置
diff --git a/go.mod b/go.mod
index 6e5daf61..c319db1c 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.20
require (
github.com/Baidu-AIP/golang-sdk v1.1.1
- github.com/FloatTech/AnimeAPI v1.7.1-0.20250530055006-50f5c7587c5b
+ github.com/FloatTech/AnimeAPI v1.7.1-0.20250717123723-d300df538b46
github.com/FloatTech/floatbox v0.0.0-20250513111443-adba80e84e80
github.com/FloatTech/gg v1.1.3
github.com/FloatTech/imgfactory v0.2.2-0.20230413152719-e101cc3606ef
@@ -45,7 +45,7 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/tidwall/gjson v1.18.0
github.com/wcharczuk/go-chart/v2 v2.1.2
- github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250330133859-27c25d9412b5
+ github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250707133321-6197b8ee5df7
gitlab.com/gomidi/midi/v2 v2.1.7
golang.org/x/image v0.24.0
golang.org/x/sys v0.30.0
diff --git a/go.sum b/go.sum
index ee423745..d4b9a611 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,8 @@
github.com/Baidu-AIP/golang-sdk v1.1.1 h1:RQsAmgDSAkiq22I6n7XJ2t3afgzFeqjY46FGhvrx4cw=
github.com/Baidu-AIP/golang-sdk v1.1.1/go.mod h1:bXnGw7xPeKt8aF7UCELKrV6UZ/46spItONK1RQBQj1Y=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
-github.com/FloatTech/AnimeAPI v1.7.1-0.20250530055006-50f5c7587c5b h1:H/1xpchTGmdoHqrszH4gjafCyHIhsGSFryAkBNsu8OI=
-github.com/FloatTech/AnimeAPI v1.7.1-0.20250530055006-50f5c7587c5b/go.mod h1:XXG1eBJf+eeWacQx5azsQKL5Gg7jDYTFyyZGIa/56js=
+github.com/FloatTech/AnimeAPI v1.7.1-0.20250717123723-d300df538b46 h1:X6ZbOWoZJIoHCin+CeU92Q3EwpvglyQ4gc5BZhOtAwo=
+github.com/FloatTech/AnimeAPI v1.7.1-0.20250717123723-d300df538b46/go.mod h1:XXG1eBJf+eeWacQx5azsQKL5Gg7jDYTFyyZGIa/56js=
github.com/FloatTech/floatbox v0.0.0-20250513111443-adba80e84e80 h1:lFD1pd8NkYCrw0QpTX/T5pJ67I7AL5eGxQ4v0r9f81Q=
github.com/FloatTech/floatbox v0.0.0-20250513111443-adba80e84e80/go.mod h1:IWoFFqu+0FeaHHQdddyiTRL5z7gJME6qHC96qh0R2sc=
github.com/FloatTech/gg v1.1.3 h1:+GlL02lTKsxJQr4WCuNwVxC1/eBZrCvypCIBtxuOFb4=
@@ -199,8 +199,8 @@ github.com/vcaesar/cedar v0.20.2/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFe
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
-github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250330133859-27c25d9412b5 h1:HsMcBsVpYuQv+W8pjX5WdwYROrFQP9c5Pbf4x4adDus=
-github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250330133859-27c25d9412b5/go.mod h1:C86nQ0gIdAri4K2vg8IIQIslt08zzrKMcqYt8zhkx1M=
+github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250707133321-6197b8ee5df7 h1:ya+lVbCC/EN5JumpQDDlVCSrWzLwHl4CHzlTANKDvrU=
+github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250707133321-6197b8ee5df7/go.mod h1:C86nQ0gIdAri4K2vg8IIQIslt08zzrKMcqYt8zhkx1M=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
diff --git a/main.go b/main.go
index a260c258..5e9b13e5 100644
--- a/main.go
+++ b/main.go
@@ -38,6 +38,8 @@ import (
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/sleepmanage" // 统计睡眠时间
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/airecord" // 群应用:AI声聊
+
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/atri" // ATRI词库
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/manager" // 群管
diff --git a/plugin/aichat/cfg.go b/plugin/aichat/cfg.go
index 2d40bf04..f9287532 100644
--- a/plugin/aichat/cfg.go
+++ b/plugin/aichat/cfg.go
@@ -1,6 +1,7 @@
package aichat
import (
+ "fmt"
"strconv"
"strings"
@@ -13,7 +14,9 @@ import (
"github.com/wdvxdr1123/ZeroBot/message"
)
-var cfg = newconfig()
+var (
+ cfg = newconfig()
+)
type config struct {
ModelName string
@@ -26,6 +29,7 @@ type config struct {
Separator string
NoReplyAT bool
NoSystemP bool
+ NoRecord bool
}
func newconfig() config {
@@ -151,3 +155,44 @@ func newextrasetfloat32(ptr *float32) func(ctx *zero.Ctx) {
ctx.SendChain(message.Text("成功"))
}
}
+
+func printConfig(rate int64, temperature int64, cfg config) string {
+ maxn := cfg.MaxN
+ if maxn == 0 {
+ maxn = 4096
+ }
+ topp := cfg.TopP
+ if topp == 0 {
+ topp = 0.9
+ }
+ var builder strings.Builder
+ builder.WriteString("当前AI聊天配置:\n")
+ builder.WriteString(fmt.Sprintf("• 模型名:%s\n", cfg.ModelName))
+ builder.WriteString(fmt.Sprintf("• 接口类型:%d(%s)\n", cfg.Type, apilist[cfg.Type]))
+ builder.WriteString(fmt.Sprintf("• 触发概率:%d%%\n", rate))
+ builder.WriteString(fmt.Sprintf("• 温度:%.2f\n", float32(temperature)/100))
+ builder.WriteString(fmt.Sprintf("• 最大长度:%d\n", maxn))
+ builder.WriteString(fmt.Sprintf("• TopP:%.1f\n", topp))
+ builder.WriteString(fmt.Sprintf("• 系统提示词:%s\n", cfg.SystemP))
+ builder.WriteString(fmt.Sprintf("• 接口地址:%s\n", cfg.API))
+ builder.WriteString(fmt.Sprintf("• 密钥:%s\n", maskKey(cfg.Key)))
+ builder.WriteString(fmt.Sprintf("• 分隔符:%s\n", cfg.Separator))
+ builder.WriteString(fmt.Sprintf("• 响应@:%s\n", yesNo(!cfg.NoReplyAT)))
+ builder.WriteString(fmt.Sprintf("• 支持系统提示词:%s\n", yesNo(!cfg.NoSystemP)))
+ builder.WriteString(fmt.Sprintf("• 以AI语音输出:%s\n", yesNo(!cfg.NoRecord)))
+ return builder.String()
+}
+
+func maskKey(key string) string {
+ if len(key) <= 4 {
+ return "****"
+ }
+ return key[:2] + strings.Repeat("*", len(key)-4) + key[len(key)-2:]
+}
+
+func yesNo(b bool) string {
+ if b {
+ return "是"
+ }
+ return "否"
+}
diff --git a/plugin/aichat/main.go b/plugin/aichat/main.go
index 3cfa2240..c8ac1df6 100644
--- a/plugin/aichat/main.go
+++ b/plugin/aichat/main.go
@@ -13,6 +13,7 @@ import (
zero "github.com/wdvxdr1123/ZeroBot"
"github.com/wdvxdr1123/ZeroBot/message"
+ "github.com/FloatTech/AnimeAPI/airecord"
"github.com/FloatTech/floatbox/process"
ctrl "github.com/FloatTech/zbpctrl"
"github.com/FloatTech/zbputils/chat"
@@ -29,7 +30,7 @@ var (
"- 设置AI聊天温度80\n" +
"- 设置AI聊天接口类型[OpenAI|OLLaMA|GenAI]\n" +
"- 设置AI聊天(不)支持系统提示词\n" +
- "- 设置AI聊天接口地址https://xxx\n" +
+ "- 设置AI聊天接口地址https://api.deepseek.com/chat/completions\n" +
"- 设置AI聊天密钥xxx\n" +
"- 设置AI聊天模型名xxx\n" +
"- 查看AI聊天系统提示词\n" +
@@ -38,16 +39,21 @@ var (
"- 设置AI聊天分隔符(留空则清除)\n" +
"- 设置AI聊天(不)响应AT\n" +
"- 设置AI聊天最大长度4096\n" +
- "- 设置AI聊天TopP 0.9",
+ "- 设置AI聊天TopP 0.9\n" +
+ "- 设置AI聊天(不)以AI语音输出\n" +
+ "- 查看AI聊天配置\n",
PrivateDataFolder: "aichat",
})
)
-var apitypes = map[string]uint8{
- "OpenAI": 0,
- "OLLaMA": 1,
- "GenAI": 2,
-}
+var (
+ apitypes = map[string]uint8{
+ "OpenAI": 0,
+ "OLLaMA": 1,
+ "GenAI": 2,
+ }
+ apilist = [3]string{"OpenAI", "OLLaMA", "GenAI"}
+)
func init() {
en.OnMessage(ensureconfig, func(ctx *zero.Ctx) bool {
@@ -135,10 +141,20 @@ func init() {
if t == "" {
continue
}
- if id != nil {
- id = ctx.SendChain(message.Reply(id), message.Text(t))
+ logrus.Infoln("[aichat] 回复内容:", t)
+ recCfg := airecord.GetConfig()
+ record := ""
+ if !cfg.NoRecord {
+ record = ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, t)
+ }
+ if record != "" {
+ ctx.SendChain(message.Record(record))
} else {
- id = ctx.SendChain(message.Text(t))
+ if id != nil {
+ id = ctx.SendChain(message.Reply(id), message.Text(t))
+ } else {
+ id = ctx.SendChain(message.Text(t))
+ }
}
process.SleepAbout1sTo2s()
}
@@ -269,4 +285,24 @@ func init() {
Handle(newextrasetuint(&cfg.MaxN))
en.OnPrefix("设置AI聊天TopP", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
Handle(newextrasetfloat32(&cfg.TopP))
+ en.OnRegex("^设置AI聊天(不)?以AI语音输出$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
+ Handle(newextrasetbool(&cfg.NoRecord))
+ en.OnFullMatch("查看AI聊天配置", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
+ Handle(func(ctx *zero.Ctx) {
+ c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
+ if !ok {
+ ctx.SendChain(message.Text("ERROR: no such plugin"))
+ return
+ }
+ gid := ctx.Event.GroupID
+ rate := c.GetData(gid) & 0xff
+ temp := (c.GetData(gid) >> 8) & 0xff
+ if temp <= 0 {
+ temp = 70 // default setting
+ }
+ if temp > 100 {
+ temp = 100
+ }
+ ctx.SendChain(message.Text(printConfig(rate, temp, cfg)))
+ })
}
diff --git a/plugin/airecord/record.go b/plugin/airecord/record.go
new file mode 100644
index 00000000..908784d4
--- /dev/null
+++ b/plugin/airecord/record.go
@@ -0,0 +1,134 @@
+// Package airecord 群应用:AI声聊
+package airecord
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/tidwall/gjson"
+
+ zero "github.com/wdvxdr1123/ZeroBot"
+ "github.com/wdvxdr1123/ZeroBot/message"
+
+ "github.com/FloatTech/AnimeAPI/airecord"
+ ctrl "github.com/FloatTech/zbpctrl"
+ "github.com/FloatTech/zbputils/control"
+)
+
+func init() {
+ en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{
+ DisableOnDefault: false,
+ Extra: control.ExtraFromString("airecord"),
+ Brief: "群应用:AI声聊",
+ Help: "- 设置AI语音群号1048452984(tips:机器人任意所在群聊即可)\n" +
+ "- 设置AI语音模型\n" +
+ "- 查看AI语音配置\n" +
+ "- 发送AI语音xxx",
+ PrivateDataFolder: "airecord",
+ })
+
+ en.OnPrefix("设置AI语音群号", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
+ Handle(func(ctx *zero.Ctx) {
+ u := strings.TrimSpace(ctx.State["args"].(string))
+ num, err := strconv.ParseInt(u, 10, 64)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: parse gid err: ", err))
+ return
+ }
+ err = airecord.SetCustomGID(num)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: set gid err: ", err))
+ return
+ }
+ ctx.SendChain(message.Text("设置AI语音群号为", num))
+ })
+ en.OnFullMatch("设置AI语音模型", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
+ Handle(func(ctx *zero.Ctx) {
+ next := zero.NewFutureEvent("message", 999, false, ctx.CheckSession())
+ recv, cancel := next.Repeat()
+ defer cancel()
+ jsonData := ctx.GetAICharacters(0, 1)
+
+ // 转换为字符串数组
+ var names []string
+ // 初始化两个映射表
+ nameToID := make(map[string]string)
+ nameToURL := make(map[string]string)
+ characters := jsonData.Get("#.characters")
+
+ // 遍历每个角色对象
+ characters.ForEach(func(_, group gjson.Result) bool {
+ group.ForEach(func(_, character gjson.Result) bool {
+ // 提取当前角色的三个字段
+ name := character.Get("character_name").String()
+ names = append(names, name)
+ // 存入映射表(重复名称会覆盖,保留最后出现的条目)
+ nameToID[name] = character.Get("character_id").String()
+ nameToURL[name] = character.Get("preview_url").String()
+ return true // 继续遍历
+ })
+ return true // 继续遍历
+ })
+ var builder strings.Builder
+ // 写入开头文本
+ builder.WriteString("请选择语音模型序号:\n")
+
+ // 遍历names数组,拼接序号和名称
+ for i, v := range names {
+ // 将数字转换为字符串(不依赖fmt)
+ numStr := strconv.Itoa(i)
+ // 拼接格式:"序号. 名称\n"
+ builder.WriteString(numStr)
+ builder.WriteString(". ")
+ builder.WriteString(v)
+ builder.WriteString("\n")
+ }
+ // 获取最终字符串
+ ctx.SendChain(message.Text(builder.String()))
+ for {
+ select {
+ case <-time.After(time.Second * 120):
+ ctx.SendChain(message.Text("设置AI语音模型指令过期"))
+ return
+ case ct := <-recv:
+ msg := ct.Event.Message.ExtractPlainText()
+ num, err := strconv.Atoi(msg)
+ if err != nil {
+ ctx.SendChain(message.Text("请输入数字!"))
+ continue
+ }
+ if num < 0 || num >= len(names) {
+ ctx.SendChain(message.Text("序号非法!"))
+ continue
+ }
+ err = airecord.SetRecordModel(names[num], nameToID[names[num]])
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: set model err: ", err))
+ continue
+ }
+ ctx.SendChain(message.Text("已选择语音模型: ", names[num]))
+ ctx.SendChain(message.Record(nameToURL[names[num]]))
+ return
+ }
+ }
+ })
+ en.OnFullMatch("查看AI语音配置", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
+ Handle(func(ctx *zero.Ctx) {
+ ctx.SendChain(message.Text(airecord.PrintRecordConfig()))
+ })
+ en.OnPrefix("发送AI语音", zero.UserOrGrpAdmin).SetBlock(true).
+ Handle(func(ctx *zero.Ctx) {
+ u := strings.TrimSpace(ctx.State["args"].(string))
+ recCfg := airecord.GetConfig()
+ record := ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, u)
+ if record == "" {
+ id := ctx.SendGroupAIRecord(recCfg.ModelID, ctx.Event.GroupID, u)
+ if id == "" {
+ ctx.SendChain(message.Text("ERROR: get record err: empty record"))
+ return
+ }
+ }
+ ctx.SendChain(message.Record(record))
+ })
+}
diff --git a/plugin/bilibili/bilibili_parse.go b/plugin/bilibili/bilibili_parse.go
index b094e0e8..189c6586 100644
--- a/plugin/bilibili/bilibili_parse.go
+++ b/plugin/bilibili/bilibili_parse.go
@@ -163,7 +163,12 @@ func handleArticle(ctx *zero.Ctx) {
}
func handleLive(ctx *zero.Ctx) {
- card, err := bz.GetLiveRoomInfo(ctx.State["regex_matched"].([]string)[1])
+ cookie, err := cfg.Load()
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ card, err := bz.GetLiveRoomInfo(ctx.State["regex_matched"].([]string)[1], cookie)
if err != nil {
ctx.SendChain(message.Text("ERROR: ", err))
return
diff --git a/plugin/bilibili/card2msg_test.go b/plugin/bilibili/card2msg_test.go
index 5c43c849..5d85d775 100644
--- a/plugin/bilibili/card2msg_test.go
+++ b/plugin/bilibili/card2msg_test.go
@@ -47,7 +47,7 @@ func TestVideoInfo(t *testing.T) {
}
func TestLiveRoomInfo(t *testing.T) {
- card, err := bz.GetLiveRoomInfo("83171")
+ card, err := bz.GetLiveRoomInfo("83171", "b_ut=7;buvid3=0;i-wanna-go-back=-1;innersign=0;")
if err != nil {
t.Fatal(err)
}
diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go
index f1a6f788..2521412b 100644
--- a/plugin/manager/manager.go
+++ b/plugin/manager/manager.go
@@ -50,6 +50,7 @@ const (
"- 列出所有提醒\n" +
"- 翻牌\n" +
"- 赞我\n" +
+ "- 群签到\n" +
"- 对信息回复: 回应表情 [表情]\n" +
"- 设置欢迎语XXX 可选添加 [{at}] [{nickname}] [{avatar}] [{uid}] [{gid}] [{groupname}]\n" +
"- 测试欢迎语\n" +
@@ -405,6 +406,12 @@ func init() { // 插件主体
ctx.SendLike(ctx.Event.UserID, 10)
ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("给你赞了10下哦,记得回我~"))
})
+ // 群签到
+ engine.OnFullMatch("群签到", zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByUser).
+ Handle(func(ctx *zero.Ctx) {
+ ctx.SetGroupSign(ctx.Event.GroupID)
+ ctx.SendChain(message.Text("群签到成功,可在手机端输入框中的打卡查看"))
+ })
facere := regexp.MustCompile(`\[CQ:face,id=(\d+)\]`)
// 给消息回应表情
engine.OnRegex(`^\[CQ:reply,id=(-?\d+)\].*回应表情\s*(.+)\s*$`, zero.AdminPermission, zero.OnlyGroup).SetBlock(true).