feat: support agent skill (#39675)

Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: 起岚 <155608604+xiaoyaoqilan@users.noreply.github.com>
Co-authored-by: Parman Mohammadalizadeh <prmma23@gmail.com>
Co-authored-by: Escape0707 <tothesong@gmail.com>
Co-authored-by: Xin Zhang <zhangxin@dify.ai>
Co-authored-by: iridescentWen <66297467+iridescentWen@users.noreply.github.com>
Co-authored-by: Bond Zhu <37842169+MRZHUH@users.noreply.github.com>
Co-authored-by: KVOJJJin <jzongcode@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: Chester <superdiao6@gmail.com>
Co-authored-by: WH-2099 <wh2099@pm.me>
Co-authored-by: Jony <619963502@qq.com>
Co-authored-by: Jony <13896935+zyz619963502zyz@users.noreply.github.com>
Co-authored-by: Harsh Kashyap <harsh.kashyap2001@gmail.com>
Co-authored-by: Harsh Kashyap <Harsh23Kashyap@users.noreply.github.com>
Co-authored-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: csurong <csurong1@gmail.com>
Co-authored-by: caosurong <surong.cao@thinkingdata.cn>
Co-authored-by: Taranum Wasu <81034301+Taranum01@users.noreply.github.com>
Co-authored-by: Taranum01 <50813317+Taranum01@users.noreply.github.com>
Co-authored-by: Pranav Agarwal <agarwalpranav0711@gmail.com>
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: zl86790 <marshal_li_b@163.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: yunlu.wen <yunlu.wen@dify.ai>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
wangxiaolei
2026-08-24 05:29:14 +00:00
committed by GitHub
parent 0ab88ef36a
commit 0024a845f7
217 changed files with 43345 additions and 683 deletions
+2
View File
@@ -157,6 +157,7 @@ from .workspace import (
models,
plugin,
rbac,
skills,
snippets,
tool_providers,
trigger_providers,
@@ -236,6 +237,7 @@ __all__ = [
"saved_message",
"setup",
"site",
"skills",
"snippet_workflow",
"snippet_workflow_draft_variable",
"snippets",
+1 -1
View File
@@ -54,7 +54,7 @@ class TagBindingRemovePayload(BaseModel):
class TagListQueryParam(BaseModel):
type: Literal["knowledge", "app", "snippet"] = Field(description="Tag type filter")
type: Literal["knowledge", "app", "snippet", "skill"] = Field(description="Tag type filter")
keyword: str | None = Field(None, description="Search keyword")
+883
View File
@@ -0,0 +1,883 @@
"""Console API for workspace-level Skill Management."""
from __future__ import annotations
import io
from flask import request, send_file
from flask_restx import Resource
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from sqlalchemy.orm import Session
from controllers.common.fields import BinaryFileResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.common.session import with_session
from controllers.console import console_ns
from controllers.console.flask_admission import console_account_admission
from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
edit_permission_required,
)
from fields.base import ResponseModel
from libs import helper
from libs.helper import dump_response
from machinery.context import RequestContext
from services.skill_management_service import (
SkillAssistMessagePayload,
SkillCreatePayload,
SkillDraftFileCheckPayload,
SkillDraftFileOperationPayload,
SkillDraftTreePayload,
SkillImportPayload,
SkillManagementService,
SkillManagementServiceError,
SkillMetadataPayload,
SkillPublishPayload,
SkillRestorePayload,
SkillVersionUpdatePayload,
)
_FILE_UPLOAD_PARAMS = {
"file": {
"description": "Skill draft file payload",
"in": "formData",
"type": "file",
"required": True,
},
}
class WorkspaceSkillsQuery(BaseModel):
keyword: str | None = Field(default=None, description="Search keyword matching skill name or description.")
page: int = Field(default=1, ge=1, le=99999, description="Page number.")
limit: int = Field(default=20, ge=1, le=100, description="Number of items per page.")
tag: list[str] = Field(
default_factory=list,
description="Skill tag filters. Repeat the parameter for multiple tags.",
)
class SkillDeletePayload(BaseModel):
model_config = ConfigDict(extra="forbid")
confirmation_name: str | None = Field(
default=None,
description="Required when deleting a referenced Skill. Must match the Skill display name.",
)
class AgentSkillBindingsPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
skill_ids: list[str] = Field(default_factory=list, description="Ordered Skill IDs bound to the Agent.")
class SkillFileQuery(BaseModel):
path: str = Field(description="Skill file path relative to the Skill root.")
version_id: str | None = Field(default=None, description="Optional published version ID. Omit for current draft.")
class SkillResponse(ResponseModel):
id: str
name: str
display_name: str
icon: str
description: str
tags: list[str] = Field(default_factory=list)
name_manually_edited: bool = False
visibility: str
latest_published_version_id: str | None = None
latest_published_version_number: int | None = None
latest_published_at: int | None = None
reference_count: int = 0
created_by: str | None = None
created_by_name: str | None = None
updated_by: str | None = None
updated_by_name: str | None = None
created_at: int
updated_at: int
class SkillFileResponse(ResponseModel):
id: str | None = None
path: str
kind: str
storage: str | None = None
mime_type: str | None = None
content: str | None = None
tool_file_id: str | None = None
size: int | None = None
hash: str | None = None
class SkillFilePreviewResponse(ResponseModel):
path: str
mime_type: str
content: str
size: int
hash: str
class SkillFileUploadResponse(ResponseModel):
id: str
name: str
mime_type: str
size: int
hash: str
class SkillFileCheckErrorResponse(ResponseModel):
code: str
message: str
class SkillFileCheckItemResponse(ResponseModel):
path: str
filename: str
extension: str
mime_type: str
size: int
errors: list[SkillFileCheckErrorResponse] = Field(default_factory=list)
class SkillFileCheckResponse(ResponseModel):
data: dict[str, SkillFileCheckItemResponse] = Field(default_factory=dict)
class SkillDetailResponse(SkillResponse):
files: list[SkillFileResponse] = Field(default_factory=list)
class SkillListResponse(ResponseModel):
data: list[SkillResponse] = Field(default_factory=list)
has_more: bool = False
limit: int = 20
page: int = 1
total: int = 0
class SkillTagResponse(ResponseModel):
tag: str
count: int
class SkillTagListResponse(ResponseModel):
data: list[SkillTagResponse] = Field(default_factory=list)
class SkillVersionResponse(ResponseModel):
id: str
skill_id: str
version_number: int
version_name: str
publish_note: str
hash_code: str
archive_size: int
published_by: str | None = None
published_by_name: str | None = None
is_latest: bool = False
created_at: int
class SkillVersionListResponse(ResponseModel):
data: list[SkillVersionResponse] = Field(default_factory=list)
class SkillVersionDetailResponse(SkillVersionResponse):
files: list[SkillFileResponse] = Field(default_factory=list)
class SkillVersionDeleteResponse(ResponseModel):
id: str
deleted: bool
latest_published_version_id: str | None = None
class SkillReferenceResponse(ResponseModel):
type: str
agent_id: str
agent_icon: str | None = None
agent_icon_background: str | None = None
agent_icon_type: str | None = None
app_id: str | None = None
name: str
display_name: str
workflow_id: str | None = None
workflow_name: str | None = None
workflow_icon: str | None = None
workflow_icon_background: str | None = None
workflow_icon_type: str | None = None
workflow_version: str | None = None
node_id: str | None = None
node_name: str | None = None
class SkillReferenceListResponse(ResponseModel):
data: list[SkillReferenceResponse] = Field(default_factory=list)
class SkillDeleteResponse(ResponseModel):
id: str
deleted: bool
class AgentSkillBindingItemResponse(ResponseModel):
id: str
priority: int
name: str
display_name: str
icon: str
description: str
tags: list[str] = Field(default_factory=list)
status: str
file_count: int
latest_published_version_id: str | None = None
latest_published_at: int | None = None
updated_at: int
class AgentSkillBindingsResponse(ResponseModel):
agent_id: str
skill_ids: list[str] = Field(default_factory=list)
data: list[AgentSkillBindingItemResponse] = Field(default_factory=list)
register_schema_models(
console_ns,
WorkspaceSkillsQuery,
SkillCreatePayload,
SkillAssistMessagePayload,
SkillMetadataPayload,
SkillDraftFileCheckPayload,
SkillDraftFileOperationPayload,
SkillDraftTreePayload,
SkillPublishPayload,
SkillRestorePayload,
SkillVersionUpdatePayload,
SkillDeletePayload,
SkillFileQuery,
AgentSkillBindingsPayload,
)
register_response_schema_models(
console_ns,
SkillResponse,
SkillFileResponse,
SkillFilePreviewResponse,
SkillFileUploadResponse,
SkillFileCheckErrorResponse,
SkillFileCheckItemResponse,
SkillFileCheckResponse,
SkillDetailResponse,
SkillListResponse,
SkillTagResponse,
SkillTagListResponse,
SkillVersionResponse,
SkillVersionListResponse,
SkillVersionDetailResponse,
SkillVersionDeleteResponse,
SkillReferenceResponse,
SkillReferenceListResponse,
SkillDeleteResponse,
AgentSkillBindingItemResponse,
AgentSkillBindingsResponse,
BinaryFileResponse,
)
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, object], int]:
body: dict[str, object] = {"code": exc.code, "message": exc.message}
if exc.details:
body["details"] = exc.details
return body, exc.status_code
def _workspace_id(context: RequestContext) -> str:
if context.active_workspace_id is None:
raise RuntimeError("Console account admission did not resolve an active workspace")
return context.active_workspace_id
@console_ns.route("/workspaces/current/skills")
class WorkspaceSkillsApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkspaceSkillsQuery))
@console_ns.response(200, "Workspace skills", console_ns.models[SkillListResponse.__name__])
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_VIEW,
rbac_resource_required=False,
)
@with_session(write=False)
def get(self, session: Session, request_context: RequestContext):
query_input: dict[str, object] = {
"keyword": request.args.get("keyword"),
"tag": request.args.getlist("tag"),
}
if "limit" in request.args:
query_input["limit"] = request.args.get("limit")
if "page" in request.args:
query_input["page"] = request.args.get("page")
query = WorkspaceSkillsQuery.model_validate(query_input)
result = SkillManagementService(session=session).list_skills(
tenant_id=_workspace_id(request_context),
keyword=query.keyword,
page=query.page,
limit=query.limit,
tags=[tag for tag in query.tag if tag],
)
return dump_response(SkillListResponse, result)
@console_ns.expect(console_ns.models[SkillCreatePayload.__name__])
@console_ns.response(201, "Skill created", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_EDIT,
rbac_resource_required=False,
)
@edit_permission_required
@with_session
def post(self, session: Session, request_context: RequestContext):
try:
payload = SkillCreatePayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).create_skill(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
payload=payload,
)
return dump_response(SkillDetailResponse, result), 201
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/files/upload")
class WorkspaceSkillFileUploadApi(Resource):
@console_ns.doc(consumes=["multipart/form-data"], params=_FILE_UPLOAD_PARAMS)
@console_ns.response(201, "Skill draft file uploaded", console_ns.models[SkillFileUploadResponse.__name__])
@console_account_admission()
@edit_permission_required
@with_session
def post(self, session: Session, request_context: RequestContext):
if "file" not in request.files:
return {"code": "no_file_uploaded", "message": "no file uploaded"}, 400
file = request.files["file"]
if not file.filename:
return {"code": "filename_missing", "message": "filename is required"}, 400
try:
result = SkillManagementService(session=session).upload_file(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
filename=file.filename,
content=file.stream.read(),
mime_type=file.mimetype,
)
return dump_response(SkillFileUploadResponse, result), 201
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/tags")
class WorkspaceSkillTagsApi(Resource):
@console_ns.response(200, "Workspace Skill tags", console_ns.models[SkillTagListResponse.__name__])
@console_account_admission()
@with_session(write=False)
def get(self, session: Session, request_context: RequestContext):
result = SkillManagementService(session=session).list_tags(tenant_id=_workspace_id(request_context))
return dump_response(SkillTagListResponse, result)
@console_ns.route("/workspaces/current/skills/import")
class WorkspaceSkillImportApi(Resource):
@console_ns.doc(description="Import a Skill zip package from multipart form field `file`.")
@console_ns.response(201, "Skill imported", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_EDIT,
rbac_resource_required=False,
)
@edit_permission_required
@with_session
def post(self, session: Session, request_context: RequestContext):
upload = request.files.get("file")
if upload is None:
return {"code": "invalid_request", "message": "file is required"}, 400
try:
payload = SkillImportPayload(content=upload.read(), filename=upload.filename or "skill.zip")
result = SkillManagementService(session=session).import_skill(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
payload=payload,
)
return dump_response(SkillDetailResponse, result), 201
except (ValidationError, ValueError) as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>")
class WorkspaceSkillApi(Resource):
@console_ns.response(200, "Skill detail", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_VIEW,
rbac_resource_required=False,
)
@with_session(write=False)
def get(self, session: Session, request_context: RequestContext, skill_id: str):
try:
result = SkillManagementService(session=session).get_skill(
tenant_id=_workspace_id(request_context), skill_id=skill_id
)
return dump_response(SkillDetailResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.expect(console_ns.models[SkillMetadataPayload.__name__])
@console_ns.response(200, "Skill updated", console_ns.models[SkillResponse.__name__])
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_EDIT,
rbac_resource_required=False,
)
@edit_permission_required
@with_session
def patch(self, session: Session, request_context: RequestContext, skill_id: str):
try:
payload = SkillMetadataPayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).update_metadata(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.expect(console_ns.models[SkillDeletePayload.__name__])
@console_ns.response(200, "Skill deleted", console_ns.models[SkillDeleteResponse.__name__])
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_DELETE,
rbac_resource_required=False,
)
@edit_permission_required
@with_session
def delete(self, session: Session, request_context: RequestContext, skill_id: str):
try:
payload = SkillDeletePayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).delete_skill(
tenant_id=_workspace_id(request_context),
skill_id=skill_id,
confirmation_name=payload.confirmation_name,
)
return dump_response(SkillDeleteResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/duplicate")
class WorkspaceSkillDuplicateApi(Resource):
@console_ns.response(201, "Skill duplicated", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_EDIT,
rbac_resource_required=False,
)
@edit_permission_required
@with_session
def post(self, session: Session, request_context: RequestContext, skill_id: str):
try:
result = SkillManagementService(session=session).duplicate_skill(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
skill_id=skill_id,
)
return dump_response(SkillDetailResponse, result), 201
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/export")
class WorkspaceSkillExportApi(Resource):
@console_ns.response(200, "Published Skill zip archive")
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_VIEW,
rbac_resource_required=False,
)
@with_session(write=False)
def get(self, session: Session, request_context: RequestContext, skill_id: str):
try:
result = SkillManagementService(session=session).pull_published_archive(
tenant_id=_workspace_id(request_context), skill_id=skill_id
)
return send_file(
io.BytesIO(result.payload),
mimetype=result.mime_type,
as_attachment=True,
download_name=result.filename,
)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/assist/messages")
class WorkspaceSkillAssistMessageApi(Resource):
"""Stream read-only Skill Authoring suggestions from the default workspace model."""
@console_ns.expect(console_ns.models[SkillAssistMessagePayload.__name__])
@console_ns.response(200, "Skill Authoring assistant event stream")
@console_account_admission()
@with_session
def post(self, session: Session, request_context: RequestContext, skill_id: str):
try:
payload = SkillAssistMessagePayload.model_validate(console_ns.payload or {})
response = SkillManagementService(session=session).create_assistant_action_stream(
tenant_id=_workspace_id(request_context),
skill_id=skill_id,
user_id=request_context.account_id,
message=payload.message,
attachments=payload.attachments,
history=payload.history,
model_payload=payload.model,
target_path=payload.target_path,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
return helper.compact_generate_response(response)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/check")
class WorkspaceSkillFilesCheckApi(Resource):
@console_ns.expect(console_ns.models[SkillDraftFileCheckPayload.__name__])
@console_ns.response(200, "Draft files checked", console_ns.models[SkillFileCheckResponse.__name__])
@console_account_admission()
@edit_permission_required
@with_session
def post(self, session: Session, request_context: RequestContext, skill_id: str):
try:
payload = SkillDraftFileCheckPayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).check_draft_files(
tenant_id=_workspace_id(request_context),
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillFileCheckResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files")
class WorkspaceSkillFilesApi(Resource):
@console_ns.expect(console_ns.models[SkillDraftFileOperationPayload.__name__])
@console_ns.response(200, "Draft file operation applied", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission()
@edit_permission_required
@with_session
def patch(self, session: Session, request_context: RequestContext, skill_id: str):
try:
payload = SkillDraftFileOperationPayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).apply_draft_file_operation(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillDetailResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.expect(console_ns.models[SkillDraftTreePayload.__name__])
@console_ns.response(200, "Draft files replaced", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission()
@edit_permission_required
@with_session
def put(self, session: Session, request_context: RequestContext, skill_id: str):
try:
payload = SkillDraftTreePayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).replace_draft_tree(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillDetailResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/preview")
class WorkspaceSkillFilePreviewApi(Resource):
@console_ns.doc(params=query_params_from_model(SkillFileQuery))
@console_ns.response(200, "Skill file text preview", console_ns.models[SkillFilePreviewResponse.__name__])
@console_account_admission()
@with_session
def get(self, session: Session, request_context: RequestContext, skill_id: str):
try:
query = SkillFileQuery.model_validate(
{
"path": request.args.get("path"),
"version_id": request.args.get("version_id"),
}
)
result = SkillManagementService(session=session).preview_file(
tenant_id=_workspace_id(request_context),
skill_id=skill_id,
path=query.path,
version_id=query.version_id,
)
return dump_response(SkillFilePreviewResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/content")
class WorkspaceSkillFileContentApi(Resource):
@console_ns.doc(params={**query_params_from_model(SkillFileQuery), "download": "Return as an attachment when 1."})
@console_ns.response(200, "Skill file content", console_ns.models[BinaryFileResponse.__name__])
@console_account_admission()
@with_session
def get(self, session: Session, request_context: RequestContext, skill_id: str):
try:
query = SkillFileQuery.model_validate(
{
"path": request.args.get("path"),
"version_id": request.args.get("version_id"),
}
)
result = SkillManagementService(session=session).pull_file(
tenant_id=_workspace_id(request_context),
skill_id=skill_id,
path=query.path,
version_id=query.version_id,
)
return send_file(
io.BytesIO(result.payload),
mimetype=result.mime_type,
as_attachment=request.args.get("download") == "1",
download_name=result.filename,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/publish")
class WorkspaceSkillPublishApi(Resource):
@console_ns.expect(console_ns.models[SkillPublishPayload.__name__])
@console_ns.response(200, "Skill published", console_ns.models[SkillVersionResponse.__name__])
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_PUBLISH,
rbac_resource_required=False,
)
@edit_permission_required
@with_session
def post(self, session: Session, request_context: RequestContext, skill_id: str):
try:
payload = SkillPublishPayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).publish_skill(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillVersionResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/restore")
class WorkspaceSkillRestoreApi(Resource):
@console_ns.expect(console_ns.models[SkillRestorePayload.__name__])
@console_ns.response(200, "Skill version restored to draft", console_ns.models[SkillDetailResponse.__name__])
@console_account_admission(
rbac_resource_scope=RBACResourceScope.WORKSPACE,
rbac_permission=RBACPermission.SKILL_PUBLISH,
rbac_resource_required=False,
)
@edit_permission_required
@with_session
def post(self, session: Session, request_context: RequestContext, skill_id: str):
try:
payload = SkillRestorePayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).restore_version(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillDetailResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/references")
class WorkspaceSkillReferencesApi(Resource):
@console_ns.response(200, "Skill references", console_ns.models[SkillReferenceListResponse.__name__])
@console_account_admission()
@with_session
def get(self, session: Session, request_context: RequestContext, skill_id: str):
try:
result = SkillManagementService(session=session).list_skill_references(
tenant_id=_workspace_id(request_context), skill_id=skill_id
)
return dump_response(SkillReferenceListResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/versions")
class WorkspaceSkillVersionsApi(Resource):
@console_ns.response(200, "Skill versions", console_ns.models[SkillVersionListResponse.__name__])
@console_account_admission()
@with_session
def get(self, session: Session, request_context: RequestContext, skill_id: str):
try:
result = SkillManagementService(session=session).list_versions(
tenant_id=_workspace_id(request_context), skill_id=skill_id
)
return dump_response(SkillVersionListResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/versions/<string:version_id>")
class WorkspaceSkillVersionApi(Resource):
@console_ns.response(200, "Skill version detail", console_ns.models[SkillVersionDetailResponse.__name__])
@console_account_admission()
@with_session
def get(self, session: Session, request_context: RequestContext, skill_id: str, version_id: str):
try:
result = SkillManagementService(session=session).get_version(
tenant_id=_workspace_id(request_context),
skill_id=skill_id,
version_id=version_id,
)
return dump_response(SkillVersionDetailResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.expect(console_ns.models[SkillVersionUpdatePayload.__name__])
@console_ns.response(200, "Skill version updated", console_ns.models[SkillVersionResponse.__name__])
@console_account_admission()
@edit_permission_required
@with_session
def patch(self, session: Session, request_context: RequestContext, skill_id: str, version_id: str):
try:
payload = SkillVersionUpdatePayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).update_version(
tenant_id=_workspace_id(request_context),
skill_id=skill_id,
version_id=version_id,
payload=payload,
)
return dump_response(SkillVersionResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.response(200, "Skill version deleted", console_ns.models[SkillVersionDeleteResponse.__name__])
@console_account_admission()
@edit_permission_required
@with_session
def delete(self, session: Session, request_context: RequestContext, skill_id: str, version_id: str):
try:
result = SkillManagementService(session=session).delete_version(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
skill_id=skill_id,
version_id=version_id,
)
return dump_response(SkillVersionDeleteResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/agents/<string:agent_id>/skills")
class WorkspaceAgentSkillBindingsApi(Resource):
@console_ns.response(200, "Agent Skill bindings", console_ns.models[AgentSkillBindingsResponse.__name__])
@console_account_admission()
@with_session
def get(self, session: Session, request_context: RequestContext, agent_id: str):
result = SkillManagementService(session=session).list_agent_bindings(
tenant_id=_workspace_id(request_context), agent_id=agent_id
)
return dump_response(AgentSkillBindingsResponse, result)
@console_ns.expect(console_ns.models[AgentSkillBindingsPayload.__name__])
@console_ns.response(200, "Agent Skill bindings replaced", console_ns.models[AgentSkillBindingsResponse.__name__])
@console_account_admission()
@edit_permission_required
@with_session
def put(self, session: Session, request_context: RequestContext, agent_id: str):
try:
payload = AgentSkillBindingsPayload.model_validate(console_ns.payload or {})
result = SkillManagementService(session=session).replace_agent_bindings(
tenant_id=_workspace_id(request_context),
user_id=request_context.account_id,
agent_id=agent_id,
skill_ids=payload.skill_ids,
)
return dump_response(AgentSkillBindingsResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
__all__ = [
"WorkspaceAgentSkillBindingsApi",
"WorkspaceSkillApi",
"WorkspaceSkillDuplicateApi",
"WorkspaceSkillExportApi",
"WorkspaceSkillFilesApi",
"WorkspaceSkillImportApi",
"WorkspaceSkillPublishApi",
"WorkspaceSkillReferencesApi",
"WorkspaceSkillRestoreApi",
"WorkspaceSkillTagsApi",
"WorkspaceSkillVersionApi",
"WorkspaceSkillVersionsApi",
"WorkspaceSkillsApi",
]
+2
View File
@@ -24,6 +24,7 @@ from .app import dsl as _app_dsl
from .knowledge import retrieval as _knowledge_retrieval
from .plugin import agent_config as _agent_config
from .plugin import plugin as _plugin
from .plugin import skills as _skills
from .workspace import workspace as _workspace
api.add_namespace(inner_api_ns)
@@ -38,6 +39,7 @@ __all__ = [
"_mail",
"_plugin",
"_runtime_credentials",
"_skills",
"_workspace",
"api",
"bp",
@@ -0,0 +1,59 @@
"""Inner API for published workspace Skills.
These endpoints are called by trusted runtime services. They expose only
published Skill artifacts, never draft files or editable metadata.
"""
from __future__ import annotations
import io
from flask import request, send_file
from flask_restx import Resource
from pydantic import BaseModel, ValidationError
from sqlalchemy.orm import Session
from controllers.common.session import with_session
from controllers.console.wraps import setup_required
from controllers.inner_api import inner_api_ns
from controllers.inner_api.wraps import plugin_inner_api_only
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
class _SkillTargetQuery(BaseModel):
tenant_id: str
def _target_query_from_request() -> _SkillTargetQuery:
return _SkillTargetQuery.model_validate({"tenant_id": request.args.get("tenant_id")})
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, str], int]:
return {"code": exc.code, "message": exc.message}, exc.status_code
@inner_api_ns.route("/skills/<string:skill_id>/pull")
class PublishedSkillPullApi(Resource):
@setup_required
@plugin_inner_api_only
@inner_api_ns.doc("published_skill_pull")
@with_session(write=False)
def get(self, session: Session, skill_id: str):
try:
query = _target_query_from_request()
result = SkillManagementService(session=session).pull_published_archive(
tenant_id=query.tenant_id, skill_id=skill_id
)
return send_file(
io.BytesIO(result.payload),
mimetype=result.mime_type,
as_attachment=True,
download_name=result.filename,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
__all__ = ["PublishedSkillPullApi"]
@@ -44,6 +44,7 @@ from core.workflow.nodes.agent_v2.runtime_request_builder import (
build_config_layer_config,
build_knowledge_layer_config,
build_shell_layer_config,
load_runtime_agent_skill_configs,
)
from models.agent_config_entities import AgentSoulConfig, AgentSoulToolsConfig
from models.provider_ids import ModelProviderID
@@ -121,14 +122,22 @@ class AgentAppRuntimeRequestBuilder:
"cli_tool_count": len(agent_soul.tools.cli_tools),
}
runtime_config_skills = load_runtime_agent_skill_configs(
tenant_id=context.dify_context.tenant_id,
agent_id=context.agent_id,
)
config_layer_config, config_warnings = build_config_layer_config(
agent_soul,
agent_id=context.agent_id,
config_version_id=context.agent_config_snapshot_id,
config_version_kind=context.agent_config_version_kind,
runtime_config_skills=runtime_config_skills,
)
append_runtime_warnings(metadata, config_warnings)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(
agent_soul,
runtime_config_skills=runtime_config_skills,
)
knowledge_config = build_knowledge_layer_config(agent_soul)
context_window_tokens = resolve_model_context_window(
run_context=context.dify_context,
+4
View File
@@ -62,6 +62,10 @@ class RBACPermission(StrEnum):
API_EXTENSION_MANAGE = "api_extension_manage"
CUSTOMIZATION_MANAGE = "customization_manage"
AGENT_MANAGE = "agent_manage"
SKILL_VIEW = "skill_view"
SKILL_EDIT = "skill_edit"
SKILL_PUBLISH = "skill_publish"
SKILL_DELETE = "skill_delete"
SNIPPETS_CREATE_AND_MODIFY = "snippets_create_and_modify"
SNIPPETS_MANAGE = "snippets_management"
@@ -37,6 +37,7 @@ from dify_agent.layers.shell import (
)
from dify_agent.protocol import CreateRunRequest, DeferredToolResultsPayload
from pydantic import BaseModel, ValidationError
from sqlalchemy.exc import OperationalError
from clients.agent_backend import (
AgentBackendModelConfig,
@@ -199,14 +200,22 @@ class WorkflowAgentRuntimeRequestBuilder:
"cli_tool_count": len(agent_soul.tools.cli_tools),
}
runtime_config_skills = load_runtime_agent_skill_configs(
tenant_id=context.dify_context.tenant_id,
agent_id=context.agent.id,
)
config_layer_config, config_warnings = build_config_layer_config(
agent_soul,
agent_id=context.agent.id,
config_version_id=context.snapshot.id,
config_version_kind="snapshot",
runtime_config_skills=runtime_config_skills,
)
append_runtime_warnings(metadata, config_warnings)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(
agent_soul,
runtime_config_skills=runtime_config_skills,
)
soul_prompt = expand_prompt_mentions(agent_soul.prompt.system_prompt, soul_prompt_resolver).strip()
knowledge_config = build_knowledge_layer_config(agent_soul)
context_window_tokens = resolve_model_context_window(
@@ -846,11 +855,16 @@ def append_runtime_warnings(metadata: dict[str, Any], warnings: list[dict[str, s
existing.extend(warnings)
def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
def build_config_aware_soul_mention_resolver(
agent_soul: AgentSoulConfig,
*,
runtime_config_skills: Sequence[DifyConfigSkillConfig] = (),
):
"""Resolve config skill/file mentions and delegate the rest to Agent Soul."""
base_resolver = build_soul_mention_resolver(agent_soul)
skill_names = {item.name for item in agent_soul.config_skills if not item.is_missing}
skill_names.update(item.name for item in runtime_config_skills)
file_names = {item.name for item in agent_soul.config_files if not item.is_missing}
def _resolve(mention: object) -> str | None:
@@ -868,12 +882,34 @@ def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
return _resolve
def load_runtime_agent_skill_configs(*, tenant_id: str, agent_id: str) -> list[DifyConfigSkillConfig]:
"""Return workspace-bound Skills as prompt-safe runtime config skills."""
from services.skill_management_service import SkillManagementService
try:
runtime_skills = SkillManagementService().list_runtime_agent_skills(tenant_id=tenant_id, agent_id=agent_id)
except OperationalError as exc:
if "no such table: agent_skill_bindings" not in str(exc.orig):
raise
runtime_skills = []
return [
DifyConfigSkillConfig(
name=str(item["name"]),
description=str(item.get("description") or ""),
size=cast(int | None, item.get("size")),
mime_type=cast(str | None, item.get("mime_type")),
)
for item in runtime_skills
]
def build_config_layer_config(
agent_soul: AgentSoulConfig,
*,
agent_id: str | None = None,
config_version_id: str | None = None,
config_version_kind: Literal["snapshot", "draft", "build_draft"] = "snapshot",
runtime_config_skills: Sequence[DifyConfigSkillConfig] = (),
) -> tuple[DifyConfigLayerConfig, list[dict[str, str]]]:
"""Build the always-present Agent config layer from Agent Soul state.
@@ -890,8 +926,23 @@ def build_config_layer_config(
)
)
available_skills = [skill for skill in agent_soul.config_skills if not skill.is_missing]
skill_configs = [
DifyConfigSkillConfig(
name=skill.name,
description=skill.description,
size=skill.size,
mime_type=skill.mime_type,
)
for skill in available_skills
]
seen_skill_names = {skill.name for skill in skill_configs}
for skill in runtime_config_skills:
if skill.name in seen_skill_names:
continue
seen_skill_names.add(skill.name)
skill_configs.append(skill)
available_files = [file_ref for file_ref in agent_soul.config_files if not file_ref.is_missing]
skill_names = {skill.name for skill in available_skills}
skill_names = {skill.name for skill in skill_configs}
file_names = {file_ref.name for file_ref in available_files}
warnings: list[dict[str, str]] = [
{
@@ -928,15 +979,7 @@ def build_config_layer_config(
kind=config_version_kind,
writable=config_version_kind == "build_draft",
),
skills=[
DifyConfigSkillConfig(
name=skill.name,
description=skill.description,
size=skill.size,
mime_type=skill.mime_type,
)
for skill in available_skills
],
skills=skill_configs,
files=[
DifyConfigFileConfig(
name=file_ref.name,
@@ -0,0 +1,186 @@
"""add workspace skill management
Revision ID: a4f8d2c9e1b0
Revises: 925e75620b69
Create Date: 2026-08-24 10:52:00.000000
"""
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import mysql
from models.types import StringUUID
# revision identifiers, used by Alembic.
revision = "a4f8d2c9e1b0"
down_revision = "925e75620b69"
branch_labels = None
depends_on = None
def _uuid_column(name: str, *, nullable: bool = False) -> sa.Column:
return sa.Column(name, StringUUID(), nullable=nullable)
def _long_text() -> sa.types.TypeEngine:
return sa.Text().with_variant(mysql.LONGTEXT(), "mysql")
def upgrade() -> None:
if not _has_table("skills"):
op.create_table(
"skills",
_uuid_column("id"),
_uuid_column("tenant_id"),
sa.Column("name", sa.String(length=64), nullable=False),
sa.Column("display_name", sa.String(length=128), nullable=False),
sa.Column("icon", sa.String(length=16), nullable=False, server_default="📄"),
sa.Column("description", sa.String(length=1024), nullable=False, server_default=""),
sa.Column("name_manually_edited", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("visibility", sa.String(length=32), nullable=False, server_default="workspace"),
_uuid_column("latest_published_version_id", nullable=True),
_uuid_column("created_by", nullable=True),
_uuid_column("updated_by", nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.PrimaryKeyConstraint("id", name="skill_pkey"),
sa.UniqueConstraint("tenant_id", "name", name="skill_tenant_name_unique"),
)
if not _has_index("skills", "skills_tenant_updated_at_idx"):
op.create_index("skills_tenant_updated_at_idx", "skills", ["tenant_id", "updated_at"])
if not _has_table("skill_draft_files"):
op.create_table(
"skill_draft_files",
_uuid_column("id"),
_uuid_column("skill_id"),
sa.Column("path", sa.String(length=512), nullable=False),
sa.Column("kind", sa.String(length=32), nullable=False),
sa.Column("storage", sa.String(length=32), nullable=True),
sa.Column("mime_type", sa.String(length=255), nullable=True),
sa.Column("content_text", _long_text(), nullable=True),
_uuid_column("tool_file_id", nullable=True),
sa.Column("size", sa.BigInteger(), nullable=True),
sa.Column("hash", sa.String(length=255), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.PrimaryKeyConstraint("id", name="skill_draft_file_pkey"),
sa.UniqueConstraint("skill_id", "path", name="skill_draft_file_skill_path_unique"),
)
if not _has_index("skill_draft_files", "skill_draft_files_skill_path_idx"):
op.create_index("skill_draft_files_skill_path_idx", "skill_draft_files", ["skill_id", "path"])
if not _has_table("skill_versions"):
op.create_table(
"skill_versions",
_uuid_column("id"),
_uuid_column("skill_id"),
sa.Column("version_number", sa.Integer(), nullable=False),
sa.Column("version_name", sa.String(length=128), nullable=False, server_default=""),
sa.Column("publish_note", sa.String(length=1024), nullable=False, server_default=""),
sa.Column("manifest", _long_text(), nullable=False),
_uuid_column("archive_tool_file_id"),
sa.Column("hash_code", sa.String(length=255), nullable=False),
sa.Column("archive_size", sa.BigInteger(), nullable=False),
_uuid_column("published_by", nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.PrimaryKeyConstraint("id", name="skill_version_pkey"),
sa.UniqueConstraint("skill_id", "version_number", name="skill_version_skill_number_unique"),
)
if not _has_index("skill_versions", "skill_versions_skill_created_at_idx"):
op.create_index("skill_versions_skill_created_at_idx", "skill_versions", ["skill_id", "created_at"])
if not _has_table("agent_skill_bindings"):
op.create_table(
"agent_skill_bindings",
_uuid_column("id"),
_uuid_column("tenant_id"),
_uuid_column("agent_id"),
_uuid_column("skill_id"),
sa.Column("priority", sa.Integer(), nullable=False),
_uuid_column("created_by", nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.PrimaryKeyConstraint("id", name="agent_skill_binding_pkey"),
sa.UniqueConstraint("tenant_id", "agent_id", "skill_id", name="agent_skill_binding_unique"),
sa.UniqueConstraint("tenant_id", "agent_id", "priority", name="agent_skill_binding_priority_unique"),
)
if not _has_index("agent_skill_bindings", "agent_skill_bindings_skill_idx"):
op.create_index("agent_skill_bindings_skill_idx", "agent_skill_bindings", ["tenant_id", "skill_id"])
if not _has_table("agent_skill_binding_snapshots"):
op.create_table(
"agent_skill_binding_snapshots",
_uuid_column("id"),
_uuid_column("tenant_id"),
_uuid_column("agent_id"),
_uuid_column("config_snapshot_id"),
_uuid_column("skill_id"),
sa.Column("priority", sa.Integer(), nullable=False),
_uuid_column("created_by", nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.PrimaryKeyConstraint("id", name="agent_skill_binding_snapshot_pkey"),
sa.UniqueConstraint(
"tenant_id",
"agent_id",
"config_snapshot_id",
"skill_id",
name="agent_skill_binding_snapshot_skill_unique",
),
sa.UniqueConstraint(
"tenant_id",
"agent_id",
"config_snapshot_id",
"priority",
name="agent_skill_binding_snapshot_priority_unique",
),
)
if not _has_index("agent_skill_binding_snapshots", "agent_skill_binding_snapshots_agent_snapshot_idx"):
op.create_index(
"agent_skill_binding_snapshots_agent_snapshot_idx",
"agent_skill_binding_snapshots",
["tenant_id", "agent_id", "config_snapshot_id"],
)
def downgrade() -> None:
if context.is_offline_mode() or _has_table("agent_skill_binding_snapshots"):
if context.is_offline_mode() or _has_index(
"agent_skill_binding_snapshots", "agent_skill_binding_snapshots_agent_snapshot_idx"
):
op.drop_index(
"agent_skill_binding_snapshots_agent_snapshot_idx",
table_name="agent_skill_binding_snapshots",
)
op.drop_table("agent_skill_binding_snapshots")
if context.is_offline_mode() or _has_table("agent_skill_bindings"):
if context.is_offline_mode() or _has_index("agent_skill_bindings", "agent_skill_bindings_skill_idx"):
op.drop_index("agent_skill_bindings_skill_idx", table_name="agent_skill_bindings")
op.drop_table("agent_skill_bindings")
if context.is_offline_mode() or _has_table("skill_versions"):
if context.is_offline_mode() or _has_index("skill_versions", "skill_versions_skill_created_at_idx"):
op.drop_index("skill_versions_skill_created_at_idx", table_name="skill_versions")
op.drop_table("skill_versions")
if context.is_offline_mode() or _has_table("skill_draft_files"):
if context.is_offline_mode() or _has_index("skill_draft_files", "skill_draft_files_skill_path_idx"):
op.drop_index("skill_draft_files_skill_path_idx", table_name="skill_draft_files")
op.drop_table("skill_draft_files")
if context.is_offline_mode() or _has_table("skills"):
if context.is_offline_mode() or _has_index("skills", "skills_tenant_updated_at_idx"):
op.drop_index("skills_tenant_updated_at_idx", table_name="skills")
op.drop_table("skills")
def _has_table(table_name: str) -> bool:
if context.is_offline_mode():
return False
return sa.inspect(op.get_bind()).has_table(table_name)
def _has_index(table_name: str, index_name: str) -> bool:
if context.is_offline_mode() or not _has_table(table_name):
return False
return any(index["name"] == index_name for index in sa.inspect(op.get_bind()).get_indexes(table_name))
+7
View File
@@ -112,6 +112,7 @@ from .provider import (
TenantDefaultModel,
TenantPreferredModelProvider,
)
from .skill import AgentSkillBinding, Skill, SkillDraftFile, SkillFileKind, SkillFileStorage, SkillVersion
from .snippet import CustomizedSnippet, SnippetType
from .source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding
from .task import CeleryTask, CeleryTaskSet
@@ -170,6 +171,7 @@ __all__ = [
"AgentIconType",
"AgentKind",
"AgentScope",
"AgentSkillBinding",
"AgentSource",
"AgentStatus",
"AgentWorkingResourceStatus",
@@ -247,6 +249,11 @@ __all__ = [
"RecommendedApp",
"SavedMessage",
"Site",
"Skill",
"SkillDraftFile",
"SkillFileKind",
"SkillFileStorage",
"SkillVersion",
"SnippetType",
"Tag",
"TagBinding",
+1
View File
@@ -249,6 +249,7 @@ class TagType(StrEnum):
KNOWLEDGE = "knowledge"
APP = "app"
SNIPPET = "snippet"
SKILL = "skill"
class DatasetMetadataType(StrEnum):
+1 -1
View File
@@ -2684,7 +2684,7 @@ class Tag(TypeBase):
sa.Index("tag_name_idx", "name"),
)
TAG_TYPE_LIST = ["knowledge", "app", "snippet"]
TAG_TYPE_LIST = ["knowledge", "app", "snippet", "skill"]
id: Mapped[str] = mapped_column(
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
+199
View File
@@ -0,0 +1,199 @@
"""Workspace-level Skill Management models.
These tables are the source of truth for reusable workspace Skills. Agent Soul
``config_skills`` and Agent Drive skill rows remain per-agent runtime/config
assets; they may consume a published Skill snapshot but do not own the Skill's
draft, metadata, version history, or Agent binding priority.
"""
from enum import StrEnum
import sqlalchemy as sa
from pydantic import BaseModel, ConfigDict
from sqlalchemy import Index, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from models.base import Base, DefaultFieldsMixin
from models.types import EnumText, JSONModelColumn, LongText, StringUUID
class SkillFileKind(StrEnum):
"""Draft file entry kind."""
FILE = "file"
DIRECTORY = "directory"
class SkillFileStorage(StrEnum):
"""How a draft file's content is stored."""
TEXT = "text"
TOOL_FILE = "tool_file"
class SkillVersionManifestFile(BaseModel):
"""One file entry captured in a published Skill snapshot manifest."""
path: str
mime_type: str | None = None
size: int
hash: str
model_config = ConfigDict(extra="forbid")
class SkillVersionManifest(BaseModel):
"""Published Skill snapshot file index."""
files: list[SkillVersionManifestFile]
name: str | None = None
display_name: str | None = None
description: str | None = None
model_config = ConfigDict(extra="forbid")
class Skill(DefaultFieldsMixin, Base):
"""Workspace-level reusable Skill metadata and draft status."""
__tablename__ = "skills"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="skill_pkey"),
UniqueConstraint("tenant_id", "name", name="skill_tenant_name_unique"),
Index("skills_tenant_updated_at_idx", "tenant_id", "updated_at"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
name: Mapped[str] = mapped_column(sa.String(64), nullable=False)
display_name: Mapped[str] = mapped_column(sa.String(128), nullable=False)
icon: Mapped[str] = mapped_column(sa.String(16), nullable=False, default="📄", server_default="📄")
description: Mapped[str] = mapped_column(sa.String(1024), nullable=False, default="", server_default="")
name_manually_edited: Mapped[bool] = mapped_column(
sa.Boolean,
nullable=False,
default=False,
server_default=sa.false(),
)
visibility: Mapped[str] = mapped_column(
sa.String(32),
nullable=False,
default="workspace",
server_default="workspace",
)
latest_published_version_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
class SkillDraftFile(DefaultFieldsMixin, Base):
"""One draft file or directory in a workspace Skill."""
__tablename__ = "skill_draft_files"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="skill_draft_file_pkey"),
UniqueConstraint("skill_id", "path", name="skill_draft_file_skill_path_unique"),
Index("skill_draft_files_skill_path_idx", "skill_id", "path"),
)
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
path: Mapped[str] = mapped_column(sa.String(512), nullable=False)
kind: Mapped[SkillFileKind] = mapped_column(EnumText(SkillFileKind, length=32), nullable=False)
storage: Mapped[SkillFileStorage | None] = mapped_column(EnumText(SkillFileStorage, length=32), nullable=True)
mime_type: Mapped[str | None] = mapped_column(sa.String(255), nullable=True)
content_text: Mapped[str | None] = mapped_column(LongText, nullable=True)
tool_file_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
size: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True)
hash: Mapped[str | None] = mapped_column(sa.String(255), nullable=True)
class SkillVersion(DefaultFieldsMixin, Base):
"""Immutable published Skill snapshot.
``hash_code`` uniquely identifies a published version for downstream
execution audit. It includes Skill identity, version number, and archive
content digest instead of being only the archive content hash.
"""
__tablename__ = "skill_versions"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="skill_version_pkey"),
UniqueConstraint("skill_id", "version_number", name="skill_version_skill_number_unique"),
Index("skill_versions_skill_created_at_idx", "skill_id", "created_at"),
)
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
version_number: Mapped[int] = mapped_column(sa.Integer, nullable=False)
version_name: Mapped[str] = mapped_column(sa.String(128), nullable=False, default="", server_default="")
publish_note: Mapped[str] = mapped_column(sa.String(1024), nullable=False, default="", server_default="")
manifest: Mapped[SkillVersionManifest] = mapped_column(JSONModelColumn(SkillVersionManifest), nullable=False)
archive_tool_file_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
hash_code: Mapped[str] = mapped_column(sa.String(255), nullable=False)
archive_size: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
published_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
class AgentSkillBinding(DefaultFieldsMixin, Base):
"""Direct Agent-to-workspace-Skill binding.
``priority`` is retained as an internal ordering column for the current
schema constraints. Runtime Skill selection is Agent-driven and must not
treat it as a matching priority.
"""
__tablename__ = "agent_skill_bindings"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_skill_binding_pkey"),
UniqueConstraint("tenant_id", "agent_id", "skill_id", name="agent_skill_binding_unique"),
UniqueConstraint("tenant_id", "agent_id", "priority", name="agent_skill_binding_priority_unique"),
Index("agent_skill_bindings_skill_idx", "tenant_id", "skill_id"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
priority: Mapped[int] = mapped_column(sa.Integer, nullable=False)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
class AgentSkillBindingSnapshot(DefaultFieldsMixin, Base):
"""Published Agent-to-workspace-Skill bindings for one Agent snapshot."""
__tablename__ = "agent_skill_binding_snapshots"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_skill_binding_snapshot_pkey"),
UniqueConstraint(
"tenant_id",
"agent_id",
"config_snapshot_id",
"skill_id",
name="agent_skill_binding_snapshot_skill_unique",
),
UniqueConstraint(
"tenant_id",
"agent_id",
"config_snapshot_id",
"priority",
name="agent_skill_binding_snapshot_priority_unique",
),
Index("agent_skill_binding_snapshots_agent_snapshot_idx", "tenant_id", "agent_id", "config_snapshot_id"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
config_snapshot_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
priority: Mapped[int] = mapped_column(sa.Integer, nullable=False)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
__all__ = [
"AgentSkillBinding",
"AgentSkillBindingSnapshot",
"Skill",
"SkillDraftFile",
"SkillFileKind",
"SkillFileStorage",
"SkillVersion",
"SkillVersionManifest",
"SkillVersionManifestFile",
]
+801 -2
View File
@@ -9271,7 +9271,7 @@ Remove one or more tag bindings from a target.
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| keyword | query | Search keyword | No | string |
| type | query | Tag type filter | Yes | string, <br>**Available values:** "app", "knowledge", "snippet" |
| type | query | Tag type filter | Yes | string, <br>**Available values:** "app", "knowledge", "skill", "snippet" |
#### Responses
@@ -9785,6 +9785,38 @@ Get list of available agent providers
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [AgentProviderListResponse](#agentproviderlistresponse)<br> |
### [GET] /workspaces/current/agents/{agent_id}/skills
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent Skill bindings | **application/json**: [AgentSkillBindingsResponse](#agentskillbindingsresponse)<br> |
### [PUT] /workspaces/current/agents/{agent_id}/skills
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [AgentSkillBindingsPayload](#agentskillbindingspayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent Skill bindings replaced | **application/json**: [AgentSkillBindingsResponse](#agentskillbindingsresponse)<br> |
### [GET] /workspaces/current/customized-snippets
**List customized snippets with pagination and search**
@@ -11720,6 +11752,360 @@ Returns permission flags that control workspace features like member invitations
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [WorkspaceAccessMatrix](#workspaceaccessmatrix)<br> |
### [GET] /workspaces/current/skills
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| keyword | query | Search keyword matching skill name or description. | No | string |
| limit | query | Number of items per page. | No | integer, <br>**Default:** 20 |
| page | query | Page number. | No | integer, <br>**Default:** 1 |
| tag | query | Skill tag filters. Repeat the parameter for multiple tags. | No | [ string ] |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Workspace skills | **application/json**: [SkillListResponse](#skilllistresponse)<br> |
### [POST] /workspaces/current/skills
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillCreatePayload](#skillcreatepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill created | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [POST] /workspaces/current/skills/files/upload
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **multipart/form-data**: { **"file"**: binary }<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill draft file uploaded | **application/json**: [SkillFileUploadResponse](#skillfileuploadresponse)<br> |
### [POST] /workspaces/current/skills/import
Import a Skill zip package from multipart form field `file`.
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill imported | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [GET] /workspaces/current/skills/tags
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Workspace Skill tags | **application/json**: [SkillTagListResponse](#skilltaglistresponse)<br> |
### [DELETE] /workspaces/current/skills/{skill_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillDeletePayload](#skilldeletepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill deleted | **application/json**: [SkillDeleteResponse](#skilldeleteresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill detail | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [PATCH] /workspaces/current/skills/{skill_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillMetadataPayload](#skillmetadatapayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill updated | **application/json**: [SkillResponse](#skillresponse)<br> |
### [POST] /workspaces/current/skills/{skill_id}/assist/messages
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillAssistMessagePayload](#skillassistmessagepayload)<br> |
#### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Skill Authoring assistant event stream |
### [POST] /workspaces/current/skills/{skill_id}/duplicate
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill duplicated | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/export
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Published Skill zip archive |
### [PATCH] /workspaces/current/skills/{skill_id}/files
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillDraftFileOperationPayload](#skilldraftfileoperationpayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Draft file operation applied | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [PUT] /workspaces/current/skills/{skill_id}/files
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillDraftTreePayload](#skilldrafttreepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Draft files replaced | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [POST] /workspaces/current/skills/{skill_id}/files/check
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillDraftFileCheckPayload](#skilldraftfilecheckpayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Draft files checked | **application/json**: [SkillFileCheckResponse](#skillfilecheckresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/files/content
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| download | query | Return as an attachment when 1. | No | string |
| path | query | Skill file path relative to the Skill root. | Yes | string |
| version_id | query | Optional published version ID. Omit for current draft. | No | string |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill file content | **application/json**: [BinaryFileResponse](#binaryfileresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/files/preview
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| path | query | Skill file path relative to the Skill root. | Yes | string |
| version_id | query | Optional published version ID. Omit for current draft. | No | string |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill file text preview | **application/json**: [SkillFilePreviewResponse](#skillfilepreviewresponse)<br> |
### [POST] /workspaces/current/skills/{skill_id}/publish
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillPublishPayload](#skillpublishpayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill published | **application/json**: [SkillVersionResponse](#skillversionresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/references
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill references | **application/json**: [SkillReferenceListResponse](#skillreferencelistresponse)<br> |
### [POST] /workspaces/current/skills/{skill_id}/restore
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillRestorePayload](#skillrestorepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill version restored to draft | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/versions
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill versions | **application/json**: [SkillVersionListResponse](#skillversionlistresponse)<br> |
### [DELETE] /workspaces/current/skills/{skill_id}/versions/{version_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
| version_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill version deleted | **application/json**: [SkillVersionDeleteResponse](#skillversiondeleteresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/versions/{version_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
| version_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill version detail | **application/json**: [SkillVersionDetailResponse](#skillversiondetailresponse)<br> |
### [PATCH] /workspaces/current/skills/{skill_id}/versions/{version_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
| version_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillVersionUpdatePayload](#skillversionupdatepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill version updated | **application/json**: [SkillVersionResponse](#skillversionresponse)<br> |
### [GET] /workspaces/current/summary
#### Responses
@@ -14346,6 +14732,37 @@ Visibility and lifecycle scope of an Agent record.
| ---- | ---- | ----------- | -------- |
| result | string | | Yes |
#### AgentSkillBindingItemResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string | | Yes |
| display_name | string | | Yes |
| file_count | integer | | Yes |
| icon | string | | Yes |
| id | string | | Yes |
| latest_published_at | integer | | No |
| latest_published_version_id | string | | No |
| name | string | | Yes |
| priority | integer | | Yes |
| status | string | | Yes |
| tags | [ string ] | | No |
| updated_at | integer | | Yes |
#### AgentSkillBindingsPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| skill_ids | [ string ] | Ordered Skill IDs bound to the Agent. | No |
#### AgentSkillBindingsResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_id | string | | Yes |
| data | [ [AgentSkillBindingItemResponse](#agentskillbindingitemresponse) ] | | No |
| skill_ids | [ string ] | | No |
#### AgentSoulAppFeaturesConfig
| Name | Type | Description | Required |
@@ -21412,6 +21829,379 @@ Simple provider entity response.
| title | string | | Yes |
| use_icon_as_answer_icon | boolean | | Yes |
#### SkillAssistAttachmentPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| mime_type | string | | No |
| name | string | | Yes |
| size | integer | | No |
| tool_file_id | string | | Yes |
#### SkillAssistHistoryMessagePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | Yes |
| role | string, <br>**Available values:** "assistant", "user" | *Enum:* `"assistant"`, `"user"` | Yes |
| suggested_display_name | string | | No |
| suggested_name | string | | No |
#### SkillAssistMessagePayload
One user message and optional uploaded context for the Skill Authoring assistant.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| attachments | [ [SkillAssistAttachmentPayload](#skillassistattachmentpayload) ] | | No |
| history | [ [SkillAssistHistoryMessagePayload](#skillassisthistorymessagepayload) ] | | No |
| message | string | | Yes |
| model | [SkillAssistModelPayload](#skillassistmodelpayload) | | No |
| target_path | string | | No |
#### SkillAssistModelPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| model | string | | Yes |
| model_settings | object | | No |
| plugin_id | string | | No |
| provider | string | | Yes |
#### SkillCreatePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string | | No |
| display_name | string | | No |
| icon | string, <br>**Default:** 📄 | | No |
| name | string | | No |
| tags | [ string ] | | No |
#### SkillDeletePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| confirmation_name | string | Required when deleting a referenced Skill. Must match the Skill display name. | No |
#### SkillDeleteResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| deleted | boolean | | Yes |
| id | string | | Yes |
#### SkillDetailResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| created_at | integer | | Yes |
| created_by | string | | No |
| created_by_name | string | | No |
| description | string | | Yes |
| display_name | string | | Yes |
| files | [ [SkillFileResponse](#skillfileresponse) ] | | No |
| icon | string | | Yes |
| id | string | | Yes |
| latest_published_at | integer | | No |
| latest_published_version_id | string | | No |
| latest_published_version_number | integer | | No |
| name | string | | Yes |
| name_manually_edited | boolean | | No |
| reference_count | integer | | No |
| tags | [ string ] | | No |
| updated_at | integer | | Yes |
| updated_by | string | | No |
| updated_by_name | string | | No |
| visibility | string | | Yes |
#### SkillDraftFileCheckItemPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| filename | string | | Yes |
| mime_type | string | | No |
| path | string | Target draft path. Defaults to filename. | No |
| size | integer | | Yes |
#### SkillDraftFileCheckPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| files | [ [SkillDraftFileCheckItemPayload](#skilldraftfilecheckitempayload) ] | | No |
#### SkillDraftFileOperation
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SkillDraftFileOperation | string | | |
#### SkillDraftFileOperationPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | No |
| expected_updated_at | integer | | No |
| hash | string | | No |
| mime_type | string | | No |
| operation | [SkillDraftFileOperation](#skilldraftfileoperation) | | Yes |
| path | string | | Yes |
| size | integer | | No |
| target_path | string | | No |
| tool_file_id | string | | No |
#### SkillDraftTreeItemPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | No |
| hash | string | | No |
| kind | [SkillFileKind](#skillfilekind) | | No |
| mime_type | string | | No |
| path | string | | Yes |
| size | integer | | No |
| storage | [SkillFileStorage](#skillfilestorage) | | No |
| tool_file_id | string | | No |
#### SkillDraftTreePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| expected_updated_at | integer | | No |
| files | [ [SkillDraftTreeItemPayload](#skilldrafttreeitempayload) ] | | No |
#### SkillFileCheckErrorResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| code | string | | Yes |
| message | string | | Yes |
#### SkillFileCheckItemResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| errors | [ [SkillFileCheckErrorResponse](#skillfilecheckerrorresponse) ] | | No |
| extension | string | | Yes |
| filename | string | | Yes |
| mime_type | string | | Yes |
| path | string | | Yes |
| size | integer | | Yes |
#### SkillFileCheckResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | object | | No |
#### SkillFileKind
Draft file entry kind.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SkillFileKind | string | Draft file entry kind. | |
#### SkillFilePreviewResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | Yes |
| hash | string | | Yes |
| mime_type | string | | Yes |
| path | string | | Yes |
| size | integer | | Yes |
#### SkillFileQuery
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| path | string | Skill file path relative to the Skill root. | Yes |
| version_id | string | Optional published version ID. Omit for current draft. | No |
#### SkillFileResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | No |
| hash | string | | No |
| id | string | | No |
| kind | string | | Yes |
| mime_type | string | | No |
| path | string | | Yes |
| size | integer | | No |
| storage | string | | No |
| tool_file_id | string | | No |
#### SkillFileStorage
How a draft file's content is stored.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SkillFileStorage | string | How a draft file's content is stored. | |
#### SkillFileUploadResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| hash | string | | Yes |
| id | string | | Yes |
| mime_type | string | | Yes |
| name | string | | Yes |
| size | integer | | Yes |
#### SkillListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [SkillResponse](#skillresponse) ] | | No |
| has_more | boolean | | No |
| limit | integer, <br>**Default:** 20 | | No |
| page | integer, <br>**Default:** 1 | | No |
| total | integer | | No |
#### SkillMetadataPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| display_name | string | | No |
| expected_updated_at | integer | | No |
| icon | string | | No |
| tags | [ string ] | | No |
#### SkillPublishPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| publish_note | string | | No |
| version_name | string | | No |
#### SkillReferenceListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [SkillReferenceResponse](#skillreferenceresponse) ] | | No |
#### SkillReferenceResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_icon | string | | No |
| agent_icon_background | string | | No |
| agent_icon_type | string | | No |
| agent_id | string | | Yes |
| app_id | string | | No |
| display_name | string | | Yes |
| name | string | | Yes |
| node_id | string | | No |
| node_name | string | | No |
| type | string | | Yes |
| workflow_icon | string | | No |
| workflow_icon_background | string | | No |
| workflow_icon_type | string | | No |
| workflow_id | string | | No |
| workflow_name | string | | No |
| workflow_version | string | | No |
#### SkillResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| created_at | integer | | Yes |
| created_by | string | | No |
| created_by_name | string | | No |
| description | string | | Yes |
| display_name | string | | Yes |
| icon | string | | Yes |
| id | string | | Yes |
| latest_published_at | integer | | No |
| latest_published_version_id | string | | No |
| latest_published_version_number | integer | | No |
| name | string | | Yes |
| name_manually_edited | boolean | | No |
| reference_count | integer | | No |
| tags | [ string ] | | No |
| updated_at | integer | | Yes |
| updated_by | string | | No |
| updated_by_name | string | | No |
| visibility | string | | Yes |
#### SkillRestorePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| publish_note | string | | No |
| version_id | string | | Yes |
| version_name | string | | No |
#### SkillTagListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [SkillTagResponse](#skilltagresponse) ] | | No |
#### SkillTagResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| count | integer | | Yes |
| tag | string | | Yes |
#### SkillVersionDeleteResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| deleted | boolean | | Yes |
| id | string | | Yes |
| latest_published_version_id | string | | No |
#### SkillVersionDetailResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| archive_size | integer | | Yes |
| created_at | integer | | Yes |
| files | [ [SkillFileResponse](#skillfileresponse) ] | | No |
| hash_code | string | | Yes |
| id | string | | Yes |
| is_latest | boolean | | No |
| publish_note | string | | Yes |
| published_by | string | | No |
| published_by_name | string | | No |
| skill_id | string | | Yes |
| version_name | string | | Yes |
| version_number | integer | | Yes |
#### SkillVersionListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [SkillVersionResponse](#skillversionresponse) ] | | No |
#### SkillVersionResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| archive_size | integer | | Yes |
| created_at | integer | | Yes |
| hash_code | string | | Yes |
| id | string | | Yes |
| is_latest | boolean | | No |
| publish_note | string | | Yes |
| published_by | string | | No |
| published_by_name | string | | No |
| skill_id | string | | Yes |
| version_name | string | | Yes |
| version_number | integer | | Yes |
#### SkillVersionUpdatePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| publish_note | string | | No |
| version_name | string | | No |
#### SnippetDependencyCheckResponse
| Name | Type | Description | Required |
@@ -21896,7 +22686,7 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| keyword | string | Search keyword | No |
| type | string, <br>**Available values:** "app", "knowledge", "snippet" | Tag type filter<br>*Enum:* `"app"`, `"knowledge"`, `"snippet"` | Yes |
| type | string, <br>**Available values:** "app", "knowledge", "skill", "snippet" | Tag type filter<br>*Enum:* `"app"`, `"knowledge"`, `"skill"`, `"snippet"` | Yes |
#### TagListResponse
@@ -24099,6 +24889,15 @@ Workflow tool configuration
| ---- | ---- | ----------- | -------- |
| permission_keys | [ string ] | | No |
#### WorkspaceSkillsQuery
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| keyword | string | Search keyword matching skill name or description. | No |
| limit | integer, <br>**Default:** 20 | Number of items per page. | No |
| page | integer, <br>**Default:** 1 | Page number. | No |
| tag | [ string ] | Skill tag filters. Repeat the parameter for multiple tags. | No |
#### WorkspaceTenantResultResponse
| Name | Type | Description | Required |
+3
View File
@@ -11,6 +11,7 @@ from libs.helper import escape_like_pattern
from models.dataset import Dataset
from models.enums import TagType
from models.model import App, Tag, TagBinding
from models.skill import Skill
from models.snippet import CustomizedSnippet
from services.tag_application_service import (
CreateTagInput,
@@ -207,6 +208,8 @@ class TagRepository(TagStore):
CustomizedSnippet.tenant_id == workspace_id,
CustomizedSnippet.id == binding.target_id,
)
elif binding.type == "skill":
stmt = select(Skill.id).where(Skill.tenant_id == workspace_id, Skill.id == binding.target_id)
else:
raise InvalidTagBindingTypeError
+48
View File
@@ -64,6 +64,7 @@ from services.entities.agent_entities import (
ComposerVariant,
WorkflowNodeJobConfig,
)
from services.skill_management_service import SkillManagementService
from tasks.collect_agent_resources_task import enqueue_agent_resource_collection
from tasks.new_agent_beta_task import register_new_agent_beta_publish_after_commit
@@ -353,6 +354,14 @@ class AgentComposerService:
icon=source_agent.icon,
icon_background=source_agent.icon_background,
)
SkillManagementService(session=session).copy_agent_bindings(
tenant_id=tenant_id,
source_agent_id=source_agent.id,
source_snapshot_id=source_version.id,
target_agent_id=inline_agent.id,
target_snapshot_id=inline_agent.active_config_snapshot_id,
user_id=account_id,
)
binding.binding_type = WorkflowAgentBindingType.INLINE_AGENT
binding.agent_id = inline_agent.id
binding.current_snapshot_id = inline_agent.active_config_snapshot_id
@@ -640,6 +649,13 @@ class AgentComposerService:
agent.updated_by = account_id
draft.base_snapshot_id = version.id
draft.updated_by = account_id
SkillManagementService(session=session).publish_agent_bindings(
tenant_id=tenant_id,
agent_id=agent.id,
snapshot_id=version.id,
user_id=account_id,
)
if not access_was_ready:
if not agent.app_id:
raise AgentNotFoundError()
@@ -1488,6 +1504,12 @@ class AgentComposerService:
agent.active_config_has_model = agent_soul_has_model(payload.agent_soul)
agent.active_config_is_published = True
agent.updated_by = account_id
SkillManagementService(session=session).publish_agent_bindings(
tenant_id=tenant_id,
agent_id=agent.id,
snapshot_id=version.id,
user_id=account_id,
)
binding.current_snapshot_id = version.id
if payload.node_job is not None:
binding.node_job_config = payload.node_job
@@ -1528,6 +1550,12 @@ class AgentComposerService:
agent.active_config_has_model = agent_soul_has_model(payload.agent_soul)
agent.active_config_is_published = True
agent.updated_by = account_id
SkillManagementService(session=session).publish_agent_bindings(
tenant_id=tenant_id,
agent_id=agent.id,
snapshot_id=version.id,
user_id=account_id,
)
binding.current_snapshot_id = version.id
binding.updated_by = account_id
if payload.node_job is not None:
@@ -1564,6 +1592,17 @@ class AgentComposerService:
operation=AgentConfigRevisionOperation.SAVE_NEW_AGENT,
version_note=payload.version_note,
)
source_agent_id = binding.agent_id if binding else None
source_snapshot_id = binding.current_snapshot_id if binding else None
if source_agent_id and source_snapshot_id and agent.active_config_snapshot_id:
SkillManagementService(session=session).copy_agent_bindings(
tenant_id=tenant_id,
source_agent_id=source_agent_id,
source_snapshot_id=source_snapshot_id,
target_agent_id=agent.id,
target_snapshot_id=agent.active_config_snapshot_id,
user_id=account_id,
)
node_job = payload.node_job or WorkflowNodeJobConfig()
if not binding:
binding = WorkflowAgentNodeBinding(
@@ -1619,6 +1658,15 @@ class AgentComposerService:
operation=AgentConfigRevisionOperation.SAVE_TO_ROSTER,
version_note=payload.version_note,
)
if source_agent.active_config_snapshot_id and roster_agent.active_config_snapshot_id:
SkillManagementService(session=session).copy_agent_bindings(
tenant_id=tenant_id,
source_agent_id=source_agent.id,
source_snapshot_id=source_version.id,
target_agent_id=roster_agent.id,
target_snapshot_id=roster_agent.active_config_snapshot_id,
user_id=account_id,
)
binding.binding_type = WorkflowAgentBindingType.ROSTER_AGENT
binding.agent_id = roster_agent.id
binding.current_snapshot_id = roster_agent.active_config_snapshot_id
+18 -1
View File
@@ -36,6 +36,17 @@ class AgentPackageOmittedAsset(BaseModel):
mime_type: str | None = None
class AgentPackageWorkspaceSkill(BaseModel):
"""Workspace Skill binding represented without source-workspace identifiers."""
model_config = ConfigDict(extra="forbid")
name: str = Field(min_length=1, max_length=64)
display_name: str = ""
description: str = ""
priority: int = Field(ge=0)
class AgentPackage(BaseModel):
"""One portable Agent Soul with display metadata and omitted asset hints."""
@@ -45,6 +56,7 @@ class AgentPackage(BaseModel):
metadata: AgentPackageMetadata
soul: AgentSoulConfig
omitted_assets: list[AgentPackageOmittedAsset] = Field(default_factory=list)
workspace_skills: list[AgentPackageWorkspaceSkill] = Field(default_factory=list)
def portable_ref(prefix: str, value: str) -> str:
@@ -75,7 +87,11 @@ def _strip_sensitive_values(value: Any) -> Any:
return result
def make_portable_agent_package(agent: Agent, agent_soul: AgentSoulConfig) -> AgentPackage:
def make_portable_agent_package(
agent: Agent,
agent_soul: AgentSoulConfig,
workspace_skills: list[AgentPackageWorkspaceSkill] | None = None,
) -> AgentPackage:
"""Return a package safe to place in YAML or the system clipboard."""
soul_data = agent_soul.model_dump(mode="json")
@@ -152,4 +168,5 @@ def make_portable_agent_package(agent: Agent, agent_soul: AgentSoulConfig) -> Ag
),
soul=portable_soul,
omitted_assets=omitted_assets,
workspace_skills=workspace_skills or [],
)
+127 -1
View File
@@ -38,6 +38,7 @@ from models.agent import (
)
from models.agent_config_entities import AgentSoulConfig, WorkflowNodeJobConfig
from models.model import App, AppModelConfig
from models.skill import AgentSkillBinding, AgentSkillBindingSnapshot, Skill
from models.workflow import Workflow
from services.agent.agent_soul_state import agent_soul_has_model
from services.agent.dsl_entities import (
@@ -45,6 +46,7 @@ from services.agent.dsl_entities import (
AGENT_PACKAGE_REF_KEY,
AgentPackage,
AgentPackageMetadata,
AgentPackageWorkspaceSkill,
make_portable_agent_package,
portable_ref,
)
@@ -108,7 +110,13 @@ class AgentDslService:
soul = AgentSoulConfig.model_validate(snapshot.config_snapshot_dict)
package_ref = "agent_1"
return package_ref, {package_ref: make_portable_agent_package(agent, soul)}
workspace_skills = self._workspace_skills_for_export(
tenant_id=app.tenant_id,
agent_id=agent.id,
snapshot_id=agent.active_config_snapshot_id,
include_draft=draft is not None,
)
return package_ref, {package_ref: make_portable_agent_package(agent, soul, workspace_skills=workspace_skills)}
def export_workflow_packages(
self, *, workflow: Workflow, graph: Mapping[str, Any]
@@ -151,6 +159,12 @@ class AgentDslService:
packages[package_ref] = make_portable_agent_package(
agent,
AgentSoulConfig.model_validate(snapshot.config_snapshot_dict),
workspace_skills=self._workspace_skills_for_export(
tenant_id=workflow.tenant_id,
agent_id=agent.id,
snapshot_id=snapshot.id,
include_draft=False,
),
)
node_data["agent_binding"] = {
"binding_type": binding.binding_type.value,
@@ -215,6 +229,14 @@ class AgentDslService:
agent_id=agent.id,
snapshot_id=agent.active_config_snapshot_id,
)
self._restore_workspace_skill_bindings(
tenant_id=app.tenant_id,
agent=agent,
snapshot=snapshot,
package=package,
warnings=warnings,
account_id=account.id,
)
self.session.add(
AgentConfigDraft(
tenant_id=app.tenant_id,
@@ -378,6 +400,102 @@ class AgentDslService:
)
return dependencies
def _workspace_skills_for_export(
self,
*,
tenant_id: str,
agent_id: str,
snapshot_id: str | None,
include_draft: bool,
) -> list[AgentPackageWorkspaceSkill]:
if include_draft:
rows = list(
self.session.execute(
select(AgentSkillBinding, Skill)
.join(Skill, Skill.id == AgentSkillBinding.skill_id)
.where(AgentSkillBinding.tenant_id == tenant_id, AgentSkillBinding.agent_id == agent_id)
.order_by(AgentSkillBinding.priority)
)
)
if rows or not snapshot_id:
return [
AgentPackageWorkspaceSkill(
name=skill.name,
display_name=skill.display_name,
description=skill.description,
priority=binding.priority,
)
for binding, skill in rows
]
if snapshot_id:
rows = list(
self.session.execute(
select(AgentSkillBindingSnapshot, Skill)
.join(Skill, Skill.id == AgentSkillBindingSnapshot.skill_id)
.where(
AgentSkillBindingSnapshot.tenant_id == tenant_id,
AgentSkillBindingSnapshot.agent_id == agent_id,
AgentSkillBindingSnapshot.config_snapshot_id == snapshot_id,
)
.order_by(AgentSkillBindingSnapshot.priority)
)
)
else:
return []
return [
AgentPackageWorkspaceSkill(
name=skill.name,
display_name=skill.display_name,
description=skill.description,
priority=binding.priority,
)
for binding, skill in rows
]
def _restore_workspace_skill_bindings(
self,
*,
tenant_id: str,
agent: Agent,
snapshot: AgentConfigSnapshot,
package: AgentPackage,
warnings: list[DslImportWarning],
account_id: str,
) -> None:
for workspace_skill in sorted(package.workspace_skills, key=lambda item: item.priority):
skill = self.session.scalar(
select(Skill).where(Skill.tenant_id == tenant_id, Skill.name == workspace_skill.name).limit(1)
)
if skill is None:
warnings.append(
DslImportWarning(
code="agent_workspace_skill_unresolved",
path="agent.workspace_skills",
message=f"Workspace Skill {workspace_skill.name!r} is unavailable in the target workspace.",
details={"name": workspace_skill.name},
)
)
continue
self.session.add(
AgentSkillBinding(
tenant_id=tenant_id,
agent_id=agent.id,
skill_id=skill.id,
priority=workspace_skill.priority,
created_by=account_id,
)
)
self.session.add(
AgentSkillBindingSnapshot(
tenant_id=tenant_id,
agent_id=agent.id,
config_snapshot_id=snapshot.id,
skill_id=skill.id,
priority=workspace_skill.priority,
created_by=account_id,
)
)
def _create_imported_inline_agent(
self,
*,
@@ -401,6 +519,14 @@ class AgentDslService:
source=AgentSource.IMPORTED,
operation=AgentConfigRevisionOperation.IMPORT_PACKAGE,
)
self._restore_workspace_skill_bindings(
tenant_id=workflow.tenant_id,
agent=agent,
snapshot=snapshot,
package=package,
warnings=warnings,
account_id=account.id,
)
return AgentPackageImportResult(agent=agent, snapshot=snapshot, warnings=warnings)
def _create_workflow_only_agent(
+13
View File
@@ -1106,6 +1106,19 @@ class AgentRosterService:
target_app_id=target_app.id,
account_id=account.id,
)
from services.skill_management_service import SkillManagementService
target_agent = self.get_app_backing_agent(tenant_id=tenant_id, app_id=target_app.id)
if target_agent is None:
raise AgentNotFoundError()
SkillManagementService(session=self._session).copy_agent_bindings(
tenant_id=tenant_id,
source_agent_id=source_agent.id,
source_snapshot_id=source_agent.active_config_snapshot_id or "",
target_agent_id=target_agent.id,
user_id=account.id,
source_include_draft=not source_agent.active_config_is_published,
)
self._session.commit()
if FeatureService.get_system_features().webapp_auth.enabled:
try:
+46 -20
View File
@@ -18,12 +18,11 @@ from __future__ import annotations
import hashlib
import io
import posixpath
import re
import zipfile
import zlib
import yaml
from pydantic import BaseModel
from pydantic import BaseModel, Field, ValidationError, field_validator
from configs import dify_config
@@ -33,7 +32,8 @@ _MAX_SKILL_MD_BYTES = 1 * 1024 * 1024
_MAX_ENTRIES = 5000
_ALLOWED_EXTENSIONS = (".zip", ".skill")
_SKILL_MD_NAME = "SKILL.md"
_HEADING_RE = re.compile(r"^\s*#\s+(.+?)\s*$", re.MULTILINE)
_SKILL_NAME_PATTERN = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
_MAX_SKILL_DESCRIPTION_LENGTH = 1024
class SkillPackageError(Exception):
@@ -53,13 +53,18 @@ class SkillPackageError(Exception):
class SkillManifest(BaseModel):
"""Validated metadata extracted from a Skill package."""
name: str
description: str
name: str = Field(min_length=1, max_length=64, pattern=_SKILL_NAME_PATTERN)
description: str = Field(min_length=1, max_length=_MAX_SKILL_DESCRIPTION_LENGTH)
entry_path: str # path of SKILL.md inside the archive
files: list[str] # all (safe) file paths inside the archive
size: int # total uncompressed bytes
hash: str # sha256 of the archive bytes
@field_validator("name", "description", mode="before")
@classmethod
def _strip_required_string(cls, value: object) -> object:
return value.strip() if isinstance(value, str) else value
class NormalizedSkillPackage(BaseModel):
"""Canonical skill package bytes and metadata ready to store as Agent config."""
@@ -108,14 +113,17 @@ class SkillPackageService:
normalized_size = sum(max(info.file_size, 0) for info in normalized_members.values())
name, description = self._parse_skill_md(skill_md)
manifest = SkillManifest(
name=name,
description=description,
entry_path=_SKILL_MD_NAME,
files=sorted(normalized_members),
size=normalized_size,
hash=hashlib.sha256(normalized_archive_bytes).hexdigest(),
)
try:
manifest = SkillManifest(
name=name,
description=description,
entry_path=_SKILL_MD_NAME,
files=sorted(normalized_members),
size=normalized_size,
hash=hashlib.sha256(normalized_archive_bytes).hexdigest(),
)
except ValidationError as exc:
raise self._manifest_validation_error(exc) from exc
return NormalizedSkillPackage(
manifest=manifest,
archive_bytes=normalized_archive_bytes,
@@ -123,6 +131,31 @@ class SkillPackageService:
strip_prefix=strip_prefix,
)
@staticmethod
def _manifest_validation_error(exc: ValidationError) -> SkillPackageError:
first_error = exc.errors()[0]
loc = first_error["loc"]
field = loc[0] if loc else "manifest"
error_type = first_error["type"]
if field == "name":
code = "missing_skill_name" if error_type == "string_too_short" else "invalid_skill_name"
message = (
"SKILL.md frontmatter name is required"
if code == "missing_skill_name"
else "SKILL.md frontmatter name must be lowercase letters, numbers, and hyphens only, "
"must not start or end with a hyphen, and must be at most 64 characters"
)
return SkillPackageError(code, message, status_code=400)
if field == "description":
code = "missing_skill_description" if error_type == "string_too_short" else "invalid_skill_description"
message = (
"SKILL.md frontmatter description is required"
if code == "missing_skill_description"
else f"SKILL.md frontmatter description must be at most {_MAX_SKILL_DESCRIPTION_LENGTH} characters"
)
return SkillPackageError(code, message, status_code=400)
return SkillPackageError("invalid_skill_manifest", "SKILL.md frontmatter is invalid", status_code=400)
def _open_archive(self, *, content: bytes, filename: str) -> zipfile.ZipFile:
self._check_extension(filename)
if not content:
@@ -281,13 +314,6 @@ class SkillPackageService:
frontmatter = cls._parse_frontmatter(content)
name = str(frontmatter.get("name") or "").strip()
description = str(frontmatter.get("description") or "").strip()
if not name:
heading = _HEADING_RE.search(content)
name = heading.group(1).strip() if heading else ""
if not name:
raise SkillPackageError(
"missing_skill_name", "SKILL.md must declare a name (frontmatter or top heading)", status_code=400
)
return name, description
@staticmethod
+129 -28
View File
@@ -50,6 +50,7 @@ from models.model import UploadFile
from models.tools import ToolFile
from services.agent.config_skill_normalize_service import ConfigSkillNormalizeService
from services.agent.skill_package_service import SkillPackageError
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
class AgentConfigVersionKind(StrEnum):
@@ -109,6 +110,7 @@ class ConfigPushPayload(BaseModel):
@dataclass(slots=True)
class AgentConfigTarget:
tenant_id: str
agent_id: str
version_id: str
kind: AgentConfigVersionKind
@@ -135,17 +137,10 @@ class ConfigDownloadRequest:
class AgentConfigService:
"""Read and update Agent Soul-backed config assets for one version target.
The service owns the lifecycle of its database sessions. Callers may inject
a session creator for an alternate engine; production defaults to the
application-wide session factory.
"""
"""Read and update Agent Soul-backed config assets for one version target."""
PREVIEW_MAX_BYTES = 64 * 1024
_session_factory: Callable[[], Session]
def __init__(
self,
*,
@@ -153,8 +148,6 @@ class AgentConfigService:
skill_normalize_service: ConfigSkillNormalizeService | None = None,
session_factory: Callable[[], Session] | None = None,
) -> None:
"""Initialize external collaborators and the service-owned session creator."""
self._tool_files = tool_file_manager or ToolFileManager()
self._skill_normalizer = skill_normalize_service or ConfigSkillNormalizeService()
self._session_factory = session_factory or default_session_factory.create_session
@@ -178,6 +171,7 @@ class AgentConfigService:
user_id=user_id,
)
return AgentConfigTarget(
tenant_id=tenant_id,
agent_id=target.agent_id,
version_id=target.version_id,
kind=target.kind,
@@ -223,7 +217,7 @@ class AgentConfigService:
return {
"agent_id": target.agent_id,
"config_version": self._config_version_payload(target),
"items": [self._serialize_skill_item(skill) for skill in target.agent_soul.config_skills],
"items": self._skill_items_for_target(target, include_runtime_workspace_skills=False),
}
def list_files(
@@ -259,8 +253,6 @@ class AgentConfigService:
name: str,
user_id: str | None = None,
) -> ConfigDownloadRequest:
"""Authorize one Config reference and return origin-free download metadata."""
target = self.resolve_target(
tenant_id=tenant_id,
agent_id=agent_id,
@@ -269,12 +261,40 @@ class AgentConfigService:
user_id=user_id,
)
if kind == "skill":
skill = self._require_skill(target.agent_soul, name=name)
try:
skill = self._require_skill(target.agent_soul, name=name)
return self._resolve_download_request(
tenant_id=tenant_id,
file_kind=skill.file_kind,
file_id=self._available_skill_file_id(skill),
filename=f"{skill.name}.zip",
default_mime_type="application/zip",
missing_code="config_skill_not_found",
missing_message="config skill payload is missing",
)
except AgentConfigServiceError as exc:
if exc.code != "config_skill_not_found":
raise
runtime_skill = next(
(
item
for item in SkillManagementService().list_runtime_agent_skills(
tenant_id=tenant_id,
agent_id=agent_id,
include_draft=config_version_kind != AgentConfigVersionKind.SNAPSHOT,
)
if item["name"] == name
),
None,
)
if runtime_skill is None:
raise AgentConfigServiceError("config_skill_not_found", "config skill not found", status_code=404)
return self._resolve_download_request(
tenant_id=tenant_id,
file_kind=skill.file_kind,
file_id=self._available_skill_file_id(skill),
filename=f"{skill.name}.zip",
file_kind="tool_file",
file_id=str(runtime_skill["file_id"]),
filename=f"{runtime_skill['name']}.zip",
default_mime_type="application/zip",
missing_code="config_skill_not_found",
missing_message="config skill payload is missing",
@@ -329,9 +349,46 @@ class AgentConfigService:
config_version_kind=config_version_kind,
user_id=user_id,
)
skill = self._require_skill(target.agent_soul, name=name)
file_id = self._available_skill_file_id(skill)
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
try:
skill = self._require_skill(target.agent_soul, name=name)
file_id = self._available_skill_file_id(skill)
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
skill_item = self._serialize_skill_item(skill)
except AgentConfigServiceError as exc:
if exc.code != "config_skill_not_found":
raise
try:
workspace_archive = SkillManagementService().pull_runtime_agent_skill(
tenant_id=tenant_id,
agent_id=agent_id,
name=name,
include_draft=config_version_kind != AgentConfigVersionKind.SNAPSHOT,
)
except SkillManagementServiceError as skill_exc:
raise AgentConfigServiceError(
"config_skill_not_found",
"config skill not found",
status_code=404,
) from skill_exc
archive_bytes = workspace_archive.payload
skill_item = next(
(
item
for item in SkillManagementService().list_runtime_agent_skills(
tenant_id=tenant_id,
agent_id=agent_id,
)
if item["name"] == name
),
{
"id": name,
"name": name,
"description": "",
"size": None,
"hash": None,
"mime_type": "application/zip",
},
)
try:
archive_items, skill_md = self._inspect_skill_archive(archive_bytes)
except (OSError, ValueError, zipfile.BadZipFile) as exc:
@@ -341,7 +398,7 @@ class AgentConfigService:
status_code=500,
) from exc
return {
**self._serialize_skill_item(skill),
**skill_item,
"source": "config_skill_zip",
"files": archive_items,
"skill_md": skill_md,
@@ -436,6 +493,34 @@ class AgentConfigService:
)
return member_path
def pull_file(
self,
*,
tenant_id: str,
agent_id: str,
config_version_id: str,
config_version_kind: AgentConfigVersionKind,
name: str,
user_id: str | None = None,
) -> ConfigDownload:
target = self.resolve_target(
tenant_id=tenant_id,
agent_id=agent_id,
config_version_id=config_version_id,
config_version_kind=config_version_kind,
user_id=user_id,
)
file_ref = self._require_file(target.agent_soul, name=name)
file_id = self._available_file_id(file_ref)
payload, filename, mime_type = self._load_file_ref_bytes(
tenant_id=tenant_id,
file_kind=file_ref.file_kind,
file_id=file_id,
)
return ConfigDownload(
filename=filename or file_ref.name, mime_type=mime_type or "application/octet-stream", payload=payload
)
def download_file_url(
self,
*,
@@ -858,6 +943,7 @@ class AgentConfigService:
status_code=404,
)
return AgentConfigTarget(
tenant_id=tenant_id,
agent_id=agent_id,
version_id=version.id,
kind=config_version_kind,
@@ -1153,7 +1239,7 @@ class AgentConfigService:
"agent_id": target.agent_id,
"config_version": AgentConfigService._config_version_payload(target),
"skills": {
"items": [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
"items": AgentConfigService._skill_items_for_target(target, include_runtime_workspace_skills=True)
},
"files": {
"items": [
@@ -1164,6 +1250,26 @@ class AgentConfigService:
"note": target.agent_soul.config_note,
}
@staticmethod
def _skill_items_for_target(
target: AgentConfigTarget, *, include_runtime_workspace_skills: bool
) -> list[dict[str, object]]:
items = [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
if not include_runtime_workspace_skills:
return items
seen_names = {str(item["name"]) for item in items}
for item in SkillManagementService().list_runtime_agent_skills(
tenant_id=target.tenant_id,
agent_id=target.agent_id,
include_draft=target.kind != AgentConfigVersionKind.SNAPSHOT,
):
if item["name"] in seen_names:
continue
seen_names.add(str(item["name"]))
items.append(item)
return items
@staticmethod
def _config_version_payload(target: AgentConfigTarget) -> dict[str, object]:
return {
@@ -1432,12 +1538,7 @@ class AgentConfigService:
query.append(("as_attachment", "true"))
uri = urllib.parse.urlunsplit(parsed._replace(query=urllib.parse.urlencode(query)))
return ConfigDownloadRequest(
filename=filename,
mime_type=mime_type,
size=size,
download_uri=uri,
)
return ConfigDownloadRequest(filename=filename, mime_type=mime_type, size=size, download_uri=uri)
__all__ = [
+11
View File
@@ -40,6 +40,7 @@ from models.agent import (
WorkflowAgentNodeBinding,
)
from models.model import App, AppMode, AppModelConfig, IconType, Site, load_annotation_reply_config
from models.skill import AgentSkillBinding
from models.workflow import Workflow
from services.agent.errors import AgentAccessNotReadyError, AgentNameConflictError
from services.agent.home_snapshot_service import AgentHomeSnapshotService
@@ -1040,6 +1041,16 @@ class AppService:
WorkflowAgentNodeBinding.app_id == app.id,
)
)
agent_ids_to_unbind = set(workflow_agent_ids)
if backing_agent is not None:
agent_ids_to_unbind.add(backing_agent.id)
if agent_ids_to_unbind:
session.execute(
delete(AgentSkillBinding).where(
AgentSkillBinding.tenant_id == app.tenant_id,
AgentSkillBinding.agent_id.in_(agent_ids_to_unbind),
)
)
account_id = current_user.id if current_user else None
if backing_agent is not None:
now = naive_utc_now()
+14
View File
@@ -304,6 +304,10 @@ class MyPermissionsResponse(_RBACModel):
# Fallback permission snapshots for legacy Dify tenant roles when external RBAC is disabled.
# Keep these keys aligned with langgenius/rbac's built-in workspace roles and access policies.
_LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [
"skill.view",
"skill.edit",
"skill.publish",
"skill.delete",
"workspace.member.manage",
"workspace.role.manage",
"data_source.manage",
@@ -334,6 +338,10 @@ _LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [
]
_LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [
"skill.view",
"skill.edit",
"skill.publish",
"skill.delete",
"workspace.member.manage",
"workspace.role.manage",
"data_source.manage",
@@ -362,6 +370,10 @@ _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [
]
_LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [
"skill.view",
"skill.edit",
"skill.publish",
"skill.delete",
"api_extension.manage",
"plugin.install",
"credential.use",
@@ -377,6 +389,7 @@ _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [
]
_LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [
"skill.view",
"api_extension.manage",
"plugin.install",
"credential.use",
@@ -384,6 +397,7 @@ _LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [
]
_LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS: list[str] = [
"skill.view",
"plugin.install",
"dataset.create_and_management",
"dataset.external.connect",
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -5,7 +5,7 @@ from typing import Literal, NamedTuple, Protocol
from machinery.context import RequestContext
type TagKind = Literal["knowledge", "app", "snippet"]
type TagKind = Literal["knowledge", "app", "snippet", "skill"]
class TagSummary(NamedTuple):
@@ -62,7 +62,7 @@ class TagNameConflictError(TagApplicationError):
class TagBindingTargetNotFoundError(TagApplicationError):
def __init__(self, target_type: TagKind) -> None:
target_name = {"knowledge": "Dataset", "app": "App", "snippet": "Snippet"}[target_type]
target_name = {"knowledge": "Dataset", "app": "App", "snippet": "Snippet", "skill": "Skill"}[target_type]
super().__init__(f"{target_name} not found")
+14 -1
View File
@@ -3,15 +3,17 @@ from typing import cast
import sqlalchemy as sa
from flask_login import current_user
from flask_sqlalchemy.session import Session as FlaskSession
from pydantic import BaseModel, Field
from sqlalchemy import delete, func, select
from sqlalchemy.engine import CursorResult
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, scoped_session
from werkzeug.exceptions import NotFound
from models.dataset import Dataset
from models.enums import TagType
from models.model import App, Tag, TagBinding
from models.skill import Skill
from models.snippet import CustomizedSnippet
type _TagTypeLike = TagType | str
@@ -39,6 +41,11 @@ class TagBindingDeletePayload(BaseModel):
class TagService:
@staticmethod
def get_tag_type(tag_id: str, tenant_id: str, session: Session | scoped_session[FlaskSession]) -> TagType | None:
tag_type = session.scalar(select(Tag.type).where(Tag.id == tag_id, Tag.tenant_id == tenant_id).limit(1))
return tag_type
@staticmethod
def get_tags(tag_type: _TagTypeLike, current_tenant_id: str, keyword: str | None = None, *, session: Session):
stmt = (
@@ -282,5 +289,11 @@ class TagService:
)
if not snippet:
raise NotFound("Snippet not found")
elif type == "skill":
skill = session.scalar(
select(Skill).where(Skill.tenant_id == current_user.current_tenant_id, Skill.id == target_id).limit(1)
)
if not skill:
raise NotFound("Skill not found")
else:
raise NotFound("Invalid binding type")
@@ -102,6 +102,22 @@ class TestTagListApi:
assert status == 200
assert result == [{"id": "tag-1", "name": "Tag", "type": "knowledge", "binding_count": "2"}]
def test_get_skill_tags_uses_same_query_boundary(
self, app: Flask, request_context: RequestContext, tags_service: MagicMock
) -> None:
tags_service.list_tags.return_value = (TagSummary("tag-1", "Skill", "skill", 1),)
with app.test_request_context("/?type=skill"):
result, status = unwrap(TagListApi().get)(
TagListApi(),
TagListQueryParam(type="skill"),
request_context,
)
tags_service.list_tags.assert_called_once_with(request_context, "skill", None)
assert status == 200
assert result[0]["type"] == "skill"
def test_get_snippet_tags_uses_same_query_boundary(
self, app: Flask, request_context: RequestContext, tags_service: MagicMock
) -> None:
@@ -378,6 +394,25 @@ class TestTagBindings:
)
assert (result, status) == ({"result": "success"}, 200)
def test_create_passes_skill_binding_input(
self, app: Flask, request_context: RequestContext, tags_service: MagicMock
) -> None:
owner = _account(TenantAccountRole.OWNER)
payload = TagBindingPayload(tag_ids=["tag-1"], target_id="skill-1", type=TagType.SKILL)
with (
app.test_request_context("/"),
patch.object(module.dify_config, "RBAC_ENABLED", False),
patch.object(module, "current_account_with_tenant", return_value=(owner, "tenant-1")),
):
result, status = unwrap(TagBindingCollectionApi().post)(TagBindingCollectionApi(), payload, request_context)
tags_service.create_bindings.assert_called_once_with(
request_context,
TagBindingInput(("tag-1",), "skill-1", "skill"),
)
assert (result, status) == ({"result": "success"}, 200)
def test_create_maps_missing_target_to_not_found(
self, app: Flask, request_context: RequestContext, tags_service: MagicMock
) -> None:
File diff suppressed because it is too large Load Diff
@@ -40,6 +40,7 @@ from enums import DeploymentEdition
from graphon.entities import WorkflowStartReason
from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus
from graphon.runtime import GraphRuntimeState, VariablePool
from libs.datetime_utils import to_utc_timestamp
from models.account import Account
from models.enums import CreatorUserRole, MessageStatus
from models.human_input import HumanInputForm
@@ -678,7 +679,7 @@ class TestHitlServiceApi:
assert pause_resp.data.reasons[0]["TYPE"] == "human_input_required"
assert pause_resp.data.reasons[0]["form_id"] == "form-1"
assert pause_resp.data.reasons[0]["form_token"] == "token"
assert pause_resp.data.reasons[0]["expiration_time"] == int(expiration_time.timestamp())
assert pause_resp.data.reasons[0]["expiration_time"] == to_utc_timestamp(expiration_time)
assert isinstance(responses[0], HumanInputRequiredResponse)
hi_resp = responses[0]
@@ -689,7 +690,7 @@ class TestHitlServiceApi:
assert hi_resp.data.actions[0].id == "approve"
assert hi_resp.data.display_in_ui is True
assert hi_resp.data.form_token == "token"
assert hi_resp.data.expiration_time == int(expiration_time.timestamp())
assert hi_resp.data.expiration_time == to_utc_timestamp(expiration_time)
# Snapshot payload contract
def test_snapshot_events_include_pause_payload_contract(
@@ -746,13 +747,13 @@ class TestHitlServiceApi:
]
assert events[2]["data"]["status"] == WorkflowNodeExecutionStatus.PAUSED.value
assert events[3]["data"]["form_token"] == "wtok"
assert events[3]["data"]["expiration_time"] == int(expiration_time.timestamp())
assert events[3]["data"]["expiration_time"] == to_utc_timestamp(expiration_time)
pause_data = events[-1]["data"]
assert pause_data["paused_nodes"] == ["node-1"]
assert pause_data["outputs"] == {"result": "value"}
assert pause_data["reasons"][0]["TYPE"] == "human_input_required"
assert pause_data["reasons"][0]["form_token"] == "wtok"
assert pause_data["reasons"][0]["expiration_time"] == int(expiration_time.timestamp())
assert pause_data["reasons"][0]["expiration_time"] == to_utc_timestamp(expiration_time)
assert pause_data["status"] == WorkflowExecutionStatus.PAUSED.value
assert pause_data["created_at"] == int(workflow_run.created_at.timestamp())
assert pause_data["elapsed_time"] == workflow_run.elapsed_time
@@ -6,6 +6,7 @@ from __future__ import annotations
from types import SimpleNamespace
import pytest
from dify_agent.layers.config import DifyConfigSkillConfig
from dify_agent.layers.dify_core_tools import DifyCoreToolConfig, DifyCoreToolsLayerConfig
from dify_agent.layers.dify_plugin import DifyPluginToolConfig, DifyPluginToolsLayerConfig
from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig
@@ -28,6 +29,14 @@ from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom
from models.agent_config_entities import AgentSoulConfig
@pytest.fixture(autouse=True)
def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs",
lambda **_kwargs: [],
)
@pytest.fixture(autouse=True)
def model_context_window_calls(monkeypatch: pytest.MonkeyPatch) -> list[tuple[object, str, str]]:
calls: list[tuple[object, str, str]] = []
@@ -522,6 +531,32 @@ class TestAgentAppConfigLayer:
"mentioned_file_names": [],
}
def test_config_layer_includes_bound_workspace_skills(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs",
lambda **_kwargs: [
DifyConfigSkillConfig(
name="workspace-skill",
description="Bound workspace skill.",
size=123,
mime_type="application/zip",
)
],
)
soul = _soul_with_model()
soul.prompt.system_prompt = "Use [§skill:workspace-skill:Workspace Skill§]."
builder = AgentAppRuntimeRequestBuilder(
dify_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
)
result = builder.build(_ctx(soul))
config = next(layer for layer in result.request.composition.layers if layer.name == DIFY_CONFIG_LAYER_ID)
assert [skill.name for skill in config.config.skills] == ["workspace-skill"]
assert config.config.mentioned_skill_names == ["workspace-skill"]
prompt_layer = next(layer for layer in result.request.composition.layers if layer.name == "agent_soul_prompt")
assert prompt_layer.config.prefix == "Use workspace-skill."
@pytest.mark.parametrize(
("system_prompt", "expected_prefix"),
[
@@ -5,6 +5,7 @@ from typing import cast
import pytest
from agenton.compositor import CompositorSessionSnapshot
from dify_agent.layers.config import DifyConfigSkillConfig
from dify_agent.layers.dify_core_tools import DifyCoreToolConfig, DifyCoreToolsLayerConfig
from dify_agent.layers.dify_plugin import DifyPluginToolConfig, DifyPluginToolsLayerConfig
from dify_agent.protocol import DIFY_AGENT_HISTORY_LAYER_ID, DIFY_AGENT_MODEL_LAYER_ID, DIFY_AGENT_OUTPUT_LAYER_ID
@@ -40,6 +41,21 @@ from models.agent_config_entities import (
)
@pytest.fixture(autouse=True)
def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs",
lambda **_kwargs: [],
)
class FakeCredentialsProvider:
def fetch(self, provider_name: str, model_name: str) -> dict[str, object]:
assert provider_name == "openai"
assert model_name == "gpt-test"
return {"api_key": "secret-key"}
@pytest.fixture(autouse=True)
def model_context_window_calls(monkeypatch: pytest.MonkeyPatch) -> list[tuple[object, str, str]]:
calls: list[tuple[object, str, str]] = []
@@ -1407,6 +1423,30 @@ def test_build_config_layer_config_includes_soul_context_and_mentions():
assert warnings == []
def test_build_config_layer_config_includes_runtime_agent_skills():
from core.workflow.nodes.agent_v2.runtime_request_builder import build_config_layer_config
soul = AgentSoulConfig(
prompt={"system_prompt": "Use [§skill:workspace-skill:Workspace Skill§]."},
model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"),
)
config, warnings = build_config_layer_config(
soul,
runtime_config_skills=[
DifyConfigSkillConfig(
name="workspace-skill",
description="Bound workspace skill.",
size=123,
mime_type="application/zip",
)
],
)
assert [skill.name for skill in config.skills] == ["workspace-skill"]
assert config.mentioned_skill_names == ["workspace-skill"]
assert warnings == []
def test_build_config_layer_config_returns_empty_config_for_empty_agent_soul():
from core.workflow.nodes.agent_v2.runtime_request_builder import build_config_layer_config
@@ -1489,6 +1529,33 @@ def test_workflow_run_request_contains_config_layer():
assert warnings == []
def test_workflow_run_request_includes_bound_workspace_skills(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs",
lambda **_kwargs: [
DifyConfigSkillConfig(
name="workspace-skill",
description="Bound workspace skill.",
size=123,
mime_type="application/zip",
)
],
)
context = _context()
context.snapshot.config_snapshot = AgentSoulConfig(
prompt={"system_prompt": "Use [§skill:workspace-skill:Workspace Skill§]."},
model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"),
)
result = WorkflowAgentRuntimeRequestBuilder().build(context)
config = next(layer for layer in result.request.composition.layers if layer.name == DIFY_CONFIG_LAYER_ID)
assert [skill.name for skill in config.config.skills] == ["workspace-skill"]
assert config.config.mentioned_skill_names == ["workspace-skill"]
soul_prompt = next(layer for layer in result.request.composition.layers if layer.name == "agent_soul_prompt")
assert soul_prompt.config.prefix == "Use workspace-skill."
def test_workflow_runtime_expands_config_mentions_in_agent_soul_prompt():
context = _context()
context.snapshot.config_snapshot = _soul_with_config_assets()
@@ -4,6 +4,7 @@ from sqlalchemy.orm import Session, sessionmaker
from models.enums import TagType
from models.model import Tag, TagBinding
from models.skill import Skill
from models.snippet import CustomizedSnippet, SnippetType
from repositories.tag_repository import TagRepository
from services.tag_application_service import (
@@ -35,6 +36,18 @@ def _snippet(snippet_id: str, *, workspace_id: str) -> CustomizedSnippet:
return snippet
def _skill(skill_id: str, *, workspace_id: str) -> Skill:
skill = Skill(
tenant_id=workspace_id,
name=f"skill-{skill_id}",
display_name="Skill",
created_by="account-1",
updated_by="account-1",
)
skill.id = skill_id
return skill
def test_list_tags_scopes_binding_counts_and_escapes_keyword(
sqlite_session_factory: sessionmaker[Session],
) -> None:
@@ -127,3 +140,38 @@ def test_binding_mutation_rejects_missing_target(sqlite_session_factory: session
"account-1",
TagBindingInput(("tag-1",), "missing", "snippet"),
)
def test_binding_mutations_validate_skill_target(
sqlite_session_factory: sessionmaker[Session],
) -> None:
with sqlite_session_factory.begin() as session:
session.add_all(
[
_skill("skill-1", workspace_id="workspace-1"),
_tag("tag-1", workspace_id="workspace-1", tag_type=TagType.SKILL, name="Skill"),
_tag("tag-2", workspace_id="workspace-1", tag_type=TagType.APP, name="Wrong type"),
_tag("tag-3", workspace_id="workspace-2", tag_type=TagType.SKILL, name="Wrong workspace"),
]
)
repository = TagRepository(sqlite_session_factory)
binding = TagBindingInput(("tag-1", "tag-2", "tag-3"), "skill-1", "skill")
repository.create_bindings("workspace-1", "account-1", binding)
with sqlite_session_factory() as session:
bindings = session.scalars(select(TagBinding).where(TagBinding.target_id == "skill-1")).all()
assert len(bindings) == 1
assert bindings[0].tag_id == "tag-1"
assert bindings[0].tenant_id == "workspace-1"
repository.delete_bindings("workspace-1", binding)
with sqlite_session_factory() as session:
assert session.scalars(select(TagBinding).where(TagBinding.target_id == "skill-1")).all() == []
with pytest.raises(TagBindingTargetNotFoundError, match="Skill not found"):
repository.create_bindings(
"workspace-1",
"account-1",
TagBindingInput(("tag-1",), "missing", "skill"),
)
@@ -246,6 +246,7 @@ def test_export_agent_app_uses_draft_or_active_snapshot(use_draft: bool) -> None
draft = SimpleNamespace(config_snapshot_dict=AgentSoulConfig(config_note="draft").model_dump(mode="json"))
session = Mock()
session.scalar.side_effect = [agent, draft if use_draft else None]
session.execute.return_value = []
service = AgentDslService(session)
require_snapshot = Mock(return_value=_snapshot(soul=AgentSoulConfig(config_note="snapshot")))
service._require_snapshot = require_snapshot
@@ -271,6 +272,7 @@ def test_export_workflow_packages_deduplicates_shared_agent() -> None:
]
session = Mock()
session.scalars.return_value.all.return_value = bindings
session.execute.return_value = []
service = AgentDslService(session)
service._require_agent = Mock(return_value=_agent())
service._require_snapshot = Mock(return_value=_snapshot())
@@ -3481,7 +3481,7 @@ def test_composer_current_version_and_error_paths(monkeypatch: pytest.MonkeyPatc
monkeypatch.setattr(
AgentComposerService,
"_require_agent",
lambda **kwargs: SimpleNamespace(updated_by=None, active_config_is_published=False),
lambda **kwargs: SimpleNamespace(id="agent-1", updated_by=None, active_config_is_published=False),
)
result = AgentComposerService._save_to_current_version(
session=session,
@@ -5159,7 +5159,12 @@ class TestAgentAppBackingAgent:
use_icon_as_answer_icon=False,
tracing=None,
)
source_agent = SimpleNamespace(id="source-agent", role="Analyst")
source_agent = SimpleNamespace(
id="source-agent",
role="Analyst",
active_config_snapshot_id="source-snapshot",
active_config_is_published=True,
)
target_app = SimpleNamespace(id="target-app")
session = sqlite_session
service = AgentRosterService(session)
@@ -5225,7 +5230,12 @@ class TestAgentAppBackingAgent:
use_icon_as_answer_icon=False,
tracing=None,
)
source_agent = SimpleNamespace(id="source-agent", role="Analyst")
source_agent = SimpleNamespace(
id="source-agent",
role="Analyst",
active_config_snapshot_id="source-snapshot",
active_config_is_published=True,
)
target_app = SimpleNamespace(id="target-app")
session = sqlite_session
service = AgentRosterService(session)
@@ -13,7 +13,7 @@ from services.agent import skill_package_service as skill_package_service_module
from services.agent.skill_package_service import NormalizedSkillPackage, SkillPackageError, SkillPackageService
_SKILL_MD = """---
name: PDF Toolkit
name: pdf-toolkit
description: Tools for working with PDF files.
---
@@ -43,7 +43,7 @@ def _archive_members(content: bytes) -> list[str]:
def test_valid_skill_normalizes_manifest():
manifest = _normalize({"SKILL.md": _SKILL_MD.encode(), "scripts/run.py": b"print('hi')\n"}).manifest
assert manifest.name == "PDF Toolkit"
assert manifest.name == "pdf-toolkit"
assert manifest.description == "Tools for working with PDF files."
assert manifest.entry_path == "SKILL.md"
assert set(manifest.files) == {"SKILL.md", "scripts/run.py"}
@@ -51,10 +51,10 @@ def test_valid_skill_normalizes_manifest():
assert len(manifest.hash) == 64
def test_name_falls_back_to_heading_without_frontmatter():
manifest = _normalize({"SKILL.md": b"# Heading Name\n\nbody"}).manifest
assert manifest.name == "Heading Name"
assert manifest.description == ""
def test_name_and_description_are_required_in_frontmatter():
with pytest.raises(SkillPackageError) as exc_info:
_normalize({"SKILL.md": b"# heading-name\n\nbody"})
assert exc_info.value.code == "missing_skill_name"
def test_shallowest_skill_md_preferred_during_normalization():
@@ -155,7 +155,18 @@ def test_validate_and_normalize_strips_deeper_selected_skill_root():
({"README.md": b"x"}, "skill.zip", "missing_skill_md"),
({"SKILL.md": _SKILL_MD.encode()}, "skill.tar", "unsupported_extension"),
({"SKILL.md": b""}, "skill.zip", "empty_skill_md"),
({"SKILL.md": b"no name here"}, "skill.zip", "missing_skill_name"),
({"SKILL.md": b"---\ndescription: valid\n---\n# no name here"}, "skill.zip", "missing_skill_name"),
({"SKILL.md": b"---\nname: pdf-toolkit\n---\n# no description"}, "skill.zip", "missing_skill_description"),
(
{"SKILL.md": b"---\nname: PDF Toolkit\ndescription: valid\n---\n# invalid name"},
"skill.zip",
"invalid_skill_name",
),
(
{"SKILL.md": f"---\nname: pdf-toolkit\ndescription: {'x' * 1025}\n---\n# long".encode()},
"skill.zip",
"invalid_skill_description",
),
({"SKILL.md": b"\xff\xfenot utf8"}, "skill.zip", "skill_md_not_utf8"),
],
)
@@ -224,10 +235,10 @@ def test_bad_frontmatter_yaml_rejected():
assert exc_info.value.code == "invalid_frontmatter"
def test_unterminated_frontmatter_falls_back_to_heading():
# leading '---' with no closing fence -> no frontmatter, use the heading
manifest = _normalize({"SKILL.md": b"---\n# Heading Wins\nbody"}).manifest
assert manifest.name == "Heading Wins"
def test_unterminated_frontmatter_rejected():
with pytest.raises(SkillPackageError) as exc_info:
_normalize({"SKILL.md": b"---\n# heading-wins\nbody"})
assert exc_info.value.code == "missing_skill_name"
def test_validate_and_normalize_rejects_files_outside_selected_skill_root():
@@ -138,6 +138,7 @@ def _target(
) -> AgentConfigTarget:
agent_soul = soul or _soul()
return AgentConfigTarget(
tenant_id=TENANT,
agent_id=AGENT,
version_id=version_id,
kind=kind,
@@ -763,6 +764,89 @@ def test_inspect_skill_maps_invalid_archives_to_service_errors(archive_bytes: by
assert exc_info.value.status_code == 500
def test_request_download_falls_back_to_workspace_runtime_skill() -> None:
service = AgentConfigService()
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
expected = SimpleNamespace(
filename="workspace-skill.zip",
mime_type="application/zip",
size=123,
download_uri="/files/tools/workspace.zip?signature=1",
)
with (
patch.object(service, "resolve_target", return_value=target),
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
patch.object(service, "_resolve_download_request", return_value=expected) as resolve_download_request,
):
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
{
"name": "workspace-skill",
"file_id": "workspace-archive-id",
}
]
download = service.request_download(
tenant_id=TENANT,
agent_id=AGENT,
config_version_id="draft-1",
config_version_kind=AgentConfigVersionKind.DRAFT,
kind="skill",
name="workspace-skill",
user_id=USER,
)
assert download is expected
resolve_download_request.assert_called_once_with(
tenant_id=TENANT,
file_kind="tool_file",
file_id="workspace-archive-id",
filename="workspace-skill.zip",
default_mime_type="application/zip",
missing_code="config_skill_not_found",
missing_message="config skill payload is missing",
)
def test_inspect_skill_falls_back_to_workspace_runtime_skill() -> None:
service = AgentConfigService()
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
archive = _zip_bytes(
{
"SKILL.md": b"---\nname: workspace-skill\ndescription: Workspace skill.\n---\n# Workspace",
"references/policy.md": b"Policy",
}
)
with (
patch.object(service, "resolve_target", return_value=target),
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
):
skill_management_service.return_value.pull_runtime_agent_skill.return_value = SimpleNamespace(payload=archive)
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
{
"id": "skill-1",
"name": "workspace-skill",
"description": "Workspace skill.",
"size": len(archive),
"hash": "hash",
"mime_type": "application/zip",
}
]
result = service.inspect_skill(
tenant_id=TENANT,
agent_id=AGENT,
config_version_id="draft-1",
config_version_kind=AgentConfigVersionKind.DRAFT,
name="workspace-skill",
user_id=USER,
)
assert result["id"] == "skill-1"
assert result["source"] == "config_skill_zip"
assert result["skill_md"]["text"] == "---\nname: workspace-skill\ndescription: Workspace skill.\n---\n# Workspace"
assert [item["path"] for item in result["files"]] == ["SKILL.md", "references", "references/policy.md"]
def test_manifest_uses_items_shape_without_download_urls() -> None:
target = _target(
kind=AgentConfigVersionKind.DRAFT,
@@ -774,7 +858,9 @@ def test_manifest_uses_items_shape_without_download_urls() -> None:
),
)
manifest = AgentConfigService._manifest_for_target(target)
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
skill_management_service.return_value.list_runtime_agent_skills.return_value = []
manifest = AgentConfigService._manifest_for_target(target)
assert manifest == {
"agent_id": AGENT,
@@ -831,7 +917,9 @@ def test_manifest_preserves_missing_config_assets_and_download_rejects_them(sqli
user_id=USER,
)
manifest = service._manifest_for_target(target)
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
skill_management_service.return_value.list_runtime_agent_skills.return_value = []
manifest = service._manifest_for_target(target)
assert manifest["skills"]["items"][0]["is_missing"] is True # type: ignore[index]
assert manifest["files"]["items"][0]["is_missing"] is True # type: ignore[index]
@@ -881,6 +969,79 @@ def test_config_asset_refs_require_file_id_unless_marked_missing() -> None:
)
def test_manifest_appends_published_workspace_skills() -> None:
target = _target(
kind=AgentConfigVersionKind.DRAFT,
writable=False,
soul=_soul(
config_skills=[AgentConfigSkillRefConfig(name="alpha", description="Alpha skill", file_id="tool-file-1")]
),
)
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
{
"id": "workspace-skill-id",
"name": "beta",
"file_id": "tool-file-2",
"description": "Beta workspace skill",
"size": 123,
"hash": "sha256:beta",
"mime_type": "application/zip",
},
{
"id": "duplicate",
"name": "alpha",
"file_id": "tool-file-ignored",
"description": "Duplicate workspace skill",
"size": 456,
"hash": "sha256:ignored",
"mime_type": "application/zip",
},
]
manifest = AgentConfigService._manifest_for_target(target)
assert [item["name"] for item in manifest["skills"]["items"]] == ["alpha", "beta"]
assert manifest["skills"]["items"][1]["file_id"] == "tool-file-2"
def test_list_skills_excludes_workspace_skill_bindings() -> None:
target = _target(
kind=AgentConfigVersionKind.DRAFT,
writable=False,
soul=_soul(
config_skills=[AgentConfigSkillRefConfig(name="alpha", description="Alpha skill", file_id="tool-file-1")]
),
)
service = AgentConfigService()
with (
patch.object(service, "resolve_target", return_value=target),
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
):
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
{
"id": "workspace-skill-id",
"name": "beta",
"file_id": "tool-file-2",
"description": "Beta workspace skill",
"size": 123,
"hash": "sha256:beta",
"mime_type": "application/zip",
}
]
result = service.list_skills(
tenant_id=target.tenant_id,
agent_id=target.agent_id,
config_version_id=target.version_id,
config_version_kind=target.kind,
user_id=None,
)
assert [item["name"] for item in result["items"]] == ["alpha"]
skill_management_service.return_value.list_runtime_agent_skills.assert_not_called()
def test_preview_skill_file_returns_text_preview() -> None:
service = AgentConfigService()
target = _target(
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,7 @@ from services.tag_application_service import (
CreateTagInput,
TagApplicationService,
TagBindingInput,
TagBindingTargetNotFoundError,
TagSummary,
UpdateTagInput,
)
@@ -47,3 +48,7 @@ def test_service_rejects_context_without_active_workspace() -> None:
with pytest.raises(RuntimeError, match="active workspace"):
service.list_tags(context, "app")
def test_skill_binding_target_error_message() -> None:
assert str(TagBindingTargetNotFoundError("skill")) == "Skill not found"
-8
View File
@@ -3414,14 +3414,6 @@
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/add-variable-popup-with-position.tsx": {
"typescript/no-explicit-any": {
"count": 2
@@ -20,7 +20,7 @@ export type TagBindingRemovePayload = {
type: TagType
}
export type TagType = 'app' | 'knowledge' | 'snippet'
export type TagType = 'app' | 'knowledge' | 'skill' | 'snippet'
export type PostTagBindingsData = {
body: TagBindingPayload
@@ -14,7 +14,7 @@ export const zSimpleResultResponse = z.object({
*
* Tag type
*/
export const zTagType = z.enum(['app', 'knowledge', 'snippet'])
export const zTagType = z.enum(['app', 'knowledge', 'skill', 'snippet'])
/**
* TagBindingPayload
@@ -22,14 +22,14 @@ export type TagUpdateRequestPayload = {
name: string
}
export type TagType = 'app' | 'knowledge' | 'snippet'
export type TagType = 'app' | 'knowledge' | 'skill' | 'snippet'
export type GetTagsData = {
body?: never
path?: never
query: {
keyword?: string
type: 'app' | 'knowledge' | 'snippet'
type: 'app' | 'knowledge' | 'skill' | 'snippet'
}
url: '/tags'
}
@@ -29,7 +29,7 @@ export const zTagUpdateRequestPayload = z.object({
*
* Tag type
*/
export const zTagType = z.enum(['app', 'knowledge', 'snippet'])
export const zTagType = z.enum(['app', 'knowledge', 'skill', 'snippet'])
/**
* TagBasePayload
@@ -41,7 +41,7 @@ export const zTagBasePayload = z.object({
export const zGetTagsQuery = z.object({
keyword: z.string().optional(),
type: z.enum(['app', 'knowledge', 'snippet']),
type: z.enum(['app', 'knowledge', 'skill', 'snippet']),
})
/**
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,16 @@ export type AgentProviderListResponse = Array<{
[key: string]: unknown
}>
export type AgentSkillBindingsResponse = {
agent_id: string
data?: Array<AgentSkillBindingItemResponse>
skill_ids?: Array<string>
}
export type AgentSkillBindingsPayload = {
skill_ids?: Array<string>
}
export type SnippetPaginationResponse = {
data: Array<SnippetListItemResponse>
has_more: boolean
@@ -642,6 +652,195 @@ export type WorkspaceAccessMatrix = {
pagination?: Pagination | null
}
export type SkillListResponse = {
data?: Array<SkillResponse>
has_more?: boolean
limit?: number
page?: number
total?: number
}
export type SkillCreatePayload = {
description?: string
display_name?: string | null
icon?: string
name?: string | null
tags?: Array<string>
}
export type SkillDetailResponse = {
created_at: number
created_by?: string | null
created_by_name?: string | null
description: string
display_name: string
files?: Array<SkillFileResponse>
icon: string
id: string
latest_published_at?: number | null
latest_published_version_id?: string | null
latest_published_version_number?: number | null
name: string
name_manually_edited?: boolean
reference_count?: number
tags?: Array<string>
updated_at: number
updated_by?: string | null
updated_by_name?: string | null
visibility: string
}
export type SkillFileUploadResponse = {
hash: string
id: string
mime_type: string
name: string
size: number
}
export type SkillTagListResponse = {
data?: Array<SkillTagResponse>
}
export type SkillDeletePayload = {
confirmation_name?: string | null
}
export type SkillDeleteResponse = {
deleted: boolean
id: string
}
export type SkillMetadataPayload = {
display_name?: string | null
expected_updated_at?: number | null
icon?: string | null
tags?: Array<string> | null
}
export type SkillResponse = {
created_at: number
created_by?: string | null
created_by_name?: string | null
description: string
display_name: string
icon: string
id: string
latest_published_at?: number | null
latest_published_version_id?: string | null
latest_published_version_number?: number | null
name: string
name_manually_edited?: boolean
reference_count?: number
tags?: Array<string>
updated_at: number
updated_by?: string | null
updated_by_name?: string | null
visibility: string
}
export type SkillAssistMessagePayload = {
attachments?: Array<SkillAssistAttachmentPayload>
history?: Array<SkillAssistHistoryMessagePayload>
message: string
model?: SkillAssistModelPayload | null
target_path?: string | null
}
export type SkillDraftFileOperationPayload = {
content?: string | null
expected_updated_at?: number | null
hash?: string | null
mime_type?: string | null
operation: SkillDraftFileOperation
path: string
size?: number | null
target_path?: string | null
tool_file_id?: string | null
}
export type SkillDraftTreePayload = {
expected_updated_at?: number | null
files?: Array<SkillDraftTreeItemPayload>
}
export type SkillDraftFileCheckPayload = {
files?: Array<SkillDraftFileCheckItemPayload>
}
export type SkillFileCheckResponse = {
data?: {
[key: string]: SkillFileCheckItemResponse
}
}
export type SkillFilePreviewResponse = {
content: string
hash: string
mime_type: string
path: string
size: number
}
export type SkillPublishPayload = {
publish_note?: string
version_name?: string | null
}
export type SkillVersionResponse = {
archive_size: number
created_at: number
hash_code: string
id: string
is_latest?: boolean
publish_note: string
published_by?: string | null
published_by_name?: string | null
skill_id: string
version_name: string
version_number: number
}
export type SkillReferenceListResponse = {
data?: Array<SkillReferenceResponse>
}
export type SkillRestorePayload = {
publish_note?: string
version_id: string
version_name?: string | null
}
export type SkillVersionListResponse = {
data?: Array<SkillVersionResponse>
}
export type SkillVersionDeleteResponse = {
deleted: boolean
id: string
latest_published_version_id?: string | null
}
export type SkillVersionDetailResponse = {
archive_size: number
created_at: number
files?: Array<SkillFileResponse>
hash_code: string
id: string
is_latest?: boolean
publish_note: string
published_by?: string | null
published_by_name?: string | null
skill_id: string
version_name: string
version_number: number
}
export type SkillVersionUpdatePayload = {
publish_note?: string
version_name?: string | null
}
export type CurrentWorkspaceSummaryResponse = {
credits: number | null
id: string
@@ -1057,6 +1256,21 @@ export type TenantListItemResponse = {
status?: string | null
}
export type AgentSkillBindingItemResponse = {
description: string
display_name: string
file_count: number
icon: string
id: string
latest_published_at?: number | null
latest_published_version_id?: string | null
name: string
priority: number
status: string
tags?: Array<string>
updated_at: number
}
export type SnippetListItemResponse = {
author_name: string | null
created_at: number
@@ -1541,6 +1755,99 @@ export type AccessPolicyRole = {
role_tag?: string
}
export type SkillFileResponse = {
content?: string | null
hash?: string | null
id?: string | null
kind: string
mime_type?: string | null
path: string
size?: number | null
storage?: string | null
tool_file_id?: string | null
}
export type SkillTagResponse = {
count: number
tag: string
}
export type SkillAssistAttachmentPayload = {
mime_type?: string | null
name: string
size?: number | null
tool_file_id: string
}
export type SkillAssistHistoryMessagePayload = {
content: string
role: 'assistant' | 'user'
suggested_display_name?: string | null
suggested_name?: string | null
}
export type SkillAssistModelPayload = {
model: string
model_settings?: {
[key: string]: unknown
} | null
plugin_id?: string | null
provider: string
}
export type SkillDraftFileOperation =
| 'delete'
| 'mkdir'
| 'rename'
| 'upsert_text'
| 'upsert_tool_file'
export type SkillDraftTreeItemPayload = {
content?: string | null
hash?: string | null
kind?: SkillFileKind
mime_type?: string | null
path: string
size?: number | null
storage?: SkillFileStorage | null
tool_file_id?: string | null
}
export type SkillDraftFileCheckItemPayload = {
filename: string
mime_type?: string | null
path?: string | null
size: number
}
export type SkillFileCheckItemResponse = {
errors?: Array<SkillFileCheckErrorResponse>
extension: string
filename: string
mime_type: string
path: string
size: number
}
export type SkillReferenceResponse = {
agent_icon?: string | null
agent_icon_background?: string | null
agent_icon_type?: string | null
agent_id: string
app_id?: string | null
display_name: string
name: string
node_id?: string | null
node_name?: string | null
type: string
workflow_icon?: string | null
workflow_icon_background?: string | null
workflow_icon_type?: string | null
workflow_id?: string | null
workflow_name?: string | null
workflow_version?: string | null
}
export type CloudPlan = 'professional' | 'sandbox' | 'team'
export type TenantAccountRole = 'admin' | 'dataset_operator' | 'editor' | 'normal' | 'owner'
@@ -2050,6 +2357,15 @@ export type PermissionCatalogItem = {
name: string
}
export type SkillFileKind = 'directory' | 'file'
export type SkillFileStorage = 'text' | 'tool_file'
export type SkillFileCheckErrorResponse = {
code: string
message: string
}
export type ToolParameter = {
auto_generate?: PluginParameterAutoGenerate | null
default?:
@@ -2533,6 +2849,38 @@ export type GetWorkspacesCurrentAgentProvidersResponses = {
export type GetWorkspacesCurrentAgentProvidersResponse =
GetWorkspacesCurrentAgentProvidersResponses[keyof GetWorkspacesCurrentAgentProvidersResponses]
export type GetWorkspacesCurrentAgentsByAgentIdSkillsData = {
body?: never
path: {
agent_id: string
}
query?: never
url: '/workspaces/current/agents/{agent_id}/skills'
}
export type GetWorkspacesCurrentAgentsByAgentIdSkillsResponses = {
200: AgentSkillBindingsResponse
}
export type GetWorkspacesCurrentAgentsByAgentIdSkillsResponse =
GetWorkspacesCurrentAgentsByAgentIdSkillsResponses[keyof GetWorkspacesCurrentAgentsByAgentIdSkillsResponses]
export type PutWorkspacesCurrentAgentsByAgentIdSkillsData = {
body: AgentSkillBindingsPayload
path: {
agent_id: string
}
query?: never
url: '/workspaces/current/agents/{agent_id}/skills'
}
export type PutWorkspacesCurrentAgentsByAgentIdSkillsResponses = {
200: AgentSkillBindingsResponse
}
export type PutWorkspacesCurrentAgentsByAgentIdSkillsResponse =
PutWorkspacesCurrentAgentsByAgentIdSkillsResponses[keyof PutWorkspacesCurrentAgentsByAgentIdSkillsResponses]
export type GetWorkspacesCurrentCustomizedSnippetsData = {
body?: never
path?: never
@@ -4733,6 +5081,385 @@ export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses = {
export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse =
GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses[keyof GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses]
export type GetWorkspacesCurrentSkillsData = {
body?: never
path?: never
query?: {
keyword?: string
limit?: number
page?: number
tag?: Array<string>
}
url: '/workspaces/current/skills'
}
export type GetWorkspacesCurrentSkillsResponses = {
200: SkillListResponse
}
export type GetWorkspacesCurrentSkillsResponse =
GetWorkspacesCurrentSkillsResponses[keyof GetWorkspacesCurrentSkillsResponses]
export type PostWorkspacesCurrentSkillsData = {
body: SkillCreatePayload
path?: never
query?: never
url: '/workspaces/current/skills'
}
export type PostWorkspacesCurrentSkillsResponses = {
201: SkillDetailResponse
}
export type PostWorkspacesCurrentSkillsResponse =
PostWorkspacesCurrentSkillsResponses[keyof PostWorkspacesCurrentSkillsResponses]
export type PostWorkspacesCurrentSkillsFilesUploadData = {
body: {
file: Blob | File
}
path?: never
query?: never
url: '/workspaces/current/skills/files/upload'
}
export type PostWorkspacesCurrentSkillsFilesUploadResponses = {
201: SkillFileUploadResponse
}
export type PostWorkspacesCurrentSkillsFilesUploadResponse =
PostWorkspacesCurrentSkillsFilesUploadResponses[keyof PostWorkspacesCurrentSkillsFilesUploadResponses]
export type PostWorkspacesCurrentSkillsImportData = {
body?: never
path?: never
query?: never
url: '/workspaces/current/skills/import'
}
export type PostWorkspacesCurrentSkillsImportResponses = {
201: SkillDetailResponse
}
export type PostWorkspacesCurrentSkillsImportResponse =
PostWorkspacesCurrentSkillsImportResponses[keyof PostWorkspacesCurrentSkillsImportResponses]
export type GetWorkspacesCurrentSkillsTagsData = {
body?: never
path?: never
query?: never
url: '/workspaces/current/skills/tags'
}
export type GetWorkspacesCurrentSkillsTagsResponses = {
200: SkillTagListResponse
}
export type GetWorkspacesCurrentSkillsTagsResponse =
GetWorkspacesCurrentSkillsTagsResponses[keyof GetWorkspacesCurrentSkillsTagsResponses]
export type DeleteWorkspacesCurrentSkillsBySkillIdData = {
body: SkillDeletePayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}'
}
export type DeleteWorkspacesCurrentSkillsBySkillIdResponses = {
200: SkillDeleteResponse
}
export type DeleteWorkspacesCurrentSkillsBySkillIdResponse =
DeleteWorkspacesCurrentSkillsBySkillIdResponses[keyof DeleteWorkspacesCurrentSkillsBySkillIdResponses]
export type GetWorkspacesCurrentSkillsBySkillIdData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}'
}
export type GetWorkspacesCurrentSkillsBySkillIdResponses = {
200: SkillDetailResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdResponse =
GetWorkspacesCurrentSkillsBySkillIdResponses[keyof GetWorkspacesCurrentSkillsBySkillIdResponses]
export type PatchWorkspacesCurrentSkillsBySkillIdData = {
body: SkillMetadataPayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}'
}
export type PatchWorkspacesCurrentSkillsBySkillIdResponses = {
200: SkillResponse
}
export type PatchWorkspacesCurrentSkillsBySkillIdResponse =
PatchWorkspacesCurrentSkillsBySkillIdResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdResponses]
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesData = {
body: SkillAssistMessagePayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/assist/messages'
}
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses = {
200: {
[key: string]: unknown
}
}
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponse =
PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses[keyof PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses]
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/duplicate'
}
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses = {
201: SkillDetailResponse
}
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateResponse =
PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses[keyof PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses]
export type GetWorkspacesCurrentSkillsBySkillIdExportData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/export'
}
export type GetWorkspacesCurrentSkillsBySkillIdExportResponses = {
200: {
[key: string]: unknown
}
}
export type GetWorkspacesCurrentSkillsBySkillIdExportResponse =
GetWorkspacesCurrentSkillsBySkillIdExportResponses[keyof GetWorkspacesCurrentSkillsBySkillIdExportResponses]
export type PatchWorkspacesCurrentSkillsBySkillIdFilesData = {
body: SkillDraftFileOperationPayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/files'
}
export type PatchWorkspacesCurrentSkillsBySkillIdFilesResponses = {
200: SkillDetailResponse
}
export type PatchWorkspacesCurrentSkillsBySkillIdFilesResponse =
PatchWorkspacesCurrentSkillsBySkillIdFilesResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdFilesResponses]
export type PutWorkspacesCurrentSkillsBySkillIdFilesData = {
body: SkillDraftTreePayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/files'
}
export type PutWorkspacesCurrentSkillsBySkillIdFilesResponses = {
200: SkillDetailResponse
}
export type PutWorkspacesCurrentSkillsBySkillIdFilesResponse =
PutWorkspacesCurrentSkillsBySkillIdFilesResponses[keyof PutWorkspacesCurrentSkillsBySkillIdFilesResponses]
export type PostWorkspacesCurrentSkillsBySkillIdFilesCheckData = {
body: SkillDraftFileCheckPayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/files/check'
}
export type PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponses = {
200: SkillFileCheckResponse
}
export type PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponse =
PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponses[keyof PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponses]
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentData = {
body?: never
path: {
skill_id: string
}
query: {
download?: string
path: string
version_id?: string
}
url: '/workspaces/current/skills/{skill_id}/files/content'
}
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses = {
200: BinaryFileResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentResponse =
GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses[keyof GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses]
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewData = {
body?: never
path: {
skill_id: string
}
query: {
path: string
version_id?: string
}
url: '/workspaces/current/skills/{skill_id}/files/preview'
}
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses = {
200: SkillFilePreviewResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse =
GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses[keyof GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses]
export type PostWorkspacesCurrentSkillsBySkillIdPublishData = {
body: SkillPublishPayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/publish'
}
export type PostWorkspacesCurrentSkillsBySkillIdPublishResponses = {
200: SkillVersionResponse
}
export type PostWorkspacesCurrentSkillsBySkillIdPublishResponse =
PostWorkspacesCurrentSkillsBySkillIdPublishResponses[keyof PostWorkspacesCurrentSkillsBySkillIdPublishResponses]
export type GetWorkspacesCurrentSkillsBySkillIdReferencesData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/references'
}
export type GetWorkspacesCurrentSkillsBySkillIdReferencesResponses = {
200: SkillReferenceListResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdReferencesResponse =
GetWorkspacesCurrentSkillsBySkillIdReferencesResponses[keyof GetWorkspacesCurrentSkillsBySkillIdReferencesResponses]
export type PostWorkspacesCurrentSkillsBySkillIdRestoreData = {
body: SkillRestorePayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/restore'
}
export type PostWorkspacesCurrentSkillsBySkillIdRestoreResponses = {
200: SkillDetailResponse
}
export type PostWorkspacesCurrentSkillsBySkillIdRestoreResponse =
PostWorkspacesCurrentSkillsBySkillIdRestoreResponses[keyof PostWorkspacesCurrentSkillsBySkillIdRestoreResponses]
export type GetWorkspacesCurrentSkillsBySkillIdVersionsData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/versions'
}
export type GetWorkspacesCurrentSkillsBySkillIdVersionsResponses = {
200: SkillVersionListResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdVersionsResponse =
GetWorkspacesCurrentSkillsBySkillIdVersionsResponses[keyof GetWorkspacesCurrentSkillsBySkillIdVersionsResponses]
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
body?: never
path: {
skill_id: string
version_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
}
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
200: SkillVersionDeleteResponse
}
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
body?: never
path: {
skill_id: string
version_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
}
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
200: SkillVersionDetailResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
body: SkillVersionUpdatePayload
path: {
skill_id: string
version_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
}
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
200: SkillVersionResponse
}
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
export type GetWorkspacesCurrentSummaryData = {
body?: never
path?: never
@@ -12,6 +12,13 @@ export const zAgentProviderResponse = z.record(z.string(), z.unknown())
*/
export const zAgentProviderListResponse = z.array(z.record(z.string(), z.unknown()))
/**
* AgentSkillBindingsPayload
*/
export const zAgentSkillBindingsPayload = z.object({
skill_ids: z.array(z.string()).optional(),
})
/**
* SnippetImportPayload
*
@@ -458,6 +465,157 @@ export const zReplaceBindingsRequest = z.object({
role_ids: z.array(z.string()).optional(),
})
/**
* SkillCreatePayload
*/
export const zSkillCreatePayload = z.object({
description: z.string().optional().default(''),
display_name: z.string().nullish(),
icon: z.string().optional().default('📄'),
name: z.string().nullish(),
tags: z.array(z.string()).optional(),
})
/**
* SkillFileUploadResponse
*/
export const zSkillFileUploadResponse = z.object({
hash: z.string(),
id: z.string(),
mime_type: z.string(),
name: z.string(),
size: z.int(),
})
/**
* SkillDeletePayload
*/
export const zSkillDeletePayload = z.object({
confirmation_name: z.string().nullish(),
})
/**
* SkillDeleteResponse
*/
export const zSkillDeleteResponse = z.object({
deleted: z.boolean(),
id: z.string(),
})
/**
* SkillMetadataPayload
*/
export const zSkillMetadataPayload = z.object({
display_name: z.string().nullish(),
expected_updated_at: z.int().nullish(),
icon: z.string().nullish(),
tags: z.array(z.string()).nullish(),
})
/**
* SkillResponse
*/
export const zSkillResponse = z.object({
created_at: z.int(),
created_by: z.string().nullish(),
created_by_name: z.string().nullish(),
description: z.string(),
display_name: z.string(),
icon: z.string(),
id: z.string(),
latest_published_at: z.int().nullish(),
latest_published_version_id: z.string().nullish(),
latest_published_version_number: z.int().nullish(),
name: z.string(),
name_manually_edited: z.boolean().optional().default(false),
reference_count: z.int().optional().default(0),
tags: z.array(z.string()).optional(),
updated_at: z.int(),
updated_by: z.string().nullish(),
updated_by_name: z.string().nullish(),
visibility: z.string(),
})
/**
* SkillListResponse
*/
export const zSkillListResponse = z.object({
data: z.array(zSkillResponse).optional(),
has_more: z.boolean().optional().default(false),
limit: z.int().optional().default(20),
page: z.int().optional().default(1),
total: z.int().optional().default(0),
})
/**
* SkillFilePreviewResponse
*/
export const zSkillFilePreviewResponse = z.object({
content: z.string(),
hash: z.string(),
mime_type: z.string(),
path: z.string(),
size: z.int(),
})
/**
* SkillPublishPayload
*/
export const zSkillPublishPayload = z.object({
publish_note: z.string().max(1024).optional().default(''),
version_name: z.string().max(128).nullish(),
})
/**
* SkillVersionResponse
*/
export const zSkillVersionResponse = z.object({
archive_size: z.int(),
created_at: z.int(),
hash_code: z.string(),
id: z.string(),
is_latest: z.boolean().optional().default(false),
publish_note: z.string(),
published_by: z.string().nullish(),
published_by_name: z.string().nullish(),
skill_id: z.string(),
version_name: z.string(),
version_number: z.int(),
})
/**
* SkillRestorePayload
*/
export const zSkillRestorePayload = z.object({
publish_note: z.string().max(1024).optional().default(''),
version_id: z.string(),
version_name: z.string().max(128).nullish(),
})
/**
* SkillVersionListResponse
*/
export const zSkillVersionListResponse = z.object({
data: z.array(zSkillVersionResponse).optional(),
})
/**
* SkillVersionDeleteResponse
*/
export const zSkillVersionDeleteResponse = z.object({
deleted: z.boolean(),
id: z.string(),
latest_published_version_id: z.string().nullish(),
})
/**
* SkillVersionUpdatePayload
*/
export const zSkillVersionUpdatePayload = z.object({
publish_note: z.string().max(1024).optional().default(''),
version_name: z.string().max(128).nullish(),
})
/**
* ApiToolProviderDeletePayload
*/
@@ -623,6 +781,33 @@ export const zSwitchWorkspacePayload = z.object({
tenant_id: z.string(),
})
/**
* AgentSkillBindingItemResponse
*/
export const zAgentSkillBindingItemResponse = z.object({
description: z.string(),
display_name: z.string(),
file_count: z.int(),
icon: z.string(),
id: z.string(),
latest_published_at: z.int().nullish(),
latest_published_version_id: z.string().nullish(),
name: z.string(),
priority: z.int(),
status: z.string(),
tags: z.array(z.string()).optional(),
updated_at: z.int(),
})
/**
* AgentSkillBindingsResponse
*/
export const zAgentSkillBindingsResponse = z.object({
agent_id: z.string(),
data: z.array(zAgentSkillBindingItemResponse).optional(),
skill_ids: z.array(z.string()).optional(),
})
/**
* IconInfo
*
@@ -1211,6 +1396,194 @@ export const zWorkspaceAccessMatrix = z.object({
pagination: zPagination.nullish(),
})
/**
* SkillFileResponse
*/
export const zSkillFileResponse = z.object({
content: z.string().nullish(),
hash: z.string().nullish(),
id: z.string().nullish(),
kind: z.string(),
mime_type: z.string().nullish(),
path: z.string(),
size: z.int().nullish(),
storage: z.string().nullish(),
tool_file_id: z.string().nullish(),
})
/**
* SkillDetailResponse
*/
export const zSkillDetailResponse = z.object({
created_at: z.int(),
created_by: z.string().nullish(),
created_by_name: z.string().nullish(),
description: z.string(),
display_name: z.string(),
files: z.array(zSkillFileResponse).optional(),
icon: z.string(),
id: z.string(),
latest_published_at: z.int().nullish(),
latest_published_version_id: z.string().nullish(),
latest_published_version_number: z.int().nullish(),
name: z.string(),
name_manually_edited: z.boolean().optional().default(false),
reference_count: z.int().optional().default(0),
tags: z.array(z.string()).optional(),
updated_at: z.int(),
updated_by: z.string().nullish(),
updated_by_name: z.string().nullish(),
visibility: z.string(),
})
/**
* SkillVersionDetailResponse
*/
export const zSkillVersionDetailResponse = z.object({
archive_size: z.int(),
created_at: z.int(),
files: z.array(zSkillFileResponse).optional(),
hash_code: z.string(),
id: z.string(),
is_latest: z.boolean().optional().default(false),
publish_note: z.string(),
published_by: z.string().nullish(),
published_by_name: z.string().nullish(),
skill_id: z.string(),
version_name: z.string(),
version_number: z.int(),
})
/**
* SkillTagResponse
*/
export const zSkillTagResponse = z.object({
count: z.int(),
tag: z.string(),
})
/**
* SkillTagListResponse
*/
export const zSkillTagListResponse = z.object({
data: z.array(zSkillTagResponse).optional(),
})
/**
* SkillAssistAttachmentPayload
*/
export const zSkillAssistAttachmentPayload = z.object({
mime_type: z.string().min(1).max(255).nullish(),
name: z.string().min(1).max(255),
size: z.int().gte(0).nullish(),
tool_file_id: z.string().min(1),
})
/**
* SkillAssistHistoryMessagePayload
*/
export const zSkillAssistHistoryMessagePayload = z.object({
content: z.string().min(1).max(8000),
role: z.enum(['assistant', 'user']),
suggested_display_name: z.string().max(128).nullish(),
suggested_name: z.string().max(128).nullish(),
})
/**
* SkillAssistModelPayload
*/
export const zSkillAssistModelPayload = z.object({
model: z.string().min(1).max(255),
model_settings: z.record(z.string(), z.unknown()).nullish(),
plugin_id: z.string().min(1).max(255).nullish(),
provider: z.string().min(1).max(255),
})
/**
* SkillAssistMessagePayload
*
* One user message and optional uploaded context for the Skill Authoring assistant.
*/
export const zSkillAssistMessagePayload = z.object({
attachments: z.array(zSkillAssistAttachmentPayload).max(10).optional(),
history: z.array(zSkillAssistHistoryMessagePayload).max(20).optional(),
message: z.string().min(1).max(8000),
model: zSkillAssistModelPayload.nullish(),
target_path: z.string().nullish(),
})
/**
* SkillDraftFileOperation
*/
export const zSkillDraftFileOperation = z.enum([
'delete',
'mkdir',
'rename',
'upsert_text',
'upsert_tool_file',
])
/**
* SkillDraftFileOperationPayload
*/
export const zSkillDraftFileOperationPayload = z.object({
content: z.string().nullish(),
expected_updated_at: z.int().nullish(),
hash: z.string().nullish(),
mime_type: z.string().nullish(),
operation: zSkillDraftFileOperation,
path: z.string(),
size: z.int().gte(0).nullish(),
target_path: z.string().nullish(),
tool_file_id: z.string().nullish(),
})
/**
* SkillDraftFileCheckItemPayload
*/
export const zSkillDraftFileCheckItemPayload = z.object({
filename: z.string().min(1).max(255),
mime_type: z.string().max(255).nullish(),
path: z.string().nullish(),
size: z.int().gte(0),
})
/**
* SkillDraftFileCheckPayload
*/
export const zSkillDraftFileCheckPayload = z.object({
files: z.array(zSkillDraftFileCheckItemPayload).max(100).optional(),
})
/**
* SkillReferenceResponse
*/
export const zSkillReferenceResponse = z.object({
agent_icon: z.string().nullish(),
agent_icon_background: z.string().nullish(),
agent_icon_type: z.string().nullish(),
agent_id: z.string(),
app_id: z.string().nullish(),
display_name: z.string(),
name: z.string(),
node_id: z.string().nullish(),
node_name: z.string().nullish(),
type: z.string(),
workflow_icon: z.string().nullish(),
workflow_icon_background: z.string().nullish(),
workflow_icon_type: z.string().nullish(),
workflow_id: z.string().nullish(),
workflow_name: z.string().nullish(),
workflow_version: z.string().nullish(),
})
/**
* SkillReferenceListResponse
*/
export const zSkillReferenceListResponse = z.object({
data: z.array(zSkillReferenceResponse).optional(),
})
/**
* CloudPlan
*
@@ -2204,6 +2577,69 @@ export const zPermissionCatalogResponse = z.object({
groups: z.array(zPermissionCatalogGroup).optional(),
})
/**
* SkillFileKind
*
* Draft file entry kind.
*/
export const zSkillFileKind = z.enum(['directory', 'file'])
/**
* SkillFileStorage
*
* How a draft file's content is stored.
*/
export const zSkillFileStorage = z.enum(['text', 'tool_file'])
/**
* SkillDraftTreeItemPayload
*/
export const zSkillDraftTreeItemPayload = z.object({
content: z.string().nullish(),
hash: z.string().nullish(),
kind: zSkillFileKind.optional().default('file'),
mime_type: z.string().nullish(),
path: z.string(),
size: z.int().gte(0).nullish(),
storage: zSkillFileStorage.nullish(),
tool_file_id: z.string().nullish(),
})
/**
* SkillDraftTreePayload
*/
export const zSkillDraftTreePayload = z.object({
expected_updated_at: z.int().nullish(),
files: z.array(zSkillDraftTreeItemPayload).optional(),
})
/**
* SkillFileCheckErrorResponse
*/
export const zSkillFileCheckErrorResponse = z.object({
code: z.string(),
message: z.string(),
})
/**
* SkillFileCheckItemResponse
*/
export const zSkillFileCheckItemResponse = z.object({
errors: z.array(zSkillFileCheckErrorResponse).optional(),
extension: z.string(),
filename: z.string(),
mime_type: z.string(),
path: z.string(),
size: z.int(),
})
/**
* SkillFileCheckResponse
*/
export const zSkillFileCheckResponse = z.object({
data: z.record(z.string(), zSkillFileCheckItemResponse).optional(),
})
/**
* Option
*/
@@ -3526,6 +3962,26 @@ export const zGetWorkspacesCurrentAgentProviderByProviderNameResponse = zAgentPr
*/
export const zGetWorkspacesCurrentAgentProvidersResponse = zAgentProviderListResponse
export const zGetWorkspacesCurrentAgentsByAgentIdSkillsPath = z.object({
agent_id: z.string(),
})
/**
* Agent Skill bindings
*/
export const zGetWorkspacesCurrentAgentsByAgentIdSkillsResponse = zAgentSkillBindingsResponse
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsBody = zAgentSkillBindingsPayload
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsPath = z.object({
agent_id: z.string(),
})
/**
* Agent Skill bindings replaced
*/
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsResponse = zAgentSkillBindingsResponse
export const zGetWorkspacesCurrentCustomizedSnippetsQuery = z.object({
creators: z.array(z.string()).optional(),
is_published: z.boolean().optional(),
@@ -4822,6 +5278,245 @@ export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdR
*/
export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse = zWorkspaceAccessMatrix
export const zGetWorkspacesCurrentSkillsQuery = z.object({
keyword: z.string().optional(),
limit: z.int().gte(1).lte(100).optional().default(20),
page: z.int().gte(1).lte(99999).optional().default(1),
tag: z.array(z.string()).optional(),
})
/**
* Workspace skills
*/
export const zGetWorkspacesCurrentSkillsResponse = zSkillListResponse
export const zPostWorkspacesCurrentSkillsBody = zSkillCreatePayload
/**
* Skill created
*/
export const zPostWorkspacesCurrentSkillsResponse = zSkillDetailResponse
export const zPostWorkspacesCurrentSkillsFilesUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
})
/**
* Skill draft file uploaded
*/
export const zPostWorkspacesCurrentSkillsFilesUploadResponse = zSkillFileUploadResponse
/**
* Skill imported
*/
export const zPostWorkspacesCurrentSkillsImportResponse = zSkillDetailResponse
/**
* Workspace Skill tags
*/
export const zGetWorkspacesCurrentSkillsTagsResponse = zSkillTagListResponse
export const zDeleteWorkspacesCurrentSkillsBySkillIdBody = zSkillDeletePayload
export const zDeleteWorkspacesCurrentSkillsBySkillIdPath = z.object({
skill_id: z.string(),
})
/**
* Skill deleted
*/
export const zDeleteWorkspacesCurrentSkillsBySkillIdResponse = zSkillDeleteResponse
export const zGetWorkspacesCurrentSkillsBySkillIdPath = z.object({
skill_id: z.string(),
})
/**
* Skill detail
*/
export const zGetWorkspacesCurrentSkillsBySkillIdResponse = zSkillDetailResponse
export const zPatchWorkspacesCurrentSkillsBySkillIdBody = zSkillMetadataPayload
export const zPatchWorkspacesCurrentSkillsBySkillIdPath = z.object({
skill_id: z.string(),
})
/**
* Skill updated
*/
export const zPatchWorkspacesCurrentSkillsBySkillIdResponse = zSkillResponse
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesBody = zSkillAssistMessagePayload
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesPath = z.object({
skill_id: z.string(),
})
/**
* Skill Authoring assistant event stream
*/
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponse = z.record(
z.string(),
z.unknown(),
)
export const zPostWorkspacesCurrentSkillsBySkillIdDuplicatePath = z.object({
skill_id: z.string(),
})
/**
* Skill duplicated
*/
export const zPostWorkspacesCurrentSkillsBySkillIdDuplicateResponse = zSkillDetailResponse
export const zGetWorkspacesCurrentSkillsBySkillIdExportPath = z.object({
skill_id: z.string(),
})
/**
* Published Skill zip archive
*/
export const zGetWorkspacesCurrentSkillsBySkillIdExportResponse = z.record(z.string(), z.unknown())
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesBody = zSkillDraftFileOperationPayload
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesPath = z.object({
skill_id: z.string(),
})
/**
* Draft file operation applied
*/
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesResponse = zSkillDetailResponse
export const zPutWorkspacesCurrentSkillsBySkillIdFilesBody = zSkillDraftTreePayload
export const zPutWorkspacesCurrentSkillsBySkillIdFilesPath = z.object({
skill_id: z.string(),
})
/**
* Draft files replaced
*/
export const zPutWorkspacesCurrentSkillsBySkillIdFilesResponse = zSkillDetailResponse
export const zPostWorkspacesCurrentSkillsBySkillIdFilesCheckBody = zSkillDraftFileCheckPayload
export const zPostWorkspacesCurrentSkillsBySkillIdFilesCheckPath = z.object({
skill_id: z.string(),
})
/**
* Draft files checked
*/
export const zPostWorkspacesCurrentSkillsBySkillIdFilesCheckResponse = zSkillFileCheckResponse
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentPath = z.object({
skill_id: z.string(),
})
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentQuery = z.object({
download: z.string().optional(),
path: z.string(),
version_id: z.string().optional(),
})
/**
* Skill file content
*/
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentResponse = zBinaryFileResponse
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewPath = z.object({
skill_id: z.string(),
})
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewQuery = z.object({
path: z.string(),
version_id: z.string().optional(),
})
/**
* Skill file text preview
*/
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse = zSkillFilePreviewResponse
export const zPostWorkspacesCurrentSkillsBySkillIdPublishBody = zSkillPublishPayload
export const zPostWorkspacesCurrentSkillsBySkillIdPublishPath = z.object({
skill_id: z.string(),
})
/**
* Skill published
*/
export const zPostWorkspacesCurrentSkillsBySkillIdPublishResponse = zSkillVersionResponse
export const zGetWorkspacesCurrentSkillsBySkillIdReferencesPath = z.object({
skill_id: z.string(),
})
/**
* Skill references
*/
export const zGetWorkspacesCurrentSkillsBySkillIdReferencesResponse = zSkillReferenceListResponse
export const zPostWorkspacesCurrentSkillsBySkillIdRestoreBody = zSkillRestorePayload
export const zPostWorkspacesCurrentSkillsBySkillIdRestorePath = z.object({
skill_id: z.string(),
})
/**
* Skill version restored to draft
*/
export const zPostWorkspacesCurrentSkillsBySkillIdRestoreResponse = zSkillDetailResponse
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsPath = z.object({
skill_id: z.string(),
})
/**
* Skill versions
*/
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsResponse = zSkillVersionListResponse
export const zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
skill_id: z.string(),
version_id: z.string(),
})
/**
* Skill version deleted
*/
export const zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
zSkillVersionDeleteResponse
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
skill_id: z.string(),
version_id: z.string(),
})
/**
* Skill version detail
*/
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
zSkillVersionDetailResponse
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdBody =
zSkillVersionUpdatePayload
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
skill_id: z.string(),
version_id: z.string(),
})
/**
* Skill version updated
*/
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
zSkillVersionResponse
/**
* Success
*/
@@ -18,6 +18,7 @@ The current Flask-RESTX generator still emits these response entries under `appl
| service | GET | `/files/{file_id}/preview` | Original file MIME type, optionally attachment | `BinaryFileResponse` |
| console | GET | `/workspaces/current/plugin/icon` | Plugin asset MIME type | `BinaryFileResponse` |
| console | GET | `/workspaces/current/plugin/asset` | `application/octet-stream` | `BinaryFileResponse` |
| console | GET | `/workspaces/current/skills/{skill_id}/files/content` | Skill file MIME type, optionally attachment | `BinaryFileResponse` |
| console | GET | `/workspaces/current/tool-provider/builtin/{provider}/icon` | Tool icon MIME type | `BinaryFileResponse` |
| console | GET | `/workspaces/current/trigger-provider/{provider}/icon` | Trigger icon response | `BinaryFileResponse` |
| console | GET | `/workspaces/{tenant_id}/model-providers/{provider}/{icon_type}/{lang}` | Model provider icon MIME type | `BinaryFileResponse` |
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-5.75 -7.25)" d="M15.5 10.2912C18.875 10.6643 21.5 13.5256 21.5 17V23.75H8V17C8 13.5256 10.625 10.6643 14 10.2912V8H15.5V10.2912ZM14.75 20.75C16.821 20.75 18.5 19.071 18.5 17C18.5 14.9289 16.821 13.25 14.75 13.25C12.6789 13.25 11 14.9289 11 17C11 19.071 12.6789 20.75 14.75 20.75ZM14.75 19.25C13.5073 19.25 12.5 18.2427 12.5 17C12.5 15.7573 13.5073 14.75 14.75 14.75C15.9927 14.75 17 15.7573 17 17C17 18.2427 15.9927 19.25 14.75 19.25ZM14.75 17.75C15.1642 17.75 15.5 17.4142 15.5 17C15.5 16.5858 15.1642 16.25 14.75 16.25C14.3358 16.25 14 16.5858 14 17C14 17.4142 14.3358 17.75 14.75 17.75Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 746 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.87891 2.81952C13.2539 3.1926 15.8789 6.05391 15.8789 9.52832V16.2783H2.37891V9.52832C2.37891 6.05391 5.00394 3.1926 8.37891 2.81952V0.52832H9.87891V2.81952ZM14.3789 14.7783V9.52832C14.3789 6.62883 12.0284 4.27832 9.12891 4.27832C6.22941 4.27832 3.87891 6.62883 3.87891 9.52832V14.7783H14.3789ZM9.12891 13.2783C7.05784 13.2783 5.37891 11.5994 5.37891 9.52832C5.37891 7.45727 7.05784 5.77832 9.12891 5.77832C11.2 5.77832 12.8789 7.45727 12.8789 9.52832C12.8789 11.5994 11.2 13.2783 9.12891 13.2783ZM9.12891 11.7783C10.3716 11.7783 11.3789 10.771 11.3789 9.52832C11.3789 8.28565 10.3716 7.27832 9.12891 7.27832C7.88623 7.27832 6.87891 8.28565 6.87891 9.52832C6.87891 10.771 7.88623 11.7783 9.12891 11.7783ZM9.12891 10.2783C8.71468 10.2783 8.37891 9.94254 8.37891 9.52832C8.37891 9.1141 8.71468 8.77832 9.12891 8.77832C9.54313 8.77832 9.87891 9.1141 9.87891 9.52832C9.87891 9.94254 9.54313 10.2783 9.12891 10.2783Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-5.75 -6.3)" d="M21.5 21.2999C21.5 21.7141 21.1642 22.0499 20.75 22.0499H8.75C8.33579 22.0499 8 21.7141 8 21.2999V13.4167C8 13.1852 8.10685 12.9667 8.28954 12.8247L14.2896 8.15798C14.5604 7.94734 14.9396 7.94734 15.2104 8.15798L21.2104 12.8247C21.3931 12.9667 21.5 13.1852 21.5 13.4167V21.2999ZM14 16.0499V20.5499H15.5V16.0499H14Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 486 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75 14.2501H14.25V7.48367L9 3.40034L3.75 7.48367V14.2501H8.25V9.75006H9.75V14.2501ZM15.75 15.0001C15.75 15.4143 15.4142 15.7501 15 15.7501H3C2.58579 15.7501 2.25 15.4143 2.25 15.0001V7.11686C2.25 6.88542 2.35685 6.66694 2.53954 6.52485L8.53957 1.85818C8.8104 1.64753 9.1896 1.64753 9.46043 1.85818L15.4604 6.52485C15.6431 6.66694 15.75 6.88542 15.75 7.11686V15.0001Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

@@ -0,0 +1,7 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-6.39355 -5.66187)" fill="currentColor">
<path d="M22.25 11.75C22.6642 11.75 23 12.0858 23 12.5V20C23 20.4142 22.6642 20.75 22.25 20.75H8.75C8.33579 20.75 8 20.4142 8 20V12.5C8 12.0858 8.33579 11.75 8.75 11.75H22.25Z"/>
<path d="M14 8C14.4142 8 14.75 8.33579 14.75 8.75V10.25H9.5V8.75C9.5 8.33579 9.83579 8 10.25 8H14Z"/>
<path d="M20.75 8C21.1642 8 21.5 8.33579 21.5 8.75V10.25H16.25V8.75C16.25 8.33579 16.5858 8 17 8H20.75Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 577 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 2.25C7.91421 2.25 8.25 2.58579 8.25 3V5.25H9.75V3C9.75 2.58579 10.0858 2.25 10.5 2.25H14.25C14.6642 2.25 15 2.58579 15 3V5.25H15.75C16.1642 5.25 16.5 5.58579 16.5 6V14.25C16.5 14.6642 16.1642 15 15.75 15H2.25C1.83579 15 1.5 14.6642 1.5 14.25V6C1.5 5.58579 1.83579 5.25 2.25 5.25H3V3C3 2.58579 3.33579 2.25 3.75 2.25H7.5ZM3 13.5H15V6.75H3V13.5ZM11.25 5.25H13.5V3.75H11.25V5.25ZM4.5 5.25H6.75V3.75H4.5V5.25Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 588 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-6 -6.98706)" d="M21.5 20H10.25C9.83579 20 9.5 20.3358 9.5 20.75C9.5 21.1642 9.83579 21.5 10.25 21.5H21.5V23H10.25C9.00736 23 8 21.9927 8 20.75V9.5C8 8.67157 8.67157 8 9.5 8H21.5V20ZM17.75 13.25V11.75H11.75V13.25H17.75Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.75 13.5H4.5C4.08579 13.5 3.75 13.8358 3.75 14.25C3.75 14.6642 4.08579 15 4.5 15H15.75V16.5H4.5C3.25736 16.5 2.25 15.4927 2.25 14.25V3C2.25 2.17157 2.92157 1.5 3.75 1.5H15.75V13.5ZM3.75 12.0375C3.87117 12.0129 3.99658 12 4.125 12H14.25V3H3.75V12.0375ZM12 6.75H6V5.25H12V6.75Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-7.25 -6.875)" d="M23.75 21.5V23H8.75V21.5H9.5V16.4317C8.59551 15.8263 8 14.7951 8 13.625C8 13.0046 8.16832 12.4072 8.47491 11.8981L10.509 8.375C10.6429 8.14295 10.8905 8 11.1585 8H21.3415C21.6094 8 21.8571 8.14295 21.991 8.375L24.0181 11.8863C24.3317 12.4072 24.5 13.0046 24.5 13.625C24.5 14.7951 23.9045 15.8263 23 16.4317V21.5H23.75ZM11.5915 9.5L9.76698 12.6599C9.59307 12.9488 9.5 13.2792 9.5 13.625C9.5 14.6605 10.3395 15.5 11.375 15.5C12.1482 15.5 12.8335 15.0277 13.1163 14.3221C13.3681 13.6942 14.257 13.6942 14.5087 14.3221C14.7915 15.0277 15.4767 15.5 16.25 15.5C17.0232 15.5 17.7085 15.0277 17.9914 14.3221C18.2431 13.6942 19.1319 13.6942 19.3836 14.3221C19.6665 15.0277 20.3518 15.5 21.125 15.5C22.1605 15.5 23 14.6605 23 13.625C23 13.2792 22.9069 12.9488 22.726 12.6481L20.9085 9.5H11.5915Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 959 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.75 9.93165V15H16.5V16.5H1.5V15H2.25V9.93165C1.34551 9.32625 0.75 8.29515 0.75 7.125C0.75 6.50464 0.918322 5.90716 1.22491 5.3981L3.25898 1.875C3.39295 1.64295 3.64054 1.5 3.90849 1.5H14.0915C14.3594 1.5 14.6071 1.64295 14.741 1.875L16.7681 5.38629C17.0817 5.90716 17.25 6.50464 17.25 7.125C17.25 8.29515 16.6545 9.32625 15.75 9.93165ZM14.25 10.4794C14.1269 10.493 14.0017 10.5 13.875 10.5C12.9307 10.5 12.0592 10.1085 11.4375 9.4599C10.8158 10.1085 9.94432 10.5 9 10.5C8.05568 10.5 7.18418 10.1085 6.5625 9.4599C5.94082 10.1085 5.06933 10.5 4.125 10.5C3.99825 10.5 3.87313 10.493 3.75 10.4794V15H14.25V10.4794ZM4.34149 3L2.51698 6.15991C2.34307 6.44882 2.25 6.77917 2.25 7.125C2.25 8.16052 3.08947 9 4.125 9C4.89822 9 5.5835 8.52772 5.86634 7.82212C6.11805 7.19417 7.00695 7.19417 7.25866 7.82212C7.54148 8.52772 8.22675 9 9 9C9.77325 9 10.4585 8.52772 10.7414 7.82212C10.9931 7.19417 11.8819 7.19417 12.1336 7.82212C12.4165 8.52772 13.1018 9 13.875 9C14.9105 9 15.75 8.16052 15.75 7.125C15.75 6.77917 15.6569 6.44882 15.476 6.1481L13.6585 3H4.34149Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-6.2 -6.70433)" d="M8 18.6501C8 19.1406 8 19.3859 8.07199 19.6049C8.13567 19.7987 8.2398 19.9767 8.37747 20.1273C8.53307 20.2974 8.74686 20.4177 9.17444 20.6581L13.76 23.2375V21.0775L8 17.8819V18.6501ZM15.2 21.0775V23.2642L21.422 18.8858C21.78 18.6339 21.9591 18.5079 22.0885 18.3446C22.203 18.2001 22.2888 18.035 22.341 17.8581C22.4 17.6583 22.4 17.4394 22.4 17.0015V16.0375L15.2 21.0775ZM8 16.1582L13.76 19.2889V17.1175L8 13.9066V16.1582ZM15.2 17.1175V19.2775L22.4 14.2375V12.0775L15.2 17.1175ZM8.72 12.4375L14.48 15.6775L21.68 10.9975L17.1357 8.44137C16.6939 8.19286 16.473 8.0686 16.2398 8.02477C16.0335 7.98601 15.8213 7.99281 15.618 8.04468C15.388 8.10333 15.1755 8.24145 14.7505 8.51771L8.72 12.4375Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 863 B

@@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.8922 4.70983L10.3274 2.14208C10.1065 2.01781 9.99601 1.95568 9.87944 1.93377C9.77627 1.91439 9.67021 1.91779 9.56848 1.94372C9.45356 1.97304 9.34729 2.04211 9.13475 2.18024L3.04371 6.13944C2.85218 6.26393 2.75642 6.32617 2.68704 6.4092C2.62562 6.4827 2.57949 6.56771 2.55133 6.65926C2.51953 6.76268 2.51953 6.8769 2.51953 7.10533V12.2864C2.51953 12.5317 2.51953 12.6544 2.55552 12.7639C2.58736 12.8607 2.63943 12.9498 2.70826 13.025C2.78607 13.1101 2.89296 13.1702 3.10675 13.2905L7.67163 15.8582C7.8926 15.9825 8.00305 16.0446 8.11962 16.0665C8.22279 16.0859 8.32885 16.0825 8.43059 16.0566C8.5455 16.0273 8.65177 15.9582 8.86424 15.8201L14.9553 11.8609C15.1468 11.7364 15.2426 11.6742 15.312 11.5911C15.3734 11.5176 15.4195 11.4326 15.4476 11.341C15.4795 11.2376 15.4795 11.1234 15.4795 10.895V5.71389C15.4795 5.4686 15.4795 5.34596 15.4435 5.23644C15.4116 5.13955 15.3596 5.05054 15.2907 4.97528C15.2129 4.89022 15.1061 4.8301 14.8922 4.70983Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M15.1199 5.27417L8.27988 9.72024L2.87988 6.68271" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M15.4795 8.28003L8.27953 12.96L2.51953 9.72003" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.28027 15.48V9.71997" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,7 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-5.75 -6.5)" fill="currentColor">
<path d="M11.1062 16.269C10.1983 16.3989 9.50011 17.1812 9.5 18.125C9.50012 19.1604 10.3395 20 11.375 20H17.1304C17.4395 19.1265 18.2706 18.5 19.25 18.5C20.4926 18.5 21.5 19.5074 21.5 20.75C21.5 21.9926 20.4926 23 19.25 23C18.2706 23 17.4395 22.3735 17.1304 21.5H11.375C9.51111 21.5 8.00012 19.9889 8 18.125C8.00011 16.4242 9.25759 15.0177 10.8938 14.7837L11.1062 16.269Z"/>
<path d="M14.75 12.5C14.9307 12.5 15.0931 12.6111 15.158 12.7798L15.6802 14.1392C15.7564 14.3371 15.9129 14.4936 16.1108 14.5698L17.4702 15.092C17.6388 15.157 17.75 15.3193 17.75 15.5C17.75 15.6807 17.6388 15.843 17.4702 15.908L16.1108 16.4302C15.9129 16.5064 15.7564 16.6629 15.6802 16.8608L15.158 18.2202C15.0931 18.3888 14.9307 18.5 14.75 18.5C14.5693 18.5 14.4069 18.3888 14.342 18.2202L13.8198 16.8608C13.7436 16.6629 13.5871 16.5064 13.3892 16.4302L12.0298 15.908C11.8612 15.843 11.75 15.6807 11.75 15.5C11.75 15.3193 11.8612 15.157 12.0298 15.092L13.3892 14.5698C13.5871 14.4936 13.7436 14.3371 13.8198 14.1392L14.342 12.7798C14.4069 12.6111 14.5693 12.5 14.75 12.5Z"/>
<path d="M10.25 8C11.2294 8 12.0605 8.62649 12.3696 9.5H18.125C19.989 9.5 21.5 11.011 21.5 12.875C21.5 14.5759 20.2425 15.9815 18.6062 16.2156L18.3938 14.731C19.3017 14.6011 20 13.8189 20 12.875C20 11.8395 19.1605 11 18.125 11H12.3696C12.0605 11.8735 11.2294 12.5 10.25 12.5C9.00736 12.5 8 11.4926 8 10.25C8 9.00736 9.00736 8 10.25 8Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,5 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.3562 9.76904C4.44832 9.89892 3.75009 10.6811 3.75 11.625C3.75012 12.6604 4.58954 13.5 5.625 13.5H11.3804C11.6895 12.6265 12.5206 12 13.5 12C14.7426 12 15.75 13.0074 15.75 14.25C15.75 15.4926 14.7426 16.5 13.5 16.5C12.5206 16.5 11.6895 15.8735 11.3804 15H5.625C3.76111 15 2.25012 13.4889 2.25 11.625C2.25009 9.92417 3.50758 8.51773 5.1438 8.28369L5.3562 9.76904ZM13.5 13.5C13.0858 13.5 12.75 13.8358 12.75 14.25C12.75 14.6642 13.0858 15 13.5 15C13.9142 15 14.25 14.6642 14.25 14.25C14.25 13.8358 13.9142 13.5 13.5 13.5Z" fill="currentColor"/>
<path d="M9 6C9.18068 6 9.34308 6.11115 9.40796 6.27979L9.93018 7.63916C10.0064 7.83706 10.1629 7.99364 10.3608 8.06982L11.7202 8.59204C11.8888 8.65695 12 8.81929 12 9C12 9.18071 11.8888 9.34305 11.7202 9.40796L10.3608 9.93018C10.1629 10.0064 10.0064 10.1629 9.93018 10.3608L9.40796 11.7202C9.34308 11.8888 9.18068 12 9 12C8.81932 12 8.65692 11.8888 8.59204 11.7202L8.06982 10.3608C7.99363 10.1629 7.83707 10.0064 7.63916 9.93018L6.27979 9.40796C6.11119 9.34305 6 9.18071 6 9C6 8.81929 6.11119 8.65695 6.27979 8.59204L7.63916 8.06982C7.83707 7.99364 7.99363 7.83706 8.06982 7.63916L8.59204 6.27979C8.65692 6.11115 8.81932 6 9 6Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 1.5C5.47939 1.5 6.31054 2.12649 6.61963 3H12.375C14.239 3 15.75 4.51104 15.75 6.375C15.75 8.0759 14.4925 9.48151 12.8562 9.71558L12.6438 8.23096C13.5517 8.10109 14.25 7.31895 14.25 6.375C14.25 5.33946 13.4105 4.5 12.375 4.5H6.61963C6.31054 5.37351 5.47939 6 4.5 6C3.25736 6 2.25 4.99264 2.25 3.75C2.25 2.50736 3.25736 1.5 4.5 1.5ZM4.5 3C4.08579 3 3.75 3.33579 3.75 3.75C3.75 4.16421 4.08579 4.5 4.5 4.5C4.91421 4.5 5.25 4.16421 5.25 3.75C5.25 3.33579 4.91421 3 4.5 3Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -1,12 +1,15 @@
{
"prefix": "custom-vender",
"lastModified": 1786000677,
"lastModified": 1785319384,
"icons": {
"agent-v2-access-point": {
"body": "<g fill=\"none\"><path d=\"M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z\" fill=\"currentColor\"/><path d=\"M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z\" fill=\"currentColor\"/><path d=\"M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z\" fill=\"currentColor\"/><path d=\"M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z\" fill=\"currentColor\"/><path d=\"M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z\" fill=\"currentColor\"/><path d=\"M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z\" fill=\"currentColor\"/></g>",
"width": 15,
"height": 15
},
"agent-v2-building-blocks": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z\" fill=\"currentColor\"/></g>"
},
"agent-v2-configure": {
"body": "<g fill=\"none\"><path d=\"M7.5 2.2912C10.875 2.66428 13.5 5.52559 13.5 9V15.75H0V9C0 5.52559 2.62504 2.66428 6 2.2912V0H7.5V2.2912ZM12 14.25V9C12 6.10051 9.6495 3.75 6.75 3.75C3.85051 3.75 1.5 6.10051 1.5 9V14.25H12ZM6.75 12.75C4.67893 12.75 3 11.071 3 9C3 6.92895 4.67893 5.25 6.75 5.25C8.82105 5.25 10.5 6.92895 10.5 9C10.5 11.071 8.82105 12.75 6.75 12.75ZM6.75 11.25C7.99268 11.25 9 10.2427 9 9C9 7.75732 7.99268 6.75 6.75 6.75C5.50732 6.75 4.5 7.75732 4.5 9C4.5 10.2427 5.50732 11.25 6.75 11.25ZM6.75 9.75C6.33578 9.75 6 9.41422 6 9C6 8.58578 6.33578 8.25 6.75 8.25C7.16422 8.25 7.5 8.58578 7.5 9C7.5 9.41422 7.16422 9.75 6.75 9.75Z\" fill=\"currentColor\"/></g>",
"width": 14
@@ -16,8 +19,7 @@
"width": 14
},
"agent-v2-configure-build": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9.66661 1.33333C9.8434 1.33335 10.013 1.40364 10.138 1.52865L11.4713 2.86198C11.5963 2.987 11.6666 3.15654 11.6666 3.33333C11.6666 3.51012 11.5963 3.67967 11.4713 3.80469L9.10932 6.16667L14.3046 11.362C14.5649 11.6223 14.5649 12.0443 14.3046 12.3047L12.3046 14.3047C12.0443 14.565 11.6223 14.565 11.3619 14.3047L6.16661 9.10938L4.4713 10.8047C4.3463 10.9297 4.17673 11 3.99995 11C3.82316 11 3.65361 10.9297 3.52859 10.8047L0.695261 7.97136C0.434913 7.71101 0.434913 7.289 0.695261 7.02865L6.19526 1.52865L6.24409 1.48438C6.36272 1.38718 6.51191 1.33333 6.66661 1.33333H9.66661ZM7.10932 8.16667L11.8333 12.8906L12.8906 11.8333L8.16661 7.10938L7.10932 8.16667ZM2.10932 7.5L3.99995 9.39063L10.0572 3.33333L9.39057 2.66667H6.94266L2.10932 7.5Z\" fill=\"currentColor\"/></g>",
"width": 16
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9.66661 1.33333C9.8434 1.33335 10.013 1.40364 10.138 1.52865L11.4713 2.86198C11.5963 2.987 11.6666 3.15654 11.6666 3.33333C11.6666 3.51012 11.5963 3.67967 11.4713 3.80469L9.10932 6.16667L14.3046 11.362C14.5649 11.6223 14.5649 12.0443 14.3046 12.3047L12.3046 14.3047C12.0443 14.565 11.6223 14.565 11.3619 14.3047L6.16661 9.10938L4.4713 10.8047C4.3463 10.9297 4.17673 11 3.99995 11C3.82316 11 3.65361 10.9297 3.52859 10.8047L0.695261 7.97136C0.434913 7.71101 0.434913 7.289 0.695261 7.02865L6.19526 1.52865L6.24409 1.48438C6.36272 1.38718 6.51191 1.33333 6.66661 1.33333H9.66661ZM7.10932 8.16667L11.8333 12.8906L12.8906 11.8333L8.16661 7.10938L7.10932 8.16667ZM2.10932 7.5L3.99995 9.39063L10.0572 3.33333L9.39057 2.66667H6.94266L2.10932 7.5Z\" fill=\"currentColor\"/></g>"
},
"agent-v2-configure-preview": {
"body": "<g fill=\"none\"><path d=\"M12.4756 4.75207L12.3112 5.12919C12.1909 5.40528 11.8091 5.40528 11.6887 5.12919L11.5244 4.75207C11.2314 4.07965 10.7037 3.54427 10.0451 3.25139L9.53867 3.02615C9.26487 2.90435 9.26487 2.50587 9.53867 2.38408L10.0168 2.17143C10.6923 1.87101 11.2295 1.31582 11.5174 0.620554L11.6862 0.21302C11.8039-0.0710068 12.1961-0.0710068 12.3137 0.21302L12.4825 0.620554C12.7705 1.31582 13.3077 1.87101 13.9832 2.17143L14.4613 2.38408C14.7351 2.50587 14.7351 2.90435 14.4613 3.02615L13.9549 3.25139C13.2963 3.54427 12.7686 4.07965 12.4756 4.75207ZM5.33333 1.33333H8V2.66667H5.33333C3.12419 2.66667 1.33333 4.45753 1.33333 6.66667C1.33333 9.07333 2.97472 10.6437 6.66667 12.3199V10.6667H8C10.2091 10.6667 12 8.8758 12 6.66667H13.3333C13.3333 9.6122 10.9455 12 8 12V14.3333C4.66667 13 0 11 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333Z\" fill=\"currentColor\"/></g>",
@@ -25,8 +27,7 @@
"height": 15
},
"agent-v2-end-user-auth": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>",
"width": 16
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>"
},
"agent-v2-plan": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z\" fill=\"currentColor\"/></g>",
@@ -35,8 +36,8 @@
},
"agent-v2-prompt-insert": {
"body": "<g fill=\"none\"><path d=\"M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z\" fill=\"currentColor\"/></g>",
"height": 14,
"width": 14
"width": 14,
"height": 14
},
"agent-v2-robot-3": {
"body": "<g fill=\"none\"><path d=\"M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z\" fill=\"currentColor\"/><path d=\"M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z\" fill=\"currentColor\"/></g>",
@@ -812,6 +813,16 @@
"width": 24,
"height": 24
},
"main-nav-agent": {
"body": "<g fill=\"none\"><path d=\"M9.87891 2.81952C13.2539 3.1926 15.8789 6.05391 15.8789 9.52832V16.2783H2.37891V9.52832C2.37891 6.05391 5.00394 3.1926 8.37891 2.81952V0.52832H9.87891V2.81952ZM14.3789 14.7783V9.52832C14.3789 6.62883 12.0284 4.27832 9.12891 4.27832C6.22941 4.27832 3.87891 6.62883 3.87891 9.52832V14.7783H14.3789ZM9.12891 13.2783C7.05784 13.2783 5.37891 11.5994 5.37891 9.52832C5.37891 7.45727 7.05784 5.77832 9.12891 5.77832C11.2 5.77832 12.8789 7.45727 12.8789 9.52832C12.8789 11.5994 11.2 13.2783 9.12891 13.2783ZM9.12891 11.7783C10.3716 11.7783 11.3789 10.771 11.3789 9.52832C11.3789 8.28565 10.3716 7.27832 9.12891 7.27832C7.88623 7.27832 6.87891 8.28565 6.87891 9.52832C6.87891 10.771 7.88623 11.7783 9.12891 11.7783ZM9.12891 10.2783C8.71468 10.2783 8.37891 9.94254 8.37891 9.52832C8.37891 9.1141 8.71468 8.77832 9.12891 8.77832C9.54313 8.77832 9.87891 9.1141 9.87891 9.52832C9.87891 9.94254 9.54313 10.2783 9.12891 10.2783Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-agent-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-5.75 -7.25)\" d=\"M15.5 10.2912C18.875 10.6643 21.5 13.5256 21.5 17V23.75H8V17C8 13.5256 10.625 10.6643 14 10.2912V8H15.5V10.2912ZM14.75 20.75C16.821 20.75 18.5 19.071 18.5 17C18.5 14.9289 16.821 13.25 14.75 13.25C12.6789 13.25 11 14.9289 11 17C11 19.071 12.6789 20.75 14.75 20.75ZM14.75 19.25C13.5073 19.25 12.5 18.2427 12.5 17C12.5 15.7573 13.5073 14.75 14.75 14.75C15.9927 14.75 17 15.7573 17 17C17 18.2427 15.9927 19.25 14.75 19.25ZM14.75 17.75C15.1642 17.75 15.5 17.4142 15.5 17C15.5 16.5858 15.1642 16.25 14.75 16.25C14.3358 16.25 14 16.5858 14 17C14 17.4142 14.3358 17.75 14.75 17.75Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-app-home": {
"body": "<g fill=\"none\"><path d=\"M2.66667 6.17435C2.66667 5.98785 2.66667 5.89459 2.69021 5.80837C2.71107 5.73198 2.74537 5.65992 2.7915 5.59556C2.84357 5.52292 2.91595 5.46411 3.0607 5.3465L7.3274 1.87983C7.56713 1.68502 7.687 1.58762 7.82027 1.55031C7.93787 1.51741 8.06213 1.51741 8.17973 1.55031C8.313 1.58762 8.43287 1.68502 8.6726 1.87983L12.9393 5.3465C13.0841 5.46411 13.1564 5.52292 13.2085 5.59556C13.2547 5.65992 13.2889 5.73198 13.3098 5.80837C13.3333 5.89459 13.3333 5.98785 13.3333 6.17435V12.2667C13.3333 12.64 13.3333 12.8267 13.2607 12.9693C13.1967 13.0947 13.0948 13.1967 12.9693 13.2607C12.8267 13.3333 12.6401 13.3333 12.2667 13.3333H3.73333C3.35997 13.3333 3.17328 13.3333 3.03067 13.2607C2.90523 13.1967 2.80325 13.0947 2.73933 12.9693C2.66667 12.8267 2.66667 12.64 2.66667 12.2667V6.17435Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/><path d=\"M10 13.3333V9.66667C10 9.11438 9.55228 8.66667 9 8.66667H7C6.44772 8.66667 6 9.11438 6 9.66667V13.3333\" stroke=\"currentColor\" stroke-width=\"1.5\"/></g>",
"width": 16,
@@ -837,6 +848,16 @@
"width": 20,
"height": 20
},
"main-nav-home-v2": {
"body": "<g fill=\"none\"><path d=\"M9.75 14.2501H14.25V7.48367L9 3.40034L3.75 7.48367V14.2501H8.25V9.75006H9.75V14.2501ZM15.75 15.0001C15.75 15.4143 15.4142 15.7501 15 15.7501H3C2.58579 15.7501 2.25 15.4143 2.25 15.0001V7.11686C2.25 6.88542 2.35685 6.66694 2.53954 6.52485L8.53957 1.85818C8.8104 1.64753 9.1896 1.64753 9.46043 1.85818L15.4604 6.52485C15.6431 6.66694 15.75 6.88542 15.75 7.11686V15.0001Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-home-v2-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-5.75 -6.3)\" d=\"M21.5 21.2999C21.5 21.7141 21.1642 22.0499 20.75 22.0499H8.75C8.33579 22.0499 8 21.7141 8 21.2999V13.4167C8 13.1852 8.10685 12.9667 8.28954 12.8247L14.2896 8.15798C14.5604 7.94734 14.9396 7.94734 15.2104 8.15798L21.2104 12.8247C21.3931 12.9667 21.5 13.1852 21.5 13.4167V21.2999ZM14 16.0499V20.5499H15.5V16.0499H14Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-integrations": {
"body": "<g fill=\"none\"><path d=\"M2.5 7.50008C2.5 7.03984 2.8731 6.66675 3.33333 6.66675H16.6667C17.1269 6.66675 17.5 7.03985 17.5 7.50008V15.0001C17.5 15.4603 17.1269 15.8334 16.6667 15.8334H3.33333C2.8731 15.8334 2.5 15.4603 2.5 15.0001V7.50008Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M4.16699 6.66659V4.58325C4.16699 3.89289 4.72663 3.33325 5.41699 3.33325H7.08366C7.77402 3.33325 8.33366 3.89289 8.33366 4.58325V6.66659\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M11.667 6.66659V4.16659C11.667 3.70635 12.0401 3.33325 12.5003 3.33325H15.0003C15.4606 3.33325 15.8337 3.70635 15.8337 4.16659V6.66659\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
"width": 20,
@@ -847,6 +868,16 @@
"width": 20,
"height": 20
},
"main-nav-integrations-v2": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M7.5 2.25C7.91421 2.25 8.25 2.58579 8.25 3V5.25H9.75V3C9.75 2.58579 10.0858 2.25 10.5 2.25H14.25C14.6642 2.25 15 2.58579 15 3V5.25H15.75C16.1642 5.25 16.5 5.58579 16.5 6V14.25C16.5 14.6642 16.1642 15 15.75 15H2.25C1.83579 15 1.5 14.6642 1.5 14.25V6C1.5 5.58579 1.83579 5.25 2.25 5.25H3V3C3 2.58579 3.33579 2.25 3.75 2.25H7.5ZM3 13.5H15V6.75H3V13.5ZM11.25 5.25H13.5V3.75H11.25V5.25ZM4.5 5.25H6.75V3.75H4.5V5.25Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-integrations-v2-active": {
"body": "<g fill=\"none\"><g transform=\"translate(-6.39355 -5.66187)\" fill=\"currentColor\"><path d=\"M22.25 11.75C22.6642 11.75 23 12.0858 23 12.5V20C23 20.4142 22.6642 20.75 22.25 20.75H8.75C8.33579 20.75 8 20.4142 8 20V12.5C8 12.0858 8.33579 11.75 8.75 11.75H22.25Z\"/><path d=\"M14 8C14.4142 8 14.75 8.33579 14.75 8.75V10.25H9.5V8.75C9.5 8.33579 9.83579 8 10.25 8H14Z\"/><path d=\"M20.75 8C21.1642 8 21.5 8.33579 21.5 8.75V10.25H16.25V8.75C16.25 8.33579 16.5858 8 17 8H20.75Z\"/></g></g>",
"width": 18,
"height": 18
},
"main-nav-knowledge": {
"body": "<g fill=\"none\"><path d=\"M7.5 9.16675H10\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M7.5 5.83325H12.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M4.16699 4.16667C4.16699 3.24619 4.91318 2.5 5.83366 2.5H15.417C15.6471 2.5 15.8337 2.68655 15.8337 2.91667V17.5H5.83366C4.91318 17.5 4.16699 16.7538 4.16699 15.8333V4.16667Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M4.16699 15.8334C4.16699 14.9129 4.91318 14.1667 5.83366 14.1667H15.8337V17.5001H5.83366C4.91318 17.5001 4.16699 16.7539 4.16699 15.8334Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/></g>",
"width": 20,
@@ -857,6 +888,16 @@
"width": 20,
"height": 20
},
"main-nav-knowledge-v2": {
"body": "<g fill=\"none\"><path d=\"M15.75 13.5H4.5C4.08579 13.5 3.75 13.8358 3.75 14.25C3.75 14.6642 4.08579 15 4.5 15H15.75V16.5H4.5C3.25736 16.5 2.25 15.4927 2.25 14.25V3C2.25 2.17157 2.92157 1.5 3.75 1.5H15.75V13.5ZM3.75 12.0375C3.87117 12.0129 3.99658 12 4.125 12H14.25V3H3.75V12.0375ZM12 6.75H6V5.25H12V6.75Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-knowledge-v2-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-6 -6.98706)\" d=\"M21.5 20H10.25C9.83579 20 9.5 20.3358 9.5 20.75C9.5 21.1642 9.83579 21.5 10.25 21.5H21.5V23H10.25C9.00736 23 8 21.9927 8 20.75V9.5C8 8.67157 8.67157 8 9.5 8H21.5V20ZM17.75 13.25V11.75H11.75V13.25H17.75Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-marketplace": {
"body": "<g fill=\"none\"><path d=\"M16.667 9.99992V16.6666H3.33366V9.99992M7.91699 3.33325H12.0837M7.91699 3.33325L7.44543 7.10578C7.25334 8.6425 8.45158 9.99992 10.0003 9.99992C11.5491 9.99992 12.7473 8.6425 12.5552 7.10578L12.0837 3.33325M7.91699 3.33325H3.75033L2.64677 6.86465C2.16081 8.41975 3.32257 9.99992 4.95179 9.99992C6.1697 9.99992 7.19703 9.093 7.34809 7.88451L7.91699 3.33325ZM12.0837 3.33325H16.2503L17.3539 6.86465C17.8398 8.41975 16.6781 9.99992 15.0489 9.99992C13.831 9.99992 12.8037 9.093 12.6526 7.88451L12.0837 3.33325Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/></g>",
"width": 20,
@@ -867,6 +908,16 @@
"width": 20,
"height": 20
},
"main-nav-marketplace-v2": {
"body": "<g fill=\"none\"><path d=\"M15.75 9.93165V15H16.5V16.5H1.5V15H2.25V9.93165C1.34551 9.32625 0.75 8.29515 0.75 7.125C0.75 6.50464 0.918322 5.90716 1.22491 5.3981L3.25898 1.875C3.39295 1.64295 3.64054 1.5 3.90849 1.5H14.0915C14.3594 1.5 14.6071 1.64295 14.741 1.875L16.7681 5.38629C17.0817 5.90716 17.25 6.50464 17.25 7.125C17.25 8.29515 16.6545 9.32625 15.75 9.93165ZM14.25 10.4794C14.1269 10.493 14.0017 10.5 13.875 10.5C12.9307 10.5 12.0592 10.1085 11.4375 9.4599C10.8158 10.1085 9.94432 10.5 9 10.5C8.05568 10.5 7.18418 10.1085 6.5625 9.4599C5.94082 10.1085 5.06933 10.5 4.125 10.5C3.99825 10.5 3.87313 10.493 3.75 10.4794V15H14.25V10.4794ZM4.34149 3L2.51698 6.15991C2.34307 6.44882 2.25 6.77917 2.25 7.125C2.25 8.16052 3.08947 9 4.125 9C4.89822 9 5.5835 8.52772 5.86634 7.82212C6.11805 7.19417 7.00695 7.19417 7.25866 7.82212C7.54148 8.52772 8.22675 9 9 9C9.77325 9 10.4585 8.52772 10.7414 7.82212C10.9931 7.19417 11.8819 7.19417 12.1336 7.82212C12.4165 8.52772 13.1018 9 13.875 9C14.9105 9 15.75 8.16052 15.75 7.125C15.75 6.77917 15.6569 6.44882 15.476 6.1481L13.6585 3H4.34149Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-marketplace-v2-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-7.25 -6.875)\" d=\"M23.75 21.5V23H8.75V21.5H9.5V16.4317C8.59551 15.8263 8 14.7951 8 13.625C8 13.0046 8.16832 12.4072 8.47491 11.8981L10.509 8.375C10.6429 8.14295 10.8905 8 11.1585 8H21.3415C21.6094 8 21.8571 8.14295 21.991 8.375L24.0181 11.8863C24.3317 12.4072 24.5 13.0046 24.5 13.625C24.5 14.7951 23.9045 15.8263 23 16.4317V21.5H23.75ZM11.5915 9.5L9.76698 12.6599C9.59307 12.9488 9.5 13.2792 9.5 13.625C9.5 14.6605 10.3395 15.5 11.375 15.5C12.1482 15.5 12.8335 15.0277 13.1163 14.3221C13.3681 13.6942 14.257 13.6942 14.5087 14.3221C14.7915 15.0277 15.4767 15.5 16.25 15.5C17.0232 15.5 17.7085 15.0277 17.9914 14.3221C18.2431 13.6942 19.1319 13.6942 19.3836 14.3221C19.6665 15.0277 20.3518 15.5 21.125 15.5C22.1605 15.5 23 14.6605 23 13.625C23 13.2792 22.9069 12.9488 22.726 12.6481L20.9085 9.5H11.5915Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-quick-search": {
"body": "<g fill=\"none\"><path d=\"M12.0004 1.875C17.0398 1.87509 21.1246 5.9602 21.1249 10.9995C21.1249 13.1138 20.4037 15.0582 19.1972 16.6055L21.7958 19.2041C22.235 19.6433 22.2348 20.3556 21.7958 20.7949C21.3565 21.2343 20.6443 21.2343 20.205 20.7949L17.6064 18.1963C16.3417 19.1831 14.8123 19.8466 13.14 20.0552C12.5235 20.132 11.9616 19.6932 11.8847 19.0767C11.8081 18.4603 12.2454 17.8981 12.8617 17.8213C16.2516 17.3983 18.8749 14.5044 18.8749 10.9995C18.8746 7.20283 15.7971 4.12509 12.0004 4.125C8.4954 4.12505 5.60139 6.74948 5.17862 10.1396C5.10154 10.7559 4.53955 11.1934 3.92325 11.1167C3.30688 11.0398 2.86963 10.4777 2.9462 9.86133C3.50765 5.35896 7.34631 1.87505 12.0004 1.875Z\" fill=\"currentColor\"/><path d=\"M3.70727 16.1747L7.91781 11.2624C8.24038 10.8861 8.85505 11.158 8.79357 11.6498L8.49979 14.0001H10.9127C11.3399 14.0001 11.5703 14.5012 11.2923 14.8255L7.08177 19.7378C6.7592 20.1141 6.14453 19.8422 6.20601 19.3504L6.49979 17.0001H4.0869C3.65972 17.0001 3.42927 16.499 3.70727 16.1747Z\" fill=\"currentColor\"/></g>",
"width": 24,
@@ -882,6 +933,16 @@
"width": 18,
"height": 18
},
"main-nav-skill": {
"body": "<g fill=\"none\"><path d=\"M14.8922 4.70983L10.3274 2.14208C10.1065 2.01781 9.99601 1.95568 9.87944 1.93377C9.77627 1.91439 9.67021 1.91779 9.56848 1.94372C9.45356 1.97304 9.34729 2.04211 9.13475 2.18024L3.04371 6.13944C2.85218 6.26393 2.75642 6.32617 2.68704 6.4092C2.62562 6.4827 2.57949 6.56771 2.55133 6.65926C2.51953 6.76268 2.51953 6.8769 2.51953 7.10533V12.2864C2.51953 12.5317 2.51953 12.6544 2.55552 12.7639C2.58736 12.8607 2.63943 12.9498 2.70826 13.025C2.78607 13.1101 2.89296 13.1702 3.10675 13.2905L7.67163 15.8582C7.8926 15.9825 8.00305 16.0446 8.11962 16.0665C8.22279 16.0859 8.32885 16.0825 8.43059 16.0566C8.5455 16.0273 8.65177 15.9582 8.86424 15.8201L14.9553 11.8609C15.1468 11.7364 15.2426 11.6742 15.312 11.5911C15.3734 11.5176 15.4195 11.4326 15.4476 11.341C15.4795 11.2376 15.4795 11.1234 15.4795 10.895V5.71389C15.4795 5.4686 15.4795 5.34596 15.4435 5.23644C15.4116 5.13955 15.3596 5.05054 15.2907 4.97528C15.2129 4.89022 15.1061 4.8301 14.8922 4.70983Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/><path d=\"M15.1199 5.27417L8.27988 9.72024L2.87988 6.68271\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/><path d=\"M15.4795 8.28003L8.27953 12.96L2.51953 9.72003\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M8.28027 15.48V9.71997\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/></g>",
"width": 18,
"height": 18
},
"main-nav-skill-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-6.2 -6.70433)\" d=\"M8 18.6501C8 19.1406 8 19.3859 8.07199 19.6049C8.13567 19.7987 8.2398 19.9767 8.37747 20.1273C8.53307 20.2974 8.74686 20.4177 9.17444 20.6581L13.76 23.2375V21.0775L8 17.8819V18.6501ZM15.2 21.0775V23.2642L21.422 18.8858C21.78 18.6339 21.9591 18.5079 22.0885 18.3446C22.203 18.2001 22.2888 18.035 22.341 17.8581C22.4 17.6583 22.4 17.4394 22.4 17.0015V16.0375L15.2 21.0775ZM8 16.1582L13.76 19.2889V17.1175L8 13.9066V16.1582ZM15.2 17.1175V19.2775L22.4 14.2375V12.0775L15.2 17.1175ZM8.72 12.4375L14.48 15.6775L21.68 10.9975L17.1357 8.44137C16.6939 8.19286 16.473 8.0686 16.2398 8.02477C16.0335 7.98601 15.8213 7.99281 15.618 8.04468C15.388 8.10333 15.1755 8.24145 14.7505 8.51771L8.72 12.4375Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-studio": {
"body": "<g fill=\"none\"><path d=\"M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z\" fill=\"currentColor\"/><path d=\"M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
"width": 20,
@@ -892,6 +953,16 @@
"width": 20,
"height": 20
},
"main-nav-studio-v2": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M5.3562 9.76904C4.44832 9.89892 3.75009 10.6811 3.75 11.625C3.75012 12.6604 4.58954 13.5 5.625 13.5H11.3804C11.6895 12.6265 12.5206 12 13.5 12C14.7426 12 15.75 13.0074 15.75 14.25C15.75 15.4926 14.7426 16.5 13.5 16.5C12.5206 16.5 11.6895 15.8735 11.3804 15H5.625C3.76111 15 2.25012 13.4889 2.25 11.625C2.25009 9.92417 3.50758 8.51773 5.1438 8.28369L5.3562 9.76904ZM13.5 13.5C13.0858 13.5 12.75 13.8358 12.75 14.25C12.75 14.6642 13.0858 15 13.5 15C13.9142 15 14.25 14.6642 14.25 14.25C14.25 13.8358 13.9142 13.5 13.5 13.5Z\" fill=\"currentColor\"/><path d=\"M9 6C9.18068 6 9.34308 6.11115 9.40796 6.27979L9.93018 7.63916C10.0064 7.83706 10.1629 7.99364 10.3608 8.06982L11.7202 8.59204C11.8888 8.65695 12 8.81929 12 9C12 9.18071 11.8888 9.34305 11.7202 9.40796L10.3608 9.93018C10.1629 10.0064 10.0064 10.1629 9.93018 10.3608L9.40796 11.7202C9.34308 11.8888 9.18068 12 9 12C8.81932 12 8.65692 11.8888 8.59204 11.7202L8.06982 10.3608C7.99363 10.1629 7.83707 10.0064 7.63916 9.93018L6.27979 9.40796C6.11119 9.34305 6 9.18071 6 9C6 8.81929 6.11119 8.65695 6.27979 8.59204L7.63916 8.06982C7.83707 7.99364 7.99363 7.83706 8.06982 7.63916L8.59204 6.27979C8.65692 6.11115 8.81932 6 9 6Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M4.5 1.5C5.47939 1.5 6.31054 2.12649 6.61963 3H12.375C14.239 3 15.75 4.51104 15.75 6.375C15.75 8.0759 14.4925 9.48151 12.8562 9.71558L12.6438 8.23096C13.5517 8.10109 14.25 7.31895 14.25 6.375C14.25 5.33946 13.4105 4.5 12.375 4.5H6.61963C6.31054 5.37351 5.47939 6 4.5 6C3.25736 6 2.25 4.99264 2.25 3.75C2.25 2.50736 3.25736 1.5 4.5 1.5ZM4.5 3C4.08579 3 3.75 3.33579 3.75 3.75C3.75 4.16421 4.08579 4.5 4.5 4.5C4.91421 4.5 5.25 4.16421 5.25 3.75C5.25 3.33579 4.91421 3 4.5 3Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-studio-v2-active": {
"body": "<g fill=\"none\"><g transform=\"translate(-5.75 -6.5)\" fill=\"currentColor\"><path d=\"M11.1062 16.269C10.1983 16.3989 9.50011 17.1812 9.5 18.125C9.50012 19.1604 10.3395 20 11.375 20H17.1304C17.4395 19.1265 18.2706 18.5 19.25 18.5C20.4926 18.5 21.5 19.5074 21.5 20.75C21.5 21.9926 20.4926 23 19.25 23C18.2706 23 17.4395 22.3735 17.1304 21.5H11.375C9.51111 21.5 8.00012 19.9889 8 18.125C8.00011 16.4242 9.25759 15.0177 10.8938 14.7837L11.1062 16.269Z\"/><path d=\"M14.75 12.5C14.9307 12.5 15.0931 12.6111 15.158 12.7798L15.6802 14.1392C15.7564 14.3371 15.9129 14.4936 16.1108 14.5698L17.4702 15.092C17.6388 15.157 17.75 15.3193 17.75 15.5C17.75 15.6807 17.6388 15.843 17.4702 15.908L16.1108 16.4302C15.9129 16.5064 15.7564 16.6629 15.6802 16.8608L15.158 18.2202C15.0931 18.3888 14.9307 18.5 14.75 18.5C14.5693 18.5 14.4069 18.3888 14.342 18.2202L13.8198 16.8608C13.7436 16.6629 13.5871 16.5064 13.3892 16.4302L12.0298 15.908C11.8612 15.843 11.75 15.6807 11.75 15.5C11.75 15.3193 11.8612 15.157 12.0298 15.092L13.3892 14.5698C13.5871 14.4936 13.7436 14.3371 13.8198 14.1392L14.342 12.7798C14.4069 12.6111 14.5693 12.5 14.75 12.5Z\"/><path d=\"M10.25 8C11.2294 8 12.0605 8.62649 12.3696 9.5H18.125C19.989 9.5 21.5 11.011 21.5 12.875C21.5 14.5759 20.2425 15.9815 18.6062 16.2156L18.3938 14.731C19.3017 14.6011 20 13.8189 20 12.875C20 11.8395 19.1605 11 18.125 11H12.3696C12.0605 11.8735 11.2294 12.5 10.25 12.5C9.00736 12.5 8 11.4926 8 10.25C8 9.00736 9.00736 8 10.25 8Z\"/></g></g>",
"width": 18,
"height": 18
},
"main-nav-workspace-settings": {
"body": "<g fill=\"none\"><g transform=\"translate(2 2.333)\"><path d=\"M1.33333 2.33333C1.33333 1.78105 1.78105 1.33333 2.33333 1.33333C2.88562 1.33333 3.33333 1.78105 3.33333 2.33333C3.33333 2.88562 2.88562 3.33333 2.33333 3.33333C1.78105 3.33333 1.33333 2.88562 1.33333 2.33333ZM2.33333 0C1.04467 0 0 1.04467 0 2.33333C0 3.622 1.04467 4.66667 2.33333 4.66667C3.622 4.66667 4.66667 3.622 4.66667 2.33333C4.66667 1.04467 3.622 0 2.33333 0ZM6 3H11.3333V1.66667H6V3ZM8.66667 9C8.66667 8.44773 9.1144 8 9.66667 8C10.2189 8 10.6667 8.44773 10.6667 9C10.6667 9.55227 10.2189 10 9.66667 10C9.1144 10 8.66667 9.55227 8.66667 9ZM9.66667 6.66667C8.378 6.66667 7.33333 7.71133 7.33333 9C7.33333 10.2887 8.378 11.3333 9.66667 11.3333C10.9553 11.3333 12 10.2887 12 9C12 7.71133 10.9553 6.66667 9.66667 6.66667ZM0.666667 8.33333V9.66667H6V8.33333H0.666667Z\" fill=\"currentColor\"/></g></g>",
"width": 16,
@@ -1,7 +1,7 @@
{
"prefix": "custom-vender",
"name": "Dify Custom Vender",
"total": 335,
"total": 346,
"version": "0.0.0-private",
"author": {
"name": "LangGenius, Inc.",
@@ -14,11 +14,11 @@
},
"samples": [
"agent-v2-access-point",
"agent-v2-building-blocks",
"agent-v2-configure",
"agent-v2-configure-active",
"agent-v2-configure-build",
"agent-v2-configure-preview",
"agent-v2-end-user-auth"
"agent-v2-configure-preview"
],
"palette": false
}
@@ -0,0 +1,5 @@
import SkillDetailPage from '@/features/skills/detail-page'
export default function Page() {
return <SkillDetailPage />
}
+5
View File
@@ -0,0 +1,5 @@
import SkillsPage from '@/features/skills/page'
export default function Page() {
return <SkillsPage />
}
@@ -91,9 +91,12 @@ describe('AgentRosterResponseContent', () => {
await user.click(processToggle)
expect(processToggle).toHaveAttribute('aria-expanded', 'true')
await waitFor(() => {
expect(screen.getByText('history answer')).toBeInTheDocument()
})
await waitFor(
() => {
expect(screen.getByText('history answer')).toBeInTheDocument()
},
{ timeout: 5000 },
)
expect(screen.queryByText('internal thought should not render')).not.toBeInTheDocument()
})
@@ -122,9 +125,12 @@ describe('AgentRosterResponseContent', () => {
render(<AgentRosterResponseContent item={item} />)
await user.click(screen.getByRole('button', { name: 'Thinking' }))
await waitFor(() => {
expect(screen.getByText('const answer = 42').tagName).toBe('CODE')
})
await waitFor(
() => {
expect(screen.getByText('const answer = 42').tagName).toBe('CODE')
},
{ timeout: 5000 },
)
})
it('should keep one collapsible thinking timeline while response parts interleave', async () => {
@@ -1,6 +1,17 @@
import { render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { Img } from '..'
vi.mock('@/app/components/base/image-gallery', () => ({
default: ({ srcs }: { srcs: string[] }) => (
<div data-testid="image-gallery">
{srcs.map((src) => (
<span key={src} data-testid="gallery-image" data-src={src} />
))}
</div>
),
}))
describe('Img', () => {
describe('Rendering', () => {
it('should render with the correct wrapper class', () => {
@@ -16,9 +27,9 @@ describe('Img', () => {
const gallery = screen.getByTestId('image-gallery')
expect(gallery).toBeInTheDocument()
const images = gallery.querySelectorAll('img')
const images = gallery.querySelectorAll('[data-testid="gallery-image"]')
expect(images).toHaveLength(1)
expect(images[0]).toHaveAttribute('src', 'https://example.com/image.png')
expect(images[0]).toHaveAttribute('data-src', 'https://example.com/image.png')
})
it('should pass src as single element array to ImageGallery', () => {
@@ -26,21 +37,21 @@ describe('Img', () => {
render(<Img src={testSrc} />)
const gallery = screen.getByTestId('image-gallery')
const images = gallery.querySelectorAll('img')
const images = gallery.querySelectorAll('[data-testid="gallery-image"]')
expect(images[0]).toHaveAttribute('src', testSrc)
expect(images[0]).toHaveAttribute('data-src', testSrc)
})
it('should render with different src values', () => {
const { rerender } = render(<Img src="https://example.com/first.png" />)
expect(screen.getByTestId('gallery-image')).toHaveAttribute(
'src',
'data-src',
'https://example.com/first.png',
)
rerender(<Img src="https://example.com/second.jpg" />)
expect(screen.getByTestId('gallery-image')).toHaveAttribute(
'src',
'data-src',
'https://example.com/second.jpg',
)
})
@@ -679,22 +679,32 @@ describe('MainNav', () => {
expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(
'team',
)
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('link', { name: /Agents/ })).toHaveAttribute('href', '/agents')
const homeLink = screen.getByRole('link', { name: /common.mainNav.home/ })
expect(homeLink).toHaveAttribute('href', '/')
expect(homeLink.querySelector('.i-custom-vender-main-nav-home')).toBeInTheDocument()
const studioLink = screen.getByRole('link', { name: /common.menus.apps/ })
expect(studioLink).toHaveAttribute('href', '/apps')
expect(studioLink.querySelector('.i-custom-vender-main-nav-studio')).toBeInTheDocument()
const agentsLink = screen.getByRole('link', { name: /Agents/ })
expect(agentsLink).toHaveAttribute('href', '/agents')
expect(screen.getByRole('link', { name: /Agents common.menus.status/ })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute(
'href',
'/datasets',
)
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute(
'href',
'/integrations/model-provider',
)
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute(
'href',
'/marketplace',
)
expect(agentsLink.querySelector('.i-custom-vender-main-nav-roster')).toBeInTheDocument()
const skillsLink = screen.getByRole('link', { name: /common.mainNav.skills/ })
expect(skillsLink).toHaveAttribute('href', '/skills')
expect(skillsLink.querySelector('.i-custom-vender-main-nav-skill')).toBeInTheDocument()
const knowledgeLink = screen.getByRole('link', { name: /common.menus.datasets/ })
expect(knowledgeLink).toHaveAttribute('href', '/datasets')
expect(knowledgeLink.querySelector('.i-custom-vender-main-nav-knowledge')).toBeInTheDocument()
const integrationsLink = screen.getByRole('link', { name: /common.mainNav.integrations/ })
expect(integrationsLink).toHaveAttribute('href', '/integrations/model-provider')
expect(
integrationsLink.querySelector('.i-custom-vender-main-nav-integrations'),
).toBeInTheDocument()
const marketplaceLink = screen.getByRole('link', { name: /common.mainNav.marketplace/ })
expect(marketplaceLink).toHaveAttribute('href', '/marketplace')
expect(
marketplaceLink.querySelector('.i-custom-vender-main-nav-marketplace'),
).toBeInTheDocument()
})
it('hides the roster entry when Agent v2 is disabled', () => {
@@ -703,6 +713,10 @@ describe('MainNav', () => {
renderMainNav()
expect(screen.queryByRole('link', { name: /Agents/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.mainNav.skills/ })).toHaveAttribute(
'href',
'/skills',
)
})
it('hides the roster entry when the user lacks agent.manage', () => {
@@ -803,6 +817,7 @@ describe('MainNav', () => {
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
expect(screen.queryByRole('link', { name: /Agents/ })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.mainNav.skills/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute(
'href',
'/datasets',
@@ -180,6 +180,31 @@ describe('MainNavLayout', () => {
expect(screen.getByRole('main')).toHaveTextContent('new knowledge detail')
})
it('hides the global main nav on a skill detail route', () => {
;(usePathname as Mock).mockReturnValue('/skills/skill-1')
render(
<MainNavLayout>
<div>skill detail</div>
</MainNavLayout>,
)
expect(screen.queryByTestId('main-nav')).not.toBeInTheDocument()
expect(screen.getByRole('main')).toHaveTextContent('skill detail')
})
it('keeps the global main nav on the skills collection route', () => {
;(usePathname as Mock).mockReturnValue('/skills')
render(
<MainNavLayout>
<div>skills collection</div>
</MainNavLayout>,
)
expect(screen.getByTestId('main-nav')).toBeInTheDocument()
})
it.each(['/datasets/create', '/datasets/new/create', '/datasets/dataset-1/documents/create'])(
'keeps the global main nav on collection and creation route %s',
(pathname) => {
+22 -4
View File
@@ -29,6 +29,8 @@ export type DetailSidebarVisibilityOptions = Pick<
const VISIBLE_TO_ALL: MainNavRouteVisibility = () => true
const CAN_MANAGE_AGENTS: MainNavRouteVisibility = (options) => options.canManageAgents
const NOT_DATASET_OPERATOR: MainNavRouteVisibility = (options) =>
!options.isCurrentWorkspaceDatasetOperator
function isPathUnderRoute(pathname: string, route: string) {
return pathname === route || pathname.startsWith(`${route}/`)
@@ -66,6 +68,15 @@ export const MAIN_NAV_ROUTES = [
visibility: CAN_MANAGE_AGENTS,
feature: 'agentV2',
},
{
key: 'skills',
href: '/skills',
labelKey: 'mainNav.skills',
active: (path: string) => isPathUnderRoute(path, '/skills'),
icon: 'i-custom-vender-main-nav-skill',
activeIcon: 'i-custom-vender-main-nav-skill-active',
visibility: NOT_DATASET_OPERATOR,
},
{
key: 'datasets',
href: '/datasets',
@@ -128,14 +139,21 @@ function isDatasetDetailPathname(pathname: string) {
return true
}
function isSkillDetailPathname(pathname: string) {
const [section, skillId] = pathname.split('/').filter(Boolean)
return section === 'skills' && !!skillId
}
export function shouldHideMainNavigation(pathname: string) {
const [section, namespace, knowledgeSpaceId] = pathname.split('/').filter(Boolean)
return (
section === 'datasets' &&
namespace === 'new' &&
!!knowledgeSpaceId &&
knowledgeSpaceId !== 'create'
(section === 'datasets' &&
namespace === 'new' &&
!!knowledgeSpaceId &&
knowledgeSpaceId !== 'create') ||
isSkillDetailPathname(pathname)
)
}
@@ -64,7 +64,12 @@ vi.mock('reactflow', () => ({
data-testid={`handle-${id ?? 'unknown'}`}
data-handleid={id}
className={className}
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') onClick?.()
}}
>
{children}
</div>
@@ -119,8 +119,10 @@ describe('ConversationVariableModal', () => {
})
expect(screen.getAllByText('session_state')).toHaveLength(2)
expect(screen.getByText((content) => content.includes('formatted-100'))).toBeInTheDocument()
expect(screen.getByTestId('conversation-code-editor')).toHaveTextContent('{"latest":1}')
expect(
await screen.findByText((content) => content.includes('formatted-100')),
).toBeInTheDocument()
expect(await screen.findByTestId('conversation-code-editor')).toHaveTextContent('{"latest":1}')
await user.click(screen.getByText('summary'))
expect(screen.getByText('latest text')).toBeInTheDocument()
+4
View File
@@ -17,6 +17,10 @@ vi.mock('@/next/navigation', () => ({
useRouter: () => ({ push: mockPush, replace: mockReplace }),
}))
vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
}))
vi.mock('@/service/common', () => ({
fetchSetupStatus: vi.fn(),
fetchInitValidateStatus: vi.fn(),
@@ -31,6 +31,16 @@ const mockConfigFiles = vi.hoisted(() => ({
}>
}>,
}))
const mockWorkspaceSkillBindings = vi.hoisted(() => ({
current: [
{
id: 'library-skill-id',
name: 'library-skill',
display_name: 'Library Skill',
description: 'A Skill imported from the workspace library.',
},
],
}))
const mockLexical = vi.hoisted(() => ({
selection: null as null | {
__range: true
@@ -211,6 +221,11 @@ vi.mock('../orchestrate/config-context', () => ({
],
}),
useAgentConfigFiles: () => ({ files: mockConfigFiles.current }),
useAgentWorkspaceSkillBindings: () => ({
data: {
data: mockWorkspaceSkillBindings.current,
},
}),
}))
const duckDuckGoSearchAction = {
@@ -579,6 +594,24 @@ describe('AgentPromptEditor', () => {
})
})
it('should list and insert workspace Library Skills', async () => {
const { store, setPromptValue } = renderAgentPromptEditor('Use')
setPromptValue('Use /')
await openSlashMenuFromEditor()
fireEvent.click(
screen.getByRole('button', { name: /agentDetail\.configure\.skills\.label/i }),
)
expect(
screen
.getAllByRole('button', { name: /Library Skill|Playwright/ })
.map((button) => button.textContent),
).toEqual(['Library Skill', 'Playwright'])
fireEvent.click(screen.getByRole('button', { name: 'Library Skill' }))
expect(store.get(agentComposerPromptAtom)).toBe('Use [§skill:library-skill:Library Skill§] ')
})
it('should support keyboard navigation and selection in the slash menu', async () => {
const user = userEvent.setup()
const { store } = renderAgentPromptEditor('Review these tenders /')
@@ -641,6 +674,13 @@ describe('AgentPromptEditor', () => {
).toHaveAttribute('data-agent-prompt-menu-active')
})
await user.keyboard('{ArrowDown}')
await waitFor(() => {
expect(textbox).toHaveFocus()
expect(screen.getByRole('button', { name: /Library Skill/i })).toHaveAttribute(
'data-agent-prompt-menu-active',
)
})
await user.keyboard('{ArrowDown}')
await waitFor(() => {
expect(textbox).toHaveFocus()
@@ -833,7 +873,11 @@ describe('AgentPromptEditor', () => {
onInsertToken={onInsertToken}
/>,
)
fireEvent.click(screen.getByRole('button', { name: /agentDetail\.configure\.skills\.add/i }))
fireEvent.click(
screen.getByRole('button', {
name: /agentDetail\.configure\.skills\.addMenu\.workspace\.label/i,
}),
)
expect(onInsertToken).toHaveBeenCalledWith('[§skill:skill-1:Skill One§]')
rerender(
@@ -0,0 +1,81 @@
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
import { AgentOrchestrateAddActionsProvider } from '../add-actions'
import {
useAgentOrchestrateAddActions,
useRegisterAgentOrchestrateAddAction,
} from '../add-actions-context'
import { AgentOrchestrateViewingVersionContext } from '../read-only-context'
function RegisteredActionProbe({ onRegister }: { onRegister: () => void }) {
useRegisterAgentOrchestrateAddAction('skills', onRegister)
return <ActionsProbe />
}
function ActionsProbe() {
const actions = useAgentOrchestrateAddActions()
return <div>{actions.skills ? 'registered' : 'empty'}</div>
}
function ToggleRegisteredActionProbe({ onRegister }: { onRegister: () => void }) {
const [visible, setVisible] = useState(true)
return (
<>
<button type="button" onClick={() => setVisible(false)}>
remove action
</button>
{visible && <RegisteredActionProbe onRegister={onRegister} />}
{!visible && <ActionsProbe />}
</>
)
}
describe('AgentOrchestrateAddActionsProvider', () => {
it('registers add actions for editable drafts', () => {
const action = vi.fn()
render(
<AgentOrchestrateAddActionsProvider>
<RegisteredActionProbe onRegister={action} />
</AgentOrchestrateAddActionsProvider>,
)
expect(screen.getByText('registered')).toBeInTheDocument()
})
it('does not expose add actions while viewing a version', () => {
const action = vi.fn()
render(
<AgentOrchestrateViewingVersionContext value>
<AgentOrchestrateAddActionsProvider>
<RegisteredActionProbe onRegister={action} />
</AgentOrchestrateAddActionsProvider>
</AgentOrchestrateViewingVersionContext>,
)
expect(screen.getByText('empty')).toBeInTheDocument()
})
it('unregisters add actions when the owning section unmounts', async () => {
const user = userEvent.setup()
const action = vi.fn()
render(
<AgentOrchestrateAddActionsProvider>
<ToggleRegisteredActionProbe onRegister={action} />
</AgentOrchestrateAddActionsProvider>,
)
expect(screen.getByText('registered')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'remove action' }))
await waitFor(() => {
expect(screen.getByText('empty')).toBeInTheDocument()
})
})
})
@@ -16,6 +16,7 @@ export type AgentOrchestrateAddedItem =
export type AgentOrchestrateAddActionOptions = {
onAdded?: (item: AgentOrchestrateAddedItem) => void
skillSource?: 'library' | 'upload'
}
export type AgentOrchestrateAddAction = (options?: AgentOrchestrateAddActionOptions) => void
@@ -8,15 +8,15 @@ import type {
} from './add-actions-context'
import { useCallback, useMemo, useState } from 'react'
import { AgentOrchestrateAddActionsContext } from './add-actions-context'
import { useAgentOrchestrateReadOnly } from './read-only-context'
import { useAgentOrchestrateViewingVersion } from './read-only-context'
export function AgentOrchestrateAddActionsProvider({ children }: { children: ReactNode }) {
const readOnly = useAgentOrchestrateReadOnly()
const isViewingVersion = useAgentOrchestrateViewingVersion()
const [actions, setActions] = useState<AgentOrchestrateAddActions>({})
const registerAction = useCallback(
(key: AgentOrchestrateAddActionKey, action: AgentOrchestrateAddAction) => {
if (readOnly) return () => undefined
if (isViewingVersion) return () => undefined
setActions((currentActions) => {
if (currentActions[key] === action) return currentActions
@@ -37,15 +37,15 @@ export function AgentOrchestrateAddActionsProvider({ children }: { children: Rea
})
}
},
[readOnly],
[isViewingVersion],
)
const value = useMemo(
() => ({
actions: readOnly ? {} : actions,
actions: isViewingVersion ? {} : actions,
registerAction,
}),
[actions, readOnly, registerAction],
[actions, isViewingVersion, registerAction],
)
return (
@@ -4,7 +4,7 @@ import type { ButtonProps } from '@langgenius/dify-ui/button'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
import { useAgentOrchestrateReadOnly } from '../read-only-context'
import { useAgentOrchestrateViewingVersion } from '../read-only-context'
type ConfigureSectionAddButtonProps = Omit<
ButtonProps,
@@ -19,9 +19,9 @@ export function ConfigureSectionAddButton({
...props
}: ConfigureSectionAddButtonProps) {
const { t } = useTranslation('common')
const readOnly = useAgentOrchestrateReadOnly()
const isViewingVersion = useAgentOrchestrateViewingVersion()
if (readOnly) return null
if (isViewingVersion) return null
return (
<Button
@@ -1,9 +1,11 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { createContext, use } from 'react'
import { agentComposerFilesAtom } from '@/features/agent-v2/agent-composer/store-modules/files'
import { agentComposerSkillsAtom } from '@/features/agent-v2/agent-composer/store-modules/skills'
import { consoleQuery } from '@/service/client'
export type AgentConfigApiContext = {
agentId: string
@@ -36,6 +38,20 @@ export const useAgentConfigSkills = () => {
}
}
export const useAgentWorkspaceSkillBindings = () => {
const { agentId } = useAgentConfigApiContext()
return useQuery(
consoleQuery.workspaces.current.agents.byAgentId.skills.get.queryOptions({
input: {
params: {
agent_id: agentId,
},
},
}),
)
}
export const useAgentConfigFiles = () => {
const apiContext = useAgentConfigApiContext()
const files = useAtomValue(agentComposerFilesAtom)
@@ -13,7 +13,10 @@ import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store
import { QueryClientTestProvider } from '@/test/console/query-provider'
import { createSystemFeaturesFixture } from '@/test/console/system-features'
import { AgentConfigApiContextProvider } from '../../config-context'
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
import {
AgentOrchestrateReadOnlyContext,
AgentOrchestrateViewingVersionContext,
} from '../../read-only-context'
import { AgentFiles } from '../index'
type ConfigFileQueryOptionsInput = {
@@ -182,10 +185,12 @@ function renderAgentFiles({
initialDraft = createInitialDraft(),
apiContext = { agentId: 'agent-1', draftType: 'draft' } satisfies AgentConfigApiContext,
readOnly = false,
viewingVersion = false,
}: {
initialDraft?: AgentSoulConfigFormState
apiContext?: AgentConfigApiContext
readOnly?: boolean
viewingVersion?: boolean
} = {}) {
const queryClient = new QueryClient({
defaultOptions: {
@@ -202,10 +207,12 @@ function renderAgentFiles({
<QueryClientTestProvider queryClient={queryClient}>
<AgentConfigApiContextProvider value={apiContext}>
<AgentComposerProvider initialDraft={initialDraft}>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentFiles />
<ConfigSnapshotProbe />
</AgentOrchestrateReadOnlyContext>
<AgentOrchestrateViewingVersionContext value={viewingVersion}>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentFiles />
<ConfigSnapshotProbe />
</AgentOrchestrateReadOnlyContext>
</AgentOrchestrateViewingVersionContext>
</AgentComposerProvider>
</AgentConfigApiContextProvider>
</QueryClientTestProvider>,
@@ -713,8 +720,8 @@ describe('AgentFiles', () => {
expect(snapshot.config_note).toBe('')
})
it('should keep flat config files visible without drive-prefix filtering and disable add in read-only mode', () => {
renderAgentFiles({ readOnly: true })
it('should keep flat config files visible without drive-prefix filtering and disable add when viewing a version', () => {
renderAgentFiles({ readOnly: true, viewingVersion: true })
expect(screen.getByText('diagram.png')).toBeInTheDocument()
expect(screen.getByText('brief.md')).toBeInTheDocument()
@@ -722,4 +729,12 @@ describe('AgentFiles', () => {
screen.queryByRole('button', { name: /agentV2\.agentDetail\.configure\.files\.add/i }),
).not.toBeInTheDocument()
})
it('should keep add action available for build drafts', () => {
renderAgentFiles({ readOnly: true })
expect(
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.files\.add/i }),
).toBeInTheDocument()
})
})
@@ -30,7 +30,10 @@ import { AgentKnowledgeRetrieval } from './knowledge'
import { AgentModelField } from './model-config/field'
import { AgentPromptEditor } from './prompt-editor'
import { AgentConfigurePublishBar } from './publish-bar'
import { AgentOrchestrateReadOnlyContext } from './read-only-context'
import {
AgentOrchestrateReadOnlyContext,
AgentOrchestrateViewingVersionContext,
} from './read-only-context'
import { AgentSkills } from './skills'
import { AgentTools } from './tools'
@@ -139,45 +142,51 @@ export function AgentOrchestratePanel({
/>
)}
<AgentOrchestrateReadOnlyContext value={readOnly}>
<div aria-readonly={readOnly} className="flex min-h-0 flex-1 flex-col">
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={showHeader ? undefined : orchestrateLabel}
aria-labelledby={showHeader ? orchestrateHeadingId : undefined}
className="overscroll-contain"
role="region"
>
<ScrollAreaContent className={cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20')}>
<AgentConfigApiContextProvider value={configApiContext}>
<AgentOrchestrateAddActionsProvider>
<AgentBuildDraftChangedKeysProvider
changedKeys={
isBuildDraftActive ? buildDraftChangedKeys : EMPTY_BUILD_DRAFT_CHANGED_KEYS
}
>
<AgentModelField
currentModel={currentModel}
textGenerationModelList={textGenerationModelList}
onSelect={onSelectModel}
/>
<AgentPromptEditor />
<AgentSkills />
<AgentFiles />
<AgentTools />
{ENABLE_AGENT_KNOWLEDGE_RETRIEVAL && <AgentKnowledgeRetrieval />}
<AgentAdvancedSettings />
</AgentBuildDraftChangedKeysProvider>
</AgentOrchestrateAddActionsProvider>
</AgentConfigApiContextProvider>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className={hasBottomAction ? 'z-20' : undefined}>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
</AgentOrchestrateReadOnlyContext>
<AgentOrchestrateViewingVersionContext value={!!selectedVersionSnapshot}>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<div aria-readonly={readOnly} className="flex min-h-0 flex-1 flex-col">
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={showHeader ? undefined : orchestrateLabel}
aria-labelledby={showHeader ? orchestrateHeadingId : undefined}
className="overscroll-contain"
role="region"
>
<ScrollAreaContent
className={cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20')}
>
<AgentConfigApiContextProvider value={configApiContext}>
<AgentOrchestrateAddActionsProvider>
<AgentBuildDraftChangedKeysProvider
changedKeys={
isBuildDraftActive
? buildDraftChangedKeys
: EMPTY_BUILD_DRAFT_CHANGED_KEYS
}
>
<AgentModelField
currentModel={currentModel}
textGenerationModelList={textGenerationModelList}
onSelect={onSelectModel}
/>
<AgentPromptEditor />
<AgentSkills />
<AgentFiles />
<AgentTools />
{ENABLE_AGENT_KNOWLEDGE_RETRIEVAL && <AgentKnowledgeRetrieval />}
<AgentAdvancedSettings />
</AgentBuildDraftChangedKeysProvider>
</AgentOrchestrateAddActionsProvider>
</AgentConfigApiContextProvider>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className={hasBottomAction ? 'z-20' : undefined}>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
</AgentOrchestrateReadOnlyContext>
</AgentOrchestrateViewingVersionContext>
{orchestrateBottomAction ? (
<AgentOrchestrateBottomActions shrinkOnOpen={!bottomAction}>
@@ -10,7 +10,10 @@ import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provid
import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store'
import { RerankingModeEnum } from '@/models/datasets'
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
import {
AgentOrchestrateReadOnlyContext,
AgentOrchestrateViewingVersionContext,
} from '../../read-only-context'
import { AgentKnowledgeRetrieval } from '../index'
vi.mock('@/context/workspace-state', async () => {
@@ -107,17 +110,21 @@ function ConfigSnapshotPreview() {
function renderKnowledgeRetrieval({
initialDraft = agentKnowledgeDraft,
readOnly = false,
viewingVersion = false,
showConfigSnapshot = false,
}: {
initialDraft?: AgentSoulConfigFormState
readOnly?: boolean
viewingVersion?: boolean
showConfigSnapshot?: boolean
} = {}) {
return render(
<AgentComposerProvider initialDraft={initialDraft}>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentKnowledgeRetrieval />
</AgentOrchestrateReadOnlyContext>
<AgentOrchestrateViewingVersionContext value={viewingVersion}>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentKnowledgeRetrieval />
</AgentOrchestrateReadOnlyContext>
</AgentOrchestrateViewingVersionContext>
{showConfigSnapshot && <ConfigSnapshotPreview />}
</AgentComposerProvider>,
)
@@ -154,8 +161,8 @@ describe('AgentKnowledgeRetrieval', () => {
).not.toBeInTheDocument()
})
it('should hide add, edit, and remove actions when readonly', () => {
renderKnowledgeRetrieval({ readOnly: true })
it('should hide add, edit, and remove actions when viewing a version', () => {
renderKnowledgeRetrieval({ readOnly: true, viewingVersion: true })
expect(
screen.getByText('agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne'),
@@ -176,6 +183,16 @@ describe('AgentKnowledgeRetrieval', () => {
}),
).not.toBeInTheDocument()
})
it('should keep add action available for build drafts', () => {
renderKnowledgeRetrieval({ readOnly: true })
expect(
screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.knowledgeRetrieval.add',
}),
).toBeInTheDocument()
})
})
describe('User Interactions', () => {
@@ -0,0 +1,47 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { AgentPromptSlashMenu } from '../slash'
describe('AgentPromptSlashMenu', () => {
it('offers library and skill.zip as separate add-skill actions', async () => {
const user = userEvent.setup()
const onAddSkill = vi.fn()
render(
<AgentPromptSlashMenu
view="skills"
categories={[
{
key: 'skills',
label: 'Skills',
icon: 'i-custom-vender-agent-v2-building-blocks',
},
]}
skills={[]}
files={[]}
configuredTools={[]}
knowledgeRetrievals={[]}
onAddProviderTools={vi.fn()}
onAddSkill={onAddSkill}
onBack={vi.fn()}
onOpenCategory={vi.fn()}
onInsertToken={vi.fn()}
/>,
)
await user.click(
screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.skills.addMenu.workspace.label',
}),
)
expect(onAddSkill).toHaveBeenLastCalledWith(expect.objectContaining({ skillSource: 'library' }))
await user.click(
screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.skills.addMenu.upload.label',
}),
)
expect(onAddSkill).toHaveBeenLastCalledWith(expect.objectContaining({ skillSource: 'upload' }))
})
})
@@ -12,6 +12,7 @@ import type { RosterReferenceToken } from '@/app/components/base/prompt-editor/p
import type {
AgentFileNode,
AgentProviderTool,
AgentSkill,
AgentTool,
} from '@/features/agent-v2/agent-composer/form-state'
import { cn } from '@langgenius/dify-ui/cn'
@@ -49,7 +50,11 @@ import {
} from '@/features/agent-v2/agent-detail/configure/feature-flags'
import { useAgentOrchestrateAddActions } from '../add-actions-context'
import { AgentConfigureTipContent } from '../common/tip-content'
import { useAgentConfigFiles, useAgentConfigSkills } from '../config-context'
import {
useAgentConfigFiles,
useAgentConfigSkills,
useAgentWorkspaceSkillBindings,
} from '../config-context'
import { useAgentOrchestrateReadOnly } from '../read-only-context'
import { useAgentPromptToolIconResolver } from './hooks'
import { insertTokenAtTextRange, replaceTrailingSlashWithToken } from './options'
@@ -417,7 +422,21 @@ export function AgentPromptEditor() {
const { t } = useTranslation('agentV2')
const readOnly = useAgentOrchestrateReadOnly()
const [value, setValue] = useAtom(agentComposerPromptAtom)
const { skills } = useAgentConfigSkills()
const { skills: embeddedSkills } = useAgentConfigSkills()
const workspaceSkillBindingsQuery = useAgentWorkspaceSkillBindings()
const skills = useMemo<AgentSkill[]>(() => {
const workspaceSkills = workspaceSkillBindingsQuery.data?.data ?? []
const workspaceSkillNames = new Set(workspaceSkills.map((skill) => skill.name))
return [
...workspaceSkills.map((skill) => ({
id: skill.name,
name: skill.display_name || skill.name,
description: skill.description,
})),
...embeddedSkills.filter((skill) => !workspaceSkillNames.has(skill.name)),
]
}, [embeddedSkills, workspaceSkillBindingsQuery.data?.data])
const { files } = useAgentConfigFiles()
const tools = useAtomValue(agentComposerToolsAtom)
const addProviderTools = useSetAtom(addProviderToolsAtom)
@@ -102,9 +102,10 @@ export function AgentPromptSlashMenu({
}: AgentPromptSlashMenuProps) {
const { t } = useTranslation('agentV2')
const title = categories.find((category) => category.key === view)?.label
const handleAddFromFooter = () => {
const handleAddFromFooter = (skillSource?: 'library' | 'upload') => {
if (view === 'skills') {
onAddSkill?.({
skillSource,
onAdded: (item) => {
if (isPromptReferenceItem(item))
onInsertToken(createConfigReferenceToken('skill', item.id, item.name))
@@ -212,17 +213,29 @@ export function AgentPromptSlashMenu({
: undefined
}
/>
) : view === 'skills' ? (
<div className="flex flex-col border-t border-divider-subtle p-1">
<AgentPromptSkillAddButton
icon="i-custom-vender-agent-v2-building-blocks"
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
onClick={() => handleAddFromFooter('library')}
/>
<AgentPromptSkillAddButton
icon="i-ri-upload-cloud-2-line"
label={t(($) => $['agentDetail.configure.skills.addMenu.upload.label'])}
onClick={() => handleAddFromFooter('upload')}
/>
</div>
) : (
<div className="border-t border-divider-subtle p-1">
<button
type="button"
{...agentPromptSlashMenuItemProps}
className="flex h-6 w-full items-center gap-1 rounded-md pr-2 pl-3 text-left hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:outline-hidden data-agent-prompt-menu-active:bg-state-base-hover"
onClick={handleAddFromFooter}
onClick={() => handleAddFromFooter()}
>
<span aria-hidden className="i-ri-add-line size-4 shrink-0 text-text-secondary" />
<span className="system-sm-regular text-text-secondary">
{view === 'skills' && t(($) => $['agentDetail.configure.skills.add'])}
{view === 'files' && t(($) => $['agentDetail.configure.files.add'])}
{view === 'knowledge' && t(($) => $['agentDetail.configure.knowledgeRetrieval.add'])}
</span>
@@ -233,6 +246,28 @@ export function AgentPromptSlashMenu({
)
}
function AgentPromptSkillAddButton({
icon,
label,
onClick,
}: {
icon: string
label: string
onClick: () => void
}) {
return (
<button
type="button"
{...agentPromptSlashMenuItemProps}
className="flex h-6 w-full items-center gap-1 rounded-md pr-2 pl-3 text-left hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:outline-hidden data-agent-prompt-menu-active:bg-state-base-hover"
onClick={onClick}
>
<span aria-hidden className={`${icon} size-4 shrink-0 text-text-secondary`} />
<span className="system-sm-regular text-text-secondary">{label}</span>
</button>
)
}
function AgentPromptSlashPanel({
className,
children,
@@ -402,7 +402,7 @@ function PublishBarActions({
variant="primary"
disabled={!canPublish}
loading={isPublishing}
className="h-8 rounded-lg px-3"
className="h-8 gap-1 rounded-lg px-3"
onClick={onPublishRequest}
>
{actionIcon && <span aria-hidden className={`${actionIcon} size-4 shrink-0`} />}
@@ -1,7 +1,12 @@
import { createContext, use } from 'react'
export const AgentOrchestrateReadOnlyContext = createContext(false)
export const AgentOrchestrateViewingVersionContext = createContext(false)
export function useAgentOrchestrateReadOnly() {
return use(AgentOrchestrateReadOnlyContext)
}
export function useAgentOrchestrateViewingVersion() {
return use(AgentOrchestrateViewingVersionContext)
}
@@ -1,30 +1,471 @@
'use client'
import type {
AgentSkillBindingItemResponse,
SkillResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { UIEvent } from 'react'
import type { AgentOrchestrateAddActionOptions } from '../add-actions-context'
import type { AgentSkill } from '@/features/agent-v2/agent-composer/form-state'
import { useMutation } from '@tanstack/react-query'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import {
keepPreviousData,
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { useAtomValue, useSetAtom } from 'jotai'
import { useCallback, useRef, useState } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SearchInput } from '@/app/components/base/search-input'
import { SkeletonRectangle } from '@/app/components/base/skeleton'
import {
agentComposerSkillsAtom,
removeAgentSkillAtom,
upsertAgentSkillAtom,
} from '@/features/agent-v2/agent-composer/store-modules/skills'
import {
getSkillErrorCode,
getSkillErrorDetailString,
normalizeSkillError,
} from '@/features/skills/error'
import { TagFilter } from '@/features/tag-management/components/tag-filter'
import Link from '@/next/link'
import { consoleQuery } from '@/service/client'
import { useRegisterAgentOrchestrateAddAction } from '../add-actions-context'
import { ConfigureSectionAddButton } from '../common/add-button'
import { ConfigureSectionEmpty } from '../common/empty'
import { ConfigureSection } from '../common/section'
import { AgentConfigureTipContent } from '../common/tip-content'
import { useAgentConfigApiContext } from '../config-context'
import {
useAgentOrchestrateReadOnly,
useAgentOrchestrateViewingVersion,
} from '../read-only-context'
import { AgentSkillItem } from './item'
import { AgentSkillUploadDialog } from './upload-dialog'
const WORKSPACE_SKILLS_PAGE_SIZE = 20
const MAX_AGENT_LIBRARY_SKILLS = 20
function AgentSkillAddMenuItem({
badge,
description,
disabled,
iconClassName,
label,
onClick,
}: {
badge?: string
description: string
disabled?: boolean
iconClassName: string
label: string
onClick: () => void
}) {
return (
<button
type="button"
disabled={disabled}
onClick={onClick}
className="flex w-full min-w-0 items-start gap-3 rounded-lg px-2 py-2 text-left outline-hidden hover:not-disabled:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:opacity-50"
>
<span
aria-hidden
className={cn('mt-0.5 size-4 shrink-0 text-text-secondary', iconClassName)}
/>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="flex min-w-0 items-center gap-2">
<span className="truncate system-sm-medium text-text-secondary">{label}</span>
{badge && (
<span className="shrink-0 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
{badge}
</span>
)}
</span>
<span className="line-clamp-2 system-xs-regular text-text-tertiary">{description}</span>
</span>
</button>
)
}
function WorkspaceSkillIcon() {
return (
<span className="flex size-6 shrink-0 items-center justify-center rounded-md border-[0.5px] border-effects-icon-border bg-background-default-dodge p-1 backdrop-blur-xs">
<span
aria-hidden
className="i-custom-vender-agent-v2-building-blocks size-4 text-text-secondary"
/>
</span>
)
}
function WorkspaceSkillRow({
unavailable,
isAdded,
isPending,
onSelect,
onPreview,
selected,
skill,
}: {
unavailable: boolean
isAdded: boolean
isPending: boolean
onSelect: (skill: SkillResponse) => void
onPreview: (skill: SkillResponse) => void
selected: boolean
skill: SkillResponse
}) {
const { t } = useTranslation('agentV2')
const cannotAdd = unavailable || isAdded || isPending
return (
<button
type="button"
aria-disabled={cannotAdd}
onClick={() => {
onPreview(skill)
if (!cannotAdd) onSelect(skill)
}}
onFocus={() => onPreview(skill)}
onMouseEnter={() => onPreview(skill)}
className={cn(
'flex h-8 w-full min-w-0 items-center gap-1 rounded-lg pr-2.5 pl-3 text-left outline-hidden hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid',
selected && 'bg-state-base-hover',
isPending && 'cursor-wait opacity-60',
)}
>
<WorkspaceSkillIcon />
<span className="w-0 min-w-0 flex-1 truncate system-sm-medium text-text-secondary">
{skill.display_name}
</span>
{isAdded && (
<span className="shrink-0 system-xs-medium text-text-tertiary">
{t(($) => $['agentDetail.configure.skills.workspaceSelector.added'])}
</span>
)}
{!isAdded && unavailable && (
<span className="shrink-0 system-xs-medium text-text-tertiary">
{t(($) => $['agentDetail.configure.skills.workspaceSelector.draft'])}
</span>
)}
</button>
)
}
function WorkspaceSkillPreview({ skill }: { skill?: SkillResponse }) {
const { t } = useTranslation('agentV2')
if (!skill) {
return (
<div className="flex min-h-32 items-center justify-center px-6 text-center system-xs-regular text-text-tertiary">
{t(($) => $['agentDetail.configure.skills.workspaceSelector.empty'])}
</div>
)
}
return (
<div className="flex max-h-[428px] flex-col gap-2 overflow-y-auto px-3 pt-3 pb-4">
<div className="flex min-w-0 flex-col items-start gap-1">
<WorkspaceSkillIcon />
<div className="min-w-0 flex-1">
<div className="truncate system-md-medium text-text-primary">{skill.display_name}</div>
<div className="truncate system-xs-regular text-text-tertiary">{skill.name}</div>
</div>
</div>
{!!skill.tags?.length && (
<div className="flex flex-wrap gap-1">
{skill.tags.slice(0, 5).map((tag) => (
<span
key={tag}
className="rounded-[5px] border border-divider-subtle bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium-uppercase text-text-tertiary"
>
{tag}
</span>
))}
</div>
)}
<p className="system-xs-regular text-text-secondary">{skill.description}</p>
{(skill.updated_by_name || skill.created_by_name) && (
<div className="mt-auto system-xs-regular text-text-tertiary">
{skill.updated_by_name || skill.created_by_name}
</div>
)}
</div>
)
}
function WorkspaceSkillSelector({
boundSkillIds,
isBindingPending,
onSelect,
}: {
boundSkillIds: string[]
isBindingPending: boolean
onSelect: (skill: SkillResponse) => void
}) {
const { t } = useTranslation('agentV2')
const [keyword, setKeyword] = useState('')
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([])
const [previewSkillId, setPreviewSkillId] = useState<string | undefined>(undefined)
const selectorRef = useRef<HTMLDivElement>(null)
const debouncedKeyword = useDebounce(keyword.trim(), { wait: 300 })
const { data: tagList = [] } = useQuery(
consoleQuery.tags.get.queryOptions({
input: {
query: {
type: 'skill',
},
},
}),
)
const tagNameById = useMemo(() => new Map(tagList.map((tag) => [tag.id, tag.name])), [tagList])
const selectedTagNames = useMemo(
() => selectedTagIds.flatMap((tagId) => tagNameById.get(tagId) ?? []),
[selectedTagIds, tagNameById],
)
const skillsQuery = useInfiniteQuery({
...consoleQuery.workspaces.current.skills.get.infiniteOptions({
input: (pageParam) => ({
query: {
limit: WORKSPACE_SKILLS_PAGE_SIZE,
page: Number(pageParam),
...(debouncedKeyword ? { keyword: debouncedKeyword } : {}),
...(selectedTagNames.length ? { tag: selectedTagNames } : {}),
},
}),
getNextPageParam: (lastPage) => (lastPage.has_more ? (lastPage.page ?? 1) + 1 : undefined),
initialPageParam: 1,
placeholderData: keepPreviousData,
}),
})
const boundSkillIdSet = useMemo(() => new Set(boundSkillIds), [boundSkillIds])
const skills =
skillsQuery.data?.pages
.flatMap((page) => page.data ?? [])
.filter((skill) => Boolean(skill.latest_published_version_id)) ?? []
const previewSkill = skills.find((skill) => skill.id === previewSkillId) ?? skills[0]
const hasNextPage = skillsQuery.hasNextPage ?? false
const isFetchingNextPage = skillsQuery.isFetchingNextPage
const fetchNextPage = skillsQuery.fetchNextPage
const handleListScroll = useCallback(
(event: UIEvent<HTMLDivElement>) => {
const target = event.currentTarget
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight
if (scrollBottom < 80 && hasNextPage && !isFetchingNextPage) void fetchNextPage()
},
[fetchNextPage, hasNextPage, isFetchingNextPage],
)
return (
<div ref={selectorRef} className="relative h-[520px] w-[320px]">
<div className="flex h-full w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-[5px]">
<div className="border-b border-divider-subtle p-2">
<div className="relative">
<SearchInput
className={keyword ? '[&_input]:pr-14' : '[&_input]:pr-9'}
value={keyword}
onValueChange={setKeyword}
placeholder={t(($) => $['agentDetail.configure.skills.workspaceSelector.search'])}
/>
<div
className={cn(
'absolute top-1/2 size-6 -translate-y-1/2',
keyword ? 'right-7' : 'right-1.5',
)}
>
<TagFilter
iconOnly
type="skill"
value={selectedTagIds}
onChange={setSelectedTagIds}
portalProps={{ container: selectorRef }}
showTagManagement={false}
triggerClassName="bg-transparent hover:bg-state-base-hover focus-visible:bg-state-base-hover data-popup-open:bg-state-base-hover"
/>
</div>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-1" onScroll={handleListScroll}>
{skillsQuery.isPending && (
<div className="space-y-1">
<SkeletonRectangle className="h-8 rounded-lg" />
<SkeletonRectangle className="h-8 rounded-lg" />
<SkeletonRectangle className="h-8 rounded-lg" />
</div>
)}
{!skillsQuery.isPending && skills.length === 0 && (
<div className="flex h-full items-center justify-center px-4 text-center system-xs-regular text-text-tertiary">
{t(($) => $['agentDetail.configure.skills.workspaceSelector.empty'])}
</div>
)}
{!skillsQuery.isPending &&
skills.map((skill) => (
<WorkspaceSkillRow
key={skill.id}
unavailable={!skill.latest_published_version_id}
isAdded={boundSkillIdSet.has(skill.id)}
isPending={isBindingPending}
selected={previewSkill?.id === skill.id}
skill={skill}
onPreview={(skill) => setPreviewSkillId(skill.id)}
onSelect={onSelect}
/>
))}
{skillsQuery.isFetchingNextPage && (
<div className="space-y-1">
<SkeletonRectangle className="h-8 rounded-lg" />
<SkeletonRectangle className="h-8 rounded-lg" />
</div>
)}
</div>
<Link
href="/skills"
className="flex h-8 items-center gap-0.5 border-t border-divider-subtle px-4 system-xs-medium text-text-tertiary outline-hidden hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
<span>{t(($) => $['agentDetail.configure.skills.workspaceSelector.manage'])}</span>
<span aria-hidden className="i-ri-arrow-right-up-line size-3" />
</Link>
</div>
<div className="absolute top-[52px] left-[-244px] w-[240px] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]">
<WorkspaceSkillPreview skill={previewSkill} />
</div>
</div>
)
}
function WorkspaceAgentSkillItem({
canRemove,
skill,
onRemove,
}: {
canRemove: boolean
skill: AgentSkillBindingItemResponse
onRemove: (skillId: string) => void
}) {
const { t } = useTranslation('agentV2')
const readOnly = useAgentOrchestrateReadOnly()
const [isActionsOpen, setIsActionsOpen] = useState(false)
const [isRemoveHighlighted, setIsRemoveHighlighted] = useState(false)
const displayName = skill.display_name || skill.name
const handleOpenInLibrary = useCallback(() => {
window.open(`/skills/${skill.id}`, '_blank', 'noopener,noreferrer')
}, [skill.id])
return (
<div
data-workspace-skill-row
className={cn(
'group relative h-8 overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs shadow-shadow-shadow-3 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm',
isRemoveHighlighted &&
'border-state-destructive-border! bg-state-destructive-hover! shadow-xs!',
)}
>
<Link
href={`/skills/${skill.id}`}
target="_blank"
rel="noreferrer"
className="flex h-full w-full min-w-0 cursor-pointer items-center gap-1 rounded-lg px-2 py-1 text-left outline-hidden select-none focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
>
<span
aria-hidden
className="i-custom-vender-agent-v2-building-blocks size-4 shrink-0 text-text-secondary"
/>
<span className="flex w-0 min-w-0 flex-1 items-center gap-1">
<span className="min-w-0 truncate system-sm-medium text-text-secondary">
{displayName}
</span>
<span
aria-hidden
className="i-ri-arrow-right-up-line size-3.5 shrink-0 text-text-quaternary opacity-0 group-focus-within:opacity-100 group-hover:opacity-100"
/>
</span>
<span
className={cn(
'shrink-0 system-xs-regular text-text-tertiary',
!readOnly && 'group-focus-within:opacity-0 group-hover:opacity-0',
isActionsOpen && 'opacity-0',
)}
>
{skill.name}
</span>
</Link>
<DropdownMenu
modal={false}
onOpenChange={(open) => {
setIsActionsOpen(open)
if (!open) setIsRemoveHighlighted(false)
}}
>
<DropdownMenuTrigger
aria-label={t(($) => $['agentDetail.configure.skills.moreActions'], {
name: displayName,
})}
className={cn(
'pointer-events-none absolute top-1/2 right-1 z-10 flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-text-tertiary opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-popup-open:pointer-events-auto data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary data-popup-open:opacity-100',
isRemoveHighlighted && 'text-text-destructive!',
)}
onClick={(event) => event.stopPropagation()}
>
<span aria-hidden className="i-ri-more-fill size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-end" sideOffset={4} className="w-48">
<DropdownMenuItem className="gap-2" onClick={handleOpenInLibrary}>
<span
aria-hidden
className="i-ri-arrow-right-up-line size-4 shrink-0 text-text-tertiary"
/>
<span>{t(($) => $['agentDetail.configure.skills.openInLibrary'])}</span>
</DropdownMenuItem>
{canRemove && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
data-workspace-skill-remove-action
className="group gap-2 data-highlighted:bg-state-destructive-hover data-highlighted:text-text-destructive"
onClick={() => onRemove(skill.id)}
onFocus={() => setIsRemoveHighlighted(true)}
onBlur={() => setIsRemoveHighlighted(false)}
onMouseEnter={() => setIsRemoveHighlighted(true)}
onMouseLeave={() => setIsRemoveHighlighted(false)}
>
<span
aria-hidden
className="i-ri-delete-bin-line size-4 shrink-0 text-text-tertiary group-data-highlighted:text-text-destructive"
/>
<span>{t(($) => $['agentDetail.configure.skills.removeAction'])}</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
export function AgentSkills() {
const { t } = useTranslation('agentV2')
const { t: tSkill } = useTranslation('skill')
const { t: tCommon } = useTranslation('common')
const skillsTip = t(($) => $['agentDetail.configure.skills.tip'])
const skillsListId = 'agent-configure-skills-list'
const queryClient = useQueryClient()
const isViewingVersion = useAgentOrchestrateViewingVersion()
const [addMenuOpen, setAddMenuOpen] = useState(false)
const [addMenuView, setAddMenuView] = useState<'menu' | 'workspace-selector'>('menu')
const [isUploadOpen, setIsUploadOpen] = useState(false)
const promptAddCallbackRef = useRef<AgentOrchestrateAddActionOptions['onAdded']>(undefined)
const apiContext = useAgentConfigApiContext()
@@ -37,12 +478,133 @@ export function AgentSkills() {
const { mutate: deleteAppSkill } = useMutation(
consoleQuery.apps.byAppId.agent.config.skills.byName.delete.mutationOptions(),
)
const agentSkillBindingsQueryOptions =
consoleQuery.workspaces.current.agents.byAgentId.skills.get.queryOptions({
input: {
params: {
agent_id: apiContext.agentId,
},
},
})
const agentSkillBindingsQuery = useQuery({
...agentSkillBindingsQueryOptions,
})
const hasLoadedAgentSkillBindings = agentSkillBindingsQuery.data !== undefined
const { isPending: isReplacingAgentSkillBindings, mutate: replaceAgentSkillBindings } =
useMutation(consoleQuery.workspaces.current.agents.byAgentId.skills.put.mutationOptions())
const workspaceSkills = agentSkillBindingsQuery.data?.data ?? []
const boundSkillIds =
agentSkillBindingsQuery.data?.skill_ids ?? workspaceSkills.map((skill) => skill.id)
const hasSkills = skills.length > 0 || workspaceSkills.length > 0
const invalidateAgentSkillBindings = useCallback(() => {
void queryClient.invalidateQueries({
queryKey: consoleQuery.workspaces.current.agents.byAgentId.skills.get.key({
type: 'query',
input: {
params: {
agent_id: apiContext.agentId,
},
},
}),
})
}, [apiContext.agentId, queryClient])
const handleOpenUpload = useCallback((options?: AgentOrchestrateAddActionOptions) => {
const replaceWorkspaceSkillBindings = useCallback(
(skillIds: string[], onSuccess?: () => void) => {
if (isViewingVersion || !hasLoadedAgentSkillBindings) return
replaceAgentSkillBindings(
{
params: {
agent_id: apiContext.agentId,
},
body: {
skill_ids: skillIds,
},
},
{
onError: async (error) => {
const normalizedError = await normalizeSkillError(error)
const errorCode = getSkillErrorCode(normalizedError)
if (errorCode === 'too_many_agent_skills') {
toast.error(
t(($) => $['agentDetail.configure.skills.workspaceSelector.limitReached']),
)
return
}
if (errorCode === 'skill_name_conflict') {
toast.error(
tSkill(($) => $['skillManagement.errors.nameConflict'], {
name: getSkillErrorDetailString(normalizedError, 'name') ?? '',
}),
)
return
}
toast.error(t(($) => $['agentDetail.configure.skills.workspaceSelector.saveFailed']))
},
onSuccess: () => {
invalidateAgentSkillBindings()
void queryClient.invalidateQueries({
queryKey: consoleQuery.agent.byAgentId.composer.get.key({
type: 'query',
input: {
params: {
agent_id: apiContext.agentId,
},
},
}),
})
onSuccess?.()
},
},
)
},
[
apiContext.agentId,
hasLoadedAgentSkillBindings,
invalidateAgentSkillBindings,
isViewingVersion,
queryClient,
replaceAgentSkillBindings,
t,
tSkill,
],
)
const handlePromptAdd = useCallback((options?: AgentOrchestrateAddActionOptions) => {
promptAddCallbackRef.current = options?.onAdded
if (options?.skillSource === 'library') {
setAddMenuView('workspace-selector')
setAddMenuOpen(true)
return
}
if (options?.skillSource === 'upload') {
setIsUploadOpen(true)
return
}
setAddMenuView('menu')
setAddMenuOpen(true)
}, [])
useRegisterAgentOrchestrateAddAction('skills', handlePromptAdd)
const handleAddMenuOpenChange = useCallback((open: boolean) => {
setAddMenuOpen(open)
if (!open) {
setAddMenuView('menu')
promptAddCallbackRef.current = undefined
}
}, [])
const handleOpenWorkspaceSelector = useCallback(() => {
setAddMenuView('workspace-selector')
}, [])
const handleOpenUploadFromMenu = useCallback(() => {
setAddMenuOpen(false)
setIsUploadOpen(true)
}, [])
useRegisterAgentOrchestrateAddAction('skills', handleOpenUpload)
const handleUploaded = useCallback(
(skill: AgentSkill) => {
@@ -53,11 +615,45 @@ export function AgentSkills() {
[upsertAgentSkill],
)
const handleSelectWorkspaceSkill = useCallback(
(skill: SkillResponse) => {
if (
!hasLoadedAgentSkillBindings ||
!skill.latest_published_version_id ||
boundSkillIds.includes(skill.id)
)
return
if (boundSkillIds.length >= MAX_AGENT_LIBRARY_SKILLS) {
toast.error(t(($) => $['agentDetail.configure.skills.workspaceSelector.limitReached']))
return
}
replaceWorkspaceSkillBindings([...boundSkillIds, skill.id], () => {
promptAddCallbackRef.current?.({
description: skill.description,
id: skill.name,
name: skill.display_name,
})
promptAddCallbackRef.current = undefined
setAddMenuOpen(false)
setAddMenuView('menu')
})
},
[boundSkillIds, hasLoadedAgentSkillBindings, replaceWorkspaceSkillBindings, t],
)
const handleUploadOpenChange = useCallback((open: boolean) => {
if (!open) promptAddCallbackRef.current = undefined
setIsUploadOpen(open)
}, [])
const handleRemoveWorkspaceSkill = useCallback(
(skillId: string) => {
replaceWorkspaceSkillBindings(boundSkillIds.filter((item) => item !== skillId))
},
[boundSkillIds, replaceWorkspaceSkillBindings],
)
const handleRemoveSkill = useCallback(
(skillId: string) => {
const skill = skills.find((item) => item.id === skillId)
@@ -113,26 +709,87 @@ export function AgentSkills() {
rootClassName="border-b border-divider-subtle pt-4"
panelContentClassName="flex flex-col gap-1 pb-4"
actions={
<ConfigureSectionAddButton
ariaLabel={t(($) => $['agentDetail.configure.skills.add'])}
onClick={() => handleOpenUpload()}
/>
!isViewingVersion && (
<Popover open={addMenuOpen} onOpenChange={handleAddMenuOpenChange}>
<PopoverTrigger
render={
<Button
aria-label={t(($) => $['agentDetail.configure.skills.add'])}
variant="ghost"
size="small"
className="shrink-0 gap-1 px-2"
>
<span aria-hidden className="i-ri-add-line size-3.5" />
<span>{tCommon(($) => $['operation.add'])}</span>
</Button>
}
/>
<PopoverContent
placement="bottom-end"
sideOffset={4}
className={
addMenuView === 'menu'
? 'w-[280px] bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]'
: 'w-[320px] overflow-visible border-none bg-transparent p-0 shadow-none'
}
>
{addMenuView === 'menu' ? (
<>
<AgentSkillAddMenuItem
iconClassName="i-custom-vender-agent-v2-building-blocks"
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
description={t(
($) => $['agentDetail.configure.skills.addMenu.workspace.description'],
)}
onClick={handleOpenWorkspaceSelector}
/>
<AgentSkillAddMenuItem
badge={t(($) => $['agentDetail.configure.skills.addMenu.upload.badge'])}
iconClassName="i-ri-upload-cloud-2-line"
label={t(($) => $['agentDetail.configure.skills.addMenu.upload.label'])}
description={t(
($) => $['agentDetail.configure.skills.addMenu.upload.description'],
)}
onClick={handleOpenUploadFromMenu}
/>
</>
) : (
<WorkspaceSkillSelector
boundSkillIds={boundSkillIds}
isBindingPending={!hasLoadedAgentSkillBindings || isReplacingAgentSkillBindings}
onSelect={handleSelectWorkspaceSkill}
/>
)}
</PopoverContent>
</Popover>
)
}
>
{skills.length === 0 ? (
{!hasSkills ? (
<ConfigureSectionEmpty
title={t(($) => $['agentDetail.configure.skills.empty.title'])}
description={t(($) => $['agentDetail.configure.skills.empty.description'])}
/>
) : (
skills.map((skill) => (
<AgentSkillItem
key={skill.id}
apiContext={apiContext}
skill={skill}
onRemove={handleRemoveSkill}
/>
))
<>
{workspaceSkills.map((skill) => (
<WorkspaceAgentSkillItem
key={skill.id}
canRemove={!isViewingVersion}
skill={skill}
onRemove={handleRemoveWorkspaceSkill}
/>
))}
{skills.map((skill) => (
<AgentSkillItem
key={skill.id}
apiContext={apiContext}
canRemove={!isViewingVersion}
skill={skill}
onRemove={handleRemoveSkill}
/>
))}
</>
)}
</ConfigureSection>
<AgentSkillUploadDialog
@@ -4,30 +4,39 @@ import type { AgentConfigApiContext } from '../config-context'
import type { AgentSkill } from '@/features/agent-v2/agent-composer/form-state'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog } from '@langgenius/dify-ui/dialog'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useQueryClient } from '@tanstack/react-query'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import { downloadUrl } from '@/utils/download'
import { MissingReferenceWarning } from '../common/missing-reference-warning'
import { useAgentOrchestrateReadOnly } from '../read-only-context'
import { AgentSkillDetailDialog } from './detail-dialog'
import { useAgentSkillDetail } from './use-skill-detail'
export function AgentSkillItem({
apiContext,
canRemove,
skill,
onRemove,
}: {
apiContext: AgentConfigApiContext
canRemove: boolean
skill: AgentSkill
onRemove: (skillId: string) => void
}) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const queryClient = useQueryClient()
const readOnly = useAgentOrchestrateReadOnly()
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
const [isActionsOpen, setIsActionsOpen] = useState(false)
const [isRemoveHighlighted, setIsRemoveHighlighted] = useState(false)
const handleRemove = useCallback(() => {
onRemove(skill.id)
}, [onRemove, skill.id])
@@ -84,16 +93,26 @@ export function AgentSkillItem({
return (
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
<div className="group relative h-8 overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs shadow-shadow-shadow-3 focus-within:bg-components-panel-on-panel-item-bg-hover focus-within:shadow-sm hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm has-[[data-agent-skill-remove-button]:focus-visible]:border-state-destructive-border! has-[[data-agent-skill-remove-button]:focus-visible]:bg-state-destructive-hover! has-[[data-agent-skill-remove-button]:focus-visible]:shadow-xs! has-[[data-agent-skill-remove-button]:hover]:border-state-destructive-border! has-[[data-agent-skill-remove-button]:hover]:bg-state-destructive-hover! has-[[data-agent-skill-remove-button]:hover]:shadow-xs!">
<div
data-agent-skill-row
className={cn(
'group relative h-8 overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs shadow-shadow-shadow-3 focus-within:bg-components-panel-on-panel-item-bg-hover focus-within:shadow-sm hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm',
isRemoveHighlighted &&
'border-state-destructive-border! bg-state-destructive-hover! shadow-xs!',
)}
>
<button
type="button"
aria-label={skill.name}
disabled={skill.isMissing}
className="flex h-full w-full min-w-0 cursor-pointer items-center gap-1 rounded-lg py-1 pr-2.5 pl-2 text-left outline-hidden select-none focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid disabled:cursor-default"
className="flex h-full w-full min-w-0 cursor-pointer items-center gap-1 rounded-lg px-2 py-1 text-left outline-hidden select-none focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid disabled:cursor-default"
onClick={handleOpenPreview}
>
<span aria-hidden className="i-custom-public-agent-building-blocks size-4 shrink-0" />
<span className="w-0 min-w-0 flex-1 truncate system-sm-medium text-text-secondary">
<span
aria-hidden
className="i-custom-vender-agent-v2-building-blocks size-4 shrink-0 text-text-secondary"
/>
<span className="w-0 min-w-0 flex-1 truncate system-sm-medium text-text-secondary decoration-divider-deep decoration-dotted group-focus-within:underline group-hover:underline">
{skill.name}
</span>
{skill.isMissing ? (
@@ -101,11 +120,11 @@ export function AgentSkillItem({
) : (
<span
className={cn(
'shrink-0 system-xs-regular text-text-tertiary',
'group-focus-within:opacity-0 group-hover:opacity-0',
'shrink-0 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary group-focus-within:opacity-0 group-hover:opacity-0',
isActionsOpen && 'opacity-0',
)}
>
{t(($) => $['agentDetail.configure.skills.itemType'])}
{t(($) => $['agentDetail.configure.skills.addMenu.upload.badge'])}
</span>
)}
</button>
@@ -115,32 +134,57 @@ export function AgentSkillItem({
label={t(($) => $['agentDetail.configure.skills.missing'])}
/>
)}
{!skill.isMissing && (
<button
type="button"
aria-label={`${tCommon(($) => $['operation.download'])} ${skill.name}`}
onClick={handleDownload}
className={cn(
'pointer-events-none absolute top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded-md text-text-tertiary opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 hover:bg-state-base-hover hover:text-text-secondary focus-visible:bg-state-base-hover focus-visible:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
readOnly ? 'right-1' : 'right-7',
)}
{(!skill.isMissing || canRemove) && (
<DropdownMenu
modal={false}
onOpenChange={(open) => {
setIsActionsOpen(open)
if (!open) setIsRemoveHighlighted(false)
}}
>
<span aria-hidden className="i-ri-download-line size-4" />
</button>
)}
{!readOnly && (
<button
type="button"
data-agent-skill-remove-button
aria-label={t(($) => $['agentDetail.configure.skills.remove'], { name: skill.name })}
onClick={handleRemove}
className={cn(
'pointer-events-none absolute top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded-md text-text-tertiary opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 hover:bg-state-destructive-hover hover:text-text-destructive focus-visible:bg-state-destructive-hover focus-visible:text-text-destructive focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
skill.isMissing ? 'right-7' : 'right-1',
)}
>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
</button>
<DropdownMenuTrigger
data-agent-skill-actions
aria-label={t(($) => $['agentDetail.configure.skills.moreActions'], {
name: skill.name,
})}
className={cn(
'pointer-events-none absolute top-1/2 right-1 z-10 flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-text-tertiary opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-popup-open:pointer-events-auto data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary data-popup-open:opacity-100',
isRemoveHighlighted && 'text-text-destructive!',
)}
onClick={(event) => event.stopPropagation()}
>
<span aria-hidden className="i-ri-more-fill size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-end" sideOffset={4} className="w-48">
{!skill.isMissing && (
<DropdownMenuItem className="gap-2" onClick={handleDownload}>
<span
aria-hidden
className="i-ri-download-line size-4 shrink-0 text-text-tertiary"
/>
<span>{tCommon(($) => $['operation.download'])}</span>
</DropdownMenuItem>
)}
{!skill.isMissing && canRemove && <DropdownMenuSeparator />}
{canRemove && (
<DropdownMenuItem
data-agent-skill-remove-button
className="group gap-2 data-highlighted:bg-state-destructive-hover data-highlighted:text-text-destructive"
onClick={handleRemove}
onFocus={() => setIsRemoveHighlighted(true)}
onBlur={() => setIsRemoveHighlighted(false)}
onMouseEnter={() => setIsRemoveHighlighted(true)}
onMouseLeave={() => setIsRemoveHighlighted(false)}
>
<span
aria-hidden
className="i-ri-delete-bin-line size-4 shrink-0 text-text-tertiary group-data-highlighted:text-text-destructive"
/>
<span>{tCommon(($) => $['operation.delete'])}</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
{isPreviewOpen && <AgentSkillDetailDialog skillName={skill.name} detail={detail} />}
@@ -16,7 +16,10 @@ import {
isAgentComposerDirtyAtom,
} from '@/features/agent-v2/agent-composer/store'
import { seedAccountProfileQuery } from '@/test/console/account-profile'
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
import {
AgentOrchestrateReadOnlyContext,
AgentOrchestrateViewingVersionContext,
} from '../../read-only-context'
import { AgentTools } from '../index'
const toolProviderState = vi.hoisted(() => ({
@@ -427,7 +430,13 @@ function renderAgentToolsWithStore(initialDraft: AgentSoulConfigFormState = agen
}
}
function renderReadonlyAgentTools(initialDraft: AgentSoulConfigFormState = agentToolsDraft) {
function renderReadonlyAgentTools({
initialDraft = agentToolsDraft,
viewingVersion = false,
}: {
initialDraft?: AgentSoulConfigFormState
viewingVersion?: boolean
} = {}) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -440,9 +449,11 @@ function renderReadonlyAgentTools(initialDraft: AgentSoulConfigFormState = agent
return render(
<QueryClientProvider client={queryClient}>
<AgentComposerProvider initialDraft={initialDraft}>
<AgentOrchestrateReadOnlyContext value>
<AgentTools />
</AgentOrchestrateReadOnlyContext>
<AgentOrchestrateViewingVersionContext value={viewingVersion}>
<AgentOrchestrateReadOnlyContext value>
<AgentTools />
</AgentOrchestrateReadOnlyContext>
</AgentOrchestrateViewingVersionContext>
</AgentComposerProvider>
</QueryClientProvider>,
)
@@ -533,9 +544,9 @@ describe('AgentTools', () => {
).toBeInTheDocument()
})
it('should hide add, edit, and remove actions when readonly', async () => {
it('should hide add, edit, and remove actions when viewing a version', async () => {
const user = userEvent.setup()
renderReadonlyAgentTools()
renderReadonlyAgentTools({ viewingVersion: true })
expect(
screen.queryByRole('button', {
@@ -576,6 +587,16 @@ describe('AgentTools', () => {
).not.toBeInTheDocument()
})
it('should keep add action available for build drafts', () => {
renderReadonlyAgentTools()
expect(
screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.tools.add',
}),
).toBeInTheDocument()
})
it('should hide CLI tool rows while CLI tools are disabled', () => {
renderAgentTools()
@@ -41,7 +41,7 @@ import { ConfigureSectionAddButton } from '../common/add-button'
import { ConfigureSectionEmpty } from '../common/empty'
import { ConfigureSection } from '../common/section'
import { AgentConfigureTipContent } from '../common/tip-content'
import { useAgentOrchestrateReadOnly } from '../read-only-context'
import { useAgentOrchestrateViewingVersion } from '../read-only-context'
import { CliToolDialog } from './cli-tool/dialog'
import { AgentCliToolItem } from './cli-tool/item'
import {
@@ -380,7 +380,7 @@ function AddToolMenu({
export function AgentTools() {
const { t } = useTranslation('agentV2')
const readOnly = useAgentOrchestrateReadOnly()
const isViewingVersion = useAgentOrchestrateViewingVersion()
const setProviderToolCredential = useSetAtom(setProviderToolCredentialAtom)
const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
@@ -530,7 +530,7 @@ export function AgentTools() {
rootClassName="border-b border-divider-subtle pt-4"
panelContentClassName="flex flex-col gap-1 pb-4"
actions={
!readOnly ? (
!isViewingVersion ? (
<AddToolMenu
onAddCliTool={openCliToolDialog}
onAddTools={addTools}
@@ -2,6 +2,7 @@
import type { AgentAppPartial, AgentIconType } from '@dify/contracts/api/console/agent/types.gen'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
DropdownMenu,
DropdownMenuContent,
@@ -224,7 +225,12 @@ function AgentRosterItem({ agent }: { agent: AgentAppPartial }) {
</div>
</div>
</div>
<div className="pointer-events-none absolute top-2 right-2 z-20 flex items-center overflow-hidden rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 opacity-0 shadow-lg backdrop-blur-xs transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 has-data-popup-open:pointer-events-auto has-data-popup-open:opacity-100">
<div
className={cn(
'pointer-events-none absolute right-2 z-20 flex items-center overflow-hidden rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 opacity-0 shadow-lg backdrop-blur-xs transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 has-data-popup-open:pointer-events-auto has-data-popup-open:opacity-100',
isDraft ? 'top-7' : 'top-2',
)}
>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
aria-label={t(($) => $['roster.moreActions'], { name: agent.name })}
@@ -5,7 +5,6 @@ import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
import { BannerItem } from '../banner-item'
const mockTrackEvent = vi.fn()
vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
}))
@@ -63,7 +62,9 @@ describe('BannerItem', () => {
accountId: 'account-123',
})
fireEvent.click(screen.getByRole('link', { name: 'Test Banner Title' }))
const link = screen.getByRole('link', { name: 'Test Banner Title' })
link.addEventListener('click', (event) => event.preventDefault())
fireEvent.click(link)
expect(mockTrackEvent).toHaveBeenCalledWith(
'explore_banner_click',
+15
View File
@@ -0,0 +1,15 @@
# Skills
Workspace Skill management UI. This module owns the Skills list, filters, and list-level actions.
## Internal Modules
None.
## External Modules
- app/components/base/search-input
- app/components/base/skeleton
- app/components/base/tooltip
- hooks/use-document-title
- hooks/use-timestamp
File diff suppressed because it is too large Load Diff
+804
View File
@@ -0,0 +1,804 @@
import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen'
import type {
SkillReferenceResponse,
SkillResponse,
SkillTagResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ReactNode } from 'react'
import { toast } from '@langgenius/dify-ui/toast'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import SkillsPage from '../page'
type SkillsInfiniteOptions = {
getNextPageParam: (lastPage: { has_more: boolean; page: number }) => number | undefined
initialPageParam: number
input: (pageParam: unknown) => {
query: Record<string, unknown>
}
}
const mocks = vi.hoisted(() => ({
createSkillMutationFn: vi.fn(),
deleteSkillMutationFn: vi.fn(),
downloadBlob: vi.fn(),
duplicateSkillMutationFn: vi.fn(),
exportSkillArchiveBlob: vi.fn(),
importSkillMutationFn: vi.fn(),
push: vi.fn(),
genericTags: [] as Tag[],
genericTagsQueryOptions: vi.fn((_options: unknown) => ({})),
queryState: {
keyword: '',
tag: [] as string[],
},
skills: [] as SkillResponse[],
skillPages: [] as SkillResponse[][],
skillsKey: vi.fn((_options: unknown): unknown[] => ['skills']),
skillsQueryOptions: vi.fn((_options: SkillsInfiniteOptions) => ({})),
skillReferences: [] as SkillReferenceResponse[],
skillReferencesQueryOptions: vi.fn((_options: unknown) => ({})),
tags: [] as SkillTagResponse[],
tagsKey: vi.fn((_options: unknown): unknown[] => ['skill-tags']),
tagsQueryOptions: vi.fn((_options: unknown) => ({})),
}))
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
error: vi.fn(),
success: vi.fn(),
},
}))
vi.mock('ahooks', () => ({
useDebounce: (value: unknown) => value,
}))
vi.mock('nuqs', async () => {
const React = await import('react')
const listeners = new Map<'keyword' | 'tag', Set<() => void>>()
const createParser = () => ({
withDefault: () => ({
withOptions: () => ({}),
}),
})
return {
debounce: () => undefined,
parseAsArrayOf: () => ({
withDefault: () => ({}),
}),
parseAsString: createParser(),
useQueryState: (name: 'keyword' | 'tag') => {
const [value, setValue] = React.useState(mocks.queryState[name])
React.useEffect(() => {
const nameListeners = listeners.get(name) ?? new Set<() => void>()
listeners.set(name, nameListeners)
const listener = () => setValue(mocks.queryState[name])
nameListeners.add(listener)
return () => {
nameListeners.delete(listener)
}
}, [name])
const setQueryValue = (nextValue: string | string[]) => {
mocks.queryState[name] = nextValue as never
setValue(nextValue as never)
listeners.get(name)?.forEach((listener) => listener())
return Promise.resolve(new URLSearchParams())
}
return [value, setQueryValue] as const
},
}
})
vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
}))
vi.mock('@/hooks/use-format-time-from-now', () => ({
useFormatTimeFromNow: () => ({
formatTimeFromNow: () => '2 hours ago',
}),
}))
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: () => '2026-07-22 10:00',
}),
}))
vi.mock('@/app/components/base/app-icon', () => ({
default: ({ icon }: { icon?: string }) => <span>{icon}</span>,
}))
vi.mock('@/features/tag-management/components/skill-card-tags', () => ({
SkillCardTags: ({ tags }: { tags: string[] }) => <div>{tags.join(', ')}</div>,
}))
vi.mock('../skill-list-tag-management-modal', () => ({
SkillListTagManagementModal: () => null,
}))
vi.mock('@/next/link', () => ({
default: ({ children, href, ...props }: { children: ReactNode; href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}))
vi.mock('@/next/navigation', () => ({
useRouter: () => ({
push: mocks.push,
}),
}))
vi.mock('@/utils/download', () => ({
downloadBlob: mocks.downloadBlob,
}))
vi.mock('../client', () => ({
fetchSkillArchiveBlob: mocks.exportSkillArchiveBlob,
uploadSkillFile: vi.fn(),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
tags: {
get: {
queryOptions: mocks.genericTagsQueryOptions,
},
},
workspaces: {
current: {
skills: {
get: {
key: mocks.skillsKey,
infiniteOptions: mocks.skillsQueryOptions,
},
post: {
mutationOptions: () => ({ mutationFn: mocks.createSkillMutationFn }),
},
import: {
post: {
mutationOptions: () => ({ mutationFn: mocks.importSkillMutationFn }),
},
},
tags: {
get: {
key: mocks.tagsKey,
queryOptions: mocks.tagsQueryOptions,
},
},
bySkillId: {
delete: {
mutationOptions: () => ({ mutationFn: mocks.deleteSkillMutationFn }),
},
references: {
get: {
queryOptions: mocks.skillReferencesQueryOptions,
},
},
duplicate: {
post: {
mutationOptions: () => ({ mutationFn: mocks.duplicateSkillMutationFn }),
},
},
},
},
},
},
},
}))
vi.mock('../permissions', () => ({
useSkillPermissions: () => ({ canDelete: true, canEdit: true, canPublish: true }),
}))
function createSkill(overrides: Partial<SkillResponse> = {}): SkillResponse {
return {
id: 'skill-1',
name: 'refund-approval',
display_name: 'Refund approval',
icon: '💳',
description: 'Handle refund requests.',
tags: ['support'],
visibility: 'workspace',
latest_published_version_id: 'version-1',
latest_published_at: 1784638400,
reference_count: 2,
created_at: 1784631405,
updated_at: 1784638487,
...overrides,
}
}
function createAgentReference(
overrides: Partial<SkillReferenceResponse> = {},
): SkillReferenceResponse {
return {
agent_id: 'agent-1',
agent_icon: '🤖',
agent_icon_background: '#EFF6FF',
agent_icon_type: 'emoji',
app_id: 'app-1',
display_name: 'Support Agent',
name: 'support-agent',
type: 'agent',
...overrides,
}
}
function renderSkillsPage() {
const queryClient = new QueryClient({
defaultOptions: {
mutations: { retry: false },
queries: { retry: false },
},
})
return render(
<QueryClientProvider client={queryClient}>
<SkillsPage />
</QueryClientProvider>,
)
}
describe('SkillsPage', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.queryState.keyword = ''
mocks.queryState.tag = []
mocks.skills = [createSkill()]
mocks.skillPages = [mocks.skills]
mocks.skillReferences = [createAgentReference()]
mocks.genericTags = [
{ binding_count: '2', id: 'tag-support', name: 'support', type: 'skill' },
{ binding_count: '1', id: 'tag-sales', name: 'sales', type: 'skill' },
]
mocks.tags = [
{ count: 2, tag: 'support' },
{ count: 1, tag: 'sales' },
]
mocks.skillsKey.mockImplementation((options) => ['skills', options])
mocks.tagsKey.mockImplementation((options) => ['skill-tags', options])
mocks.skillsQueryOptions.mockImplementation((options) => ({
queryKey: ['skills', options],
queryFn: async ({ pageParam }: { pageParam: unknown }) => {
const page = Number(pageParam)
return {
data: mocks.skillPages[page - 1] ?? [],
has_more: page < mocks.skillPages.length,
page,
total: mocks.skillPages.flat().length,
}
},
getNextPageParam: options.getNextPageParam,
initialPageParam: options.initialPageParam,
}))
mocks.tagsQueryOptions.mockImplementation((options) => ({
queryKey: ['skill-tags', options],
queryFn: async () => ({
data: mocks.tags,
}),
}))
mocks.genericTagsQueryOptions.mockImplementation((options) => ({
queryKey: ['tags', options],
queryFn: async () => mocks.genericTags,
}))
mocks.skillReferencesQueryOptions.mockImplementation((options) => ({
queryKey: ['skill-references', options],
queryFn: async () => ({
data: mocks.skillReferences,
}),
}))
mocks.createSkillMutationFn.mockResolvedValue(createSkill({ id: 'created-skill' }))
mocks.importSkillMutationFn.mockResolvedValue(createSkill({ id: 'imported-skill' }))
mocks.duplicateSkillMutationFn.mockResolvedValue(createSkill({ id: 'duplicated-skill' }))
mocks.exportSkillArchiveBlob.mockResolvedValue(new Blob(['skill archive']))
mocks.deleteSkillMutationFn.mockResolvedValue({
deleted: true,
id: 'skill-1',
})
})
it('renders skills with tags, reference count, and detail links', async () => {
renderSkillsPage()
const skillLink = await screen.findByRole('link', { name: /Refund approval/ })
expect(skillLink).toHaveAttribute('href', '/skills/skill-1')
expect(screen.getByText('refund-approval')).toBeInTheDocument()
expect(screen.getByText('Handle refund requests.')).toBeInTheDocument()
expect(screen.getByText('support')).toBeInTheDocument()
expect(
screen.getByText('skill.skillManagement.referenceCount_other:{"count":2}'),
).toBeInTheDocument()
expect(
screen.getByText('skill.skillManagement.publishedAt:{"time":"2 hours ago"}'),
).toBeInTheDocument()
})
it('renders draft update time as relative time', async () => {
mocks.skills = [createSkill({ latest_published_version_id: null, latest_published_at: null })]
mocks.skillPages = [mocks.skills]
renderSkillsPage()
expect(
await screen.findByText('skill.skillManagement.editedAt:{"time":"2 hours ago"}'),
).toBeInTheDocument()
})
it('shows guidance instead of a persisted editor placeholder when description is empty', async () => {
mocks.skills = [createSkill({ description: '' })]
mocks.skillPages = [mocks.skills]
renderSkillsPage()
expect(await screen.findByText('skill.skillManagement.noDescription')).toBeInTheDocument()
})
it('passes keyword and selected tags to the list query', async () => {
mocks.queryState.keyword = 'refund'
mocks.queryState.tag = ['support']
renderSkillsPage()
await waitFor(() => {
const queryOptions = mocks.skillsQueryOptions.mock.lastCall?.[0]
expect(queryOptions?.input(1)).toEqual({
query: {
keyword: 'refund',
limit: 20,
page: 1,
tag: ['support'],
},
})
})
})
it('clears stale tag names from the URL-backed filter state', async () => {
mocks.queryState.tag = ['renamed-tag']
renderSkillsPage()
await waitFor(() => {
expect(mocks.queryState.tag).toEqual([])
})
await waitFor(() => {
const queryOptions = mocks.skillsQueryOptions.mock.lastCall?.[0]
expect(queryOptions?.input(1)).toEqual({
query: {
limit: 20,
page: 1,
},
})
})
})
it('loads the next skill page when the list scrolls near the bottom', async () => {
const firstPageSkills = Array.from({ length: 20 }, (_, index) =>
createSkill({
id: `skill-${index + 1}`,
name: `skill-${index + 1}`,
display_name: `Skill ${index + 1}`,
}),
)
const nextPageSkill = createSkill({
id: 'skill-21',
name: 'skill-21',
display_name: 'Skill 21',
})
mocks.skills = firstPageSkills
mocks.skillPages = [firstPageSkills, [nextPageSkill]]
renderSkillsPage()
const skillList = await screen.findByRole('region', {
name: 'skill.skillManagement.listLabel',
})
await screen.findByRole('heading', { name: 'Skill 1' })
expect(within(skillList).getAllByRole('article')).toHaveLength(20)
const scrollViewport = skillList.parentElement?.parentElement
expect(scrollViewport).not.toBeNull()
Object.defineProperties(scrollViewport!, {
clientHeight: { configurable: true, value: 600 },
scrollHeight: { configurable: true, value: 1200 },
scrollTop: { configurable: true, value: 560 },
})
fireEvent.scroll(scrollViewport!)
expect(await screen.findByRole('heading', { name: 'Skill 21' })).toBeInTheDocument()
expect(within(skillList).getAllByRole('article')).toHaveLength(21)
expect(mocks.skillsQueryOptions.mock.lastCall?.[0].input(2)).toEqual({
query: {
limit: 20,
page: 2,
},
})
})
it('creates a placeholder skill and navigates to its detail page', async () => {
const user = userEvent.setup()
const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries')
renderSkillsPage()
await user.click(await screen.findByRole('button', { name: 'skill.skillManagement.create' }))
await waitFor(() => {
expect(mocks.createSkillMutationFn).toHaveBeenCalledWith(
{
body: {},
},
expect.anything(),
)
})
expect(toast.success).toHaveBeenCalledWith('skill.skillManagement.createSuccess')
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['skills', { type: 'query' }] })
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['skills', { type: 'infinite' }] })
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: ['skill-tags', { type: 'query' }],
})
expect(mocks.push).toHaveBeenCalledWith('/skills/created-skill')
invalidateQueries.mockRestore()
})
it('explains when the workspace skill limit blocks draft creation', async () => {
const user = userEvent.setup()
mocks.createSkillMutationFn.mockRejectedValueOnce({ code: 'skill_limit_exceeded' })
renderSkillsPage()
await user.click(await screen.findByRole('button', { name: 'skill.skillManagement.create' }))
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('skill.skillManagement.errors.workspaceLimit')
})
})
it('imports a package file and navigates to the imported skill', async () => {
const user = userEvent.setup()
const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries')
const { container } = renderSkillsPage()
const fileInput = container.querySelector<HTMLInputElement>('input[type="file"]')
expect(fileInput).not.toBeNull()
const file = new File(['skill'], 'refund.skill', { type: 'application/zip' })
await user.upload(fileInput!, file)
await waitFor(() => {
expect(mocks.importSkillMutationFn).toHaveBeenCalledWith(
{
body: {
file,
},
},
expect.anything(),
)
})
expect(toast.success).toHaveBeenCalledWith('skill.skillManagement.importSuccess')
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['skills', { type: 'query' }] })
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['skills', { type: 'infinite' }] })
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: ['skill-tags', { type: 'query' }],
})
expect(mocks.push).toHaveBeenCalledWith('/skills/imported-skill')
invalidateQueries.mockRestore()
})
it.each([
{
error: {
data: {
body: {
code: 'skill_name_conflict',
details: { name: 'refund-approval' },
},
},
},
message: 'skill.skillManagement.errors.nameConflict:{"name":"refund-approval"}',
},
{
error: { code: 'skill_limit_exceeded' },
message: 'skill.skillManagement.errors.workspaceLimit',
},
{
error: { code: 'missing_skill_md' },
message: 'skill.skillManagement.errors.missingSkillMd',
},
{
error: { message: 'Skill package must contain SKILL.md' },
message: 'skill.skillManagement.errors.missingSkillMd',
},
{
error: { message: 'Skill name "refund-approval" already exists' },
message: 'skill.skillManagement.errors.nameConflict:{"name":"refund-approval"}',
},
])('explains import errors for $error.code', async ({ error, message }) => {
const user = userEvent.setup()
mocks.importSkillMutationFn.mockRejectedValueOnce(error)
const { container } = renderSkillsPage()
const fileInput = container.querySelector<HTMLInputElement>('input[type="file"]')
const file = new File(['skill'], 'refund.skill', { type: 'application/zip' })
await user.upload(fileInput!, file)
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(message)
})
})
it('explains an import error returned as a Response body', async () => {
const user = userEvent.setup()
mocks.importSkillMutationFn.mockRejectedValueOnce(
new Response(JSON.stringify({ message: 'Skill package must contain SKILL.md' }), {
status: 400,
}),
)
const { container } = renderSkillsPage()
const fileInput = container.querySelector<HTMLInputElement>('input[type="file"]')
await user.upload(fileInput!, new File(['skill'], 'refund.skill', { type: 'application/zip' }))
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('skill.skillManagement.errors.missingSkillMd')
})
})
it('duplicates a skill from the card action menu', async () => {
const user = userEvent.setup()
renderSkillsPage()
await user.click(
await screen.findByRole('button', {
name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
}),
)
await user.click(await screen.findByText('common.operation.duplicate'))
await waitFor(() => {
expect(mocks.duplicateSkillMutationFn).toHaveBeenCalledWith(
{
params: {
skill_id: 'skill-1',
},
},
expect.anything(),
)
})
expect(toast.success).toHaveBeenCalledWith('skill.skillManagement.duplicateSuccess')
})
it('exports a published skill from the card action menu', async () => {
const user = userEvent.setup()
renderSkillsPage()
await user.click(
await screen.findByRole('button', {
name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
}),
)
await user.click(await screen.findByText('common.operation.export'))
await waitFor(() => {
expect(mocks.exportSkillArchiveBlob).toHaveBeenCalledWith('skill-1')
})
expect(mocks.downloadBlob).toHaveBeenCalledWith({
data: expect.any(Blob),
fileName: 'refund-approval.zip',
})
})
it('does not show export for an unpublished skill', async () => {
const user = userEvent.setup()
mocks.skills = [createSkill({ latest_published_version_id: null })]
mocks.skillPages = [mocks.skills]
renderSkillsPage()
await user.click(
await screen.findByRole('button', {
name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
}),
)
expect(screen.queryByText('common.operation.export')).not.toBeInTheDocument()
})
it('confirms deletion with the skill name and refreshes list data', async () => {
const user = userEvent.setup()
renderSkillsPage()
await user.click(
await screen.findByRole('button', {
name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
}),
)
await user.click(await screen.findByText('common.operation.delete'))
const dialog = await screen.findByRole('alertdialog')
expect(
within(dialog).getByText(
'skill.skillManagement.deleteDialog.referencedDescription_other:{"count":2}',
),
).toBeInTheDocument()
expect(await within(dialog).findByText('Support Agent')).toBeInTheDocument()
expect(within(dialog).getByRole('link', { name: /Support Agent/ })).toHaveAttribute(
'target',
'_blank',
)
expect(within(dialog).getByTestId('skill-delete-reference-list')).toBeInTheDocument()
await user.type(
within(dialog).getByPlaceholderText(
'skill.skillManagement.deleteDialog.confirmInputPlaceholder',
),
'Refund approval',
)
await user.click(within(dialog).getByRole('button', { name: 'common.operation.delete' }))
await waitFor(() => {
expect(mocks.deleteSkillMutationFn).toHaveBeenCalledWith(
{
body: {
confirmation_name: 'Refund approval',
},
params: {
skill_id: 'skill-1',
},
},
expect.anything(),
)
})
expect(toast.success).toHaveBeenCalledWith('skill.skillManagement.deleteSuccess')
})
it('loads references in the delete confirmation when the list reference count is stale', async () => {
const user = userEvent.setup()
mocks.skills = [createSkill({ reference_count: 0 })]
mocks.skillPages = [mocks.skills]
mocks.skillReferences = [
createAgentReference({
agent_id: 'agent-stale-reference',
display_name: 'Support Agent From References API',
name: 'support-agent-from-references-api',
}),
]
renderSkillsPage()
await user.click(
await screen.findByRole('button', {
name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
}),
)
await user.click(await screen.findByText('common.operation.delete'))
const dialog = await screen.findByRole('alertdialog')
expect(await within(dialog).findByText('Support Agent From References API')).toBeInTheDocument()
expect(
within(dialog).getByText(
'skill.skillManagement.deleteDialog.referencedDescription_one:{"count":1}',
),
).toBeInTheDocument()
expect(mocks.skillReferencesQueryOptions).toHaveBeenCalledWith(
expect.objectContaining({
input: {
params: {
skill_id: 'skill-1',
},
},
}),
)
})
it('keeps deletion disabled while cached references refresh', async () => {
const user = userEvent.setup()
let referenceRequestCount = 0
let shouldHangReferenceRequest = false
mocks.skills = [createSkill({ reference_count: 0 })]
mocks.skillPages = [mocks.skills]
mocks.skillReferencesQueryOptions.mockImplementation((options) => ({
queryKey: ['skill-references-pending', options],
queryFn: () => {
referenceRequestCount += 1
if (!shouldHangReferenceRequest) return Promise.resolve({ data: [] })
return new Promise(() => {})
},
}))
renderSkillsPage()
const moreButton = await screen.findByRole('button', {
name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
})
await user.click(moreButton)
await user.click(await screen.findByText('common.operation.delete'))
let dialog = await screen.findByRole('alertdialog')
await waitFor(() => {
expect(within(dialog).getByRole('button', { name: 'common.operation.delete' })).toBeEnabled()
})
const initialRequestCount = referenceRequestCount
shouldHangReferenceRequest = true
await user.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' }))
await waitFor(() => {
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
await user.click(moreButton)
await user.click(await screen.findByText('common.operation.delete'))
dialog = await screen.findByRole('alertdialog')
expect(
within(dialog).getByRole('button', {
name: 'common.operation.delete',
}),
).toBeDisabled()
await waitFor(() => {
expect(referenceRequestCount).toBeGreaterThan(initialRequestCount)
})
})
it('collapses long reference lists in the delete confirmation', async () => {
const user = userEvent.setup()
mocks.skillReferences = Array.from({ length: 7 }, (_, index) =>
createAgentReference({
agent_id: `agent-${index}`,
display_name: `Support Agent ${index + 1}`,
name: `support-agent-${index + 1}`,
}),
)
renderSkillsPage()
await user.click(
await screen.findByRole('button', {
name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
}),
)
await user.click(await screen.findByText('common.operation.delete'))
const dialog = await screen.findByRole('alertdialog')
expect(await within(dialog).findByText('Support Agent 5')).toBeInTheDocument()
expect(within(dialog).queryByText('Support Agent 6')).not.toBeInTheDocument()
expect(within(dialog).getByTestId('skill-delete-reference-list')).not.toHaveAttribute(
'data-scrollable',
'true',
)
await user.click(
within(dialog).getByRole('button', {
name: 'skill.skillManagement.detail.showMoreReferences:{"count":2}',
}),
)
expect(within(dialog).getByText('Support Agent 6')).toBeInTheDocument()
expect(within(dialog).getByText('Support Agent 7')).toBeInTheDocument()
expect(within(dialog).getByTestId('skill-delete-reference-list')).toHaveAttribute(
'data-scrollable',
'true',
)
expect(within(dialog).getByTestId('skill-delete-reference-list')).toHaveClass(
'max-h-[240px]',
'overflow-y-auto',
)
})
it('shows the empty-search state without create or import actions', async () => {
mocks.queryState.keyword = 'missing'
mocks.skills = []
mocks.skillPages = [[]]
renderSkillsPage()
expect(await screen.findByText('skill.skillManagement.emptySearch')).toBeInTheDocument()
expect(
screen.queryByText('skill.skillManagement.emptyAction.createTitle'),
).not.toBeInTheDocument()
expect(
screen.queryByText('skill.skillManagement.emptyAction.importTitle'),
).not.toBeInTheDocument()
})
})
+215
View File
@@ -0,0 +1,215 @@
import type {
SkillAssistAttachmentPayload,
SkillAssistHistoryMessagePayload,
SkillFileUploadResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type {
DefaultModel,
FormValue,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
// oxlint-disable-next-line no-restricted-imports
import type { IOnCompleted, IOnData, IOnError } from '@/service/base'
// oxlint-disable-next-line no-restricted-imports
import { get, post, ssePost, upload } from '@/service/base'
function parseSkillUploadErrorMessage(message: string) {
const trimmedMessage = message.trim()
if (!trimmedMessage.startsWith('{')) return trimmedMessage
try {
const parsed: unknown = JSON.parse(trimmedMessage)
if (parsed && typeof parsed === 'object') {
const parsedMessage = (parsed as Record<string, unknown>).message
if (typeof parsedMessage === 'string' && parsedMessage.trim()) return parsedMessage.trim()
}
} catch {
return trimmedMessage
}
return trimmedMessage
}
function readSkillUploadErrorMessage(
error: unknown,
visited = new Set<unknown>(),
): string | undefined {
if (!error || visited.has(error)) return undefined
if (typeof error === 'string') return parseSkillUploadErrorMessage(error)
if (typeof error !== 'object') return undefined
visited.add(error)
const record = error as Record<string, unknown>
for (const key of ['data', 'body', 'error', 'cause', 'response']) {
const nestedMessage = readSkillUploadErrorMessage(record[key], visited)
if (nestedMessage) return nestedMessage
}
const message = record.message
if (typeof message === 'string' && message.trim()) return parseSkillUploadErrorMessage(message)
return undefined
}
async function getSkillUploadResponseErrorMessage(response: Response) {
try {
const data: unknown = await response.clone().json()
return readSkillUploadErrorMessage(data)
} catch {
try {
const text = await response.clone().text()
if (text.trim()) return parseSkillUploadErrorMessage(text)
} catch {}
}
}
export async function uploadSkillFile(
file: File,
options?: {
onProgress?: (progress: number) => void
xhr?: XMLHttpRequest
},
) {
const body = new FormData()
body.append('file', file)
try {
if (options?.onProgress) {
const onProgress = (event: ProgressEvent) => {
if (!event.lengthComputable) return
options.onProgress?.(Math.floor((event.loaded / event.total) * 100))
}
const response = await upload(
{
xhr: options.xhr ?? new XMLHttpRequest(),
data: body,
onprogress: onProgress,
},
false,
'/workspaces/current/skills/files/upload',
)
return response as SkillFileUploadResponse
}
return await post<SkillFileUploadResponse>(
'/workspaces/current/skills/files/upload',
{ body },
{
bodyStringify: false,
deleteContentType: true,
silent: true,
},
)
} catch (error) {
const message =
error instanceof Response
? await getSkillUploadResponseErrorMessage(error)
: readSkillUploadErrorMessage(error)
if (message) {
const normalizedError = new Error(message)
normalizedError.cause = error
throw normalizedError
}
throw error
}
}
export async function fetchSkillFileBlob({
download = false,
path,
skillId,
versionId,
}: {
download?: boolean
path: string
skillId: string
versionId: string | null
}) {
const params = new URLSearchParams({ path })
if (versionId) params.set('version_id', versionId)
if (download) params.set('download', '1')
const response = await get<Response>(
`/workspaces/current/skills/${encodeURIComponent(skillId)}/files/content?${params.toString()}`,
{},
{ needAllResponseContent: true },
)
return response.blob()
}
export async function fetchSkillArchiveBlob(skillId: string) {
const response = await get<Response>(
`/workspaces/current/skills/${encodeURIComponent(skillId)}/export`,
{},
{ needAllResponseContent: true },
)
return response.blob()
}
export function sendSkillAssistMessage({
attachments,
getAbortController,
message,
model,
onCompleted,
onData,
onError,
onUnhandledEvent,
skillId,
targetPath,
history,
}: {
attachments?: SkillAssistAttachmentPayload[]
history?: SkillAssistHistoryMessagePayload[]
getAbortController?: (abortController: AbortController) => void
message: string
model?: DefaultModel & {
model_settings?: FormValue
}
onCompleted?: IOnCompleted
onData?: IOnData
onError?: IOnError
onUnhandledEvent?: (event: Record<string, unknown>) => void
skillId: string
targetPath?: string
}) {
let streamErrorHandled = false
return ssePost(
`/workspaces/current/skills/${encodeURIComponent(skillId)}/assist/messages`,
{
body: {
attachments,
history,
message,
model,
target_path: targetPath,
},
},
{
silent: true,
getAbortController,
onCompleted: (hasError, errorMessage) => {
onCompleted?.(streamErrorHandled && hasError ? false : hasError, errorMessage)
},
onData: (chunk, isFirstMessage, moreInfo) => {
if (moreInfo.errorMessage) {
streamErrorHandled = true
onError?.(moreInfo.errorMessage, moreInfo.errorCode)
return
}
onData?.(chunk, isFirstMessage, moreInfo)
},
onError: (errorMessage, errorCode) => {
streamErrorHandled = true
onError?.(errorMessage, errorCode)
},
onUnhandledEvent,
},
)
}
+10
View File
@@ -0,0 +1,10 @@
'use client'
import { useParams } from '@/next/navigation'
import { SkillDetailPage } from './detail/page'
export default function SkillDetailPageRoute() {
const { skillId } = useParams<{ skillId: string }>()
return <SkillDetailPage skillId={skillId} />
}

Some files were not shown because too many files have changed in this diff Show More