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]
}