diff --git a/.env.example b/.env.example index 7fde1d8..488632c 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ ENVIRONMENT=dev PORT=21333 - +DATABASE_PATH="./data/database.db" ENABLE_CONSOLE=true diff --git a/.gitignore b/.gitignore index 9f2daec..8337d30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /.env /data -__pycache__ \ No newline at end of file +__pycache__ + +*.db \ No newline at end of file diff --git a/bot.py b/bot.py index 782c870..e4c56ca 100644 --- a/bot.py +++ b/bot.py @@ -10,6 +10,7 @@ from nonebot.adapters.onebot.v11 import Adapter as OnebotAdapter from konabot.common.log import init_logger from konabot.common.nb.exc import BotExceptionMessage from konabot.common.path import LOG_PATH +from konabot.core.preinit import preinit dotenv.load_dotenv() env = os.environ.get("ENVIRONMENT", "prod") @@ -48,6 +49,9 @@ def main(): nonebot.load_plugins("konabot/plugins") nonebot.load_plugin("nonebot_plugin_analysis_bilibili") + # 预加载 + preinit("konabot/plugins") + nonebot.run() if __name__ == "__main__": diff --git a/konabot/common/database/__init__.py b/konabot/common/database/__init__.py new file mode 100644 index 0000000..2a44469 --- /dev/null +++ b/konabot/common/database/__init__.py @@ -0,0 +1,64 @@ +import os +import sqlite3 +from typing import List, Dict, Any, Optional + +class DatabaseManager: + """超级无敌神奇的数据库!""" + + @classmethod + def query(cls, query: str, params: Optional[tuple] = None) -> List[Dict[str, Any]]: + """执行查询语句并返回结果""" + conn = sqlite3.connect(os.environ.get('DATABASE_PATH', './data/database.db')) + cursor = conn.cursor() + cursor.execute(query, params or ()) + columns = [description[0] for description in cursor.description] + results = [dict(zip(columns, row)) for row in cursor.fetchall()] + cursor.close() + conn.close() + return results + + @classmethod + def query_by_sql_file(cls, file_path: str, params: Optional[tuple] = None) -> List[Dict[str, Any]]: + """从 SQL 文件中读取查询语句并执行""" + with open(file_path, 'r', encoding='utf-8') as f: + query = f.read() + return cls.query(query, params) + + @classmethod + def execute(cls, command: str, params: Optional[tuple] = None) -> None: + """执行非查询语句""" + conn = sqlite3.connect(os.environ.get('DATABASE_PATH', './data/database.db')) + cursor = conn.cursor() + cursor.execute(command, params or ()) + conn.commit() + cursor.close() + conn.close() + + @classmethod + def execute_by_sql_file(cls, file_path: str, params: Optional[tuple] = None) -> None: + """从 SQL 文件中读取非查询语句并执行""" + with open(file_path, 'r', encoding='utf-8') as f: + command = f.read() + # 按照需要执行多条语句 + commands = command.split(';') + for cmd in commands: + cmd = cmd.strip() + if cmd: + cls.execute(cmd, params) + + @classmethod + def execute_many(cls, command: str, seq_of_params: List[tuple]) -> None: + """执行多条非查询语句""" + conn = sqlite3.connect(os.environ.get('DATABASE_PATH', './data/database.db')) + cursor = conn.cursor() + cursor.executemany(command, seq_of_params) + conn.commit() + cursor.close() + conn.close() + + @classmethod + def execute_many_values_by_sql_file(cls, file_path: str, seq_of_params: List[tuple]) -> None: + """从 SQL 文件中读取一条语句,但是被不同值同时执行""" + with open(file_path, 'r', encoding='utf-8') as f: + command = f.read() + cls.execute_many(command, seq_of_params) \ No newline at end of file diff --git a/konabot/core/preinit.py b/konabot/core/preinit.py new file mode 100644 index 0000000..ccfd3f7 --- /dev/null +++ b/konabot/core/preinit.py @@ -0,0 +1,15 @@ +from pathlib import Path + +from nonebot import logger + +def preinit(path: str): + # 执行预初始化,递归找到位于对应路径内文件名为 __preinit__.py 的所有文件都会被执行 + dir_path = Path(path) + for item in dir_path.iterdir(): + if item.is_dir(): + preinit(item) + elif item.is_file() and item.name == "__preinit__.py": + # 动态导入该文件以执行预初始化代码 + module_path = str(item.with_suffix("")).replace("/", ".").replace("\\", ".") + __import__(module_path) + logger.info(f"Preinitialized module: {module_path}") \ No newline at end of file diff --git a/konabot/plugins/air_conditioner/__init__.py b/konabot/plugins/air_conditioner/__init__.py index 4f921fe..e148954 100644 --- a/konabot/plugins/air_conditioner/__init__.py +++ b/konabot/plugins/air_conditioner/__init__.py @@ -7,16 +7,19 @@ from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent from nonebot_plugin_alconna import Alconna, AlconnaMatcher, Args, UniMessage, on_alconna from PIL import Image import numpy as np +from konabot.common.database import DatabaseManager from konabot.common.longtask import DepLongTaskTarget from konabot.common.path import ASSETS_PATH from konabot.common.web_render import WebRenderer from konabot.plugins.air_conditioner.ac import AirConditioner, CrashType, generate_ac_image, wiggle_transform - +from pathlib import Path import random import math +ROOT_PATH = Path(__file__).resolve().parent + def get_ac(id: str) -> AirConditioner: - ac = AirConditioner.air_conditioners.get(id) + ac = AirConditioner.get_ac(id) if ac is None: ac = AirConditioner(id) return ac @@ -61,7 +64,7 @@ evt = on_alconna(Alconna( async def _(event: BaseEvent, target: DepLongTaskTarget): id = target.channel_id ac = get_ac(id) - ac.on = True + ac.update_ac(state=True) await send_ac_image(evt, ac) evt = on_alconna(Alconna( @@ -72,7 +75,7 @@ evt = on_alconna(Alconna( async def _(event: BaseEvent, target: DepLongTaskTarget): id = target.channel_id ac = get_ac(id) - ac.on = False + ac.update_ac(state=False) await send_ac_image(evt, ac) evt = on_alconna(Alconna( @@ -82,6 +85,8 @@ evt = on_alconna(Alconna( @evt.handle() async def _(event: BaseEvent, target: DepLongTaskTarget, temp: Optional[Union[int, float]] = 1): + if temp is None: + temp = 1 if temp <= 0: return id = target.channel_id @@ -89,7 +94,7 @@ async def _(event: BaseEvent, target: DepLongTaskTarget, temp: Optional[Union[in if not ac.on or ac.burnt == True or ac.frozen == True: await send_ac_image(evt, ac) return - ac.temperature += temp + ac.update_ac(temperature_delta=temp) if ac.temperature > 40: # 根据温度随机出是否爆炸,40度开始,呈指数增长 possibility = -math.e ** ((40-ac.temperature) / 50) + 1 @@ -115,6 +120,8 @@ evt = on_alconna(Alconna( @evt.handle() async def _(event: BaseEvent, target: DepLongTaskTarget, temp: Optional[Union[int, float]] = 1): + if temp is None: + temp = 1 if temp <= 0: return id = target.channel_id @@ -122,7 +129,7 @@ async def _(event: BaseEvent, target: DepLongTaskTarget, temp: Optional[Union[in if not ac.on or ac.burnt == True or ac.frozen == True: await send_ac_image(evt, ac) return - ac.temperature -= temp + ac.update_ac(temperature_delta=-temp) if ac.temperature < 0: # 根据温度随机出是否冻结,0度开始,呈指数增长 possibility = -math.e ** (ac.temperature / 50) + 1 @@ -141,6 +148,16 @@ async def _(event: BaseEvent, target: DepLongTaskTarget): ac.change_ac() await send_ac_image(evt, ac) +def query_number_ranking(id: str) -> tuple[int, int]: + result = DatabaseManager.query_by_sql_file( + ROOT_PATH / "sql" / "query_crash_and_rank.sql", + (id,id) + ) + if len(result) == 0: + return 0, 0 + else: + return result[0].values() + evt = on_alconna(Alconna( "空调炸炸排行榜", ), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True) @@ -148,8 +165,9 @@ evt = on_alconna(Alconna( @evt.handle() async def _(event: BaseEvent, target: DepLongTaskTarget): id = target.channel_id - ac = get_ac(id) - number, ranking = ac.get_crashes_and_ranking() + # ac = get_ac(id) + # number, ranking = ac.get_crashes_and_ranking() + number, ranking = query_number_ranking(id) params = { "number": number, "ranking": ranking diff --git a/konabot/plugins/air_conditioner/__preinit__.py b/konabot/plugins/air_conditioner/__preinit__.py new file mode 100644 index 0000000..67054a0 --- /dev/null +++ b/konabot/plugins/air_conditioner/__preinit__.py @@ -0,0 +1,9 @@ +# 预初始化,只要是导入本插件包就会执行这里的代码 +from pathlib import Path + +from konabot.common.database import DatabaseManager + +# 初始化数据库表 +DatabaseManager.execute_by_sql_file( + Path(__file__).resolve().parent / "sql" / "create_table.sql" +) diff --git a/konabot/plugins/air_conditioner/ac.py b/konabot/plugins/air_conditioner/ac.py index 6614784..9be7619 100644 --- a/konabot/plugins/air_conditioner/ac.py +++ b/konabot/plugins/air_conditioner/ac.py @@ -1,20 +1,71 @@ from enum import Enum from io import BytesIO +from pathlib import Path import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont +from konabot.common.database import DatabaseManager from konabot.common.path import ASSETS_PATH, FONTS_PATH from konabot.common.path import DATA_PATH import json +ROOT_PATH = Path(__file__).resolve().parent + class CrashType(Enum): BURNT = 0 FROZEN = 1 class AirConditioner: - air_conditioners: dict[str, "AirConditioner"] = {} + @classmethod + def get_ac(cls, id: str) -> 'AirConditioner': + result = DatabaseManager.query_by_sql_file(ROOT_PATH / "sql" / "query_ac.sql", (id,)) + if len(result) == 0: + ac = cls.create_ac(id) + return ac + ac_data = result[0] + ac = AirConditioner(id) + ac.on = bool(ac_data["on"]) + ac.temperature = float(ac_data["temperature"]) + ac.burnt = bool(ac_data["burnt"]) + ac.frozen = bool(ac_data["frozen"]) + return ac + + @classmethod + def create_ac(cls, id: str) -> 'AirConditioner': + ac = AirConditioner(id) + DatabaseManager.execute_by_sql_file( + ROOT_PATH / "sql" / "insert_ac.sql", + (id, ac.on, ac.temperature, ac.burnt, ac.frozen) + ) + return ac + + def update_ac(self, state: bool = None, temperature_delta: float = None, burnt: bool = None, frozen: bool = None) -> 'AirConditioner': + if state is not None: + self.on = state + if temperature_delta is not None: + self.temperature += temperature_delta + if burnt is not None: + self.burnt = burnt + if frozen is not None: + self.frozen = frozen + DatabaseManager.execute_by_sql_file( + ROOT_PATH / "sql" / "update_ac.sql", + (self.on, self.temperature, self.burnt, self.frozen, self.id) + ) + return self + + def change_ac(self) -> 'AirConditioner': + self.on = False + self.temperature = 24 + self.burnt = False + self.frozen = False + DatabaseManager.execute_by_sql_file( + ROOT_PATH / "sql" / "update_ac.sql", + (self.on, self.temperature, self.burnt, self.frozen, self.id) + ) + return self def __init__(self, id: str) -> None: self.id = id @@ -22,45 +73,40 @@ class AirConditioner: self.temperature = 24 # 默认温度 self.burnt = False self.frozen = False - AirConditioner.air_conditioners[id] = self - - def change_ac(self): - self.burnt = False - self.frozen = False - self.on = False - self.temperature = 24 # 重置为默认温度 def broke_ac(self, crash_type: CrashType): ''' - 让空调坏掉,并保存数据 - + 让空调坏掉 :param crash_type: CrashType 枚举,表示空调坏掉的类型 ''' match crash_type: case CrashType.BURNT: - self.burnt = True + self.update_ac(burnt=True) case CrashType.FROZEN: - self.frozen = True - self.save_crash_data(crash_type) + self.update_ac(frozen=True) + DatabaseManager.execute_by_sql_file( + ROOT_PATH / "sql" / "insert_crash.sql", + (self.id, crash_type.value) + ) - def save_crash_data(self, crash_type: CrashType): - ''' - 如果空调爆炸了,就往本地的 ac_crash_data.json 里该 id 的记录加一 - ''' - data_file = DATA_PATH / "ac_crash_data.json" - crash_data = {} - if data_file.exists(): - with open(data_file, "r", encoding="utf-8") as f: - crash_data = json.load(f) - if self.id not in crash_data: - crash_data[self.id] = {"burnt": 0, "frozen": 0} - match crash_type: - case CrashType.BURNT: - crash_data[self.id]["burnt"] += 1 - case CrashType.FROZEN: - crash_data[self.id]["frozen"] += 1 - with open(data_file, "w", encoding="utf-8") as f: - json.dump(crash_data, f, ensure_ascii=False, indent=4) + # def save_crash_data(self, crash_type: CrashType): + # ''' + # 如果空调爆炸了,就往本地的 ac_crash_data.json 里该 id 的记录加一 + # ''' + # data_file = DATA_PATH / "ac_crash_data.json" + # crash_data = {} + # if data_file.exists(): + # with open(data_file, "r", encoding="utf-8") as f: + # crash_data = json.load(f) + # if self.id not in crash_data: + # crash_data[self.id] = {"burnt": 0, "frozen": 0} + # match crash_type: + # case CrashType.BURNT: + # crash_data[self.id]["burnt"] += 1 + # case CrashType.FROZEN: + # crash_data[self.id]["frozen"] += 1 + # with open(data_file, "w", encoding="utf-8") as f: + # json.dump(crash_data, f, ensure_ascii=False, indent=4) def get_crashes_and_ranking(self) -> tuple[int, int]: ''' diff --git a/konabot/plugins/air_conditioner/sql/create_table.sql b/konabot/plugins/air_conditioner/sql/create_table.sql new file mode 100644 index 0000000..5203e23 --- /dev/null +++ b/konabot/plugins/air_conditioner/sql/create_table.sql @@ -0,0 +1,15 @@ +-- 创建所有表 +CREATE TABLE IF NOT EXISTS air_conditioner ( + id VARCHAR(128) PRIMARY KEY, + 'on' BOOLEAN NOT NULL, + temperature REAL NOT NULL, + burnt BOOLEAN NOT NULL, + frozen BOOLEAN NOT NULL +); + +CREATE TABLE IF NOT EXISTS air_conditioner_crash_log ( + id VARCHAR(128) NOT NULL, + crash_type INT NOT NULL, + timestamp DATETIME NOT NULL, + FOREIGN KEY (id) REFERENCES air_conditioner(id) +); \ No newline at end of file diff --git a/konabot/plugins/air_conditioner/sql/insert_ac.sql b/konabot/plugins/air_conditioner/sql/insert_ac.sql new file mode 100644 index 0000000..3fb1c76 --- /dev/null +++ b/konabot/plugins/air_conditioner/sql/insert_ac.sql @@ -0,0 +1,3 @@ +-- 插入一台新空调 +INSERT INTO air_conditioner (id, 'on', temperature, burnt, frozen) +VALUES (?, ?, ?, ?, ?); \ No newline at end of file diff --git a/konabot/plugins/air_conditioner/sql/insert_crash.sql b/konabot/plugins/air_conditioner/sql/insert_crash.sql new file mode 100644 index 0000000..aae3898 --- /dev/null +++ b/konabot/plugins/air_conditioner/sql/insert_crash.sql @@ -0,0 +1,3 @@ +-- 插入一条空调爆炸记录 +INSERT INTO air_conditioner_crash_log (id, crash_type, timestamp) +VALUES (?, ?, CURRENT_TIMESTAMP); \ No newline at end of file diff --git a/konabot/plugins/air_conditioner/sql/query_ac.sql b/konabot/plugins/air_conditioner/sql/query_ac.sql new file mode 100644 index 0000000..db957d3 --- /dev/null +++ b/konabot/plugins/air_conditioner/sql/query_ac.sql @@ -0,0 +1,4 @@ +-- 查询空调状态,如果没有就插入一条新的记录 +SELECT * +FROM air_conditioner +WHERE id = ?; \ No newline at end of file diff --git a/konabot/plugins/air_conditioner/sql/query_crash_and_rank.sql b/konabot/plugins/air_conditioner/sql/query_crash_and_rank.sql new file mode 100644 index 0000000..c180638 --- /dev/null +++ b/konabot/plugins/air_conditioner/sql/query_crash_and_rank.sql @@ -0,0 +1,23 @@ +-- 从 air_conditioner_crash_log 表中获取指定 id 损坏的次数以及损坏次数的排名 +SELECT crash_count, crash_rank +FROM ( + SELECT id, + COUNT(*) AS crash_count, + RANK() OVER (ORDER BY COUNT(*) DESC) AS crash_rank + FROM air_conditioner_crash_log + GROUP BY id +) AS ranked_data +WHERE id = ? +-- 如果该 id 没有损坏记录,则返回 0 次损坏和对应的最后一名 +UNION +SELECT 0 AS crash_count, + (SELECT COUNT(DISTINCT id) + 1 FROM air_conditioner_crash_log) AS crash_rank +FROM ( + SELECT DISTINCT id + FROM air_conditioner_crash_log +) AS ranked_data +WHERE NOT EXISTS ( + SELECT 1 + FROM air_conditioner_crash_log + WHERE id = ? +); \ No newline at end of file diff --git a/konabot/plugins/air_conditioner/sql/update_ac.sql b/konabot/plugins/air_conditioner/sql/update_ac.sql new file mode 100644 index 0000000..df9145e --- /dev/null +++ b/konabot/plugins/air_conditioner/sql/update_ac.sql @@ -0,0 +1,4 @@ +-- 更新空调状态 +UPDATE air_conditioner +SET 'on' = ?, temperature = ?, burnt = ?, frozen = ? +WHERE id = ?; \ No newline at end of file diff --git a/konabot/plugins/idiomgame/__init__.py b/konabot/plugins/idiomgame/__init__.py index ee4e26c..36710aa 100644 --- a/konabot/plugins/idiomgame/__init__.py +++ b/konabot/plugins/idiomgame/__init__.py @@ -18,11 +18,14 @@ from nonebot_plugin_alconna import ( on_alconna, ) +from konabot.common.database import DatabaseManager from konabot.common.longtask import DepLongTaskTarget from konabot.common.path import ASSETS_PATH from konabot.common.llm import get_llm +ROOT_PATH = Path(__file__).resolve().parent + DATA_DIR = Path(__file__).parent.parent.parent.parent / "data" DATA_FILE_PATH = ( @@ -94,18 +97,19 @@ class IdiomGameLLM: @classmethod async def storage_idiom(cls, idiom: str): - # 将 idiom 存入本地文件以备后续分析 - with open(DATA_DIR / "idiom_llm_storage.txt", "a", encoding="utf-8") as f: - f.write(idiom + "\n") - IdiomGame.append_into_word_list(idiom) + # 将 idiom 存入数据库 + DatabaseManager.execute_by_sql_file( + ROOT_PATH / "sql" / "insert_custom_word.sql", + (idiom,) + ) class IdiomGame: - ALL_WORDS = [] # 所有四字词语 - ALL_IDIOMS = [] # 所有成语 + # ALL_WORDS = [] # 所有四字词语 + # ALL_IDIOMS = [] # 所有成语 INSTANCE_LIST: dict[str, "IdiomGame"] = {} # 群号对应的游戏实例 - IDIOM_FIRST_CHAR = {} # 所有成语包括词语的首字字典 - AVALIABLE_IDIOM_FIRST_CHAR = {} # 真正有效的成语首字字典 + # IDIOM_FIRST_CHAR = {} # 所有成语包括词语的首字字典 + # AVALIABLE_IDIOM_FIRST_CHAR = {} # 真正有效的成语首字字典 __inited = False @@ -130,11 +134,10 @@ class IdiomGame: ''' 将一个新词加入到词语列表中 ''' - if word not in cls.ALL_WORDS: - cls.ALL_WORDS.append(word) - if word[0] not in cls.IDIOM_FIRST_CHAR: - cls.IDIOM_FIRST_CHAR[word[0]] = [] - cls.IDIOM_FIRST_CHAR[word[0]].append(word) + DatabaseManager.execute_by_sql_file( + ROOT_PATH / "sql" / "insert_custom_word.sql", + (word,) + ) def be_able_to_play(self) -> bool: if self.last_play_date != datetime.date.today(): @@ -145,11 +148,17 @@ class IdiomGame: return True return False + @staticmethod + def random_idiom() -> str: + return DatabaseManager.query_by_sql_file( + ROOT_PATH / "sql" / "random_choose_idiom.sql" + )[0]["idiom"] + def choose_start_idiom(self) -> str: """ 随机选择一个成语作为起始成语 """ - self.last_idiom = secrets.choice(IdiomGame.ALL_IDIOMS) + self.last_idiom = IdiomGame.random_idiom() self.last_char = self.last_idiom[-1] if not self.is_nextable(self.last_char): self.choose_start_idiom() @@ -208,7 +217,7 @@ class IdiomGame: return self.last_idiom def _skip_idiom_async(self) -> str: - self.last_idiom = secrets.choice(IdiomGame.ALL_IDIOMS) + self.last_idiom = IdiomGame.random_idiom() self.last_char = self.last_idiom[-1] if not self.is_nextable(self.last_char): self._skip_idiom_async() @@ -228,8 +237,11 @@ class IdiomGame: """ 判断是否有成语可以接 """ - return last_char in IdiomGame.AVALIABLE_IDIOM_FIRST_CHAR - + return DatabaseManager.query_by_sql_file( + ROOT_PATH / "sql" / "is_nextable.sql", + (last_char,) + )[0]["DEED"] == 1 + def add_already_idiom(self, idiom: str): if idiom in self.already_idioms: self.already_idioms[idiom] += 1 @@ -259,7 +271,12 @@ class IdiomGame: if idiom[0] != self.last_char: state.append(TryVerifyState.WRONG_FIRST_CHAR) return state - if idiom not in IdiomGame.ALL_IDIOMS and idiom not in IdiomGame.ALL_WORDS: + # 成语是否存在 + result = DatabaseManager.query_by_sql_file( + ROOT_PATH / "sql" / "query_idiom.sql", + (idiom, idiom, idiom) + )[0]["status"] + if result == -1: logger.info(f"用户 {user_id} 发送了未知词语 {idiom},正在使用 LLM 进行验证") try: if not await IdiomGameLLM.verify_idiom_with_llm(idiom): @@ -281,7 +298,7 @@ class IdiomGame: self.last_idiom = idiom self.last_char = idiom[-1] self.add_score(user_id, 1 * score_k) # 先加 1 分 - if idiom in IdiomGame.ALL_IDIOMS: + if result == 1: state.append(TryVerifyState.VERIFIED_AND_REAL) self.add_score(user_id, 4 * score_k) # 再加 4 分 self.remain_rounds -= 1 @@ -319,14 +336,21 @@ class IdiomGame: @classmethod def random_idiom_starting_with(cls, first_char: str) -> Optional[str]: cls.init_lexicon() - if first_char not in cls.AVALIABLE_IDIOM_FIRST_CHAR: + result = DatabaseManager.query_by_sql_file( + ROOT_PATH / "sql" / "query_idiom_start_with.sql", + (first_char,) + ) + if len(result) == 0: return None - return secrets.choice(cls.AVALIABLE_IDIOM_FIRST_CHAR[first_char]) + return result[0]["idiom"] @classmethod def init_lexicon(cls): if cls.__inited: return + DatabaseManager.execute_by_sql_file( + ROOT_PATH / "sql" / "create_table.sql" + ) # 确保数据库初始化 cls.__inited = True # 成语大表 @@ -334,11 +358,12 @@ class IdiomGame: ALL_IDIOMS_INFOS = json.load(f) # 词语大表 + ALL_WORDS = [] with open(ASSETS_PATH / "lexicon" / "ci.json", "r", encoding="utf-8") as f: jsonData = json.load(f) - cls.ALL_WORDS = [item["ci"] for item in jsonData] - logger.debug(f"Loaded {len(cls.ALL_WORDS)} words from ci.json") - logger.debug(f"Sample words: {cls.ALL_WORDS[:5]}") + ALL_WORDS = [item["ci"] for item in jsonData] + logger.debug(f"Loaded {len(ALL_WORDS)} words from ci.json") + logger.debug(f"Sample words: {ALL_WORDS[:5]}") COMMON_WORDS = [] # 读取 COMMON 词语大表 @@ -389,29 +414,44 @@ class IdiomGame: logger.debug(f"Loaded additional {len(LOCAL_LLM_WORDS)} words from idiom_llm_storage.txt") # 只有成语的大表 - cls.ALL_IDIOMS = [idiom["word"] for idiom in ALL_IDIOMS_INFOS] + THUOCL_IDIOMS - cls.ALL_IDIOMS = list(set(cls.ALL_IDIOMS)) # 去重 + ALL_IDIOMS = [idiom["word"] for idiom in ALL_IDIOMS_INFOS] + THUOCL_IDIOMS + ALL_IDIOMS = list(set(ALL_IDIOMS)) # 去重 + # 批量插入数据库 + DatabaseManager.execute_many_values_by_sql_file( + ROOT_PATH / "sql" / "insert_idiom.sql", + [(idiom,) for idiom in ALL_IDIOMS] + ) + # 其他四字词语表,仅表示可以有这个词 - cls.ALL_WORDS = ( - [word for word in cls.ALL_WORDS if len(word) == 4] + ALL_WORDS = ( + [word for word in ALL_WORDS if len(word) == 4] + THUOCL_WORDS + COMMON_WORDS - + LOCAL_LLM_WORDS ) - cls.ALL_WORDS = list(set(cls.ALL_WORDS)) # 去重 + # 插入数据库 + DatabaseManager.execute_many_values_by_sql_file( + ROOT_PATH / "sql" / "insert_word.sql", + [(word,) for word in ALL_WORDS] + ) - # 根据成语大表,划分出成语首字字典 - for idiom in cls.ALL_IDIOMS + cls.ALL_WORDS: - if idiom[0] not in cls.IDIOM_FIRST_CHAR: - cls.IDIOM_FIRST_CHAR[idiom[0]] = [] - cls.IDIOM_FIRST_CHAR[idiom[0]].append(idiom) + # 自定义词语 LOCAL_LLM_WORDS 插入数据库,兼容用 + DatabaseManager.execute_many_values_by_sql_file( + ROOT_PATH / "sql" / "insert_custom_word.sql", + [(word,) for word in LOCAL_LLM_WORDS] + ) - # 根据真正的成语大表,划分出有效成语首字字典 - for idiom in cls.ALL_IDIOMS: - if idiom[0] not in cls.AVALIABLE_IDIOM_FIRST_CHAR: - cls.AVALIABLE_IDIOM_FIRST_CHAR[idiom[0]] = [] - cls.AVALIABLE_IDIOM_FIRST_CHAR[idiom[0]].append(idiom) + # # 根据成语大表,划分出成语首字字典 + # for idiom in cls.ALL_IDIOMS + cls.ALL_WORDS: + # if idiom[0] not in cls.IDIOM_FIRST_CHAR: + # cls.IDIOM_FIRST_CHAR[idiom[0]] = [] + # cls.IDIOM_FIRST_CHAR[idiom[0]].append(idiom) + + # # 根据真正的成语大表,划分出有效成语首字字典 + # for idiom in cls.ALL_IDIOMS: + # if idiom[0] not in cls.AVALIABLE_IDIOM_FIRST_CHAR: + # cls.AVALIABLE_IDIOM_FIRST_CHAR[idiom[0]] = [] + # cls.AVALIABLE_IDIOM_FIRST_CHAR[idiom[0]].append(idiom) evt = on_alconna( @@ -514,7 +554,9 @@ async def end_game(event: BaseEvent, group_id: str): for line in history_lines: result_text += line + "\n" await evt.send(await result_text.export()) - instance.clear_score_board() + # instance.clear_score_board() + # 将实例删除 + del IdiomGame.INSTANCE_LIST[group_id] evt = on_alconna( diff --git a/konabot/plugins/idiomgame/sql/create_table.sql b/konabot/plugins/idiomgame/sql/create_table.sql new file mode 100644 index 0000000..5d38580 --- /dev/null +++ b/konabot/plugins/idiomgame/sql/create_table.sql @@ -0,0 +1,15 @@ +-- 创建成语大表 +CREATE TABLE IF NOT EXISTS all_idioms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + idiom VARCHAR(128) NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS all_words ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + word VARCHAR(128) NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS custom_words ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + word VARCHAR(128) NOT NULL UNIQUE +); \ No newline at end of file diff --git a/konabot/plugins/idiomgame/sql/insert_custom_word.sql b/konabot/plugins/idiomgame/sql/insert_custom_word.sql new file mode 100644 index 0000000..212c8a2 --- /dev/null +++ b/konabot/plugins/idiomgame/sql/insert_custom_word.sql @@ -0,0 +1,3 @@ +-- 插入自定义词 +INSERT OR IGNORE INTO custom_words (word) +VALUES (?); \ No newline at end of file diff --git a/konabot/plugins/idiomgame/sql/insert_idiom.sql b/konabot/plugins/idiomgame/sql/insert_idiom.sql new file mode 100644 index 0000000..eaedae8 --- /dev/null +++ b/konabot/plugins/idiomgame/sql/insert_idiom.sql @@ -0,0 +1,3 @@ +-- 插入成语大表,避免重复插入 +INSERT OR IGNORE INTO all_idioms (idiom) +VALUES (?); \ No newline at end of file diff --git a/konabot/plugins/idiomgame/sql/insert_word.sql b/konabot/plugins/idiomgame/sql/insert_word.sql new file mode 100644 index 0000000..b085aab --- /dev/null +++ b/konabot/plugins/idiomgame/sql/insert_word.sql @@ -0,0 +1,3 @@ +-- 插入词 +INSERT OR IGNORE INTO all_words (word) +VALUES (?); \ No newline at end of file diff --git a/konabot/plugins/idiomgame/sql/is_nextable.sql b/konabot/plugins/idiomgame/sql/is_nextable.sql new file mode 100644 index 0000000..a7bbeb1 --- /dev/null +++ b/konabot/plugins/idiomgame/sql/is_nextable.sql @@ -0,0 +1,5 @@ +-- 查询是否有以 xx 开头的成语,有则返回真,否则假 +SELECT EXISTS( + SELECT 1 FROM all_idioms + WHERE idiom LIKE ? || '%' +) AS DEED; diff --git a/konabot/plugins/idiomgame/sql/query_idiom.sql b/konabot/plugins/idiomgame/sql/query_idiom.sql new file mode 100644 index 0000000..fa3bf93 --- /dev/null +++ b/konabot/plugins/idiomgame/sql/query_idiom.sql @@ -0,0 +1,7 @@ +-- 查询成语是否在 all_idioms 中,如果存在则返回 1,否则再判断是否在 custom_words 或 all_words 中,存在则返回 0,否则返回 -1 +SELECT + CASE + WHEN EXISTS (SELECT 1 FROM all_idioms WHERE idiom = ?) THEN 1 + WHEN EXISTS (SELECT 1 FROM custom_words WHERE word = ?) OR EXISTS (SELECT 1 FROM all_words WHERE word = ?) THEN 0 + ELSE -1 + END AS status; \ No newline at end of file diff --git a/konabot/plugins/idiomgame/sql/query_idiom_start_with.sql b/konabot/plugins/idiomgame/sql/query_idiom_start_with.sql new file mode 100644 index 0000000..a6e8fc6 --- /dev/null +++ b/konabot/plugins/idiomgame/sql/query_idiom_start_with.sql @@ -0,0 +1,4 @@ +-- 查询以 xx 开头的成语,随机打乱后只取第一个 +SELECT idiom FROM all_idioms +WHERE idiom LIKE ? || '%' +ORDER BY RANDOM() LIMIT 1; \ No newline at end of file diff --git a/konabot/plugins/idiomgame/sql/random_choose_idiom.sql b/konabot/plugins/idiomgame/sql/random_choose_idiom.sql new file mode 100644 index 0000000..f706092 --- /dev/null +++ b/konabot/plugins/idiomgame/sql/random_choose_idiom.sql @@ -0,0 +1,2 @@ +-- 随机从 all_idioms 表中选择一个成语 +SELECT idiom FROM all_idioms ORDER BY RANDOM() LIMIT 1; \ No newline at end of file