Skip to content

Commit 573f990

Browse files
committed
optimize-plugin-tools
1 parent 2be12b4 commit 573f990

File tree

2 files changed

+150
-111
lines changed

2 files changed

+150
-111
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.idea/
22
.vscode/
3+
.DS_Store
34
venv/
45
.venv/
56
.python-version

backend/plugin/tools.py

Lines changed: 149 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import sys
77
import warnings
88

9+
from typing import Any
10+
911
import rtoml
1012

1113
from fastapi import APIRouter
@@ -17,156 +19,192 @@
1719

1820

1921
class PluginInjectError(Exception):
22+
"""插件注入错误"""
2023
pass
2124

2225

2326
def get_plugins() -> list[str]:
24-
"""获取插件"""
27+
"""获取插件列表"""
2528
plugin_packages = []
26-
29+
30+
# 遍历插件目录
2731
for item in os.listdir(PLUGIN_DIR):
2832
item_path = os.path.join(PLUGIN_DIR, item)
29-
30-
if os.path.isdir(item_path):
31-
if '__init__.py' in os.listdir(item_path):
32-
plugin_packages.append(item)
33-
33+
34+
# 检查是否为目录且包含 __init__.py 文件
35+
if os.path.isdir(item_path) and '__init__.py' in os.listdir(item_path):
36+
plugin_packages.append(item)
37+
3438
return plugin_packages
3539

3640

37-
def get_plugin_models() -> list:
41+
def get_plugin_models() -> list[type]:
3842
"""获取插件所有模型类"""
3943
classes = []
44+
45+
# 获取所有插件
4046
plugins = get_plugins()
47+
48+
# 遍历插件列表
4149
for plugin in plugins:
50+
# 导入插件的模型模块
4251
module_path = f'backend.plugin.{plugin}.model'
4352
module = import_module_cached(module_path)
53+
54+
# 获取模块中的所有类
4455
for name, obj in inspect.getmembers(module):
4556
if inspect.isclass(obj):
4657
classes.append(obj)
58+
4759
return classes
4860

4961

50-
def plugin_router_inject() -> None:
62+
def _load_plugin_config(plugin: str) -> dict[str, Any]:
63+
"""
64+
加载插件配置
65+
66+
:param plugin: 插件名称
5167
"""
52-
插件路由注入
68+
toml_path = os.path.join(PLUGIN_DIR, plugin, 'plugin.toml')
69+
if not os.path.exists(toml_path):
70+
raise PluginInjectError(f'插件 {plugin} 缺少 plugin.toml 配置文件,请检查插件是否合法')
71+
72+
with open(toml_path, 'r', encoding='utf-8') as f:
73+
return rtoml.load(f)
5374

