mirror of https://github.com/langgenius/dify.git
feat(cli_api): implement CLI API for external sandbox interactions, including session management and request handling
This commit is contained in:
parent
42fd0a0a62
commit
16f26c4f99
|
|
@ -716,3 +716,11 @@ SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
|
|||
# Sandbox Dify CLI configuration
|
||||
# Directory containing dify CLI binaries (dify-cli-<os>-<arch>). Defaults to api/bin when unset.
|
||||
SANDBOX_DIFY_CLI_ROOT=
|
||||
|
||||
|
||||
# CLI API URL for sandbox (dify-sandbox or e2b) to call back to Dify API.
|
||||
# This URL must be accessible from the sandbox environment.
|
||||
# For local development: use http://localhost:5001 or http://127.0.0.1:5001
|
||||
# For Docker deployment: use http://api:5001 (internal Docker network)
|
||||
# For external sandbox (e.g., e2b): use a publicly accessible URL
|
||||
CLI_API_URL=http://localhost:5001
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -244,6 +244,17 @@ class PluginConfig(BaseSettings):
|
|||
)
|
||||
|
||||
|
||||
class CliApiConfig(BaseSettings):
|
||||
"""
|
||||
Configuration for CLI API (for dify-cli to call back from external sandbox environments)
|
||||
"""
|
||||
|
||||
CLI_API_URL: str = Field(
|
||||
description="CLI API URL for external sandbox (e.g., e2b) to call back.",
|
||||
default="http://localhost:5001",
|
||||
)
|
||||
|
||||
|
||||
class MarketplaceConfig(BaseSettings):
|
||||
"""
|
||||
Configuration for marketplace
|
||||
|
|
@ -1299,6 +1310,7 @@ class FeatureConfig(
|
|||
TriggerConfig,
|
||||
AsyncWorkflowConfig,
|
||||
PluginConfig,
|
||||
CliApiConfig,
|
||||
MarketplaceConfig,
|
||||
DataSetConfig,
|
||||
EndpointConfig,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
from flask import Blueprint
|
||||
from flask_restx import Namespace
|
||||
|
||||
from libs.external_api import ExternalApi
|
||||
|
||||
bp = Blueprint("cli_api", __name__, url_prefix="/cli/api")
|
||||
|
||||
api = ExternalApi(
|
||||
bp,
|
||||
version="1.0",
|
||||
title="CLI API",
|
||||
description="APIs for Dify CLI to call back from external sandbox environments (e.g., e2b)",
|
||||
)
|
||||
|
||||
# Create namespace
|
||||
cli_api_ns = Namespace("cli_api", description="CLI API operations", path="/")
|
||||
|
||||
from .plugin import plugin as _plugin
|
||||
|
||||
api.add_namespace(cli_api_ns)
|
||||
|
||||
__all__ = [
|
||||
"_plugin",
|
||||
"api",
|
||||
"bp",
|
||||
"cli_api_ns",
|
||||
]
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
from flask_restx import Resource
|
||||
|
||||
from controllers.cli_api import cli_api_ns
|
||||
from controllers.cli_api.plugin.wraps import get_user_tenant, plugin_data
|
||||
from controllers.cli_api.wraps import cli_api_only
|
||||
from controllers.console.wraps import setup_required
|
||||
from core.file.helpers import get_signed_file_url_for_plugin
|
||||
from core.plugin.backwards_invocation.app import PluginAppBackwardsInvocation
|
||||
from core.plugin.backwards_invocation.base import BaseBackwardsInvocationResponse
|
||||
from core.plugin.backwards_invocation.model import PluginModelBackwardsInvocation
|
||||
from core.plugin.backwards_invocation.tool import PluginToolBackwardsInvocation
|
||||
from core.plugin.entities.request import (
|
||||
RequestInvokeApp,
|
||||
RequestInvokeLLM,
|
||||
RequestInvokeTool,
|
||||
RequestRequestUploadFile,
|
||||
)
|
||||
from core.tools.entities.tool_entities import ToolProviderType
|
||||
from libs.helper import length_prefixed_response
|
||||
from models import Account, Tenant
|
||||
from models.model import EndUser
|
||||
|
||||
|
||||
@cli_api_ns.route("/invoke/llm")
|
||||
class CliInvokeLLMApi(Resource):
|
||||
@get_user_tenant
|
||||
@setup_required
|
||||
@cli_api_only
|
||||
@plugin_data(payload_type=RequestInvokeLLM)
|
||||
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeLLM):
|
||||
def generator():
|
||||
response = PluginModelBackwardsInvocation.invoke_llm(user_model.id, tenant_model, payload)
|
||||
return PluginModelBackwardsInvocation.convert_to_event_stream(response)
|
||||
|
||||
return length_prefixed_response(0xF, generator())
|
||||
|
||||
|
||||
@cli_api_ns.route("/invoke/tool")
|
||||
class CliInvokeToolApi(Resource):
|
||||
@get_user_tenant
|
||||
@setup_required
|
||||
@cli_api_only
|
||||
@plugin_data(payload_type=RequestInvokeTool)
|
||||
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeTool):
|
||||
def generator():
|
||||
return PluginToolBackwardsInvocation.convert_to_event_stream(
|
||||
PluginToolBackwardsInvocation.invoke_tool(
|
||||
tenant_id=tenant_model.id,
|
||||
user_id=user_model.id,
|
||||
tool_type=ToolProviderType.value_of(payload.tool_type),
|
||||
provider=payload.provider,
|
||||
tool_name=payload.tool,
|
||||
tool_parameters=payload.tool_parameters,
|
||||
credential_id=payload.credential_id,
|
||||
),
|
||||
)
|
||||
|
||||
return length_prefixed_response(0xF, generator())
|
||||
|
||||
|
||||
@cli_api_ns.route("/invoke/app")
|
||||
class CliInvokeAppApi(Resource):
|
||||
@get_user_tenant
|
||||
@setup_required
|
||||
@cli_api_only
|
||||
@plugin_data(payload_type=RequestInvokeApp)
|
||||
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeApp):
|
||||
response = PluginAppBackwardsInvocation.invoke_app(
|
||||
app_id=payload.app_id,
|
||||
user_id=user_model.id,
|
||||
tenant_id=tenant_model.id,
|
||||
conversation_id=payload.conversation_id,
|
||||
query=payload.query,
|
||||
stream=payload.response_mode == "streaming",
|
||||
inputs=payload.inputs,
|
||||
files=payload.files,
|
||||
)
|
||||
|
||||
return length_prefixed_response(0xF, PluginAppBackwardsInvocation.convert_to_event_stream(response))
|
||||
|
||||
|
||||
@cli_api_ns.route("/upload/file/request")
|
||||
class CliUploadFileRequestApi(Resource):
|
||||
@get_user_tenant
|
||||
@setup_required
|
||||
@cli_api_only
|
||||
@plugin_data(payload_type=RequestRequestUploadFile)
|
||||
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestRequestUploadFile):
|
||||
# generate signed url
|
||||
url = get_signed_file_url_for_plugin(
|
||||
filename=payload.filename,
|
||||
mimetype=payload.mimetype,
|
||||
tenant_id=tenant_model.id,
|
||||
user_id=user_model.id,
|
||||
)
|
||||
return BaseBackwardsInvocationResponse(data={"url": url}).model_dump()
|
||||
|
||||
|
||||
@cli_api_ns.route("/fetch/tools/list")
|
||||
class CliFetchToolsListApi(Resource):
|
||||
@get_user_tenant
|
||||
@setup_required
|
||||
@cli_api_only
|
||||
def post(self, user_model: Account | EndUser, tenant_model: Tenant):
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from extensions.ext_database import db
|
||||
from services.tools.api_tools_manage_service import ApiToolManageService
|
||||
from services.tools.builtin_tools_manage_service import BuiltinToolManageService
|
||||
from services.tools.mcp_tools_manage_service import MCPToolManageService
|
||||
from services.tools.workflow_tools_manage_service import WorkflowToolManageService
|
||||
|
||||
providers = []
|
||||
|
||||
# Get builtin tools
|
||||
builtin_providers = BuiltinToolManageService.list_builtin_tools(user_model.id, tenant_model.id)
|
||||
for provider in builtin_providers:
|
||||
providers.append(provider.to_dict())
|
||||
|
||||
# Get API tools
|
||||
api_providers = ApiToolManageService.list_api_tools(tenant_model.id)
|
||||
for provider in api_providers:
|
||||
providers.append(provider.to_dict())
|
||||
|
||||
# Get workflow tools
|
||||
workflow_providers = WorkflowToolManageService.list_tenant_workflow_tools(user_model.id, tenant_model.id)
|
||||
for provider in workflow_providers:
|
||||
providers.append(provider.to_dict())
|
||||
|
||||
# Get MCP tools
|
||||
with Session(db.engine) as session:
|
||||
mcp_service = MCPToolManageService(session)
|
||||
mcp_providers = mcp_service.list_providers(tenant_id=tenant_model.id, for_list=True)
|
||||
for provider in mcp_providers:
|
||||
providers.append(provider.to_dict())
|
||||
|
||||
return BaseBackwardsInvocationResponse(data={"providers": providers}).model_dump()
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import ParamSpec, TypeVar
|
||||
|
||||
from flask import current_app, request
|
||||
from flask_login import user_logged_in
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.session.cli_api import CliApiSession, CliApiSessionManager
|
||||
from extensions.ext_database import db
|
||||
from libs.login import current_user
|
||||
from models.account import Tenant
|
||||
from models.model import DefaultEndUserSessionID, EndUser
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
class TenantUserPayload(BaseModel):
|
||||
tenant_id: str
|
||||
user_id: str
|
||||
|
||||
|
||||
def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
||||
"""
|
||||
Get current user
|
||||
|
||||
NOTE: user_id is not trusted, it could be maliciously set to any value.
|
||||
As a result, it could only be considered as an end user id.
|
||||
"""
|
||||
if not user_id:
|
||||
user_id = DefaultEndUserSessionID.DEFAULT_SESSION_ID
|
||||
is_anonymous = user_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID
|
||||
try:
|
||||
with Session(db.engine) as session:
|
||||
user_model = None
|
||||
|
||||
if is_anonymous:
|
||||
user_model = (
|
||||
session.query(EndUser)
|
||||
.where(
|
||||
EndUser.session_id == user_id,
|
||||
EndUser.tenant_id == tenant_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
else:
|
||||
user_model = (
|
||||
session.query(EndUser)
|
||||
.where(
|
||||
EndUser.id == user_id,
|
||||
EndUser.tenant_id == tenant_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not user_model:
|
||||
user_model = EndUser(
|
||||
tenant_id=tenant_id,
|
||||
type="service_api",
|
||||
is_anonymous=is_anonymous,
|
||||
session_id=user_id,
|
||||
)
|
||||
session.add(user_model)
|
||||
session.commit()
|
||||
session.refresh(user_model)
|
||||
|
||||
except Exception:
|
||||
raise ValueError("user not found")
|
||||
|
||||
return user_model
|
||||
|
||||
|
||||
def get_user_tenant(view_func: Callable[P, R]):
|
||||
@wraps(view_func)
|
||||
def decorated_view(*args: P.args, **kwargs: P.kwargs):
|
||||
session_id = request.headers.get("X-Cli-Api-Session-Id")
|
||||
|
||||
if session_id:
|
||||
session: CliApiSession | None = CliApiSessionManager().get(session_id)
|
||||
if not session:
|
||||
raise ValueError("session not found")
|
||||
user_id = session.user_id
|
||||
tenant_id = session.tenant_id
|
||||
else:
|
||||
payload = TenantUserPayload.model_validate(request.get_json(silent=True) or {})
|
||||
user_id = payload.user_id
|
||||
tenant_id = payload.tenant_id
|
||||
|
||||
if not tenant_id:
|
||||
raise ValueError("tenant_id is required")
|
||||
|
||||
if not user_id:
|
||||
user_id = DefaultEndUserSessionID.DEFAULT_SESSION_ID
|
||||
|
||||
try:
|
||||
tenant_model = (
|
||||
db.session.query(Tenant)
|
||||
.where(
|
||||
Tenant.id == tenant_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
except Exception:
|
||||
raise ValueError("tenant not found")
|
||||
|
||||
if not tenant_model:
|
||||
raise ValueError("tenant not found")
|
||||
|
||||
kwargs["tenant_model"] = tenant_model
|
||||
|
||||
user = get_user(tenant_id, user_id)
|
||||
kwargs["user_model"] = user
|
||||
|
||||
current_app.login_manager._update_request_context_with_user(user) # type: ignore
|
||||
user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore
|
||||
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
return decorated_view
|
||||
|
||||
|
||||
def plugin_data(view: Callable[P, R] | None = None, *, payload_type: type[BaseModel]):
|
||||
def decorator(view_func: Callable[P, R]):
|
||||
def decorated_view(*args: P.args, **kwargs: P.kwargs):
|
||||
try:
|
||||
data = request.get_json()
|
||||
except Exception:
|
||||
raise ValueError("invalid json")
|
||||
|
||||
try:
|
||||
payload = payload_type.model_validate(data)
|
||||
except Exception as e:
|
||||
raise ValueError(f"invalid payload: {str(e)}")
|
||||
|
||||
kwargs["payload"] = payload
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
return decorated_view
|
||||
|
||||
if view is None:
|
||||
return decorator
|
||||
else:
|
||||
return decorator(view)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import ParamSpec, TypeVar
|
||||
|
||||
from flask import abort, request
|
||||
|
||||
from core.session.cli_api import CliApiSessionManager
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
SIGNATURE_TTL_SECONDS = 300
|
||||
|
||||
|
||||
def _verify_signature(session_secret: str, timestamp: str, body: bytes, signature: str) -> bool:
|
||||
expected = hmac.new(
|
||||
session_secret.encode(),
|
||||
f"{timestamp}.".encode() + body,
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(f"sha256={expected}", signature)
|
||||
|
||||
|
||||
def cli_api_only(view: Callable[P, R]):
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||
session_id = request.headers.get("X-Cli-Api-Session-Id")
|
||||
timestamp = request.headers.get("X-Cli-Api-Timestamp")
|
||||
signature = request.headers.get("X-Cli-Api-Signature")
|
||||
|
||||
if not session_id or not timestamp or not signature:
|
||||
abort(401)
|
||||
|
||||
try:
|
||||
ts = int(timestamp)
|
||||
if abs(time.time() - ts) > SIGNATURE_TTL_SECONDS:
|
||||
abort(401)
|
||||
except ValueError:
|
||||
abort(401)
|
||||
|
||||
session = CliApiSessionManager().get(session_id)
|
||||
if not session:
|
||||
abort(401)
|
||||
|
||||
body = request.get_data()
|
||||
if not _verify_signature(session.secret, timestamp, body, signature):
|
||||
abort(401)
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
|
@ -7,7 +7,6 @@ from flask_login import user_logged_in
|
|||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.session.inner_api import InnerApiSession, InnerApiSessionManager
|
||||
from extensions.ext_database import db
|
||||
from libs.login import current_user
|
||||
from models.account import Tenant
|
||||
|
|
@ -75,18 +74,9 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
|||
def get_user_tenant(view_func: Callable[P, R]):
|
||||
@wraps(view_func)
|
||||
def decorated_view(*args: P.args, **kwargs: P.kwargs):
|
||||
session_id = request.headers.get("X-Inner-Api-Session-Id")
|
||||
|
||||
if session_id:
|
||||
session: InnerApiSession | None = InnerApiSessionManager().get(session_id)
|
||||
if not session:
|
||||
raise ValueError("session not found")
|
||||
user_id = session.user_id
|
||||
tenant_id = session.tenant_id
|
||||
else:
|
||||
payload = TenantUserPayload.model_validate(request.get_json(silent=True) or {})
|
||||
user_id = payload.user_id
|
||||
tenant_id = payload.tenant_id
|
||||
payload = TenantUserPayload.model_validate(request.get_json(silent=True) or {})
|
||||
user_id = payload.user_id
|
||||
tenant_id = payload.tenant_id
|
||||
|
||||
if not tenant_id:
|
||||
raise ValueError("tenant_id is required")
|
||||
|
|
|
|||
|
|
@ -5,16 +5,15 @@ from hashlib import sha1
|
|||
from hmac import new as hmac_new
|
||||
from typing import ParamSpec, TypeVar
|
||||
|
||||
from core.session.inner_api import InnerApiSessionManager
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
from flask import abort, request
|
||||
|
||||
from configs import dify_config
|
||||
from extensions.ext_database import db
|
||||
from models.model import EndUser
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
def billing_inner_api_only(view: Callable[P, R]):
|
||||
@wraps(view)
|
||||
|
|
@ -87,19 +86,14 @@ def enterprise_inner_api_user_auth(view: Callable[P, R]):
|
|||
def plugin_inner_api_only(view: Callable[P, R]):
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||
# if session id is provided, using session id to validate
|
||||
session_id = request.headers.get("X-Inner-Api-Session-Id")
|
||||
if session_id and InnerApiSessionManager().exists(session_id):
|
||||
return view(*args, **kwargs)
|
||||
|
||||
if not dify_config.PLUGIN_DAEMON_KEY:
|
||||
abort(404)
|
||||
|
||||
# if inner api key is provided, using inner api key to validate
|
||||
# validate using inner api key
|
||||
inner_api_key = request.headers.get("X-Inner-Api-Key")
|
||||
if inner_api_key and inner_api_key == dify_config.INNER_API_KEY_FOR_PLUGIN:
|
||||
return view(*args, **kwargs)
|
||||
|
||||
abort(404)
|
||||
abort(401)
|
||||
|
||||
return decorated
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ from typing import TYPE_CHECKING, Any
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.sandbox.constants import DIFY_CLI_PATH_PATTERN
|
||||
from core.session.cli_api import CliApiSession
|
||||
from core.tools.entities.tool_entities import ToolProviderType
|
||||
from core.virtual_environment.__base.entities import Arch, OperatingSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -48,8 +50,9 @@ class DifyCliLocator:
|
|||
|
||||
class DifyCliEnvConfig(BaseModel):
|
||||
files_url: str
|
||||
inner_api_url: str
|
||||
inner_api_session_id: str
|
||||
cli_api_url: str
|
||||
cli_api_session_id: str
|
||||
cli_api_secret: str
|
||||
|
||||
|
||||
class DifyCliToolConfig(BaseModel):
|
||||
|
|
@ -58,10 +61,22 @@ class DifyCliToolConfig(BaseModel):
|
|||
description: dict[str, Any]
|
||||
parameters: list[dict[str, Any]]
|
||||
|
||||
@classmethod
|
||||
def transform_provider_type(cls, tool_provider_type: ToolProviderType) -> str:
|
||||
provider_type = tool_provider_type
|
||||
match tool_provider_type:
|
||||
case ToolProviderType.BUILT_IN | ToolProviderType.PLUGIN:
|
||||
provider_type = "builtin"
|
||||
case ToolProviderType.MCP | ToolProviderType.WORKFLOW | ToolProviderType.API:
|
||||
provider_type = provider_type
|
||||
case _:
|
||||
raise ValueError(f"Invalid tool provider type: {tool_provider_type}")
|
||||
return provider_type
|
||||
|
||||
@classmethod
|
||||
def create_from_tool(cls, tool: Tool) -> DifyCliToolConfig:
|
||||
return cls(
|
||||
provider_type=tool.tool_provider_type().value,
|
||||
provider_type=cls.transform_provider_type(tool.tool_provider_type()),
|
||||
identity=tool.entity.identity.model_dump(),
|
||||
description=tool.entity.description.model_dump() if tool.entity.description else {},
|
||||
parameters=[param.model_dump() for param in tool.entity.parameters],
|
||||
|
|
@ -73,14 +88,17 @@ class DifyCliConfig(BaseModel):
|
|||
tools: list[DifyCliToolConfig]
|
||||
|
||||
@classmethod
|
||||
def create(cls, session_id: str, tools: list[Tool]) -> DifyCliConfig:
|
||||
def create(cls, session: CliApiSession, tools: list[Tool]) -> DifyCliConfig:
|
||||
from configs import dify_config
|
||||
|
||||
cli_api_url = dify_config.CLI_API_URL
|
||||
|
||||
return cls(
|
||||
env=DifyCliEnvConfig(
|
||||
files_url=dify_config.FILES_URL,
|
||||
inner_api_url=dify_config.CONSOLE_API_URL,
|
||||
inner_api_session_id=session_id,
|
||||
cli_api_url=cli_api_url,
|
||||
cli_api_session_id=session.id,
|
||||
cli_api_secret=session.secret,
|
||||
),
|
||||
tools=[DifyCliToolConfig.create_from_tool(tool) for tool in tools],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from core.sandbox.bash_tool import SandboxBashTool
|
|||
from core.sandbox.constants import DIFY_CLI_CONFIG_PATH, DIFY_CLI_PATH
|
||||
from core.sandbox.dify_cli import DifyCliConfig
|
||||
from core.sandbox.manager import SandboxManager
|
||||
from core.session.inner_api import InnerApiSessionManager
|
||||
from core.session.cli_api import CliApiSessionManager
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
|
||||
|
||||
|
|
@ -39,11 +39,11 @@ class SandboxSession:
|
|||
if sandbox is None:
|
||||
raise RuntimeError(f"Sandbox not found for workflow_execution_id={self._workflow_execution_id}")
|
||||
|
||||
session = InnerApiSessionManager().create(tenant_id=self._tenant_id, user_id=self._user_id)
|
||||
session = CliApiSessionManager().create(tenant_id=self._tenant_id, user_id=self._user_id)
|
||||
self._session_id = session.id
|
||||
|
||||
try:
|
||||
config = DifyCliConfig.create(self._session_id, self._tools)
|
||||
config = DifyCliConfig.create(session, self._tools)
|
||||
config_json = json.dumps(config.model_dump(mode="json"), ensure_ascii=False)
|
||||
|
||||
sandbox.upload_file(DIFY_CLI_CONFIG_PATH, BytesIO(config_json.encode("utf-8")))
|
||||
|
|
@ -58,7 +58,7 @@ class SandboxSession:
|
|||
sandbox.release_connection(connection_handle)
|
||||
|
||||
except Exception:
|
||||
InnerApiSessionManager().delete(session.id)
|
||||
CliApiSessionManager().delete(session.id)
|
||||
self._session_id = None
|
||||
raise
|
||||
|
||||
|
|
@ -85,11 +85,9 @@ class SandboxSession:
|
|||
return self._bash_tool
|
||||
|
||||
def cleanup(self) -> None:
|
||||
from core.session.inner_api import InnerApiSessionManager
|
||||
|
||||
if self._session_id is None:
|
||||
return
|
||||
|
||||
InnerApiSessionManager().delete(self._session_id)
|
||||
CliApiSessionManager().delete(self._session_id)
|
||||
logger.debug("Cleaned up SandboxSession session_id=%s", self._session_id)
|
||||
self._session_id = None
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from .inner_api import InnerApiSession, InnerApiSessionManager
|
||||
from .cli_api import CliApiSession, CliApiSessionManager
|
||||
from .session import BaseSession, RedisSessionStorage, SessionManager, SessionStorage
|
||||
|
||||
__all__ = [
|
||||
"BaseSession",
|
||||
"InnerApiSession",
|
||||
"InnerApiSessionManager",
|
||||
"CliApiSession",
|
||||
"CliApiSessionManager",
|
||||
"RedisSessionStorage",
|
||||
"SessionManager",
|
||||
"SessionStorage",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import secrets
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from .session import BaseSession, SessionManager
|
||||
|
||||
|
||||
class CliApiSession(BaseSession):
|
||||
secret: str = Field(default_factory=lambda: secrets.token_urlsafe(32))
|
||||
secret: str = Field(default_factory=lambda: secrets.token_urlsafe(32))
|
||||
|
||||
|
||||
class CliApiSessionManager(SessionManager[CliApiSession]):
|
||||
def __init__(self, ttl: int | None = None):
|
||||
super().__init__(key_prefix="cli_api_session", session_class=CliApiSession, ttl=ttl)
|
||||
|
||||
def create(self, tenant_id: str, user_id: str, context: dict[str, Any] | None = None) -> CliApiSession:
|
||||
session = CliApiSession(tenant_id=tenant_id, user_id=user_id, context=context or {})
|
||||
self.save(session)
|
||||
return session
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
from typing import Any
|
||||
|
||||
from .session import BaseSession, SessionManager
|
||||
|
||||
|
||||
class InnerApiSession(BaseSession):
|
||||
"""Inner API Session"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InnerApiSessionManager(SessionManager[InnerApiSession]):
|
||||
def __init__(self, ttl: int | None = None):
|
||||
super().__init__(key_prefix="inner_api_session", session_class=InnerApiSession, ttl=ttl)
|
||||
|
||||
def create(self, tenant_id: str, user_id: str, context: dict[str, Any] | None = None) -> InnerApiSession:
|
||||
session = InnerApiSession(tenant_id=tenant_id, user_id=user_id, context=context or {})
|
||||
self.save(session)
|
||||
return session
|
||||
|
|
@ -25,6 +25,7 @@ def _apply_cors_once(bp, /, **cors_kwargs):
|
|||
def init_app(app: DifyApp):
|
||||
# register blueprint routers
|
||||
|
||||
from controllers.cli_api import bp as cli_api_bp
|
||||
from controllers.console import bp as console_app_bp
|
||||
from controllers.files import bp as files_bp
|
||||
from controllers.inner_api import bp as inner_api_bp
|
||||
|
|
@ -88,6 +89,7 @@ def init_app(app: DifyApp):
|
|||
app.register_blueprint(files_bp)
|
||||
|
||||
app.register_blueprint(inner_api_bp)
|
||||
app.register_blueprint(cli_api_bp)
|
||||
app.register_blueprint(mcp_bp)
|
||||
|
||||
# Register trigger blueprint with CORS for webhook calls
|
||||
|
|
|
|||
|
|
@ -989,6 +989,12 @@ EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
|
|||
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||
|
||||
# CLI API URL for sandbox (dify-sandbox or e2b) to call back to Dify API.
|
||||
# This URL must be accessible from the sandbox environment.
|
||||
# For Docker deployment: use http://api:5001 (internal Docker network)
|
||||
# For external sandbox (e.g., e2b): use a publicly accessible URL
|
||||
CLI_API_URL=http://api:5001
|
||||
|
||||
# The sandbox service endpoint.
|
||||
CODE_EXECUTION_ENDPOINT=http://sandbox:8194
|
||||
CODE_EXECUTION_API_KEY=dify-sandbox
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ server {
|
|||
include proxy.conf;
|
||||
}
|
||||
|
||||
location /cli {
|
||||
proxy_pass http://api:5001;
|
||||
include proxy.conf;
|
||||
}
|
||||
|
||||
# placeholder for acme challenge location
|
||||
${ACME_CHALLENGE_LOCATION}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue