Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugin/tarot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
## 触发方式

- [x] 抽[塔罗牌|大阿卡纳|小阿卡纳]
- [x] 抽[塔罗牌|大阿卡纳|小阿卡纳] [询问的事情](追加大模型解析)
- [x] 抽n张[塔罗牌|大阿卡纳|小阿卡纳]
- [x] 解塔罗牌[牌名]
- [x] [塔罗|大阿卡纳|小阿卡纳|混合]牌阵[圣三角|时间之流|四要素|五牌阵|吉普赛十字|马蹄|六芒星]
- [x] [塔罗|大阿卡纳|小阿卡纳|混合]牌阵[牌阵名] [询问的事情](追加大模型解析)

只有在单张抽牌或牌阵命令后附带询问内容时,塔罗插件才会复用 AI 聊天配置调用大模型。

## 致谢

Expand Down
173 changes: 167 additions & 6 deletions plugin/tarot/tarot.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import (
"github.com/FloatTech/floatbox/binary"
fcext "github.com/FloatTech/floatbox/ctxext"
ctrl "github.com/FloatTech/zbpctrl"
"github.com/FloatTech/zbputils/chat"
"github.com/FloatTech/zbputils/control"
"github.com/FloatTech/zbputils/ctxext"
"github.com/FloatTech/zbputils/img/text"
"github.com/fumiama/deepinfra"
"github.com/fumiama/deepinfra/model"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
zero "github.com/wdvxdr1123/ZeroBot"
"github.com/wdvxdr1123/ZeroBot/message"
Expand All @@ -35,6 +39,14 @@ type formation struct {
IsCut bool `json:"is_cut"`
Represent [][]string `json:"represent"`
}

type drawResult struct {
Name string
Position string
Description string
Represent string
}

type cardSet = map[string]card

var (
Expand All @@ -52,10 +64,10 @@ func init() {
engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{
DisableOnDefault: false,
Brief: "塔罗牌",
Help: "- 抽[塔罗牌|大阿卡纳|小阿卡纳]\n" +
Help: "- 抽[塔罗牌|大阿卡纳|小阿卡纳] [询问的事情]\n" +
"- 抽n张[塔罗牌|大阿卡纳|小阿卡纳]\n" +
"- 解塔罗牌[牌名]\n" +
"- [塔罗|大阿卡纳|小阿卡纳|混合]牌阵[圣三角|时间之流|四要素|五牌阵|吉普赛十字|马蹄|六芒星]",
"- [塔罗|大阿卡纳|小阿卡纳|混合]牌阵[圣三角|时间之流|四要素|五牌阵|吉普赛十字|马蹄|六芒星] [询问的事情]",
PublicDataFolder: "Tarot",
}).ApplySingle(ctxext.DefaultSingle)

Expand Down Expand Up @@ -112,9 +124,10 @@ func init() {
logrus.Infof("[tarot]读取%d组塔罗牌阵", len(formationMap))
return true
})
engine.OnRegex(`^抽(\d{1,2}张)?((塔罗牌|大阿(尔)?卡纳)|小阿(尔)?卡纳)$`, getTarot).SetBlock(true).Limit(ctxext.LimitByGroup).Handle(func(ctx *zero.Ctx) {
engine.OnRegex(`^抽(\d{1,2}张)?((塔罗牌|大阿(尔)?卡纳)|小阿(尔)?卡纳)\s?(.*)$`, getTarot).SetBlock(true).Limit(ctxext.LimitByGroup).Handle(func(ctx *zero.Ctx) {
match := ctx.State["regex_matched"].([]string)[1]
cardType := ctx.State["regex_matched"].([]string)[2]
question, withQuestion := splitSingleCardQuestion(match, ctx.State["regex_matched"].([]string)[6])
n := 1
reasons := [...]string{"您抽到的是~\n", "锵锵锵,塔罗牌的预言是~\n", "诶,让我看看您抽到了~\n"}
position := [...]string{"『正位』", "『逆位』"}
Expand Down Expand Up @@ -155,10 +168,24 @@ func init() {
// ctx.SendChain(message.Text("ERROR: ", err))
logrus.Infof("[tarot]获取图片失败: %v", err)
ctx.SendChain(message.Text(reasons[rand.Intn(len(reasons))], position[p], "的『", name, "』\n其释义为: ", description))
if withQuestion {
sendTarotAnalysis(ctx, question, "", []drawResult{{
Name: name,
Position: position[p],
Description: description,
}})
}
return
}
ctx.SendChain(message.ImageBytes(data))
ctx.SendChain(message.Text(reasons[rand.Intn(len(reasons))], position[p], "的『", name, "』\n其释义为: ", description))
if withQuestion {
Comment thread
shudorcl marked this conversation as resolved.
Outdated
sendTarotAnalysis(ctx, question, "", []drawResult{{
Name: name,
Position: position[p],
Description: description,
}})
}
return
}
msg := make(message.Message, n)
Expand Down Expand Up @@ -238,8 +265,9 @@ func init() {
})
engine.OnRegex(`^((塔罗|大阿(尔)?卡纳)|小阿(尔)?卡纳|混合)牌阵\s?(.*)`, getTarot).SetBlock(true).Limit(ctxext.LimitByGroup).Handle(func(ctx *zero.Ctx) {
cardType := ctx.State["regex_matched"].([]string)[1]
match := ctx.State["regex_matched"].([]string)[5]
info, ok := formationMap[match]
rawMatch := ctx.State["regex_matched"].([]string)[5]
match, question, ok := splitFormationQuestion(rawMatch, formationMap)
info := formationMap[match]
position := [...]string{"『正位』", "『逆位』"}
reverse := [...]string{"", "Reverse/"}
start, length := 0, 22
Expand All @@ -258,6 +286,7 @@ func init() {
build.WriteString(match)
build.WriteString("\n")
msg := make(message.Message, info.CardsNum+1)
results := make([]drawResult, 0, info.CardsNum)
randomIntMap := make(map[int]int, 30)
for i := 0; i < info.CardsNum; i++ {
j := rand.Intn(length)
Expand Down Expand Up @@ -295,6 +324,12 @@ func init() {
build.WriteString("』\n其释义为: \n")
build.WriteString(description)
build.WriteString("\n")
results = append(results, drawResult{
Name: name,
Position: position[p],
Description: description,
Represent: info.Represent[0][i],
})
msg[i] = ctxext.FakeSenderForwardNode(ctx, tarotmsg...)
}
txt := build.String()
Expand All @@ -307,8 +342,134 @@ func init() {
if id := ctx.Send(msg).ID(); id == 0 {
ctx.SendChain(message.Text("ERROR: 可能被风控了"))
}
if question != "" {
Comment thread
shudorcl marked this conversation as resolved.
Outdated
sendTarotAnalysis(ctx, question, match, results)
}
} else {
ctx.SendChain(message.Text("没有找到", match, "噢~\n现有牌阵列表: \n", strings.Join(formationName, "\n")))
ctx.SendChain(message.Text("没有找到", rawMatch, "噢~\n现有牌阵列表: \n", strings.Join(formationName, "\n")))
}
})
}

func splitSingleCardQuestion(drawCount, question string) (string, bool) {
Comment thread
shudorcl marked this conversation as resolved.
Outdated
if drawCount != "" {
return "", false
}
question = strings.TrimSpace(question)
return question, question != ""
}

func splitFormationQuestion(raw string, formations map[string]formation) (string, string, bool) {
Comment thread
shudorcl marked this conversation as resolved.
Outdated
raw = strings.TrimSpace(raw)
var match string
for name := range formations {
if strings.HasPrefix(raw, name) && len(name) > len(match) {
match = name
}
}
if match == "" {
return "", "", false
}
return match, strings.TrimSpace(strings.TrimPrefix(raw, match)), true
}

func buildTarotPrompt(question, formationName string, draws []drawResult) string {
Comment thread
shudorcl marked this conversation as resolved.
Outdated
var build strings.Builder
build.WriteString("你是一位谨慎的塔罗牌解读者。请围绕用户的问题和本次牌面解读,先说明牌面,再给出综合建议,字数控制在300-500字。")
build.WriteString("不要把占卜结果表述为确定事实。\n")
build.WriteString("用户问题: ")
build.WriteString(question)
build.WriteByte('\n')
if formationName != "" {
build.WriteString("牌阵: ")
build.WriteString(formationName)
build.WriteByte('\n')
}
build.WriteString("牌面:\n")
for i, draw := range draws {
build.WriteString(strconv.Itoa(i + 1))
build.WriteString(". ")
if draw.Represent != "" {
build.WriteString("牌位: ")
build.WriteString(draw.Represent)
build.WriteString("; ")
}
build.WriteString("牌: ")
build.WriteString(draw.Name)
build.WriteString("; 方位: ")
build.WriteString(draw.Position)
build.WriteString("; 固定释义: ")
build.WriteString(draw.Description)
build.WriteByte('\n')
}
return build.String()
}

func sendTarotAnalysis(ctx *zero.Ctx, question, formationName string, draws []drawResult) {
Comment thread
shudorcl marked this conversation as resolved.
Outdated
reply, err := requestTarotAnalysis(ctx, buildTarotPrompt(question, formationName, draws))
if err != nil {
logrus.Warnln("[tarot]大模型解析失败:", err)
ctx.SendChain(message.Text("塔罗解析失败: ", err))
return
}
if reply == "" {
ctx.SendChain(message.Text("塔罗解析失败: 大模型返回为空"))
return
}
chunks := buildTarotAnalysisChunks(reply, 1000)
msg := make(message.Message, 0, len(chunks))
for _, chunk := range chunks {
msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(chunk)))
}
if id := ctx.Send(msg).ID(); id == 0 {
ctx.SendChain(message.Text("ERROR: 可能被风控了"))
}
}

func requestTarotAnalysis(ctx *zero.Ctx, prompt string) (string, error) {
if !chat.EnsureConfig(ctx) {
return "", errors.New("无法读取 AI 聊天配置")
}

gid := ctx.Event.GroupID
if gid == 0 {
gid = -ctx.Event.UserID
}
stor, err := chat.NewStorage(ctx, gid)
if err != nil {
return "", errors.Wrap(err, "读取 AI 聊天温度配置失败")
}

topp, maxn := chat.AC.MParams()
mod, err := chat.AC.Type.Protocol(chat.AC.ModelName, stor.Temp(), topp, maxn, chat.AC.ReasoningEffort)
if err != nil {
return "", errors.Wrap(err, "创建 AI 模型协议失败")
}

api := deepinfra.NewAPI(chat.AC.API, string(chat.AC.Key))
data, err := api.Request(mod.User(model.NewContentText(prompt)))
if err != nil {
return "", errors.Wrap(err, "请求 AI 模型失败")
}
return strings.TrimSpace(data), nil
}

func buildTarotAnalysisChunks(reply string, maxRunes int) []string {
return splitTextChunks("塔罗解析:\n"+reply, maxRunes)
}

func splitTextChunks(txt string, maxRunes int) []string {
runes := []rune(txt)
if maxRunes <= 0 || len(runes) <= maxRunes {
return []string{txt}
}
chunks := make([]string, 0, (len(runes)+maxRunes-1)/maxRunes)
for len(runes) > maxRunes {
chunks = append(chunks, string(runes[:maxRunes]))
runes = runes[maxRunes:]
}
if len(runes) > 0 {
chunks = append(chunks, string(runes))
}
return chunks
}
Loading
Loading