mirror of
https://github.com/TriM-Organization/LiteyukiBot-TriM.git
synced 2024-11-28 16:24:51 +08:00
feat: 添加了自动安装插件功能
This commit is contained in:
parent
ca997f727a
commit
e24c5c912e
@ -1 +1,13 @@
|
|||||||
|
from src.utils.data import Database, LiteModel
|
||||||
|
from src.utils.data_manager import plugin_db
|
||||||
|
|
||||||
LNPM_COMMAND_START = "lnpm"
|
LNPM_COMMAND_START = "lnpm"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InstalledPlugin(LiteModel):
|
||||||
|
module_name: str
|
||||||
|
|
||||||
|
|
||||||
|
plugin_db.auto_migrate(InstalledPlugin)
|
||||||
|
@ -1,23 +1,27 @@
|
|||||||
import json
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
|
from io import StringIO
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
from arclet.alconna import Arparma, MultiVar
|
from arclet.alconna import Arparma, MultiVar
|
||||||
from nonebot.permission import SUPERUSER
|
from nonebot.permission import SUPERUSER
|
||||||
|
from nonebot.utils import run_sync
|
||||||
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand
|
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand
|
||||||
import pip
|
import pip
|
||||||
|
|
||||||
import aiohttp, aiofiles
|
import aiohttp, aiofiles
|
||||||
from typing_extensions import Any
|
from typing_extensions import Any
|
||||||
|
|
||||||
from src.utils.data import LiteModel
|
|
||||||
from src.utils.language import get_user_lang
|
from src.utils.language import get_user_lang
|
||||||
from src.utils.message import button, send_markdown
|
from src.utils.message import Markdown as md, send_markdown
|
||||||
from src.utils.resource import get_res
|
from src.utils.resource import get_res
|
||||||
from src.utils.typing import T_Bot, T_MessageEvent
|
from src.utils.typing import T_Bot, T_MessageEvent
|
||||||
|
|
||||||
|
from .common import *
|
||||||
|
|
||||||
npm_alc = on_alconna(
|
npm_alc = on_alconna(
|
||||||
Alconna(
|
Alconna(
|
||||||
"lnpm",
|
"lnpm",
|
||||||
@ -28,17 +32,17 @@ npm_alc = on_alconna(
|
|||||||
Subcommand(
|
Subcommand(
|
||||||
"search",
|
"search",
|
||||||
Args["keywords", MultiVar(str)]["page", int, 1],
|
Args["keywords", MultiVar(str)]["page", int, 1],
|
||||||
alias=["s"],
|
alias=["s", "搜索"],
|
||||||
),
|
),
|
||||||
Subcommand(
|
Subcommand(
|
||||||
"install",
|
"install",
|
||||||
Args["plugin_name", str],
|
Args["plugin_name", str],
|
||||||
alias=["i"],
|
alias=["i", "安装"],
|
||||||
),
|
),
|
||||||
Subcommand(
|
Subcommand(
|
||||||
"remove",
|
"remove",
|
||||||
Args["plugin_name", str],
|
Args["plugin_name", str],
|
||||||
alias=["rm"],
|
alias=["rm", "移除", "卸载"],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
permission=SUPERUSER
|
permission=SUPERUSER
|
||||||
@ -82,20 +86,49 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
|||||||
elif result.subcommands.get("search"):
|
elif result.subcommands.get("search"):
|
||||||
keywords: list[str] = result.subcommands["search"].args.get("keywords")
|
keywords: list[str] = result.subcommands["search"].args.get("keywords")
|
||||||
rs = await npm_search(keywords)
|
rs = await npm_search(keywords)
|
||||||
|
max_show = 20
|
||||||
if len(rs):
|
if len(rs):
|
||||||
reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***"
|
reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***"
|
||||||
for plugin in rs[:min(10, len(rs))]:
|
for plugin in rs[:min(max_show, len(rs))]:
|
||||||
reply += (f"\n{button(ulang.get('npm.install'), 'lnpm install %s' % plugin.module_name)} | **{plugin.name}**\n"
|
btn_install = md.button(ulang.get('npm.install'), 'lnpm install %s' % plugin.module_name)
|
||||||
|
link_page = md.link(ulang.get('npm.homepage'), plugin.homepage)
|
||||||
|
|
||||||
|
reply += (f"\n{btn_install} | **{plugin.name}**\n"
|
||||||
f"\n > **{plugin.desc}**\n"
|
f"\n > **{plugin.desc}**\n"
|
||||||
f"\n > {ulang.get('npm.author')}: {plugin.author} | [🔗{ulang.get('npm.homepage')}]({plugin.homepage})\n\n***\n")
|
f"\n > {ulang.get('npm.author')}: {plugin.author} | {link_page}\n\n***\n")
|
||||||
if len(rs) > 10:
|
if len(rs) > max_show:
|
||||||
reply += (f"\n{ulang.get('npm.too_many_results')}"
|
reply += f"\n{ulang.get('npm.too_many_results')}"
|
||||||
f"\n{button(ulang.get('npm.prev_page'), 'lnpm search %s %s' % (' '.join(keywords), 2))} | "
|
|
||||||
f"{button(ulang.get('npm.next_page'), 'lnpm search %s %s' % (' '.join(keywords), 2))}")
|
|
||||||
else:
|
else:
|
||||||
reply = ulang.get("npm.search_no_result")
|
reply = ulang.get("npm.search_no_result")
|
||||||
await send_markdown(reply, bot, event=event)
|
await send_markdown(reply, bot, event=event)
|
||||||
|
|
||||||
|
elif result.subcommands.get("install"):
|
||||||
|
plugin_name: str = result.subcommands["install"].args.get("plugin_name")
|
||||||
|
r, log = npm_install(plugin_name)
|
||||||
|
log = log.replace("\\", "/")
|
||||||
|
if r:
|
||||||
|
nonebot.load_plugin(plugin_name)
|
||||||
|
installed_plugin = InstalledPlugin(module_name=plugin_name)
|
||||||
|
store_plugin = await get_store_plugin(plugin_name)
|
||||||
|
plugin_db.save(installed_plugin)
|
||||||
|
await send_markdown(
|
||||||
|
f"**{ulang.get('npm.install_success', NAME=store_plugin.name)}**\n\n"
|
||||||
|
f"```\n{log}\n```",
|
||||||
|
bot,
|
||||||
|
event=event
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await send_markdown(
|
||||||
|
f"{ulang.get('npm.install_success', NAME=plugin_name)}\n\n"
|
||||||
|
f"```\n{log}\n```",
|
||||||
|
bot,
|
||||||
|
event=event
|
||||||
|
)
|
||||||
|
|
||||||
|
elif result.subcommands.get("remove"):
|
||||||
|
plugin_name: str = result.subcommands["remove"].args.get("plugin_name")
|
||||||
|
await npm_alc.finish(ulang.get("npm.remove_success"))
|
||||||
|
|
||||||
|
|
||||||
async def npm_update() -> bool:
|
async def npm_update() -> bool:
|
||||||
"""
|
"""
|
||||||
@ -107,7 +140,6 @@ async def npm_update() -> bool:
|
|||||||
url_list = [
|
url_list = [
|
||||||
"https://registry.nonebot.dev/plugins.json",
|
"https://registry.nonebot.dev/plugins.json",
|
||||||
]
|
]
|
||||||
# 用aiohttp请求json文件,成功就覆盖本地文件,否则尝试下一个url
|
|
||||||
for url in url_list:
|
for url in url_list:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as resp:
|
async with session.get(url) as resp:
|
||||||
@ -115,7 +147,6 @@ async def npm_update() -> bool:
|
|||||||
async with aiofiles.open("data/liteyuki/plugins.json", "wb") as f:
|
async with aiofiles.open("data/liteyuki/plugins.json", "wb") as f:
|
||||||
data = await resp.read()
|
data = await resp.read()
|
||||||
await f.write(data)
|
await f.write(data)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -150,10 +181,54 @@ async def npm_search(keywords: list[str]) -> list[StorePlugin]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def install(plugin_name) -> bool:
|
async def get_store_plugin(plugin_module_name: str) -> Optional[StorePlugin]:
|
||||||
try:
|
"""
|
||||||
pip.main(['install', plugin_name])
|
获取插件信息
|
||||||
return True
|
|
||||||
except Exception as e:
|
Args:
|
||||||
print(e)
|
plugin_module_name (str): 插件模块名
|
||||||
return False
|
|
||||||
|
Returns:
|
||||||
|
Optional[StorePlugin]: 插件信息
|
||||||
|
"""
|
||||||
|
async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f:
|
||||||
|
plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())]
|
||||||
|
for plugin in plugins:
|
||||||
|
if plugin.module_name == plugin_module_name:
|
||||||
|
return plugin
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def npm_install(plugin_module_name) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plugin_module_name:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[bool, str]:
|
||||||
|
|
||||||
|
"""
|
||||||
|
buffer = StringIO()
|
||||||
|
sys.stdout = buffer
|
||||||
|
sys.stderr = buffer
|
||||||
|
|
||||||
|
mirrors = [
|
||||||
|
"https://pypi.tuna.tsinghua.edu.cn/simple",
|
||||||
|
"https://pypi.org/simple",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 使用pip安装包,对每个镜像尝试一次,成功后返回值
|
||||||
|
success = False
|
||||||
|
for mirror in mirrors:
|
||||||
|
try:
|
||||||
|
result = pip.main(['install', plugin_module_name, "-i", mirror])
|
||||||
|
success = result == 0
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
success = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
sys.stderr = sys.__stderr__
|
||||||
|
|
||||||
|
return success, buffer.getvalue()
|
||||||
|
@ -2,7 +2,7 @@ import nonebot.plugin
|
|||||||
from nonebot import on_command
|
from nonebot import on_command
|
||||||
from nonebot.permission import SUPERUSER
|
from nonebot.permission import SUPERUSER
|
||||||
|
|
||||||
from src.utils.message import button, send_markdown
|
from src.utils.message import Markdown as md, send_markdown
|
||||||
from src.utils.typing import T_Bot, T_MessageEvent
|
from src.utils.typing import T_Bot, T_MessageEvent
|
||||||
from src.utils.language import get_user_lang
|
from src.utils.language import get_user_lang
|
||||||
|
|
||||||
@ -17,11 +17,11 @@ async def _(event: T_MessageEvent, bot: T_Bot):
|
|||||||
for plugin in nonebot.get_loaded_plugins():
|
for plugin in nonebot.get_loaded_plugins():
|
||||||
# 检查是否有 metadata 属性
|
# 检查是否有 metadata 属性
|
||||||
if plugin.metadata:
|
if plugin.metadata:
|
||||||
reply += (f"\n{button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} "
|
reply += (f"\n{md.button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} "
|
||||||
f"**{plugin.metadata.name}**\n"
|
f"**{plugin.metadata.name}**\n"
|
||||||
f"\n > {plugin.metadata.description}\n\n***\n")
|
f"\n > {plugin.metadata.description}\n\n***\n")
|
||||||
else:
|
else:
|
||||||
reply += (f"\n{button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} "
|
reply += (f"\n{md.button(lang.get('npm.help'), 'help %s' % plugin.name, False, False)} "
|
||||||
f"**{plugin.name}**\n"
|
f"**{plugin.name}**\n"
|
||||||
f"\n > {lang.get('npm.no_description')}\n\n***\n")
|
f"\n > {lang.get('npm.no_description')}\n\n***\n")
|
||||||
await send_markdown(reply, bot, event=event)
|
await send_markdown(reply, bot, event=event)
|
||||||
|
@ -22,7 +22,9 @@ npm.store_update_success=Plugin store data updated successfully
|
|||||||
npm.store_update_failed=Plugin store data update failed
|
npm.store_update_failed=Plugin store data update failed
|
||||||
npm.search_result=Search results
|
npm.search_result=Search results
|
||||||
npm.search_no_result=No result found
|
npm.search_no_result=No result found
|
||||||
npm.too_many_results=Too many results found
|
npm.too_many_results=Too many results found, please refine your search
|
||||||
|
npm.install_success={NAME} installed successfully
|
||||||
|
npm.install_failed={NAME} installation failed
|
||||||
npm.author=Author
|
npm.author=Author
|
||||||
npm.homepage=Homepage
|
npm.homepage=Homepage
|
||||||
npm.next_page=Next
|
npm.next_page=Next
|
||||||
|
@ -23,6 +23,8 @@ npm.store_update_failed=プラグインストアのデータの更新に失敗
|
|||||||
npm.search_result=検索結果
|
npm.search_result=検索結果
|
||||||
npm.search_no_result=検索結果がありません
|
npm.search_no_result=検索結果がありません
|
||||||
npm.too_many_results=検索結果が多すぎます。ページをめくってください
|
npm.too_many_results=検索結果が多すぎます。ページをめくってください
|
||||||
|
npm.install_success={NAME} が正常にインストールされました
|
||||||
|
npm.install_failed={NAME} のインストールに失敗しました
|
||||||
npm.author=著者
|
npm.author=著者
|
||||||
npm.homepage=ホームページ
|
npm.homepage=ホームページ
|
||||||
npm.next_page=次のページ
|
npm.next_page=次のページ
|
||||||
|
@ -22,7 +22,9 @@ npm.store_update_success=插件商店数据更新成功
|
|||||||
npm.store_update_failed=插件商店数据更新失败
|
npm.store_update_failed=插件商店数据更新失败
|
||||||
npm.search_result=搜索结果
|
npm.search_result=搜索结果
|
||||||
npm.search_no_result=无搜索结果
|
npm.search_no_result=无搜索结果
|
||||||
npm.too_many_results=搜索结果过多,请翻页查看
|
npm.too_many_results=搜索结果过多,请限制关键字
|
||||||
|
npm.install_success={NAME} 安装成功
|
||||||
|
npm.install_failed={NAME} 安装失败
|
||||||
npm.author=作者
|
npm.author=作者
|
||||||
npm.homepage=主页
|
npm.homepage=主页
|
||||||
npm.next_page=下一页
|
npm.next_page=下一页
|
||||||
|
@ -5,6 +5,7 @@ from src.utils.data import LiteModel, Database as DB
|
|||||||
DATA_PATH = "data/liteyuki"
|
DATA_PATH = "data/liteyuki"
|
||||||
|
|
||||||
user_db = DB(os.path.join(DATA_PATH, 'users.ldb'))
|
user_db = DB(os.path.join(DATA_PATH, 'users.ldb'))
|
||||||
|
plugin_db = DB(os.path.join(DATA_PATH, 'plugins.ldb'))
|
||||||
|
|
||||||
|
|
||||||
class User(LiteModel):
|
class User(LiteModel):
|
||||||
|
@ -73,16 +73,31 @@ async def send_markdown(markdown: str, bot: T_Bot, *, message_type: str = None,
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str:
|
class Markdown:
|
||||||
"""生成点击按钮
|
@staticmethod
|
||||||
Args:
|
def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str:
|
||||||
name: 按钮显示内容
|
"""生成点击按钮
|
||||||
cmd: 发送的命令,已在函数内url编码,不需要再次编码
|
Args:
|
||||||
reply: 是否以回复的方式发送消息
|
name: 按钮显示内容
|
||||||
enter: 自动发送消息则为True,否则填充到输入框
|
cmd: 发送的命令,已在函数内url编码,不需要再次编码
|
||||||
|
reply: 是否以回复的方式发送消息
|
||||||
|
enter: 自动发送消息则为True,否则填充到输入框
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
markdown格式的可点击回调按钮
|
markdown格式的可点击回调按钮
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return f"[{name}](mqqapi://aio/inlinecmd?command={encode_url(cmd)}&reply={str(reply).lower()}&enter={str(enter).lower()})"
|
return f"[{name}](mqqapi://aio/inlinecmd?command={encode_url(cmd)}&reply={str(reply).lower()}&enter={str(enter).lower()})"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def link(name: str, url: str) -> str:
|
||||||
|
"""生成链接
|
||||||
|
Args:
|
||||||
|
name: 链接显示内容
|
||||||
|
url: 链接地址
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
markdown格式的链接
|
||||||
|
|
||||||
|
"""
|
||||||
|
return f"[链接{name}]({encode_url(url)})"
|
||||||
|
Loading…
Reference in New Issue
Block a user