diff --git a/plugins/permission_example/_manifest.json b/plugins/permission_example/_manifest.json deleted file mode 100644 index 30564d391..000000000 --- a/plugins/permission_example/_manifest.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "manifest_version": 1, - "name": "权限示例插件 (Permission Example Plugin)", - "version": "1.0.0", - "description": "MaiCore权限系统演示插件,包含权限节点注册、权限检查和多种权限命令示例。", - "author": { - "name": "MoFox-Studio", - "url": "https://github.com/MoFox-Studio" - }, - "license": "GPL-v3.0-or-later", - "host_application": { - "min_version": "0.10.0" - }, - "keywords": ["permission", "example", "权限", "admin", "user", "master", "demo", "tutorial"], - "categories": ["Examples", "Tutorial", "Permission"], - "default_locale": "zh-CN", - "locales_path": "_locales", - "plugin_info": { - "is_built_in": false, - "plugin_type": "example", - "components": [ - { - "type": "command", - "name": "admin_example", - "description": "管理员权限示例命令", - "pattern": "/admin_example" - }, - { - "type": "command", - "name": "user_example", - "description": "用户权限示例命令", - "pattern": "/user_example" - }, - { - "type": "command", - "name": "master_example", - "description": "Master专用示例命令", - "pattern": "/master_example" - } - ] - } -} diff --git a/plugins/permission_example/plugin.py b/plugins/permission_example/plugin.py deleted file mode 100644 index 8a2909414..000000000 --- a/plugins/permission_example/plugin.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -权限系统示例插件 - -演示如何在插件中使用权限系统,包括权限节点注册、权限检查等功能。 -""" - -from typing import List - -from src.plugin_system.apis.plugin_register_api import register_plugin -from src.plugin_system.base.base_plugin import BasePlugin -from src.plugin_system.base.base_command import BaseCommand -from src.plugin_system.apis.logging_api import get_logger -from src.plugin_system.base.config_types import ConfigField -from src.plugin_system.utils.permission_decorators import require_permission, require_master -from src.common.message import ChatStream, Message - - -logger = get_logger(__name__) - - -class ExampleAdminCommand(BaseCommand): - """需要管理员权限的示例命令""" - - - command_name = "admin_example" - command_description = "管理员权限示例命令" - command_pattern = r"^/admin_example$" - command_help = "管理员权限示例命令" - command_examples = ["/admin_example"] - intercept_message = True - - def can_execute(self, message: Message, chat_stream: ChatStream) -> bool: - """基本检查""" - return True - - @require_permission("plugin.example.admin") - async def execute(self, message: Message, chat_stream: ChatStream, args: List[str]) -> None: - """执行管理员命令""" - await self.send_text("✅ 你有管理员权限!这是一个管理员专用功能。") - return True, "执行成功", True - - -class ExampleUserCommand(BaseCommand): - """普通用户权限的示例命令""" - command_name = "user_example" - command_description = "用户权限示例命令" - command_pattern = r"^/user_example$" - command_help = "用户权限示例命令" - command_examples = ["/user_example"] - intercept_message = True - - def can_execute(self, message: Message, chat_stream: ChatStream) -> bool: - """基本检查""" - return True - - @require_permission("plugin.example.user") - async def execute(self, message: Message, chat_stream: ChatStream, args: List[str]) -> None: - """执行用户命令""" - await self.send_text("✅ 你有用户权限!这是一个普通用户功能。") - - -class ExampleMasterCommand(BaseCommand): - """Master专用的示例命令""" - - command_name = "master_example" - command_description = "Master专用示例命令" - command_pattern = r"^/master_example$" - command_help = "Master专用示例命令" - command_examples = ["/master_example"] - intercept_message = True - - def can_execute(self, message: Message, chat_stream: ChatStream) -> bool: - """基本检查""" - return True - - @require_master() - async def execute(self, message: Message, chat_stream: ChatStream, args: List[str]) -> None: - """执行Master命令""" - await self.send_text("👑 你是Master用户!这是Master专用功能。") - -@register_plugin -class HelloWorldPlugin(BasePlugin): - """权限系统示例插件""" - - # 插件基本信息 - plugin_name: str = "permission_example" # 内部标识符 - enable_plugin: bool = True - dependencies: List[str] = [] # 插件依赖列表 - python_dependencies: List[str] = [] # Python包依赖列表 - - config_file_name: str = "config.toml" # 配置文件名 - - - # 配置Schema定义 - config_schema: dict = { - "plugin": { - "name": ConfigField(type=str, default="permission_example", description="插件名称"), - "version": ConfigField(type=str, default="1.0.0", description="插件版本"), - "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), - } - } - - def get_plugin_components(self): - return [(ExampleAdminCommand.get_command_info,ExampleAdminCommand), - (ExampleUserCommand.get_command_info,ExampleUserCommand), - (ExampleMasterCommand.get_command_info,ExampleMasterCommand) - ] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7485a43cc..0bfafb8da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -64,4 +64,7 @@ chromadb asyncio tavily-python google-generativeai -lunar_python \ No newline at end of file +lunar_python + +python-multipart +aiofiles \ No newline at end of file diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 4a946bb72..8975a0c83 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -133,7 +133,7 @@ class CycleProcessor: await stop_typing() # 在一轮动作执行完毕后,增加睡眠压力 - if self.context.energy_manager and global_config.wakeup_system.enable_insomnia_system: + if self.context.energy_manager and global_config.sleep_system.enable_insomnia_system: if action_type not in ["no_reply", "no_action"]: self.context.energy_manager.increase_sleep_pressure() diff --git a/src/chat/chat_loop/energy_manager.py b/src/chat/chat_loop/energy_manager.py index a2c444326..55e5f9556 100644 --- a/src/chat/chat_loop/energy_manager.py +++ b/src/chat/chat_loop/energy_manager.py @@ -98,7 +98,7 @@ class EnergyManager: if is_sleeping: # 睡眠中:减少睡眠压力 - decay_per_10s = global_config.wakeup_system.sleep_pressure_decay_rate / 6 + decay_per_10s = global_config.sleep_system.sleep_pressure_decay_rate / 6 self.context.sleep_pressure -= decay_per_10s self.context.sleep_pressure = max(self.context.sleep_pressure, 0) self._log_sleep_pressure_change("睡眠压力释放") @@ -145,7 +145,7 @@ class EnergyManager: """ 在执行动作后增加睡眠压力 """ - increment = global_config.wakeup_system.sleep_pressure_increment + increment = global_config.sleep_system.sleep_pressure_increment self.context.sleep_pressure += increment self.context.sleep_pressure = min(self.context.sleep_pressure, 100.0) # 设置一个100的上限 self._log_sleep_pressure_change("执行动作,睡眠压力累积") diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index ccb90da2d..7e386ceac 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -209,12 +209,12 @@ class HeartFChatting: if self.wakeup_manager and self.wakeup_manager.check_for_insomnia(): # 触发失眠 self.context.is_in_insomnia = True - duration = global_config.wakeup_system.insomnia_duration_minutes * 60 + duration = global_config.sleep_system.insomnia_duration_minutes * 60 self.context.insomnia_end_time = time.time() + duration # 判断失眠原因并触发思考 reason = "random" - if self.context.sleep_pressure < global_config.wakeup_system.sleep_pressure_threshold: + if self.context.sleep_pressure < global_config.sleep_system.sleep_pressure_threshold: reason = "low_pressure" await self.proactive_thinker.trigger_insomnia_thinking(reason) @@ -274,7 +274,7 @@ class HeartFChatting: # --- 重新入睡逻辑 --- # 如果被吵醒了,并且在一定时间内没有新消息,则尝试重新入睡 if schedule_manager._is_woken_up and not has_new_messages: - re_sleep_delay = global_config.wakeup_system.re_sleep_delay_minutes * 60 + re_sleep_delay = global_config.sleep_system.re_sleep_delay_minutes * 60 # 使用 last_message_time 来判断空闲时间 if time.time() - self.context.last_message_time > re_sleep_delay: logger.info(f"{self.context.log_prefix} 已被唤醒且超过 {re_sleep_delay / 60} 分钟无新消息,尝试重新入睡。") diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py index 37fd755aa..01c95103e 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/wakeup_manager.py @@ -31,22 +31,22 @@ class WakeUpManager: self.log_interval = 30 # 从配置文件获取参数 - wakeup_config = global_config.wakeup_system - self.wakeup_threshold = wakeup_config.wakeup_threshold - self.private_message_increment = wakeup_config.private_message_increment - self.group_mention_increment = wakeup_config.group_mention_increment - self.decay_rate = wakeup_config.decay_rate - self.decay_interval = wakeup_config.decay_interval - self.angry_duration = wakeup_config.angry_duration - self.enabled = wakeup_config.enable - self.angry_prompt = wakeup_config.angry_prompt + sleep_config = global_config.sleep_system + self.wakeup_threshold = sleep_config.wakeup_threshold + self.private_message_increment = sleep_config.private_message_increment + self.group_mention_increment = sleep_config.group_mention_increment + self.decay_rate = sleep_config.decay_rate + self.decay_interval = sleep_config.decay_interval + self.angry_duration = sleep_config.angry_duration + self.enabled = sleep_config.enable + self.angry_prompt = sleep_config.angry_prompt # 失眠系统参数 - self.insomnia_enabled = wakeup_config.enable_insomnia_system - self.sleep_pressure_threshold = wakeup_config.sleep_pressure_threshold - self.deep_sleep_threshold = wakeup_config.deep_sleep_threshold - self.insomnia_chance_low_pressure = wakeup_config.insomnia_chance_low_pressure - self.insomnia_chance_normal_pressure = wakeup_config.insomnia_chance_normal_pressure + self.insomnia_enabled = sleep_config.enable_insomnia_system + self.sleep_pressure_threshold = sleep_config.sleep_pressure_threshold + self.deep_sleep_threshold = sleep_config.deep_sleep_threshold + self.insomnia_chance_low_pressure = sleep_config.insomnia_chance_low_pressure + self.insomnia_chance_normal_pressure = sleep_config.insomnia_chance_normal_pressure self._load_wakeup_state() diff --git a/src/chat/utils/rust-video/.gitignore b/src/chat/utils/rust-video/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/src/chat/utils/rust-video/.gitignore @@ -0,0 +1 @@ +/target diff --git a/src/chat/utils/rust-video/Cargo.lock b/src/chat/utils/rust-video/Cargo.lock new file mode 100644 index 000000000..8041152b2 --- /dev/null +++ b/src/chat/utils/rust-video/Cargo.lock @@ -0,0 +1,610 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rust-video" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "rayon", + "serde", + "serde_json", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/src/chat/utils/rust-video/Cargo.toml b/src/chat/utils/rust-video/Cargo.toml new file mode 100644 index 000000000..4120f6f2c --- /dev/null +++ b/src/chat/utils/rust-video/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "rust-video" +version = "0.1.0" +edition = "2021" +authors = ["VideoAnalysis Team"] +description = "Ultra-fast video keyframe extraction tool in Rust" +license = "GPL-3.0" + +[dependencies] +anyhow = "1.0" +clap = { version = "4.0", features = ["derive"] } +rayon = "1.11" + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +chrono = { version = "0.4", features = ["serde"] } + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" +strip = true diff --git a/src/chat/utils/rust-video/README.md b/src/chat/utils/rust-video/README.md new file mode 100644 index 000000000..e4c38b0ab --- /dev/null +++ b/src/chat/utils/rust-video/README.md @@ -0,0 +1,221 @@ +# 🎯 Rust Video Keyframe Extraction API + +高性能视频关键帧提取API服务,基于Rust后端 + Python FastAPI。 + +## 📁 项目结构 + +``` +rust-video/ +├── outputs/ # 关键帧输出目录 +├── src/ # Rust源码 +│ └── main.rs +├── target/ # Rust编译文件 +├── api_server.py # 🚀 主API服务器 (整合版) +├── start_server.py # 生产启动脚本 +├── config.py # 配置管理 +├── config.toml # 配置文件 +├── Cargo.toml # Rust项目配置 +├── Cargo.lock # Rust依赖锁定 +├── .gitignore # Git忽略文件 +└── README.md # 项目文档 +``` + +## 快速开始 + +### 1. 安装依赖 +```bash +pip install fastapi uvicorn python-multipart aiofiles +``` + +### 2. 启动服务 +```bash +# 开发模式 +python api_server.py + +# 生产模式 +python start_server.py --mode prod --port 8050 +``` + +### 3. 访问API +- **服务地址**: http://localhost:8050 +- **API文档**: http://localhost:8050/docs +- **健康检查**: http://localhost:8050/health +- **性能指标**: http://localhost:8050/metrics + +## API使用方法 + +### 主要端点 + +#### 1. 提取关键帧 (JSON响应) +```http +POST /extract-keyframes +Content-Type: multipart/form-data + +- video: 视频文件 (.mp4, .avi, .mov, .mkv) +- scene_threshold: 场景变化阈值 (0.1-1.0, 默认0.3) +- max_frames: 最大关键帧数 (1-200, 默认50) +- resize_width: 调整宽度 (可选, 100-1920) +- time_interval: 时间间隔秒数 (可选, 0.1-60.0) +``` + +#### 2. 提取关键帧 (ZIP下载) +```http +POST /extract-keyframes-zip +Content-Type: multipart/form-data + +参数同上,返回包含所有关键帧的ZIP文件 +``` + +#### 3. 健康检查 +```http +GET /health +``` + +#### 4. 性能指标 +```http +GET /metrics +``` + +### Python客户端示例 + +```python +import requests + +# 上传视频并提取关键帧 +files = {'video': open('video.mp4', 'rb')} +data = { + 'scene_threshold': 0.3, + 'max_frames': 50, + 'resize_width': 800 +} + +response = requests.post( + 'http://localhost:8050/extract-keyframes', + files=files, + data=data +) + +result = response.json() +print(f"提取了 {result['keyframe_count']} 个关键帧") +print(f"处理时间: {result['performance']['total_api_time']:.2f}秒") +``` + +### JavaScript客户端示例 + +```javascript +const formData = new FormData(); +formData.append('video', videoFile); +formData.append('scene_threshold', '0.3'); +formData.append('max_frames', '50'); + +fetch('http://localhost:8050/extract-keyframes', { + method: 'POST', + body: formData +}) +.then(response => response.json()) +.then(data => { + console.log(`提取了 ${data.keyframe_count} 个关键帧`); + console.log(`处理时间: ${data.performance.total_api_time}秒`); +}); +``` + +### cURL示例 + +```bash +curl -X POST "http://localhost:8050/extract-keyframes" \ + -H "accept: application/json" \ + -H "Content-Type: multipart/form-data" \ + -F "video=@video.mp4" \ + -F "scene_threshold=0.3" \ + -F "max_frames=50" +``` + +## ⚙️ 配置 + +编辑 `config.toml` 文件: + +```toml +[server] +host = "0.0.0.0" +port = 8050 +debug = false + +[processing] +default_scene_threshold = 0.3 +default_max_frames = 50 +timeout_seconds = 300 + +[performance] +async_workers = 4 +max_file_size_mb = 500 +``` + +## 性能特性 + +- **异步I/O**: 文件上传/下载异步处理 +- **多线程处理**: 视频处理在独立线程池 +- **内存优化**: 流式处理,减少内存占用 +- **智能清理**: 自动临时文件管理 +- **性能监控**: 实时处理时间和吞吐量统计 + +总之就是非常快() + +## 响应格式 + +```json +{ + "status": "success", + "processing_time": 4.5, + "output_directory": "/tmp/output_xxx", + "keyframe_count": 15, + "keyframes": [ + "/tmp/output_xxx/frame_001.jpg", + "/tmp/output_xxx/frame_002.jpg" + ], + "performance": { + "file_size_mb": 209.7, + "upload_time": 0.23, + "processing_time": 4.5, + "total_api_time": 4.73, + "upload_speed_mbps": 912.2 + }, + "rust_output": "处理完成", + "command": "rust-video input.mp4 output/ --scene-threshold 0.3 --max-frames 50" +} +``` + +## 故障排除 + +### 常见问题 + +1. **Rust binary not found** + ```bash + cargo build # 重新构建Rust项目 + ``` + +2. **端口被占用** + ```bash + # 修改config.toml中的端口号 + port = 8051 + ``` + +3. **内存不足** + ```bash + # 减少max_frames或resize_width参数 + ``` + +### 日志查看 + +服务启动时会显示详细的状态信息,包括: +- Rust二进制文件位置 +- 配置加载状态 +- 服务监听地址 + +## 集成支持 + +本API设计为独立服务,可轻松集成到任何项目中: + +- **AI Bot项目**: 通过HTTP API调用 +- **Web应用**: 直接前端调用或后端代理 +- **移动应用**: REST API标准接口 +- **批处理脚本**: Python/Shell脚本调用 diff --git a/src/chat/utils/rust-video/api_server.py b/src/chat/utils/rust-video/api_server.py new file mode 100644 index 000000000..aeb3fa248 --- /dev/null +++ b/src/chat/utils/rust-video/api_server.py @@ -0,0 +1,472 @@ +#!/usr/bin/env python3 +""" +Rust Video Keyframe Extraction API Server +高性能视频关键帧提取API服务 + +功能: +- 视频上传和关键帧提取 +- 异步多线程处理 +- 性能监控和健康检查 +- 自动资源清理 + +启动: python api_server.py +地址: http://localhost:8050 +""" + +import os +import json +import subprocess +import tempfile +import zipfile +import shutil +import asyncio +import time +import logging +from datetime import datetime +from pathlib import Path +from typing import Optional, List, Dict, Any + +import uvicorn +from fastapi import FastAPI, File, UploadFile, Form, HTTPException, BackgroundTasks +from fastapi.responses import FileResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field + +# 导入配置管理 +from config import config + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# ============================================================================ +# 内置视频处理器 (整合版) +# ============================================================================ + +class VideoKeyframeExtractor: + """整合的视频关键帧提取器""" + + def __init__(self, rust_binary_path: Optional[str] = None): + self.rust_binary_path = rust_binary_path or self._find_rust_binary() + if not self.rust_binary_path or not Path(self.rust_binary_path).exists(): + raise FileNotFoundError(f"Rust binary not found: {self.rust_binary_path}") + + def _find_rust_binary(self) -> str: + """查找Rust二进制文件""" + possible_paths = [ + "./target/debug/rust-video.exe", + "./target/release/rust-video.exe", + "./target/debug/rust-video", + "./target/release/rust-video" + ] + + for path in possible_paths: + if Path(path).exists(): + return str(Path(path).absolute()) + + # 尝试构建 + try: + subprocess.run(["cargo", "build"], check=True, capture_output=True) + for path in possible_paths: + if Path(path).exists(): + return str(Path(path).absolute()) + except subprocess.CalledProcessError: + pass + + raise FileNotFoundError("Rust binary not found and build failed") + + def process_video( + self, + video_path: str, + output_dir: str = "outputs", + scene_threshold: float = 0.3, + max_frames: int = 50, + resize_width: Optional[int] = None, + time_interval: Optional[float] = None + ) -> Dict[str, Any]: + """处理视频提取关键帧""" + + video_path = Path(video_path) + if not video_path.exists(): + raise FileNotFoundError(f"Video file not found: {video_path}") + + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # 构建命令 + cmd = [self.rust_binary_path, str(video_path), str(output_dir)] + cmd.extend(["--scene-threshold", str(scene_threshold)]) + cmd.extend(["--max-frames", str(max_frames)]) + + if resize_width: + cmd.extend(["--resize-width", str(resize_width)]) + if time_interval: + cmd.extend(["--time-interval", str(time_interval)]) + + # 执行处理 + start_time = time.time() + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + timeout=300 # 5分钟超时 + ) + + processing_time = time.time() - start_time + + # 解析输出 + output_files = list(output_dir.glob("*.jpg")) + + return { + "status": "success", + "processing_time": processing_time, + "output_directory": str(output_dir), + "keyframe_count": len(output_files), + "keyframes": [str(f) for f in output_files], + "rust_output": result.stdout, + "command": " ".join(cmd) + } + + except subprocess.TimeoutExpired: + raise HTTPException(status_code=408, detail="Video processing timeout") + except subprocess.CalledProcessError as e: + raise HTTPException( + status_code=500, + detail=f"Video processing failed: {e.stderr}" + ) + +# ============================================================================ +# 异步处理器 (整合版) +# ============================================================================ + +class AsyncVideoProcessor: + """高性能异步视频处理器""" + + def __init__(self): + self.extractor = VideoKeyframeExtractor() + + async def process_video_async( + self, + upload_file: UploadFile, + processing_params: Dict[str, Any] + ) -> Dict[str, Any]: + """异步视频处理主流程""" + + start_time = time.time() + + # 1. 异步保存上传文件 + upload_start = time.time() + temp_fd, temp_path_str = tempfile.mkstemp(suffix='.mp4') + temp_path = Path(temp_path_str) + + try: + os.close(temp_fd) + + # 异步读取并保存文件 + content = await upload_file.read() + with open(temp_path, 'wb') as f: + f.write(content) + + upload_time = time.time() - upload_start + file_size = len(content) + + # 2. 多线程处理视频 + process_start = time.time() + temp_output_dir = tempfile.mkdtemp() + output_path = Path(temp_output_dir) + + try: + # 在线程池中异步处理 + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + None, + self._process_video_sync, + str(temp_path), + str(output_path), + processing_params + ) + + process_time = time.time() - process_start + total_time = time.time() - start_time + + # 添加性能指标 + result.update({ + 'performance': { + 'file_size_mb': file_size / (1024 * 1024), + 'upload_time': upload_time, + 'processing_time': process_time, + 'total_api_time': total_time, + 'upload_speed_mbps': (file_size / (1024 * 1024)) / upload_time if upload_time > 0 else 0 + } + }) + + return result + + finally: + # 清理输出目录 + try: + shutil.rmtree(temp_output_dir, ignore_errors=True) + except Exception as e: + logger.warning(f"Failed to cleanup output directory: {e}") + + finally: + # 清理临时文件 + try: + if temp_path.exists(): + temp_path.unlink() + except Exception as e: + logger.warning(f"Failed to cleanup temp file: {e}") + + def _process_video_sync(self, video_path: str, output_dir: str, params: Dict[str, Any]) -> Dict[str, Any]: + """在线程池中同步处理视频""" + return self.extractor.process_video( + video_path=video_path, + output_dir=output_dir, + **params + ) + +# ============================================================================ +# FastAPI 应用初始化 +# ============================================================================ + +app = FastAPI( + title="Rust Video Keyframe API", + description="高性能视频关键帧提取API服务", + version="2.0.0", + docs_url="/docs", + redoc_url="/redoc" +) + +# CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 全局处理器实例 +video_processor = AsyncVideoProcessor() + +# 简单的统计 +stats = { + "total_requests": 0, + "processing_times": [], + "start_time": datetime.now() +} + +# ============================================================================ +# API 路由 +# ============================================================================ + +@app.get("/", response_class=JSONResponse) +async def root(): + """API根路径""" + return { + "message": "Rust Video Keyframe Extraction API", + "version": "2.0.0", + "status": "ready", + "docs": "/docs", + "health": "/health", + "metrics": "/metrics" + } + +@app.get("/health") +async def health_check(): + """健康检查端点""" + try: + # 检查Rust二进制 + rust_binary = video_processor.extractor.rust_binary_path + rust_status = "ok" if Path(rust_binary).exists() else "missing" + + return { + "status": rust_status, + "timestamp": datetime.now().isoformat(), + "version": "2.0.0", + "rust_binary": rust_binary + } + except Exception as e: + raise HTTPException(status_code=503, detail=f"Health check failed: {str(e)}") + +@app.get("/metrics") +async def get_metrics(): + """获取性能指标""" + avg_time = sum(stats["processing_times"]) / len(stats["processing_times"]) if stats["processing_times"] else 0 + uptime = (datetime.now() - stats["start_time"]).total_seconds() + + return { + "total_requests": stats["total_requests"], + "average_processing_time": avg_time, + "last_24h_requests": stats["total_requests"], # 简化版本 + "system_info": { + "uptime_seconds": uptime, + "memory_usage": "N/A", # 可以扩展 + "cpu_usage": "N/A" + } + } + +@app.post("/extract-keyframes") +async def extract_keyframes( + video: UploadFile = File(..., description="视频文件"), + scene_threshold: float = Form(0.3, description="场景变化阈值"), + max_frames: int = Form(50, description="最大关键帧数量"), + resize_width: Optional[int] = Form(None, description="调整宽度"), + time_interval: Optional[float] = Form(None, description="时间间隔") +): + """提取视频关键帧 (主要API端点)""" + + # 参数验证 + if not video.filename.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')): + raise HTTPException(status_code=400, detail="不支持的视频格式") + + # 更新统计 + stats["total_requests"] += 1 + + try: + # 构建处理参数 + params = { + "scene_threshold": scene_threshold, + "max_frames": max_frames + } + if resize_width: + params["resize_width"] = resize_width + if time_interval: + params["time_interval"] = time_interval + + # 异步处理 + start_time = time.time() + result = await video_processor.process_video_async(video, params) + processing_time = time.time() - start_time + + # 更新统计 + stats["processing_times"].append(processing_time) + if len(stats["processing_times"]) > 100: # 保持最近100次记录 + stats["processing_times"] = stats["processing_times"][-100:] + + return JSONResponse(content=result) + + except Exception as e: + logger.error(f"Processing failed: {str(e)}") + raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}") + +@app.post("/extract-keyframes-zip") +async def extract_keyframes_zip( + video: UploadFile = File(...), + scene_threshold: float = Form(0.3), + max_frames: int = Form(50), + resize_width: Optional[int] = Form(None), + time_interval: Optional[float] = Form(None) +): + """提取关键帧并返回ZIP文件""" + + # 验证文件类型 + if not video.filename.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')): + raise HTTPException(status_code=400, detail="不支持的视频格式") + + # 创建临时目录 + temp_input_fd, temp_input_path = tempfile.mkstemp(suffix='.mp4') + temp_output_dir = tempfile.mkdtemp() + + try: + os.close(temp_input_fd) + + # 保存上传的视频 + content = await video.read() + with open(temp_input_path, 'wb') as f: + f.write(content) + + # 处理参数 + params = { + "scene_threshold": scene_threshold, + "max_frames": max_frames + } + if resize_width: + params["resize_width"] = resize_width + if time_interval: + params["time_interval"] = time_interval + + # 处理视频 + result = video_processor.extractor.process_video( + video_path=temp_input_path, + output_dir=temp_output_dir, + **params + ) + + # 创建ZIP文件 + zip_fd, zip_path = tempfile.mkstemp(suffix='.zip') + os.close(zip_fd) + + with zipfile.ZipFile(zip_path, 'w') as zip_file: + # 添加关键帧图片 + for keyframe_path in result.get("keyframes", []): + if Path(keyframe_path).exists(): + zip_file.write(keyframe_path, Path(keyframe_path).name) + + # 添加处理信息 + info_content = json.dumps(result, indent=2, ensure_ascii=False) + zip_file.writestr("processing_info.json", info_content) + + # 返回ZIP文件 + return FileResponse( + zip_path, + media_type='application/zip', + filename=f"keyframes_{video.filename}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}") + + finally: + # 清理临时文件 + for path in [temp_input_path, temp_output_dir]: + try: + if Path(path).is_file(): + Path(path).unlink() + elif Path(path).is_dir(): + shutil.rmtree(path, ignore_errors=True) + except Exception: + pass + +# ============================================================================ +# 应用启动 +# ============================================================================ + +def main(): + """启动API服务器""" + + # 获取配置 + server_config = config.get('server') + host = server_config.get('host', '0.0.0.0') + port = server_config.get('port', 8050) + + print(f""" +Rust Video Keyframe Extraction API +===================================== +地址: http://{host}:{port} +文档: http://{host}:{port}/docs +健康检查: http://{host}:{port}/health +性能指标: http://{host}:{port}/metrics +===================================== + """) + + # 检查Rust二进制 + try: + rust_binary = video_processor.extractor.rust_binary_path + print(f"✓ Rust binary: {rust_binary}") + except Exception as e: + print(f"⚠️ Rust binary check failed: {e}") + + # 启动服务器 + uvicorn.run( + "api_server:app", + host=host, + port=port, + reload=False, # 生产环境关闭热重载 + access_log=True + ) + +if __name__ == "__main__": + main() diff --git a/src/chat/utils/rust-video/config.py b/src/chat/utils/rust-video/config.py new file mode 100644 index 000000000..c85b8f9ea --- /dev/null +++ b/src/chat/utils/rust-video/config.py @@ -0,0 +1,115 @@ +""" +配置管理模块 +处理 config.toml 文件的读取和管理 +""" + +import os +from pathlib import Path +from typing import Dict, Any + +try: + import toml +except ImportError: + print("⚠️ 需要安装 toml: pip install toml") + # 提供基础配置作为后备 + toml = None + +class ConfigManager: + """配置管理器""" + + def __init__(self, config_file: str = "config.toml"): + self.config_file = Path(config_file) + self._config = self._load_config() + + def _load_config(self) -> Dict[str, Any]: + """加载配置文件""" + if toml is None or not self.config_file.exists(): + return self._get_default_config() + + try: + with open(self.config_file, 'r', encoding='utf-8') as f: + return toml.load(f) + except Exception as e: + print(f"⚠️ 配置文件读取失败: {e}") + return self._get_default_config() + + def _get_default_config(self) -> Dict[str, Any]: + """默认配置""" + return { + "server": { + "host": "0.0.0.0", + "port": 8000, + "workers": 1, + "reload": False, + "log_level": "info" + }, + "api": { + "title": "Video Keyframe Extraction API", + "description": "高性能视频关键帧提取服务", + "version": "1.0.0", + "max_file_size": "100MB" + }, + "processing": { + "default_threshold": 0.3, + "default_output_format": "png", + "max_frames": 10000, + "temp_dir": "temp", + "upload_dir": "uploads", + "output_dir": "outputs" + }, + "rust": { + "executable_name": "video_keyframe_extractor", + "executable_path": "target/release" + }, + "ffmpeg": { + "auto_detect": True, + "custom_path": "", + "timeout": 300 + }, + "storage": { + "cleanup_interval": 3600, + "max_storage_size": "10GB", + "result_retention_days": 7 + }, + "monitoring": { + "enable_metrics": True, + "enable_logging": True, + "log_file": "logs/api.log", + "max_log_size": "100MB" + }, + "security": { + "allowed_origins": ["*"], + "max_concurrent_tasks": 10, + "rate_limit_per_minute": 60 + }, + "development": { + "debug": False, + "auto_reload": False, + "cors_enabled": True + } + } + + def get(self, section: str, key: str = None, default=None): + """获取配置值""" + if key is None: + return self._config.get(section, default) + return self._config.get(section, {}).get(key, default) + + def get_server_config(self): + """获取服务器配置""" + return self.get("server") + + def get_api_config(self): + """获取API配置""" + return self.get("api") + + def get_processing_config(self): + """获取处理配置""" + return self.get("processing") + + def reload(self): + """重新加载配置""" + self._config = self._load_config() + +# 全局配置实例 +config = ConfigManager() diff --git a/src/chat/utils/rust-video/config.toml b/src/chat/utils/rust-video/config.toml new file mode 100644 index 000000000..56e7799cc --- /dev/null +++ b/src/chat/utils/rust-video/config.toml @@ -0,0 +1,70 @@ +# 🔧 Video Keyframe Extraction API 配置文件 + +[server] +# 服务器配置 +host = "0.0.0.0" +port = 8050 +workers = 1 +reload = false +log_level = "info" + +[api] +# API 基础配置 +title = "Video Keyframe Extraction API" +description = "视频关键帧提取服务" +version = "1.0.0" +max_file_size = "100MB" # 最大文件大小 + +[processing] +# 视频处理配置 +default_threshold = 0.3 +default_output_format = "png" +max_frames = 10000 +temp_dir = "temp" +upload_dir = "uploads" +output_dir = "outputs" + +[rust] +# Rust 程序配置 +executable_name = "video_keyframe_extractor" +executable_path = "target/release" # 相对路径,自动检测 + +[ffmpeg] +# FFmpeg 配置 +auto_detect = true +custom_path = "" # 留空则自动检测 +timeout = 300 # 秒 + +[performance] +# 性能优化配置 +async_workers = 4 # 异步文件处理工作线程数 +upload_chunk_size = 8192 # 上传块大小 (字节) +max_concurrent_uploads = 10 # 最大并发上传数 +compression_level = 1 # ZIP 压缩级别 (0-9, 1=快速) +stream_chunk_size = 8192 # 流式响应块大小 +enable_performance_metrics = true # 启用性能监控 + +[storage] +# 存储配置 +cleanup_interval = 3600 # 清理间隔(秒) +max_storage_size = "10GB" +result_retention_days = 7 + +[monitoring] +# 监控配置 +enable_metrics = true +enable_logging = true +log_file = "logs/api.log" +max_log_size = "100MB" + +[security] +# 安全配置 +allowed_origins = ["*"] +max_concurrent_tasks = 10 +rate_limit_per_minute = 60 + +[development] +# 开发环境配置 +debug = false +auto_reload = false +cors_enabled = true diff --git a/src/chat/utils/rust-video/src/main.rs b/src/chat/utils/rust-video/src/main.rs new file mode 100644 index 000000000..13fd98cbb --- /dev/null +++ b/src/chat/utils/rust-video/src/main.rs @@ -0,0 +1,710 @@ +//! # Rust Video Keyframe Extractor +//! +//! Ultra-fast video keyframe extraction tool with SIMD optimization. +//! +//! ## Features +//! - AVX2/SSE2 SIMD optimization for maximum performance +//! - Memory-efficient streaming processing with FFmpeg +//! - Multi-threaded parallel processing +//! - Release-optimized for production use +//! +//! ## Performance +//! - 150+ FPS processing speed +//! - Real-time video analysis capability +//! - Minimal memory footprint +//! +//! ## Usage +//! ```bash +//! # Single video processing +//! rust-video --input video.mp4 --output ./keyframes --threshold 2.0 +//! +//! # Benchmark mode +//! rust-video --benchmark --input video.mp4 --output ./results +//! ``` + +use anyhow::{Context, Result}; +use chrono::prelude::*; +use clap::Parser; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::io::{BufReader, Read}; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::time::Instant; + +#[cfg(target_arch = "x86_64")] +use std::arch::x86_64::*; + +/// Ultra-fast video keyframe extraction tool +#[derive(Parser)] +#[command(name = "rust-video")] +#[command(version = "0.1.0")] +#[command(about = "Ultra-fast video keyframe extraction with SIMD optimization")] +#[command(long_about = None)] +struct Args { + /// Input video file path + #[arg(short, long, help = "Path to the input video file")] + input: Option, + + /// Output directory for keyframes and results + #[arg(short, long, default_value = "./output", help = "Output directory")] + output: PathBuf, + + /// Change threshold for keyframe detection (higher = fewer keyframes) + #[arg(short, long, default_value = "2.0", help = "Keyframe detection threshold")] + threshold: f64, + + /// Number of parallel threads (0 = auto-detect) + #[arg(short = 'j', long, default_value = "0", help = "Number of threads")] + threads: usize, + + /// Maximum number of keyframes to save (0 = save all) + #[arg(short, long, default_value = "50", help = "Maximum keyframes to save")] + max_save: usize, + + /// Run performance benchmark suite + #[arg(long, help = "Run comprehensive benchmark tests")] + benchmark: bool, + + /// Maximum frames to process (0 = process all frames) + #[arg(long, default_value = "0", help = "Limit number of frames to process")] + max_frames: usize, + + /// FFmpeg executable path + #[arg(long, default_value = "ffmpeg", help = "Path to FFmpeg executable")] + ffmpeg_path: PathBuf, + + /// Enable SIMD optimizations (AVX2/SSE2) + #[arg(long, default_value = "true", help = "Enable SIMD optimizations")] + use_simd: bool, + + /// Processing block size for cache optimization + #[arg(long, default_value = "8192", help = "Block size for processing")] + block_size: usize, + + /// Verbose output + #[arg(short, long, help = "Enable verbose output")] + verbose: bool, +} + +/// Video frame representation optimized for SIMD processing +#[derive(Debug, Clone)] +struct VideoFrame { + frame_number: usize, + width: usize, + height: usize, + data: Vec, // Grayscale data, aligned for SIMD +} + +impl VideoFrame { + /// Create a new video frame with SIMD-aligned data + fn new(frame_number: usize, width: usize, height: usize, mut data: Vec) -> Self { + // Ensure data length is multiple of 32 for AVX2 processing + let remainder = data.len() % 32; + if remainder != 0 { + data.resize(data.len() + (32 - remainder), 0); + } + + Self { + frame_number, + width, + height, + data, + } + } + + /// Calculate frame difference using parallel SIMD processing + fn calculate_difference_parallel_simd(&self, other: &VideoFrame, block_size: usize, use_simd: bool) -> f64 { + if self.width != other.width || self.height != other.height { + return f64::MAX; + } + + let total_pixels = self.width * self.height; + let num_blocks = (total_pixels + block_size - 1) / block_size; + + let total_diff: u64 = (0..num_blocks) + .into_par_iter() + .map(|block_idx| { + let start = block_idx * block_size; + let end = ((block_idx + 1) * block_size).min(total_pixels); + let block_len = end - start; + + if use_simd { + #[cfg(target_arch = "x86_64")] + { + unsafe { + if std::arch::is_x86_feature_detected!("avx2") { + return self.calculate_difference_avx2_block(&other.data, start, block_len); + } else if std::arch::is_x86_feature_detected!("sse2") { + return self.calculate_difference_sse2_block(&other.data, start, block_len); + } + } + } + } + + // Fallback scalar implementation + self.data[start..end] + .iter() + .zip(other.data[start..end].iter()) + .map(|(a, b)| (*a as i32 - *b as i32).abs() as u64) + .sum() + }) + .sum(); + + total_diff as f64 / total_pixels as f64 + } + + /// Standard frame difference calculation (non-SIMD) + fn calculate_difference_standard(&self, other: &VideoFrame) -> f64 { + if self.width != other.width || self.height != other.height { + return f64::MAX; + } + + let len = self.width * self.height; + let total_diff: u64 = self.data[..len] + .iter() + .zip(other.data[..len].iter()) + .map(|(a, b)| (*a as i32 - *b as i32).abs() as u64) + .sum(); + + total_diff as f64 / len as f64 + } + + /// AVX2 optimized block processing + #[cfg(target_arch = "x86_64")] + #[target_feature(enable = "avx2")] + unsafe fn calculate_difference_avx2_block(&self, other_data: &[u8], start: usize, len: usize) -> u64 { + let mut total_diff = 0u64; + let chunks = len / 32; + + for i in 0..chunks { + let offset = start + i * 32; + + let a = _mm256_loadu_si256(self.data.as_ptr().add(offset) as *const __m256i); + let b = _mm256_loadu_si256(other_data.as_ptr().add(offset) as *const __m256i); + + let diff = _mm256_sad_epu8(a, b); + let result = _mm256_extract_epi64(diff, 0) as u64 + + _mm256_extract_epi64(diff, 1) as u64 + + _mm256_extract_epi64(diff, 2) as u64 + + _mm256_extract_epi64(diff, 3) as u64; + + total_diff += result; + } + + // Process remaining bytes + for i in (start + chunks * 32)..(start + len) { + total_diff += (self.data[i] as i32 - other_data[i] as i32).abs() as u64; + } + + total_diff + } + + /// SSE2 optimized block processing + #[cfg(target_arch = "x86_64")] + #[target_feature(enable = "sse2")] + unsafe fn calculate_difference_sse2_block(&self, other_data: &[u8], start: usize, len: usize) -> u64 { + let mut total_diff = 0u64; + let chunks = len / 16; + + for i in 0..chunks { + let offset = start + i * 16; + + let a = _mm_loadu_si128(self.data.as_ptr().add(offset) as *const __m128i); + let b = _mm_loadu_si128(other_data.as_ptr().add(offset) as *const __m128i); + + let diff = _mm_sad_epu8(a, b); + let result = _mm_extract_epi64(diff, 0) as u64 + _mm_extract_epi64(diff, 1) as u64; + + total_diff += result; + } + + // Process remaining bytes + for i in (start + chunks * 16)..(start + len) { + total_diff += (self.data[i] as i32 - other_data[i] as i32).abs() as u64; + } + + total_diff + } +} + +/// Performance measurement results +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PerformanceResult { + test_name: String, + video_file: String, + total_time_ms: f64, + frame_extraction_time_ms: f64, + keyframe_analysis_time_ms: f64, + total_frames: usize, + keyframes_extracted: usize, + keyframe_ratio: f64, + processing_fps: f64, + threshold: f64, + optimization_type: String, + simd_enabled: bool, + threads_used: usize, + timestamp: String, +} + +/// Extract video frames using FFmpeg memory streaming +fn extract_frames_memory_stream( + video_path: &PathBuf, + ffmpeg_path: &PathBuf, + max_frames: usize, + verbose: bool, +) -> Result<(Vec, usize, usize)> { + if verbose { + println!("🎬 Extracting frames using FFmpeg memory streaming..."); + println!("📁 Video: {}", video_path.display()); + } + + // Get video information + let probe_output = Command::new(ffmpeg_path) + .args(["-i", video_path.to_str().unwrap(), "-hide_banner"]) + .output() + .context("Failed to probe video with FFmpeg")?; + + let probe_info = String::from_utf8_lossy(&probe_output.stderr); + let (width, height) = parse_video_dimensions(&probe_info) + .ok_or_else(|| anyhow::anyhow!("Cannot parse video dimensions"))?; + + if verbose { + println!("📐 Video dimensions: {}x{}", width, height); + } + + // Build optimized FFmpeg command + let mut cmd = Command::new(ffmpeg_path); + cmd.args([ + "-i", video_path.to_str().unwrap(), + "-f", "rawvideo", + "-pix_fmt", "gray", + "-an", // No audio + "-threads", "0", // Auto-detect threads + "-preset", "ultrafast", // Fastest preset + ]); + + if max_frames > 0 { + cmd.args(["-frames:v", &max_frames.to_string()]); + } + + cmd.args(["-"]).stdout(Stdio::piped()).stderr(Stdio::null()); + + let start_time = Instant::now(); + let mut child = cmd.spawn().context("Failed to spawn FFmpeg process")?; + let stdout = child.stdout.take().unwrap(); + let mut reader = BufReader::with_capacity(1024 * 1024, stdout); // 1MB buffer + + let frame_size = width * height; + let mut frames = Vec::new(); + let mut frame_count = 0; + let mut frame_buffer = vec![0u8; frame_size]; + + if verbose { + println!("📦 Frame size: {} bytes", frame_size); + } + + // Stream frame data directly into memory + loop { + match reader.read_exact(&mut frame_buffer) { + Ok(()) => { + frames.push(VideoFrame::new( + frame_count, + width, + height, + frame_buffer.clone(), + )); + frame_count += 1; + + if verbose && frame_count % 200 == 0 { + print!("\r⚡ Frames processed: {}", frame_count); + } + + if max_frames > 0 && frame_count >= max_frames { + break; + } + } + Err(_) => break, // End of stream + } + } + + let _ = child.wait(); + + if verbose { + println!("\r✅ Frame extraction complete: {} frames in {:.2}s", + frame_count, start_time.elapsed().as_secs_f64()); + } + + Ok((frames, width, height)) +} + +/// Parse video dimensions from FFmpeg probe output +fn parse_video_dimensions(probe_info: &str) -> Option<(usize, usize)> { + for line in probe_info.lines() { + if line.contains("Video:") && line.contains("x") { + for part in line.split_whitespace() { + if let Some(x_pos) = part.find('x') { + let width_str = &part[..x_pos]; + let height_part = &part[x_pos + 1..]; + let height_str = height_part.split(',').next().unwrap_or(height_part); + + if let (Ok(width), Ok(height)) = (width_str.parse::(), height_str.parse::()) { + return Some((width, height)); + } + } + } + } + } + None +} + +/// Extract keyframes using optimized algorithms +fn extract_keyframes_optimized( + frames: &[VideoFrame], + threshold: f64, + use_simd: bool, + block_size: usize, + verbose: bool, +) -> Result> { + if frames.len() < 2 { + return Ok(Vec::new()); + } + + let optimization_name = if use_simd { "SIMD+Parallel" } else { "Standard Parallel" }; + if verbose { + println!("🚀 Keyframe analysis (threshold: {}, optimization: {})", threshold, optimization_name); + } + + let start_time = Instant::now(); + + // Parallel computation of frame differences + let differences: Vec = frames + .par_windows(2) + .map(|pair| { + if use_simd { + pair[0].calculate_difference_parallel_simd(&pair[1], block_size, true) + } else { + pair[0].calculate_difference_standard(&pair[1]) + } + }) + .collect(); + + // Find keyframes based on threshold + let keyframe_indices: Vec = differences + .par_iter() + .enumerate() + .filter_map(|(i, &diff)| { + if diff > threshold { + Some(i + 1) + } else { + None + } + }) + .collect(); + + if verbose { + println!("⚡ Analysis complete in {:.2}s", start_time.elapsed().as_secs_f64()); + println!("🎯 Found {} keyframes", keyframe_indices.len()); + } + + Ok(keyframe_indices) +} + +/// Save keyframes as JPEG images using FFmpeg +fn save_keyframes_optimized( + video_path: &PathBuf, + keyframe_indices: &[usize], + output_dir: &PathBuf, + ffmpeg_path: &PathBuf, + max_save: usize, + verbose: bool, +) -> Result { + if keyframe_indices.is_empty() { + if verbose { + println!("⚠️ No keyframes to save"); + } + return Ok(0); + } + + if verbose { + println!("💾 Saving keyframes..."); + } + + fs::create_dir_all(output_dir).context("Failed to create output directory")?; + + let save_count = keyframe_indices.len().min(max_save); + let mut saved = 0; + + for (i, &frame_idx) in keyframe_indices.iter().take(save_count).enumerate() { + let output_path = output_dir.join(format!("keyframe_{:03}.jpg", i + 1)); + let timestamp = frame_idx as f64 / 30.0; // Assume 30 FPS + + let output = Command::new(ffmpeg_path) + .args([ + "-i", video_path.to_str().unwrap(), + "-ss", ×tamp.to_string(), + "-vframes", "1", + "-q:v", "2", // High quality + "-y", + output_path.to_str().unwrap(), + ]) + .output() + .context("Failed to extract keyframe with FFmpeg")?; + + if output.status.success() { + saved += 1; + if verbose && (saved % 10 == 0 || saved == save_count) { + print!("\r💾 Saved: {}/{} keyframes", saved, save_count); + } + } else if verbose { + eprintln!("⚠️ Failed to save keyframe {}", frame_idx); + } + } + + if verbose { + println!("\r✅ Keyframe saving complete: {}/{}", saved, save_count); + } + + Ok(saved) +} + +/// Run performance test +fn run_performance_test( + video_path: &PathBuf, + threshold: f64, + test_name: &str, + ffmpeg_path: &PathBuf, + max_frames: usize, + use_simd: bool, + block_size: usize, + verbose: bool, +) -> Result { + if verbose { + println!("\n{}", "=".repeat(60)); + println!("⚡ Running test: {}", test_name); + println!("{}", "=".repeat(60)); + } + + let total_start = Instant::now(); + + // Frame extraction + let extraction_start = Instant::now(); + let (frames, _width, _height) = extract_frames_memory_stream(video_path, ffmpeg_path, max_frames, verbose)?; + let extraction_time = extraction_start.elapsed().as_secs_f64() * 1000.0; + + // Keyframe analysis + let analysis_start = Instant::now(); + let keyframe_indices = extract_keyframes_optimized(&frames, threshold, use_simd, block_size, verbose)?; + let analysis_time = analysis_start.elapsed().as_secs_f64() * 1000.0; + + let total_time = total_start.elapsed().as_secs_f64() * 1000.0; + + let optimization_type = if use_simd { + format!("SIMD+Parallel(block:{})", block_size) + } else { + "Standard Parallel".to_string() + }; + + let result = PerformanceResult { + test_name: test_name.to_string(), + video_file: video_path.file_name().unwrap().to_string_lossy().to_string(), + total_time_ms: total_time, + frame_extraction_time_ms: extraction_time, + keyframe_analysis_time_ms: analysis_time, + total_frames: frames.len(), + keyframes_extracted: keyframe_indices.len(), + keyframe_ratio: keyframe_indices.len() as f64 / frames.len() as f64 * 100.0, + processing_fps: frames.len() as f64 / (total_time / 1000.0), + threshold, + optimization_type, + simd_enabled: use_simd, + threads_used: rayon::current_num_threads(), + timestamp: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(), + }; + + if verbose { + println!("\n⚡ Test Results:"); + println!(" 🕐 Total time: {:.2}ms ({:.2}s)", result.total_time_ms, result.total_time_ms / 1000.0); + println!(" 📥 Extraction: {:.2}ms ({:.1}%)", result.frame_extraction_time_ms, + result.frame_extraction_time_ms / result.total_time_ms * 100.0); + println!(" 🧮 Analysis: {:.2}ms ({:.1}%)", result.keyframe_analysis_time_ms, + result.keyframe_analysis_time_ms / result.total_time_ms * 100.0); + println!(" 📊 Frames: {}", result.total_frames); + println!(" 🎯 Keyframes: {}", result.keyframes_extracted); + println!(" 🚀 Speed: {:.1} FPS", result.processing_fps); + println!(" ⚙️ Optimization: {}", result.optimization_type); + } + + Ok(result) +} + +/// Run comprehensive benchmark suite +fn run_benchmark_suite(video_path: &PathBuf, output_dir: &PathBuf, ffmpeg_path: &PathBuf, args: &Args) -> Result<()> { + println!("🚀 Rust Video Keyframe Extractor - Benchmark Suite"); + println!("🕐 Time: {}", Local::now().format("%Y-%m-%d %H:%M:%S")); + println!("🎬 Video: {}", video_path.display()); + println!("🧵 Threads: {}", rayon::current_num_threads()); + + // CPU feature detection + #[cfg(target_arch = "x86_64")] + { + println!("🔧 CPU Features:"); + if std::arch::is_x86_feature_detected!("avx2") { + println!(" ✅ AVX2 supported"); + } else if std::arch::is_x86_feature_detected!("sse2") { + println!(" ✅ SSE2 supported"); + } else { + println!(" ⚠️ Scalar only"); + } + } + + let test_configs = vec![ + ("Standard Parallel", false, 8192), + ("SIMD 8K blocks", true, 8192), + ("SIMD 16K blocks", true, 16384), + ("SIMD 32K blocks", true, 32768), + ]; + + let mut results = Vec::new(); + + for (test_name, use_simd, block_size) in test_configs { + match run_performance_test( + video_path, + args.threshold, + test_name, + ffmpeg_path, + 1000, // Test with 1000 frames + use_simd, + block_size, + args.verbose, + ) { + Ok(result) => results.push(result), + Err(e) => println!("❌ Test failed {}: {:?}", test_name, e), + } + } + + // Performance comparison table + println!("\n{}", "=".repeat(120)); + println!("🏆 Benchmark Results"); + println!("{}", "=".repeat(120)); + + println!("{:<20} {:<15} {:<12} {:<12} {:<12} {:<8} {:<8} {:<12} {:<20}", + "Test", "Total(ms)", "Extract(ms)", "Analyze(ms)", "Speed(FPS)", "Frames", "Keyframes", "Threads", "Optimization"); + println!("{}", "-".repeat(120)); + + for result in &results { + println!("{:<20} {:<15.1} {:<12.1} {:<12.1} {:<12.1} {:<8} {:<8} {:<12} {:<20}", + result.test_name, + result.total_time_ms, + result.frame_extraction_time_ms, + result.keyframe_analysis_time_ms, + result.processing_fps, + result.total_frames, + result.keyframes_extracted, + result.threads_used, + result.optimization_type); + } + + // Find best performance + if let Some(best_result) = results.iter().max_by(|a, b| a.processing_fps.partial_cmp(&b.processing_fps).unwrap()) { + println!("\n🏆 Best Performance: {}", best_result.test_name); + println!(" ⚡ Speed: {:.1} FPS", best_result.processing_fps); + println!(" 🕐 Time: {:.2}s", best_result.total_time_ms / 1000.0); + println!(" 🧮 Analysis: {:.2}s", best_result.keyframe_analysis_time_ms / 1000.0); + println!(" ⚙️ Tech: {}", best_result.optimization_type); + } + + // Save detailed results + fs::create_dir_all(output_dir).context("Failed to create output directory")?; + let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); + let results_file = output_dir.join(format!("benchmark_results_{}.json", timestamp)); + + let json_results = serde_json::to_string_pretty(&results)?; + fs::write(&results_file, json_results)?; + + println!("\n📄 Detailed results saved to: {}", results_file.display()); + println!("{}", "=".repeat(120)); + + Ok(()) +} + +fn main() -> Result<()> { + let args = Args::parse(); + + // Setup thread pool + if args.threads > 0 { + rayon::ThreadPoolBuilder::new() + .num_threads(args.threads) + .build_global() + .context("Failed to set thread pool")?; + } + + println!("🚀 Rust Video Keyframe Extractor v0.1.0"); + println!("🧵 Threads: {}", rayon::current_num_threads()); + + // Verify FFmpeg availability + if !args.ffmpeg_path.exists() && args.ffmpeg_path.to_str() == Some("ffmpeg") { + // Try to find ffmpeg in PATH + if Command::new("ffmpeg").arg("-version").output().is_err() { + anyhow::bail!("FFmpeg not found. Please install FFmpeg or specify path with --ffmpeg-path"); + } + } else if !args.ffmpeg_path.exists() { + anyhow::bail!("FFmpeg not found at: {}", args.ffmpeg_path.display()); + } + + if args.benchmark { + // Benchmark mode + let video_path = args.input.clone() + .ok_or_else(|| anyhow::anyhow!("Benchmark requires input video file --input "))?; + + if !video_path.exists() { + anyhow::bail!("Video file not found: {}", video_path.display()); + } + + run_benchmark_suite(&video_path, &args.output, &args.ffmpeg_path, &args)?; + } else { + // Single processing mode + let video_path = args.input + .ok_or_else(|| anyhow::anyhow!("Please specify input video file --input "))?; + + if !video_path.exists() { + anyhow::bail!("Video file not found: {}", video_path.display()); + } + + // Run single keyframe extraction + let result = run_performance_test( + &video_path, + args.threshold, + "Single Processing", + &args.ffmpeg_path, + args.max_frames, + args.use_simd, + args.block_size, + args.verbose, + )?; + + // Extract and save keyframes + let (frames, _, _) = extract_frames_memory_stream(&video_path, &args.ffmpeg_path, args.max_frames, args.verbose)?; + let keyframe_indices = extract_keyframes_optimized(&frames, args.threshold, args.use_simd, args.block_size, args.verbose)?; + let saved_count = save_keyframes_optimized(&video_path, &keyframe_indices, &args.output, &args.ffmpeg_path, args.max_save, args.verbose)?; + + println!("\n✅ Processing Complete!"); + println!("🎯 Keyframes extracted: {}", result.keyframes_extracted); + println!("💾 Keyframes saved: {}", saved_count); + println!("⚡ Processing speed: {:.1} FPS", result.processing_fps); + println!("📁 Output directory: {}", args.output.display()); + + // Save processing report + let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); + let report_file = args.output.join(format!("processing_report_{}.json", timestamp)); + let json_result = serde_json::to_string_pretty(&result)?; + fs::write(&report_file, json_result)?; + + if args.verbose { + println!("📄 Processing report saved to: {}", report_file.display()); + } + } + + Ok(()) +} diff --git a/src/chat/utils/rust-video/start_server.py b/src/chat/utils/rust-video/start_server.py new file mode 100644 index 000000000..b1547d441 --- /dev/null +++ b/src/chat/utils/rust-video/start_server.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +""" +启动脚本 + +支持开发模式和生产模式启动 +""" + +import os +import sys +import subprocess +import argparse +from pathlib import Path +from config import config + + +def check_rust_executable(): + """检查 Rust 可执行文件是否存在""" + rust_config = config.get("rust") + executable_name = rust_config.get("executable_name", "video_keyframe_extractor") + executable_path = rust_config.get("executable_path", "target/release") + + possible_paths = [ + f"./{executable_path}/{executable_name}.exe", + f"./{executable_path}/{executable_name}", + f"./{executable_name}.exe", + f"./{executable_name}" + ] + + for path in possible_paths: + if Path(path).exists(): + print(f"✓ Found Rust executable: {path}") + return str(Path(path).absolute()) + + print("⚠ Warning: Rust executable not found") + print("Please compile first: cargo build --release") + return None + + +def check_dependencies(): + """检查 Python 依赖""" + try: + import fastapi + import uvicorn + print("✓ FastAPI dependencies available") + return True + except ImportError as e: + print(f"✗ Missing dependencies: {e}") + print("Please install: pip install -r requirements.txt") + return False + + +def install_dependencies(): + """安装依赖""" + print("Installing dependencies...") + try: + subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], + check=True) + print("✓ Dependencies installed successfully") + return True + except subprocess.CalledProcessError as e: + print(f"✗ Failed to install dependencies: {e}") + return False + + +def start_development_server(host="127.0.0.1", port=8050, reload=True): + """启动开发服务器""" + print(f" Starting development server on http://{host}:{port}") + print(f" API docs: http://{host}:{port}/docs") + print(f" Health check: http://{host}:{port}/health") + + try: + import uvicorn + uvicorn.run( + "api_server:app", + host=host, + port=port, + reload=reload, + log_level="info" + ) + except ImportError: + print("uvicorn not found, trying with subprocess...") + subprocess.run([ + sys.executable, "-m", "uvicorn", + "api_server:app", + "--host", host, + "--port", str(port), + "--reload" if reload else "" + ]) + + +def start_production_server(host="0.0.0.0", port=8000, workers=4): + """启动生产服务器""" + print(f"🚀 Starting production server on http://{host}:{port}") + print(f"Workers: {workers}") + + subprocess.run([ + sys.executable, "-m", "uvicorn", + "api_server:app", + "--host", host, + "--port", str(port), + "--workers", str(workers), + "--log-level", "warning" + ]) + + +def create_systemd_service(): + """创建 systemd 服务文件""" + current_dir = Path.cwd() + python_path = sys.executable + + service_content = f"""[Unit] +Description=Video Keyframe Extraction API Server +After=network.target + +[Service] +Type=exec +User=www-data +WorkingDirectory={current_dir} +Environment=PATH=/usr/bin:/usr/local/bin +ExecStart={python_path} -m uvicorn api_server:app --host 0.0.0.0 --port 8000 --workers 4 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +""" + + service_file = Path("/etc/systemd/system/video-keyframe-api.service") + + try: + with open(service_file, 'w') as f: + f.write(service_content) + + print(f"✓ Systemd service created: {service_file}") + print("To enable and start:") + print(" sudo systemctl enable video-keyframe-api") + print(" sudo systemctl start video-keyframe-api") + + except PermissionError: + print("✗ Permission denied. Please run with sudo for systemd service creation") + + # 创建本地副本 + local_service = Path("./video-keyframe-api.service") + with open(local_service, 'w') as f: + f.write(service_content) + + print(f"✓ Service file created locally: {local_service}") + print(f"To install: sudo cp {local_service} /etc/systemd/system/") + + +def main(): + parser = argparse.ArgumentParser(description="Video Keyframe Extraction API Server") + + # 从配置文件获取默认值 + server_config = config.get_server_config() + + parser.add_argument("--mode", choices=["dev", "prod", "install"], default="dev", + help="运行模式: dev (开发), prod (生产), install (安装依赖)") + parser.add_argument("--host", default=server_config.get("host", "127.0.0.1"), help="绑定主机") + parser.add_argument("--port", type=int, default=server_config.get("port", 8000), help="端口号") + parser.add_argument("--workers", type=int, default=server_config.get("workers", 4), help="生产模式工作进程数") + parser.add_argument("--no-reload", action="store_true", help="禁用自动重载") + parser.add_argument("--check", action="store_true", help="仅检查环境") + parser.add_argument("--create-service", action="store_true", help="创建 systemd 服务") + + args = parser.parse_args() + + print("=== Video Keyframe Extraction API Server ===") + + # 检查环境 + rust_exe = check_rust_executable() + deps_ok = check_dependencies() + + if args.check: + print("\n=== Environment Check ===") + print(f"Rust executable: {'✓' if rust_exe else '✗'}") + print(f"Python dependencies: {'✓' if deps_ok else '✗'}") + return + + if args.create_service: + create_systemd_service() + return + + # 安装模式 + if args.mode == "install": + if not deps_ok: + install_dependencies() + else: + print("✓ Dependencies already installed") + return + + # 检查必要条件 + if not rust_exe: + print("✗ Cannot start without Rust executable") + print("Please run: cargo build --release") + sys.exit(1) + + if not deps_ok: + print("Installing missing dependencies...") + if not install_dependencies(): + sys.exit(1) + + # 启动服务器 + if args.mode == "dev": + start_development_server( + host=args.host, + port=args.port, + reload=not args.no_reload + ) + elif args.mode == "prod": + start_production_server( + host=args.host, + port=args.port, + workers=args.workers + ) + + +if __name__ == "__main__": + main() diff --git a/src/config/config.py b/src/config/config.py index e403f7ce0..9aa25cc15 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -41,7 +41,7 @@ from src.config.official_configs import ( WebSearchConfig, AntiPromptInjectionConfig, PluginsConfig, - WakeUpSystemConfig, + SleepSystemConfig, MonthlyPlanSystemConfig, CrossContextConfig, PermissionConfig, @@ -390,7 +390,7 @@ class Config(ValidatedConfigBase): dependency_management: DependencyManagementConfig = Field(default_factory=lambda: DependencyManagementConfig(), description="依赖管理配置") web_search: WebSearchConfig = Field(default_factory=lambda: WebSearchConfig(), description="网络搜索配置") plugins: PluginsConfig = Field(default_factory=lambda: PluginsConfig(), description="插件配置") - wakeup_system: WakeUpSystemConfig = Field(default_factory=lambda: WakeUpSystemConfig(), description="唤醒度系统配置") + sleep_system: SleepSystemConfig = Field(default_factory=lambda: SleepSystemConfig(), description="睡眠系统配置") monthly_plan_system: MonthlyPlanSystemConfig = Field(default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置") cross_context: CrossContextConfig = Field(default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置") maizone_intercom: MaizoneIntercomConfig = Field(default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 28e617474..7e8dd4446 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -530,15 +530,6 @@ class ScheduleConfig(ValidatedConfigBase): enable: bool = Field(default=True, description="启用") guidelines: Optional[str] = Field(default=None, description="指导方针") enable_is_sleep: bool = Field(default=True, description="让AI会根据日程表睡觉和苏醒") - - enable_flexible_sleep: bool = Field(default=True, description="是否启用弹性睡眠") - flexible_sleep_pressure_threshold: float = Field(default=40.0, description="触发弹性睡眠的睡眠压力阈值,低于该值可能延迟入睡") - max_sleep_delay_minutes: int = Field(default=60, description="单日最大延迟入睡分钟数") - - enable_pre_sleep_notification: bool = Field(default=True, description="是否启用睡前消息") - pre_sleep_notification_groups: List[str] = Field(default_factory=list, description="接收睡前消息的群号列表, 格式: [\"platform:group_id1\", \"platform:group_id2\"]") - pre_sleep_prompt: str = Field(default="我准备睡觉了,请生成一句简短自然的晚安问候。", description="用于生成睡前消息的提示") - class DependencyManagementConfig(ValidatedConfigBase): @@ -610,10 +601,10 @@ class PluginsConfig(ValidatedConfigBase): centralized_config: bool = Field(default=True, description="是否启用插件配置集中化管理") -class WakeUpSystemConfig(ValidatedConfigBase): - """唤醒度与失眠系统配置类""" +class SleepSystemConfig(ValidatedConfigBase): + """睡眠系统配置类""" - enable: bool = Field(default=True, description="是否启用唤醒度系统") + enable: bool = Field(default=True, description="是否启用睡眠系统") wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒") private_message_increment: float = Field(default=3.0, ge=0.1, description="私聊消息增加的唤醒度") group_mention_increment: float = Field(default=2.0, ge=0.1, description="群聊艾特增加的唤醒度") @@ -633,6 +624,14 @@ class WakeUpSystemConfig(ValidatedConfigBase): sleep_pressure_increment: float = Field(default=1.5, ge=0.0, description="每次AI执行动作后,增加的睡眠压力值") sleep_pressure_decay_rate: float = Field(default=1.5, ge=0.0, description="睡眠时,每分钟衰减的睡眠压力值") + # --- 弹性睡眠与睡前消息 --- + enable_flexible_sleep: bool = Field(default=True, description="是否启用弹性睡眠") + flexible_sleep_pressure_threshold: float = Field(default=40.0, description="触发弹性睡眠的睡眠压力阈值,低于该值可能延迟入睡") + max_sleep_delay_minutes: int = Field(default=60, description="单日最大延迟入睡分钟数") + enable_pre_sleep_notification: bool = Field(default=True, description="是否启用睡前消息") + pre_sleep_notification_groups: List[str] = Field(default_factory=list, description="接收睡前消息的群号列表, 格式: [\"platform:group_id1\", \"platform:group_id2\"]") + pre_sleep_prompt: str = Field(default="我准备睡觉了,请生成一句简短自然的晚安问候。", description="用于生成睡前消息的提示") + class MonthlyPlanSystemConfig(ValidatedConfigBase): """月度计划系统配置类""" diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 01859d257..8341653ff 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -294,7 +294,7 @@ class LLMRequest: for model_info, api_provider, client in model_scheduler: start_time = time.time() model_name = model_info.name - logger.info(f"正在尝试使用模型: {model_name}") + logger.debug(f"正在尝试使用模型: {model_name}") # 你不许刷屏 try: # 检查是否启用反截断 @@ -370,7 +370,7 @@ class LLMRequest: raise RuntimeError("生成空回复") content = "生成的响应为空" - logger.info(f"模型 '{model_name}' 成功生成回复。") + logger.debug(f"模型 '{model_name}' 成功生成回复。") # 你也不许刷屏 return content, (reasoning_content, model_name, tool_calls) except RespNotOkException as e: diff --git a/src/plugin_system/core/plugin_hot_reload.py b/src/plugin_system/core/plugin_hot_reload.py index a88d3e568..c35aa71ed 100644 --- a/src/plugin_system/core/plugin_hot_reload.py +++ b/src/plugin_system/core/plugin_hot_reload.py @@ -5,7 +5,9 @@ """ import os +import sys import time +import importlib from pathlib import Path from threading import Thread from typing import Dict, Set, List, Optional, Tuple @@ -27,7 +29,8 @@ class PluginFileHandler(FileSystemEventHandler): self.hot_reload_manager = hot_reload_manager self.pending_reloads: Set[str] = set() # 待重载的插件名称 self.last_reload_time: Dict[str, float] = {} # 上次重载时间 - self.debounce_delay = 1.0 # 防抖延迟(秒) + self.debounce_delay = 2.0 # 增加防抖延迟到2秒,确保文件写入完成 + self.file_change_cache: Dict[str, float] = {} # 文件变化缓存 def on_modified(self, event): """文件修改事件""" @@ -60,26 +63,42 @@ class PluginFileHandler(FileSystemEventHandler): plugin_name, source_type = plugin_info current_time = time.time() - last_time = self.last_reload_time.get(plugin_name, 0) - - # 防抖处理,避免频繁重载 - if current_time - last_time < self.debounce_delay: + + # 文件变化缓存,避免重复处理同一文件的快速连续变化 + file_cache_key = f"{file_path}_{change_type}" + last_file_time = self.file_change_cache.get(file_cache_key, 0) + if current_time - last_file_time < 0.5: # 0.5秒内的重复文件变化忽略 + return + self.file_change_cache[file_cache_key] = current_time + + # 插件级别的防抖处理 + last_plugin_time = self.last_reload_time.get(plugin_name, 0) + if current_time - last_plugin_time < self.debounce_delay: + # 如果在防抖期内,更新待重载标记但不立即处理 + self.pending_reloads.add(plugin_name) return file_name = Path(file_path).name - logger.info(f"📁 检测到插件文件变化: {file_name} ({change_type}) [{source_type}]") + logger.info(f"📁 检测到插件文件变化: {file_name} ({change_type}) [{source_type}] -> {plugin_name}") # 如果是删除事件,处理关键文件删除 if change_type == "deleted": + # 解析实际的插件名称 + actual_plugin_name = self.hot_reload_manager._resolve_plugin_name(plugin_name) + if file_name == "plugin.py": - if plugin_name in plugin_manager.loaded_plugins: - logger.info(f"🗑️ 插件主文件被删除,卸载插件: {plugin_name} [{source_type}]") - self.hot_reload_manager._unload_plugin(plugin_name) + if actual_plugin_name in plugin_manager.loaded_plugins: + logger.info(f"🗑️ 插件主文件被删除,卸载插件: {plugin_name} -> {actual_plugin_name} [{source_type}]") + self.hot_reload_manager._unload_plugin(actual_plugin_name) + else: + logger.info(f"🗑️ 插件主文件被删除,但插件未加载: {plugin_name} -> {actual_plugin_name} [{source_type}]") return elif file_name in ("manifest.toml", "_manifest.json"): - if plugin_name in plugin_manager.loaded_plugins: - logger.info(f"🗑️ 插件配置文件被删除,卸载插件: {plugin_name} [{source_type}]") - self.hot_reload_manager._unload_plugin(plugin_name) + if actual_plugin_name in plugin_manager.loaded_plugins: + logger.info(f"🗑️ 插件配置文件被删除,卸载插件: {plugin_name} -> {actual_plugin_name} [{source_type}]") + self.hot_reload_manager._unload_plugin(actual_plugin_name) + else: + logger.info(f"🗑️ 插件配置文件被删除,但插件未加载: {plugin_name} -> {actual_plugin_name} [{source_type}]") return # 对于修改和创建事件,都进行重载 @@ -87,29 +106,43 @@ class PluginFileHandler(FileSystemEventHandler): self.pending_reloads.add(plugin_name) self.last_reload_time[plugin_name] = current_time - # 延迟重载,避免文件正在写入时重载 + # 延迟重载,确保文件写入完成 reload_thread = Thread( target=self._delayed_reload, - args=(plugin_name, source_type), + args=(plugin_name, source_type, current_time), daemon=True ) reload_thread.start() except Exception as e: - logger.error(f"❌ 处理文件变化时发生错误: {e}") + logger.error(f"❌ 处理文件变化时发生错误: {e}", exc_info=True) - def _delayed_reload(self, plugin_name: str, source_type: str): + def _delayed_reload(self, plugin_name: str, source_type: str, trigger_time: float): """延迟重载插件""" try: + # 等待文件写入完成 time.sleep(self.debounce_delay) - if plugin_name in self.pending_reloads: - self.pending_reloads.remove(plugin_name) - logger.info(f"🔄 延迟重载插件: {plugin_name} [{source_type}]") - self.hot_reload_manager._reload_plugin(plugin_name) + # 检查是否还需要重载(可能在等待期间有更新的变化) + if plugin_name not in self.pending_reloads: + return + + # 检查是否有更新的重载请求 + if self.last_reload_time.get(plugin_name, 0) > trigger_time: + return + + self.pending_reloads.discard(plugin_name) + logger.info(f"🔄 开始延迟重载插件: {plugin_name} [{source_type}]") + + # 执行深度重载 + success = self.hot_reload_manager._deep_reload_plugin(plugin_name) + if success: + logger.info(f"✅ 插件重载成功: {plugin_name} [{source_type}]") + else: + logger.error(f"❌ 插件重载失败: {plugin_name} [{source_type}]") except Exception as e: - logger.error(f"❌ 延迟重载插件 {plugin_name} 时发生错误: {e}") + logger.error(f"❌ 延迟重载插件 {plugin_name} 时发生错误: {e}", exc_info=True) def _get_plugin_info_from_path(self, file_path: str) -> Optional[Tuple[str, str]]: """从文件路径获取插件信息 @@ -237,35 +270,131 @@ class PluginHotReloadManager: logger.info("🛑 插件热重载已停止") def _reload_plugin(self, plugin_name: str): - """重载指定插件""" + """重载指定插件(简单重载)""" try: - logger.info(f"🔄 开始重载插件: {plugin_name}") + # 解析实际的插件名称 + actual_plugin_name = self._resolve_plugin_name(plugin_name) + logger.info(f"🔄 开始简单重载插件: {plugin_name} -> {actual_plugin_name}") - if plugin_manager.reload_plugin(plugin_name): - logger.info(f"✅ 插件重载成功: {plugin_name}") + if plugin_manager.reload_plugin(actual_plugin_name): + logger.info(f"✅ 插件简单重载成功: {actual_plugin_name}") + return True else: - logger.error(f"❌ 插件重载失败: {plugin_name}") + logger.error(f"❌ 插件简单重载失败: {actual_plugin_name}") + return False except Exception as e: - logger.error(f"❌ 重载插件 {plugin_name} 时发生错误: {e}") + logger.error(f"❌ 重载插件 {plugin_name} 时发生错误: {e}", exc_info=True) + return False + + def _resolve_plugin_name(self, folder_name: str) -> str: + """ + 将文件夹名称解析为实际的插件名称 + 通过检查插件管理器中的路径映射来找到对应的插件名 + """ + # 首先检查是否直接匹配 + if folder_name in plugin_manager.plugin_classes: + logger.debug(f"🔍 直接匹配插件名: {folder_name}") + return folder_name + + # 如果没有直接匹配,搜索路径映射,并优先返回在插件类中存在的名称 + matched_plugins = [] + for plugin_name, plugin_path in plugin_manager.plugin_paths.items(): + # 检查路径是否包含该文件夹名 + if folder_name in plugin_path: + matched_plugins.append((plugin_name, plugin_path)) + + # 在匹配的插件中,优先选择在插件类中存在的 + for plugin_name, plugin_path in matched_plugins: + if plugin_name in plugin_manager.plugin_classes: + logger.debug(f"🔍 文件夹名 '{folder_name}' 映射到插件名 '{plugin_name}' (路径: {plugin_path})") + return plugin_name + + # 如果还是没找到在插件类中存在的,返回第一个匹配项 + if matched_plugins: + plugin_name, plugin_path = matched_plugins[0] + logger.warning(f"⚠️ 文件夹 '{folder_name}' 映射到 '{plugin_name}',但该插件类不存在") + return plugin_name + + # 如果还是没找到,返回原文件夹名 + logger.warning(f"⚠️ 无法找到文件夹 '{folder_name}' 对应的插件名,使用原名称") + return folder_name + + def _deep_reload_plugin(self, plugin_name: str): + """深度重载指定插件(清理模块缓存)""" + try: + # 解析实际的插件名称 + actual_plugin_name = self._resolve_plugin_name(plugin_name) + logger.info(f"🔄 开始深度重载插件: {plugin_name} -> {actual_plugin_name}") + + # 强制清理相关模块缓存 + self._force_clear_plugin_modules(plugin_name) + + # 使用插件管理器的强制重载功能 + success = plugin_manager.force_reload_plugin(actual_plugin_name) + + if success: + logger.info(f"✅ 插件深度重载成功: {actual_plugin_name}") + return True + else: + logger.error(f"❌ 插件深度重载失败,尝试简单重载: {actual_plugin_name}") + # 如果深度重载失败,尝试简单重载 + return self._reload_plugin(actual_plugin_name) + + except Exception as e: + logger.error(f"❌ 深度重载插件 {plugin_name} 时发生错误: {e}", exc_info=True) + # 出错时尝试简单重载 + return self._reload_plugin(plugin_name) + + def _force_clear_plugin_modules(self, plugin_name: str): + """强制清理插件相关的模块缓存""" + import sys + + # 找到所有相关的模块名 + modules_to_remove = [] + plugin_module_prefix = f"src.plugins.built_in.{plugin_name}" + + for module_name in list(sys.modules.keys()): + if plugin_module_prefix in module_name: + modules_to_remove.append(module_name) + + # 删除模块缓存 + for module_name in modules_to_remove: + if module_name in sys.modules: + logger.debug(f"🗑️ 清理模块缓存: {module_name}") + del sys.modules[module_name] + + def _force_reimport_plugin(self, plugin_name: str): + """强制重新导入插件(委托给插件管理器)""" + try: + # 使用插件管理器的重载功能 + success = plugin_manager.reload_plugin(plugin_name) + return success + + except Exception as e: + logger.error(f"❌ 强制重新导入插件 {plugin_name} 时发生错误: {e}", exc_info=True) + return False def _unload_plugin(self, plugin_name: str): """卸载指定插件""" try: logger.info(f"🗑️ 开始卸载插件: {plugin_name}") - + if plugin_manager.unload_plugin(plugin_name): logger.info(f"✅ 插件卸载成功: {plugin_name}") + return True else: logger.error(f"❌ 插件卸载失败: {plugin_name}") + return False except Exception as e: - logger.error(f"❌ 卸载插件 {plugin_name} 时发生错误: {e}") + logger.error(f"❌ 卸载插件 {plugin_name} 时发生错误: {e}", exc_info=True) + return False def reload_all_plugins(self): """重载所有插件""" try: - logger.info("🔄 开始重载所有插件...") + logger.info("🔄 开始深度重载所有插件...") # 获取当前已加载的插件列表 loaded_plugins = list(plugin_manager.loaded_plugins.keys()) @@ -274,15 +403,42 @@ class PluginHotReloadManager: fail_count = 0 for plugin_name in loaded_plugins: - if plugin_manager.reload_plugin(plugin_name): + logger.info(f"🔄 重载插件: {plugin_name}") + if self._deep_reload_plugin(plugin_name): success_count += 1 else: fail_count += 1 logger.info(f"✅ 插件重载完成: 成功 {success_count} 个,失败 {fail_count} 个") + + # 清理全局缓存 + importlib.invalidate_caches() except Exception as e: - logger.error(f"❌ 重载所有插件时发生错误: {e}") + logger.error(f"❌ 重载所有插件时发生错误: {e}", exc_info=True) + + def force_reload_plugin(self, plugin_name: str): + """手动强制重载指定插件(委托给插件管理器)""" + try: + logger.info(f"🔄 手动强制重载插件: {plugin_name}") + + # 清理待重载列表中的该插件(避免重复重载) + for handler in self.file_handlers: + handler.pending_reloads.discard(plugin_name) + + # 使用插件管理器的强制重载功能 + success = plugin_manager.force_reload_plugin(plugin_name) + + if success: + logger.info(f"✅ 手动强制重载成功: {plugin_name}") + else: + logger.error(f"❌ 手动强制重载失败: {plugin_name}") + + return success + + except Exception as e: + logger.error(f"❌ 手动强制重载插件 {plugin_name} 时发生错误: {e}", exc_info=True) + return False def add_watch_directory(self, directory: str): """添加新的监听目录""" @@ -321,14 +477,33 @@ class PluginHotReloadManager: def get_status(self) -> dict: """获取热重载状态""" + pending_reloads = set() + if self.file_handlers: + for handler in self.file_handlers: + pending_reloads.update(handler.pending_reloads) + return { "is_running": self.is_running, "watch_directories": self.watch_directories, "active_observers": len(self.observers), "loaded_plugins": len(plugin_manager.loaded_plugins), "failed_plugins": len(plugin_manager.failed_plugins), + "pending_reloads": list(pending_reloads), + "debounce_delay": self.file_handlers[0].debounce_delay if self.file_handlers else 0, } + def clear_all_caches(self): + """清理所有Python模块缓存""" + try: + logger.info("🧹 开始清理所有Python模块缓存...") + + # 重新扫描所有插件目录,这会重新加载模块 + plugin_manager.rescan_plugin_directory() + logger.info("✅ 模块缓存清理完成") + + except Exception as e: + logger.error(f"❌ 清理模块缓存时发生错误: {e}", exc_info=True) + # 全局热重载管理器实例 hot_reload_manager = PluginHotReloadManager() diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 5da07369f..4b4de3684 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -2,6 +2,7 @@ import asyncio import os import traceback import sys +import importlib from typing import Dict, List, Optional, Tuple, Type, Any from importlib.util import spec_from_file_location, module_from_spec @@ -289,11 +290,11 @@ class PluginManager: Args: plugin_file: 插件文件路径 - plugin_name: 插件名称 - plugin_dir: 插件目录路径 """ - # 生成模块名 + # 生成模块名和插件信息 plugin_path = Path(plugin_file) + plugin_dir = plugin_path.parent # 插件目录 + plugin_name = plugin_dir.name # 插件名称 module_name = ".".join(plugin_path.parent.parts) try: @@ -307,13 +308,13 @@ class PluginManager: module.__package__ = module_name # 设置模块包名 spec.loader.exec_module(module) - logger.debug(f"插件模块加载成功: {plugin_file}") + logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})") return True except Exception as e: error_msg = f"加载插件模块 {plugin_file} 失败: {e}" logger.error(error_msg) - self.failed_plugins[module_name] = error_msg + self.failed_plugins[plugin_name if 'plugin_name' in locals() else module_name] = error_msg return False # == 兼容性检查 == @@ -527,6 +528,10 @@ class PluginManager: # 从已加载插件中移除 del self.loaded_plugins[plugin_name] + # 从插件类注册表中移除 + if plugin_name in self.plugin_classes: + del self.plugin_classes[plugin_name] + # 从失败列表中移除(如果存在) if plugin_name in self.failed_plugins: del self.failed_plugins[plugin_name] @@ -535,7 +540,7 @@ class PluginManager: return True except Exception as e: - logger.error(f"❌ 插件卸载失败: {plugin_name} - {str(e)}") + logger.error(f"❌ 插件卸载失败: {plugin_name} - {str(e)}", exc_info=True) return False def reload_plugin(self, plugin_name: str) -> bool: @@ -548,55 +553,54 @@ class PluginManager: bool: 重载是否成功 """ try: - # 先卸载插件 + logger.info(f"🔄 开始重载插件: {plugin_name}") + + # 卸载插件 if plugin_name in self.loaded_plugins: - self.unload_plugin(plugin_name) + if not self.unload_plugin(plugin_name): + logger.warning(f"⚠️ 插件卸载失败,继续重载: {plugin_name}") - # 清除Python模块缓存 - plugin_path = self.plugin_paths.get(plugin_name) - if plugin_path: - plugin_file = os.path.join(plugin_path, "plugin.py") - if os.path.exists(plugin_file): - # 从sys.modules中移除相关模块 - modules_to_remove = [] - plugin_module_prefix = ".".join(Path(plugin_file).parent.parts) + # 重新扫描插件目录 + self.rescan_plugin_directory() - for module_name in sys.modules: - if module_name.startswith(plugin_module_prefix): - modules_to_remove.append(module_name) - - for module_name in modules_to_remove: - del sys.modules[module_name] - - # 从插件类注册表中移除 - if plugin_name in self.plugin_classes: - del self.plugin_classes[plugin_name] - - # 重新加载插件模块 - if self._load_plugin_module_file(plugin_file): - # 重新加载插件实例 - success, _ = self.load_registered_plugin_classes(plugin_name) - if success: - logger.info(f"🔄 插件重载成功: {plugin_name}") - return True - else: - logger.error(f"❌ 插件重载失败: {plugin_name} - 实例化失败") - return False - else: - logger.error(f"❌ 插件重载失败: {plugin_name} - 模块加载失败") - return False + # 重新加载插件实例 + if plugin_name in self.plugin_classes: + success, _ = self.load_registered_plugin_classes(plugin_name) + if success: + logger.info(f"✅ 插件重载成功: {plugin_name}") + return True else: - logger.error(f"❌ 插件重载失败: {plugin_name} - 插件文件不存在") + logger.error(f"❌ 插件重载失败: {plugin_name} - 实例化失败") return False else: - logger.error(f"❌ 插件重载失败: {plugin_name} - 插件路径未知") + logger.error(f"❌ 插件重载失败: {plugin_name} - 插件类未找到") return False except Exception as e: - logger.error(f"❌ 插件重载失败: {plugin_name} - {str(e)}") - logger.debug("详细错误信息: ", exc_info=True) + logger.error(f"❌ 插件重载失败: {plugin_name} - {str(e)}", exc_info=True) return False + def force_reload_plugin(self, plugin_name: str) -> bool: + """强制重载插件(使用简化的方法) + + Args: + plugin_name: 插件名称 + + Returns: + bool: 重载是否成功 + """ + return self.reload_plugin(plugin_name) + + def clear_all_plugin_caches(self): + """清理所有插件相关的模块缓存(简化版)""" + try: + logger.info("🧹 清理模块缓存...") + # 清理importlib缓存 + importlib.invalidate_caches() + logger.info("🧹 模块缓存清理完成") + except Exception as e: + logger.error(f"❌ 清理模块缓存时发生错误: {e}", exc_info=True) + # 全局插件管理器实例 plugin_manager = PluginManager() diff --git a/src/plugins/built_in/maizone_refactored/actions/read_feed_action.py b/src/plugins/built_in/maizone_refactored/actions/read_feed_action.py index 934704dd3..d9089b52a 100644 --- a/src/plugins/built_in/maizone_refactored/actions/read_feed_action.py +++ b/src/plugins/built_in/maizone_refactored/actions/read_feed_action.py @@ -7,6 +7,7 @@ from typing import Tuple from src.common.logger import get_logger from src.plugin_system import BaseAction, ActionActivationType, ChatMode from src.plugin_system.apis import person_api, generator_api +from src.plugin_system.apis.permission_api import permission_api from ..services.manager import get_qzone_service, get_config_getter logger = get_logger("MaiZone.ReadFeedAction") @@ -32,27 +33,11 @@ class ReadFeedAction(BaseAction): async def _check_permission(self) -> bool: """检查当前用户是否有权限执行此动作""" - user_name = self.action_data.get("user_name", "") - person_id = person_api.get_person_id_by_name(user_name) - if not person_id: - return False + platform = self.chat_stream.platform + user_id = self.chat_stream.user_info.user_id - user_id = await person_api.get_person_value(person_id, "user_id") - if not user_id: - return False - - get_config = get_config_getter() - permission_list = get_config("read.permission", []) - permission_type = get_config("read.permission_type", "blacklist") - - if not isinstance(permission_list, list): - return False - - if permission_type == 'whitelist': - return user_id in permission_list - elif permission_type == 'blacklist': - return user_id not in permission_list - return False + # 使用权限API检查用户是否有阅读说说的权限 + return permission_api.check_permission(platform, user_id, "plugin.maizone.read_feed") async def execute(self) -> Tuple[bool, str]: """ diff --git a/src/plugins/built_in/maizone_refactored/actions/send_feed_action.py b/src/plugins/built_in/maizone_refactored/actions/send_feed_action.py index 7b11ffe23..08fa372e1 100644 --- a/src/plugins/built_in/maizone_refactored/actions/send_feed_action.py +++ b/src/plugins/built_in/maizone_refactored/actions/send_feed_action.py @@ -7,6 +7,7 @@ from typing import Tuple from src.common.logger import get_logger from src.plugin_system import BaseAction, ActionActivationType, ChatMode from src.plugin_system.apis import person_api, generator_api +from src.plugin_system.apis.permission_api import permission_api from ..services.manager import get_qzone_service, get_config_getter logger = get_logger("MaiZone.SendFeedAction") @@ -32,27 +33,11 @@ class SendFeedAction(BaseAction): async def _check_permission(self) -> bool: """检查当前用户是否有权限执行此动作""" - user_name = self.action_data.get("user_name", "") - person_id = person_api.get_person_id_by_name(user_name) - if not person_id: - return False + platform = self.chat_stream.platform + user_id = self.chat_stream.user_info.user_id - user_id = await person_api.get_person_value(person_id, "user_id") - if not user_id: - return False - - get_config = get_config_getter() - permission_list = get_config("send.permission", []) - permission_type = get_config("send.permission_type", "whitelist") - - if not isinstance(permission_list, list): - return False - - if permission_type == 'whitelist': - return user_id in permission_list - elif permission_type == 'blacklist': - return user_id not in permission_list - return False + # 使用权限API检查用户是否有发送说说的权限 + return permission_api.check_permission(platform, user_id, "plugin.maizone.send_feed") async def execute(self) -> Tuple[bool, str]: """ diff --git a/src/plugins/built_in/maizone_refactored/commands/send_feed_command.py b/src/plugins/built_in/maizone_refactored/commands/send_feed_command.py index 3a7be39d5..fc41b51ff 100644 --- a/src/plugins/built_in/maizone_refactored/commands/send_feed_command.py +++ b/src/plugins/built_in/maizone_refactored/commands/send_feed_command.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -发送说说命令组件 +发送说说命令 await self.send_text(f"收到!正在为你生成关于"{topic or '随机'}"的说说,请稍候...【热重载测试成功】")件 """ from typing import Tuple @@ -16,15 +16,16 @@ logger = get_logger("MaiZone.SendFeedCommand") class SendFeedCommand(PlusCommand): """ 响应用户通过 `/send_feed` 命令发送说说的请求。 + 测试热重载功能 - 这是一个测试注释,现在应该可以正常工作了! """ command_name: str = "send_feed" - command_description: str = "发送一条QQ空间说说" + command_description: str = "发一条QQ空间说说" command_aliases = ["发空间"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - @require_permission("plugin.send.permission") + @require_permission("plugin.maizone.send_feed", "❌ 你没有发送QQ空间说说的权限") async def execute(self, args: CommandArgs) -> Tuple[bool, str, bool]: """ 执行命令的核心逻辑。 diff --git a/src/plugins/built_in/maizone_refactored/plugin.py b/src/plugins/built_in/maizone_refactored/plugin.py index e392f475b..6507c1c92 100644 --- a/src/plugins/built_in/maizone_refactored/plugin.py +++ b/src/plugins/built_in/maizone_refactored/plugin.py @@ -84,12 +84,19 @@ class MaiZoneRefactoredPlugin(BasePlugin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # 注册权限节点 permission_api.register_permission_node( - "plugin.send.permission", - "是否可以使用机器人发送说说", + "plugin.maizone.send_feed", + "是否可以使用机器人发送QQ空间说说", "maiZone", False ) + permission_api.register_permission_node( + "plugin.maizone.read_feed", + "是否可以使用机器人读取QQ空间说说", + "maiZone", + True + ) content_service = ContentService(self.get_config) image_service = ImageService(self.get_config) cookie_service = CookieService(self.get_config) @@ -102,10 +109,18 @@ class MaiZoneRefactoredPlugin(BasePlugin): register_service("reply_tracker", reply_tracker_service) register_service("get_config", self.get_config) - asyncio.create_task(scheduler_service.start()) - asyncio.create_task(monitor_service.start()) + # 保存服务引用以便后续启动 + self.scheduler_service = scheduler_service + self.monitor_service = monitor_service - logger.info("MaiZone重构版插件已加载,服务已注册,后台任务已启动。") + logger.info("MaiZone重构版插件已加载,服务已注册。") + + async def on_plugin_loaded(self): + """插件加载完成后的回调,启动异步服务""" + if hasattr(self, 'scheduler_service') and hasattr(self, 'monitor_service'): + asyncio.create_task(self.scheduler_service.start()) + asyncio.create_task(self.monitor_service.start()) + logger.info("MaiZone后台任务已启动。") def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: return [ diff --git a/src/plugins/built_in/plugin_management/plugin.py b/src/plugins/built_in/plugin_management/plugin.py index a0135d4d9..d0ffd484d 100644 --- a/src/plugins/built_in/plugin_management/plugin.py +++ b/src/plugins/built_in/plugin_management/plugin.py @@ -13,226 +13,313 @@ from src.plugin_system import ( ComponentType, send_api, ) +from src.plugin_system.base.plus_command import PlusCommand +from src.plugin_system.base.command_args import CommandArgs +from src.plugin_system.base.component_types import PlusCommandInfo, ChatType +from src.plugin_system.apis.permission_api import permission_api +from src.plugin_system.utils.permission_decorators import require_permission +from src.plugin_system.core.plugin_hot_reload import hot_reload_manager -class ManagementCommand(BaseCommand): - command_name: str = "management" - description: str = "管理命令" - command_pattern: str = r"(?P^/pm(\s[a-zA-Z0-9_]+)*\s*$)" +class ManagementCommand(PlusCommand): + """插件管理命令 - 使用PlusCommand系统""" + + command_name = "pm" + command_description = "插件管理命令,支持插件和组件的管理操作" + command_aliases = ["pluginmanage", "插件管理"] + priority = 10 + chat_type_allow = ChatType.ALL + intercept_message = True - async def execute(self) -> Tuple[bool, str, bool]: - # sourcery skip: merge-duplicate-blocks - if ( - not self.message - or not self.message.message_info - or not self.message.message_info.user_info - or str(self.message.message_info.user_info.user_id) not in self.get_config("plugin.permission", []) # type: ignore - ): - await self._send_message("你没有权限使用插件管理命令") - return False, "没有权限", True - if not self.message.chat_stream: - await self._send_message("无法获取聊天流信息") - return False, "无法获取聊天流信息", True - self.stream_id = self.message.chat_stream.stream_id - if not self.stream_id: - await self._send_message("无法获取聊天流信息") - return False, "无法获取聊天流信息", True - command_list = self.matched_groups["manage_command"].strip().split(" ") - if len(command_list) == 1: - await self.show_help("all") - return True, "帮助已发送", True - if len(command_list) == 2: - match command_list[1]: - case "plugin": - await self.show_help("plugin") - case "component": - await self.show_help("component") - case "help": - await self.show_help("all") - case _: - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - if len(command_list) == 3: - if command_list[1] == "plugin": - match command_list[2]: - case "help": - await self.show_help("plugin") - case "list": - await self._list_registered_plugins() - case "list_enabled": - await self._list_loaded_plugins() - case "rescan": - await self._rescan_plugin_dirs() - case _: - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - elif command_list[1] == "component": - if command_list[2] == "list": - await self._list_all_registered_components() - elif command_list[2] == "help": - await self.show_help("component") - else: - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - else: - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - if len(command_list) == 4: - if command_list[1] == "plugin": - match command_list[2]: - case "load": - await self._load_plugin(command_list[3]) - case "unload": - await self._unload_plugin(command_list[3]) - case "reload": - await self._reload_plugin(command_list[3]) - case "add_dir": - await self._add_dir(command_list[3]) - case _: - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - elif command_list[1] == "component": - if command_list[2] != "list": - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - if command_list[3] == "enabled": + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @require_permission("plugin.management.admin", "❌ 你没有插件管理的权限") + async def execute(self, args: CommandArgs) -> Tuple[bool, str, bool]: + """执行插件管理命令""" + if args.is_empty(): + await self._show_help("all") + return True, "显示帮助信息", True + + subcommand = args.get_first().lower() + remaining_args = args.get_args()[1:] # 获取除第一个参数外的所有参数 + + if subcommand in ["plugin", "插件"]: + return await self._handle_plugin_commands(remaining_args) + elif subcommand in ["component", "组件", "comp"]: + return await self._handle_component_commands(remaining_args) + elif subcommand in ["help", "帮助"]: + await self._show_help("all") + return True, "显示帮助信息", True + else: + await self.send_text(f"❌ 未知的子命令: {subcommand}\n使用 /pm help 查看帮助") + return True, "未知子命令", True + + async def _handle_plugin_commands(self, args: List[str]) -> Tuple[bool, str, bool]: + """处理插件相关命令""" + if not args: + await self._show_help("plugin") + return True, "显示插件帮助", True + + action = args[0].lower() + + if action in ["help", "帮助"]: + await self._show_help("plugin") + elif action in ["list", "列表"]: + await self._list_registered_plugins() + elif action in ["list_enabled", "已启用"]: + await self._list_loaded_plugins() + elif action in ["rescan", "重扫"]: + await self._rescan_plugin_dirs() + elif action in ["load", "加载"] and len(args) > 1: + await self._load_plugin(args[1]) + elif action in ["unload", "卸载"] and len(args) > 1: + await self._unload_plugin(args[1]) + elif action in ["reload", "重载"] and len(args) > 1: + await self._reload_plugin(args[1]) + elif action in ["force_reload", "强制重载"] and len(args) > 1: + await self._force_reload_plugin(args[1]) + elif action in ["add_dir", "添加目录"] and len(args) > 1: + await self._add_dir(args[1]) + elif action in ["hotreload_status", "热重载状态"]: + await self._show_hotreload_status() + elif action in ["clear_cache", "清理缓存"]: + await self._clear_all_caches() + else: + await self.send_text("❌ 插件管理命令不合法\n使用 /pm plugin help 查看帮助") + return False, "命令不合法", True + + return True, "插件命令执行完成", True + + async def _handle_component_commands(self, args: List[str]) -> Tuple[bool, str, bool]: + """处理组件相关命令""" + if not args: + await self._show_help("component") + return True, "显示组件帮助", True + + action = args[0].lower() + + if action in ["help", "帮助"]: + await self._show_help("component") + elif action in ["list", "列表"]: + if len(args) == 1: + await self._list_all_registered_components() + elif len(args) == 2: + if args[1] in ["enabled", "启用"]: await self._list_enabled_components() - elif command_list[3] == "disabled": + elif args[1] in ["disabled", "禁用"]: await self._list_disabled_components() else: - await self._send_message("插件管理命令不合法") + await self.send_text("❌ 组件列表命令不合法") return False, "命令不合法", True - else: - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - if len(command_list) == 5: - if command_list[1] != "component": - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - if command_list[2] != "list": - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - if command_list[3] == "enabled": - await self._list_enabled_components(target_type=command_list[4]) - elif command_list[3] == "disabled": - await self._list_disabled_components(target_type=command_list[4]) - elif command_list[3] == "type": - await self._list_registered_components_by_type(command_list[4]) - else: - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - if len(command_list) == 6: - if command_list[1] != "component": - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - if command_list[2] == "enable": - if command_list[3] == "global": - await self._globally_enable_component(command_list[4], command_list[5]) - elif command_list[3] == "local": - await self._locally_enable_component(command_list[4], command_list[5]) + elif len(args) == 3: + if args[1] in ["enabled", "启用"]: + await self._list_enabled_components(target_type=args[2]) + elif args[1] in ["disabled", "禁用"]: + await self._list_disabled_components(target_type=args[2]) + elif args[1] in ["type", "类型"]: + await self._list_registered_components_by_type(args[2]) else: - await self._send_message("插件管理命令不合法") - return False, "命令不合法", True - elif command_list[2] == "disable": - if command_list[3] == "global": - await self._globally_disable_component(command_list[4], command_list[5]) - elif command_list[3] == "local": - await self._locally_disable_component(command_list[4], command_list[5]) - else: - await self._send_message("插件管理命令不合法") + await self.send_text("❌ 组件列表命令不合法") return False, "命令不合法", True + elif action in ["enable", "启用"] and len(args) >= 4: + scope = args[1].lower() + component_name = args[2] + component_type = args[3] + if scope in ["global", "全局"]: + await self._globally_enable_component(component_name, component_type) + elif scope in ["local", "本地"]: + await self._locally_enable_component(component_name, component_type) else: - await self._send_message("插件管理命令不合法") + await self.send_text("❌ 组件启用命令不合法,范围应为 global 或 local") return False, "命令不合法", True + elif action in ["disable", "禁用"] and len(args) >= 4: + scope = args[1].lower() + component_name = args[2] + component_type = args[3] + if scope in ["global", "全局"]: + await self._globally_disable_component(component_name, component_type) + elif scope in ["local", "本地"]: + await self._locally_disable_component(component_name, component_type) + else: + await self.send_text("❌ 组件禁用命令不合法,范围应为 global 或 local") + return False, "命令不合法", True + else: + await self.send_text("❌ 组件管理命令不合法\n使用 /pm component help 查看帮助") + return False, "命令不合法", True + + return True, "组件命令执行完成", True - return True, "命令执行完成", True - - async def show_help(self, target: str): + async def _show_help(self, target: str): + """显示帮助信息""" help_msg = "" - match target: - case "all": - help_msg = ( - "管理命令帮助\n" - "/pm help 管理命令提示\n" - "/pm plugin 插件管理命令\n" - "/pm component 组件管理命令\n" - "使用 /pm plugin help 或 /pm component help 获取具体帮助" - ) - case "plugin": - help_msg = ( - "插件管理命令帮助\n" - "/pm plugin help 插件管理命令提示\n" - "/pm plugin list 列出所有注册的插件\n" - "/pm plugin list_enabled 列出所有加载(启用)的插件\n" - "/pm plugin rescan 重新扫描所有目录\n" - "/pm plugin load 加载指定插件\n" - "/pm plugin unload 卸载指定插件\n" - "/pm plugin reload 重新加载指定插件\n" - "/pm plugin add_dir 添加插件目录\n" - ) - case "component": - help_msg = ( - "组件管理命令帮助\n" - "/pm component help 组件管理命令提示\n" - "/pm component list 列出所有注册的组件\n" - "/pm component list enabled <可选: type> 列出所有启用的组件\n" - "/pm component list disabled <可选: type> 列出所有禁用的组件\n" - " - 可选项: local,代表当前聊天中的;global,代表全局的\n" - " - 不填时为 global\n" - "/pm component list type 列出已经注册的指定类型的组件\n" - "/pm component enable global 全局启用组件\n" - "/pm component enable local 本聊天启用组件\n" - "/pm component disable global 全局禁用组件\n" - "/pm component disable local 本聊天禁用组件\n" - " - 可选项: action, command, event_handler\n" - ) - case _: - return - await self._send_message(help_msg) + if target == "all": + help_msg = """📋 插件管理命令帮助 + +🔧 主要功能: +• `/pm help` - 显示此帮助 +• `/pm plugin` - 插件管理命令 +• `/pm component` - 组件管理命令 + +📝 使用示例: +• `/pm plugin help` - 查看插件管理帮助 +• `/pm component help` - 查看组件管理帮助 + +🔄 别名:可以使用 `/pluginmanage` 或 `/插件管理` 代替 `/pm`""" + elif target == "plugin": + help_msg = """🔌 插件管理命令帮助 + +📋 基本操作: +• `/pm plugin help` - 显示插件管理帮助 +• `/pm plugin list` - 列出所有注册的插件 +• `/pm plugin list_enabled` - 列出所有加载(启用)的插件 +• `/pm plugin rescan` - 重新扫描所有插件目录 + +⚙️ 插件控制: +• `/pm plugin load <插件名>` - 加载指定插件 +• `/pm plugin unload <插件名>` - 卸载指定插件 +• `/pm plugin reload <插件名>` - 重新加载指定插件 +• `/pm plugin force_reload <插件名>` - 强制重载指定插件(深度清理) +• `/pm plugin add_dir <目录路径>` - 添加插件目录 + +� 热重载管理: +• `/pm plugin hotreload_status` - 查看热重载状态 +• `/pm plugin clear_cache` - 清理所有模块缓存 + +�📝 示例: +• `/pm plugin load echo_example` +• `/pm plugin force_reload permission_manager_plugin` +• `/pm plugin clear_cache`""" + elif target == "component": + help_msg = """🧩 组件管理命令帮助 + +📋 基本查看: +• `/pm component help` - 显示组件管理帮助 +• `/pm component list` - 列出所有注册的组件 +• `/pm component list enabled [类型]` - 列出启用的组件 +• `/pm component list disabled [类型]` - 列出禁用的组件 +• `/pm component list type <组件类型>` - 列出指定类型的组件 + +⚙️ 组件控制: +• `/pm component enable global <组件名> <类型>` - 全局启用组件 +• `/pm component enable local <组件名> <类型>` - 本聊天启用组件 +• `/pm component disable global <组件名> <类型>` - 全局禁用组件 +• `/pm component disable local <组件名> <类型>` - 本聊天禁用组件 + +📝 组件类型: +• `action` - 动作组件 +• `command` - 命令组件 +• `event_handler` - 事件处理组件 +• `plus_command` - 增强命令组件 + +💡 示例: +• `/pm component list type plus_command` +• `/pm component enable global echo_command command`""" + + await self.send_text(help_msg) async def _list_loaded_plugins(self): + """列出已加载的插件""" plugins = plugin_manage_api.list_loaded_plugins() - await self._send_message(f"已加载的插件: {', '.join(plugins)}") + await self.send_text(f"📦 已加载的插件: {', '.join(plugins) if plugins else '无'}") async def _list_registered_plugins(self): + """列出已注册的插件""" plugins = plugin_manage_api.list_registered_plugins() - await self._send_message(f"已注册的插件: {', '.join(plugins)}") + await self.send_text(f"📋 已注册的插件: {', '.join(plugins) if plugins else '无'}") async def _rescan_plugin_dirs(self): + """重新扫描插件目录""" plugin_manage_api.rescan_plugin_directory() - await self._send_message("插件目录重新扫描执行中") + await self.send_text("🔄 插件目录重新扫描已启动") async def _load_plugin(self, plugin_name: str): + """加载指定插件""" success, count = plugin_manage_api.load_plugin(plugin_name) if success: - await self._send_message(f"插件加载成功: {plugin_name}") + await self.send_text(f"✅ 插件加载成功: `{plugin_name}`") else: if count == 0: - await self._send_message(f"插件{plugin_name}为禁用状态") - await self._send_message(f"插件加载失败: {plugin_name}") + await self.send_text(f"⚠️ 插件 `{plugin_name}` 为禁用状态") + else: + await self.send_text(f"❌ 插件加载失败: `{plugin_name}`") async def _unload_plugin(self, plugin_name: str): + """卸载指定插件""" success = await plugin_manage_api.remove_plugin(plugin_name) if success: - await self._send_message(f"插件卸载成功: {plugin_name}") + await self.send_text(f"✅ 插件卸载成功: `{plugin_name}`") else: - await self._send_message(f"插件卸载失败: {plugin_name}") + await self.send_text(f"❌ 插件卸载失败: `{plugin_name}`") async def _reload_plugin(self, plugin_name: str): + """重新加载指定插件""" success = await plugin_manage_api.reload_plugin(plugin_name) if success: - await self._send_message(f"插件重新加载成功: {plugin_name}") + await self.send_text(f"✅ 插件重新加载成功: `{plugin_name}`") else: - await self._send_message(f"插件重新加载失败: {plugin_name}") + await self.send_text(f"❌ 插件重新加载失败: `{plugin_name}`") + + async def _force_reload_plugin(self, plugin_name: str): + """强制重载指定插件(深度清理)""" + await self.send_text(f"🔄 开始强制重载插件: `{plugin_name}`...") + + try: + success = hot_reload_manager.force_reload_plugin(plugin_name) + if success: + await self.send_text(f"✅ 插件强制重载成功: `{plugin_name}`") + else: + await self.send_text(f"❌ 插件强制重载失败: `{plugin_name}`") + except Exception as e: + await self.send_text(f"❌ 强制重载过程中发生错误: {str(e)}") + + async def _show_hotreload_status(self): + """显示热重载状态""" + try: + status = hot_reload_manager.get_status() + + status_text = f"""🔄 **热重载系统状态** + +🟢 **运行状态:** {'运行中' if status['is_running'] else '已停止'} +📂 **监听目录:** {len(status['watch_directories'])} 个 +👁️ **活跃观察者:** {status['active_observers']} 个 +📦 **已加载插件:** {status['loaded_plugins']} 个 +❌ **失败插件:** {status['failed_plugins']} 个 +⏱️ **防抖延迟:** {status.get('debounce_delay', 0)} 秒 + +📋 **监听的目录:**""" + + for i, watch_dir in enumerate(status['watch_directories'], 1): + dir_type = "(内置插件)" if "src" in watch_dir else "(外部插件)" + status_text += f"\n{i}. `{watch_dir}` {dir_type}" + + if status.get('pending_reloads'): + status_text += f"\n\n⏳ **待重载插件:** {', '.join([f'`{p}`' for p in status['pending_reloads']])}" + + await self.send_text(status_text) + + except Exception as e: + await self.send_text(f"❌ 获取热重载状态时发生错误: {str(e)}") + + async def _clear_all_caches(self): + """清理所有模块缓存""" + await self.send_text("🧹 开始清理所有Python模块缓存...") + + try: + hot_reload_manager.clear_all_caches() + await self.send_text("✅ 模块缓存清理完成!建议重载相关插件以确保生效。") + except Exception as e: + await self.send_text(f"❌ 清理缓存时发生错误: {str(e)}") async def _add_dir(self, dir_path: str): - await self._send_message(f"正在添加插件目录: {dir_path}") + """添加插件目录""" + await self.send_text(f"📁 正在添加插件目录: `{dir_path}`") success = plugin_manage_api.add_plugin_directory(dir_path) await asyncio.sleep(0.5) # 防止乱序发送 if success: - await self._send_message(f"插件目录添加成功: {dir_path}") + await self.send_text(f"✅ 插件目录添加成功: `{dir_path}`") else: - await self._send_message(f"插件目录添加失败: {dir_path}") + await self.send_text(f"❌ 插件目录添加失败: `{dir_path}`") def _fetch_all_registered_components(self) -> List[ComponentInfo]: all_plugin_info = component_manage_api.get_all_plugin_info() @@ -245,47 +332,55 @@ class ManagementCommand(BaseCommand): return components_info def _fetch_locally_disabled_components(self) -> List[str]: + """获取本地禁用的组件列表""" + stream_id = self.message.chat_stream.stream_id locally_disabled_components_actions = component_manage_api.get_locally_disabled_components( - self.message.chat_stream.stream_id, ComponentType.ACTION + stream_id, ComponentType.ACTION ) locally_disabled_components_commands = component_manage_api.get_locally_disabled_components( - self.message.chat_stream.stream_id, ComponentType.COMMAND + stream_id, ComponentType.COMMAND + ) + locally_disabled_components_plus_commands = component_manage_api.get_locally_disabled_components( + stream_id, ComponentType.PLUS_COMMAND ) locally_disabled_components_event_handlers = component_manage_api.get_locally_disabled_components( - self.message.chat_stream.stream_id, ComponentType.EVENT_HANDLER + stream_id, ComponentType.EVENT_HANDLER ) return ( locally_disabled_components_actions + locally_disabled_components_commands + + locally_disabled_components_plus_commands + locally_disabled_components_event_handlers ) async def _list_all_registered_components(self): + """列出所有已注册的组件""" components_info = self._fetch_all_registered_components() if not components_info: - await self._send_message("没有注册的组件") + await self.send_text("📋 没有注册的组件") return all_components_str = ", ".join( - f"{component.name} ({component.component_type})" for component in components_info + f"`{component.name}` ({component.component_type})" for component in components_info ) - await self._send_message(f"已注册的组件: {all_components_str}") + await self.send_text(f"📋 已注册的组件:\n{all_components_str}") async def _list_enabled_components(self, target_type: str = "global"): + """列出启用的组件""" components_info = self._fetch_all_registered_components() if not components_info: - await self._send_message("没有注册的组件") + await self.send_text("📋 没有注册的组件") return if target_type == "global": enabled_components = [component for component in components_info if component.enabled] if not enabled_components: - await self._send_message("没有满足条件的已启用全局组件") + await self.send_text("📋 没有满足条件的已启用全局组件") return enabled_components_str = ", ".join( - f"{component.name} ({component.component_type})" for component in enabled_components + f"`{component.name}` ({component.component_type})" for component in enabled_components ) - await self._send_message(f"满足条件的已启用全局组件: {enabled_components_str}") + await self.send_text(f"✅ 满足条件的已启用全局组件:\n{enabled_components_str}") elif target_type == "local": locally_disabled_components = self._fetch_locally_disabled_components() enabled_components = [ @@ -294,28 +389,29 @@ class ManagementCommand(BaseCommand): if (component.name not in locally_disabled_components and component.enabled) ] if not enabled_components: - await self._send_message("本聊天没有满足条件的已启用组件") + await self.send_text("📋 本聊天没有满足条件的已启用组件") return enabled_components_str = ", ".join( - f"{component.name} ({component.component_type})" for component in enabled_components + f"`{component.name}` ({component.component_type})" for component in enabled_components ) - await self._send_message(f"本聊天满足条件的已启用组件: {enabled_components_str}") + await self.send_text(f"✅ 本聊天满足条件的已启用组件:\n{enabled_components_str}") async def _list_disabled_components(self, target_type: str = "global"): + """列出禁用的组件""" components_info = self._fetch_all_registered_components() if not components_info: - await self._send_message("没有注册的组件") + await self.send_text("📋 没有注册的组件") return if target_type == "global": disabled_components = [component for component in components_info if not component.enabled] if not disabled_components: - await self._send_message("没有满足条件的已禁用全局组件") + await self.send_text("📋 没有满足条件的已禁用全局组件") return disabled_components_str = ", ".join( - f"{component.name} ({component.component_type})" for component in disabled_components + f"`{component.name}` ({component.component_type})" for component in disabled_components ) - await self._send_message(f"满足条件的已禁用全局组件: {disabled_components_str}") + await self.send_text(f"❌ 满足条件的已禁用全局组件:\n{disabled_components_str}") elif target_type == "local": locally_disabled_components = self._fetch_locally_disabled_components() disabled_components = [ @@ -324,110 +420,115 @@ class ManagementCommand(BaseCommand): if (component.name in locally_disabled_components or not component.enabled) ] if not disabled_components: - await self._send_message("本聊天没有满足条件的已禁用组件") + await self.send_text("📋 本聊天没有满足条件的已禁用组件") return disabled_components_str = ", ".join( - f"{component.name} ({component.component_type})" for component in disabled_components + f"`{component.name}` ({component.component_type})" for component in disabled_components ) - await self._send_message(f"本聊天满足条件的已禁用组件: {disabled_components_str}") + await self.send_text(f"❌ 本聊天满足条件的已禁用组件:\n{disabled_components_str}") async def _list_registered_components_by_type(self, target_type: str): - match target_type: - case "action": - component_type = ComponentType.ACTION - case "command": - component_type = ComponentType.COMMAND - case "event_handler": - component_type = ComponentType.EVENT_HANDLER - case _: - await self._send_message(f"未知组件类型: {target_type}") - return + """按类型列出已注册的组件""" + type_mapping = { + "action": ComponentType.ACTION, + "command": ComponentType.COMMAND, + "event_handler": ComponentType.EVENT_HANDLER, + "plus_command": ComponentType.PLUS_COMMAND, + } + + component_type = type_mapping.get(target_type.lower()) + if not component_type: + await self.send_text(f"❌ 未知组件类型: `{target_type}`\n支持的类型: action, command, event_handler, plus_command") + return components_info = component_manage_api.get_components_info_by_type(component_type) if not components_info: - await self._send_message(f"没有注册的 {target_type} 组件") + await self.send_text(f"📋 没有注册的 `{target_type}` 组件") return components_str = ", ".join( - f"{name} ({component.component_type})" for name, component in components_info.items() + f"`{name}` ({component.component_type})" for name, component in components_info.items() ) - await self._send_message(f"注册的 {target_type} 组件: {components_str}") + await self.send_text(f"📋 注册的 `{target_type}` 组件:\n{components_str}") async def _globally_enable_component(self, component_name: str, component_type: str): - match component_type: - case "action": - target_component_type = ComponentType.ACTION - case "command": - target_component_type = ComponentType.COMMAND - case "event_handler": - target_component_type = ComponentType.EVENT_HANDLER - case _: - await self._send_message(f"未知组件类型: {component_type}") - return + """全局启用组件""" + type_mapping = { + "action": ComponentType.ACTION, + "command": ComponentType.COMMAND, + "event_handler": ComponentType.EVENT_HANDLER, + "plus_command": ComponentType.PLUS_COMMAND, + } + + target_component_type = type_mapping.get(component_type.lower()) + if not target_component_type: + await self.send_text(f"❌ 未知组件类型: `{component_type}`") + return + if component_manage_api.globally_enable_component(component_name, target_component_type): - await self._send_message(f"全局启用组件成功: {component_name}") + await self.send_text(f"✅ 全局启用组件成功: `{component_name}`") else: - await self._send_message(f"全局启用组件失败: {component_name}") + await self.send_text(f"❌ 全局启用组件失败: `{component_name}`") async def _globally_disable_component(self, component_name: str, component_type: str): - match component_type: - case "action": - target_component_type = ComponentType.ACTION - case "command": - target_component_type = ComponentType.COMMAND - case "event_handler": - target_component_type = ComponentType.EVENT_HANDLER - case _: - await self._send_message(f"未知组件类型: {component_type}") - return + """全局禁用组件""" + type_mapping = { + "action": ComponentType.ACTION, + "command": ComponentType.COMMAND, + "event_handler": ComponentType.EVENT_HANDLER, + "plus_command": ComponentType.PLUS_COMMAND, + } + + target_component_type = type_mapping.get(component_type.lower()) + if not target_component_type: + await self.send_text(f"❌ 未知组件类型: `{component_type}`") + return + success = await component_manage_api.globally_disable_component(component_name, target_component_type) if success: - await self._send_message(f"全局禁用组件成功: {component_name}") + await self.send_text(f"✅ 全局禁用组件成功: `{component_name}`") else: - await self._send_message(f"全局禁用组件失败: {component_name}") + await self.send_text(f"❌ 全局禁用组件失败: `{component_name}`") async def _locally_enable_component(self, component_name: str, component_type: str): - match component_type: - case "action": - target_component_type = ComponentType.ACTION - case "command": - target_component_type = ComponentType.COMMAND - case "event_handler": - target_component_type = ComponentType.EVENT_HANDLER - case _: - await self._send_message(f"未知组件类型: {component_type}") - return - if component_manage_api.locally_enable_component( - component_name, - target_component_type, - self.message.chat_stream.stream_id, - ): - await self._send_message(f"本地启用组件成功: {component_name}") + """本地启用组件""" + type_mapping = { + "action": ComponentType.ACTION, + "command": ComponentType.COMMAND, + "event_handler": ComponentType.EVENT_HANDLER, + "plus_command": ComponentType.PLUS_COMMAND, + } + + target_component_type = type_mapping.get(component_type.lower()) + if not target_component_type: + await self.send_text(f"❌ 未知组件类型: `{component_type}`") + return + + stream_id = self.message.chat_stream.stream_id + if component_manage_api.locally_enable_component(component_name, target_component_type, stream_id): + await self.send_text(f"✅ 本地启用组件成功: `{component_name}`") else: - await self._send_message(f"本地启用组件失败: {component_name}") + await self.send_text(f"❌ 本地启用组件失败: `{component_name}`") async def _locally_disable_component(self, component_name: str, component_type: str): - match component_type: - case "action": - target_component_type = ComponentType.ACTION - case "command": - target_component_type = ComponentType.COMMAND - case "event_handler": - target_component_type = ComponentType.EVENT_HANDLER - case _: - await self._send_message(f"未知组件类型: {component_type}") - return - if component_manage_api.locally_disable_component( - component_name, - target_component_type, - self.message.chat_stream.stream_id, - ): - await self._send_message(f"本地禁用组件成功: {component_name}") + """本地禁用组件""" + type_mapping = { + "action": ComponentType.ACTION, + "command": ComponentType.COMMAND, + "event_handler": ComponentType.EVENT_HANDLER, + "plus_command": ComponentType.PLUS_COMMAND, + } + + target_component_type = type_mapping.get(component_type.lower()) + if not target_component_type: + await self.send_text(f"❌ 未知组件类型: `{component_type}`") + return + + stream_id = self.message.chat_stream.stream_id + if component_manage_api.locally_disable_component(component_name, target_component_type, stream_id): + await self.send_text(f"✅ 本地禁用组件成功: `{component_name}`") else: - await self._send_message(f"本地禁用组件失败: {component_name}") - - async def _send_message(self, message: str): - await send_api.text_to_stream(message, self.stream_id, typing=False, storage_message=False) + await self.send_text(f"❌ 本地禁用组件失败: `{component_name}`") @register_plugin @@ -441,14 +542,22 @@ class PluginManagementPlugin(BasePlugin): "plugin": { "enabled": ConfigField(bool, default=False, description="是否启用插件"), "config_version": ConfigField(type=str, default="1.1.0", description="配置文件版本"), - "permission": ConfigField( - list, default=[], description="有权限使用插件管理命令的用户列表,请填写字符串形式的用户ID" - ), }, } - def get_plugin_components(self) -> List[Tuple[CommandInfo, Type[BaseCommand]]]: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # 注册权限节点 + permission_api.register_permission_node( + "plugin.management.admin", + "插件管理:可以管理插件和组件的加载、卸载、启用、禁用等操作", + "plugin_management", + False + ) + + def get_plugin_components(self) -> List[Tuple[PlusCommandInfo, Type[PlusCommand]]]: + """返回插件的PlusCommand组件""" components = [] if self.get_config("plugin.enabled", True): - components.append((ManagementCommand.get_command_info(), ManagementCommand)) + components.append((ManagementCommand.get_plus_command_info(), ManagementCommand)) return components diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d3b997526..999310ffd 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.5.7" +version = "6.5.8" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -89,6 +89,7 @@ learning_strength = 1.0 [[expression.rules]] chat_stream_id = "qq:1919810:group" +group = "group_A" use_expression = true learn_expression = true learning_strength = 1.5 @@ -100,29 +101,18 @@ use_expression = true learn_expression = false learning_strength = 0.5 -[[expression.rules]] -chat_stream_id = "qq:1919810:private" -group = "group_A" -use_expression = true -learn_expression = true -learning_strength = 1.0 - - - [chat] #MoFox-Bot的聊天通用设置 +# 群聊聊天模式设置 +group_chat_mode = "auto" # 群聊聊天模式:auto-自动切换,normal-强制普通模式,focus-强制专注模式 focus_value = 1 -# MoFox-Bot的专注思考能力,越高越容易专注,可能消耗更多token +# MoFox-Bot的专注思考能力,越高越容易专注,可能消耗更多token,仅限自动切换模式下使用哦 # 专注时能更好把握发言时机,能够进行持久的连续对话 -talk_frequency = 1 # MoFox-Bot活跃度,越高,MoFox-Bot回复越频繁 +talk_frequency = 1 # MoFox-Bot活跃度,越高,MoFox-Bot回复越频繁,仅限normal/或者自动切换的normal模式下使用哦 # 强制私聊专注模式 force_focus_private = false # 是否强制私聊进入专注模式,开启后私聊将始终保持专注状态 -# 群聊聊天模式设置 -group_chat_mode = "auto" # 群聊聊天模式:auto-自动切换,normal-强制普通模式,focus-强制专注模式 - - max_context_size = 25 # 上下文长度 thinking_timeout = 40 # MoFox-Bot一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢) replyer_random_probability = 0.5 # 首要replyer模型被选择的概率 @@ -273,7 +263,7 @@ enable_vector_instant_memory = true # 是否启用基于向量的瞬时记忆 memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ] [voice] -enable_asr = false # 是否启用语音识别,启用后MoFox-Bot可以识别语音消息,启用该功能需要配置语音识别模型[model.voice]s +enable_asr = false # 是否启用语音识别,启用后MoFox-Bot可以识别语音消息,启用该功能需要配置语音识别模型[model.voice] [lpmm_knowledge] # lpmm知识库配置 enable = false # 是否启用lpmm知识库 @@ -347,8 +337,6 @@ auto_install_timeout = 300 # 是否使用PyPI镜像源(推荐,可加速下载) use_mirror = true mirror_url = "https://pypi.tuna.tsinghua.edu.cn/simple" # PyPI镜像源URL,如: "https://pypi.tuna.tsinghua.edu.cn/simple" -# 安装前是否提示用户(暂未实现) -prompt_before_install = false # 依赖安装日志级别 install_log_level = "INFO" @@ -376,23 +364,6 @@ guidelines = """ 晚上我希望你能多和朋友们交流,维系好彼此的关系。 另外,请保证充足的休眠时间来处理和整合一天的数据。 """ -enable_is_sleep = false - -# --- 弹性睡眠与睡前消息 --- -# 是否启用弹性睡眠。启用后,AI不会到点立刻入睡,而是会根据睡眠压力增加5-10分钟的缓冲,并可能因为压力不足而推迟睡眠。 -enable_flexible_sleep = true -# 触发弹性睡眠的睡眠压力阈值。当AI的睡眠压力低于此值时,可能会推迟入睡。 -flexible_sleep_pressure_threshold = 40.0 -# 每日最大可推迟入睡的总分钟数。 -max_sleep_delay_minutes = 60 - -# 是否在进入“准备入睡”状态时发送一条消息通知。 -enable_pre_sleep_notification = true -# 接收睡前消息的群组列表。格式为: ["platform:group_id1", "platform:group_id2"],例如 ["qq:12345678"] -pre_sleep_notification_groups = [] -# 用于生成睡前消息的提示。AI会根据这个提示生成一句晚安问候。 -pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问候。" - [video_analysis] # 视频分析配置 enable = true # 是否启用视频分析功能 analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择) @@ -456,8 +427,8 @@ guidelines = """ 请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 """ -[wakeup_system] -enable = false #"是否启用唤醒度系统" +[sleep_system] +enable = false #"是否启用睡眠系统" wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒" private_message_increment = 3.0 #"私聊消息增加的唤醒度" group_mention_increment = 2.0 #"群聊艾特增加的唤醒度" @@ -482,6 +453,21 @@ sleep_pressure_increment = 1.5 sleep_pressure_decay_rate = 1.5 insomnia_duration_minutes = 30 # 单次失眠状态的持续时间(分钟) +# --- 弹性睡眠与睡前消息 --- +# 是否启用弹性睡眠。启用后,AI不会到点立刻入睡,而是会根据睡眠压力增加5-10分钟的缓冲,并可能因为压力不足而推迟睡眠。 +enable_flexible_sleep = false +# 触发弹性睡眠的睡眠压力阈值。当AI的睡眠压力低于此值时,可能会推迟入睡。 +flexible_sleep_pressure_threshold = 40.0 +# 每日最大可推迟入睡的总分钟数。 +max_sleep_delay_minutes = 60 + +# 是否在进入“准备入睡”状态时发送一条消息通知。 +enable_pre_sleep_notification = false +# 接收睡前消息的群组列表。格式为: ["platform:group_id1", "platform:group_id2"],例如 ["qq:12345678"] +pre_sleep_notification_groups = [] +# 用于生成睡前消息的提示。AI会根据这个提示生成一句晚安问候。 +pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问候。" + [cross_context] # 跨群聊上下文共享配置 # 这是总开关,用于一键启用或禁用此功能 enable = false