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 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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 (
  19. PluginDecodeResponse,
  20. PluginInstallTask,
  21. PluginListResponse,
  22. PluginVerification,
  23. )
  24. from core.plugin.impl.asset import PluginAssetManager
  25. from core.plugin.impl.debugging import PluginDebuggingClient
  26. from core.plugin.impl.plugin import PluginInstaller
  27. from extensions.ext_redis import redis_client
  28. from services.errors.plugin import PluginInstallationForbiddenError
  29. from services.feature_service import FeatureService, PluginInstallationScope
  30. logger = logging.getLogger(__name__)
  31. class PluginService:
  32. class LatestPluginCache(BaseModel):
  33. plugin_id: str
  34. version: str
  35. unique_identifier: str
  36. status: str
  37. deprecated_reason: str
  38. alternative_plugin_id: str
  39. REDIS_KEY_PREFIX = "plugin_service:latest_plugin:"
  40. REDIS_TTL = 60 * 5 # 5 minutes
  41. @staticmethod
  42. def fetch_latest_plugin_version(plugin_ids: Sequence[str]) -> Mapping[str, Optional[LatestPluginCache]]:
  43. """
  44. Fetch the latest plugin version
  45. """
  46. result: dict[str, Optional[PluginService.LatestPluginCache]] = {}
  47. try:
  48. cache_not_exists = []
  49. # Try to get from Redis first
  50. for plugin_id in plugin_ids:
  51. cached_data = redis_client.get(f"{PluginService.REDIS_KEY_PREFIX}{plugin_id}")
  52. if cached_data:
  53. result[plugin_id] = PluginService.LatestPluginCache.model_validate_json(cached_data)
  54. else:
  55. cache_not_exists.append(plugin_id)
  56. if cache_not_exists:
  57. manifests = {
  58. manifest.plugin_id: manifest
  59. for manifest in marketplace.batch_fetch_plugin_manifests(cache_not_exists)
  60. }
  61. for plugin_id, manifest in manifests.items():
  62. latest_plugin = PluginService.LatestPluginCache(
  63. plugin_id=plugin_id,
  64. version=manifest.latest_version,
  65. unique_identifier=manifest.latest_package_identifier,
  66. status=manifest.status,
  67. deprecated_reason=manifest.deprecated_reason,
  68. alternative_plugin_id=manifest.alternative_plugin_id,
  69. )
  70. # Store in Redis
  71. redis_client.setex(
  72. f"{PluginService.REDIS_KEY_PREFIX}{plugin_id}",
  73. PluginService.REDIS_TTL,
  74. latest_plugin.model_dump_json(),
  75. )
  76. result[plugin_id] = latest_plugin
  77. # pop plugin_id from cache_not_exists
  78. cache_not_exists.remove(plugin_id)
  79. for plugin_id in cache_not_exists:
  80. result[plugin_id] = None
  81. return result
  82. except Exception:
  83. logger.exception("failed to fetch latest plugin version")
  84. return result
  85. @staticmethod
  86. def _check_marketplace_only_permission():
  87. """
  88. Check if the marketplace only permission is enabled
  89. """
  90. features = FeatureService.get_system_features()
  91. if features.plugin_installation_permission.restrict_to_marketplace_only:
  92. raise PluginInstallationForbiddenError("Plugin installation is restricted to marketplace only")
  93. @staticmethod
  94. def _check_plugin_installation_scope(plugin_verification: Optional[PluginVerification]):
  95. """
  96. Check the plugin installation scope
  97. """
  98. features = FeatureService.get_system_features()
  99. match features.plugin_installation_permission.plugin_installation_scope:
  100. case PluginInstallationScope.OFFICIAL_ONLY:
  101. if (
  102. plugin_verification is None
  103. or plugin_verification.authorized_category != PluginVerification.AuthorizedCategory.Langgenius
  104. ):
  105. raise PluginInstallationForbiddenError("Plugin installation is restricted to official only")
  106. case PluginInstallationScope.OFFICIAL_AND_SPECIFIC_PARTNERS:
  107. if plugin_verification is None or plugin_verification.authorized_category not in [
  108. PluginVerification.AuthorizedCategory.Langgenius,
  109. PluginVerification.AuthorizedCategory.Partner,
  110. ]:
  111. raise PluginInstallationForbiddenError(
  112. "Plugin installation is restricted to official and specific partners"
  113. )
  114. case PluginInstallationScope.NONE:
  115. raise PluginInstallationForbiddenError("Installing plugins is not allowed")
  116. case PluginInstallationScope.ALL:
  117. pass
  118. @staticmethod
  119. def get_debugging_key(tenant_id: str) -> str:
  120. """
  121. get the debugging key of the tenant
  122. """
  123. manager = PluginDebuggingClient()
  124. return manager.get_debugging_key(tenant_id)
  125. @staticmethod
  126. def list_latest_versions(plugin_ids: Sequence[str]) -> Mapping[str, Optional[LatestPluginCache]]:
  127. """
  128. List the latest versions of the plugins
  129. """
  130. return PluginService.fetch_latest_plugin_version(plugin_ids)
  131. @staticmethod
  132. def list(tenant_id: str) -> list[PluginEntity]:
  133. """
  134. list all plugins of the tenant
  135. """
  136. manager = PluginInstaller()
  137. plugins = manager.list_plugins(tenant_id)
  138. return plugins
  139. @staticmethod
  140. def list_with_total(tenant_id: str, page: int, page_size: int) -> PluginListResponse:
  141. """
  142. list all plugins of the tenant
  143. """
  144. manager = PluginInstaller()
  145. plugins = manager.list_plugins_with_total(tenant_id, page, page_size)
  146. return plugins
  147. @staticmethod
  148. def list_installations_from_ids(tenant_id: str, ids: Sequence[str]) -> Sequence[PluginInstallation]:
  149. """
  150. List plugin installations from ids
  151. """
  152. manager = PluginInstaller()
  153. return manager.fetch_plugin_installation_by_ids(tenant_id, ids)
  154. @staticmethod
  155. def get_asset(tenant_id: str, asset_file: str) -> tuple[bytes, str]:
  156. """
  157. get the asset file of the plugin
  158. """
  159. manager = PluginAssetManager()
  160. # guess mime type
  161. mime_type, _ = guess_type(asset_file)
  162. return manager.fetch_asset(tenant_id, asset_file), mime_type or "application/octet-stream"
  163. @staticmethod
  164. def check_plugin_unique_identifier(tenant_id: str, plugin_unique_identifier: str) -> bool:
  165. """
  166. check if the plugin unique identifier is already installed by other tenant
  167. """
  168. manager = PluginInstaller()
  169. return manager.fetch_plugin_by_identifier(tenant_id, plugin_unique_identifier)
  170. @staticmethod
  171. def fetch_plugin_manifest(tenant_id: str, plugin_unique_identifier: str) -> PluginDeclaration:
  172. """
  173. Fetch plugin manifest
  174. """
  175. manager = PluginInstaller()
  176. return manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
  177. @staticmethod
  178. def is_plugin_verified(tenant_id: str, plugin_unique_identifier: str) -> bool:
  179. """
  180. Check if the plugin is verified
  181. """
  182. manager = PluginInstaller()
  183. try:
  184. return manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier).verified
  185. except Exception:
  186. return False
  187. @staticmethod
  188. def fetch_install_tasks(tenant_id: str, page: int, page_size: int) -> Sequence[PluginInstallTask]:
  189. """
  190. Fetch plugin installation tasks
  191. """
  192. manager = PluginInstaller()
  193. return manager.fetch_plugin_installation_tasks(tenant_id, page, page_size)
  194. @staticmethod
  195. def fetch_install_task(tenant_id: str, task_id: str) -> PluginInstallTask:
  196. manager = PluginInstaller()
  197. return manager.fetch_plugin_installation_task(tenant_id, task_id)
  198. @staticmethod
  199. def delete_install_task(tenant_id: str, task_id: str) -> bool:
  200. """
  201. Delete a plugin installation task
  202. """
  203. manager = PluginInstaller()
  204. return manager.delete_plugin_installation_task(tenant_id, task_id)
  205. @staticmethod
  206. def delete_all_install_task_items(
  207. tenant_id: str,
  208. ) -> bool:
  209. """
  210. Delete all plugin installation task items
  211. """
  212. manager = PluginInstaller()
  213. return manager.delete_all_plugin_installation_task_items(tenant_id)
  214. @staticmethod
  215. def delete_install_task_item(tenant_id: str, task_id: str, identifier: str) -> bool:
  216. """
  217. Delete a plugin installation task item
  218. """
  219. manager = PluginInstaller()
  220. return manager.delete_plugin_installation_task_item(tenant_id, task_id, identifier)
  221. @staticmethod
  222. def upgrade_plugin_with_marketplace(
  223. tenant_id: str, original_plugin_unique_identifier: str, new_plugin_unique_identifier: str
  224. ):
  225. """
  226. Upgrade plugin with marketplace
  227. """
  228. if not dify_config.MARKETPLACE_ENABLED:
  229. raise ValueError("marketplace is not enabled")
  230. if original_plugin_unique_identifier == new_plugin_unique_identifier:
  231. raise ValueError("you should not upgrade plugin with the same plugin")
  232. # check if plugin pkg is already downloaded
  233. manager = PluginInstaller()
  234. features = FeatureService.get_system_features()
  235. try:
  236. manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier)
  237. # already downloaded, skip, and record install event
  238. marketplace.record_install_plugin_event(new_plugin_unique_identifier)
  239. except Exception:
  240. # plugin not installed, download and upload pkg
  241. pkg = download_plugin_pkg(new_plugin_unique_identifier)
  242. response = manager.upload_pkg(
  243. tenant_id,
  244. pkg,
  245. verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
  246. )
  247. # check if the plugin is available to install
  248. PluginService._check_plugin_installation_scope(response.verification)
  249. return manager.upgrade_plugin(
  250. tenant_id,
  251. original_plugin_unique_identifier,
  252. new_plugin_unique_identifier,
  253. PluginInstallationSource.Marketplace,
  254. {
  255. "plugin_unique_identifier": new_plugin_unique_identifier,
  256. },
  257. )
  258. @staticmethod
  259. def upgrade_plugin_with_github(
  260. tenant_id: str,
  261. original_plugin_unique_identifier: str,
  262. new_plugin_unique_identifier: str,
  263. repo: str,
  264. version: str,
  265. package: str,
  266. ):
  267. """
  268. Upgrade plugin with github
  269. """
  270. PluginService._check_marketplace_only_permission()
  271. manager = PluginInstaller()
  272. return manager.upgrade_plugin(
  273. tenant_id,
  274. original_plugin_unique_identifier,
  275. new_plugin_unique_identifier,
  276. PluginInstallationSource.Github,
  277. {
  278. "repo": repo,
  279. "version": version,
  280. "package": package,
  281. },
  282. )
  283. @staticmethod
  284. def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginDecodeResponse:
  285. """
  286. Upload plugin package files
  287. returns: plugin_unique_identifier
  288. """
  289. PluginService._check_marketplace_only_permission()
  290. manager = PluginInstaller()
  291. features = FeatureService.get_system_features()
  292. response = manager.upload_pkg(
  293. tenant_id,
  294. pkg,
  295. verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
  296. )
  297. return response
  298. @staticmethod
  299. def upload_pkg_from_github(
  300. tenant_id: str, repo: str, version: str, package: str, verify_signature: bool = False
  301. ) -> PluginDecodeResponse:
  302. """
  303. Install plugin from github release package files,
  304. returns plugin_unique_identifier
  305. """
  306. PluginService._check_marketplace_only_permission()
  307. pkg = download_with_size_limit(
  308. f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE
  309. )
  310. features = FeatureService.get_system_features()
  311. manager = PluginInstaller()
  312. response = manager.upload_pkg(
  313. tenant_id,
  314. pkg,
  315. verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
  316. )
  317. return response
  318. @staticmethod
  319. def upload_bundle(
  320. tenant_id: str, bundle: bytes, verify_signature: bool = False
  321. ) -> Sequence[PluginBundleDependency]:
  322. """
  323. Upload a plugin bundle and return the dependencies.
  324. """
  325. manager = PluginInstaller()
  326. PluginService._check_marketplace_only_permission()
  327. return manager.upload_bundle(tenant_id, bundle, verify_signature)
  328. @staticmethod
  329. def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]):
  330. PluginService._check_marketplace_only_permission()
  331. manager = PluginInstaller()
  332. return manager.install_from_identifiers(
  333. tenant_id,
  334. plugin_unique_identifiers,
  335. PluginInstallationSource.Package,
  336. [{}],
  337. )
  338. @staticmethod
  339. def install_from_github(tenant_id: str, plugin_unique_identifier: str, repo: str, version: str, package: str):
  340. """
  341. Install plugin from github release package files,
  342. returns plugin_unique_identifier
  343. """
  344. PluginService._check_marketplace_only_permission()
  345. manager = PluginInstaller()
  346. return manager.install_from_identifiers(
  347. tenant_id,
  348. [plugin_unique_identifier],
  349. PluginInstallationSource.Github,
  350. [
  351. {
  352. "repo": repo,
  353. "version": version,
  354. "package": package,
  355. }
  356. ],
  357. )
  358. @staticmethod
  359. def fetch_marketplace_pkg(tenant_id: str, plugin_unique_identifier: str) -> PluginDeclaration:
  360. """
  361. Fetch marketplace package
  362. """
  363. if not dify_config.MARKETPLACE_ENABLED:
  364. raise ValueError("marketplace is not enabled")
  365. features = FeatureService.get_system_features()
  366. manager = PluginInstaller()
  367. try:
  368. declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
  369. except Exception:
  370. pkg = download_plugin_pkg(plugin_unique_identifier)
  371. response = manager.upload_pkg(
  372. tenant_id,
  373. pkg,
  374. verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
  375. )
  376. # check if the plugin is available to install
  377. PluginService._check_plugin_installation_scope(response.verification)
  378. declaration = response.manifest
  379. return declaration
  380. @staticmethod
  381. def install_from_marketplace_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]):
  382. """
  383. Install plugin from marketplace package files,
  384. returns installation task id
  385. """
  386. if not dify_config.MARKETPLACE_ENABLED:
  387. raise ValueError("marketplace is not enabled")
  388. manager = PluginInstaller()
  389. # collect actual plugin_unique_identifiers
  390. actual_plugin_unique_identifiers = []
  391. metas = []
  392. features = FeatureService.get_system_features()
  393. # check if already downloaded
  394. for plugin_unique_identifier in plugin_unique_identifiers:
  395. try:
  396. manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
  397. plugin_decode_response = manager.decode_plugin_from_identifier(tenant_id, plugin_unique_identifier)
  398. # check if the plugin is available to install
  399. PluginService._check_plugin_installation_scope(plugin_decode_response.verification)
  400. # already downloaded, skip
  401. actual_plugin_unique_identifiers.append(plugin_unique_identifier)
  402. metas.append({"plugin_unique_identifier": plugin_unique_identifier})
  403. except Exception:
  404. # plugin not installed, download and upload pkg
  405. pkg = download_plugin_pkg(plugin_unique_identifier)
  406. response = manager.upload_pkg(
  407. tenant_id,
  408. pkg,
  409. verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
  410. )
  411. # check if the plugin is available to install
  412. PluginService._check_plugin_installation_scope(response.verification)
  413. # use response plugin_unique_identifier
  414. actual_plugin_unique_identifiers.append(response.unique_identifier)
  415. metas.append({"plugin_unique_identifier": response.unique_identifier})
  416. return manager.install_from_identifiers(
  417. tenant_id,
  418. actual_plugin_unique_identifiers,
  419. PluginInstallationSource.Marketplace,
  420. metas,
  421. )
  422. @staticmethod
  423. def uninstall(tenant_id: str, plugin_installation_id: str) -> bool:
  424. manager = PluginInstaller()
  425. return manager.uninstall(tenant_id, plugin_installation_id)
  426. @staticmethod
  427. def check_tools_existence(tenant_id: str, provider_ids: Sequence[GenericProviderID]) -> Sequence[bool]:
  428. """
  429. Check if the tools exist
  430. """
  431. manager = PluginInstaller()
  432. return manager.check_tools_existence(tenant_id, provider_ids)