mirror of https://github.com/langgenius/dify.git
feat: support install plugin
This commit is contained in:
parent
56b7853afe
commit
e27a03ae15
|
|
@ -295,4 +295,6 @@ POSITION_PROVIDER_EXCLUDES=
|
|||
# Plugin configuration
|
||||
PLUGIN_API_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||
PLUGIN_API_URL=http://127.0.0.1:5002
|
||||
PLUGIN_REMOTE_INSTALL_PORT=5003
|
||||
PLUGIN_REMOTE_INSTALL_HOST=localhost
|
||||
INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||
|
|
@ -127,6 +127,16 @@ class PluginConfig(BaseSettings):
|
|||
|
||||
INNER_API_KEY_FOR_PLUGIN: str = Field(description="Inner api key for plugin", default="inner-api-key")
|
||||
|
||||
PLUGIN_REMOTE_INSTALL_HOST: str = Field(
|
||||
description="Plugin Remote Install Host",
|
||||
default="localhost",
|
||||
)
|
||||
|
||||
PLUGIN_REMOTE_INSTALL_PORT: PositiveInt = Field(
|
||||
description="Plugin Remote Install Port",
|
||||
default=5003,
|
||||
)
|
||||
|
||||
|
||||
class EndpointConfig(BaseSettings):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import io
|
||||
import json
|
||||
|
||||
from flask import Response, request, send_file
|
||||
from flask_login import current_user
|
||||
from flask_restful import Resource
|
||||
from flask_restful import Resource, reqparse
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from libs.login import login_required
|
||||
from services.plugin.plugin_debugging_service import PluginDebuggingService
|
||||
from services.plugin.plugin_service import PluginService
|
||||
|
||||
|
||||
class PluginDebuggingKeyApi(Resource):
|
||||
|
|
@ -19,7 +25,99 @@ class PluginDebuggingKeyApi(Resource):
|
|||
raise Forbidden()
|
||||
|
||||
tenant_id = user.current_tenant_id
|
||||
return {"key": PluginDebuggingService.get_plugin_debugging_key(tenant_id)}
|
||||
|
||||
return {
|
||||
"key": PluginService.get_plugin_debugging_key(tenant_id),
|
||||
"host": dify_config.PLUGIN_REMOTE_INSTALL_HOST,
|
||||
"port": dify_config.PLUGIN_REMOTE_INSTALL_PORT,
|
||||
}
|
||||
|
||||
|
||||
class PluginListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
user = current_user
|
||||
tenant_id = user.current_tenant_id
|
||||
plugins = PluginService.list_plugins(tenant_id)
|
||||
return jsonable_encoder({"plugins": plugins})
|
||||
|
||||
|
||||
class PluginIconApi(Resource):
|
||||
@setup_required
|
||||
def get(self):
|
||||
req = reqparse.RequestParser()
|
||||
req.add_argument("tenant_id", type=str, required=True, location="args")
|
||||
req.add_argument("filename", type=str, required=True, location="args")
|
||||
args = req.parse_args()
|
||||
|
||||
icon_bytes, mimetype = PluginService.get_asset(args["tenant_id"], args["filename"])
|
||||
|
||||
icon_cache_max_age = dify_config.TOOL_ICON_CACHE_MAX_AGE
|
||||
return send_file(io.BytesIO(icon_bytes), mimetype=mimetype, max_age=icon_cache_max_age)
|
||||
|
||||
|
||||
class PluginInstallCheckUniqueIdentifierApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
req = reqparse.RequestParser()
|
||||
req.add_argument("plugin_unique_identifier", type=str, required=True, location="args")
|
||||
args = req.parse_args()
|
||||
|
||||
user = current_user
|
||||
tenant_id = user.current_tenant_id
|
||||
|
||||
return {"installed": PluginService.check_plugin_unique_identifier(tenant_id, args["plugin_unique_identifier"])}
|
||||
|
||||
|
||||
class PluginInstallFromUniqueIdentifierApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
req = reqparse.RequestParser()
|
||||
req.add_argument("plugin_unique_identifier", type=str, required=True, location="json")
|
||||
args = req.parse_args()
|
||||
|
||||
user = current_user
|
||||
if not user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
|
||||
tenant_id = user.current_tenant_id
|
||||
|
||||
return {
|
||||
"success": PluginService.install_plugin_from_unique_identifier(tenant_id, args["plugin_unique_identifier"])
|
||||
}
|
||||
|
||||
|
||||
class PluginInstallFromPkgApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
user = current_user
|
||||
if not user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
|
||||
tenant_id = user.current_tenant_id
|
||||
|
||||
file = request.files["pkg"]
|
||||
content = file.read()
|
||||
|
||||
def generator():
|
||||
response = PluginService.install_plugin_from_pkg(tenant_id, content)
|
||||
for message in response:
|
||||
yield f"data: {json.dumps(jsonable_encoder(message))}\n\n"
|
||||
|
||||
return Response(generator(), mimetype="text/event-stream")
|
||||
|
||||
|
||||
api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key")
|
||||
api.add_resource(PluginListApi, "/workspaces/current/plugin/list")
|
||||
api.add_resource(PluginIconApi, "/workspaces/current/plugin/icon")
|
||||
api.add_resource(PluginInstallCheckUniqueIdentifierApi, "/workspaces/current/plugin/install/check_unique_identifier")
|
||||
api.add_resource(PluginInstallFromUniqueIdentifierApi, "/workspaces/current/plugin/install/from_unique_identifier")
|
||||
api.add_resource(PluginInstallFromPkgApi, "/workspaces/current/plugin/install/from_pkg")
|
||||
|
|
|
|||
|
|
@ -1,10 +1,77 @@
|
|||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.model_runtime.entities.provider_entities import ProviderEntity
|
||||
from core.plugin.entities.base import BasePluginEntity
|
||||
from core.plugin.entities.endpoint import EndpointEntity
|
||||
from core.tools.entities.common_entities import I18nObject
|
||||
from core.tools.entities.tool_entities import ToolProviderEntity
|
||||
|
||||
|
||||
class PluginResourceRequirements(BaseModel):
|
||||
memory: int
|
||||
|
||||
class Permission(BaseModel):
|
||||
class Tool(BaseModel):
|
||||
enabled: Optional[bool] = Field(default=False)
|
||||
|
||||
class Model(BaseModel):
|
||||
enabled: Optional[bool] = Field(default=False)
|
||||
llm: Optional[bool] = Field(default=False)
|
||||
text_embedding: Optional[bool] = Field(default=False)
|
||||
rerank: Optional[bool] = Field(default=False)
|
||||
tts: Optional[bool] = Field(default=False)
|
||||
speech2text: Optional[bool] = Field(default=False)
|
||||
moderation: Optional[bool] = Field(default=False)
|
||||
|
||||
class Node(BaseModel):
|
||||
enabled: Optional[bool] = Field(default=False)
|
||||
|
||||
class Endpoint(BaseModel):
|
||||
enabled: Optional[bool] = Field(default=False)
|
||||
|
||||
class Storage(BaseModel):
|
||||
enabled: Optional[bool] = Field(default=False)
|
||||
size: int = Field(ge=1024, le=1073741824, default=1048576)
|
||||
|
||||
tool: Optional[Tool] = Field(default=None)
|
||||
model: Optional[Model] = Field(default=None)
|
||||
node: Optional[Node] = Field(default=None)
|
||||
endpoint: Optional[Endpoint] = Field(default=None)
|
||||
storage: Storage = Field(default=None)
|
||||
|
||||
permission: Optional[Permission]
|
||||
|
||||
|
||||
class PluginDeclaration(BaseModel):
|
||||
class Plugins(BaseModel):
|
||||
tools: list[str] = Field(default_factory=list)
|
||||
models: list[str] = Field(default_factory=list)
|
||||
endpoints: list[str] = Field(default_factory=list)
|
||||
|
||||
version: str = Field(..., pattern=r"^\d{1,4}(\.\d{1,4}){1,3}(-\w{1,16})?$")
|
||||
author: Optional[str] = Field(..., pattern=r"^[a-zA-Z0-9_-]{1,64}$")
|
||||
name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$")
|
||||
icon: str
|
||||
label: I18nObject
|
||||
created_at: datetime.datetime
|
||||
resource: PluginResourceRequirements
|
||||
plugins: Plugins
|
||||
tool: Optional[ToolProviderEntity] = None
|
||||
model: Optional[ProviderEntity] = None
|
||||
endpoint: Optional[EndpointEntity] = None
|
||||
|
||||
|
||||
class PluginEntity(BasePluginEntity):
|
||||
name: str
|
||||
plugin_id: str
|
||||
plugin_unique_identifier: str
|
||||
declaration: PluginDeclaration
|
||||
installation_id: str
|
||||
tenant_id: str
|
||||
endpoints_setups: int
|
||||
endpoints_active: int
|
||||
runtime_type: str
|
||||
version: str
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class BasePluginManager:
|
|||
headers: dict | None = None,
|
||||
data: bytes | dict | str | None = None,
|
||||
params: dict | None = None,
|
||||
files: dict | None = None,
|
||||
stream: bool = False,
|
||||
) -> requests.Response:
|
||||
"""
|
||||
|
|
@ -44,7 +45,7 @@ class BasePluginManager:
|
|||
data = json.dumps(data)
|
||||
|
||||
response = requests.request(
|
||||
method=method, url=str(url), headers=headers, data=data, params=params, stream=stream
|
||||
method=method, url=str(url), headers=headers, data=data, params=params, stream=stream, files=files
|
||||
)
|
||||
return response
|
||||
|
||||
|
|
@ -55,11 +56,12 @@ class BasePluginManager:
|
|||
params: dict | None = None,
|
||||
headers: dict | None = None,
|
||||
data: bytes | dict | None = None,
|
||||
files: dict | None = None,
|
||||
) -> Generator[bytes, None, None]:
|
||||
"""
|
||||
Make a stream request to the plugin daemon inner API
|
||||
"""
|
||||
response = self._request(method, path, headers, data, params, stream=True)
|
||||
response = self._request(method, path, headers, data, params, files, stream=True)
|
||||
for line in response.iter_lines():
|
||||
line = line.decode("utf-8").strip()
|
||||
if line.startswith("data:"):
|
||||
|
|
@ -75,11 +77,12 @@ class BasePluginManager:
|
|||
headers: dict | None = None,
|
||||
data: bytes | dict | None = None,
|
||||
params: dict | None = None,
|
||||
files: dict | None = None,
|
||||
) -> Generator[T, None, None]:
|
||||
"""
|
||||
Make a stream request to the plugin daemon inner API and yield the response as a model.
|
||||
"""
|
||||
for line in self._stream_request(method, path, params, headers, data):
|
||||
for line in self._stream_request(method, path, params, headers, data, files):
|
||||
yield type(**json.loads(line))
|
||||
|
||||
def _request_with_model(
|
||||
|
|
@ -90,11 +93,12 @@ class BasePluginManager:
|
|||
headers: dict | None = None,
|
||||
data: bytes | None = None,
|
||||
params: dict | None = None,
|
||||
files: dict | None = None,
|
||||
) -> T:
|
||||
"""
|
||||
Make a request to the plugin daemon inner API and return the response as a model.
|
||||
"""
|
||||
response = self._request(method, path, headers, data, params)
|
||||
response = self._request(method, path, headers, data, params, files)
|
||||
return type(**response.json())
|
||||
|
||||
def _request_with_plugin_daemon_response(
|
||||
|
|
@ -105,12 +109,13 @@ class BasePluginManager:
|
|||
headers: dict | None = None,
|
||||
data: bytes | dict | None = None,
|
||||
params: dict | None = None,
|
||||
files: dict | None = None,
|
||||
transformer: Callable[[dict], dict] | None = None,
|
||||
) -> T:
|
||||
"""
|
||||
Make a request to the plugin daemon inner API and return the response as a model.
|
||||
"""
|
||||
response = self._request(method, path, headers, data, params)
|
||||
response = self._request(method, path, headers, data, params, files)
|
||||
json_response = response.json()
|
||||
if transformer:
|
||||
json_response = transformer(json_response)
|
||||
|
|
@ -138,11 +143,12 @@ class BasePluginManager:
|
|||
headers: dict | None = None,
|
||||
data: bytes | dict | None = None,
|
||||
params: dict | None = None,
|
||||
files: dict | None = None,
|
||||
) -> Generator[T, None, None]:
|
||||
"""
|
||||
Make a stream request to the plugin daemon inner API and yield the response as a model.
|
||||
"""
|
||||
for line in self._stream_request(method, path, params, headers, data):
|
||||
for line in self._stream_request(method, path, params, headers, data, files):
|
||||
line_data = json.loads(line)
|
||||
rep = PluginDaemonBasicResponse[type](**line_data)
|
||||
if rep.code != 0:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ class PluginInstallationManager(BasePluginManager):
|
|||
# urlencode the identifier
|
||||
|
||||
return self._request_with_plugin_daemon_response(
|
||||
"GET", f"plugin/{tenant_id}/fetch/identifier", bool, params={"plugin_unique_identifier": identifier}
|
||||
"GET",
|
||||
f"plugin/{tenant_id}/management/fetch/identifier",
|
||||
bool,
|
||||
params={"plugin_unique_identifier": identifier},
|
||||
)
|
||||
|
||||
def list_plugins(self, tenant_id: str) -> list[PluginEntity]:
|
||||
|
|
@ -29,7 +32,10 @@ class PluginInstallationManager(BasePluginManager):
|
|||
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
|
||||
"POST",
|
||||
f"plugin/{tenant_id}/management/install/pkg",
|
||||
InstallPluginMessage,
|
||||
files=body,
|
||||
)
|
||||
|
||||
def install_from_identifier(self, tenant_id: str, identifier: str) -> bool:
|
||||
|
|
@ -39,14 +45,12 @@ class PluginInstallationManager(BasePluginManager):
|
|||
# exception will be raised if the request failed
|
||||
return self._request_with_plugin_daemon_response(
|
||||
"POST",
|
||||
f"plugin/{tenant_id}/install/identifier",
|
||||
f"plugin/{tenant_id}/management/install/identifier",
|
||||
bool,
|
||||
params={
|
||||
"plugin_unique_identifier": identifier,
|
||||
},
|
||||
data={
|
||||
"plugin_unique_identifier": identifier,
|
||||
},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
def uninstall(self, tenant_id: str, identifier: str) -> bool:
|
||||
|
|
@ -54,5 +58,11 @@ class PluginInstallationManager(BasePluginManager):
|
|||
Uninstall a plugin.
|
||||
"""
|
||||
return self._request_with_plugin_daemon_response(
|
||||
"DELETE", f"plugin/{tenant_id}/uninstall", bool, params={"plugin_unique_identifier": identifier}
|
||||
"DELETE",
|
||||
f"plugin/{tenant_id}/management/uninstall",
|
||||
bool,
|
||||
data={
|
||||
"plugin_unique_identifier": identifier,
|
||||
},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
from core.plugin.manager.debugging import PluginDebuggingManager
|
||||
|
||||
|
||||
class PluginDebuggingService:
|
||||
@staticmethod
|
||||
def get_plugin_debugging_key(tenant_id: str) -> str:
|
||||
manager = PluginDebuggingManager()
|
||||
return manager.get_debugging_key(tenant_id)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from collections.abc import Generator
|
||||
from mimetypes import guess_type
|
||||
|
||||
from core.plugin.entities.plugin import PluginEntity
|
||||
from core.plugin.entities.plugin_daemon import InstallPluginMessage, PluginDaemonInnerError
|
||||
from core.plugin.manager.asset import PluginAssetManager
|
||||
from core.plugin.manager.debugging import PluginDebuggingManager
|
||||
from core.plugin.manager.plugin import PluginInstallationManager
|
||||
|
||||
|
||||
class PluginService:
|
||||
@staticmethod
|
||||
def get_plugin_debugging_key(tenant_id: str) -> str:
|
||||
manager = PluginDebuggingManager()
|
||||
return manager.get_debugging_key(tenant_id)
|
||||
|
||||
@staticmethod
|
||||
def list_plugins(tenant_id: str) -> list[PluginEntity]:
|
||||
manager = PluginInstallationManager()
|
||||
return manager.list_plugins(tenant_id)
|
||||
|
||||
@staticmethod
|
||||
def get_asset(tenant_id: str, asset_file: str) -> tuple[bytes, str]:
|
||||
manager = PluginAssetManager()
|
||||
# guess mime type
|
||||
mime_type, _ = guess_type(asset_file)
|
||||
return manager.fetch_asset(tenant_id, asset_file), mime_type or "application/octet-stream"
|
||||
|
||||
@staticmethod
|
||||
def check_plugin_unique_identifier(tenant_id: str, plugin_unique_identifier: str) -> bool:
|
||||
manager = PluginInstallationManager()
|
||||
return manager.fetch_plugin_by_identifier(tenant_id, plugin_unique_identifier)
|
||||
|
||||
@staticmethod
|
||||
def install_plugin_from_unique_identifier(tenant_id: str, plugin_unique_identifier: str) -> bool:
|
||||
manager = PluginInstallationManager()
|
||||
return manager.install_from_identifier(tenant_id, plugin_unique_identifier)
|
||||
|
||||
@staticmethod
|
||||
def install_plugin_from_pkg(tenant_id: str, pkg: bytes) -> Generator[InstallPluginMessage, None, None]:
|
||||
manager = PluginInstallationManager()
|
||||
try:
|
||||
yield from manager.install_from_pkg(tenant_id, pkg)
|
||||
except PluginDaemonInnerError as e:
|
||||
yield InstallPluginMessage(event=InstallPluginMessage.Event.Error, data=str(e.message))
|
||||
Loading…
Reference in New Issue