Compare commits

...

10 Commits

5 changed files with 109 additions and 34 deletions

View File

@@ -18,24 +18,21 @@ from ..CoreModules.database import (
class ClockScheduler: class ClockScheduler:
_instance: Optional["ClockScheduler"] = None
def __init__(self) -> None: def __init__(self) -> None:
self._db: Database = get_db() self._db: Database = get_db()
self._config: ProjectConfig = Architecture.Get(ProjectConfig) self._config: ProjectConfig = Architecture.Get(ProjectConfig)
self._logger = self._config self._logger = self._config
self._tick_ms = max(int(self._config.FindItem("scheduler_tick_ms", 1000)), 10) self._tick_ms = int(self._config.FindItem("scheduler_tick_ms", 1000*60))
self._batch_size = max(int(self._config.FindItem("scheduler_max_batch", 50)), 1) self._batch_size = int(self._config.FindItem("scheduler_max_batch", 1000))
self._loop_task: Optional[asyncio.Task] = None self._loop_task: Optional[asyncio.Task] = None
self._running = False self._running = False
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
Architecture.Register(ClockScheduler, self, lambda: None)
self._config.SaveProperties() self._config.SaveProperties()
@classmethod @classmethod
def instance(cls) -> "ClockScheduler": def instance(cls) -> "ClockScheduler":
if not Architecture.Contains(ClockScheduler): if not Architecture.Contains(ClockScheduler):
cls._instance = ClockScheduler() Architecture.Register(ClockScheduler, ClockScheduler(), lambda: None)
return Architecture.Get(ClockScheduler) return Architecture.Get(ClockScheduler)
async def start(self) -> None: async def start(self) -> None:
@@ -114,7 +111,6 @@ class ClockScheduler:
try: try:
self._db.update_task_status(task_id, STATUS_RUNNING, attempts=attempts) self._db.update_task_status(task_id, STATUS_RUNNING, attempts=attempts)
await self._execute_task(task) await self._execute_task(task)
self._db.update_task_status(task_id, STATUS_COMPLETED, attempts=attempts)
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
message = f"{type(exc).__name__}: {exc}" message = f"{type(exc).__name__}: {exc}"
self._logger.Log( self._logger.Log(
@@ -129,30 +125,35 @@ class ClockScheduler:
) )
async def _execute_task(self, task: Any) -> None: async def _execute_task(self, task: Any) -> None:
task_id = int(task["id"])
attempts = int(task["attempts"])
plugin_module = task["plugin_module"] plugin_module = task["plugin_module"]
plugin_class = task["plugin_class"] plugin_class = task["plugin_class"]
callback_name = task["callback_name"] callback_name = task["callback_name"]
payload_raw = task["payload"] payload_raw = task["payload"]
args, kwargs = self._decode_payload(payload_raw) args, kwargs = self._decode_payload(payload_raw)
callback = self._resolve_callback(plugin_module, plugin_class, callback_name) callback = self._resolve_callback(plugin_module, plugin_class, callback_name)
if not callback:
self._db.update_task_status(task_id, STATUS_FAILED, attempts=attempts+1)
return
result = callback(*args, **kwargs) result = callback(*args, **kwargs)
if inspect.isawaitable(result): if inspect.isawaitable(result):
await result await result
self._db.update_task_status(task_id, STATUS_COMPLETED, attempts=attempts)
def _resolve_callback( def _resolve_callback(
self, self,
plugin_module: str, plugin_module: str,
plugin_class: Optional[str], plugin_class: Optional[str],
callback_name: str, callback_name: str,
) -> Callable[..., Any]: ) -> Optional[Callable[..., Any]]:
callback: Optional[Callable[..., Any]] = None
module = importlib.import_module(plugin_module) module = importlib.import_module(plugin_module)
if plugin_class: if plugin_class:
target_class = getattr(module, plugin_class) target_class = getattr(module, plugin_class)
if Architecture.Contains(target_class): if Architecture.Contains(target_class):
instance = Architecture.Get(target_class) instance = Architecture.Get(target_class)
else: callback = getattr(instance, callback_name)
instance = target_class()
callback = getattr(instance, callback_name)
else: else:
callback = getattr(module, callback_name) callback = getattr(module, callback_name)
return callback return callback

View File

@@ -22,7 +22,7 @@ class DatabaseModel(BaseModel):
class PluginInterface(ABC): class PluginInterface(ABC):
plugin_instances: Dict[str, "PluginInterface"] = {} plugin_instances: Dict[str, "PluginInterface"] = {}
async def callback(self, message: str, chat_id: int, user_id: int) -> str|None: async def callback(self, message: str|None|Literal[""], chat_id: int, user_id: int) -> str|None:
''' '''
继承后重写该方法接受消息并返回消息 继承后重写该方法接受消息并返回消息
返回空字符串代表不进行反馈 返回空字符串代表不进行反馈
@@ -35,35 +35,87 @@ class PluginInterface(ABC):
return "" return ""
@final @final
def execute(self, path:str) -> Optional[APIRouter]: def register_table(self, db_model: DatabaseModel) -> None:
db = get_db()
cursor = db.conn.cursor()
sql = f"CREATE TABLE IF NOT EXISTS {db_model.table_name} ({', '.join([f'{name} {field_def}' for name, field_def in db_model.column_defs.items()])})"
config.Log("Info", f"{ConsoleFrontColor.LIGHTMAGENTA_EX}为表 {db_model.table_name} 创建: {sql}{ConsoleFrontColor.RESET}")
try:
cursor.execute(sql)
except Exception as e:
config.Log("Error", f"{ConsoleFrontColor.RED}为表 {db_model.table_name} 创建失败: {e}{ConsoleFrontColor.RESET}")
try:
cursor.execute(f"PRAGMA table_info({db_model.table_name})")
existing_columns = {row["name"] for row in cursor.fetchall()}
except Exception as e:
config.Log("Error", f"{ConsoleFrontColor.RED}查询表 {db_model.table_name} 列信息失败: {e}{ConsoleFrontColor.RESET}")
return
constraint_keywords = ("PRIMARY KEY", "FOREIGN KEY", "UNIQUE", "CHECK")
for column_name, column_def in db_model.column_defs.items():
column_name_upper = column_name.upper()
column_def_upper = column_def.upper()
if any(keyword in column_name_upper for keyword in constraint_keywords) or any(
column_def_upper.startswith(keyword) for keyword in constraint_keywords
):
continue
if " " in column_name or "(" in column_name:
continue
if column_name in existing_columns:
continue
alter_sql = f"ALTER TABLE {db_model.table_name} ADD COLUMN {column_name} {column_def}"
config.Log(
"Info",
f"{ConsoleFrontColor.LIGHTMAGENTA_EX}为表 {db_model.table_name} 添加缺失列: {alter_sql}{ConsoleFrontColor.RESET}",
)
try:
cursor.execute(alter_sql)
except Exception as e:
config.Log("Error", f"{ConsoleFrontColor.RED}为表 {db_model.table_name} 添加列 {column_name} 失败: {e}{ConsoleFrontColor.RESET}")
continue
try:
db.conn.commit()
except Exception as e:
config.Log("Error", f"{ConsoleFrontColor.RED}提交表 {db_model.table_name} 列更新失败: {e}{ConsoleFrontColor.RESET}")
@final
def execute(self) -> Optional[APIRouter]:
''' '''
继承后是否返回路由决定是否启动该插件 继承后是否返回路由决定是否启动该插件
若返回None, 则不启动该插件 若返回None, 则不启动该插件
''' '''
Architecture.Register(self.__class__, self, self.wake_up, *self.dependencies())
router = APIRouter()
router.post(path)(self.generate_router_callback())
# 在数据库保证必要的表和列存在
db = get_db()
db_model = self.register_db_model()
if db_model:
cursor = db.conn.cursor()
sql = f"CREATE TABLE IF NOT EXISTS {db_model.table_name} ({', '.join([f'{name} {field_def}' for name, field_def in db_model.column_defs.items()])})"
config.Log("Info", f"{ConsoleFrontColor.LIGHTMAGENTA_EX}为表 {db_model.table_name} 创建: {sql}{ConsoleFrontColor.RESET}")
try:
cursor.execute(sql)
except Exception as e:
config.Log("Error", f"{ConsoleFrontColor.RED}为表 {db_model.table_name} 创建失败: {e}{ConsoleFrontColor.RESET}")
def setup() -> None:
# 在数据库保证必要的表和列存在
db_model = self.register_db_model()
if db_model is None:
pass
elif isinstance(db_model, DatabaseModel):
self.register_table(db_model)
else:
for model in db_model:
self.register_table(model)
self.wake_up()
Architecture.Register(self.__class__, self, setup, *self.dependencies())
router = APIRouter()
router.post(f"/{self.__class__.__name__}/callback")(self.generate_router_callback())
if self.generate_router_illustrated_guide() is not None:
router.get(f"/{self.__class__.__name__}")(self.generate_router_illustrated_guide())
return router return router
def generate_router_callback(self) -> Callable|Coroutine: def generate_router_callback(self) -> Callable|Coroutine:
''' '''
继承后重写该方法生成路由回调函数 继承后重写该方法生成路由回调函数
''' '''
async def callback(message: str, chat_id: int, user_id: int) -> Any: return self.callback
return await self.callback(message, chat_id, user_id)
return callback def generate_router_illustrated_guide(self) -> Callable|Coroutine|None:
'''
继承后重写该方法生成渲染图鉴与攻略网页的函数
'''
return None
def dependencies(self) -> List[Type]: def dependencies(self) -> List[Type]:
''' '''
@@ -89,7 +141,7 @@ class PluginInterface(ABC):
config.Log("Info", f"插件{self.__class__.__name__}已注册命令{command}") config.Log("Info", f"插件{self.__class__.__name__}已注册命令{command}")
PluginInterface.plugin_instances[command] = self PluginInterface.plugin_instances[command] = self
def register_db_model(self) -> Optional[DatabaseModel]: def register_db_model(self) -> List[DatabaseModel]|DatabaseModel|None:
''' '''
继承后重写该方法注册数据库模型 继承后重写该方法注册数据库模型
''' '''
@@ -172,7 +224,7 @@ def ImportPlugins(app: FastAPI, plugin_dir:str = "Plugins") -> None:
plugin = plugin_class() plugin = plugin_class()
if plugin.is_enable_plugin() == False: if plugin.is_enable_plugin() == False:
continue continue
router = plugin.execute(f"/{module_file.GetFullPath().replace(".py", '')}/{class_name}") router = plugin.execute()
if router: if router:
app.include_router(router, prefix=f"/api", tags=[plugin.get_plugin_tag()]) app.include_router(router, prefix=f"/api", tags=[plugin.get_plugin_tag()])
except Exception as e: except Exception as e:

View File

@@ -4,6 +4,7 @@ from ..Convention.Runtime.Architecture import Architecture
from fastapi import APIRouter, Request from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import re
from ..CoreModules.models import CallbackRequest from ..CoreModules.models import CallbackRequest
from ..CoreModules.plugin_interface import PluginInterface from ..CoreModules.plugin_interface import PluginInterface
@@ -21,13 +22,25 @@ async def callback_verify():
logger.Log("Info", "收到Callback验证请求") logger.Log("Info", "收到Callback验证请求")
return JSONResponse({"result": "ok"}) return JSONResponse({"result": "ok"})
# 机器人名称模式(用于从@消息中提取,兼容半角/全角空格)
AT_PATTERN = re.compile(r'@[^\s]+[\s\u3000]+(.+)', re.DOTALL)
def parse_message_after_at(message: str) -> str:
# 去除首尾空格
message = message.strip()
# 尝试提取@后的内容
at_match = AT_PATTERN.search(message)
if at_match:
return at_match.group(1).strip()
return message
@router.post("/callback/construct") @router.post("/callback/construct")
async def callback_receive_construct(callback_data: CallbackRequest): async def callback_receive_construct(callback_data: CallbackRequest):
"""以构造好的Callback消息进行处理, 已知方式""" """以构造好的Callback消息进行处理, 已知方式"""
try: try:
# 解析指令 # 解析指令
content = callback_data.content content = parse_message_after_at(callback_data.content)
command = content.split(" ")[0] command = content.split(" ")[0]
message = content[len(command):].strip() message = content[len(command):].strip()
logger.Log("Info", f"识别指令: command={command}") logger.Log("Info", f"识别指令: command={command}")
@@ -65,7 +78,7 @@ async def callback_receive(request: Request):
return JSONResponse({"result": "error", "message": str(e)}) return JSONResponse({"result": "error", "message": str(e)})
# 解析指令 # 解析指令
content = callback_data.content content = parse_message_after_at(callback_data.content)
command = content.split(" ")[0] command = content.split(" ")[0]
message = content[len(command):].strip() message = content[len(command):].strip()
logger.Log("Info", f"识别指令: command={command}") logger.Log("Info", f"识别指令: command={command}")
@@ -103,6 +116,9 @@ async def handle_command(command: str, message: str,
if plugin: if plugin:
logger.Log("Info", f"已找到插件注册指令: {command}, class: {plugin.__class__.__name__}") logger.Log("Info", f"已找到插件注册指令: {command}, class: {plugin.__class__.__name__}")
return await plugin.callback(message, chat_id, user_id) return await plugin.callback(message, chat_id, user_id)
elif "default" in PluginInterface.plugin_instances:
logger.Log("Info", f"未找到插件注册指令: {command}, 使用默认插件: {PluginInterface.plugin_instances["default"].__class__.__name__}")
return await PluginInterface.plugin_instances["default"].callback(command+" "+message, chat_id, user_id)
else: else:
return f"❌ 未识别指令: {command}" return f"❌ 未识别指令: {command}"
except Exception as e: except Exception as e:

3
swagger-ui-bundle.js Normal file

File diff suppressed because one or more lines are too long

3
swagger-ui.css Normal file

File diff suppressed because one or more lines are too long