Преглед изворни кода

feat: support install plugin

Yeuoly пре 7 месеци
родитељ
комит
eef79a5196

+ 10 - 1
api/controllers/console/__init__.py

@@ -56,4 +56,13 @@ from .explore import (
 from .tag import tags
 
 # Import workspace controllers
-from .workspace import account, load_balancing_config, members, model_providers, models, tool_providers, workspace, plugin
+from .workspace import (
+    account,
+    load_balancing_config,
+    members,
+    model_providers,
+    models,
+    plugin,
+    tool_providers,
+    workspace,
+)

+ 15 - 1
api/core/plugin/entities/plugin_daemon.py

@@ -1,8 +1,9 @@
+from enum import Enum
 from typing import Generic, Optional, TypeVar
 
 from pydantic import BaseModel
 
-T = TypeVar("T", bound=(BaseModel | dict))
+T = TypeVar("T", bound=(BaseModel | dict | bool))
 
 
 class PluginDaemonBasicResponse(BaseModel, Generic[T]):
@@ -13,3 +14,16 @@ class PluginDaemonBasicResponse(BaseModel, Generic[T]):
     code: int
     message: str
     data: Optional[T]
+
+
+class InstallPluginMessage(BaseModel):
+    """
+    Message for installing a plugin.
+    """
+    class Event(Enum):
+        Info = "info"
+        Done = "done"
+        Error = "error"
+
+    event: Event
+    data: str

+ 16 - 11
api/core/plugin/manager/base.py

@@ -12,12 +12,17 @@ from core.plugin.entities.plugin_daemon import PluginDaemonBasicResponse
 plugin_daemon_inner_api_baseurl = dify_config.PLUGIN_API_URL
 plugin_daemon_inner_api_key = dify_config.PLUGIN_API_KEY
 
-T = TypeVar("T", bound=(BaseModel | dict))
+T = TypeVar("T", bound=(BaseModel | dict | bool))
 
 
 class BasePluginManager:
     def _request(
-        self, method: str, path: str, headers: dict | None = None, data: bytes | None = None, stream: bool = False
+        self,
+        method: str,
+        path: str,
+        headers: dict | None = None,
+        data: bytes | dict | None = None,
+        stream: bool = False,
     ) -> requests.Response:
         """
         Make a request to the plugin daemon inner API.
@@ -29,7 +34,7 @@ class BasePluginManager:
         return response
 
     def _stream_request(
-        self, method: str, path: str, headers: dict | None = None, data: bytes | None = None
+        self, method: str, path: str, headers: dict | None = None, data: bytes | dict | None = None
     ) -> Generator[bytes, None, None]:
         """
         Make a stream request to the plugin daemon inner API
@@ -43,7 +48,7 @@ class BasePluginManager:
         path: str,
         type: type[T],
         headers: dict | None = None,
-        data: bytes | None = None,
+        data: bytes | dict | None = None,
     ) -> Generator[T, None, None]:
         """
         Make a stream request to the plugin daemon inner API and yield the response as a model.
@@ -61,7 +66,7 @@ class BasePluginManager:
         return type(**response.json())
 
     def _request_with_plugin_daemon_response(
-        self, method: str, path: str, type: type[T], headers: dict | None = None, data: bytes | None = None
+        self, method: str, path: str, type: type[T], headers: dict | None = None, data: bytes | dict | None = None
     ) -> T:
         """
         Make a request to the plugin daemon inner API and return the response as a model.
@@ -72,11 +77,11 @@ class BasePluginManager:
             raise ValueError(f"got error from plugin daemon: {rep.message}, code: {rep.code}")
         if rep.data is None:
             raise ValueError("got empty data from plugin daemon")
-        
+
         return rep.data
-    
+
     def _request_with_plugin_daemon_response_stream(
-        self, method: str, path: str, type: type[T], headers: dict | None = None, data: bytes | None = None
+        self, method: str, path: str, type: type[T], headers: dict | None = None, data: bytes | dict | None = None
     ) -> Generator[T, None, None]:
         """
         Make a stream request to the plugin daemon inner API and yield the response as a model.
@@ -85,7 +90,7 @@ class BasePluginManager:
             line_data = json.loads(line)
             rep = PluginDaemonBasicResponse[type](**line_data)
             if rep.code != 0:
-                raise Exception(f"got error from plugin daemon: {rep.message}, code: {rep.code}")
+                raise ValueError(f"got error from plugin daemon: {rep.message}, code: {rep.code}")
             if rep.data is None:
-                raise Exception("got empty data from plugin daemon")
-            yield rep.data
+                raise ValueError("got empty data from plugin daemon")
+            yield rep.data

+ 46 - 0
api/core/plugin/manager/plugin.py

@@ -0,0 +1,46 @@
+from collections.abc import Generator
+from urllib.parse import quote
+
+from core.plugin.entities.plugin_daemon import InstallPluginMessage
+from core.plugin.manager.base import BasePluginManager
+
+
+class PluginInstallationManager(BasePluginManager):
+    def fetch_plugin_by_identifier(self, tenant_id: str, identifier: str) -> bool:
+        # urlencode the identifier
+
+        identifier = quote(identifier)
+        return self._request_with_plugin_daemon_response(
+            "GET", f"/plugin/{tenant_id}/fetch/identifier?plugin_unique_identifier={identifier}", bool
+        )
+
+    def install_from_pkg(self, tenant_id: str, pkg: bytes) -> Generator[InstallPluginMessage, None, None]:
+        """
+        Install a plugin from a package.
+        """
+        # using multipart/form-data to encode body
+        body = {"dify_pkg": ("dify_pkg", pkg, "application/octet-stream")}
+
+        return self._request_with_plugin_daemon_response_stream(
+            "POST", f"/plugin/{tenant_id}/install/pkg", InstallPluginMessage, data=body
+        )
+
+    def install_from_identifier(self, tenant_id: str, identifier: str) -> bool:
+        """
+        Install a plugin from an identifier.
+        """
+        identifier = quote(identifier)
+        # exception will be raised if the request failed
+        self._request_with_plugin_daemon_response(
+            "POST",
+            f"/plugin/{tenant_id}/install/identifier",
+            dict,
+            headers={
+                "Content-Type": "application/json",
+            },
+            data={
+                "plugin_unique_identifier": identifier,
+            },
+        )
+
+        return True