feat: airecord (#1180)

Co-authored-by: 源文雨 <41315874+fumiama@users.noreply.github.com>
This commit is contained in:
himawari 2025-07-20 13:31:06 +08:00 committed by GitHub
parent cb0ffa0c17
commit 617d4f50a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 267 additions and 20 deletions

View File

@ -256,6 +256,8 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w]
- [x] 赞我
- [x] 群签到
- [x] [开启 | 关闭]入群验证
- [x] [开启 | 关闭]gist加群自动审批
@ -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} 是当前群群名
</details>
<details>
<summary>群应用AI声聊</summary>
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/airecord"`
- [x] 设置AI语音群号1048452984(tips机器人任意所在群聊即可)
- [x] 设置AI语音模型
- [x] 查看AI语音配置
- [x] 发送AI语音xxx
</details>
<details>
<summary>定时指令触发器</summary>
@ -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聊天配置
</details>
<details>

4
go.mod
View File

@ -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

8
go.sum
View File

@ -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=

View File

@ -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" // 群管

View File

@ -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 "否"
}

View File

@ -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聊天分隔符</think>(留空则清除)\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{
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,11 +141,21 @@ func init() {
if t == "" {
continue
}
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 {
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)))
})
}

134
plugin/airecord/record.go Normal file
View File

@ -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))
})
}

View File

@ -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

View File

@ -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)
}

View File

@ -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).