You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

plugin_service.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import logging
  2. from collections.abc import Mapping, Sequence
  3. from mimetypes import guess_type
  4. from typing import Optional
  5. from pydantic import BaseModel
  6. from configs import dify_config
  7. from core.helper import marketplace
  8. from core.helper.download import download_with_size_limit
  9. from core.helper.marketplace import download_plugin_pkg
  10. from core.plugin.entities.bundle import PluginBundleDependency
  11. from core.plugin.entities.plugin import (
  12. GenericProviderID,
  13. PluginDeclaration,
  14. PluginEntity,
  15. PluginInstallation,
  16. PluginInstallationSource,
  17. )
  18. from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginUploadResponse
  19. from core.plugin.manager.asset import PluginAssetManager
  20. from core.plugin.manager.debugging import PluginDebuggingManager
  21. from core.plugin.manager.plugin import PluginInstallationManager
  22. from extensions.ext_redis import redis_client
  23. logger = logging.getLogger(__name__)
  24. class PluginService:
  25. class LatestPluginCache(BaseModel):
  26. plugin_id: str
  27. version: str
  28. unique_identifier: str
  29. REDIS_KEY_PREFIX = "plugin_service:latest_plugin:"
  30. REDIS_TTL = 60 * 5 # 5 minutes
  31. @staticmethod
  32. def fetch_latest_plugin_version(plugin_ids: Sequence[str]) -> Mapping[str, Optional[LatestPluginCache]]:
  33. """
  34. Fetch the latest plugin version
  35. """
  36. result: dict[str, Optional[PluginService.LatestPluginCache]] = {}
  37. try:
  38. cache_not_exists = []
  39. # Try to get from Redis first
  40. for plugin_id in plugin_ids:
  41. cached_data = redis_client.get(f"{PluginService.REDIS_KEY_PREFIX}{plugin_id}")
  42. if cached_data:
  43. result[plugin_id] = PluginService.LatestPluginCache.model_validate_json(cached_data)
  44. else:
  45. cache_not_exists.append(plugin_id)
  46. if cache_not_exists:
  47. manifests = {
  48. manifest.plugin_id: manifest
  49. for manifest in marketplace.batch_fetch_plugin_manifests(cache_not_exists)
  50. }
  51. for plugin_id, manifest in manifests.items():
  52. latest_plugin = PluginService.LatestPluginCache(
  53. plugin_id=plugin_id,
  54. version=manifest.latest_version,
  55. unique_identifier=manifest.latest_package_identifier,
  56. )
  57. # Store in Redis
  58. redis_client.setex(
  59. f"{PluginService.REDIS_KEY_PREFIX}{plugin_id}",
  60. PluginService.REDIS_TTL,
  61. latest_plugin.model_dump_json(),
  62. )
  63. result[plugin_id] = latest_plugin
  64. # pop plugin_id from cache_not_exists
  65. cache_not_exists.remove(plugin_id)
  66. for plugin_id in cache_not_exists:
  67. result[plugin_id] = None
  68. return result
  69. except Exception:
  70. logger.exception("failed to fetch latest plugin version")
  71. return result
  72. @staticmethod
  73. def get_debugging_key(tenant_id: str) -> str:
  74. """
  75. get the debugging key of the tenant
  76. """
  77. manager = PluginDebuggingManager()
  78. return manager.get_debugging_key(tenant_id)
  79. @staticmethod
  80. def list_latest_versions(plugin_ids: Sequence[str]) -> Mapping[str, Optional[LatestPluginCache]]:
  81. """
  82. List the latest versions of the plugins
  83. """
  84. return PluginService.fetch_latest_plugin_version(plugin_ids)
  85. @staticmethod
  86. def list(tenant_id: str) -> list[PluginEntity]:
  87. """
  88. list all plugins of the tenant
  89. """
  90. manager = PluginInstallationManager()
  91. plugins = manager.list_plugins(tenant_id)
  92. return plugins
  93. @staticmethod
  94. def list_installations_from_ids(tenant_id: str, ids: Sequence[str]) -> Sequence[PluginInstallation]:
  95. """
  96. List plugin installations from ids
  97. """
  98. manager = PluginInstallationManager()
  99. return manager.fetch_plugin_installation_by_ids(tenant_id, ids)
  100. @staticmethod
  101. def get_asset(tenant_id: str, asset_file: str) -> tuple[bytes, str]:
  102. """
  103. get the asset file of the plugin
  104. """
  105. manager = PluginAssetManager()
  106. # guess mime type
  107. mime_type, _ = guess_type(asset_file)
  108. return manager.fetch_asset(tenant_id, asset_file), mime_type or "application/octet-stream"
  109. @staticmethod
  110. def check_plugin_unique_identifier(tenant_id: str, plugin_unique_identifier: str) -> bool:
  111. """
  112. check if the plugin unique identifier is already installed by other tenant
  113. """
  114. manager = PluginInstallationManager()
  115. return manager.fetch_plugin_by_identifier(tenant_id, plugin_unique_identifier)
  116. @staticmethod
  117. def fetch_plugin_manifest(tenant_id: str, plugin_unique_identifier: str) -> PluginDeclaration:
  118. """
  119. Fetch plugin manifest
  120. """
  121. manager = PluginInstallationManager()
  122. return manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
  123. @staticmethod
  124. def fetch_install_tasks(tenant_id: str, page: int, page_size: int) -> Sequence[PluginInstallTask]:
  125. """
  126. Fetch plugin installation tasks
  127. """
  128. manager = PluginInstallationManager()
  129. return manager.fetch_plugin_installation_tasks(tenant_id, page, page_size)
  130. @staticmethod
  131. def fetch_install_task(tenant_id: str, task_id: str) -> PluginInstallTask:
  132. manager = PluginInstallationManager()
  133. return manager.fetch_plugin_installation_task(tenant_id, task_id)
  134. @staticmethod
  135. def delete_install_task(tenant_id: str, task_id: str) -> bool:
  136. """
  137. Delete a plugin installation task
  138. """
  139. manager = PluginInstallationManager()
  140. return manager.delete_plugin_installation_task(tenant_id, task_id)
  141. @staticmethod
  142. def delete_all_install_task_items(
  143. tenant_id: str,
  144. ) -> bool:
  145. """
  146. Delete all plugin installation task items
  147. """
  148. manager = PluginInstallationManager()
  149. return manager.delete_all_plugin_installation_task_items(tenant_id)
  150. @staticmethod
  151. def delete_install_task_item(tenant_id: str, task_id: str, identifier: str) -> bool:
  152. """
  153. Delete a plugin installation task item
  154. """
  155. manager = PluginInstallationManager()
  156. return manager.delete_plugin_installation_task_item(tenant_id, task_id, identifier)
  157. @staticmethod
  158. def upgrade_plugin_with_marketplace(
  159. tenant_id: str, original_plugin_unique_identifier: str, new_plugin_unique_identifier: str
  160. ):
  161. """
  162. Upgrade plugin with marketplace
  163. """
  164. if original_plugin_unique_identifier == new_plugin_unique_identifier:
  165. raise ValueError("you should not upgrade plugin with the same plugin")
  166. # check if plugin pkg is already downloaded
  167. manager = PluginInstallationManager()
  168. try:
  169. manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier)
  170. # already downloaded, skip, and record install event
  171. marketplace.record_install_plugin_event(new_plugin_unique_identifier)
  172. except Exception:
  173. # plugin not installed, download and upload pkg
  174. pkg = download_plugin_pkg(new_plugin_unique_identifier)
  175. manager.upload_pkg(tenant_id, pkg, verify_signature=False)
  176. return manager.upgrade_plugin(
  177. tenant_id,
  178. original_plugin_unique_identifier,
  179. new_plugin_unique_identifier,
  180. PluginInstallationSource.Marketplace,
  181. {
  182. "plugin_unique_identifier": new_plugin_unique_identifier,
  183. },
  184. )
  185. @staticmethod
  186. def upgrade_plugin_with_github(
  187. tenant_id: str,
  188. original_plugin_unique_identifier: str,
  189. new_plugin_unique_identifier: str,
  190. repo: str,
  191. version: str,
  192. package: str,
  193. ):
  194. """
  195. Upgrade plugin with github
  196. """
  197. manager = PluginInstallationManager()
  198. return manager.upgrade_plugin(
  199. tenant_id,
  200. original_plugin_unique_identifier,
  201. new_plugin_unique_identifier,
  202. PluginInstallationSource.Github,
  203. {
  204. "repo": repo,
  205. "version": version,
  206. "package": package,
  207. },
  208. )
  209. @staticmethod
  210. def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginUploadResponse:
  211. """
  212. Upload plugin package files
  213. returns: plugin_unique_identifier
  214. """
  215. manager = PluginInstallationManager()
  216. return manager.upload_pkg(tenant_id, pkg, verify_signature)
  217. @staticmethod
  218. def upload_pkg_from_github(
  219. tenant_id: str, repo: str, version: str, package: str, verify_signature: bool = False
  220. ) -> PluginUploadResponse:
  221. """
  222. Install plugin from github release package files,
  223. returns plugin_unique_identifier
  224. """
  225. pkg = download_with_size_limit(
  226. f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE
  227. )
  228. manager = PluginInstallationManager()
  229. return manager.upload_pkg(
  230. tenant_id,
  231. pkg,
  232. verify_signature,
  233. )
  234. @staticmethod
  235. def upload_bundle(
  236. tenant_id: str, bundle: bytes, verify_signature: bool = False
  237. ) -> Sequence[PluginBundleDependency]:
  238. """
  239. Upload a plugin bundle and return the dependencies.
  240. """
  241. manager = PluginInstallationManager()
  242. return manager.upload_bundle(tenant_id, bundle, verify_signature)
  243. @staticmethod
  244. def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]):
  245. manager = PluginInstallationManager()
  246. return manager.install_from_identifiers(
  247. tenant_id,
  248. plugin_unique_identifiers,
  249. PluginInstallationSource.Package,
  250. [{}],
  251. )
  252. @staticmethod
  253. def install_from_github(tenant_id: str, plugin_unique_identifier: str, repo: str, version: str, package: str):
  254. """
  255. Install plugin from github release package files,
  256. returns plugin_unique_identifier
  257. """
  258. manager = PluginInstallationManager()
  259. return manager.install_from_identifiers(
  260. tenant_id,
  261. [plugin_unique_identifier],
  262. PluginInstallationSource.Github,
  263. [
  264. {
  265. "repo": repo,
  266. "version": version,
  267. "package": package,
  268. }
  269. ],
  270. )
  271. @staticmethod
  272. def install_from_marketplace_pkg(
  273. tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False
  274. ):
  275. """
  276. Install plugin from marketplace package files,
  277. returns installation task id
  278. """
  279. manager = PluginInstallationManager()
  280. # check if already downloaded
  281. for plugin_unique_identifier in plugin_unique_identifiers:
  282. try:
  283. manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
  284. # already downloaded, skip
  285. except Exception:
  286. # plugin not installed, download and upload pkg
  287. pkg = download_plugin_pkg(plugin_unique_identifier)
  288. manager.upload_pkg(tenant_id, pkg, verify_signature)
  289. return manager.install_from_identifiers(
  290. tenant_id,
  291. plugin_unique_identifiers,
  292. PluginInstallationSource.Marketplace,
  293. [
  294. {
  295. "plugin_unique_identifier": plugin_unique_identifier,
  296. }
  297. for plugin_unique_identifier in plugin_unique_identifiers
  298. ],
  299. )
  300. @staticmethod
  301. def uninstall(tenant_id: str, plugin_installation_id: str) -> bool:
  302. manager = PluginInstallationManager()
  303. return manager.uninstall(tenant_id, plugin_installation_id)
  304. @staticmethod
  305. def check_tools_existence(tenant_id: str, provider_ids: Sequence[GenericProviderID]) -> Sequence[bool]:
  306. """
  307. Check if the tools exist
  308. """
  309. manager = PluginInstallationManager()
  310. return manager.check_tools_existence(tenant_id, provider_ids)