mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
104 lines
4.7 KiB
Python
104 lines
4.7 KiB
Python
"""Package ordinary App DSLs while retaining the existing Roster Agent format."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import io
|
|
import re
|
|
import tempfile
|
|
import zipfile
|
|
import zlib
|
|
from typing import BinaryIO, Literal, cast
|
|
|
|
import yaml
|
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
|
|
from services.agent.errors import InvalidRosterAgentPackageError, RosterAgentPackageTooLargeError
|
|
from services.agent.roster_package_entities import RosterAgentPackageApp, RosterAgentPackageExport
|
|
from services.agent.roster_package_reader import RosterAgentPackageReader
|
|
from services.dsl_content import DSL_MAX_SIZE
|
|
|
|
|
|
class AppPackageManifest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
format: Literal["dify.app"]
|
|
format_version: Literal[1]
|
|
apps: list[RosterAgentPackageApp] = Field(min_length=1, max_length=1)
|
|
|
|
|
|
class AppPackageService(RosterAgentPackageReader):
|
|
"""Reuse the bounded ZIP reader; ordinary Apps retain DSL import semantics."""
|
|
|
|
def read_dsl(self, source: BinaryIO) -> str | None:
|
|
"""Return an ordinary DSL, or rewind a Roster package for its importer."""
|
|
try:
|
|
with tempfile.SpooledTemporaryFile(max_size=16 * 1024 * 1024, mode="w+b") as spool:
|
|
self._copy_bounded(source, cast(BinaryIO, spool))
|
|
spool.seek(0)
|
|
with zipfile.ZipFile(spool) as archive:
|
|
infos = self._index_archive(archive)
|
|
data, _ = self._read_yaml_document(archive, infos, "manifest.yaml")
|
|
if isinstance(data, dict) and data.get("format") == "dify.roster-agent":
|
|
return None
|
|
try:
|
|
manifest = AppPackageManifest.model_validate(data)
|
|
except ValidationError as exc:
|
|
raise InvalidRosterAgentPackageError("App package manifest is invalid") from exc
|
|
resource = manifest.apps[0]
|
|
if set(infos) != {"manifest.yaml", resource.path}:
|
|
raise InvalidRosterAgentPackageError("App package members do not match the manifest")
|
|
app_data, _ = self._read_yaml_document(
|
|
archive, infos, resource.path, resource=resource, max_bytes=DSL_MAX_SIZE
|
|
)
|
|
if (
|
|
not isinstance(app_data, dict)
|
|
or app_data.get("kind") != "app"
|
|
or not isinstance(app_data.get("app"), dict)
|
|
or app_data["app"].get("mode")
|
|
not in {
|
|
"workflow",
|
|
"advanced-chat",
|
|
"chat",
|
|
"completion",
|
|
"agent-chat",
|
|
"channel",
|
|
"rag-pipeline",
|
|
}
|
|
):
|
|
raise InvalidRosterAgentPackageError("App package DSL is invalid")
|
|
# Preserve the original text for version confirmation and legacy DSL handling.
|
|
return archive.read(resource.path).decode("utf-8")
|
|
except (InvalidRosterAgentPackageError, RosterAgentPackageTooLargeError):
|
|
raise
|
|
except (OSError, zipfile.BadZipFile, EOFError, RuntimeError, ValueError, zlib.error) as exc:
|
|
raise InvalidRosterAgentPackageError("App package is not a valid ZIP") from exc
|
|
finally:
|
|
source.seek(0)
|
|
|
|
def export(self, *, dsl: str, name: str) -> RosterAgentPackageExport:
|
|
payload = dsl.encode("utf-8")
|
|
if len(payload) > DSL_MAX_SIZE:
|
|
raise RosterAgentPackageTooLargeError("App package DSL exceeds the size limit")
|
|
manifest = AppPackageManifest(
|
|
format="dify.app",
|
|
format_version=1,
|
|
apps=[
|
|
RosterAgentPackageApp(path="app.yaml", size=len(payload), sha256=hashlib.sha256(payload).hexdigest())
|
|
],
|
|
)
|
|
archive = io.BytesIO()
|
|
try:
|
|
with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as package:
|
|
package.writestr("manifest.yaml", yaml.safe_dump(manifest.model_dump(mode="json")))
|
|
package.writestr("app.yaml", payload)
|
|
size = archive.tell()
|
|
archive.seek(0)
|
|
# Validate exported containers against the same limits as imports.
|
|
self.read_dsl(archive)
|
|
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")[:80] or "app"
|
|
return RosterAgentPackageExport(archive=archive, filename=f"{slug}.ifpkg", size=size)
|
|
except Exception:
|
|
archive.close()
|
|
raise
|