mirror of
https://github.com/FloatTech/ZeroBot-Plugin.git
synced 2026-02-04 22:41:15 +00:00
Add files via upload
This commit is contained in:
parent
9cb3a5019f
commit
26e57b65c1
182
plugin/handou/baiAPI.go
Normal file
182
plugin/handou/baiAPI.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
677
plugin/handou/game.go
Normal file
677
plugin/handou/game.go
Normal file
@ -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
|
||||||
|
}
|
||||||
348
plugin/handou/habits.go
Normal file
348
plugin/handou/habits.go
Normal file
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user