54-
:return:
75+
76+
def _inject_extra_router(plugin: str, data: dict[str, Any]) -> None:
5577
"""
56-
plugins = get_plugins()
57-
for plugin in plugins:
58-
toml_path = os.path.join(PLUGIN_DIR, plugin, 'plugin.toml')
59-
if not os.path.exists(toml_path):
60-
raise PluginInjectError(f'插件 {plugin} 缺少 plugin.toml 配置文件,请检查插件是否合法')
61-
62-
# 获取 plugin.toml 配置
63-
with open(toml_path, 'r', encoding='utf-8') as f:
64-
data = rtoml.load(f)
65-
api = data.get('api', {})
66-
67-
# 非独立 app
68-
if api:
69-
app_include = data.get('app', {}).get('include', '')
70-
if not app_include:
71-
raise PluginInjectError(f'非独立 app 插件 {plugin} 配置文件存在错误,请检查')
72-
73-
# 插件中 API 路由文件的路径
74-
plugin_api_path = os.path.join(PLUGIN_DIR, plugin, 'api')
75-
if not os.path.exists(plugin_api_path):
76-
raise PluginInjectError(f'插件 {plugin} 缺少 api 目录,请检查插件文件是否完整')
77-
78-
# 将插件路由注入到对应模块的路由中
79-
for root, _, api_files in os.walk(plugin_api_path):
80-
for file in api_files:
81-
if file.endswith('.py') and file != '__init__.py':
82-
# 解析插件路由配置
83-
prefix = data.get('api', {}).get(f'{file[:-3]}', {}).get('prefix', '')
84-
tags = data.get('api', {}).get(f'{file[:-3]}', {}).get('tags', [])
85-
86-
# 获取插件路由模块
87-
file_path = os.path.join(root, file)
88-
path_to_module_str = os.path.relpath(file_path, PLUGIN_DIR).replace(os.sep, '.')[:-3]
89-
module_path = f'backend.plugin.{path_to_module_str}'
90-
try:
91-
module = import_module_cached(module_path)
92-
except PluginInjectError as e:
93-
raise PluginInjectError(f'导入非独立 app 插件 {plugin} 模块 {module_path} 失败:{e}') from e
94-
plugin_router = getattr(module, 'router', None)
95-
if not plugin_router:
96-
warnings.warn(
97-
f'非独立 app 插件 {plugin} 模块 {module_path} 中没有有效的 router,'
98-
'请检查插件文件是否完整',
99-
FutureWarning,
100-
)
101-
continue
102-
103-
# 获取源程序路由模块
104-
relative_path = os.path.relpath(root, plugin_api_path)
105-
target_module_path = f'backend.app.{app_include}.api.{relative_path.replace(os.sep, ".")}'
106-
try:
107-
target_module = import_module_cached(target_module_path)
108-
except PluginInjectError as e:
109-
raise PluginInjectError(f'导入源程序模块 {target_module_path} 失败:{e}') from e
110-
target_router = getattr(target_module, 'router', None)
111-
if not target_router or not isinstance(target_router, APIRouter):
112-
raise PluginInjectError(
113-
f'非独立 app 插件 {plugin} 模块 {module_path} 中没有有效的 router,'
114-
'请检查插件文件是否完整'
115-
)
116-
117-
# 将插件路由注入到目标 router 中
118-
target_router.include_router(
119-
router=plugin_router,
120-
prefix=prefix,
121-
tags=[tags] if tags else [],
122-
)
123-
# 独立 app
124-
else:
125-
# 将插件中的路由直接注入到总路由中
126-
module_path = f'backend.plugin.{plugin}.api.router'
78+
扩展级插件路由注入
79+
80+
:param plugin: 插件名称
81+
:param data: 插件配置数据
82+
"""
83+
app_include = data.get('app', {}).get('include', '')
84+
if not app_include:
85+
raise PluginInjectError(f'扩展级插件 {plugin} 配置文件存在错误,请检查')
86+
87+
plugin_api_path = os.path.join(PLUGIN_DIR, plugin, 'api')
88+
if not os.path.exists(plugin_api_path):
89+
raise PluginInjectError(f'插件 {plugin} 缺少 api 目录,请检查插件文件是否完整')
90+
91+
for root, _, api_files in os.walk(plugin_api_path):
92+
for file in api_files:
93+
if not (file.endswith('.py') and file != '__init__.py'):
94+
continue
95+
96+
file_config = data.get('api', {}).get(f'{file[:-3]}', {})
97+
prefix = file_config.get('prefix', '')
98+
tags = file_config.get('tags', [])
99+
100+
file_path = os.path.join(root, file)
101+
path_to_module_str = os.path.relpath(file_path, PLUGIN_DIR).replace(os.sep, '.')[:-3]
102+
module_path = f'backend.plugin.{path_to_module_str}'
103+
127104
try:
128105
module = import_module_cached(module_path)
129-
except PluginInjectError as e:
130-
raise PluginInjectError(f'导入独立 app 插件 {plugin} 模块 {module_path} 失败:{e}') from e
131-
routers = data.get('app', {}).get('router', [])
132-
if not routers or not isinstance(routers, list):
133-
raise PluginInjectError(f'独立 app 插件 {plugin} 配置文件存在错误,请检查')
134-
for router in routers:
135-
plugin_router = getattr(module, router, None)
136-
if not plugin_router or not isinstance(plugin_router, APIRouter):
137-
raise PluginInjectError(
138-
f'独立 app 插件 {plugin} 模块 {module_path} 中没有有效的 router,请检查插件文件是否完整'
106+
plugin_router = getattr(module, 'router', None)
107+
if not plugin_router:
108+
warnings.warn(
109+
f'扩展级插件 {plugin} 模块 {module_path} 中没有有效的 router,'
110+
'请检查插件文件是否完整',
111+
FutureWarning,
139112
)
140-
target_module_path = 'backend.app.router'
113+
continue
114+
115+
relative_path = os.path.relpath(root, plugin_api_path)
116+
target_module_path = f'backend.app.{app_include}.api.{relative_path.replace(os.sep, ".")}'
141117
target_module = import_module_cached(target_module_path)
142-
target_router = getattr(target_module, 'router')
118+
target_router = getattr(target_module, 'router', None)
119+
120+
if not target_router or not isinstance(target_router, APIRouter):
121+
raise PluginInjectError(
122+
f'扩展级插件 {plugin} 模块 {module_path} 中没有有效的 router,'
123+
'请检查插件文件是否完整'
124+
)
143125

144-
# 将插件路由注入到目标 router 中
145-
target_router.include_router(plugin_router)
126+
target_router.include_router(
127+
router=plugin_router,
128+
prefix=prefix,
129+
tags=[tags] if tags else [],
130+
)
131+
except Exception as e:
132+
raise PluginInjectError(f'注入扩展级插件 {plugin} 路由失败:{str(e)}') from e
133+
134+
135+
def _inject_app_router(plugin: str, data: dict[str, Any]) -> None:
136+
"""
137+
应用级插件路由注入
138+
139+
:param plugin: 插件名称
140+
:param data: 插件配置数据
141+
"""
142+
module_path = f'backend.plugin.{plugin}.api.router'
143+
try:
144+
module = import_module_cached(module_path)
145+
routers = data.get('app', {}).get('router', [])
146+
if not routers or not isinstance(routers, list):
147+
raise PluginInjectError(f'应用级插件 {plugin} 配置文件存在错误,请检查')
148+
149+
target_module = import_module_cached('backend.app.router')
150+
target_router = getattr(target_module, 'router')
151+
152+
for router in routers:
153+
plugin_router = getattr(module, router, None)
154+
if not plugin_router or not isinstance(plugin_router, APIRouter):
155+
raise PluginInjectError(
156+
f'应用级插件 {plugin} 模块 {module_path} 中没有有效的 router,请检查插件文件是否完整'
157+
)
158+
target_router.include_router(plugin_router)
159+
except Exception as e:
160+
raise PluginInjectError(f'注入应用级插件 {plugin} 路由失败:{str(e)}') from e
161+
162+
163+
def plugin_router_inject() -> None:
164+
"""插件路由注入"""
165+
for plugin in get_plugins():
166+
try:
167+
data = _load_plugin_config(plugin)
168+
# 基于插件 plugin.toml 配置文件,判断插件类型
169+
if data.get('api'):
170+
_inject_extra_router(plugin, data)
171+
else:
172+
_inject_app_router(plugin, data)
173+
except Exception as e:
174+
raise PluginInjectError(f'插件 {plugin} 路由注入失败:{str(e)}') from e
175+
176+
177+
def _install_plugin_requirements(plugin: str, requirements_file: str) -> None:
178+
"""
179+
安装单个插件的依赖
180+
181+
:param plugin: 插件名称
182+
:param requirements_file: 依赖文件路径
183+
"""
184+
try:
185+
ensurepip_install = [sys.executable, '-m', 'ensurepip', '--upgrade']
186+
pip_install = [sys.executable, '-m', 'pip', 'install', '-r', requirements_file]
187+
if settings.PLUGIN_PIP_CHINA:
188+
pip_install.extend(['-i', settings.PLUGIN_PIP_INDEX_URL])
189+
subprocess.check_call(ensurepip_install)
190+
subprocess.check_call(pip_install)
191+
except subprocess.CalledProcessError as e:
192+
raise PluginInjectError(f'插件 {plugin} 依赖安装失败:{e.stderr}') from e
146193

147194

148195
def install_requirements() -> None:
149196
"""安装插件依赖"""
150-
plugins = get_plugins()
151-
for plugin in plugins:
197+
for plugin in get_plugins():
152198
requirements_file = os.path.join(PLUGIN_DIR, plugin, 'requirements.txt')
153-
if not os.path.exists(requirements_file):
154-
continue
155-
else:
156-
try:
157-
ensurepip_install = [sys.executable, '-m', 'ensurepip', '--upgrade']
158-
pip_install = [sys.executable, '-m', 'pip', 'install', '-r', requirements_file]
159-
if settings.PLUGIN_PIP_CHINA:
160-
pip_install.extend(['-i', settings.PLUGIN_PIP_INDEX_URL])
161-
subprocess.check_call(ensurepip_install)
162-
subprocess.check_call(pip_install)
163-
except subprocess.CalledProcessError as e:
164-
raise PluginInjectError(f'插件 {plugin} 依赖安装失败:{e.stderr}') from e
199+
if os.path.exists(requirements_file):
200+
_install_plugin_requirements(plugin, requirements_file)
165201

166202

167203
async def install_requirements_async() -> None:
168204
"""
169-
异步安装插件依赖(由于 Windows 平台限制,无法实现完美的全异步方案),详情:
205+
异步安装插件依赖
206+
207+
由于 Windows 平台限制,无法实现完美的全异步方案,详情:
170208
https://stackoverflow.com/questions/44633458/why-am-i-getting-notimplementederror-with-async-and-await-on-windows
171209
"""
172210
await run_in_threadpool(install_requirements)

0 commit comments

Comments
 (0)