This commit is contained in:
Windpicker-owo
2025-08-29 14:16:30 +08:00
27 changed files with 3178 additions and 623 deletions

View File

@@ -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"
}
]
}
}

View File

@@ -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)
]

View File

@@ -65,3 +65,6 @@ asyncio
tavily-python
google-generativeai
lunar_python
python-multipart
aiofiles

View File

@@ -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()

View File

@@ -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("执行动作,睡眠压力累积")

View File

@@ -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} 分钟无新消息,尝试重新入睡。")

View File

@@ -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()

1
src/chat/utils/rust-video/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

610
src/chat/utils/rust-video/Cargo.lock generated Normal file
View File

@@ -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"

View File

@@ -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

View File

@@ -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脚本调用

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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<PathBuf>,
/// 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<u8>, // 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<u8>) -> 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<VideoFrame>, 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::<usize>(), height_str.parse::<usize>()) {
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<Vec<usize>> {
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<f64> = 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<usize> = 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<usize> {
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", &timestamp.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<PerformanceResult> {
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 <path>"))?;
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 <path>"))?;
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(())
}

View File

@@ -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()

View File

@@ -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互通组配置")

View File

@@ -531,15 +531,6 @@ class ScheduleConfig(ValidatedConfigBase):
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):
"""插件Python依赖管理配置类"""
@@ -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):
"""月度计划系统配置类"""

View File

@@ -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:

View File

@@ -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,17 +270,110 @@ 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):
"""卸载指定插件"""
@@ -256,16 +382,19 @@ class PluginHotReloadManager:
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()

View File

@@ -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):
# 重新加载插件实例
if plugin_name in self.plugin_classes:
success, _ = self.load_registered_plugin_classes(plugin_name)
if success:
logger.info(f"🔄 插件重载成功: {plugin_name}")
logger.info(f" 插件重载成功: {plugin_name}")
return True
else:
logger.error(f"❌ 插件重载失败: {plugin_name} - 实例化失败")
return False
else:
logger.error(f"❌ 插件重载失败: {plugin_name} - 模块加载失败")
return False
else:
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()

View File

@@ -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]:
"""

View File

@@ -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]:
"""

View File

@@ -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]:
"""
执行命令的核心逻辑。

View File

@@ -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 [

View File

@@ -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<manage_command>^/pm(\s[a-zA-Z0-9_]+)*\s*$)"
class ManagementCommand(PlusCommand):
"""插件管理命令 - 使用PlusCommand系统"""
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":
command_name = "pm"
command_description = "插件管理命令,支持插件和组件的管理操作"
command_aliases = ["pluginmanage", "插件管理"]
priority = 10
chat_type_allow = ChatType.ALL
intercept_message = True
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()
case "list_enabled":
elif action in ["list_enabled", "已启用"]:
await self._list_loaded_plugins()
case "rescan":
elif action in ["rescan", "重扫"]:
await self._rescan_plugin_dirs()
case _:
await self._send_message("插件管理命令不合法")
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
elif command_list[1] == "component":
if command_list[2] == "list":
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 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":
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
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_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_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_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])
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("插件管理命令不合法")
return False, "命令不合法", True
else:
await self._send_message("插件管理命令不合法")
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 <plugin_name> 加载指定插件\n"
"/pm plugin unload <plugin_name> 卸载指定插件\n"
"/pm plugin reload <plugin_name> 重新加载指定插件\n"
"/pm plugin add_dir <directory_path> 添加插件目录\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"
" - <type> 可选项: local代表当前聊天中的global代表全局的\n"
" - <type> 不填时为 global\n"
"/pm component list type <component_type> 列出已经注册的指定类型的组件\n"
"/pm component enable global <component_name> <component_type> 全局启用组件\n"
"/pm component enable local <component_name> <component_type> 本聊天启用组件\n"
"/pm component disable global <component_name> <component_type> 全局禁用组件\n"
"/pm component disable local <component_name> <component_type> 本聊天禁用组件\n"
" - <component_type> 可选项: 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 <目录路径>` - 添加插件目录
<EFBFBD> 热重载管理:
• `/pm plugin hotreload_status` - 查看热重载状态
• `/pm plugin clear_cache` - 清理所有模块缓存
<EFBFBD>📝 示例:
• `/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}")
"""按类型列出已注册的组件"""
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}")
"""全局启用组件"""
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}")
"""全局禁用组件"""
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}")
"""本地启用组件"""
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.locally_enable_component(
component_name,
target_component_type,
self.message.chat_stream.stream_id,
):
await self._send_message(f"本地启用组件成功: {component_name}")
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}")
else:
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,
}
async def _send_message(self, message: str):
await send_api.text_to_stream(message, self.stream_id, typing=False, storage_message=False)
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_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

View File

@@ -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