From 26e57b65c106beb5ddc9d7a19d25bbb2bd0871ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E6=9F=B3=E7=85=9C?= <101934327+fangliuyu@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:03:56 +0800 Subject: [PATCH 1/6] Add files via upload --- plugin/handou/baiAPI.go | 182 +++++++++++ plugin/handou/game.go | 677 ++++++++++++++++++++++++++++++++++++++++ plugin/handou/habits.go | 348 +++++++++++++++++++++ 3 files changed, 1207 insertions(+) create mode 100644 plugin/handou/baiAPI.go create mode 100644 plugin/handou/game.go create mode 100644 plugin/handou/habits.go diff --git a/plugin/handou/baiAPI.go b/plugin/handou/baiAPI.go new file mode 100644 index 0000000000..59f00d6eef --- /dev/null +++ b/plugin/handou/baiAPI.go @@ -0,0 +1,182 @@ +// Package handou 猜成语 +package handou + +import ( + "encoding/json" + "fmt" + "net/url" + "os" + "slices" + "strings" + "sync" + + "github.com/FloatTech/floatbox/web" + "github.com/sirupsen/logrus" +) + +type baiduAPIData struct { + Errno int `json:"errno"` + Errmsg string `json:"errmsg"` + Data struct { + IdiomVersion int `json:"idiomVersion"` + Name string `json:"name"` + Sid string `json:"sid"` + Type string `json:"type"` + LessonInfo any `json:"lessonInfo"` + RelationInfo struct { + RelationName string `json:"relationName"` + RelationList []struct { + Name string `json:"name"` + Imgs []string `json:"imgs"` + } `json:"relationList"` + } `json:"relationInfo"` + Imgs []string `json:"imgs"` + Definition []struct { + Pinyin string `json:"pinyin"` + Voice string `json:"voice"` + Definition []string `json:"definition"` + DetailDefinition any `json:"detailDefinition"` + } `json:"definition"` + DefinitionInfo struct { + Definition string `json:"definition"` + SimilarDefinition string `json:"similarDefinition"` + AncientDefinition string `json:"ancientDefinition"` + ModernDefinition string `json:"modernDefinition"` + DetailMeans []struct { + Word string `json:"word"` + Definition string `json:"definition"` + } `json:"detailMeans"` + UsageTips any `json:"usageTips"` + Yicuodian any `json:"yicuodian"` + Baobian string `json:"baobian"` + WordFormation string `json:"wordFormation"` + } `json:"definitionInfo"` + Liju []struct { + Name string `json:"name"` + ShowName string `json:"showName"` + } `json:"liju"` + Source string `json:"source"` + Story any `json:"story"` + Antonym []struct { + Name string `json:"name"` + IsClick bool `json:"isClick"` + } `json:"antonym"` + Synonym []string `json:"synonym"` + Synonyms []struct { + Name string `json:"name"` + IsClick bool `json:"isClick"` + } `json:"synonyms"` + Tongyiyixing []struct { + Name string `json:"name"` + IsClick bool `json:"isClick"` + } `json:"tongyiyixing"` + ChuChu []struct { + SourceChapter string `json:"sourceChapter"` + Source string `json:"source"` + Dynasty string `json:"dynasty"` + CiteOriginalText string `json:"citeOriginalText"` + Author string `json:"author"` + } `json:"chuChu"` + YinZheng []struct { + SourceChapter string `json:"sourceChapter"` + Source string `json:"source"` + Dynasty string `json:"dynasty"` + CiteOriginalText string `json:"citeOriginalText"` + Author string `json:"author"` + } `json:"yinZheng"` + PictureList []any `json:"pictureList"` + LessonTerms struct { + TermList any `json:"termList"` + HasTerms int `json:"hasTerms"` + } `json:"lessonTerms"` + LessonTermsNew struct { + TermList any `json:"termList"` + HasTerms int `json:"hasTerms"` + } `json:"lessonTermsNew"` + Baobian string `json:"baobian"` + Structure string `json:"structure"` + Pinyin string `json:"pinyin"` + Voice string `json:"voice"` + ZuowenQuery string `json:"zuowen_query"` + } `json:"data"` +} + +func geiAPIdata(s string) (*idiomJson, error) { + url := "https://hanyuapp.baidu.com/dictapp/swan/termdetail?wd=" + url.QueryEscape(s) + "&client=pc&source_tag=2&lesson_from=xiaodu" + logrus.Warningln(url) + data, err := web.GetData(url) + if err != nil { + return nil, err + } + + var apiData baiduAPIData + err = json.Unmarshal(data, &apiData) + if err != nil { + return nil, err + } + if apiData.Data.Name == "" { + return nil, fmt.Errorf("未找到该成语") + } + derivation := "" + for _, v := range apiData.Data.ChuChu { + if derivation != "" { + derivation += "\n" + } + derivation += v.Dynasty + "·" + v.Author + " " + v.Source + ":" + v.CiteOriginalText + } + + explanation := apiData.Data.DefinitionInfo.Definition + apiData.Data.DefinitionInfo.ModernDefinition + if derivation == "" && explanation == "" { + return nil, fmt.Errorf("无法获取成语词源和解释") + } + synonyms := make([]string, len(apiData.Data.Synonyms)) + for i, synonym := range apiData.Data.Synonyms { + synonyms[i] = synonym.Name + } + for i, synonym := range apiData.Data.Synonym { + if !slices.Contains(synonyms, synonym) { + synonyms[i] = synonym + } + } + liju := "" + if len(apiData.Data.Liju) > 0 { + liju = apiData.Data.Liju[0].Name + } + + // 生成字符切片 + chars := make([]string, 0, len(s)) + for _, r := range s { + chars = append(chars, string(r)) + } + // 分割拼音 + pinyinSlice := strings.Split(apiData.Data.Pinyin, " ") + if len(pinyinSlice) != len(chars) { + pinyinSlice = strings.Split(apiData.Data.Definition[0].Pinyin, " ") + } + + newIdiom := idiomJson{ + Word: apiData.Data.Name, + Chars: chars, + Pinyin: pinyinSlice, + Baobian: apiData.Data.Baobian, + Explanation: explanation, + Derivation: derivation, + Example: liju, + Abbreviation: apiData.Data.Structure, + Synonyms: synonyms, + } + return &newIdiom, nil +} + +var mu sync.Mutex + +func saveIdiomJson() error { + mu.Lock() + defer mu.Unlock() + f, err := os.Create(idiomFilePath) + if err != nil { + return err + } + defer f.Close() + return json.NewEncoder(f).Encode(&idiomInfoMap) +} diff --git a/plugin/handou/game.go b/plugin/handou/game.go new file mode 100644 index 0000000000..e61fdf0624 --- /dev/null +++ b/plugin/handou/game.go @@ -0,0 +1,677 @@ +// Package handou 猜成语 +package handou + +import ( + "encoding/json" + "errors" + "fmt" + "image" + "image/color" + "math" + "math/rand" + "slices" + "strings" + "sync" + "time" + + "github.com/FloatTech/imgfactory" + "github.com/sirupsen/logrus" + + fcext "github.com/FloatTech/floatbox/ctxext" + "github.com/FloatTech/floatbox/file" + "github.com/FloatTech/gg" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/ctxext" + "github.com/FloatTech/zbputils/img/text" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +type idiomJson struct { + Word string `json:"word"` // 成语 + Chars []string `json:"chars"` // 成语 + Pinyin []string `json:"pinyin"` // 拼音 + Baobian string `json:"baobian"` // 褒贬义 + Explanation string `json:"explanation"` // 解释 + Derivation string `json:"derivation"` // 词源 + Example string `json:"example"` // 例句 + Abbreviation string `json:"abbreviation"` // 结构 + Synonyms []string `json:"synonyms"` // 近义词 +} + +const ( + kong = rune(' ') + pinFontSize = 45.0 + hanFontSize = 150.0 +) + +const ( + match = iota + exist + notexist + blockmatch + blockexist +) + +var colors = [...]color.RGBA{ + {0, 153, 0, 255}, + {255, 128, 0, 255}, + {123, 123, 123, 255}, + {125, 166, 108, 255}, + {199, 183, 96, 255}, +} + +var ( + en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Brief: "猜成语", + Help: "- 个人猜成语\n" + + "- 团队猜成语\n", + PublicDataFolder: "Handou", + }).ApplySingle(ctxext.NewGroupSingle("已经有正在进行的游戏...")) + userHabitsFile = file.BOTPATH + "/" + en.DataFolder() + "userHabits.json" + idiomFilePath = file.BOTPATH + "/" + en.DataFolder() + "idiom.json" + initialized = fcext.DoOnceOnSuccess( + func(ctx *zero.Ctx) bool { + idiomFile, err := en.GetLazyData("idiom.json", true) + if err != nil { + ctx.SendChain(message.Text("ERROR: 下载字典时发生错误.\n", err)) + return false + } + err = json.Unmarshal(idiomFile, &idiomInfoMap) + if err != nil { + ctx.SendChain(message.Text("ERROR: 解析字典时发生错误.\n", err)) + return false + } + habitsIdiomKeys = make([]string, 0, len(idiomInfoMap)) + for k := range idiomInfoMap { + habitsIdiomKeys = append(habitsIdiomKeys, k) + } + // 构建用户习惯库(全局高频N-gram) + err = initUserHabits() + if err != nil { + ctx.SendChain(message.Text("ERROR: 构建用户习惯库时发生错误.\n", err)) + return false + } + // 下载字体 + data, err := file.GetLazyData(text.BoldFontFile, control.Md5File, true) + if err != nil { + ctx.SendChain(message.Text("ERROR: 加载字体时发生错误.\n", err)) + return false + } + pinyinFont = data + return true + }, + ) + + pinyinFont []byte + idiomInfoMap = make(map[string]idiomJson) + habitsIdiomKeys = make([]string, 0) + + errHadGuessed = errors.New("had guessed") + errLengthNotEnough = errors.New("length not enough") + errUnknownWord = errors.New("unknown word") + errTimesRunOut = errors.New("times run out") +) + +func init() { + en.OnRegex(`^猜成语热门(汉字|成语)$`, zero.OnlyGroup, initialized).SetBlock(true).Limit(ctxext.LimitByUser).Handle(func(ctx *zero.Ctx) { + if ctx.State["regex_matched"].([]string)[1] == "汉字" { + topChars := getTopCharacters(10) + ctx.SendChain(message.Text("热门汉字:\n", strings.Join(topChars, "\n"))) + } else { + topIdioms := getTopIdioms(10) + ctx.SendChain(message.Text("热门成语:\n", strings.Join(topIdioms, "\n"))) + } + }) + en.OnRegex(`^(个人|团队)猜成语$`, zero.OnlyGroup, initialized).SetBlock(true).Limit(ctxext.LimitByUser).Handle(func(ctx *zero.Ctx) { + target := poolIdiom() + idiomData := idiomInfoMap[target] + game := newHandouGame(idiomData) + _, img, _ := game("") + anser := anserOutString(idiomData) + worldLength := len(idiomData.Chars) + ctx.Send( + message.ReplyWithMessage(ctx.Event.MessageID, + message.ImageBytes(img), + message.Text("你有", 7, "次机会猜出", worldLength, "字成语\n首字拼音为:", idiomData.Pinyin[0]), + ), + ) + var next *zero.FutureEvent + if ctx.State["regex_matched"].([]string)[1] == "个人" { + next = zero.NewFutureEvent("message", 999, false, zero.RegexRule(fmt.Sprintf(`^([\p{Han},,]){%d}$`, worldLength)), + zero.OnlyGroup, ctx.CheckSession()) + } else { + next = zero.NewFutureEvent("message", 999, false, zero.RegexRule(fmt.Sprintf(`^([\p{Han},,]){%d}$`, worldLength)), + zero.OnlyGroup, zero.CheckGroup(ctx.Event.GroupID)) + } + var err error + var win bool + recv, cancel := next.Repeat() + defer cancel() + tick := time.NewTimer(105 * time.Second) + after := time.NewTimer(120 * time.Second) + for { + select { + case <-tick.C: + ctx.SendChain(message.Text("猜成语,你还有15s作答时间")) + case <-after.C: + ctx.Send( + message.ReplyWithMessage(ctx.Event.MessageID, + message.Text("猜成语超时,游戏结束...\n答案是: ", anser), + ), + ) + return + case c := <-recv: + tick.Reset(105 * time.Second) + after.Reset(120 * time.Second) + err = updateHabits(c.Event.Message.String()) + if err != nil { + logrus.Warn("更新用户习惯库时发生错误: ", err) + } + win, img, err = game(c.Event.Message.String()) + switch { + case win: + tick.Stop() + after.Stop() + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.ImageBytes(img), + message.Text("太棒了,你猜出来了!\n答案是: ", anser), + ), + ) + return + case err == errTimesRunOut: + tick.Stop() + after.Stop() + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.ImageBytes(img), + message.Text("游戏结束...\n答案是: ", anser), + ), + ) + return + case err == errLengthNotEnough: + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.Text("成语长度错误"), + ), + ) + case err == errHadGuessed: + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.Text("该成语已经猜过了"), + ), + ) + case err == errUnknownWord: + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.Text("你确定存在这样的成语吗?"), + ), + ) + default: + if img != nil { + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.ImageBytes(img), + ), + ) + } else { + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.Text("回答错误。"), + ), + ) + + } + } + } + } + }) +} + +func poolIdiom() string { + prioritizedData := prioritizeData(habitsIdiomKeys) + if len(prioritizedData) > 0 { + return prioritizedData[rand.Intn(len(prioritizedData))] + } + // 如果没有优先级数据,则随机选择一个成语 + keys := make([]string, 0, len(idiomInfoMap)) + for k := range idiomInfoMap { + keys = append(keys, k) + } + return keys[rand.Intn(len(keys))] +} + +func newHandouGame(target idiomJson) func(string) (bool, []byte, error) { + var ( + class = len(target.Chars) + words = target.Word + chars = target.Chars + pinyin = target.Pinyin + + tickTruePinyin = make([]string, class) + tickExistChars = make([]string, class) + tickExistPinyin = make([]string, 0, class) + + record = make([]string, 0, 7) + ) + // 初始化 tick, 第一个是已知的拼音 + for i := range class { + if i == 0 { + tickTruePinyin[i] = pinyin[0] + } else { + tickTruePinyin[i] = "" + } + tickExistChars[i] = "?" + } + + return func(s string) (win bool, data []byte, err error) { + answer := []rune(s) + var answerData idiomJson + + if s != "" { + if words == s { + win = true + } + + if len(answer) != len(chars) { + err = errLengthNotEnough + return + } + if slices.Contains(record, s) { + err = errHadGuessed + return + } + + answerInfo, ok := idiomInfoMap[s] + if !ok { + newIdiom, err1 := geiAPIdata(s) + if err1 != nil { + logrus.Warningln("通过API获取成语信息时发生错误: ", err1) + err = errUnknownWord + return + } + logrus.Warningln("通过API获取成语信息: ", newIdiom.Word) + if newIdiom.Word != "" { + idiomInfoMap[newIdiom.Word] = *newIdiom + go func() { _ = saveIdiomJson() }() + } + if newIdiom.Word != s { + err = errUnknownWord + return + } + answerData = *newIdiom + } else { + answerData = answerInfo + } + if len(record) >= 6 || win { + // 结束了显示答案 + tickTruePinyin = target.Pinyin + tickExistChars = target.Chars + } else { + // 处理汉字匹配逻辑 + for i := range class { + char := answerData.Chars[i] + if char == chars[i] { + tickExistChars[i] = char + } else { + tickExistChars[i] = "?" + } + } + + // 确保 tickExistPinyin 有足够的长度 + if len(tickExistPinyin) < class { + for i := len(tickExistPinyin); i < class; i++ { + tickExistPinyin = append(tickExistPinyin, "") + } + } + + // 处理拼音匹配逻辑 + minPinyinLen := min(len(pinyin), len(answerData.Pinyin)) + for i := range minPinyinLen { + pyChar := pinyin[i] + answerPinyinChar := []rune(pyChar) + tickTruePinyinChar := make([]rune, len(answerPinyinChar)) + tickExistPinyinChar := []rune(tickExistPinyin[i]) + + if tickTruePinyin[i] != "" { + copy(tickTruePinyinChar, []rune(tickTruePinyin[i])) + } else { + for k := range answerPinyinChar { + tickTruePinyinChar[k] = kong + } + } + + PinyinChar := answerData.Pinyin[i] + for j, c := range []rune(PinyinChar) { + if c == kong { + continue + } + switch { + case j < len(answerPinyinChar) && c == answerPinyinChar[j]: + tickTruePinyinChar[j] = c + case slices.Contains(answerPinyinChar, c): + // 如果字符存在但位置不对,添加到 tickExistPinyinChar + if !slices.Contains(tickExistPinyinChar, c) { + tickExistPinyinChar = append(tickExistPinyinChar, c) + } + default: + if j < len(tickTruePinyinChar) { + tickTruePinyinChar[j] = kong + } + } + } + + // 处理提示逻辑,将非匹配位置设为下划线 + matchIndex := -1 + for j, v := range tickTruePinyinChar { + if v != kong && v != '_' { + matchIndex = j + } + } + for j := range tickTruePinyinChar { + if j > matchIndex { + break + } + if tickTruePinyinChar[j] == kong { + tickTruePinyinChar[j] = '_' + } + } + // 更新提示拼音 + tickTruePinyin[i] = string(tickTruePinyinChar) + tickExistPinyin[i] = string(tickExistPinyinChar) + } + if len(record) == 2 { + tickTruePinyin[0] = pinyin[0] + tickExistChars[0] = chars[0] + } + } + } + + // 准备绘制数据 + existPinyin := make([]string, 0, class) + for _, v := range tickExistPinyin { + if v != "" { + v = "?" + v + } + existPinyin = append(existPinyin, v) + } + tickIdiom := idiomJson{ + Chars: tickExistChars, + Pinyin: tickTruePinyin, + } + + // 确保所有切片长度一致 + if len(tickIdiom.Chars) < class { + // 如果答案字符数不足,用问号填充 + for i := len(tickIdiom.Chars); i < class; i++ { + tickIdiom.Chars = append(tickIdiom.Chars, "?") + } + } + if len(tickIdiom.Pinyin) < class { + // 如果答案拼音数不足,用空字符串填充 + for i := len(tickIdiom.Pinyin); i < class; i++ { + tickIdiom.Pinyin = append(tickIdiom.Pinyin, "") + } + } + + if s == "" { + answerData = tickIdiom + } + + var ( + tickImage image.Image + answerImage image.Image + imgHistery = make([]image.Image, 0) + hisH = 0 + wg = &sync.WaitGroup{} + ) + wg.Add(2) + + go func() { + defer wg.Done() + tickImage = drawHanBlock(hanFontSize/2, pinFontSize/2, tickIdiom, target, existPinyin...) + }() + go func() { + defer wg.Done() + answerImage = drawHanBlock(hanFontSize, pinFontSize, answerData, target) + }() + if len(record) > 0 { + wg.Add(len(record)) + for i, v := range record { + imgHistery = append(imgHistery, nil) + go func(i int, v string) { + defer wg.Done() + idiom, ok := idiomInfoMap[v] + if !ok { + return + } + hisImage := drawHanBlock(hanFontSize/3, pinFontSize/3, idiom, target) + imgHistery[i] = hisImage + if i == 0 { + hisH = hisImage.Bounds().Dy() + } + }(i, v) + } + } + wg.Wait() + + // 记录猜过的成语 + if s != "" && !win { + record = append(record, s) + } + + if tickImage == nil || answerImage == nil { + return + } + + tickW, tickH := tickImage.Bounds().Dx(), tickImage.Bounds().Dy() + answerW, answerH := answerImage.Bounds().Dx(), answerImage.Bounds().Dy() + + ctx := gg.NewContext(1, 1) + _ = ctx.ParseFontFace(pinyinFont, pinFontSize/2) + wordH, _ := ctx.MeasureString("M") + + ctxWidth := max(tickW, answerW) + ctxHeight := tickH + answerH + int(wordH) + hisH*(len(imgHistery)+1)/2 + + ctx = gg.NewContext(ctxWidth, ctxHeight) + ctx.SetColor(color.RGBA{255, 255, 255, 255}) + ctx.Clear() + + ctx.SetColor(color.RGBA{0, 0, 0, 255}) + _ = ctx.ParseFontFace(pinyinFont, hanFontSize/2) + ctx.DrawStringAnchored("题目:", float64(ctxWidth-tickW)/4, float64(tickH)/2, 0.5, 0.5) + + ctx.DrawImageAnchored(tickImage, ctxWidth/2, tickH/2, 0.5, 0.5) + ctx.DrawImageAnchored(answerImage, ctxWidth/2, tickH+int(wordH)+answerH/2, 0.5, 0.5) + + k := 0 + for i, v := range imgHistery { + if v == nil { + continue + } + x := ctxWidth / 4 + y := tickH + int(wordH) + answerH + hisH*k + + if i%2 == 1 { + x = ctxWidth * 3 / 4 + y = tickH + int(wordH) + answerH + hisH*k + k++ + } + ctx.DrawImageAnchored(v, x, y+hisH/2, 0.5, 0.5) + } + + data, err = imgfactory.ToBytes(ctx.Image()) + if len(record) >= cap(record) { + err = errTimesRunOut + return + } + + return + } +} + +// drawHanBlock 绘制汉字方块,支持多行显示(6字以上时分成两行) +func drawHanBlock(hanFontSize, pinFontSize float64, idiom, target idiomJson, exitPinyin ...string) image.Image { + class := len(target.Chars) + + // 确保切片长度一致 + if len(idiom.Chars) < class { + temp := make([]string, class) + copy(temp, idiom.Chars) + for i := len(idiom.Chars); i < class; i++ { + temp[i] = "?" + } + idiom.Chars = temp + } + if len(idiom.Pinyin) < class { + temp := make([]string, class) + copy(temp, idiom.Pinyin) + for i := len(idiom.Pinyin); i < class; i++ { + temp[i] = "" + } + idiom.Pinyin = temp + } + + chars := idiom.Chars + pinyin := idiom.Pinyin + + // 确定行数和每行字数 + rows := 1 + charsPerRow := class + if class > 6 { + rows = 2 + charsPerRow = (class + 1) / 2 + } + + ctx := gg.NewContext(1, 1) + _ = ctx.ParseFontFace(pinyinFont, pinFontSize) + pinWidth, pinHeight := ctx.MeasureString("w") + _ = ctx.ParseFontFace(pinyinFont, hanFontSize) + hanWidth, hanHeight := ctx.MeasureString("拼") + + space := int(pinHeight / 2) + blockPinWidth := int(pinWidth*6) + space + boxPadding := math.Min(math.Abs(float64(blockPinWidth)-hanWidth)/2, hanHeight*0.3) + + // 计算总宽度和高度 + width := space + charsPerRow*blockPinWidth + space + height := space + rows*(int(pinHeight+hanHeight+boxPadding*2)+space*2) + space + if len(exitPinyin) > 0 { + height = space + rows*(int(pinHeight+hanHeight+boxPadding*2+pinHeight)+space*2) + space + } + + ctx = gg.NewContext(width, height) + ctx.SetColor(color.RGBA{255, 255, 255, 255}) + ctx.Clear() + + for i := range class { + // 边界检查 + if i >= len(chars) || i >= len(pinyin) || i >= len(target.Pinyin) || i >= len(target.Chars) { + break + } + + // 计算当前字符在哪一行哪一列 + idiom_rows := 0 + col := i + if rows > 1 { + idiom_rows = i / charsPerRow + col = i % charsPerRow + } + + x := float64(space + col*blockPinWidth) + // 如果上一层字数是奇数就额外移位 + if idiom_rows%2 == 1 { + x += float64(blockPinWidth) / 2 + } + y := float64(idiom_rows*(int(pinHeight+hanHeight+boxPadding*2)+space*2) + space) + if len(exitPinyin) > 0 { + y = float64(idiom_rows*(int(pinHeight+hanHeight+boxPadding*2+pinHeight)+space*2) + space) + } + + // 绘制拼音 + _ = ctx.ParseFontFace(pinyinFont, pinFontSize) + if i < len(pinyin) { + targetPinyinByte := []rune(target.Pinyin[i]) + pinyinByte := []rune(pinyin[i]) + + // 取两者中的最大长度 + pinTotalWidth := pinWidth * float64(len(pinyinByte)) + pinX := x + float64(blockPinWidth)/2 - pinTotalWidth/2 + pinY := y + pinHeight/2 + + for k, ch := range pinyinByte { + ctx.SetColor(colors[notexist]) + for m, c := range targetPinyinByte { + if k == m && ch == c { + ctx.SetColor(colors[match]) + break + } else if ch == c { + ctx.SetColor(colors[exist]) + } + } + ctx.DrawStringAnchored(string(ch), pinX+pinWidth*float64(k)+pinWidth/2, pinY, 0.5, 0.5) + } + } + + // 绘制汉字方框 + boxX := x + boxPadding + boxY := y + pinHeight + float64(space) + boxWidth := float64(blockPinWidth) - boxPadding*2 + boxHeight := float64(hanHeight) + boxPadding*2 + ctx.DrawRectangle(boxX, boxY, boxWidth, boxHeight) + + // 设置方框颜色 + char := chars[i] + switch { + case char == target.Chars[i]: + ctx.SetColor(colors[blockmatch]) + case char != "" && strings.Contains(target.Word, char): + ctx.SetColor(colors[blockexist]) + default: + ctx.SetColor(colors[notexist]) + } + ctx.Fill() + + // 绘制汉字 + _ = ctx.ParseFontFace(pinyinFont, hanFontSize) + ctx.SetColor(color.RGBA{255, 255, 255, 255}) + hanX := boxX + boxWidth/2 + hanY := boxY + boxHeight/2 + ctx.DrawStringAnchored(char, hanX, hanY, 0.5, 0.5) + + // 绘制题目的拼音提示 + ctx.SetColor(colors[exist]) + _ = ctx.ParseFontFace(pinyinFont, pinFontSize) + if len(exitPinyin) > i && exitPinyin[i] != "" { + tickY := boxY + boxHeight + float64(space) + pinHeight/2 + ctx.DrawStringAnchored(exitPinyin[i], hanX, tickY, 0.5, 0.5) + } + } + return ctx.Image() +} + +func anserOutString(s idiomJson) string { + msg := s.Word + if s.Baobian != "" && s.Baobian != "-" { + msg += "\n" + s.Baobian + "词" + } + if s.Derivation != "" && s.Derivation != "-" { + msg += "\n词源:\n" + s.Derivation + } else { + msg += "\n词源:无" + } + if s.Explanation != "" && s.Explanation != "-" { + msg += "\n解释:\n" + s.Explanation + } else { + msg += "\n解释:无" + } + if len(s.Synonyms) > 0 { + msg += "\n近义词:\n" + strings.Join(s.Synonyms, ",") + } + + return msg +} diff --git a/plugin/handou/habits.go b/plugin/handou/habits.go new file mode 100644 index 0000000000..fa8eae6691 --- /dev/null +++ b/plugin/handou/habits.go @@ -0,0 +1,348 @@ +// Package handou 猜成语 +package handou + +import ( + "encoding/json" + "fmt" + "math" + "math/rand" + "os" + "slices" + "sync" + "time" + + "github.com/FloatTech/floatbox/file" + "github.com/sirupsen/logrus" +) + +type UserHabits struct { + mu sync.RWMutex + habits map[string]int // 单字频率 + bigrams map[string]int // 二元组频率 + idioms map[string]int // 成语出现频率 + totalWords int // 总字数 + totalIdioms int // 总成语数 + lastUpdate time.Time // 最后更新时间 +} + +var userHabits *UserHabits + +// 初始化用户习惯 +func initUserHabits() error { + userHabits = &UserHabits{ + habits: make(map[string]int), + bigrams: make(map[string]int), + idioms: make(map[string]int), + } + + if file.IsNotExist(userHabitsFile) { + f, err := os.Create(userHabitsFile) + if err != nil { + return fmt.Errorf("创建用户习惯库时发生错误: %v", err) + } + _ = f.Close() + return saveHabits() + } + + // 读取现有习惯数据 + habitsFile, err := os.ReadFile(userHabitsFile) + if err != nil { + return fmt.Errorf("读取用户习惯库时发生错误: %v", err) + } + + var savedData struct { + Habits map[string]int `json:"habits"` + Bigrams map[string]int `json:"bigrams"` + Idioms map[string]int `json:"idioms"` + TotalWords int `json:"total_words"` + TotalIdioms int `json:"total_idioms"` + LastUpdate time.Time `json:"last_update"` + } + + if err := json.Unmarshal(habitsFile, &savedData); err != nil { + // 如果是旧格式,尝试兼容 + var oldHabits map[string]int + if err := json.Unmarshal(habitsFile, &oldHabits); err == nil { + savedData.Habits = oldHabits + // 从旧数据重新计算统计信息 + for _, count := range oldHabits { + savedData.TotalWords += count + } + } else { + return fmt.Errorf("解析用户习惯库时发生错误: %v", err) + } + } + + userHabits.mu.Lock() + defer userHabits.mu.Unlock() + + userHabits.habits = savedData.Habits + userHabits.bigrams = savedData.Bigrams + userHabits.idioms = savedData.Idioms + userHabits.totalWords = savedData.TotalWords + userHabits.totalIdioms = savedData.TotalIdioms + userHabits.lastUpdate = savedData.LastUpdate + + return nil +} + +// 保存习惯数据 +func saveHabits() error { + userHabits.mu.RLock() + defer userHabits.mu.RUnlock() + + data := struct { + Habits map[string]int `json:"habits"` + Bigrams map[string]int `json:"bigrams"` + Idioms map[string]int `json:"idioms"` + TotalWords int `json:"total_words"` + TotalIdioms int `json:"total_idioms"` + LastUpdate time.Time `json:"last_update"` + }{ + Habits: userHabits.habits, + Bigrams: userHabits.bigrams, + Idioms: userHabits.idioms, + TotalWords: userHabits.totalWords, + TotalIdioms: userHabits.totalIdioms, + LastUpdate: time.Now(), + } + + f, err := os.Create(userHabitsFile) + if err != nil { + return err + } + defer f.Close() + + encoder := json.NewEncoder(f) + encoder.SetIndent("", " ") + return encoder.Encode(data) +} + +// 更新用户习惯(累加频率) +func updateHabits(input string) error { + if userHabits == nil { + if err := initUserHabits(); err != nil { + return err + } + } + + userHabits.mu.Lock() + defer userHabits.mu.Unlock() + + // 统计单字和二元组 + chars := []rune(input) + userHabits.totalWords += len(chars) + + // 更新单字频率 + for _, char := range chars { + charStr := string(char) + userHabits.habits[charStr]++ + } + + // 仅当成语存在时,更新成语相关频率 + if slices.Contains(habitsIdiomKeys, input) { + // 更新二元组频率(N=2的gram) + for i := 0; i < len(chars)-1; i++ { + bigram := string(chars[i]) + string(chars[i+1]) + userHabits.bigrams[bigram]++ + } + // 更新成语频率 + userHabits.idioms[input]++ + userHabits.totalIdioms++ + } + + // 异步保存到文件 + go func() { + if err := saveHabits(); err != nil { + logrus.Warn("保存用户习惯时发生错误: ", err) + } + }() + + return nil +} + +// 计算成语的优先级分数 +func calculatePriorityScore(idiom string) float64 { + if userHabits == nil || userHabits.totalWords == 0 { + return 0 + } + + userHabits.mu.RLock() + defer userHabits.mu.RUnlock() + + chars := []rune(idiom) + charsLenght := len(chars) + + // 1. 基于单字频率的分数 + charsScore := 0.0 + for _, char := range chars { + charStr := string(char) + if count, exists := userHabits.habits[charStr]; exists { + // 使用TF-IDF思想:频率越高,权重越高,但通过总字数归一化 + tf := float64(count*10) / float64(userHabits.totalWords) + // score += tf * 100 + charsScore += 100 / (1 + 10*math.Abs(tf-5)) // 规避一直是最热门的汉字 + } + } + charsScore = charsScore / float64(charsLenght) * 60 / 100 + + // 2. 基于二元组频率的分数(词序的重要性) + bigramScore := 0.0 + for i := 0; i < charsLenght-1; i++ { + bigram := string(chars[i]) + string(chars[i+1]) + if count, exists := userHabits.bigrams[bigram]; exists { + tf := float64(count*10) / float64(userHabits.totalWords) + // score += tf * 150 // 二元组比单字更重要 + bigramScore += 100 / (1 + 2*math.Abs(tf-5)) // 规避一直是最热门的词组 + } + } + bigramScore = bigramScore / float64(charsLenght-1) * 40 / 100 + + // 3. 基于成语本身的频率(降低常见成语的优先级,增加多样性) + penaltyScore := 0.0 + if idiomCount, exists := userHabits.idioms[idiom]; exists { + // 出现次数越多,优先级越低(避免总是出现相同的成语) + penalty := float64(idiomCount) / float64(userHabits.totalIdioms) * 100 + penaltyScore -= penalty + } + + // 4. 考虑成语长度, 让长成语也有机会被选中 + idiomScore := 0.0 + if rand.Intn(100) < 80 { + idiomScore = 20 / (1 + 1*math.Abs(float64(charsLenght)-4)) + } else { + idiomScore = float64(charsLenght) * 10 + } + + finalScore := charsScore + bigramScore + penaltyScore + idiomScore + + return finalScore +} + +// 优先抽取数据 +func prioritizeData(data []string) []string { + if len(data) == 0 { + return data + } + + // 计算每个成语的优先级分数 + idiomScores := make([]struct { + idiom string + score float64 + }, len(data)) + + for i, idiom := range data { + idiomScores[i] = struct { + idiom string + score float64 + }{ + idiom: idiom, + score: calculatePriorityScore(idiom), + } + } + + // 按分数排序(从高到低) + slices.SortFunc(idiomScores, func(a, b struct { + idiom string + score float64 + }) int { + if a.score > b.score { + return -1 + } else if a.score < b.score { + return 1 + } + return 0 + }) + + // 排除的前1/3的数量, 去除分数太高的成语 + excludeCount := int(float64(len(idiomScores)) * 0.333) + if excludeCount < 1 && len(idiomScores) > 1 { + excludeCount = 1 + } + startIndex := excludeCount + if startIndex >= len(idiomScores) { + startIndex = 0 + } + + // 选择接下来前10个作为优先数据 + limit := min(len(idiomScores)-startIndex, 10) + + prioritized := make([]string, limit) + for i := range limit { + prioritized[i] = idiomScores[startIndex+i].idiom + logrus.Warningf("成语 '%s' 分数=%.2f", + idiomScores[startIndex+i].idiom, idiomScores[startIndex+i].score) + } + + return prioritized +} + +// 获取热门汉字(用于调试或展示) +func getTopCharacters(limit int) []string { + if userHabits == nil { + return nil + } + + userHabits.mu.RLock() + defer userHabits.mu.RUnlock() + + type charFreq struct { + char string + count int + } + + chars := make([]charFreq, 0, len(userHabits.habits)) + for char, count := range userHabits.habits { + chars = append(chars, charFreq{char, count}) + } + + slices.SortFunc(chars, func(a, b charFreq) int { + return b.count - a.count + }) + + if len(chars) > limit { + chars = chars[:limit] + } + + result := make([]string, len(chars)) + for i, cf := range chars { + result[i] = fmt.Sprintf("%s:%d", cf.char, cf.count) + } + + return result +} + +// 获取热门成语(用于调试或展示) +func getTopIdioms(limit int) []string { + if userHabits == nil { + return nil + } + + userHabits.mu.RLock() + defer userHabits.mu.RUnlock() + + type idiomFreq struct { + idiom string + count int + } + + idioms := make([]idiomFreq, 0, len(userHabits.idioms)) + for char, count := range userHabits.idioms { + idioms = append(idioms, idiomFreq{char, count}) + } + + slices.SortFunc(idioms, func(a, b idiomFreq) int { + return b.count - a.count + }) + + if len(idioms) > limit { + idioms = idioms[:limit] + } + + result := make([]string, len(idioms)) + for i, cf := range idioms { + result[i] = fmt.Sprintf("%s:%d", cf.idiom, cf.count) + } + + return result +} From 8d6bb609ac79a233d7fb065854ef53ff95bb295d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E6=9F=B3=E7=85=9C?= <101934327+fangliuyu@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:05:37 +0800 Subject: [PATCH 2/6] Add handou plugin import to main.go --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index 8770d1a014..ee9bd997b3 100644 --- a/main.go +++ b/main.go @@ -104,6 +104,7 @@ import ( _ "github.com/FloatTech/ZeroBot-Plugin/plugin/gif" // 制图 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/github" // 搜索GitHub仓库 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic" // 猜歌 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/handou" // 猜成语 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hitokoto" // 一言 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hs" // 炉石 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hyaku" // 百人一首 From 307bd0d63a6635d2b650f863eca5b7acc6dfb88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E6=9F=B3=E7=85=9C?= <101934327+fangliuyu@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:08:01 +0800 Subject: [PATCH 3/6] Add section for guessing idioms in README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index ad09e3d30f..8041d77df2 100644 --- a/README.md +++ b/README.md @@ -845,6 +845,16 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 下载歌单[网易云歌单链接/ID]到[歌单名称] - [x] 解除绑定 [歌单名称] + +
+ 猜成语 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/handou"` + + - [x] 个人猜成语 + + - [x] 团队猜成语 +
一言 From 1f74485f286129df7327a760b3d6eb9b813b67bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E6=9F=B3=E7=85=9C?= <101934327+fangliuyu@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:38:06 +0800 Subject: [PATCH 4/6] Add files via upload --- plugin/handou/baiAPI.go | 12 ++++++------ plugin/handou/game.go | 26 +++++++++++++------------- plugin/handou/habits.go | 8 +++++--- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/plugin/handou/baiAPI.go b/plugin/handou/baiAPI.go index 59f00d6eef..86ceeb9de9 100644 --- a/plugin/handou/baiAPI.go +++ b/plugin/handou/baiAPI.go @@ -3,7 +3,7 @@ package handou import ( "encoding/json" - "fmt" + "errors" "net/url" "os" "slices" @@ -101,7 +101,7 @@ type baiduAPIData struct { } `json:"data"` } -func geiAPIdata(s string) (*idiomJson, error) { +func geiAPIdata(s string) (*idiomJSON, error) { url := "https://hanyuapp.baidu.com/dictapp/swan/termdetail?wd=" + url.QueryEscape(s) + "&client=pc&source_tag=2&lesson_from=xiaodu" logrus.Warningln(url) data, err := web.GetData(url) @@ -115,7 +115,7 @@ func geiAPIdata(s string) (*idiomJson, error) { return nil, err } if apiData.Data.Name == "" { - return nil, fmt.Errorf("未找到该成语") + return nil, errors.New("未找到该成语") } derivation := "" for _, v := range apiData.Data.ChuChu { @@ -127,7 +127,7 @@ func geiAPIdata(s string) (*idiomJson, error) { explanation := apiData.Data.DefinitionInfo.Definition + apiData.Data.DefinitionInfo.ModernDefinition if derivation == "" && explanation == "" { - return nil, fmt.Errorf("无法获取成语词源和解释") + return nil, errors.New("无法获取成语词源和解释") } synonyms := make([]string, len(apiData.Data.Synonyms)) for i, synonym := range apiData.Data.Synonyms { @@ -154,7 +154,7 @@ func geiAPIdata(s string) (*idiomJson, error) { pinyinSlice = strings.Split(apiData.Data.Definition[0].Pinyin, " ") } - newIdiom := idiomJson{ + newIdiom := idiomJSON{ Word: apiData.Data.Name, Chars: chars, Pinyin: pinyinSlice, @@ -170,7 +170,7 @@ func geiAPIdata(s string) (*idiomJson, error) { var mu sync.Mutex -func saveIdiomJson() error { +func saveIdiomJSON() error { mu.Lock() defer mu.Unlock() f, err := os.Create(idiomFilePath) diff --git a/plugin/handou/game.go b/plugin/handou/game.go index e61fdf0624..d2e44248ac 100644 --- a/plugin/handou/game.go +++ b/plugin/handou/game.go @@ -28,7 +28,7 @@ import ( "github.com/wdvxdr1123/ZeroBot/message" ) -type idiomJson struct { +type idiomJSON struct { Word string `json:"word"` // 成语 Chars []string `json:"chars"` // 成语 Pinyin []string `json:"pinyin"` // 拼音 @@ -106,7 +106,7 @@ var ( ) pinyinFont []byte - idiomInfoMap = make(map[string]idiomJson) + idiomInfoMap = make(map[string]idiomJSON) habitsIdiomKeys = make([]string, 0) errHadGuessed = errors.New("had guessed") @@ -244,7 +244,7 @@ func poolIdiom() string { return keys[rand.Intn(len(keys))] } -func newHandouGame(target idiomJson) func(string) (bool, []byte, error) { +func newHandouGame(target idiomJSON) func(string) (bool, []byte, error) { var ( class = len(target.Chars) words = target.Word @@ -269,7 +269,7 @@ func newHandouGame(target idiomJson) func(string) (bool, []byte, error) { return func(s string) (win bool, data []byte, err error) { answer := []rune(s) - var answerData idiomJson + var answerData idiomJSON if s != "" { if words == s { @@ -296,7 +296,7 @@ func newHandouGame(target idiomJson) func(string) (bool, []byte, error) { logrus.Warningln("通过API获取成语信息: ", newIdiom.Word) if newIdiom.Word != "" { idiomInfoMap[newIdiom.Word] = *newIdiom - go func() { _ = saveIdiomJson() }() + go func() { _ = saveIdiomJSON() }() } if newIdiom.Word != s { err = errUnknownWord @@ -398,7 +398,7 @@ func newHandouGame(target idiomJson) func(string) (bool, []byte, error) { } existPinyin = append(existPinyin, v) } - tickIdiom := idiomJson{ + tickIdiom := idiomJSON{ Chars: tickExistChars, Pinyin: tickTruePinyin, } @@ -515,7 +515,7 @@ func newHandouGame(target idiomJson) func(string) (bool, []byte, error) { } // drawHanBlock 绘制汉字方块,支持多行显示(6字以上时分成两行) -func drawHanBlock(hanFontSize, pinFontSize float64, idiom, target idiomJson, exitPinyin ...string) image.Image { +func drawHanBlock(hanFontSize, pinFontSize float64, idiom, target idiomJSON, exitPinyin ...string) image.Image { class := len(target.Chars) // 确保切片长度一致 @@ -575,21 +575,21 @@ func drawHanBlock(hanFontSize, pinFontSize float64, idiom, target idiomJson, exi } // 计算当前字符在哪一行哪一列 - idiom_rows := 0 + idiomRows := 0 col := i if rows > 1 { - idiom_rows = i / charsPerRow + idiomRows = i / charsPerRow col = i % charsPerRow } x := float64(space + col*blockPinWidth) // 如果上一层字数是奇数就额外移位 - if idiom_rows%2 == 1 { + if idiomRows%2 == 1 { x += float64(blockPinWidth) / 2 } - y := float64(idiom_rows*(int(pinHeight+hanHeight+boxPadding*2)+space*2) + space) + y := float64(idiomRows*(int(pinHeight+hanHeight+boxPadding*2)+space*2) + space) if len(exitPinyin) > 0 { - y = float64(idiom_rows*(int(pinHeight+hanHeight+boxPadding*2+pinHeight)+space*2) + space) + y = float64(idiomRows*(int(pinHeight+hanHeight+boxPadding*2+pinHeight)+space*2) + space) } // 绘制拼音 @@ -654,7 +654,7 @@ func drawHanBlock(hanFontSize, pinFontSize float64, idiom, target idiomJson, exi return ctx.Image() } -func anserOutString(s idiomJson) string { +func anserOutString(s idiomJSON) string { msg := s.Word if s.Baobian != "" && s.Baobian != "-" { msg += "\n" + s.Baobian + "词" diff --git a/plugin/handou/habits.go b/plugin/handou/habits.go index fa8eae6691..209fd6c895 100644 --- a/plugin/handou/habits.go +++ b/plugin/handou/habits.go @@ -3,6 +3,7 @@ package handou import ( "encoding/json" + "errors" "fmt" "math" "math/rand" @@ -15,6 +16,7 @@ import ( "github.com/sirupsen/logrus" ) +// UserHabits 用户习惯 type UserHabits struct { mu sync.RWMutex habits map[string]int // 单字频率 @@ -38,7 +40,7 @@ func initUserHabits() error { if file.IsNotExist(userHabitsFile) { f, err := os.Create(userHabitsFile) if err != nil { - return fmt.Errorf("创建用户习惯库时发生错误: %v", err) + return errors.New("创建用户习惯库时发生错误: " + err.Error()) } _ = f.Close() return saveHabits() @@ -47,7 +49,7 @@ func initUserHabits() error { // 读取现有习惯数据 habitsFile, err := os.ReadFile(userHabitsFile) if err != nil { - return fmt.Errorf("读取用户习惯库时发生错误: %v", err) + return errors.New("读取用户习惯库时发生错误: " + err.Error()) } var savedData struct { @@ -69,7 +71,7 @@ func initUserHabits() error { savedData.TotalWords += count } } else { - return fmt.Errorf("解析用户习惯库时发生错误: %v", err) + return errors.New("解析用户习惯库时发生错误: " + err.Error()) } } From 3eb2a36f3b7eb97053c7095c46e091dc3e0a9ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E6=9F=B3=E7=85=9C?= <101934327+fangliuyu@users.noreply.github.com> Date: Sun, 1 Feb 2026 23:44:26 +0800 Subject: [PATCH 5/6] Add files via upload --- plugin/handou/baiAPI.go | 2 +- plugin/handou/game.go | 4 ++-- plugin/handou/habits.go | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugin/handou/baiAPI.go b/plugin/handou/baiAPI.go index 86ceeb9de9..6004e892c0 100644 --- a/plugin/handou/baiAPI.go +++ b/plugin/handou/baiAPI.go @@ -103,7 +103,7 @@ type baiduAPIData struct { func geiAPIdata(s string) (*idiomJSON, error) { url := "https://hanyuapp.baidu.com/dictapp/swan/termdetail?wd=" + url.QueryEscape(s) + "&client=pc&source_tag=2&lesson_from=xiaodu" - logrus.Warningln(url) + logrus.Debugln(url) data, err := web.GetData(url) if err != nil { return nil, err diff --git a/plugin/handou/game.go b/plugin/handou/game.go index d2e44248ac..81ff252475 100644 --- a/plugin/handou/game.go +++ b/plugin/handou/game.go @@ -289,11 +289,11 @@ func newHandouGame(target idiomJSON) func(string) (bool, []byte, error) { if !ok { newIdiom, err1 := geiAPIdata(s) if err1 != nil { - logrus.Warningln("通过API获取成语信息时发生错误: ", err1) + logrus.Debugln("通过API获取成语信息时发生错误: ", err1) err = errUnknownWord return } - logrus.Warningln("通过API获取成语信息: ", newIdiom.Word) + logrus.Debugln("通过API获取成语信息: ", newIdiom.Word) if newIdiom.Word != "" { idiomInfoMap[newIdiom.Word] = *newIdiom go func() { _ = saveIdiomJSON() }() diff --git a/plugin/handou/habits.go b/plugin/handou/habits.go index 209fd6c895..0fa66414c7 100644 --- a/plugin/handou/habits.go +++ b/plugin/handou/habits.go @@ -156,7 +156,7 @@ func updateHabits(input string) error { // 异步保存到文件 go func() { if err := saveHabits(); err != nil { - logrus.Warn("保存用户习惯时发生错误: ", err) + logrus.Debugln("保存用户习惯时发生错误: ", err) } }() @@ -210,10 +210,11 @@ func calculatePriorityScore(idiom string) float64 { // 4. 考虑成语长度, 让长成语也有机会被选中 idiomScore := 0.0 - if rand.Intn(100) < 80 { + if rand.Intn(100) < 60 { idiomScore = 20 / (1 + 1*math.Abs(float64(charsLenght)-4)) } else { - idiomScore = float64(charsLenght) * 10 + count := 2.0 + float64(rand.Intn(18)) + idiomScore = 100 / (1 + 1*math.Abs(float64(charsLenght)-count)) } finalScore := charsScore + bigramScore + penaltyScore + idiomScore @@ -272,7 +273,7 @@ func prioritizeData(data []string) []string { prioritized := make([]string, limit) for i := range limit { prioritized[i] = idiomScores[startIndex+i].idiom - logrus.Warningf("成语 '%s' 分数=%.2f", + logrus.Debugf("成语 '%s' 分数=%.2f", idiomScores[startIndex+i].idiom, idiomScores[startIndex+i].score) } From 259660f2828b3b5bf04e7fcad750e2cb39a6e9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E6=9F=B3=E7=85=9C?= <101934327+fangliuyu@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:15:46 +0800 Subject: [PATCH 6/6] Change condition to check record length --- plugin/handou/game.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/handou/game.go b/plugin/handou/game.go index 81ff252475..b35017ec12 100644 --- a/plugin/handou/game.go +++ b/plugin/handou/game.go @@ -383,7 +383,7 @@ func newHandouGame(target idiomJSON) func(string) (bool, []byte, error) { tickTruePinyin[i] = string(tickTruePinyinChar) tickExistPinyin[i] = string(tickExistPinyinChar) } - if len(record) == 2 { + if len(record) >= 2 { tickTruePinyin[0] = pinyin[0] tickExistChars[0] = chars[0] }