Files
konabot/commit_f21da65.diff

849 lines
30 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

commit f21da657dbc79c2d139265a69696e5ad213f5c53
Author: MixBadGun <1059129006@qq.com>
Date: Tue Nov 18 19:36:05 2025 +0800
database 接入
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,66 +1,112 @@
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
- def __init__(self, id: str) -> None:
- self.id = id
+ @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.temperature = 24
self.burnt = False
self.frozen = False
- AirConditioner.air_conditioners[id] = self
+ 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):
+ def __init__(self, id: str) -> None:
+ self.id = id
+ self.on = False
+ self.temperature = 24 # 默认温度
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]
+ )
+
+ # 自定义词语 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 + 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 + 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)
+ # # 根据真正的成语大表,划分出有效成语首字字典
+ # 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