選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

plugin_service.py 13KB

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