您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

plugin_service.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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.impl.asset import PluginAssetManager
  20. from core.plugin.impl.debugging import PluginDebuggingClient
  21. from core.plugin.impl.plugin import PluginInstaller
  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 = PluginDebuggingClient()
  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 = PluginInstaller()
  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 = PluginInstaller()
  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 = PluginInstaller()
  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 = PluginInstaller()
  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 = PluginInstaller()
  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 = PluginInstaller()
  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 = PluginInstaller()
  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 = PluginInstaller()
  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 = PluginInstaller()
  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 not dify_config.MARKETPLACE_ENABLED:
  165. raise ValueError("marketplace is not enabled")
  166. if original_plugin_unique_identifier == new_plugin_unique_identifier:
  167. raise ValueError("you should not upgrade plugin with the same plugin")
  168. # check if plugin pkg is already downloaded
  169. manager = PluginInstaller()
  170. try:
  171. manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier)
  172. # already downloaded, skip, and record install event
  173. marketplace.record_install_plugin_event(new_plugin_unique_identifier)
  174. except Exception:
  175. # plugin not installed, download and upload pkg
  176. pkg = download_plugin_pkg(new_plugin_unique_identifier)
  177. manager.upload_pkg(tenant_id, pkg, verify_signature=False)
  178. return manager.upgrade_plugin(
  179. tenant_id,
  180. original_plugin_unique_identifier,
  181. new_plugin_unique_identifier,
  182. PluginInstallationSource.Marketplace,
  183. {
  184. "plugin_unique_identifier": new_plugin_unique_identifier,
  185. },
  186. )
  187. @staticmethod
  188. def upgrade_plugin_with_github(
  189. tenant_id: str,
  190. original_plugin_unique_identifier: str,
  191. new_plugin_unique_identifier: str,
  192. repo: str,
  193. version: str,
  194. package: str,
  195. ):
  196. """
  197. Upgrade plugin with github
  198. """
  199. manager = PluginInstaller()
  200. return manager.upgrade_plugin(
  201. tenant_id,
  202. original_plugin_unique_identifier,
  203. new_plugin_unique_identifier,
  204. PluginInstallationSource.Github,
  205. {
  206. "repo": repo,
  207. "version": version,
  208. "package": package,
  209. },
  210. )
  211. @staticmethod
  212. def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginUploadResponse:
  213. """
  214. Upload plugin package files
  215. returns: plugin_unique_identifier
  216. """
  217. manager = PluginInstaller()
  218. return manager.upload_pkg(tenant_id, pkg, verify_signature)
  219. @staticmethod
  220. def upload_pkg_from_github(
  221. tenant_id: str, repo: str, version: str, package: str, verify_signature: bool = False
  222. ) -> PluginUploadResponse:
  223. """
  224. Install plugin from github release package files,
  225. returns plugin_unique_identifier
  226. """
  227. pkg = download_with_size_limit(
  228. f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE
  229. )
  230. manager = PluginInstaller()
  231. return manager.upload_pkg(
  232. tenant_id,
  233. pkg,
  234. verify_signature,
  235. )
  236. @staticmethod
  237. def upload_bundle(
  238. tenant_id: str, bundle: bytes, verify_signature: bool = False
  239. ) -> Sequence[PluginBundleDependency]:
  240. """
  241. Upload a plugin bundle and return the dependencies.
  242. """
  243. manager = PluginInstaller()
  244. return manager.upload_bundle(tenant_id, bundle, verify_signature)
  245. @staticmethod
  246. def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]):
  247. manager = PluginInstaller()
  248. return manager.install_from_identifiers(
  249. tenant_id,
  250. plugin_unique_identifiers,
  251. PluginInstallationSource.Package,
  252. [{}],
  253. )
  254. @staticmethod
  255. def install_from_github(tenant_id: str, plugin_unique_identifier: str, repo: str, version: str, package: str):
  256. """
  257. Install plugin from github release package files,
  258. returns plugin_unique_identifier
  259. """
  260. manager = PluginInstaller()
  261. return manager.install_from_identifiers(
  262. tenant_id,
  263. [plugin_unique_identifier],
  264. PluginInstallationSource.Github,
  265. [
  266. {
  267. "repo": repo,
  268. "version": version,
  269. "package": package,
  270. }
  271. ],
  272. )
  273. @staticmethod
  274. def fetch_marketplace_pkg(
  275. tenant_id: str, plugin_unique_identifier: str, verify_signature: bool = False
  276. ) -> PluginDeclaration:
  277. """
  278. Fetch marketplace package
  279. """
  280. if not dify_config.MARKETPLACE_ENABLED:
  281. raise ValueError("marketplace is not enabled")
  282. manager = PluginInstaller()
  283. try:
  284. declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
  285. except Exception:
  286. pkg = download_plugin_pkg(plugin_unique_identifier)
  287. declaration = manager.upload_pkg(tenant_id, pkg, verify_signature).manifest
  288. return declaration
  289. @staticmethod
  290. def install_from_marketplace_pkg(
  291. tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False
  292. ):
  293. """
  294. Install plugin from marketplace package files,
  295. returns installation task id
  296. """
  297. if not dify_config.MARKETPLACE_ENABLED:
  298. raise ValueError("marketplace is not enabled")
  299. manager = PluginInstaller()
  300. # check if already downloaded
  301. for plugin_unique_identifier in plugin_unique_identifiers:
  302. try:
  303. manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
  304. # already downloaded, skip
  305. except Exception:
  306. # plugin not installed, download and upload pkg
  307. pkg = download_plugin_pkg(plugin_unique_identifier)
  308. manager.upload_pkg(tenant_id, pkg, verify_signature)
  309. return manager.install_from_identifiers(
  310. tenant_id,
  311. plugin_unique_identifiers,
  312. PluginInstallationSource.Marketplace,
  313. [
  314. {
  315. "plugin_unique_identifier": plugin_unique_identifier,
  316. }
  317. for plugin_unique_identifier in plugin_unique_identifiers
  318. ],
  319. )
  320. @staticmethod
  321. def uninstall(tenant_id: str, plugin_installation_id: str) -> bool:
  322. manager = PluginInstaller()
  323. return manager.uninstall(tenant_id, plugin_installation_id)
  324. @staticmethod
  325. def check_tools_existence(tenant_id: str, provider_ids: Sequence[GenericProviderID]) -> Sequence[bool]:
  326. """
  327. Check if the tools exist
  328. """
  329. manager = PluginInstaller()
  330. return manager.check_tools_existence(tenant_id, provider_ids)