// Package aichat OpenAI聊天和群聊总结 package aichat import ( "errors" "math/rand" "strconv" "strings" "time" "github.com/fumiama/deepinfra" "github.com/fumiama/deepinfra/model" goba "github.com/fumiama/go-onebot-agent" "github.com/sirupsen/logrus" "github.com/tidwall/gjson" 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" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" ) var ( // en data [8 temp] [8 rate] LSB en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, Extra: control.ExtraFromString("aichat"), Brief: "OpenAI聊天", Help: "- 设置AI聊天触发概率10\n" + "- 设置AI聊天温度80\n" + "- 设置AI聊天接口类型[OpenAI|OLLaMA|GenAI]\n" + "- 设置AI聊天(不)使用Agent模式\n" + "- 设置AI聊天(不)支持系统提示词\n" + "- 设置AI聊天接口地址https://api.siliconflow.cn/v1/chat/completions\n" + "- 设置AI聊天密钥xxx\n" + "- 设置AI聊天模型名Qwen/Qwen3-8B\n" + "- 查看AI聊天系统提示词\n" + "- 重置AI聊天系统提示词\n" + "- 设置AI聊天系统提示词xxx\n" + "- 设置AI聊天分隔符(留空则清除)\n" + "- 设置AI聊天(不)响应AT\n" + "- 设置AI聊天最大长度4096\n" + "- 设置AI聊天TopP 0.9\n" + "- 设置AI聊天(不)以AI语音输出\n" + "- 查看AI聊天配置\n" + "- 重置AI聊天\n" + "- 群聊总结 [消息数目]|群聊总结 1000\n" + "- /gpt [内容] (使用大模型聊天)\n", PrivateDataFolder: "aichat", }) ) var ( apitypes = map[string]uint8{ "OpenAI": 0, "OLLaMA": 1, "GenAI": 2, } apilist = [3]string{"OpenAI", "OLLaMA", "GenAI"} limit = ctxext.NewLimiterManager(time.Second*30, 1) ) // getModelParams 获取模型参数:温度(float32(temp)/100)、TopP和最大长度 func getModelParams(temp int64) (temperature float32, topp float32, maxn uint) { // 处理温度参数 if temp <= 0 { temp = 70 // default setting } if temp > 100 { temp = 100 } temperature = float32(temp) / 100 // 处理TopP参数 topp = cfg.TopP if topp == 0 { topp = 0.9 } // 处理最大长度参数 maxn = cfg.MaxN if maxn == 0 { maxn = 4096 } return temperature, topp, maxn } func init() { en.OnMessage(ensureconfig, func(ctx *zero.Ctx) bool { return ctx.ExtractPlainText() != "" && (!cfg.NoReplyAT || (cfg.NoReplyAT && !ctx.Event.IsToMe)) }).SetBlock(false).Handle(func(ctx *zero.Ctx) { gid := ctx.Event.GroupID if gid == 0 { gid = -ctx.Event.UserID } c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) if !ok { return } rate := c.GetData(gid) temp := (rate >> 8) & 0xff rate &= 0xff if !ctx.Event.IsToMe && rand.Intn(100) >= int(rate) { return } if ctx.Event.IsToMe { ctx.Block() } if cfg.Key == "" { logrus.Warnln("ERROR: get extra err: empty key") return } temperature, topp, maxn := getModelParams(temp) x := deepinfra.NewAPI(cfg.API, cfg.Key) var mod model.Protocol switch cfg.Type { case 0: mod = model.NewOpenAI( cfg.ModelName, cfg.Separator, temperature, topp, maxn, ) case 1: mod = model.NewOLLaMA( cfg.ModelName, cfg.Separator, temperature, topp, maxn, ) case 2: mod = model.NewGenAI( cfg.ModelName, temperature, topp, maxn, ) default: logrus.Warnln("[aichat] unsupported AI type", cfg.Type) return } if !cfg.NoAgent { role := goba.PermRoleUser if zero.AdminPermission(ctx) { role = goba.PermRoleAdmin if zero.SuperUserPermission(ctx) { role = goba.PermRoleOwner } } reqs, err := chat.AgentOf(ctx.Event.SelfID).GetAction(x, mod, gid, role, false) if err != nil { logrus.Warnln("[aichat] agent err:", err, reqs) return } logrus.Infoln("[aichat] agent do:", reqs) for _, req := range reqs { if req.Action == "send_group_msg" { gid, ok := req.Params["group_id"].(int64) if !ok { logrus.Warnln("[aichat] invalid", req.Action, req.Params) continue } if ctx.Event.GroupID != gid && !zero.SuperUserPermission(ctx) { logrus.Warnln("[aichat] refuse to send out of grp from", ctx.Event.GroupID, "to", gid) continue } } ctx.CallAction(req.Action, req.Params) process.SleepAbout1sTo2s() } return } data, err := x.Request(chat.GetChatContext(mod, gid, cfg.SystemP, cfg.NoSystemP)) if err != nil { logrus.Warnln("[aichat] post err:", err) return } txt := chat.Sanitize(strings.Trim(data, "\n  ")) if len(txt) > 0 { chat.AddChatReply(gid, txt) nick := zero.BotConfig.NickName[rand.Intn(len(zero.BotConfig.NickName))] txt = strings.ReplaceAll(txt, "{name}", ctx.CardOrNickName(ctx.Event.UserID)) txt = strings.ReplaceAll(txt, "{me}", nick) id := any(nil) if ctx.Event.IsToMe { id = ctx.Event.MessageID } for _, t := range strings.Split(txt, "{segment}") { 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() } } }) en.OnPrefix("设置AI聊天触发概率", zero.AdminPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { args := strings.TrimSpace(ctx.State["args"].(string)) if args == "" { ctx.SendChain(message.Text("ERROR: empty args")) return } c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) if !ok { ctx.SendChain(message.Text("ERROR: no such plugin")) return } r, err := strconv.Atoi(args) if err != nil { ctx.SendChain(message.Text("ERROR: parse rate err: ", err)) return } if r > 100 { r = 100 } else if r < 0 { r = 0 } gid := ctx.Event.GroupID if gid == 0 { gid = -ctx.Event.UserID } val := c.GetData(gid) & (^0xff) err = c.SetData(gid, val|int64(r&0xff)) if err != nil { ctx.SendChain(message.Text("ERROR: set data err: ", err)) return } ctx.SendChain(message.Text("成功")) }) en.OnPrefix("设置AI聊天温度", zero.AdminPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { args := strings.TrimSpace(ctx.State["args"].(string)) if args == "" { ctx.SendChain(message.Text("ERROR: empty args")) return } c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) if !ok { ctx.SendChain(message.Text("ERROR: no such plugin")) return } r, err := strconv.Atoi(args) if err != nil { ctx.SendChain(message.Text("ERROR: parse rate err: ", err)) return } if r > 100 { r = 100 } else if r < 0 { r = 0 } gid := ctx.Event.GroupID if gid == 0 { gid = -ctx.Event.UserID } val := c.GetData(gid) & (^0xff00) err = c.SetData(gid, val|(int64(r&0xff)<<8)) if err != nil { ctx.SendChain(message.Text("ERROR: set data err: ", err)) return } ctx.SendChain(message.Text("成功")) }) en.OnPrefix("设置AI聊天接口类型", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { args := strings.TrimSpace(ctx.State["args"].(string)) if args == "" { ctx.SendChain(message.Text("ERROR: empty args")) return } c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) if !ok { ctx.SendChain(message.Text("ERROR: no such plugin")) return } typ, ok := apitypes[args] if !ok { ctx.SendChain(message.Text("ERROR: 未知类型 ", args)) return } cfg.Type = int(typ) err := c.SetExtra(&cfg) if err != nil { ctx.SendChain(message.Text("ERROR: set extra err: ", err)) return } ctx.SendChain(message.Text("成功")) }) en.OnPrefix("设置AI聊天接口地址", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). Handle(newextrasetstr(&cfg.API)) en.OnPrefix("设置AI聊天密钥", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). Handle(newextrasetstr(&cfg.Key)) en.OnPrefix("设置AI聊天模型名", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). Handle(newextrasetstr(&cfg.ModelName)) en.OnPrefix("设置AI聊天系统提示词", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). Handle(newextrasetstr(&cfg.SystemP)) en.OnFullMatch("查看AI聊天系统提示词", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { ctx.SendChain(message.Text(cfg.SystemP)) }) 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 } cfg.SystemP = chat.SystemPrompt err := c.SetExtra(&cfg) if err != nil { ctx.SendChain(message.Text("ERROR: set extra err: ", err)) return } ctx.SendChain(message.Text("成功")) }) en.OnPrefix("设置AI聊天分隔符", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). Handle(newextrasetstr(&cfg.Separator)) en.OnRegex("^设置AI聊天(不)?响应AT$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). Handle(newextrasetbool(&cfg.NoReplyAT)) en.OnRegex("^设置AI聊天(不)?支持系统提示词$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). Handle(newextrasetbool(&cfg.NoSystemP)) en.OnRegex("^设置AI聊天(不)?使用Agent模式$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). Handle(newextrasetbool(&cfg.NoAgent)) en.OnPrefix("设置AI聊天最大长度", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). 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))) }) en.OnFullMatch("重置AI聊天", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { chat.ResetChat() ctx.SendChain(message.Text("成功")) }) // 添加群聊总结功能 en.OnRegex(`^群聊总结\s?(\d*)$`, ensureconfig, zero.OnlyGroup, zero.AdminPermission).SetBlock(true).Limit(limit.LimitByGroup).Handle(func(ctx *zero.Ctx) { ctx.SendChain(message.Text("少女思考中...")) gid := ctx.Event.GroupID if gid == 0 { gid = -ctx.Event.UserID } c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) if !ok { return } rate := c.GetData(gid) temp := (rate >> 8) & 0xff p, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[1], 10, 64) if p > 1000 { p = 1000 } if p == 0 { p = 200 } group := ctx.GetGroupInfo(gid, false) if group.MemberCount == 0 { ctx.SendChain(message.Text(zero.BotConfig.NickName[0], "未加入", group.Name, "(", gid, "),无法获取总结")) return } var messages []string h := ctx.GetGroupMessageHistory(gid, 0, p, false) h.Get("messages").ForEach(func(_, msgObj gjson.Result) bool { nickname := msgObj.Get("sender.nickname").Str text := strings.TrimSpace(message.ParseMessageFromString(msgObj.Get("raw_message").Str).ExtractPlainText()) if text != "" { messages = append(messages, nickname+": "+text) } return true }) if len(messages) == 0 { ctx.SendChain(message.Text("ERROR: 历史消息为空或者无法获得历史消息")) return } // 构造总结请求提示 summaryPrompt := "请总结这个群聊内容,要求按发言顺序梳理,明确标注每个发言者的昵称,并完整呈现其核心观点、提出的问题、发表的看法或做出的回应,确保不遗漏关键信息,且能体现成员间的对话逻辑和互动关系:\n" + strings.Join(messages, "\n") // 调用大模型API进行总结 summary, err := llmchat(summaryPrompt, temp) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) return } var b strings.Builder b.WriteString("群 ") b.WriteString(group.Name) b.WriteByte('(') b.WriteString(strconv.FormatInt(gid, 10)) b.WriteString(") 的 ") b.WriteString(strconv.FormatInt(p, 10)) b.WriteString(" 条消息总结:\n\n") b.WriteString(summary) // 分割总结内容为多段(按1000字符长度切割) summaryText := b.String() msg := make(message.Message, 0) for len(summaryText) > 0 { if len(summaryText) <= 1000 { msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(summaryText))) break } // 查找1000字符内的最后一个换行符,尽量在换行处分割 chunk := summaryText[:1000] lastNewline := strings.LastIndex(chunk, "\n") if lastNewline > 0 { chunk = summaryText[:lastNewline+1] } msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(chunk))) summaryText = summaryText[len(chunk):] } if len(msg) > 0 { ctx.Send(msg) } }) // 添加 /gpt 命令处理(同时支持回复消息和直接使用) en.OnKeyword("/gpt", ensureconfig).SetBlock(true).Handle(func(ctx *zero.Ctx) { gid := ctx.Event.GroupID if gid == 0 { gid = -ctx.Event.UserID } c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) if !ok { return } rate := c.GetData(gid) temp := (rate >> 8) & 0xff text := ctx.MessageString() var query string var replyContent string // 检查是否是回复消息 (使用MessageElement检查而不是CQ码) for _, elem := range ctx.Event.Message { if elem.Type == "reply" { // 提取被回复的消息ID replyIDStr := elem.Data["id"] replyID, err := strconv.ParseInt(replyIDStr, 10, 64) if err == nil { // 获取被回复的消息内容 replyMsg := ctx.GetMessage(replyID) if replyMsg.Elements != nil { replyContent = replyMsg.Elements.ExtractPlainText() } } break // 找到回复元素后退出循环 } } // 提取 /gpt 后面的内容 parts := strings.SplitN(text, "/gpt", 2) var gContent string if len(parts) > 1 { gContent = strings.TrimSpace(parts[1]) } // 组合内容:优先使用回复内容,如果同时有/gpt内容则拼接 switch { case replyContent != "" && gContent != "": query = replyContent + "\n" + gContent case replyContent != "": query = replyContent case gContent != "": query = gContent default: return } // 调用大模型API进行聊天 reply, err := llmchat(query, temp) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) return } // 分割总结内容为多段(按1000字符长度切割) msg := make(message.Message, 0) for len(reply) > 0 { if len(reply) <= 1000 { msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(reply))) break } // 查找1000字符内的最后一个换行符,尽量在换行处分割 chunk := reply[:1000] lastNewline := strings.LastIndex(chunk, "\n") if lastNewline > 0 { chunk = reply[:lastNewline+1] } msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(chunk))) reply = reply[len(chunk):] } if len(msg) > 0 { ctx.Send(msg) } }) } // llmchat 调用大模型API包装 func llmchat(prompt string, temp int64) (string, error) { temperature, topp, maxn := getModelParams(temp) // 使用默认温度70 x := deepinfra.NewAPI(cfg.API, cfg.Key) var mod model.Protocol switch cfg.Type { case 0: mod = model.NewOpenAI( cfg.ModelName, cfg.Separator, temperature, topp, maxn, ) case 1: mod = model.NewOLLaMA( cfg.ModelName, cfg.Separator, temperature, topp, maxn, ) case 2: mod = model.NewGenAI( cfg.ModelName, temperature, topp, maxn, ) default: logrus.Warnln("[aichat] unsupported AI type", cfg.Type) return "", errors.New("不支持的AI类型") } data, err := x.Request(mod.User(prompt)) if err != nil { return "", err } return strings.TrimSpace(data), nil }