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
10 changed files with 267 additions and 20 deletions

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{
"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)))
})
}

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