-
Notifications
You must be signed in to change notification settings - Fork 605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
自动更新模板文件夹 #1677
自动更新模板文件夹 #1677
Conversation
- 扩展插件添加、移除和更新功能,支持使用插件ID或模块名 - 增加更新全部插件的功能 - 优化插件商店的命令使用说明 - 修复了一些与插件模块名相关的逻辑问题
- 修复了插件更新函数中的条件判断逻辑
调整了插件更新通知的文本格式,去掉了多余的换行符,使消息内容更加紧凑和清晰。
- 将插件ID和模块名的检查逻辑移至单独的私有方法 _resolve_plugin_key - 简化了 get_info 和 update_plugin 方法中的逻辑 - 提高了代码的可读性和可维护性
简化了ShopManage类中查询插件信息的逻辑。通过新增的_resolve_plugin_key类方法来解析插件ID或模块名,如果解析失败则捕获ValueError异常并返回错误信息。这样可以更清晰地处理插件查询逻辑,并避免冗余代码。
更新全部插件功能中,移除了日志记录中的f-string,简化了日志消息的格式。这个更改可能是为了统一日志记录的风格或者减少不必要的字符串格式化操作。
- 在帮助页面中使用配置的BOT名称替代硬编码的"真寻BOT" - 更新自动更新配置,将资源模板文件夹加入替换列表 - 修改帮助插件以传递BOT名称到模板 - 修复签到插件中的商品描述拼写错误
审核指南由 Sourcery 提供此拉取请求对 Zhenxun Bot 项目进行了多项改进和更改。更改包括配置处理的更新、各种插件的重构、Web UI 的改进以及机器人核心功能的增强。 更新的 ShopManage 类图classDiagram
class ShopManage {
+get_data() dict[str, StorePluginInfo]
+version_check(plugin_info: StorePluginInfo, suc_plugin: dict[str, str]) str
+check_version_is_new(plugin_info: StorePluginInfo, suc_plugin: dict[str, str]) bool
+get_loaded_plugins(*args) list[tuple[str, str]]
+add_plugin(plugin_id: int | str) str
+install_plugin_with_repo(github_url: str, module_path: str, is_dir: bool, is_external: bool = False)
+remove_plugin(plugin_id: int | str) str
+update_plugin(plugin_id: int | str) str
+update_all_plugin() str
+_resolve_plugin_key(plugin_id: int | str) str
}
更新的 AsyncHttpx 类图classDiagram
class AsyncHttpx {
+get(url: str | list[str], params: dict[str, Any] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, timeout: int, **kwargs) Response
+_get_first_successful(urls: list[str], **kwargs) Response
+_get_single(url: str, params: dict[str, Any] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, timeout: int, **kwargs) Response
+head(url: str, params: dict[str, Any] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, timeout: int, **kwargs) Response
+post(url: str, data: dict[str, Any] | None, content: Any, files: Any, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, json: dict[str, Any] | None, params: dict[str, str] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, timeout: int, **kwargs) Response
+download_file(url: str | list[str], path: str | Path, params: dict[str, str] | None, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, timeout: int, stream: bool, follow_redirects: bool, **kwargs) bool
+gather_download_file(url_list: list[str] | list[list[str]], path_list: list[str | Path], limit_async_number: int | None, params: dict[str, str] | None, use_proxy: bool, proxy: dict[str, str] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, timeout: int, **kwargs) list[bool]
+get_fastest_mirror(url_list: list[str]) list[str]
}
更新的 PluginManage 类图classDiagram
class PluginManage {
+set_default_status(plugin_name: str, status: bool) str
+set_all_plugin_status(status: bool, default_status: bool, group_id: str | None) str
+_change_group_task(group_id: str, task_name: str, status: bool) str
+_change_group_plugin(group_id: str, plugin_name: str, status: bool) str
+superuser_task_handle(task_name: str, status: bool, group_id: str | None) str
+superuser_block(plugin_name: str, block_type: BlockType | None, group_id: str | None) str
+superuser_unblock(plugin_name: str, block_type: BlockType | None, group_id: str | None) str
}
文件级更改
提示和命令与 Sourcery 互动
自定义您的体验访问您的仪表板以:
获取帮助Original review guide in EnglishReviewer's Guide by SourceryThis pull request implements several improvements and changes to the Zhenxun Bot project. The changes include updates to configuration handling, refactoring of various plugins, improvements to the web UI, and enhancements to the bot's core functionality. Updated class diagram for ShopManageclassDiagram
class ShopManage {
+get_data() dict[str, StorePluginInfo]
+version_check(plugin_info: StorePluginInfo, suc_plugin: dict[str, str]) str
+check_version_is_new(plugin_info: StorePluginInfo, suc_plugin: dict[str, str]) bool
+get_loaded_plugins(*args) list[tuple[str, str]]
+add_plugin(plugin_id: int | str) str
+install_plugin_with_repo(github_url: str, module_path: str, is_dir: bool, is_external: bool = False)
+remove_plugin(plugin_id: int | str) str
+update_plugin(plugin_id: int | str) str
+update_all_plugin() str
+_resolve_plugin_key(plugin_id: int | str) str
}
Updated class diagram for AsyncHttpxclassDiagram
class AsyncHttpx {
+get(url: str | list[str], params: dict[str, Any] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, timeout: int, **kwargs) Response
+_get_first_successful(urls: list[str], **kwargs) Response
+_get_single(url: str, params: dict[str, Any] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, timeout: int, **kwargs) Response
+head(url: str, params: dict[str, Any] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, timeout: int, **kwargs) Response
+post(url: str, data: dict[str, Any] | None, content: Any, files: Any, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, json: dict[str, Any] | None, params: dict[str, str] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, timeout: int, **kwargs) Response
+download_file(url: str | list[str], path: str | Path, params: dict[str, str] | None, verify: bool, use_proxy: bool, proxy: dict[str, str] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, timeout: int, stream: bool, follow_redirects: bool, **kwargs) bool
+gather_download_file(url_list: list[str] | list[list[str]], path_list: list[str | Path], limit_async_number: int | None, params: dict[str, str] | None, use_proxy: bool, proxy: dict[str, str] | None, headers: dict[str, str] | None, cookies: dict[str, str] | None, timeout: int, **kwargs) list[bool]
+get_fastest_mirror(url_list: list[str]) list[str]
}
Updated class diagram for PluginManageclassDiagram
class PluginManage {
+set_default_status(plugin_name: str, status: bool) str
+set_all_plugin_status(status: bool, default_status: bool, group_id: str | None) str
+_change_group_task(group_id: str, task_name: str, status: bool) str
+_change_group_plugin(group_id: str, plugin_name: str, status: bool) str
+superuser_task_handle(task_name: str, status: bool, group_id: str | None) str
+superuser_block(plugin_name: str, block_type: BlockType | None, group_id: str | None) str
+superuser_unblock(plugin_name: str, block_type: BlockType | None, group_id: str | None) str
}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
嗨 @molanp - 我已经审查了你的更改,发现了一些需要解决的问题。
阻塞问题:
- URL 路径中可能存在硬编码的秘密。(链接)
这是我在审查期间查看的内容
- 🟡 一般问题:发现 8 个问题
- 🔴 安全性:1 个阻塞问题
- 🟡 测试:发现 8 个问题
- 🟡 复杂性:发现 1 个问题
- 🟢 文档:一切看起来都很好
帮助我变得更有用!请在每条评论上点击 👍 或 👎,我将使用反馈来改进你的审查。
Original comment in English
Hey @molanp - I've reviewed your changes and found some issues that need to be addressed.
Blocking issues:
- Potential hard-coded secret in URL path. (link)
Here's what I looked at during the review
- 🟡 General issues: 8 issues found
- 🔴 Security: 1 blocking issue
- 🟡 Testing: 8 issues found
- 🟡 Complexity: 1 issue found
- 🟢 Documentation: all looks good
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
zhenxun/utils/http_utils.py
Outdated
@@ -295,6 +393,39 @@ async def gather_download_file( | |||
tasks.clear() | |||
return result_ | |||
|
|||
@classmethod | |||
async def get_fastest_mirror(cls, url_list: list[str]) -> list[str]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议: 添加更详细的注释以解释逻辑
此方法包含复杂的逻辑。考虑添加更详细的注释,解释它如何确定最快的镜像以及在实现中为何做出某些决策。
@classmethod
async def get_fastest_mirror(cls, url_list: list[str]) -> list[str]:
"""
从给定的 URL 列表中确定并返回最快的镜像。
此方法测试每个 URL 的响应时间并将其从快到慢排序。它处理连接错误和超时。
"""
assert url_list
Original comment in English
suggestion: Add more detailed comments explaining the logic
This method contains complex logic. Consider adding more detailed comments explaining how it determines the fastest mirror and why certain decisions were made in the implementation.
@classmethod
async def get_fastest_mirror(cls, url_list: list[str]) -> list[str]:
"""
Determine and return the fastest mirror from the given URL list.
This method tests the response time of each URL and sorts them
from fastest to slowest. It handles connection errors and timeouts.
"""
assert url_list
GROUP_HELP_PATH = DATA_PATH / "group_help" | ||
|
||
|
||
def delete_help_image(gid: str | None = None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议: 考虑为文件操作添加错误处理
文件操作可能由于各种原因失败(例如,权限、磁盘已满)。考虑添加 try-except 块以处理潜在的异常并适当地记录错误。
def delete_help_image(gid: str | None = None):
try:
# 现有函数实现
pass
except OSError as e:
logger.error(f"删除帮助图片时出错,群组 {gid}: {e}")
except Exception as e:
logger.error(f"删除帮助图片时出现意外错误,群组 {gid}: {e}")
Original comment in English
suggestion: Consider adding error handling for file operations
File operations can fail for various reasons (e.g., permissions, disk full). Consider adding try-except blocks to handle potential exceptions and log errors appropriately.
def delete_help_image(gid: str | None = None):
try:
# Existing function implementation here
pass
except OSError as e:
logger.error(f"Error deleting help image for group {gid}: {e}")
except Exception as e:
logger.error(f"Unexpected error deleting help image for group {gid}: {e}")
@@ -51,35 +50,36 @@ async def rank( | |||
"user_id", flat=True | |||
) | |||
query = query.filter(user_id__in=user_list) | |||
all_list = ( | |||
user_list = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议(性能): 考虑为初始查询添加限制以防止大数据集的潜在内存问题。
虽然一次性获取所有数据对于较小的数据集可能更高效,但对于非常大的用户群可能会导致内存问题。考虑在查询中添加 LIMIT 子句,并在需要时实现分页。这在内存使用和数据库查询频率之间进行权衡。
user_list = ( | |
user_list = ( | |
await query.annotate() | |
.order_by("-impression") | |
.limit(1000) # 添加限制以防止潜在的内存问题 | |
.values_list("user_id", "impression", "sign_count", "platform") | |
) | |
user_id_list = [user[0] for user in user_list] | |
index = user_id_list.index(user_id) + 1 | |
user_list = user_list[:num] if num < len(user_list) else user_list |
Original comment in English
suggestion (performance): Consider adding a limit to the initial query to prevent potential memory issues with large datasets.
While fetching all data at once can be more efficient for smaller datasets, it might cause memory problems for very large user bases. Consider adding a LIMIT clause to the query and implementing pagination if needed. This trades off between memory usage and database query frequency.
user_list = ( | |
user_list = ( | |
await query.annotate() | |
.order_by("-impression") | |
.limit(1000) # Add a limit to prevent potential memory issues | |
.values_list("user_id", "impression", "sign_count", "platform") | |
) | |
user_id_list = [user[0] for user in user_list] | |
index = user_id_list.index(user_id) + 1 | |
user_list = user_list[:num] if num < len(user_list) else user_list |
@@ -38,6 +40,7 @@ async def _(bot_id: str | None = None) -> Result: | |||
返回: | |||
Result: 获取指定bot信息与bot列表 | |||
""" | |||
global run_time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议: 避免使用全局变量;考虑替代的设计模式。
使用全局变量可能导致可维护性问题,并使代码更难以理解。考虑将 run_time 作为参数传递,或者如果需要在多个函数中共享,将其封装在类或模块级变量中。
class RuntimeManager:
_instance = None
run_time = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
@classmethod
def get_run_time(cls):
return cls.get_instance().run_time
@classmethod
def set_run_time(cls, value):
cls.get_instance().run_time = value
Original comment in English
suggestion: Avoid using global variables; consider alternative design patterns.
Using global variables can lead to maintainability issues and make the code harder to reason about. Consider passing the run_time as a parameter or encapsulating it within a class or module-level variable if it needs to be shared across multiple functions.
class RuntimeManager:
_instance = None
run_time = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
@classmethod
def get_run_time(cls):
return cls.get_instance().run_time
@classmethod
def set_run_time(cls, value):
cls.get_instance().run_time = value
@@ -21,6 +23,8 @@ | |||
).dict(), | |||
) | |||
|
|||
TEMP_LIST = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议(bug风险): 考虑为 TEMP_LIST 使用更线程安全的数据结构。
在多线程环境中,使用简单的列表作为 TEMP_LIST 可能导致竞争条件。考虑使用线程安全的数据结构,如 collections.deque 和 threading.Lock,或者如果更适合生产者-消费者模式,则使用 queue.Queue。
TEMP_LIST = [] | |
from collections import deque | |
from threading import Lock | |
TEMP_LIST = deque() | |
TEMP_LIST_LOCK = Lock() |
Original comment in English
suggestion (bug_risk): Consider using a more thread-safe data structure for TEMP_LIST.
Using a simple list for TEMP_LIST might lead to race conditions in a multi-threaded environment. Consider using a thread-safe data structure like collections.deque with a threading.Lock, or queue.Queue if producer-consumer pattern is more appropriate.
TEMP_LIST = [] | |
from collections import deque | |
from threading import Lock | |
TEMP_LIST = deque() | |
TEMP_LIST_LOCK = Lock() |
result=None, | ||
bot=bot, | ||
) | ||
assert mocked_api["basic_plugins"].called |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议(测试): 考虑为插件移除添加更多断言
测试验证了插件文件被移除,但也可以检查插件不再存在于数据库或任何其他相关数据结构中。
assert not (mock_base_path / "plugins" / "search_image").exists()
from zhenxun.builtin_plugins.plugin_store.data_source import get_plugin_list
plugin_list = await get_plugin_list()
assert not any(plugin['id'] == plugin_id for plugin in plugin_list)
Original comment in English
suggestion (testing): Consider adding more assertions for plugin removal
The test verifies that the plugin file is removed, but it could also check that the plugin is no longer in the database or any other relevant data structures.
assert not (mock_base_path / "plugins" / "search_image").exists()
from zhenxun.builtin_plugins.plugin_store.data_source import get_plugin_list
plugin_list = await get_plugin_list()
assert not any(plugin['id'] == plugin_id for plugin in plugin_list)
mock_build_message_return.send.assert_awaited_once() | ||
|
||
assert mocked_api["basic_plugins"].called | ||
assert mocked_api["extra_plugins"].called |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议(测试): 为插件商店中的错误处理添加测试
当前测试仅涵盖成功场景。考虑添加一个测试用例,以应对 API 调用失败或返回意外数据的情况。
async def test_plugin_store_error_handling(
app: App,
mocker: MockerFixture,
mocked_api: MockRouter,
create_bot: Callable,
tmp_path: Path,
) -> None:
mocked_api["basic_plugins"].side_effect = Exception("API Error")
mocked_api["extra_plugins"].side_effect = Exception("API Error")
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
event = _v11_group_message_event(message="插件商店", to_me=True)
await ctx.receive_event(bot=bot, event=event)
assert "获取插件列表失败" in str(ctx.call_next.send.call_args)
Original comment in English
suggestion (testing): Add test for error handling in plugin store
The current test only covers the successful scenario. Consider adding a test case for when the API calls fail or return unexpected data.
async def test_plugin_store_error_handling(
app: App,
mocker: MockerFixture,
mocked_api: MockRouter,
create_bot: Callable,
tmp_path: Path,
) -> None:
mocked_api["basic_plugins"].side_effect = Exception("API Error")
mocked_api["extra_plugins"].side_effect = Exception("API Error")
async with app.test_matcher(_matcher) as ctx:
bot = create_bot(ctx)
event = _v11_group_message_event(message="插件商店", to_me=True)
await ctx.receive_event(bot=bot, event=event)
assert "获取插件列表失败" in str(ctx.call_next.send.call_args)
bot=bot, | ||
) | ||
assert mocked_api["basic_plugins"].called | ||
assert mocked_api["extra_plugins"].called |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议(测试): 为部分更新场景添加测试
考虑添加一个测试用例,其中一些插件成功更新,而其他插件失败。这将测试更新过程的错误处理和报告。
async def test_update_all_plugin_partial_success(
app: App,
mocker: MockerFixture,
mocked_api: MockRouter,
create_bot: Callable,
tmp_path: Path,
) -> None:
# 设置代码类似于 test_update_all_plugin_basic_need_update
# ...
# 模拟部分成功:一个插件更新,一个失败
mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.update_plugin",
side_effect=[True, Exception("Update failed")],
)
# 测试执行和断言
# ...
Original comment in English
suggestion (testing): Add test for partial update scenario
Consider adding a test case where some plugins are updated successfully while others fail. This would test the error handling and reporting of the update process.
async def test_update_all_plugin_partial_success(
app: App,
mocker: MockerFixture,
mocked_api: MockRouter,
create_bot: Callable,
tmp_path: Path,
) -> None:
# Setup code similar to test_update_all_plugin_basic_need_update
# ...
# Mock partial success: one plugin updates, one fails
mocker.patch(
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.update_plugin",
side_effect=[True, Exception("Update failed")],
)
# Test execution and assertions
# ...
router = APIRouter(prefix="/dashboard") | ||
|
||
|
||
@router.get( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
问题(复杂性): 考虑重构通用查询逻辑为可重用函数以降低复杂性。
虽然当前结构为特定查询提供了清晰、独立的端点,但我们可以通过一些有针对性的更改来降低复杂性并提高可维护性:
- 将通用查询逻辑提取为可重用函数:
def get_filtered_query(model, bot_id=None):
query = model
if bot_id:
query = query.filter(bot_id=bot_id)
return query
def get_count_for_timeframe(query, days=0, hours=0, minutes=0):
now = datetime.now()
return query.filter(
create_time__gte=now - timedelta(days=days, hours=hours, minutes=minutes)
).count()
- 使用这些函数简化端点逻辑:
@router.get("/get_chat_and_call_count")
async def get_chat_and_call_count(bot_id: str | None = None) -> Result:
chat_query = get_filtered_query(ChatHistory, bot_id)
call_query = get_filtered_query(Statistics, bot_id)
return Result.ok(
QueryChatCallCount(
chat_num=await chat_query.count(),
chat_day=await get_count_for_timeframe(chat_query),
call_num=await call_query.count(),
call_day=await get_count_for_timeframe(call_query)
)
)
- 考虑为这些仪表板查询创建一个单独的文件,例如
dashboard_queries.py
,以改善组织。
这些更改将减少代码重复并提高可维护性,同时保留每个端点的清晰性和特定目的。提取的函数可以在多个端点中重用,使得将来添加或修改查询更容易。
Original comment in English
issue (complexity): Consider refactoring common query logic into reusable functions to reduce complexity.
While the current structure provides clear, separate endpoints for specific queries, we can reduce complexity and improve maintainability with a few targeted changes:
- Extract common query logic into reusable functions:
def get_filtered_query(model, bot_id=None):
query = model
if bot_id:
query = query.filter(bot_id=bot_id)
return query
def get_count_for_timeframe(query, days=0, hours=0, minutes=0):
now = datetime.now()
return query.filter(
create_time__gte=now - timedelta(days=days, hours=hours, minutes=minutes)
).count()
- Use these functions to simplify endpoint logic:
@router.get("/get_chat_and_call_count")
async def get_chat_and_call_count(bot_id: str | None = None) -> Result:
chat_query = get_filtered_query(ChatHistory, bot_id)
call_query = get_filtered_query(Statistics, bot_id)
return Result.ok(
QueryChatCallCount(
chat_num=await chat_query.count(),
chat_day=await get_count_for_timeframe(chat_query),
call_num=await call_query.count(),
call_day=await get_count_for_timeframe(call_query)
)
)
- Consider creating a separate file for these dashboard queries, e.g.,
dashboard_queries.py
, to improve organization.
These changes will reduce code duplication and improve maintainability while preserving the clarity and specific purpose of each endpoint. The extracted functions can be reused across multiple endpoints, making it easier to add or modify queries in the future.
@@ -20,16 +20,24 @@ async def favicon(): | |||
return FileResponse(PUBLIC_PATH / "favicon.ico") | |||
|
|||
|
|||
@router.get("/79edfa81f3308a9f.jfif") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚨 问题(安全性): URL 路径中可能存在硬编码的秘密。
URL 路径 '/79edfa81f3308a9f.jfif' 似乎是一个硬编码的值。如果这是一个敏感的标识符或密钥,考虑使用更安全的方法来处理它。
Original comment in English
🚨 issue (security): Potential hard-coded secret in URL path.
The URL path '/79edfa81f3308a9f.jfif' appears to be a hard-coded value. If this is a sensitive identifier or key, consider using a more secure method to handle it.
Summary by Sourcery
更新项目以支持模板文件夹的自动更新,修复登录插件中的拼写错误,并通过使用可配置的机器人名称增强帮助页面。
新功能:
错误修复:
增强功能:
Original summary in English
Summary by Sourcery
Update the project to support automatic updates of the template folder, fix a typo in the sign-in plugin, and enhance the help page by using a configurable bot name.
New Features:
Bug Fixes:
Enhancements: