plugin_service.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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(tenant_id: str) -> list[PluginEntity]:
  81. """
  82. list all plugins of the tenant
  83. """
  84. manager = PluginInstallationManager()
  85. plugins = manager.list_plugins(tenant_id)
  86. plugin_ids = [plugin.plugin_id for plugin in plugins if plugin.source == PluginInstallationSource.Marketplace]
  87. try:
  88. manifests = PluginService.fetch_latest_plugin_version(plugin_ids)
  89. except Exception:
  90. manifests = {}
  91. logger.exception("failed to fetch plugin manifests")
  92. for plugin in plugins:
  93. if plugin.source == PluginInstallationSource.Marketplace:
  94. if plugin.plugin_id in manifests:
  95. latest_plugin_cache = manifests[plugin.plugin_id]
  96. if latest_plugin_cache:
  97. # set latest_version
  98. plugin.latest_version = latest_plugin_cache.version
  99. plugin.latest_unique_identifier = latest_plugin_cache.unique_identifier
  100. return plugins
  101. @staticmethod
  102. def list_installations_from_ids(tenant_id: str, ids: Sequence[str]) -> Sequence[PluginInstallation]:
  103. """
  104. List plugin installations from ids
  105. """
  106. manager = PluginInstallationManager()
  107. return manager.fetch_plugin_installation_by_ids(tenant_id, ids)
  108. @staticmethod
  109. def get_asset(tenant_id: str, asset_file: str) -> tuple[bytes, str]:
  110. """
  111. get the asset file of the plugin
  112. """
  113. manager = PluginAssetManager()
  114. # guess mime type
  115. mime_type, _ = guess_type(asset_file)
  116. return manager.fetch_asset(tenant_id, asset_file), mime_type or "application/octet-stream"
  117. @staticmethod
  118. def check_plugin_unique_identifier(tenant_id: str, plugin_unique_identifier: str) -> bool:
  119. """
  120. check if the plugin unique identifier is already installed by other tenant
  121. """
  122. manager = PluginInstallationManager()
  123. return manager.fetch_plugin_by_identifier(tenant_id, plugin_unique_identifier)
  124. @staticmethod
  125. def fetch_plugin_manifest(tenant_id: str, plugin_unique_identifier: str) -> PluginDeclaration:
  126. """
  127. Fetch plugin manifest
  128. """
  129. manager = PluginInstallationManager()
  130. return manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
  131. @staticmethod
  132. def fetch_install_tasks(tenant_id: str, page: int, page_size: int) -> Sequence[PluginInstallTask]:
  133. """
  134. Fetch plugin installation tasks
  135. """
  136. manager = PluginInstallationManager()
  137. return manager.fetch_plugin_installation_tasks(tenant_id, page, page_size)
  138. @staticmethod
  139. def fetch_install_task(tenant_id: str, task_id: str) -> PluginInstallTask:
  140. manager = PluginInstallationManager()
  141. return manager.fetch_plugin_installation_task(tenant_id, task_id)
  142. @staticmethod
  143. def delete_install_task(tenant_id: str, task_id: str) -> bool:
  144. """
  145. Delete a plugin installation task
  146. """
  147. manager = PluginInstallationManager()
  148. return manager.delete_plugin_installation_task(tenant_id, task_id)
  149. @staticmethod
  150. def delete_all_install_task_items(
  151. tenant_id: str,
  152. ) -> bool:
  153. """
  154. Delete all plugin installation task items
  155. """
  156. manager = PluginInstallationManager()
  157. return manager.delete_all_plugin_installation_task_items(tenant_id)
  158. @staticmethod
  159. def delete_install_task_item(tenant_id: str, task_id: str, identifier: str) -> bool:
  160. """
  161. Delete a plugin installation task item
  162. """
  163. manager = PluginInstallationManager()
  164. return manager.delete_plugin_installation_task_item(tenant_id, task_id, identifier)
  165. @staticmethod
  166. def upgrade_plugin_with_marketplace(
  167. tenant_id: str, original_plugin_unique_identifier: str, new_plugin_unique_identifier: str
  168. ):
  169. """
  170. Upgrade plugin with marketplace
  171. """
  172. if original_plugin_unique_identifier == new_plugin_unique_identifier:
  173. raise ValueError("you should not upgrade plugin with the same plugin")
  174. # check if plugin pkg is already downloaded
  175. manager = PluginInstallationManager()
  176. try:
  177. manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier)
  178. # already downloaded, skip, and record install event
  179. marketplace.record_install_plugin_event(new_plugin_unique_identifier)
  180. except Exception:
  181. # plugin not installed, download and upload pkg
  182. pkg = download_plugin_pkg(new_plugin_unique_identifier)
  183. manager.upload_pkg(tenant_id, pkg, verify_signature=False)
  184. return manager.upgrade_plugin(
  185. tenant_id,
  186. original_plugin_unique_identifier,
  187. new_plugin_unique_identifier,
  188. PluginInstallationSource.Marketplace,
  189. {
  190. "plugin_unique_identifier": new_plugin_unique_identifier,
  191. },
  192. )
  193. @staticmethod
  194. def upgrade_plugin_with_github(
  195. tenant_id: str,
  196. original_plugin_unique_identifier: str,
  197. new_plugin_unique_identifier: str,
  198. repo: str,
  199. version: str,
  200. package: str,
  201. ):
  202. """
  203. Upgrade plugin with github
  204. """
  205. manager = PluginInstallationManager()
  206. return manager.upgrade_plugin(
  207. tenant_id,
  208. original_plugin_unique_identifier,
  209. new_plugin_unique_identifier,
  210. PluginInstallationSource.Github,
  211. {
  212. "repo": repo,
  213. "version": version,
  214. "package": package,
  215. },
  216. )
  217. @staticmethod
  218. def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginUploadResponse:
  219. """
  220. Upload plugin package files
  221. returns: plugin_unique_identifier
  222. """
  223. manager = PluginInstallationManager()
  224. return manager.upload_pkg(tenant_id, pkg, verify_signature)
  225. @staticmethod
  226. def upload_pkg_from_github(
  227. tenant_id: str, repo: str, version: str, package: str, verify_signature: bool = False
  228. ) -> PluginUploadResponse:
  229. """
  230. Install plugin from github release package files,
  231. returns plugin_unique_identifier
  232. """
  233. pkg = download_with_size_limit(
  234. f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE
  235. )
  236. manager = PluginInstallationManager()
  237. return manager.upload_pkg(
  238. tenant_id,
  239. pkg,
  240. verify_signature,
  241. )
  242. @staticmethod
  243. def upload_bundle(
  244. tenant_id: str, bundle: bytes, verify_signature: bool = False
  245. ) -> Sequence[PluginBundleDependency]:
  246. """
  247. Upload a plugin bundle and return the dependencies.
  248. """
  249. manager = PluginInstallationManager()
  250. return manager.upload_bundle(tenant_id, bundle, verify_signature)
  251. @staticmethod
  252. def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]):
  253. manager = PluginInstallationManager()
  254. return manager.install_from_identifiers(
  255. tenant_id,
  256. plugin_unique_identifiers,
  257. PluginInstallationSource.Package,
  258. [{}],
  259. )
  260. @staticmethod
  261. def install_from_github(tenant_id: str, plugin_unique_identifier: str, repo: str, version: str, package: str):
  262. """
  263. Install plugin from github release package files,
  264. returns plugin_unique_identifier
  265. """
  266. manager = PluginInstallationManager()
  267. return manager.install_from_identifiers(
  268. tenant_id,
  269. [plugin_unique_identifier],
  270. PluginInstallationSource.Github,
  271. [
  272. {
  273. "repo": repo,
  274. "version": version,
  275. "package": package,
  276. }
  277. ],
  278. )
  279. @staticmethod
  280. def install_from_marketplace_pkg(
  281. tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False
  282. ):
  283. """
  284. Install plugin from marketplace package files,
  285. returns installation task id
  286. """
  287. manager = PluginInstallationManager()
  288. # check if already downloaded
  289. for plugin_unique_identifier in plugin_unique_identifiers:
  290. try:
  291. manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
  292. # already downloaded, skip
  293. except Exception:
  294. # plugin not installed, download and upload pkg
  295. pkg = download_plugin_pkg(plugin_unique_identifier)
  296. manager.upload_pkg(tenant_id, pkg, verify_signature)
  297. return manager.install_from_identifiers(
  298. tenant_id,
  299. plugin_unique_identifiers,
  300. PluginInstallationSource.Marketplace,
  301. [
  302. {
  303. "plugin_unique_identifier": plugin_unique_identifier,
  304. }
  305. for plugin_unique_identifier in plugin_unique_identifiers
  306. ],
  307. )
  308. @staticmethod
  309. def uninstall(tenant_id: str, plugin_installation_id: str) -> bool:
  310. manager = PluginInstallationManager()
  311. return manager.uninstall(tenant_id, plugin_installation_id)
  312. @staticmethod
  313. def check_tools_existence(tenant_id: str, provider_ids: Sequence[GenericProviderID]) -> Sequence[bool]:
  314. """
  315. Check if the tools exist
  316. """
  317. manager = PluginInstallationManager()
  318. return manager.check_tools_existence(tenant_id, provider_ids)