Skip to content

Commit f44c83b

Browse files
authored
feat: 抽塔罗牌追加LLM解析 (#1310)
1 parent d60d1bb commit f44c83b

3 files changed

Lines changed: 209 additions & 9 deletions

File tree

plugin/tarot/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
## 触发方式
66

77
- [x][塔罗牌|大阿卡纳|小阿卡纳]
8+
- [x][塔罗牌|大阿卡纳|小阿卡纳] [询问的事情](追加大模型解析)
89
- [x] 抽n张[塔罗牌|大阿卡纳|小阿卡纳]
910
- [x] 解塔罗牌[牌名]
1011
- [x] [塔罗|大阿卡纳|小阿卡纳|混合]牌阵[圣三角|时间之流|四要素|五牌阵|吉普赛十字|马蹄|六芒星]
12+
- [x] [塔罗|大阿卡纳|小阿卡纳|混合]牌阵[牌阵名] [询问的事情](追加大模型解析)
13+
14+
只有在单张抽牌或牌阵命令后附带询问内容时,塔罗插件才会复用 AI 聊天配置调用大模型。
1115

1216
## 致谢
1317

plugin/tarot/tarot.go

Lines changed: 178 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import (
1212
"github.com/FloatTech/floatbox/binary"
1313
fcext "github.com/FloatTech/floatbox/ctxext"
1414
ctrl "github.com/FloatTech/zbpctrl"
15+
"github.com/FloatTech/zbputils/chat"
1516
"github.com/FloatTech/zbputils/control"
1617
"github.com/FloatTech/zbputils/ctxext"
1718
"github.com/FloatTech/zbputils/img/text"
19+
"github.com/fumiama/deepinfra"
20+
"github.com/fumiama/deepinfra/model"
21+
"github.com/pkg/errors"
1822
"github.com/sirupsen/logrus"
1923
zero "github.com/wdvxdr1123/ZeroBot"
2024
"github.com/wdvxdr1123/ZeroBot/message"
@@ -35,6 +39,16 @@ type formation struct {
3539
IsCut bool `json:"is_cut"`
3640
Represent [][]string `json:"represent"`
3741
}
42+
43+
type drawResult struct {
44+
Name string
45+
Position string
46+
Description string
47+
Represent string
48+
}
49+
50+
type drawResults []drawResult
51+
3852
type cardSet = map[string]card
3953

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

@@ -112,9 +126,11 @@ func init() {
112126
logrus.Infof("[tarot]读取%d组塔罗牌阵", len(formationMap))
113127
return true
114128
})
115-
engine.OnRegex(`^抽(\d{1,2}张)?((塔罗牌|大阿(尔)?卡纳)|小阿(尔)?卡纳)$`, getTarot).SetBlock(true).Limit(ctxext.LimitByGroup).Handle(func(ctx *zero.Ctx) {
129+
engine.OnRegex(`^抽(\d{1,2}张)?((塔罗牌|大阿(尔)?卡纳)|小阿(尔)?卡纳)\s?(.*)$`, getTarot).SetBlock(true).Limit(ctxext.LimitByGroup).Handle(func(ctx *zero.Ctx) {
116130
match := ctx.State["regex_matched"].([]string)[1]
117131
cardType := ctx.State["regex_matched"].([]string)[2]
132+
question := strings.TrimSpace(ctx.State["regex_matched"].([]string)[6])
133+
withQuestion := match == "" && question != ""
118134
n := 1
119135
reasons := [...]string{"您抽到的是~\n", "锵锵锵,塔罗牌的预言是~\n", "诶,让我看看您抽到了~\n"}
120136
position := [...]string{"『正位』", "『逆位』"}
@@ -154,11 +170,47 @@ func init() {
154170
if err != nil {
155171
// ctx.SendChain(message.Text("ERROR: ", err))
156172
logrus.Infof("[tarot]获取图片失败: %v", err)
157-
ctx.SendChain(message.Text(reasons[rand.Intn(len(reasons))], position[p], "的『", name, "』\n其释义为: ", description))
158-
return
173+
} else {
174+
ctx.SendChain(message.ImageBytes(data))
159175
}
160-
ctx.SendChain(message.ImageBytes(data))
161176
ctx.SendChain(message.Text(reasons[rand.Intn(len(reasons))], position[p], "的『", name, "』\n其释义为: ", description))
177+
if !withQuestion {
178+
return
179+
}
180+
if !chat.EnsureConfig(ctx) {
181+
ctx.SendChain(message.Text("塔罗解析失败: 无法读取 AI 聊天配置"))
182+
return
183+
}
184+
gid := ctx.Event.GroupID
185+
if gid == 0 {
186+
gid = -ctx.Event.UserID
187+
}
188+
stor, err := chat.NewStorage(ctx, gid)
189+
if err != nil {
190+
ctx.SendChain(message.Text("塔罗解析失败: ", errors.Wrap(err, "读取 AI 聊天温度配置失败")))
191+
return
192+
}
193+
reply, err := drawResults{{
194+
Name: name,
195+
Position: position[p],
196+
Description: description,
197+
}}.analyze(question, "", stor.Temp())
198+
if err != nil {
199+
logrus.Warnln("[tarot]大模型解析失败:", err)
200+
ctx.SendChain(message.Text("塔罗解析失败: ", err))
201+
return
202+
}
203+
if reply == "" {
204+
ctx.SendChain(message.Text("塔罗解析失败: 大模型返回为空"))
205+
return
206+
}
207+
if id := ctx.Send(makeNodeMessage(
208+
reply,
209+
ctx.CardOrNickName(ctx.Event.UserID),
210+
ctx.Event.UserID,
211+
)).ID(); id == 0 {
212+
ctx.SendChain(message.Text("ERROR: 可能被风控了"))
213+
}
162214
return
163215
}
164216
msg := make(message.Message, n)
@@ -238,8 +290,16 @@ func init() {
238290
})
239291
engine.OnRegex(`^((塔罗|大阿(尔)?卡纳)|小阿(尔)?卡纳|混合)牌阵\s?(.*)`, getTarot).SetBlock(true).Limit(ctxext.LimitByGroup).Handle(func(ctx *zero.Ctx) {
240292
cardType := ctx.State["regex_matched"].([]string)[1]
241-
match := ctx.State["regex_matched"].([]string)[5]
242-
info, ok := formationMap[match]
293+
rawMatch := strings.TrimSpace(ctx.State["regex_matched"].([]string)[5])
294+
var match string
295+
for name := range formationMap {
296+
if strings.HasPrefix(rawMatch, name) && len(name) > len(match) {
297+
match = name
298+
}
299+
}
300+
question := strings.TrimSpace(strings.TrimPrefix(rawMatch, match))
301+
_, ok := formationMap[match]
302+
info := formationMap[match]
243303
position := [...]string{"『正位』", "『逆位』"}
244304
reverse := [...]string{"", "Reverse/"}
245305
start, length := 0, 22
@@ -258,6 +318,7 @@ func init() {
258318
build.WriteString(match)
259319
build.WriteString("\n")
260320
msg := make(message.Message, info.CardsNum+1)
321+
results := make(drawResults, 0, info.CardsNum)
261322
randomIntMap := make(map[int]int, 30)
262323
for i := 0; i < info.CardsNum; i++ {
263324
j := rand.Intn(length)
@@ -295,6 +356,12 @@ func init() {
295356
build.WriteString("』\n其释义为: \n")
296357
build.WriteString(description)
297358
build.WriteString("\n")
359+
results = append(results, drawResult{
360+
Name: name,
361+
Position: position[p],
362+
Description: description,
363+
Represent: info.Represent[0][i],
364+
})
298365
msg[i] = ctxext.FakeSenderForwardNode(ctx, tarotmsg...)
299366
}
300367
txt := build.String()
@@ -307,8 +374,110 @@ func init() {
307374
if id := ctx.Send(msg).ID(); id == 0 {
308375
ctx.SendChain(message.Text("ERROR: 可能被风控了"))
309376
}
377+
if question == "" {
378+
return
379+
}
380+
381+
if !chat.EnsureConfig(ctx) {
382+
ctx.SendChain(message.Text("塔罗解析失败: 无法读取 AI 聊天配置"))
383+
return
384+
}
385+
gid := ctx.Event.GroupID
386+
if gid == 0 {
387+
gid = -ctx.Event.UserID
388+
}
389+
stor, err := chat.NewStorage(ctx, gid)
390+
if err != nil {
391+
ctx.SendChain(message.Text("塔罗解析失败: ", errors.Wrap(err, "读取 AI 聊天温度配置失败")))
392+
return
393+
}
394+
reply, err := results.analyze(question, match, stor.Temp())
395+
if err != nil {
396+
logrus.Warnln("[tarot]大模型解析失败:", err)
397+
ctx.SendChain(message.Text("塔罗解析失败: ", err))
398+
return
399+
}
400+
if reply == "" {
401+
ctx.SendChain(message.Text("塔罗解析失败: 大模型返回为空"))
402+
return
403+
}
404+
if id := ctx.Send(makeNodeMessage(
405+
reply,
406+
ctx.CardOrNickName(ctx.Event.UserID),
407+
ctx.Event.UserID,
408+
)).ID(); id == 0 {
409+
ctx.SendChain(message.Text("ERROR: 可能被风控了"))
410+
}
310411
} else {
311-
ctx.SendChain(message.Text("没有找到", match, "噢~\n现有牌阵列表: \n", strings.Join(formationName, "\n")))
412+
ctx.SendChain(message.Text("没有找到", rawMatch, "噢~\n现有牌阵列表: \n", strings.Join(formationName, "\n")))
312413
}
313414
})
314415
}
416+
417+
func (draws drawResults) analyze(question, formationName string, temperature float32) (string, error) {
418+
var build strings.Builder
419+
build.WriteString("你是一位谨慎的塔罗牌解读者。请围绕用户的问题和本次牌面解读,先说明牌面,再给出综合建议,字数控制在300-500字。")
420+
build.WriteString("不要把占卜结果表述为确定事实。\n")
421+
build.WriteString("用户问题: ")
422+
build.WriteString(question)
423+
build.WriteByte('\n')
424+
if formationName != "" {
425+
build.WriteString("牌阵: ")
426+
build.WriteString(formationName)
427+
build.WriteByte('\n')
428+
}
429+
build.WriteString("牌面:\n")
430+
for i, draw := range draws {
431+
build.WriteString(strconv.Itoa(i + 1))
432+
build.WriteString(". ")
433+
if draw.Represent != "" {
434+
build.WriteString("牌位: ")
435+
build.WriteString(draw.Represent)
436+
build.WriteString("; ")
437+
}
438+
build.WriteString("牌: ")
439+
build.WriteString(draw.Name)
440+
build.WriteString("; 方位: ")
441+
build.WriteString(draw.Position)
442+
build.WriteString("; 固定释义: ")
443+
build.WriteString(draw.Description)
444+
build.WriteByte('\n')
445+
}
446+
topp, maxn := chat.AC.MParams()
447+
mod, err := chat.AC.Type.Protocol(chat.AC.ModelName, temperature, topp, maxn, chat.AC.ReasoningEffort)
448+
if err != nil {
449+
return "", errors.Wrap(err, "创建 AI 模型协议失败")
450+
}
451+
452+
api := deepinfra.NewAPI(chat.AC.API, string(chat.AC.Key))
453+
data, err := api.Request(mod.User(model.NewContentText(build.String())))
454+
if err != nil {
455+
return "", errors.Wrap(err, "请求 AI 模型失败")
456+
}
457+
return strings.TrimSpace(data), nil
458+
}
459+
460+
func makeNodeMessage(reply, nickname string, userID int64) message.Message {
461+
chunks := splitTextChunks("塔罗解析:\n"+reply, 1000)
462+
msg := make(message.Message, 0, len(chunks))
463+
for _, chunk := range chunks {
464+
msg = append(msg, message.CustomNode(nickname, userID, message.Message{message.Text(chunk)}))
465+
}
466+
return msg
467+
}
468+
469+
func splitTextChunks(txt string, maxRunes int) []string {
470+
runes := []rune(txt)
471+
if maxRunes <= 0 || len(runes) <= maxRunes {
472+
return []string{txt}
473+
}
474+
chunks := make([]string, 0, (len(runes)+maxRunes-1)/maxRunes)
475+
for len(runes) > maxRunes {
476+
chunks = append(chunks, string(runes[:maxRunes]))
477+
runes = runes[maxRunes:]
478+
}
479+
if len(runes) > 0 {
480+
chunks = append(chunks, string(runes))
481+
}
482+
return chunks
483+
}

plugin/tarot/tarot_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package tarot
2+
3+
import "testing"
4+
5+
func TestSplitTextChunks(t *testing.T) {
6+
got := splitTextChunks("甲乙丙丁", 3)
7+
if len(got) != 2 {
8+
t.Fatalf("splitTextChunks() chunks = %d, want 2", len(got))
9+
}
10+
if got[0] != "甲乙丙" || got[1] != "丁" {
11+
t.Fatalf("splitTextChunks() = %#v, want []string{\"甲乙丙\", \"\"}", got)
12+
}
13+
}
14+
15+
func TestBuildMessage(t *testing.T) {
16+
msg := makeNodeMessage("结果", "占卜者", 1)
17+
if len(msg) != 1 {
18+
t.Fatalf("buildMessage() message segments = %d, want 1", len(msg))
19+
}
20+
}
21+
22+
func TestDrawResultsAnalyzeSignature(t *testing.T) {
23+
var analyze func(drawResults, string, string, float32) (string, error) = drawResults.analyze
24+
if analyze == nil {
25+
t.Fatal("drawResults.analyze is nil")
26+
}
27+
}

0 commit comments

Comments
 (0)