|
6 | 6 | import sys |
7 | 7 | import warnings |
8 | 8 |
|
| 9 | +from typing import Any |
| 10 | + |
9 | 11 | import rtoml |
10 | 12 |
|
11 | 13 | from fastapi import APIRouter |
|
17 | 19 |
|
18 | 20 |
|
19 | 21 | class PluginInjectError(Exception): |
| 22 | + """插件注入错误""" |
20 | 23 | pass |
21 | 24 |
|
22 | 25 |
|
23 | 26 | def get_plugins() -> list[str]: |
24 | | - """获取插件""" |
| 27 | + """获取插件列表""" |
25 | 28 | plugin_packages = [] |
26 | | - |
| 29 | + |
| 30 | + # 遍历插件目录 |
27 | 31 | for item in os.listdir(PLUGIN_DIR): |
28 | 32 | 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 | + |
34 | 38 | return plugin_packages |
35 | 39 |
|
36 | 40 |
|
37 | | -def get_plugin_models() -> list: |
| 41 | +def get_plugin_models() -> list[type]: |
38 | 42 | """获取插件所有模型类""" |
39 | 43 | classes = [] |
| 44 | + |
| 45 | + # 获取所有插件 |
40 | 46 | plugins = get_plugins() |
| 47 | + |
| 48 | + # 遍历插件列表 |
41 | 49 | for plugin in plugins: |
| 50 | + # 导入插件的模型模块 |
42 | 51 | module_path = f'backend.plugin.{plugin}.model' |
43 | 52 | module = import_module_cached(module_path) |
| 53 | + |
| 54 | + # 获取模块中的所有类 |
44 | 55 | for name, obj in inspect.getmembers(module): |
45 | 56 | if inspect.isclass(obj): |
46 | 57 | classes.append(obj) |
| 58 | + |
47 | 59 | return classes |
48 | 60 |
|
49 | 61 |
|
50 | | -def plugin_router_inject() -> None: |
| 62 | +def _load_plugin_config(plugin: str) -> dict[str, Any]: |
| 63 | + """ |
| 64 | + 加载插件配置 |
| 65 | + |
| 66 | + :param plugin: 插件名称 |
51 | 67 | """ |
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) |
53 | 74 |
|
54 | | - :return: |
| 75 | + |
| 76 | +def _inject_extra_router(plugin: str, data: dict[str, Any]) -> None: |
55 | 77 | """ |
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 | + |
127 | 104 | try: |
128 | 105 | 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, |
139 | 112 | ) |
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, ".")}' |
141 | 117 | 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 | + ) |
143 | 125 |
|
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 |
146 | 193 |
|
147 | 194 |
|
148 | 195 | def install_requirements() -> None: |
149 | 196 | """安装插件依赖""" |
150 | | - plugins = get_plugins() |
151 | | - for plugin in plugins: |
| 197 | + for plugin in get_plugins(): |
152 | 198 | 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) |
165 | 201 |
|
166 | 202 |
|
167 | 203 | async def install_requirements_async() -> None: |
168 | 204 | """ |
169 | | - 异步安装插件依赖(由于 Windows 平台限制,无法实现完美的全异步方案),详情: |
| 205 | + 异步安装插件依赖 |
| 206 | + |
| 207 | + 由于 Windows 平台限制,无法实现完美的全异步方案,详情: |
170 | 208 | https://stackoverflow.com/questions/44633458/why-am-i-getting-notimplementederror-with-async-and-await-on-windows |
171 | 209 | """ |
172 | 210 | await run_in_threadpool(install_requirements) |
0 commit comments