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

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