Merge branch 'master' of https://github.com/MoFox-Studio/MoFox_Bot
This commit is contained in:
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
]
|
|
||||||
@@ -64,4 +64,7 @@ chromadb
|
|||||||
asyncio
|
asyncio
|
||||||
tavily-python
|
tavily-python
|
||||||
google-generativeai
|
google-generativeai
|
||||||
lunar_python
|
lunar_python
|
||||||
|
|
||||||
|
python-multipart
|
||||||
|
aiofiles
|
||||||
@@ -133,7 +133,7 @@ class CycleProcessor:
|
|||||||
await stop_typing()
|
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"]:
|
if action_type not in ["no_reply", "no_action"]:
|
||||||
self.context.energy_manager.increase_sleep_pressure()
|
self.context.energy_manager.increase_sleep_pressure()
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class EnergyManager:
|
|||||||
|
|
||||||
if is_sleeping:
|
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 -= decay_per_10s
|
||||||
self.context.sleep_pressure = max(self.context.sleep_pressure, 0)
|
self.context.sleep_pressure = max(self.context.sleep_pressure, 0)
|
||||||
self._log_sleep_pressure_change("睡眠压力释放")
|
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 += increment
|
||||||
self.context.sleep_pressure = min(self.context.sleep_pressure, 100.0) # 设置一个100的上限
|
self.context.sleep_pressure = min(self.context.sleep_pressure, 100.0) # 设置一个100的上限
|
||||||
self._log_sleep_pressure_change("执行动作,睡眠压力累积")
|
self._log_sleep_pressure_change("执行动作,睡眠压力累积")
|
||||||
|
|||||||
@@ -209,12 +209,12 @@ class HeartFChatting:
|
|||||||
if self.wakeup_manager and self.wakeup_manager.check_for_insomnia():
|
if self.wakeup_manager and self.wakeup_manager.check_for_insomnia():
|
||||||
# 触发失眠
|
# 触发失眠
|
||||||
self.context.is_in_insomnia = True
|
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
|
self.context.insomnia_end_time = time.time() + duration
|
||||||
|
|
||||||
# 判断失眠原因并触发思考
|
# 判断失眠原因并触发思考
|
||||||
reason = "random"
|
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"
|
reason = "low_pressure"
|
||||||
await self.proactive_thinker.trigger_insomnia_thinking(reason)
|
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:
|
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 来判断空闲时间
|
# 使用 last_message_time 来判断空闲时间
|
||||||
if time.time() - self.context.last_message_time > re_sleep_delay:
|
if time.time() - self.context.last_message_time > re_sleep_delay:
|
||||||
logger.info(f"{self.context.log_prefix} 已被唤醒且超过 {re_sleep_delay / 60} 分钟无新消息,尝试重新入睡。")
|
logger.info(f"{self.context.log_prefix} 已被唤醒且超过 {re_sleep_delay / 60} 分钟无新消息,尝试重新入睡。")
|
||||||
|
|||||||
@@ -31,22 +31,22 @@ class WakeUpManager:
|
|||||||
self.log_interval = 30
|
self.log_interval = 30
|
||||||
|
|
||||||
# 从配置文件获取参数
|
# 从配置文件获取参数
|
||||||
wakeup_config = global_config.wakeup_system
|
sleep_config = global_config.sleep_system
|
||||||
self.wakeup_threshold = wakeup_config.wakeup_threshold
|
self.wakeup_threshold = sleep_config.wakeup_threshold
|
||||||
self.private_message_increment = wakeup_config.private_message_increment
|
self.private_message_increment = sleep_config.private_message_increment
|
||||||
self.group_mention_increment = wakeup_config.group_mention_increment
|
self.group_mention_increment = sleep_config.group_mention_increment
|
||||||
self.decay_rate = wakeup_config.decay_rate
|
self.decay_rate = sleep_config.decay_rate
|
||||||
self.decay_interval = wakeup_config.decay_interval
|
self.decay_interval = sleep_config.decay_interval
|
||||||
self.angry_duration = wakeup_config.angry_duration
|
self.angry_duration = sleep_config.angry_duration
|
||||||
self.enabled = wakeup_config.enable
|
self.enabled = sleep_config.enable
|
||||||
self.angry_prompt = wakeup_config.angry_prompt
|
self.angry_prompt = sleep_config.angry_prompt
|
||||||
|
|
||||||
# 失眠系统参数
|
# 失眠系统参数
|
||||||
self.insomnia_enabled = wakeup_config.enable_insomnia_system
|
self.insomnia_enabled = sleep_config.enable_insomnia_system
|
||||||
self.sleep_pressure_threshold = wakeup_config.sleep_pressure_threshold
|
self.sleep_pressure_threshold = sleep_config.sleep_pressure_threshold
|
||||||
self.deep_sleep_threshold = wakeup_config.deep_sleep_threshold
|
self.deep_sleep_threshold = sleep_config.deep_sleep_threshold
|
||||||
self.insomnia_chance_low_pressure = wakeup_config.insomnia_chance_low_pressure
|
self.insomnia_chance_low_pressure = sleep_config.insomnia_chance_low_pressure
|
||||||
self.insomnia_chance_normal_pressure = wakeup_config.insomnia_chance_normal_pressure
|
self.insomnia_chance_normal_pressure = sleep_config.insomnia_chance_normal_pressure
|
||||||
|
|
||||||
self._load_wakeup_state()
|
self._load_wakeup_state()
|
||||||
|
|
||||||
|
|||||||
1
src/chat/utils/rust-video/.gitignore
vendored
Normal file
1
src/chat/utils/rust-video/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
610
src/chat/utils/rust-video/Cargo.lock
generated
Normal file
610
src/chat/utils/rust-video/Cargo.lock
generated
Normal 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"
|
||||||
24
src/chat/utils/rust-video/Cargo.toml
Normal file
24
src/chat/utils/rust-video/Cargo.toml
Normal 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
|
||||||
221
src/chat/utils/rust-video/README.md
Normal file
221
src/chat/utils/rust-video/README.md
Normal 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脚本调用
|
||||||
472
src/chat/utils/rust-video/api_server.py
Normal file
472
src/chat/utils/rust-video/api_server.py
Normal 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()
|
||||||
115
src/chat/utils/rust-video/config.py
Normal file
115
src/chat/utils/rust-video/config.py
Normal 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()
|
||||||
70
src/chat/utils/rust-video/config.toml
Normal file
70
src/chat/utils/rust-video/config.toml
Normal 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
|
||||||
710
src/chat/utils/rust-video/src/main.rs
Normal file
710
src/chat/utils/rust-video/src/main.rs
Normal 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", ×tamp.to_string(),
|
||||||
|
"-vframes", "1",
|
||||||
|
"-q:v", "2", // High quality
|
||||||
|
"-y",
|
||||||
|
output_path.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.context("Failed to extract keyframe with FFmpeg")?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
saved += 1;
|
||||||
|
if verbose && (saved % 10 == 0 || saved == save_count) {
|
||||||
|
print!("\r💾 Saved: {}/{} keyframes", saved, save_count);
|
||||||
|
}
|
||||||
|
} else if verbose {
|
||||||
|
eprintln!("⚠️ Failed to save keyframe {}", frame_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
println!("\r✅ Keyframe saving complete: {}/{}", saved, save_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run performance test
|
||||||
|
fn run_performance_test(
|
||||||
|
video_path: &PathBuf,
|
||||||
|
threshold: f64,
|
||||||
|
test_name: &str,
|
||||||
|
ffmpeg_path: &PathBuf,
|
||||||
|
max_frames: usize,
|
||||||
|
use_simd: bool,
|
||||||
|
block_size: usize,
|
||||||
|
verbose: bool,
|
||||||
|
) -> Result<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(())
|
||||||
|
}
|
||||||
219
src/chat/utils/rust-video/start_server.py
Normal file
219
src/chat/utils/rust-video/start_server.py
Normal 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()
|
||||||
@@ -41,7 +41,7 @@ from src.config.official_configs import (
|
|||||||
WebSearchConfig,
|
WebSearchConfig,
|
||||||
AntiPromptInjectionConfig,
|
AntiPromptInjectionConfig,
|
||||||
PluginsConfig,
|
PluginsConfig,
|
||||||
WakeUpSystemConfig,
|
SleepSystemConfig,
|
||||||
MonthlyPlanSystemConfig,
|
MonthlyPlanSystemConfig,
|
||||||
CrossContextConfig,
|
CrossContextConfig,
|
||||||
PermissionConfig,
|
PermissionConfig,
|
||||||
@@ -390,7 +390,7 @@ class Config(ValidatedConfigBase):
|
|||||||
dependency_management: DependencyManagementConfig = Field(default_factory=lambda: DependencyManagementConfig(), description="依赖管理配置")
|
dependency_management: DependencyManagementConfig = Field(default_factory=lambda: DependencyManagementConfig(), description="依赖管理配置")
|
||||||
web_search: WebSearchConfig = Field(default_factory=lambda: WebSearchConfig(), description="网络搜索配置")
|
web_search: WebSearchConfig = Field(default_factory=lambda: WebSearchConfig(), description="网络搜索配置")
|
||||||
plugins: PluginsConfig = Field(default_factory=lambda: PluginsConfig(), 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="月层计划系统配置")
|
monthly_plan_system: MonthlyPlanSystemConfig = Field(default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置")
|
||||||
cross_context: CrossContextConfig = Field(default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置")
|
cross_context: CrossContextConfig = Field(default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置")
|
||||||
maizone_intercom: MaizoneIntercomConfig = Field(default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置")
|
maizone_intercom: MaizoneIntercomConfig = Field(default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置")
|
||||||
|
|||||||
@@ -530,15 +530,6 @@ class ScheduleConfig(ValidatedConfigBase):
|
|||||||
enable: bool = Field(default=True, description="启用")
|
enable: bool = Field(default=True, description="启用")
|
||||||
guidelines: Optional[str] = Field(default=None, description="指导方针")
|
guidelines: Optional[str] = Field(default=None, description="指导方针")
|
||||||
enable_is_sleep: bool = Field(default=True, description="让AI会根据日程表睡觉和苏醒")
|
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):
|
class DependencyManagementConfig(ValidatedConfigBase):
|
||||||
@@ -610,10 +601,10 @@ class PluginsConfig(ValidatedConfigBase):
|
|||||||
centralized_config: bool = Field(default=True, description="是否启用插件配置集中化管理")
|
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="唤醒阈值,达到此值时会被唤醒")
|
wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒")
|
||||||
private_message_increment: float = Field(default=3.0, ge=0.1, 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="群聊艾特增加的唤醒度")
|
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_increment: float = Field(default=1.5, ge=0.0, description="每次AI执行动作后,增加的睡眠压力值")
|
||||||
sleep_pressure_decay_rate: float = Field(default=1.5, ge=0.0, description="睡眠时,每分钟衰减的睡眠压力值")
|
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):
|
class MonthlyPlanSystemConfig(ValidatedConfigBase):
|
||||||
"""月度计划系统配置类"""
|
"""月度计划系统配置类"""
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ class LLMRequest:
|
|||||||
for model_info, api_provider, client in model_scheduler:
|
for model_info, api_provider, client in model_scheduler:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
model_name = model_info.name
|
model_name = model_info.name
|
||||||
logger.info(f"正在尝试使用模型: {model_name}")
|
logger.debug(f"正在尝试使用模型: {model_name}") # 你不许刷屏
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 检查是否启用反截断
|
# 检查是否启用反截断
|
||||||
@@ -370,7 +370,7 @@ class LLMRequest:
|
|||||||
raise RuntimeError("生成空回复")
|
raise RuntimeError("生成空回复")
|
||||||
content = "生成的响应为空"
|
content = "生成的响应为空"
|
||||||
|
|
||||||
logger.info(f"模型 '{model_name}' 成功生成回复。")
|
logger.debug(f"模型 '{model_name}' 成功生成回复。") # 你也不许刷屏
|
||||||
return content, (reasoning_content, model_name, tool_calls)
|
return content, (reasoning_content, model_name, tool_calls)
|
||||||
|
|
||||||
except RespNotOkException as e:
|
except RespNotOkException as e:
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import importlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Dict, Set, List, Optional, Tuple
|
from typing import Dict, Set, List, Optional, Tuple
|
||||||
@@ -27,7 +29,8 @@ class PluginFileHandler(FileSystemEventHandler):
|
|||||||
self.hot_reload_manager = hot_reload_manager
|
self.hot_reload_manager = hot_reload_manager
|
||||||
self.pending_reloads: Set[str] = set() # 待重载的插件名称
|
self.pending_reloads: Set[str] = set() # 待重载的插件名称
|
||||||
self.last_reload_time: Dict[str, float] = {} # 上次重载时间
|
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):
|
def on_modified(self, event):
|
||||||
"""文件修改事件"""
|
"""文件修改事件"""
|
||||||
@@ -60,26 +63,42 @@ class PluginFileHandler(FileSystemEventHandler):
|
|||||||
|
|
||||||
plugin_name, source_type = plugin_info
|
plugin_name, source_type = plugin_info
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
last_time = self.last_reload_time.get(plugin_name, 0)
|
|
||||||
|
# 文件变化缓存,避免重复处理同一文件的快速连续变化
|
||||||
# 防抖处理,避免频繁重载
|
file_cache_key = f"{file_path}_{change_type}"
|
||||||
if current_time - last_time < self.debounce_delay:
|
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
|
return
|
||||||
|
|
||||||
file_name = Path(file_path).name
|
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":
|
if change_type == "deleted":
|
||||||
|
# 解析实际的插件名称
|
||||||
|
actual_plugin_name = self.hot_reload_manager._resolve_plugin_name(plugin_name)
|
||||||
|
|
||||||
if file_name == "plugin.py":
|
if file_name == "plugin.py":
|
||||||
if plugin_name in plugin_manager.loaded_plugins:
|
if actual_plugin_name in plugin_manager.loaded_plugins:
|
||||||
logger.info(f"🗑️ 插件主文件被删除,卸载插件: {plugin_name} [{source_type}]")
|
logger.info(f"🗑️ 插件主文件被删除,卸载插件: {plugin_name} -> {actual_plugin_name} [{source_type}]")
|
||||||
self.hot_reload_manager._unload_plugin(plugin_name)
|
self.hot_reload_manager._unload_plugin(actual_plugin_name)
|
||||||
|
else:
|
||||||
|
logger.info(f"🗑️ 插件主文件被删除,但插件未加载: {plugin_name} -> {actual_plugin_name} [{source_type}]")
|
||||||
return
|
return
|
||||||
elif file_name in ("manifest.toml", "_manifest.json"):
|
elif file_name in ("manifest.toml", "_manifest.json"):
|
||||||
if plugin_name in plugin_manager.loaded_plugins:
|
if actual_plugin_name in plugin_manager.loaded_plugins:
|
||||||
logger.info(f"🗑️ 插件配置文件被删除,卸载插件: {plugin_name} [{source_type}]")
|
logger.info(f"🗑️ 插件配置文件被删除,卸载插件: {plugin_name} -> {actual_plugin_name} [{source_type}]")
|
||||||
self.hot_reload_manager._unload_plugin(plugin_name)
|
self.hot_reload_manager._unload_plugin(actual_plugin_name)
|
||||||
|
else:
|
||||||
|
logger.info(f"🗑️ 插件配置文件被删除,但插件未加载: {plugin_name} -> {actual_plugin_name} [{source_type}]")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 对于修改和创建事件,都进行重载
|
# 对于修改和创建事件,都进行重载
|
||||||
@@ -87,29 +106,43 @@ class PluginFileHandler(FileSystemEventHandler):
|
|||||||
self.pending_reloads.add(plugin_name)
|
self.pending_reloads.add(plugin_name)
|
||||||
self.last_reload_time[plugin_name] = current_time
|
self.last_reload_time[plugin_name] = current_time
|
||||||
|
|
||||||
# 延迟重载,避免文件正在写入时重载
|
# 延迟重载,确保文件写入完成
|
||||||
reload_thread = Thread(
|
reload_thread = Thread(
|
||||||
target=self._delayed_reload,
|
target=self._delayed_reload,
|
||||||
args=(plugin_name, source_type),
|
args=(plugin_name, source_type, current_time),
|
||||||
daemon=True
|
daemon=True
|
||||||
)
|
)
|
||||||
reload_thread.start()
|
reload_thread.start()
|
||||||
|
|
||||||
except Exception as e:
|
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:
|
try:
|
||||||
|
# 等待文件写入完成
|
||||||
time.sleep(self.debounce_delay)
|
time.sleep(self.debounce_delay)
|
||||||
|
|
||||||
if plugin_name in self.pending_reloads:
|
# 检查是否还需要重载(可能在等待期间有更新的变化)
|
||||||
self.pending_reloads.remove(plugin_name)
|
if plugin_name not in self.pending_reloads:
|
||||||
logger.info(f"🔄 延迟重载插件: {plugin_name} [{source_type}]")
|
return
|
||||||
self.hot_reload_manager._reload_plugin(plugin_name)
|
|
||||||
|
# 检查是否有更新的重载请求
|
||||||
|
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:
|
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]]:
|
def _get_plugin_info_from_path(self, file_path: str) -> Optional[Tuple[str, str]]:
|
||||||
"""从文件路径获取插件信息
|
"""从文件路径获取插件信息
|
||||||
@@ -237,35 +270,131 @@ class PluginHotReloadManager:
|
|||||||
logger.info("🛑 插件热重载已停止")
|
logger.info("🛑 插件热重载已停止")
|
||||||
|
|
||||||
def _reload_plugin(self, plugin_name: str):
|
def _reload_plugin(self, plugin_name: str):
|
||||||
"""重载指定插件"""
|
"""重载指定插件(简单重载)"""
|
||||||
try:
|
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):
|
if plugin_manager.reload_plugin(actual_plugin_name):
|
||||||
logger.info(f"✅ 插件重载成功: {plugin_name}")
|
logger.info(f"✅ 插件简单重载成功: {actual_plugin_name}")
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error(f"❌ 插件重载失败: {plugin_name}")
|
logger.error(f"❌ 插件简单重载失败: {actual_plugin_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
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):
|
def _unload_plugin(self, plugin_name: str):
|
||||||
"""卸载指定插件"""
|
"""卸载指定插件"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"🗑️ 开始卸载插件: {plugin_name}")
|
logger.info(f"🗑️ 开始卸载插件: {plugin_name}")
|
||||||
|
|
||||||
if plugin_manager.unload_plugin(plugin_name):
|
if plugin_manager.unload_plugin(plugin_name):
|
||||||
logger.info(f"✅ 插件卸载成功: {plugin_name}")
|
logger.info(f"✅ 插件卸载成功: {plugin_name}")
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error(f"❌ 插件卸载失败: {plugin_name}")
|
logger.error(f"❌ 插件卸载失败: {plugin_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
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):
|
def reload_all_plugins(self):
|
||||||
"""重载所有插件"""
|
"""重载所有插件"""
|
||||||
try:
|
try:
|
||||||
logger.info("🔄 开始重载所有插件...")
|
logger.info("🔄 开始深度重载所有插件...")
|
||||||
|
|
||||||
# 获取当前已加载的插件列表
|
# 获取当前已加载的插件列表
|
||||||
loaded_plugins = list(plugin_manager.loaded_plugins.keys())
|
loaded_plugins = list(plugin_manager.loaded_plugins.keys())
|
||||||
@@ -274,15 +403,42 @@ class PluginHotReloadManager:
|
|||||||
fail_count = 0
|
fail_count = 0
|
||||||
|
|
||||||
for plugin_name in loaded_plugins:
|
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
|
success_count += 1
|
||||||
else:
|
else:
|
||||||
fail_count += 1
|
fail_count += 1
|
||||||
|
|
||||||
logger.info(f"✅ 插件重载完成: 成功 {success_count} 个,失败 {fail_count} 个")
|
logger.info(f"✅ 插件重载完成: 成功 {success_count} 个,失败 {fail_count} 个")
|
||||||
|
|
||||||
|
# 清理全局缓存
|
||||||
|
importlib.invalidate_caches()
|
||||||
|
|
||||||
except Exception as e:
|
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):
|
def add_watch_directory(self, directory: str):
|
||||||
"""添加新的监听目录"""
|
"""添加新的监听目录"""
|
||||||
@@ -321,14 +477,33 @@ class PluginHotReloadManager:
|
|||||||
|
|
||||||
def get_status(self) -> dict:
|
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 {
|
return {
|
||||||
"is_running": self.is_running,
|
"is_running": self.is_running,
|
||||||
"watch_directories": self.watch_directories,
|
"watch_directories": self.watch_directories,
|
||||||
"active_observers": len(self.observers),
|
"active_observers": len(self.observers),
|
||||||
"loaded_plugins": len(plugin_manager.loaded_plugins),
|
"loaded_plugins": len(plugin_manager.loaded_plugins),
|
||||||
"failed_plugins": len(plugin_manager.failed_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()
|
hot_reload_manager = PluginHotReloadManager()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
import sys
|
import sys
|
||||||
|
import importlib
|
||||||
|
|
||||||
from typing import Dict, List, Optional, Tuple, Type, Any
|
from typing import Dict, List, Optional, Tuple, Type, Any
|
||||||
from importlib.util import spec_from_file_location, module_from_spec
|
from importlib.util import spec_from_file_location, module_from_spec
|
||||||
@@ -289,11 +290,11 @@ class PluginManager:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
plugin_file: 插件文件路径
|
plugin_file: 插件文件路径
|
||||||
plugin_name: 插件名称
|
|
||||||
plugin_dir: 插件目录路径
|
|
||||||
"""
|
"""
|
||||||
# 生成模块名
|
# 生成模块名和插件信息
|
||||||
plugin_path = Path(plugin_file)
|
plugin_path = Path(plugin_file)
|
||||||
|
plugin_dir = plugin_path.parent # 插件目录
|
||||||
|
plugin_name = plugin_dir.name # 插件名称
|
||||||
module_name = ".".join(plugin_path.parent.parts)
|
module_name = ".".join(plugin_path.parent.parts)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -307,13 +308,13 @@ class PluginManager:
|
|||||||
module.__package__ = module_name # 设置模块包名
|
module.__package__ = module_name # 设置模块包名
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
logger.debug(f"插件模块加载成功: {plugin_file}")
|
logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"加载插件模块 {plugin_file} 失败: {e}"
|
error_msg = f"加载插件模块 {plugin_file} 失败: {e}"
|
||||||
logger.error(error_msg)
|
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
|
return False
|
||||||
|
|
||||||
# == 兼容性检查 ==
|
# == 兼容性检查 ==
|
||||||
@@ -527,6 +528,10 @@ class PluginManager:
|
|||||||
# 从已加载插件中移除
|
# 从已加载插件中移除
|
||||||
del self.loaded_plugins[plugin_name]
|
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:
|
if plugin_name in self.failed_plugins:
|
||||||
del self.failed_plugins[plugin_name]
|
del self.failed_plugins[plugin_name]
|
||||||
@@ -535,7 +540,7 @@ class PluginManager:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ 插件卸载失败: {plugin_name} - {str(e)}")
|
logger.error(f"❌ 插件卸载失败: {plugin_name} - {str(e)}", exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def reload_plugin(self, plugin_name: str) -> bool:
|
def reload_plugin(self, plugin_name: str) -> bool:
|
||||||
@@ -548,55 +553,54 @@ class PluginManager:
|
|||||||
bool: 重载是否成功
|
bool: 重载是否成功
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 先卸载插件
|
logger.info(f"🔄 开始重载插件: {plugin_name}")
|
||||||
|
|
||||||
|
# 卸载插件
|
||||||
if plugin_name in self.loaded_plugins:
|
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)
|
self.rescan_plugin_directory()
|
||||||
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)
|
|
||||||
|
|
||||||
for module_name in sys.modules:
|
# 重新加载插件实例
|
||||||
if module_name.startswith(plugin_module_prefix):
|
if plugin_name in self.plugin_classes:
|
||||||
modules_to_remove.append(module_name)
|
success, _ = self.load_registered_plugin_classes(plugin_name)
|
||||||
|
if success:
|
||||||
for module_name in modules_to_remove:
|
logger.info(f"✅ 插件重载成功: {plugin_name}")
|
||||||
del sys.modules[module_name]
|
return True
|
||||||
|
|
||||||
# 从插件类注册表中移除
|
|
||||||
if plugin_name in self.plugin_classes:
|
|
||||||
del self.plugin_classes[plugin_name]
|
|
||||||
|
|
||||||
# 重新加载插件模块
|
|
||||||
if self._load_plugin_module_file(plugin_file):
|
|
||||||
# 重新加载插件实例
|
|
||||||
success, _ = self.load_registered_plugin_classes(plugin_name)
|
|
||||||
if success:
|
|
||||||
logger.info(f"🔄 插件重载成功: {plugin_name}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"❌ 插件重载失败: {plugin_name} - 实例化失败")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logger.error(f"❌ 插件重载失败: {plugin_name} - 模块加载失败")
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"❌ 插件重载失败: {plugin_name} - 插件文件不存在")
|
logger.error(f"❌ 插件重载失败: {plugin_name} - 实例化失败")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.error(f"❌ 插件重载失败: {plugin_name} - 插件路径未知")
|
logger.error(f"❌ 插件重载失败: {plugin_name} - 插件类未找到")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ 插件重载失败: {plugin_name} - {str(e)}")
|
logger.error(f"❌ 插件重载失败: {plugin_name} - {str(e)}", exc_info=True)
|
||||||
logger.debug("详细错误信息: ", exc_info=True)
|
|
||||||
return False
|
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()
|
plugin_manager = PluginManager()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import Tuple
|
|||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
||||||
from src.plugin_system.apis import person_api, generator_api
|
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
|
from ..services.manager import get_qzone_service, get_config_getter
|
||||||
|
|
||||||
logger = get_logger("MaiZone.ReadFeedAction")
|
logger = get_logger("MaiZone.ReadFeedAction")
|
||||||
@@ -32,27 +33,11 @@ class ReadFeedAction(BaseAction):
|
|||||||
|
|
||||||
async def _check_permission(self) -> bool:
|
async def _check_permission(self) -> bool:
|
||||||
"""检查当前用户是否有权限执行此动作"""
|
"""检查当前用户是否有权限执行此动作"""
|
||||||
user_name = self.action_data.get("user_name", "")
|
platform = self.chat_stream.platform
|
||||||
person_id = person_api.get_person_id_by_name(user_name)
|
user_id = self.chat_stream.user_info.user_id
|
||||||
if not person_id:
|
|
||||||
return False
|
|
||||||
|
|
||||||
user_id = await person_api.get_person_value(person_id, "user_id")
|
# 使用权限API检查用户是否有阅读说说的权限
|
||||||
if not user_id:
|
return permission_api.check_permission(platform, user_id, "plugin.maizone.read_feed")
|
||||||
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
|
|
||||||
|
|
||||||
async def execute(self) -> Tuple[bool, str]:
|
async def execute(self) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import Tuple
|
|||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
||||||
from src.plugin_system.apis import person_api, generator_api
|
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
|
from ..services.manager import get_qzone_service, get_config_getter
|
||||||
|
|
||||||
logger = get_logger("MaiZone.SendFeedAction")
|
logger = get_logger("MaiZone.SendFeedAction")
|
||||||
@@ -32,27 +33,11 @@ class SendFeedAction(BaseAction):
|
|||||||
|
|
||||||
async def _check_permission(self) -> bool:
|
async def _check_permission(self) -> bool:
|
||||||
"""检查当前用户是否有权限执行此动作"""
|
"""检查当前用户是否有权限执行此动作"""
|
||||||
user_name = self.action_data.get("user_name", "")
|
platform = self.chat_stream.platform
|
||||||
person_id = person_api.get_person_id_by_name(user_name)
|
user_id = self.chat_stream.user_info.user_id
|
||||||
if not person_id:
|
|
||||||
return False
|
|
||||||
|
|
||||||
user_id = await person_api.get_person_value(person_id, "user_id")
|
# 使用权限API检查用户是否有发送说说的权限
|
||||||
if not user_id:
|
return permission_api.check_permission(platform, user_id, "plugin.maizone.send_feed")
|
||||||
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
|
|
||||||
|
|
||||||
async def execute(self) -> Tuple[bool, str]:
|
async def execute(self) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
发送说说命令组件
|
发送说说命令 await self.send_text(f"收到!正在为你生成关于"{topic or '随机'}"的说说,请稍候...【热重载测试成功】")件
|
||||||
"""
|
"""
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
@@ -16,15 +16,16 @@ logger = get_logger("MaiZone.SendFeedCommand")
|
|||||||
class SendFeedCommand(PlusCommand):
|
class SendFeedCommand(PlusCommand):
|
||||||
"""
|
"""
|
||||||
响应用户通过 `/send_feed` 命令发送说说的请求。
|
响应用户通过 `/send_feed` 命令发送说说的请求。
|
||||||
|
测试热重载功能 - 这是一个测试注释,现在应该可以正常工作了!
|
||||||
"""
|
"""
|
||||||
command_name: str = "send_feed"
|
command_name: str = "send_feed"
|
||||||
command_description: str = "发送一条QQ空间说说"
|
command_description: str = "发一条QQ空间说说"
|
||||||
command_aliases = ["发空间"]
|
command_aliases = ["发空间"]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*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]:
|
async def execute(self, args: CommandArgs) -> Tuple[bool, str, bool]:
|
||||||
"""
|
"""
|
||||||
执行命令的核心逻辑。
|
执行命令的核心逻辑。
|
||||||
|
|||||||
@@ -84,12 +84,19 @@ class MaiZoneRefactoredPlugin(BasePlugin):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
# 注册权限节点
|
||||||
permission_api.register_permission_node(
|
permission_api.register_permission_node(
|
||||||
"plugin.send.permission",
|
"plugin.maizone.send_feed",
|
||||||
"是否可以使用机器人发送说说",
|
"是否可以使用机器人发送QQ空间说说",
|
||||||
"maiZone",
|
"maiZone",
|
||||||
False
|
False
|
||||||
)
|
)
|
||||||
|
permission_api.register_permission_node(
|
||||||
|
"plugin.maizone.read_feed",
|
||||||
|
"是否可以使用机器人读取QQ空间说说",
|
||||||
|
"maiZone",
|
||||||
|
True
|
||||||
|
)
|
||||||
content_service = ContentService(self.get_config)
|
content_service = ContentService(self.get_config)
|
||||||
image_service = ImageService(self.get_config)
|
image_service = ImageService(self.get_config)
|
||||||
cookie_service = CookieService(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("reply_tracker", reply_tracker_service)
|
||||||
register_service("get_config", self.get_config)
|
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]]:
|
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -13,226 +13,313 @@ from src.plugin_system import (
|
|||||||
ComponentType,
|
ComponentType,
|
||||||
send_api,
|
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):
|
class ManagementCommand(PlusCommand):
|
||||||
command_name: str = "management"
|
"""插件管理命令 - 使用PlusCommand系统"""
|
||||||
description: str = "管理命令"
|
|
||||||
command_pattern: str = r"(?P<manage_command>^/pm(\s[a-zA-Z0-9_]+)*\s*$)"
|
command_name = "pm"
|
||||||
|
command_description = "插件管理命令,支持插件和组件的管理操作"
|
||||||
|
command_aliases = ["pluginmanage", "插件管理"]
|
||||||
|
priority = 10
|
||||||
|
chat_type_allow = ChatType.ALL
|
||||||
|
intercept_message = True
|
||||||
|
|
||||||
async def execute(self) -> Tuple[bool, str, bool]:
|
def __init__(self, *args, **kwargs):
|
||||||
# sourcery skip: merge-duplicate-blocks
|
super().__init__(*args, **kwargs)
|
||||||
if (
|
|
||||||
not self.message
|
@require_permission("plugin.management.admin", "❌ 你没有插件管理的权限")
|
||||||
or not self.message.message_info
|
async def execute(self, args: CommandArgs) -> Tuple[bool, str, bool]:
|
||||||
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
|
if args.is_empty():
|
||||||
):
|
await self._show_help("all")
|
||||||
await self._send_message("你没有权限使用插件管理命令")
|
return True, "显示帮助信息", True
|
||||||
return False, "没有权限", True
|
|
||||||
if not self.message.chat_stream:
|
subcommand = args.get_first().lower()
|
||||||
await self._send_message("无法获取聊天流信息")
|
remaining_args = args.get_args()[1:] # 获取除第一个参数外的所有参数
|
||||||
return False, "无法获取聊天流信息", True
|
|
||||||
self.stream_id = self.message.chat_stream.stream_id
|
if subcommand in ["plugin", "插件"]:
|
||||||
if not self.stream_id:
|
return await self._handle_plugin_commands(remaining_args)
|
||||||
await self._send_message("无法获取聊天流信息")
|
elif subcommand in ["component", "组件", "comp"]:
|
||||||
return False, "无法获取聊天流信息", True
|
return await self._handle_component_commands(remaining_args)
|
||||||
command_list = self.matched_groups["manage_command"].strip().split(" ")
|
elif subcommand in ["help", "帮助"]:
|
||||||
if len(command_list) == 1:
|
await self._show_help("all")
|
||||||
await self.show_help("all")
|
return True, "显示帮助信息", True
|
||||||
return True, "帮助已发送", True
|
else:
|
||||||
if len(command_list) == 2:
|
await self.send_text(f"❌ 未知的子命令: {subcommand}\n使用 /pm help 查看帮助")
|
||||||
match command_list[1]:
|
return True, "未知子命令", True
|
||||||
case "plugin":
|
|
||||||
await self.show_help("plugin")
|
async def _handle_plugin_commands(self, args: List[str]) -> Tuple[bool, str, bool]:
|
||||||
case "component":
|
"""处理插件相关命令"""
|
||||||
await self.show_help("component")
|
if not args:
|
||||||
case "help":
|
await self._show_help("plugin")
|
||||||
await self.show_help("all")
|
return True, "显示插件帮助", True
|
||||||
case _:
|
|
||||||
await self._send_message("插件管理命令不合法")
|
action = args[0].lower()
|
||||||
return False, "命令不合法", True
|
|
||||||
if len(command_list) == 3:
|
if action in ["help", "帮助"]:
|
||||||
if command_list[1] == "plugin":
|
await self._show_help("plugin")
|
||||||
match command_list[2]:
|
elif action in ["list", "列表"]:
|
||||||
case "help":
|
await self._list_registered_plugins()
|
||||||
await self.show_help("plugin")
|
elif action in ["list_enabled", "已启用"]:
|
||||||
case "list":
|
await self._list_loaded_plugins()
|
||||||
await self._list_registered_plugins()
|
elif action in ["rescan", "重扫"]:
|
||||||
case "list_enabled":
|
await self._rescan_plugin_dirs()
|
||||||
await self._list_loaded_plugins()
|
elif action in ["load", "加载"] and len(args) > 1:
|
||||||
case "rescan":
|
await self._load_plugin(args[1])
|
||||||
await self._rescan_plugin_dirs()
|
elif action in ["unload", "卸载"] and len(args) > 1:
|
||||||
case _:
|
await self._unload_plugin(args[1])
|
||||||
await self._send_message("插件管理命令不合法")
|
elif action in ["reload", "重载"] and len(args) > 1:
|
||||||
return False, "命令不合法", True
|
await self._reload_plugin(args[1])
|
||||||
elif command_list[1] == "component":
|
elif action in ["force_reload", "强制重载"] and len(args) > 1:
|
||||||
if command_list[2] == "list":
|
await self._force_reload_plugin(args[1])
|
||||||
await self._list_all_registered_components()
|
elif action in ["add_dir", "添加目录"] and len(args) > 1:
|
||||||
elif command_list[2] == "help":
|
await self._add_dir(args[1])
|
||||||
await self.show_help("component")
|
elif action in ["hotreload_status", "热重载状态"]:
|
||||||
else:
|
await self._show_hotreload_status()
|
||||||
await self._send_message("插件管理命令不合法")
|
elif action in ["clear_cache", "清理缓存"]:
|
||||||
return False, "命令不合法", True
|
await self._clear_all_caches()
|
||||||
else:
|
else:
|
||||||
await self._send_message("插件管理命令不合法")
|
await self.send_text("❌ 插件管理命令不合法\n使用 /pm plugin help 查看帮助")
|
||||||
return False, "命令不合法", True
|
return False, "命令不合法", True
|
||||||
if len(command_list) == 4:
|
|
||||||
if command_list[1] == "plugin":
|
return True, "插件命令执行完成", True
|
||||||
match command_list[2]:
|
|
||||||
case "load":
|
async def _handle_component_commands(self, args: List[str]) -> Tuple[bool, str, bool]:
|
||||||
await self._load_plugin(command_list[3])
|
"""处理组件相关命令"""
|
||||||
case "unload":
|
if not args:
|
||||||
await self._unload_plugin(command_list[3])
|
await self._show_help("component")
|
||||||
case "reload":
|
return True, "显示组件帮助", True
|
||||||
await self._reload_plugin(command_list[3])
|
|
||||||
case "add_dir":
|
action = args[0].lower()
|
||||||
await self._add_dir(command_list[3])
|
|
||||||
case _:
|
if action in ["help", "帮助"]:
|
||||||
await self._send_message("插件管理命令不合法")
|
await self._show_help("component")
|
||||||
return False, "命令不合法", True
|
elif action in ["list", "列表"]:
|
||||||
elif command_list[1] == "component":
|
if len(args) == 1:
|
||||||
if command_list[2] != "list":
|
await self._list_all_registered_components()
|
||||||
await self._send_message("插件管理命令不合法")
|
elif len(args) == 2:
|
||||||
return False, "命令不合法", True
|
if args[1] in ["enabled", "启用"]:
|
||||||
if command_list[3] == "enabled":
|
|
||||||
await self._list_enabled_components()
|
await self._list_enabled_components()
|
||||||
elif command_list[3] == "disabled":
|
elif args[1] in ["disabled", "禁用"]:
|
||||||
await self._list_disabled_components()
|
await self._list_disabled_components()
|
||||||
else:
|
else:
|
||||||
await self._send_message("插件管理命令不合法")
|
await self.send_text("❌ 组件列表命令不合法")
|
||||||
return False, "命令不合法", True
|
return False, "命令不合法", True
|
||||||
else:
|
elif len(args) == 3:
|
||||||
await self._send_message("插件管理命令不合法")
|
if args[1] in ["enabled", "启用"]:
|
||||||
return False, "命令不合法", True
|
await self._list_enabled_components(target_type=args[2])
|
||||||
if len(command_list) == 5:
|
elif args[1] in ["disabled", "禁用"]:
|
||||||
if command_list[1] != "component":
|
await self._list_disabled_components(target_type=args[2])
|
||||||
await self._send_message("插件管理命令不合法")
|
elif args[1] in ["type", "类型"]:
|
||||||
return False, "命令不合法", True
|
await self._list_registered_components_by_type(args[2])
|
||||||
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:
|
else:
|
||||||
await self._send_message("插件管理命令不合法")
|
await self.send_text("❌ 组件列表命令不合法")
|
||||||
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
|
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:
|
else:
|
||||||
await self._send_message("插件管理命令不合法")
|
await self.send_text("❌ 组件启用命令不合法,范围应为 global 或 local")
|
||||||
return False, "命令不合法", True
|
return False, "命令不合法", True
|
||||||
|
elif action in ["disable", "禁用"] and len(args) >= 4:
|
||||||
|
scope = args[1].lower()
|
||||||
|
component_name = args[2]
|
||||||
|
component_type = args[3]
|
||||||
|
if scope in ["global", "全局"]:
|
||||||
|
await self._globally_disable_component(component_name, component_type)
|
||||||
|
elif scope in ["local", "本地"]:
|
||||||
|
await self._locally_disable_component(component_name, component_type)
|
||||||
|
else:
|
||||||
|
await self.send_text("❌ 组件禁用命令不合法,范围应为 global 或 local")
|
||||||
|
return False, "命令不合法", True
|
||||||
|
else:
|
||||||
|
await self.send_text("❌ 组件管理命令不合法\n使用 /pm component help 查看帮助")
|
||||||
|
return False, "命令不合法", True
|
||||||
|
|
||||||
|
return True, "组件命令执行完成", True
|
||||||
|
|
||||||
return True, "命令执行完成", True
|
async def _show_help(self, target: str):
|
||||||
|
"""显示帮助信息"""
|
||||||
async def show_help(self, target: str):
|
|
||||||
help_msg = ""
|
help_msg = ""
|
||||||
match target:
|
if target == "all":
|
||||||
case "all":
|
help_msg = """📋 插件管理命令帮助
|
||||||
help_msg = (
|
|
||||||
"管理命令帮助\n"
|
🔧 主要功能:
|
||||||
"/pm help 管理命令提示\n"
|
• `/pm help` - 显示此帮助
|
||||||
"/pm plugin 插件管理命令\n"
|
• `/pm plugin` - 插件管理命令
|
||||||
"/pm component 组件管理命令\n"
|
• `/pm component` - 组件管理命令
|
||||||
"使用 /pm plugin help 或 /pm component help 获取具体帮助"
|
|
||||||
)
|
📝 使用示例:
|
||||||
case "plugin":
|
• `/pm plugin help` - 查看插件管理帮助
|
||||||
help_msg = (
|
• `/pm component help` - 查看组件管理帮助
|
||||||
"插件管理命令帮助\n"
|
|
||||||
"/pm plugin help 插件管理命令提示\n"
|
🔄 别名:可以使用 `/pluginmanage` 或 `/插件管理` 代替 `/pm`"""
|
||||||
"/pm plugin list 列出所有注册的插件\n"
|
elif target == "plugin":
|
||||||
"/pm plugin list_enabled 列出所有加载(启用)的插件\n"
|
help_msg = """🔌 插件管理命令帮助
|
||||||
"/pm plugin rescan 重新扫描所有目录\n"
|
|
||||||
"/pm plugin load <plugin_name> 加载指定插件\n"
|
📋 基本操作:
|
||||||
"/pm plugin unload <plugin_name> 卸载指定插件\n"
|
• `/pm plugin help` - 显示插件管理帮助
|
||||||
"/pm plugin reload <plugin_name> 重新加载指定插件\n"
|
• `/pm plugin list` - 列出所有注册的插件
|
||||||
"/pm plugin add_dir <directory_path> 添加插件目录\n"
|
• `/pm plugin list_enabled` - 列出所有加载(启用)的插件
|
||||||
)
|
• `/pm plugin rescan` - 重新扫描所有插件目录
|
||||||
case "component":
|
|
||||||
help_msg = (
|
⚙️ 插件控制:
|
||||||
"组件管理命令帮助\n"
|
• `/pm plugin load <插件名>` - 加载指定插件
|
||||||
"/pm component help 组件管理命令提示\n"
|
• `/pm plugin unload <插件名>` - 卸载指定插件
|
||||||
"/pm component list 列出所有注册的组件\n"
|
• `/pm plugin reload <插件名>` - 重新加载指定插件
|
||||||
"/pm component list enabled <可选: type> 列出所有启用的组件\n"
|
• `/pm plugin force_reload <插件名>` - 强制重载指定插件(深度清理)
|
||||||
"/pm component list disabled <可选: type> 列出所有禁用的组件\n"
|
• `/pm plugin add_dir <目录路径>` - 添加插件目录
|
||||||
" - <type> 可选项: local,代表当前聊天中的;global,代表全局的\n"
|
|
||||||
" - <type> 不填时为 global\n"
|
<EFBFBD> 热重载管理:
|
||||||
"/pm component list type <component_type> 列出已经注册的指定类型的组件\n"
|
• `/pm plugin hotreload_status` - 查看热重载状态
|
||||||
"/pm component enable global <component_name> <component_type> 全局启用组件\n"
|
• `/pm plugin clear_cache` - 清理所有模块缓存
|
||||||
"/pm component enable local <component_name> <component_type> 本聊天启用组件\n"
|
|
||||||
"/pm component disable global <component_name> <component_type> 全局禁用组件\n"
|
<EFBFBD>📝 示例:
|
||||||
"/pm component disable local <component_name> <component_type> 本聊天禁用组件\n"
|
• `/pm plugin load echo_example`
|
||||||
" - <component_type> 可选项: action, command, event_handler\n"
|
• `/pm plugin force_reload permission_manager_plugin`
|
||||||
)
|
• `/pm plugin clear_cache`"""
|
||||||
case _:
|
elif target == "component":
|
||||||
return
|
help_msg = """🧩 组件管理命令帮助
|
||||||
await self._send_message(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):
|
async def _list_loaded_plugins(self):
|
||||||
|
"""列出已加载的插件"""
|
||||||
plugins = plugin_manage_api.list_loaded_plugins()
|
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):
|
async def _list_registered_plugins(self):
|
||||||
|
"""列出已注册的插件"""
|
||||||
plugins = plugin_manage_api.list_registered_plugins()
|
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):
|
async def _rescan_plugin_dirs(self):
|
||||||
|
"""重新扫描插件目录"""
|
||||||
plugin_manage_api.rescan_plugin_directory()
|
plugin_manage_api.rescan_plugin_directory()
|
||||||
await self._send_message("插件目录重新扫描执行中")
|
await self.send_text("🔄 插件目录重新扫描已启动")
|
||||||
|
|
||||||
async def _load_plugin(self, plugin_name: str):
|
async def _load_plugin(self, plugin_name: str):
|
||||||
|
"""加载指定插件"""
|
||||||
success, count = plugin_manage_api.load_plugin(plugin_name)
|
success, count = plugin_manage_api.load_plugin(plugin_name)
|
||||||
if success:
|
if success:
|
||||||
await self._send_message(f"插件加载成功: {plugin_name}")
|
await self.send_text(f"✅ 插件加载成功: `{plugin_name}`")
|
||||||
else:
|
else:
|
||||||
if count == 0:
|
if count == 0:
|
||||||
await self._send_message(f"插件{plugin_name}为禁用状态")
|
await self.send_text(f"⚠️ 插件 `{plugin_name}` 为禁用状态")
|
||||||
await self._send_message(f"插件加载失败: {plugin_name}")
|
else:
|
||||||
|
await self.send_text(f"❌ 插件加载失败: `{plugin_name}`")
|
||||||
|
|
||||||
async def _unload_plugin(self, plugin_name: str):
|
async def _unload_plugin(self, plugin_name: str):
|
||||||
|
"""卸载指定插件"""
|
||||||
success = await plugin_manage_api.remove_plugin(plugin_name)
|
success = await plugin_manage_api.remove_plugin(plugin_name)
|
||||||
if success:
|
if success:
|
||||||
await self._send_message(f"插件卸载成功: {plugin_name}")
|
await self.send_text(f"✅ 插件卸载成功: `{plugin_name}`")
|
||||||
else:
|
else:
|
||||||
await self._send_message(f"插件卸载失败: {plugin_name}")
|
await self.send_text(f"❌ 插件卸载失败: `{plugin_name}`")
|
||||||
|
|
||||||
async def _reload_plugin(self, plugin_name: str):
|
async def _reload_plugin(self, plugin_name: str):
|
||||||
|
"""重新加载指定插件"""
|
||||||
success = await plugin_manage_api.reload_plugin(plugin_name)
|
success = await plugin_manage_api.reload_plugin(plugin_name)
|
||||||
if success:
|
if success:
|
||||||
await self._send_message(f"插件重新加载成功: {plugin_name}")
|
await self.send_text(f"✅ 插件重新加载成功: `{plugin_name}`")
|
||||||
else:
|
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):
|
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)
|
success = plugin_manage_api.add_plugin_directory(dir_path)
|
||||||
await asyncio.sleep(0.5) # 防止乱序发送
|
await asyncio.sleep(0.5) # 防止乱序发送
|
||||||
if success:
|
if success:
|
||||||
await self._send_message(f"插件目录添加成功: {dir_path}")
|
await self.send_text(f"✅ 插件目录添加成功: `{dir_path}`")
|
||||||
else:
|
else:
|
||||||
await self._send_message(f"插件目录添加失败: {dir_path}")
|
await self.send_text(f"❌ 插件目录添加失败: `{dir_path}`")
|
||||||
|
|
||||||
def _fetch_all_registered_components(self) -> List[ComponentInfo]:
|
def _fetch_all_registered_components(self) -> List[ComponentInfo]:
|
||||||
all_plugin_info = component_manage_api.get_all_plugin_info()
|
all_plugin_info = component_manage_api.get_all_plugin_info()
|
||||||
@@ -245,47 +332,55 @@ class ManagementCommand(BaseCommand):
|
|||||||
return components_info
|
return components_info
|
||||||
|
|
||||||
def _fetch_locally_disabled_components(self) -> List[str]:
|
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(
|
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(
|
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(
|
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 (
|
return (
|
||||||
locally_disabled_components_actions
|
locally_disabled_components_actions
|
||||||
+ locally_disabled_components_commands
|
+ locally_disabled_components_commands
|
||||||
|
+ locally_disabled_components_plus_commands
|
||||||
+ locally_disabled_components_event_handlers
|
+ locally_disabled_components_event_handlers
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _list_all_registered_components(self):
|
async def _list_all_registered_components(self):
|
||||||
|
"""列出所有已注册的组件"""
|
||||||
components_info = self._fetch_all_registered_components()
|
components_info = self._fetch_all_registered_components()
|
||||||
if not components_info:
|
if not components_info:
|
||||||
await self._send_message("没有注册的组件")
|
await self.send_text("📋 没有注册的组件")
|
||||||
return
|
return
|
||||||
|
|
||||||
all_components_str = ", ".join(
|
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"):
|
async def _list_enabled_components(self, target_type: str = "global"):
|
||||||
|
"""列出启用的组件"""
|
||||||
components_info = self._fetch_all_registered_components()
|
components_info = self._fetch_all_registered_components()
|
||||||
if not components_info:
|
if not components_info:
|
||||||
await self._send_message("没有注册的组件")
|
await self.send_text("📋 没有注册的组件")
|
||||||
return
|
return
|
||||||
|
|
||||||
if target_type == "global":
|
if target_type == "global":
|
||||||
enabled_components = [component for component in components_info if component.enabled]
|
enabled_components = [component for component in components_info if component.enabled]
|
||||||
if not enabled_components:
|
if not enabled_components:
|
||||||
await self._send_message("没有满足条件的已启用全局组件")
|
await self.send_text("📋 没有满足条件的已启用全局组件")
|
||||||
return
|
return
|
||||||
enabled_components_str = ", ".join(
|
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":
|
elif target_type == "local":
|
||||||
locally_disabled_components = self._fetch_locally_disabled_components()
|
locally_disabled_components = self._fetch_locally_disabled_components()
|
||||||
enabled_components = [
|
enabled_components = [
|
||||||
@@ -294,28 +389,29 @@ class ManagementCommand(BaseCommand):
|
|||||||
if (component.name not in locally_disabled_components and component.enabled)
|
if (component.name not in locally_disabled_components and component.enabled)
|
||||||
]
|
]
|
||||||
if not enabled_components:
|
if not enabled_components:
|
||||||
await self._send_message("本聊天没有满足条件的已启用组件")
|
await self.send_text("📋 本聊天没有满足条件的已启用组件")
|
||||||
return
|
return
|
||||||
enabled_components_str = ", ".join(
|
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"):
|
async def _list_disabled_components(self, target_type: str = "global"):
|
||||||
|
"""列出禁用的组件"""
|
||||||
components_info = self._fetch_all_registered_components()
|
components_info = self._fetch_all_registered_components()
|
||||||
if not components_info:
|
if not components_info:
|
||||||
await self._send_message("没有注册的组件")
|
await self.send_text("📋 没有注册的组件")
|
||||||
return
|
return
|
||||||
|
|
||||||
if target_type == "global":
|
if target_type == "global":
|
||||||
disabled_components = [component for component in components_info if not component.enabled]
|
disabled_components = [component for component in components_info if not component.enabled]
|
||||||
if not disabled_components:
|
if not disabled_components:
|
||||||
await self._send_message("没有满足条件的已禁用全局组件")
|
await self.send_text("📋 没有满足条件的已禁用全局组件")
|
||||||
return
|
return
|
||||||
disabled_components_str = ", ".join(
|
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":
|
elif target_type == "local":
|
||||||
locally_disabled_components = self._fetch_locally_disabled_components()
|
locally_disabled_components = self._fetch_locally_disabled_components()
|
||||||
disabled_components = [
|
disabled_components = [
|
||||||
@@ -324,110 +420,115 @@ class ManagementCommand(BaseCommand):
|
|||||||
if (component.name in locally_disabled_components or not component.enabled)
|
if (component.name in locally_disabled_components or not component.enabled)
|
||||||
]
|
]
|
||||||
if not disabled_components:
|
if not disabled_components:
|
||||||
await self._send_message("本聊天没有满足条件的已禁用组件")
|
await self.send_text("📋 本聊天没有满足条件的已禁用组件")
|
||||||
return
|
return
|
||||||
disabled_components_str = ", ".join(
|
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):
|
async def _list_registered_components_by_type(self, target_type: str):
|
||||||
match target_type:
|
"""按类型列出已注册的组件"""
|
||||||
case "action":
|
type_mapping = {
|
||||||
component_type = ComponentType.ACTION
|
"action": ComponentType.ACTION,
|
||||||
case "command":
|
"command": ComponentType.COMMAND,
|
||||||
component_type = ComponentType.COMMAND
|
"event_handler": ComponentType.EVENT_HANDLER,
|
||||||
case "event_handler":
|
"plus_command": ComponentType.PLUS_COMMAND,
|
||||||
component_type = ComponentType.EVENT_HANDLER
|
}
|
||||||
case _:
|
|
||||||
await self._send_message(f"未知组件类型: {target_type}")
|
component_type = type_mapping.get(target_type.lower())
|
||||||
return
|
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)
|
components_info = component_manage_api.get_components_info_by_type(component_type)
|
||||||
if not components_info:
|
if not components_info:
|
||||||
await self._send_message(f"没有注册的 {target_type} 组件")
|
await self.send_text(f"📋 没有注册的 `{target_type}` 组件")
|
||||||
return
|
return
|
||||||
|
|
||||||
components_str = ", ".join(
|
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):
|
async def _globally_enable_component(self, component_name: str, component_type: str):
|
||||||
match component_type:
|
"""全局启用组件"""
|
||||||
case "action":
|
type_mapping = {
|
||||||
target_component_type = ComponentType.ACTION
|
"action": ComponentType.ACTION,
|
||||||
case "command":
|
"command": ComponentType.COMMAND,
|
||||||
target_component_type = ComponentType.COMMAND
|
"event_handler": ComponentType.EVENT_HANDLER,
|
||||||
case "event_handler":
|
"plus_command": ComponentType.PLUS_COMMAND,
|
||||||
target_component_type = ComponentType.EVENT_HANDLER
|
}
|
||||||
case _:
|
|
||||||
await self._send_message(f"未知组件类型: {component_type}")
|
target_component_type = type_mapping.get(component_type.lower())
|
||||||
return
|
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):
|
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:
|
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):
|
async def _globally_disable_component(self, component_name: str, component_type: str):
|
||||||
match component_type:
|
"""全局禁用组件"""
|
||||||
case "action":
|
type_mapping = {
|
||||||
target_component_type = ComponentType.ACTION
|
"action": ComponentType.ACTION,
|
||||||
case "command":
|
"command": ComponentType.COMMAND,
|
||||||
target_component_type = ComponentType.COMMAND
|
"event_handler": ComponentType.EVENT_HANDLER,
|
||||||
case "event_handler":
|
"plus_command": ComponentType.PLUS_COMMAND,
|
||||||
target_component_type = ComponentType.EVENT_HANDLER
|
}
|
||||||
case _:
|
|
||||||
await self._send_message(f"未知组件类型: {component_type}")
|
target_component_type = type_mapping.get(component_type.lower())
|
||||||
return
|
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)
|
success = await component_manage_api.globally_disable_component(component_name, target_component_type)
|
||||||
if success:
|
if success:
|
||||||
await self._send_message(f"全局禁用组件成功: {component_name}")
|
await self.send_text(f"✅ 全局禁用组件成功: `{component_name}`")
|
||||||
else:
|
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):
|
async def _locally_enable_component(self, component_name: str, component_type: str):
|
||||||
match component_type:
|
"""本地启用组件"""
|
||||||
case "action":
|
type_mapping = {
|
||||||
target_component_type = ComponentType.ACTION
|
"action": ComponentType.ACTION,
|
||||||
case "command":
|
"command": ComponentType.COMMAND,
|
||||||
target_component_type = ComponentType.COMMAND
|
"event_handler": ComponentType.EVENT_HANDLER,
|
||||||
case "event_handler":
|
"plus_command": ComponentType.PLUS_COMMAND,
|
||||||
target_component_type = ComponentType.EVENT_HANDLER
|
}
|
||||||
case _:
|
|
||||||
await self._send_message(f"未知组件类型: {component_type}")
|
target_component_type = type_mapping.get(component_type.lower())
|
||||||
return
|
if not target_component_type:
|
||||||
if component_manage_api.locally_enable_component(
|
await self.send_text(f"❌ 未知组件类型: `{component_type}`")
|
||||||
component_name,
|
return
|
||||||
target_component_type,
|
|
||||||
self.message.chat_stream.stream_id,
|
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_message(f"本地启用组件成功: {component_name}")
|
await self.send_text(f"✅ 本地启用组件成功: `{component_name}`")
|
||||||
else:
|
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):
|
async def _locally_disable_component(self, component_name: str, component_type: str):
|
||||||
match component_type:
|
"""本地禁用组件"""
|
||||||
case "action":
|
type_mapping = {
|
||||||
target_component_type = ComponentType.ACTION
|
"action": ComponentType.ACTION,
|
||||||
case "command":
|
"command": ComponentType.COMMAND,
|
||||||
target_component_type = ComponentType.COMMAND
|
"event_handler": ComponentType.EVENT_HANDLER,
|
||||||
case "event_handler":
|
"plus_command": ComponentType.PLUS_COMMAND,
|
||||||
target_component_type = ComponentType.EVENT_HANDLER
|
}
|
||||||
case _:
|
|
||||||
await self._send_message(f"未知组件类型: {component_type}")
|
target_component_type = type_mapping.get(component_type.lower())
|
||||||
return
|
if not target_component_type:
|
||||||
if component_manage_api.locally_disable_component(
|
await self.send_text(f"❌ 未知组件类型: `{component_type}`")
|
||||||
component_name,
|
return
|
||||||
target_component_type,
|
|
||||||
self.message.chat_stream.stream_id,
|
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_message(f"本地禁用组件成功: {component_name}")
|
await self.send_text(f"✅ 本地禁用组件成功: `{component_name}`")
|
||||||
else:
|
else:
|
||||||
await self._send_message(f"本地禁用组件失败: {component_name}")
|
await self.send_text(f"❌ 本地禁用组件失败: `{component_name}`")
|
||||||
|
|
||||||
async def _send_message(self, message: str):
|
|
||||||
await send_api.text_to_stream(message, self.stream_id, typing=False, storage_message=False)
|
|
||||||
|
|
||||||
|
|
||||||
@register_plugin
|
@register_plugin
|
||||||
@@ -441,14 +542,22 @@ class PluginManagementPlugin(BasePlugin):
|
|||||||
"plugin": {
|
"plugin": {
|
||||||
"enabled": ConfigField(bool, default=False, description="是否启用插件"),
|
"enabled": ConfigField(bool, default=False, description="是否启用插件"),
|
||||||
"config_version": ConfigField(type=str, default="1.1.0", 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 = []
|
components = []
|
||||||
if self.get_config("plugin.enabled", True):
|
if self.get_config("plugin.enabled", True):
|
||||||
components.append((ManagementCommand.get_command_info(), ManagementCommand))
|
components.append((ManagementCommand.get_plus_command_info(), ManagementCommand))
|
||||||
return components
|
return components
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[inner]
|
[inner]
|
||||||
version = "6.5.7"
|
version = "6.5.8"
|
||||||
|
|
||||||
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
||||||
#如果你想要修改配置文件,请递增version的值
|
#如果你想要修改配置文件,请递增version的值
|
||||||
@@ -89,6 +89,7 @@ learning_strength = 1.0
|
|||||||
|
|
||||||
[[expression.rules]]
|
[[expression.rules]]
|
||||||
chat_stream_id = "qq:1919810:group"
|
chat_stream_id = "qq:1919810:group"
|
||||||
|
group = "group_A"
|
||||||
use_expression = true
|
use_expression = true
|
||||||
learn_expression = true
|
learn_expression = true
|
||||||
learning_strength = 1.5
|
learning_strength = 1.5
|
||||||
@@ -100,29 +101,18 @@ use_expression = true
|
|||||||
learn_expression = false
|
learn_expression = false
|
||||||
learning_strength = 0.5
|
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的聊天通用设置
|
[chat] #MoFox-Bot的聊天通用设置
|
||||||
|
# 群聊聊天模式设置
|
||||||
|
group_chat_mode = "auto" # 群聊聊天模式:auto-自动切换,normal-强制普通模式,focus-强制专注模式
|
||||||
focus_value = 1
|
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 # 是否强制私聊进入专注模式,开启后私聊将始终保持专注状态
|
force_focus_private = false # 是否强制私聊进入专注模式,开启后私聊将始终保持专注状态
|
||||||
|
|
||||||
# 群聊聊天模式设置
|
|
||||||
group_chat_mode = "auto" # 群聊聊天模式:auto-自动切换,normal-强制普通模式,focus-强制专注模式
|
|
||||||
|
|
||||||
|
|
||||||
max_context_size = 25 # 上下文长度
|
max_context_size = 25 # 上下文长度
|
||||||
thinking_timeout = 40 # MoFox-Bot一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢)
|
thinking_timeout = 40 # MoFox-Bot一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢)
|
||||||
replyer_random_probability = 0.5 # 首要replyer模型被选择的概率
|
replyer_random_probability = 0.5 # 首要replyer模型被选择的概率
|
||||||
@@ -273,7 +263,7 @@ enable_vector_instant_memory = true # 是否启用基于向量的瞬时记忆
|
|||||||
memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ]
|
memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ]
|
||||||
|
|
||||||
[voice]
|
[voice]
|
||||||
enable_asr = false # 是否启用语音识别,启用后MoFox-Bot可以识别语音消息,启用该功能需要配置语音识别模型[model.voice]s
|
enable_asr = false # 是否启用语音识别,启用后MoFox-Bot可以识别语音消息,启用该功能需要配置语音识别模型[model.voice]
|
||||||
|
|
||||||
[lpmm_knowledge] # lpmm知识库配置
|
[lpmm_knowledge] # lpmm知识库配置
|
||||||
enable = false # 是否启用lpmm知识库
|
enable = false # 是否启用lpmm知识库
|
||||||
@@ -347,8 +337,6 @@ auto_install_timeout = 300
|
|||||||
# 是否使用PyPI镜像源(推荐,可加速下载)
|
# 是否使用PyPI镜像源(推荐,可加速下载)
|
||||||
use_mirror = true
|
use_mirror = true
|
||||||
mirror_url = "https://pypi.tuna.tsinghua.edu.cn/simple" # PyPI镜像源URL,如: "https://pypi.tuna.tsinghua.edu.cn/simple"
|
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"
|
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] # 视频分析配置
|
[video_analysis] # 视频分析配置
|
||||||
enable = true # 是否启用视频分析功能
|
enable = true # 是否启用视频分析功能
|
||||||
analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择)
|
analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择)
|
||||||
@@ -456,8 +427,8 @@ guidelines = """
|
|||||||
请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。
|
请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[wakeup_system]
|
[sleep_system]
|
||||||
enable = false #"是否启用唤醒度系统"
|
enable = false #"是否启用睡眠系统"
|
||||||
wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒"
|
wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒"
|
||||||
private_message_increment = 3.0 #"私聊消息增加的唤醒度"
|
private_message_increment = 3.0 #"私聊消息增加的唤醒度"
|
||||||
group_mention_increment = 2.0 #"群聊艾特增加的唤醒度"
|
group_mention_increment = 2.0 #"群聊艾特增加的唤醒度"
|
||||||
@@ -482,6 +453,21 @@ sleep_pressure_increment = 1.5
|
|||||||
sleep_pressure_decay_rate = 1.5
|
sleep_pressure_decay_rate = 1.5
|
||||||
insomnia_duration_minutes = 30 # 单次失眠状态的持续时间(分钟)
|
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] # 跨群聊上下文共享配置
|
[cross_context] # 跨群聊上下文共享配置
|
||||||
# 这是总开关,用于一键启用或禁用此功能
|
# 这是总开关,用于一键启用或禁用此功能
|
||||||
enable = false
|
enable = false
|
||||||
|
|||||||
Reference in New Issue
Block a user