Revert "feat(agent): version agent soul drive file refs"

This reverts commit a6a6bc322b.
This commit is contained in:
Yanli 盐粒 2026-06-25 16:18:56 +08:00
parent 3f653b2dfb
commit 3948133faf
10 changed files with 101 additions and 494 deletions

View File

@ -297,7 +297,15 @@ def _delete_drive_file_for_app(*, current_user: Account, app_model: App, allow_n
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
result = [{"key": key, "removed": True}]
try:
result = AgentDriveService().commit(
tenant_id=app_model.tenant_id,
user_id=current_user.id,
agent_id=agent_id,
items=[DriveCommitItem(key=key, file_ref=None)],
)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
_sync_active_soul_files(
tenant_id=app_model.tenant_id,
agent_id=agent_id,
@ -318,33 +326,24 @@ def _delete_skill_for_app(*, current_user: Account, app_model: App, slug: str, a
return {"code": "drive_key_invalid", "message": "skill slug must be a single path segment"}, 400
try:
agent_soul = AgentSoulFilesService.active_agent_soul(
session=db.session,
result = AgentDriveService().commit(
tenant_id=app_model.tenant_id,
user_id=current_user.id,
agent_id=agent_id,
items=[
DriveCommitItem(key=f"{slug}/SKILL.md", file_ref=None),
DriveCommitItem(key=f"{slug}/.DIFY-SKILL-FULL.zip", file_ref=None),
],
)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
skill_prefix = f"{slug}/"
removed_keys = [
file_ref.drive_key
for skill in agent_soul.files.skills
if (
skill.path or (AgentSoulFilesService.skill_path_from_key(skill.skill_md_key) if skill.skill_md_key else "")
).strip("/")
== slug
for file_ref in skill.file_refs
if file_ref.drive_key
]
if f"{slug}/SKILL.md" not in removed_keys:
removed_keys.append(f"{slug}/SKILL.md")
result = [{"key": key, "removed": True} for key in removed_keys if key.startswith(skill_prefix)]
_sync_active_soul_files(
tenant_id=app_model.tenant_id,
agent_id=agent_id,
account_id=current_user.id,
committed_items=result,
)
removed_keys = [item["key"] for item in result if item.get("removed")]
return {"result": "success", "removed_keys": removed_keys}

View File

@ -31,7 +31,6 @@ from controllers.console.wraps import account_initialization_required, setup_req
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.login import login_required
from models.agent import AgentDriveFileKind
from models.model import App, AppMode
from services.agent.composer_service import AgentComposerService
from services.agent.soul_files_service import AgentSoulFilesService
@ -168,12 +167,7 @@ def _versioned_manifest(*, tenant_id: str, agent_id: str, prefix: str = "") -> l
normalized_prefix = prefix.strip().lstrip("/")
skill_prefixes = AgentSoulFilesService.allowed_skill_prefixes(agent_soul)
if normalized_prefix and any(normalized_prefix.startswith(p) for p in skill_prefixes):
return AgentSoulFilesService.list_manifest_items(
session=db.session,
tenant_id=tenant_id,
agent_id=agent_id,
prefix=normalized_prefix,
)
return AgentDriveService().manifest(tenant_id=tenant_id, agent_id=agent_id, prefix=normalized_prefix)
return AgentSoulFilesService.list_files(
session=db.session,
tenant_id=tenant_id,
@ -196,108 +190,6 @@ def _assert_key_in_active_soul(*, tenant_id: str, agent_id: str, key: str) -> No
)
def _file_ref_for_active_soul(*, tenant_id: str, agent_id: str, key: str):
agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id)
file_ref = AgentSoulFilesService.file_ref_for_key(agent_soul=agent_soul, key=key)
if file_ref is None and not AgentSoulFilesService.key_allowed_by_soul(agent_soul=agent_soul, key=key):
raise AgentDriveError(
"drive_key_not_in_agent_soul",
"drive key is not part of the active Agent Soul version",
status_code=404,
)
return file_ref
def _archive_member_ref_for_active_soul(
*, tenant_id: str, agent_id: str, key: str
) -> tuple[AgentDriveFileKind, str, str] | None:
agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id)
normalized_key = key.strip().lstrip("/")
for skill in agent_soul.files.skills:
skill_path = skill.path or (
AgentSoulFilesService.skill_path_from_key(skill.skill_md_key) if skill.skill_md_key else None
)
if not skill_path or not normalized_key.startswith(f"{skill_path}/"):
continue
member_path = normalized_key.removeprefix(f"{skill_path}/")
if member_path == ".DIFY-SKILL-FULL.zip":
return None
manifest_files = {str(path).strip().lstrip("/") for path in (skill.manifest_files or [])}
if manifest_files and member_path not in manifest_files:
return None
archive_file_id = skill.full_archive_file_id
if not archive_file_id:
return None
return AgentDriveFileKind.TOOL_FILE, archive_file_id, member_path
return None
def _file_kind_from_ref(file_ref) -> AgentDriveFileKind | None:
raw = file_ref.transfer_method or ("upload_file" if file_ref.upload_file_id else None)
if raw is None:
return None
try:
return AgentDriveFileKind(raw)
except ValueError as exc:
raise AgentDriveError("invalid_drive_file_ref", "Agent Soul file ref has invalid transfer method") from exc
def _preview_versioned_file(*, tenant_id: str, agent_id: str, key: str) -> dict[str, Any]:
file_ref = _file_ref_for_active_soul(tenant_id=tenant_id, agent_id=agent_id, key=key)
if file_ref is None:
archive_member = _archive_member_ref_for_active_soul(tenant_id=tenant_id, agent_id=agent_id, key=key)
if archive_member is not None:
archive_file_kind, archive_file_id, member_path = archive_member
return AgentDriveService().preview_archive_member_for_ref(
tenant_id=tenant_id,
agent_id=agent_id,
key=key,
archive_file_kind=archive_file_kind,
archive_file_id=archive_file_id,
member_path=member_path,
)
return AgentDriveService().preview(tenant_id=tenant_id, agent_id=agent_id, key=key)
file_kind = _file_kind_from_ref(file_ref)
file_id = file_ref.file_id or file_ref.upload_file_id
if file_kind is None or not file_id:
raise AgentDriveError("invalid_drive_file_ref", "Agent Soul file ref is missing file id", status_code=404)
return AgentDriveService().preview_file_ref(
tenant_id=tenant_id,
agent_id=agent_id,
key=key,
file_kind=file_kind,
file_id=file_id,
size=file_ref.get("size"),
)
def _download_versioned_file(*, tenant_id: str, agent_id: str, key: str) -> str:
file_ref = _file_ref_for_active_soul(tenant_id=tenant_id, agent_id=agent_id, key=key)
if file_ref is None:
archive_member = _archive_member_ref_for_active_soul(tenant_id=tenant_id, agent_id=agent_id, key=key)
if archive_member is not None:
archive_file_kind, archive_file_id, member_path = archive_member
return AgentDriveService().download_url_archive_member_for_ref(
tenant_id=tenant_id,
agent_id=agent_id,
key=key,
archive_file_kind=archive_file_kind,
archive_file_id=archive_file_id,
member_path=member_path,
)
return AgentDriveService().download_url(tenant_id=tenant_id, agent_id=agent_id, key=key)
file_kind = _file_kind_from_ref(file_ref)
file_id = file_ref.file_id or file_ref.upload_file_id
if file_kind is None or not file_id:
raise AgentDriveError("invalid_drive_file_ref", "Agent Soul file ref is missing file id", status_code=404)
return AgentDriveService().download_url_for_ref(
tenant_id=tenant_id,
agent_id=agent_id,
file_kind=file_kind,
file_id=file_id,
)
def _assert_skill_in_active_soul(*, tenant_id: str, agent_id: str, skill_path: str) -> None:
agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id)
wanted = skill_path.strip().strip("/")
@ -402,7 +294,8 @@ class AgentDrivePreviewByAgentApi(Resource):
query = query_params_from_request(AgentDriveFileByAgentQuery)
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
return _preview_versioned_file(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
_assert_key_in_active_soul(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
return AgentDriveService().preview(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
except AgentDriveError as exc:
return _handle(exc)
@ -421,7 +314,8 @@ class AgentDriveDownloadByAgentApi(Resource):
query = query_params_from_request(AgentDriveFileByAgentQuery)
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
url = _download_versioned_file(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
_assert_key_in_active_soul(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
url = AgentDriveService().download_url(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
except AgentDriveError as exc:
return _handle(exc)
return {"url": url}
@ -523,7 +417,8 @@ class AgentDrivePreviewApi(Resource):
if not agent_id:
return _agent_not_bound()
try:
return _preview_versioned_file(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key)
_assert_key_in_active_soul(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key)
return AgentDriveService().preview(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key)
except AgentDriveError as exc:
return _handle(exc)
@ -544,7 +439,8 @@ class AgentDriveDownloadApi(Resource):
if not agent_id:
return _agent_not_bound()
try:
url = _download_versioned_file(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key)
_assert_key_in_active_soul(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key)
url = AgentDriveService().download_url(tenant_id=app_model.tenant_id, agent_id=agent_id, key=query.key)
except AgentDriveError as exc:
return _handle(exc)
return {"url": url}

View File

@ -18,7 +18,6 @@ from controllers.inner_api import inner_api_ns
from controllers.inner_api.plugin.wraps import get_user
from controllers.inner_api.wraps import plugin_inner_api_only
from extensions.ext_database import db
from models.agent import AgentDriveFileKind
from services.agent.soul_files_service import AgentSoulFilesService
from services.agent_drive_service import (
AgentDriveError,
@ -47,31 +46,17 @@ def _versioned_manifest(
) -> list[dict[str, object]]:
agent_soul = AgentSoulFilesService.active_agent_soul(session=db.session, tenant_id=tenant_id, agent_id=agent_id)
normalized_prefix = prefix.strip().lstrip("/")
items = AgentSoulFilesService.list_manifest_items(
session=db.session,
items = AgentDriveService().manifest(
tenant_id=tenant_id,
agent_id=agent_id,
prefix=normalized_prefix,
include_download_url=include_download_url,
)
skill_prefixes = AgentSoulFilesService.allowed_skill_prefixes(agent_soul)
if normalized_prefix and not any(normalized_prefix.startswith(p) for p in skill_prefixes):
allowed_keys = AgentSoulFilesService.allowed_drive_keys(agent_soul)
items = [item for item in items if item.get("key") in allowed_keys]
if include_download_url:
for item in items:
file_kind = item.get("file_kind")
file_id = item.get("file_id")
if not file_kind or not file_id:
continue
try:
item["download_url"] = AgentDriveService.resolve_download_url_for_ref(
tenant_id=tenant_id,
file_kind=AgentDriveFileKind(str(file_kind)),
file_id=str(file_id),
)
except ValueError:
item["download_url"] = None
return items
if normalized_prefix and any(normalized_prefix.startswith(p) for p in skill_prefixes):
return items
allowed_keys = AgentSoulFilesService.allowed_drive_keys(agent_soul)
return [item for item in items if item.get("key") in allowed_keys]
@inner_api_ns.route("/drive/<string:drive_ref>/manifest")

View File

@ -72,7 +72,7 @@ from services.agent.prompt_mentions import (
parse_prompt_mentions,
)
from services.agent.soul_files_service import AgentSoulFilesService
from services.agent_drive_service import decode_drive_mention_ref
from services.agent_drive_service import AgentDriveError, AgentDriveService, decode_drive_mention_ref
from .output_failure_orchestrator import retry_idempotency_key
from .plugin_tools_builder import WorkflowAgentPluginToolsBuilder, WorkflowAgentPluginToolsBuildError
@ -735,11 +735,12 @@ def build_drive_layer_config(
for skill in agent_soul.files.skills
if skill.skill_md_key
]
soul_file_keys = {
key
for key in AgentSoulFilesService.allowed_drive_keys(agent_soul)
if key not in {skill["skill_md_key"] for skill in skills_catalog}
}
soul_file_keys = {file_ref.drive_key for file_ref in agent_soul.files.files if file_ref.drive_key}
try:
manifest_items = AgentDriveService().manifest(tenant_id=tenant_id, agent_id=agent_id)
except AgentDriveError:
manifest_items = []
manifest_by_key = {item["key"]: item for item in manifest_items}
skill_keys = {skill["skill_md_key"] for skill in skills_catalog}
warnings: list[dict[str, str]] = []
mentioned_skill_keys: list[str] = []
@ -758,6 +759,15 @@ def build_drive_layer_config(
"message": f"drive mention '{drive_key}' has no matching drive entry.",
}
)
for drive_key in sorted(skill_keys | soul_file_keys):
if drive_key not in manifest_by_key:
warnings.append(
{
"section": "agent_soul.files",
"code": "drive_value_missing",
"message": f"Agent Soul drive ref '{drive_key}' has no backing drive value.",
}
)
skills = [
DifyDriveSkillConfig(

View File

@ -160,10 +160,6 @@ class AgentSkillRefConfig(AgentFlexibleConfig):
# Zip member path listing from standardization (ENG-371): lets infer-tools
# show the model strong signals like ``scripts/*.sh`` without unpacking.
manifest_files: list[str] | None = None
# Versioned drive KV entries that belong to this skill package. The drive
# table remains the storage index, but Agent Soul owns which concrete file
# ids are visible for a snapshot/version.
file_refs: list[AgentFileRefConfig] = Field(default_factory=list)
class AgentSoulFilesConfig(BaseModel):

View File

@ -106,35 +106,6 @@ class AgentSoulFilesService:
items.append(item)
return items
@classmethod
def list_manifest_items(
cls,
*,
session: Session,
tenant_id: str,
agent_id: str,
prefix: str = "",
) -> list[dict[str, Any]]:
agent_soul = cls.active_agent_soul(session=session, tenant_id=tenant_id, agent_id=agent_id)
refs = cls._all_file_refs(agent_soul)
if prefix:
normalized_prefix = normalize_drive_key(prefix)
refs = [ref for ref in refs if (ref.drive_key or "").startswith(normalized_prefix)]
keys = [ref.drive_key for ref in refs if ref.drive_key]
rows = cls._drive_rows_by_key(session=session, tenant_id=tenant_id, agent_id=agent_id, keys=keys)
items: list[dict[str, Any]] = []
for file_ref in refs:
key = file_ref.drive_key
if not key:
continue
row = rows.get(key)
item = cls._file_item_from_ref(file_ref)
item.update(cls._row_item(row) if row is not None else {"key": key, "missing": True})
item["file_id"] = file_ref.file_id or file_ref.upload_file_id
items.append(item)
return sorted(items, key=lambda item: str(item.get("key") or ""))
@classmethod
def list_skills(
cls,
@ -155,16 +126,15 @@ class AgentSoulFilesService:
continue
row = rows.get(skill.skill_md_key)
archive_key = skill.full_archive_key if skill.full_archive_key in rows else None
skill_md_ref = cls.file_ref_for_key(agent_soul=agent_soul, key=skill.skill_md_key)
items.append(
{
"path": skill.path or cls.skill_path_from_key(skill.skill_md_key),
"skill_md_key": skill.skill_md_key,
"archive_key": archive_key or skill.full_archive_key,
"archive_key": archive_key,
"name": skill.name,
"description": skill.description,
"size": row.size if row is not None else None,
"mime_type": row.mime_type if row is not None else (skill_md_ref.type if skill_md_ref else None),
"mime_type": row.mime_type if row is not None else None,
"hash": row.hash if row is not None else None,
"created_at": int(row.created_at.timestamp()) if row is not None and row.created_at else None,
"missing": row is None,
@ -183,9 +153,6 @@ class AgentSoulFilesService:
keys.add(skill.skill_md_key)
if skill.full_archive_key:
keys.add(skill.full_archive_key)
for file_ref in skill.file_refs:
if file_ref.drive_key:
keys.add(file_ref.drive_key)
return keys
@classmethod
@ -204,18 +171,6 @@ class AgentSoulFilesService:
return True
return any(normalized_key.startswith(prefix) for prefix in cls.allowed_skill_prefixes(agent_soul))
@classmethod
def file_ref_for_key(cls, *, agent_soul: AgentSoulConfig, key: str) -> AgentFileRefConfig | None:
normalized_key = normalize_drive_key(key)
for file_ref in agent_soul.files.files:
if file_ref.drive_key == normalized_key:
return file_ref
for skill in agent_soul.files.skills:
for file_ref in skill.file_refs:
if file_ref.drive_key == normalized_key:
return file_ref
return None
@classmethod
def drive_copy_scopes(cls, *, agent_soul: AgentSoulConfig) -> tuple[set[str], set[str]]:
exact_keys = cls.allowed_drive_keys(agent_soul)
@ -268,8 +223,6 @@ class AgentSoulFilesService:
return
if key.startswith(_FILES_PREFIX):
cls._upsert_file_ref(agent_soul=agent_soul, key=key, item=item)
return
cls._upsert_skill_file_ref(agent_soul=agent_soul, key=key, item=item)
@classmethod
def _upsert_skill_ref(cls, *, agent_soul: AgentSoulConfig, key: str, item: dict[str, Any]) -> None:
@ -286,18 +239,6 @@ class AgentSoulFilesService:
full_archive_key=cls.skill_archive_key(key),
manifest_files=metadata.manifest_files,
)
existing_ref = next(
(existing for existing in agent_soul.files.skills if existing.skill_md_key == key or existing.path == path),
None,
)
file_refs = list(existing_ref.file_refs) if existing_ref else []
file_refs = [file_ref for file_ref in file_refs if file_ref.drive_key != key]
file_refs.append(cls._file_ref_from_item(key=key, item=item, name="SKILL.md"))
archive_key = cls.skill_archive_key(key)
archive_ref = next((file_ref for file_ref in file_refs if file_ref.drive_key == archive_key), None)
if archive_ref:
ref.full_archive_file_id = archive_ref.file_id
ref.file_refs = sorted(file_refs, key=lambda value: value.drive_key or value.name)
skills = [
existing for existing in agent_soul.files.skills if existing.skill_md_key != key and existing.path != path
]
@ -317,59 +258,12 @@ class AgentSoulFilesService:
type=str(item.get("mime_type") or ""),
transfer_method=str(item.get("file_kind") or ""),
drive_key=key,
size=item.get("size"),
hash=item.get("hash"),
)
files = [existing for existing in agent_soul.files.files if existing.drive_key != key]
files.append(ref)
files.sort(key=lambda value: value.drive_key or value.name)
agent_soul.files.files = files
@classmethod
def _upsert_skill_file_ref(cls, *, agent_soul: AgentSoulConfig, key: str, item: dict[str, Any]) -> None:
path = key.split("/", 1)[0]
if not path:
return
updated: list[AgentSkillRefConfig] = []
changed = False
for skill in agent_soul.files.skills:
skill_path = skill.path or (cls.skill_path_from_key(skill.skill_md_key) if skill.skill_md_key else "")
if skill_path != path:
updated.append(skill)
continue
file_ref = cls._file_ref_from_item(key=key, item=item)
file_refs = [existing for existing in skill.file_refs if existing.drive_key != key]
file_refs.append(file_ref)
replacement = skill.model_copy(
update={"file_refs": sorted(file_refs, key=lambda value: value.drive_key or value.name)}
)
if key.endswith(f"/{_SKILL_ARCHIVE_NAME}"):
replacement = replacement.model_copy(
update={
"full_archive_key": key,
"full_archive_file_id": file_ref.file_id,
}
)
updated.append(replacement)
changed = True
if changed:
agent_soul.files.skills = updated
@staticmethod
def _file_ref_from_item(*, key: str, item: dict[str, Any], name: str | None = None) -> AgentFileRefConfig:
file_id = str(item.get("file_id") or "")
return AgentFileRefConfig(
id=key,
file_id=file_id,
upload_file_id=file_id if item.get("file_kind") == "upload_file" else None,
name=name or key.rsplit("/", 1)[-1],
type=str(item.get("mime_type") or ""),
transfer_method=str(item.get("file_kind") or ""),
drive_key=key,
size=item.get("size"),
hash=item.get("hash"),
)
@classmethod
def _remove_ref(cls, *, agent_soul: AgentSoulConfig, key: str) -> None:
agent_soul.files.files = [file_ref for file_ref in agent_soul.files.files if file_ref.drive_key != key]
@ -381,35 +275,11 @@ class AgentSoulFilesService:
return
if key.endswith(f"/{_SKILL_ARCHIVE_NAME}"):
agent_soul.files.skills = [
skill.model_copy(
update={
"full_archive_key": None,
"full_archive_file_id": None,
"file_refs": [file_ref for file_ref in skill.file_refs if file_ref.drive_key != key],
}
)
skill.model_copy(update={"full_archive_key": None})
if skill.full_archive_key == key
else skill
for skill in agent_soul.files.skills
]
return
path = key.split("/", 1)[0]
if path:
agent_soul.files.skills = [
skill.model_copy(
update={"file_refs": [file_ref for file_ref in skill.file_refs if file_ref.drive_key != key]}
)
if (skill.path or (cls.skill_path_from_key(skill.skill_md_key) if skill.skill_md_key else "")) == path
else skill
for skill in agent_soul.files.skills
]
@staticmethod
def _all_file_refs(agent_soul: AgentSoulConfig) -> list[AgentFileRefConfig]:
refs = list(agent_soul.files.files)
for skill in agent_soul.files.skills:
refs.extend(skill.file_refs)
return refs
@staticmethod
def _parse_skill_metadata(raw_metadata: Any) -> DriveSkillMetadata:
@ -464,8 +334,6 @@ class AgentSoulFilesService:
"mime_type": file_ref.type,
"file_kind": file_ref.transfer_method,
"is_skill": False,
"size": file_ref.get("size"),
"hash": file_ref.get("hash"),
}
@classmethod

View File

@ -13,7 +13,7 @@ from models import ToolFile, UploadFile
from models.agent import (
Agent,
AgentConfigSnapshot,
AgentDriveFileKind,
AgentDriveFile,
AgentScope,
AgentStatus,
WorkflowAgentBindingType,
@ -184,7 +184,7 @@ class WorkflowAgentPublishService:
wanted_keys: dict[str, tuple[str, str]] = {}
soul_skill_keys = {skill.skill_md_key for skill in agent_soul.files.skills if skill.skill_md_key}
soul_file_keys = AgentSoulFilesService.allowed_drive_keys(agent_soul=agent_soul) - soul_skill_keys
soul_file_keys = {file_ref.drive_key for file_ref in agent_soul.files.files if file_ref.drive_key}
for mention in parse_prompt_mentions(agent_soul.prompt.system_prompt):
if mention.kind not in {MentionKind.SKILL, MentionKind.FILE}:
continue
@ -200,11 +200,15 @@ class WorkflowAgentPublishService:
check_keys = sorted(set(wanted_keys) | declared_keys)
if not check_keys:
return
existing_keys = {
key
for key in check_keys
if cls._soul_file_ref_exists(session=session, tenant_id=binding.tenant_id, agent_soul=agent_soul, key=key)
}
existing_keys = set(
session.scalars(
select(AgentDriveFile.key).where(
AgentDriveFile.tenant_id == binding.tenant_id,
AgentDriveFile.agent_id == binding.agent_id,
AgentDriveFile.key.in_(check_keys),
)
).all()
)
messages: list[str] = []
for key, (code, display) in wanted_keys.items():
if code == "skill_ref_dangling" and key not in soul_skill_keys:
@ -225,35 +229,6 @@ class WorkflowAgentPublishService:
f"Workflow Agent node {binding.node_id} has invalid Agent Soul drive refs: {'; '.join(messages)}"
)
@staticmethod
def _soul_file_ref_exists(
*,
session: Session,
tenant_id: str,
agent_soul: AgentSoulConfig,
key: str,
) -> bool:
file_ref = AgentSoulFilesService.file_ref_for_key(agent_soul=agent_soul, key=key)
if file_ref is None:
return False
file_id = file_ref.file_id or file_ref.upload_file_id
if not file_id:
return False
raw_kind = file_ref.transfer_method or ("upload_file" if file_ref.upload_file_id else None)
try:
file_kind = AgentDriveFileKind(str(raw_kind))
except ValueError:
return False
if file_kind == AgentDriveFileKind.TOOL_FILE:
return (
session.scalar(select(ToolFile.id).where(ToolFile.tenant_id == tenant_id, ToolFile.id == file_id))
is not None
)
return (
session.scalar(select(UploadFile.id).where(UploadFile.tenant_id == tenant_id, UploadFile.id == file_id))
is not None
)
@classmethod
def sync_agent_bindings_for_draft(
cls,

View File

@ -838,24 +838,6 @@ class AgentDriveService:
except ValueError:
return None
@classmethod
def resolve_download_url_for_ref(
cls,
*,
tenant_id: str,
file_kind: AgentDriveFileKind,
file_id: str,
for_external: bool = False,
as_attachment: bool = False,
) -> str | None:
return cls._resolve_download_url(
tenant_id=tenant_id,
file_kind=file_kind,
file_id=file_id,
for_external=for_external,
as_attachment=as_attachment,
)
# ── console drive inspector (ENG-624) ────────────────────────────────────
# SKILL.md is the primary preview use case; 64 KiB covers it with headroom
@ -1035,33 +1017,6 @@ class AgentDriveService:
break
return self._preview_bytes(key=response_key, size=size, payload=bytes(data))
def preview_file_ref(
self,
*,
tenant_id: str,
agent_id: str,
key: str,
file_kind: AgentDriveFileKind,
file_id: str,
size: int | None = None,
) -> dict[str, Any]:
"""Preview a concrete versioned file ref recorded in Agent Soul."""
with session_factory.create_session() as session:
self._assert_agent_belongs_to_tenant(session, tenant_id=tenant_id, agent_id=agent_id)
storage_key = self._storage_key_for_ref(
session,
tenant_id=tenant_id,
file_kind=file_kind,
file_id=file_id,
)
data = bytearray()
for chunk in storage.load_stream(storage_key):
data.extend(chunk)
if len(data) > self.PREVIEW_MAX_BYTES:
break
return self._preview_bytes(key=key, size=size, payload=bytes(data))
def preview_archive_member_for_ref(
self,
*,
@ -1140,27 +1095,6 @@ class AgentDriveService:
as_attachment=True,
)
def download_url_for_ref(
self,
*,
tenant_id: str,
agent_id: str,
file_kind: AgentDriveFileKind,
file_id: str,
) -> str:
with session_factory.create_session() as session:
self._assert_agent_belongs_to_tenant(session, tenant_id=tenant_id, agent_id=agent_id)
url = self._resolve_download_url(
tenant_id=tenant_id,
file_kind=file_kind,
file_id=file_id,
for_external=True,
as_attachment=True,
)
if url is None:
raise AgentDriveError("drive_key_not_found", "drive value cannot be resolved", status_code=404)
return url
@staticmethod
def _secret_key() -> bytes:
return dify_config.SECRET_KEY.encode()
@ -1290,7 +1224,6 @@ class AgentDriveService:
filename = normalize_drive_key(key).rsplit("/", 1)[-1]
return payload, mime_type, filename
__all__ = [
"AgentDriveError",
"AgentDriveService",

View File

@ -959,11 +959,41 @@ def _soul_with_drive_skill() -> AgentSoulConfig:
def _mock_drive_catalog(monkeypatch: pytest.MonkeyPatch) -> None:
return None
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.list_skills",
lambda self, *, tenant_id, agent_id: [
{
"path": "tender-analyzer",
"skill_md_key": "tender-analyzer/SKILL.md",
"archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip",
"name": "Tender Analyzer",
"description": "Parses RFPs.",
"size": 123,
"mime_type": "text/markdown",
"hash": "hash-1",
"created_at": 1,
}
],
)
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.manifest",
lambda self, *, tenant_id, agent_id, prefix="", include_download_url=False: [
{"key": "tender-analyzer/SKILL.md", "is_skill": True},
{"key": "tender-analyzer/.DIFY-SKILL-FULL.zip", "is_skill": False},
{"key": "files/sample.pdf", "is_skill": False},
],
)
def _mock_empty_drive_catalog(monkeypatch: pytest.MonkeyPatch) -> None:
return None
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.list_skills",
lambda self, *, tenant_id, agent_id: [],
)
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.manifest",
lambda self, *, tenant_id, agent_id, prefix="", include_download_url=False: [],
)
def test_build_drive_layer_config_catalogs_drive_skills_and_mentions(monkeypatch: pytest.MonkeyPatch):
@ -1089,6 +1119,14 @@ def test_workflow_runtime_missing_drive_mentions_fall_back_to_label_then_decoded
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.dify_config.AGENT_DRIVE_MANIFEST_ENABLED", True
)
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.list_skills",
lambda self, *, tenant_id, agent_id: [],
)
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.AgentDriveService.manifest",
lambda self, *, tenant_id, agent_id, prefix="", include_download_url=False: [],
)
context = _context()
context.snapshot.config_snapshot = AgentSoulConfig(
prompt={

View File

@ -33,26 +33,6 @@ def test_apply_drive_commit_records_skill_and_file_refs_in_agent_soul():
"is_skill": False,
},
)
AgentSoulFilesService._apply_commit_item(
agent_soul=soul,
item={
"key": "tender-analyzer/.DIFY-SKILL-FULL.zip",
"file_kind": "tool_file",
"file_id": "archive-file",
"mime_type": "application/zip",
"is_skill": False,
},
)
AgentSoulFilesService._apply_commit_item(
agent_soul=soul,
item={
"key": "tender-analyzer/src/main.py",
"file_kind": "tool_file",
"file_id": "member-file",
"mime_type": "text/x-python",
"is_skill": False,
},
)
assert [skill.model_dump(mode="json", exclude_none=True) for skill in soul.files.skills] == [
{
@ -64,34 +44,7 @@ def test_apply_drive_commit_records_skill_and_file_refs_in_agent_soul():
"skill_md_key": "tender-analyzer/SKILL.md",
"skill_md_file_id": "skill-md-file",
"full_archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip",
"full_archive_file_id": "archive-file",
"manifest_files": ["SKILL.md", "src/main.py"],
"file_refs": [
{
"id": "tender-analyzer/.DIFY-SKILL-FULL.zip",
"file_id": "archive-file",
"name": ".DIFY-SKILL-FULL.zip",
"type": "application/zip",
"transfer_method": "tool_file",
"drive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip",
},
{
"id": "tender-analyzer/SKILL.md",
"file_id": "skill-md-file",
"name": "SKILL.md",
"type": "",
"transfer_method": "tool_file",
"drive_key": "tender-analyzer/SKILL.md",
},
{
"id": "tender-analyzer/src/main.py",
"file_id": "member-file",
"name": "main.py",
"type": "text/x-python",
"transfer_method": "tool_file",
"drive_key": "tender-analyzer/src/main.py",
},
],
}
]
assert [file_ref.model_dump(mode="json", exclude_none=True) for file_ref in soul.files.files] == [
@ -118,23 +71,6 @@ def test_apply_drive_commit_removes_refs_without_touching_unrelated_entries():
"path": "tender-analyzer",
"skill_md_key": "tender-analyzer/SKILL.md",
"full_archive_key": "tender-analyzer/.DIFY-SKILL-FULL.zip",
"file_refs": [
{
"id": "tender-analyzer/SKILL.md",
"file_id": "skill-md-file",
"name": "SKILL.md",
"type": "",
"transfer_method": "tool_file",
"drive_key": "tender-analyzer/SKILL.md",
},
{
"id": "tender-analyzer/src/main.py",
"file_id": "member-file",
"name": "main.py",
"transfer_method": "tool_file",
"drive_key": "tender-analyzer/src/main.py",
},
],
}
],
"files": [
@ -180,32 +116,3 @@ def test_drive_copy_and_access_scopes_come_from_agent_soul_files():
assert prefixes == {"tender-analyzer/"}
assert AgentSoulFilesService.key_allowed_by_soul(agent_soul=soul, key="tender-analyzer/src/main.py") is True
assert AgentSoulFilesService.key_allowed_by_soul(agent_soul=soul, key="files/other.pdf") is False
def test_file_ref_for_key_resolves_skill_member_refs():
soul = AgentSoulConfig.model_validate(
{
"files": {
"skills": [
{
"path": "tender-analyzer",
"skill_md_key": "tender-analyzer/SKILL.md",
"file_refs": [
{
"id": "tender-analyzer/src/main.py",
"file_id": "member-file",
"name": "main.py",
"transfer_method": "tool_file",
"drive_key": "tender-analyzer/src/main.py",
}
],
}
]
}
}
)
file_ref = AgentSoulFilesService.file_ref_for_key(agent_soul=soul, key="tender-analyzer/src/main.py")
assert file_ref is not None
assert file_ref.file_id == "member-file"