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>
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
@@ -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",
|
||||
|
||||
@@ -249,6 +249,7 @@ class TagType(StrEnum):
|
||||
KNOWLEDGE = "knowledge"
|
||||
APP = "app"
|
||||
SNIPPET = "snippet"
|
||||
SKILL = "skill"
|
||||
|
||||
|
||||
class DatasetMetadataType(StrEnum):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 [],
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__ = [
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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']),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 />
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||