feat(sandbox): skill initialize & draft run

This commit is contained in:
Harry
2026-01-19 18:15:24 +08:00
parent 3bb9c4b280
commit 956436b943
18 changed files with 531 additions and 280 deletions

View File

@ -1,33 +1,25 @@
import logging
from io import BytesIO
from sqlalchemy.orm import Session
from core.app_assets.paths import AssetPaths
from core.virtual_environment.__base.helpers import execute, with_connection
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
from extensions.ext_database import db
from extensions.ext_storage import storage
from models.app_asset import AppAssets
from ..constants import APP_ASSETS_PATH, APP_ASSETS_ZIP_PATH, DIFY_CLI_ROOT
from ..constants import APP_ASSETS_PATH, APP_ASSETS_ZIP_PATH
from .base import SandboxInitializer
logger = logging.getLogger(__name__)
class AppAssetsInitializer(SandboxInitializer):
def __init__(self, tenant_id: str, app_id: str) -> None:
def __init__(self, tenant_id: str, app_id: str, assets_id: str) -> None:
self._tenant_id = tenant_id
self._app_id = app_id
self._assets_id = assets_id
def initialize(self, env: VirtualEnvironment) -> None:
published = self._get_latest_published()
if not published:
logger.debug("No published assets for app_id=%s, skipping", self._app_id)
return
zip_key = AssetPaths.published_zip(self._tenant_id, self._app_id, published.id)
zip_key = AssetPaths.build_zip(self._tenant_id, self._app_id, self._assets_id)
try:
zip_data = storage.load_once(zip_key)
except Exception:
@ -42,18 +34,6 @@ class AppAssetsInitializer(SandboxInitializer):
env.upload_file(APP_ASSETS_ZIP_PATH, BytesIO(zip_data))
with with_connection(env) as conn:
execute(
env,
["mkdir", "-p", f"{DIFY_CLI_ROOT}/tmp"],
connection=conn,
error_message="Failed to create temp directory",
)
execute(
env,
["mkdir", "-p", APP_ASSETS_PATH],
connection=conn,
error_message="Failed to create assets directory",
)
execute(
env,
["unzip", "-o", APP_ASSETS_ZIP_PATH, "-d", APP_ASSETS_PATH],
@ -71,18 +51,5 @@ class AppAssetsInitializer(SandboxInitializer):
logger.info(
"App assets initialized for app_id=%s, published_id=%s",
self._app_id,
published.id,
self._assets_id,
)
def _get_latest_published(self) -> AppAssets | None:
with Session(db.engine) as session:
return (
session.query(AppAssets)
.filter(
AppAssets.tenant_id == self._tenant_id,
AppAssets.app_id == self._app_id,
AppAssets.version != AppAssets.VERSION_DRAFT,
)
.order_by(AppAssets.created_at.desc())
.first()
)

View File

@ -1,37 +1,147 @@
from __future__ import annotations
import json
import logging
from io import BytesIO
from pathlib import Path
from core.virtual_environment.__base.helpers import execute
from core.app.entities.app_invoke_entities import InvokeFrom
from core.app_assets.entities import ToolType
from core.session.cli_api import CliApiSessionManager
from core.skill.entities import ToolManifest
from core.skill.skill_manager import SkillManager
from core.tools.__base.tool import Tool
from core.tools.entities.tool_entities import ToolProviderType
from core.tools.tool_manager import ToolManager
from core.virtual_environment.__base.helpers import execute, with_connection
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
from ..bash.dify_cli import DifyCliLocator
from ..constants import DIFY_CLI_PATH, DIFY_CLI_ROOT
from ..bash.dify_cli import DifyCliConfig, DifyCliLocator
from ..constants import (
DIFY_CLI_CONFIG_FILENAME,
DIFY_CLI_GLOBAL_TOOLS_PATH,
DIFY_CLI_PATH,
DIFY_CLI_ROOT,
)
from .base import SandboxInitializer
logger = logging.getLogger(__name__)
class DifyCliInitializer(SandboxInitializer):
def __init__(self, cli_root: str | Path | None = None) -> None:
def __init__(
self,
tenant_id: str,
app_id: str,
assets_id: str,
cli_root: str | Path | None = None,
) -> None:
self._tenant_id = tenant_id
self._app_id = app_id
self._assets_id = assets_id
self._locator = DifyCliLocator(root=cli_root)
self._tools = []
self._cli_api_session = None
def initialize(self, env: VirtualEnvironment) -> None:
binary = self._locator.resolve(env.metadata.os, env.metadata.arch)
execute(
env,
["mkdir", "-p", f"{DIFY_CLI_ROOT}/bin"],
timeout=10,
error_message="Failed to create dify CLI directory",
)
with with_connection(env) as conn:
execute(
env,
["mkdir", "-p", f"{DIFY_CLI_ROOT}/bin"],
connection=conn,
timeout=10,
error_message="Failed to create dify CLI directory",
)
env.upload_file(DIFY_CLI_PATH, BytesIO(binary.path.read_bytes()))
env.upload_file(DIFY_CLI_PATH, BytesIO(binary.path.read_bytes()))
execute(
env,
["chmod", "+x", DIFY_CLI_PATH],
timeout=10,
error_message="Failed to mark dify CLI as executable",
)
logger.info("Dify CLI uploaded to sandbox, path=%s", DIFY_CLI_PATH)
execute(
env,
["chmod", "+x", DIFY_CLI_PATH],
connection=conn,
timeout=10,
error_message="Failed to mark dify CLI as executable",
)
logger.info("Dify CLI uploaded to sandbox, path=%s", DIFY_CLI_PATH)
manifest = SkillManager.load_tool_manifest(
self._tenant_id,
self._app_id,
self._assets_id,
)
if manifest is None or not manifest.tools:
logger.info("No tools found in manifest for assets_id=%s", self._assets_id)
return
self._tools = self._resolve_tools_from_manifest(manifest)
self._cli_api_session = CliApiSessionManager().create(tenant_id=self._tenant_id, user_id="system")
execute(
env,
["mkdir", "-p", DIFY_CLI_GLOBAL_TOOLS_PATH],
connection=conn,
timeout=10,
error_message="Failed to create global tools directory",
)
config_json = json.dumps(
DifyCliConfig.create(self._cli_api_session, self._tools).model_dump(mode="json"), ensure_ascii=False
)
env.upload_file(
f"{DIFY_CLI_GLOBAL_TOOLS_PATH}/{DIFY_CLI_CONFIG_FILENAME}", BytesIO(config_json.encode("utf-8"))
)
execute(
env,
[DIFY_CLI_PATH, "init"],
connection=conn,
timeout=30,
cwd=DIFY_CLI_GLOBAL_TOOLS_PATH,
error_message="Failed to initialize Dify CLI",
)
logger.info(
"Global tools initialized, path=%s, tool_count=%d",
DIFY_CLI_GLOBAL_TOOLS_PATH,
len(self._tools),
)
def _resolve_tools_from_manifest(self, manifest: ToolManifest) -> list[Tool]:
tools: list[Tool] = []
for entry in manifest.tools.values():
if entry.provider is None or entry.tool_name is None:
logger.warning("Skipping tool entry with missing provider or tool_name: %s", entry.uuid)
continue
try:
provider_type = self._convert_tool_type(entry.type)
tool = ToolManager.get_tool_runtime(
tenant_id=self._tenant_id,
provider_type=provider_type,
provider_id=entry.provider,
tool_name=entry.tool_name,
invoke_from=InvokeFrom.AGENT,
credential_id=entry.credential_id,
)
tools.append(tool)
except Exception as e:
logger.warning("Failed to resolve tool %s/%s: %s", entry.provider, entry.tool_name, e)
continue
return tools
@staticmethod
def _convert_tool_type(tool_type: ToolType) -> ToolProviderType:
match tool_type:
case ToolType.BUILTIN:
return ToolProviderType.BUILT_IN
case ToolType.MCP:
return ToolProviderType.MCP
case _:
raise ValueError(f"Unsupported tool type: {tool_type}")