Compare commits
84
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bb01984ed | ||
|
|
77e41a37ee | ||
|
|
79229b7ede | ||
|
|
36ae648a63 | ||
|
|
c409d531da | ||
|
|
511ca69e4e | ||
|
|
4c7e60d7e7 | ||
|
|
a9e163403d | ||
|
|
bc136b89f9 | ||
|
|
b1bd4f9a8b | ||
|
|
06d3927f05 | ||
|
|
5c3516cae8 | ||
|
|
1c18d8ddbd | ||
|
|
253dfe9351 | ||
|
|
2d3999e984 | ||
|
|
a431cc726d | ||
|
|
08493d2429 | ||
|
|
8dd0969006 | ||
|
|
2d80d3c35c | ||
|
|
4da764904c | ||
|
|
5c4f4fd1ef | ||
|
|
54843971ac | ||
|
|
5ec5d3aeb9 | ||
|
|
d661b53e49 | ||
|
|
fd1e777f85 | ||
|
|
e5f77ce185 | ||
|
|
99929d1c16 | ||
|
|
c699aa11db | ||
|
|
06cd0b56ac | ||
|
|
241a9e1fec | ||
|
|
3d79689fb5 | ||
|
|
9f050f7957 | ||
|
|
cb691d47d3 | ||
|
|
04f5ee58d0 | ||
|
|
f5cff724f4 | ||
|
|
1f4ccafdea | ||
|
|
163049f6ca | ||
|
|
65e7507ca3 | ||
|
|
ba157f9604 | ||
|
|
0dc913630e | ||
|
|
875cd30b1f | ||
|
|
aa4a32ae84 | ||
|
|
8573e14777 | ||
|
|
1855be234c | ||
|
|
9bb960ff12 | ||
|
|
1c14c7d467 | ||
|
|
61faec16ca | ||
|
|
57c836e692 | ||
|
|
626cc282b1 | ||
|
|
52624d54e3 | ||
|
|
5ce038ef92 | ||
|
|
30f4d4c0c6 | ||
|
|
510679a7d1 | ||
|
|
9237f2a14a | ||
|
|
bd178c7b29 | ||
|
|
d80947aa72 | ||
|
|
1618c37d26 | ||
|
|
701ab64462 | ||
|
|
b3298800e9 | ||
|
|
0f1c6b3f78 | ||
|
|
9b4b246aad | ||
|
|
1bd654a289 | ||
|
|
fc70329bdb | ||
|
|
cd8a82fbd4 | ||
|
|
550cb7eff5 | ||
|
|
2e748c16e9 | ||
|
|
577012b66d | ||
|
|
251c324180 | ||
|
|
865a618fd5 | ||
|
|
991116990a | ||
|
|
eb5d1da0e8 | ||
|
|
dce3b7a7fc | ||
|
|
be386aba3b | ||
|
|
02e51e7d7c | ||
|
|
96b6d4f2c0 | ||
|
|
4c84c5957d | ||
|
|
34613ecdc5 | ||
|
|
6a14245401 | ||
|
|
a758ca2aef | ||
|
|
9e60d4e213 | ||
|
|
ef29c8442c | ||
|
|
a1b45415ac | ||
|
|
7fc46d75bd | ||
|
|
953a4ef0ca |
@@ -144,6 +144,7 @@ from .workspace import (
|
||||
models,
|
||||
plugin,
|
||||
rbac,
|
||||
skills,
|
||||
snippets,
|
||||
tool_providers,
|
||||
trigger_providers,
|
||||
@@ -225,6 +226,7 @@ __all__ = [
|
||||
"saved_message",
|
||||
"setup",
|
||||
"site",
|
||||
"skills",
|
||||
"snippet_workflow",
|
||||
"snippet_workflow_draft_variable",
|
||||
"snippets",
|
||||
|
||||
@@ -59,7 +59,7 @@ class TagBindingRemovePayload(BaseModel):
|
||||
|
||||
|
||||
class TagListQueryParam(BaseModel):
|
||||
type: Literal["knowledge", "app", "snippet", ""] = Field("", description="Tag type filter")
|
||||
type: TagType | Literal[""] = Field("", description="Tag type filter")
|
||||
keyword: str | None = Field(None, description="Search keyword")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,834 @@
|
||||
"""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 controllers.common.fields import BinaryFileResponse
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
from fields.base import ResponseModel
|
||||
from libs import helper
|
||||
from libs.helper import dump_response
|
||||
from libs.login import login_required
|
||||
from models.account import Account
|
||||
from services.skill_management_service import (
|
||||
SkillAssistMessagePayload,
|
||||
SkillCreatePayload,
|
||||
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 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
|
||||
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 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,
|
||||
SkillDraftFileOperationPayload,
|
||||
SkillDraftTreePayload,
|
||||
SkillPublishPayload,
|
||||
SkillRestorePayload,
|
||||
SkillVersionUpdatePayload,
|
||||
SkillDeletePayload,
|
||||
SkillFileQuery,
|
||||
AgentSkillBindingsPayload,
|
||||
)
|
||||
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
SkillResponse,
|
||||
SkillFileResponse,
|
||||
SkillFilePreviewResponse,
|
||||
SkillFileUploadResponse,
|
||||
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
|
||||
|
||||
|
||||
@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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str):
|
||||
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().list_skills(
|
||||
tenant_id=current_tenant_id,
|
||||
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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
try:
|
||||
payload = SkillCreatePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().create_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
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
|
||||
|
||||
result = SkillManagementService().upload_file(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
filename=file.filename,
|
||||
content=file.stream.read(),
|
||||
mime_type=file.mimetype,
|
||||
)
|
||||
return dump_response(SkillFileUploadResponse, result), 201
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/tags")
|
||||
class WorkspaceSkillTagsApi(Resource):
|
||||
@console_ns.response(200, "Workspace Skill tags", console_ns.models[SkillTagListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str):
|
||||
result = SkillManagementService().list_tags(tenant_id=current_tenant_id)
|
||||
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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
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().import_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().get_skill(tenant_id=current_tenant_id, 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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillMetadataPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().update_metadata(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
payload = SkillDeletePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().delete_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().duplicate_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().pull_published_archive(tenant_id=current_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 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")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillAssistMessagePayload.model_validate(console_ns.payload or {})
|
||||
response = SkillManagementService().create_assistant_action_stream(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
user_id=current_user.id,
|
||||
message=payload.message,
|
||||
attachments=payload.attachments,
|
||||
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")
|
||||
class WorkspaceSkillFilesApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SkillDraftFileOperationPayload.__name__])
|
||||
@console_ns.response(200, "Draft file operation applied", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillDraftFileOperationPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().apply_draft_file_operation(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillDraftTreePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().replace_draft_tree(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
query = SkillFileQuery.model_validate(
|
||||
{
|
||||
"path": request.args.get("path"),
|
||||
"version_id": request.args.get("version_id"),
|
||||
}
|
||||
)
|
||||
result = SkillManagementService().preview_file(
|
||||
tenant_id=current_tenant_id,
|
||||
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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
query = SkillFileQuery.model_validate(
|
||||
{
|
||||
"path": request.args.get("path"),
|
||||
"version_id": request.args.get("version_id"),
|
||||
}
|
||||
)
|
||||
result = SkillManagementService().pull_file(
|
||||
tenant_id=current_tenant_id,
|
||||
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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillPublishPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().publish_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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", console_ns.models[SkillVersionResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillRestorePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().restore_version(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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>/references")
|
||||
class WorkspaceSkillReferencesApi(Resource):
|
||||
@console_ns.response(200, "Skill references", console_ns.models[SkillReferenceListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().list_skill_references(tenant_id=current_tenant_id, 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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().list_versions(tenant_id=current_tenant_id, 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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str, version_id: str):
|
||||
try:
|
||||
result = SkillManagementService().get_version(
|
||||
tenant_id=current_tenant_id,
|
||||
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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, skill_id: str, version_id: str):
|
||||
try:
|
||||
payload = SkillVersionUpdatePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().update_version(
|
||||
tenant_id=current_tenant_id,
|
||||
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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, current_user: Account, skill_id: str, version_id: str):
|
||||
try:
|
||||
result = SkillManagementService().delete_version(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, agent_id: str):
|
||||
result = SkillManagementService().list_agent_bindings(tenant_id=current_tenant_id, 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__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, current_user: Account, agent_id: str):
|
||||
try:
|
||||
payload = AgentSkillBindingsPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().replace_agent_bindings(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.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",
|
||||
]
|
||||
@@ -23,6 +23,7 @@ from .knowledge import retrieval as _knowledge_retrieval
|
||||
from .plugin import agent_config as _agent_config
|
||||
from .plugin import agent_drive as _agent_drive
|
||||
from .plugin import plugin as _plugin
|
||||
from .plugin import skills as _skills
|
||||
from .workspace import workspace as _workspace
|
||||
|
||||
api.add_namespace(inner_api_ns)
|
||||
@@ -36,6 +37,7 @@ __all__ = [
|
||||
"_mail",
|
||||
"_plugin",
|
||||
"_runtime_credentials",
|
||||
"_skills",
|
||||
"_workspace",
|
||||
"api",
|
||||
"bp",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""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 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")
|
||||
def get(self, skill_id: str):
|
||||
try:
|
||||
query = _target_query_from_request()
|
||||
result = SkillManagementService().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"]
|
||||
@@ -43,6 +43,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
|
||||
@@ -125,14 +126,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)
|
||||
|
||||
request = self._request_builder.build_for_agent_app(
|
||||
|
||||
@@ -38,6 +38,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,
|
||||
@@ -206,14 +207,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)
|
||||
|
||||
@@ -883,11 +892,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:
|
||||
@@ -905,12 +919,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.
|
||||
|
||||
@@ -927,8 +963,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]] = [
|
||||
{
|
||||
@@ -965,15 +1016,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,
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
"""add workspace skill management
|
||||
|
||||
Revision ID: a4f8d2c9e1b0
|
||||
Revises: 6f5a9c2d8e1b
|
||||
Create Date: 2026-07-09 12:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
from models.types import StringUUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a4f8d2c9e1b0"
|
||||
down_revision = "6f5a9c2d8e1b"
|
||||
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:
|
||||
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"),
|
||||
)
|
||||
op.create_index("skills_tenant_updated_at_idx", "skills", ["tenant_id", "updated_at"])
|
||||
|
||||
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"),
|
||||
)
|
||||
op.create_index("skill_draft_files_skill_path_idx", "skill_draft_files", ["skill_id", "path"])
|
||||
|
||||
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"),
|
||||
)
|
||||
op.create_index("skill_versions_skill_created_at_idx", "skill_versions", ["skill_id", "created_at"])
|
||||
|
||||
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"),
|
||||
)
|
||||
op.create_index("agent_skill_bindings_skill_idx", "agent_skill_bindings", ["tenant_id", "skill_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("agent_skill_bindings_skill_idx", table_name="agent_skill_bindings")
|
||||
op.drop_table("agent_skill_bindings")
|
||||
op.drop_index("skill_versions_skill_created_at_idx", table_name="skill_versions")
|
||||
op.drop_table("skill_versions")
|
||||
op.drop_index("skill_draft_files_skill_path_idx", table_name="skill_draft_files")
|
||||
op.drop_table("skill_draft_files")
|
||||
op.drop_index("skills_tenant_updated_at_idx", table_name="skills")
|
||||
op.drop_table("skills")
|
||||
+110
-23
@@ -18,31 +18,109 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _is_pg(conn) -> bool:
|
||||
return conn.dialect.name == "postgresql"
|
||||
|
||||
|
||||
def _uuid_column(name: str, *, nullable: bool = False, primary_key: bool = False) -> sa.Column:
|
||||
kwargs = {"nullable": nullable, "primary_key": primary_key}
|
||||
if primary_key and _is_pg(op.get_bind()):
|
||||
kwargs["server_default"] = sa.text("uuidv7()")
|
||||
return sa.Column(name, models.types.StringUUID(), **kwargs)
|
||||
|
||||
|
||||
def _has_table(table_name: str) -> bool:
|
||||
return sa.inspect(op.get_bind()).has_table(table_name)
|
||||
|
||||
|
||||
def _has_column(table_name: str, column_name: str) -> bool:
|
||||
return any(
|
||||
column["name"] == column_name for column in sa.inspect(op.get_bind()).get_columns(table_name)
|
||||
)
|
||||
|
||||
|
||||
def _has_unique_constraint(table_name: str, constraint_name: str) -> bool:
|
||||
return any(
|
||||
constraint["name"] == constraint_name
|
||||
for constraint in sa.inspect(op.get_bind()).get_unique_constraints(table_name)
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
if not _has_table("agent_debug_conversations"):
|
||||
op.create_table(
|
||||
"agent_debug_conversations",
|
||||
_uuid_column("id", primary_key=True),
|
||||
sa.Column("tenant_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("agent_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("app_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("account_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("conversation_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column(
|
||||
"draft_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default=sa.text("'debug_build'"),
|
||||
),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("agent_debug_conversation_pkey")),
|
||||
sa.UniqueConstraint(
|
||||
"tenant_id",
|
||||
"agent_id",
|
||||
"account_id",
|
||||
"draft_type",
|
||||
name=op.f("agent_debug_conversation_agent_account_draft_type_unique"),
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"agent_debug_conversation_conversation_idx",
|
||||
"agent_debug_conversations",
|
||||
["conversation_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"agent_debug_conversation_account_idx",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "account_id"],
|
||||
)
|
||||
return
|
||||
|
||||
# Existing pointers have always represented Build chat because the Agent
|
||||
# detail API exposes them as ``debug_conversation_id`` for that surface.
|
||||
op.add_column(
|
||||
if not _has_column("agent_debug_conversations", "draft_type"):
|
||||
op.add_column(
|
||||
"agent_debug_conversations",
|
||||
sa.Column(
|
||||
"draft_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default=sa.text("'debug_build'"),
|
||||
),
|
||||
)
|
||||
if _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
sa.Column(
|
||||
"draft_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default=sa.text("'debug_build'"),
|
||||
),
|
||||
)
|
||||
op.drop_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
):
|
||||
op.drop_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
if not _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id", "draft_type"],
|
||||
)
|
||||
):
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id", "draft_type"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
if not _has_table("agent_debug_conversations"):
|
||||
return
|
||||
|
||||
debug_conversations = sa.table(
|
||||
"agent_debug_conversations",
|
||||
sa.column("tenant_id", models.types.StringUUID()),
|
||||
@@ -64,14 +142,23 @@ def downgrade():
|
||||
),
|
||||
)
|
||||
)
|
||||
op.drop_constraint(
|
||||
if _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
):
|
||||
op.drop_constraint(
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
if not _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id"],
|
||||
)
|
||||
op.drop_column("agent_debug_conversations", "draft_type")
|
||||
):
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id"],
|
||||
)
|
||||
if _has_column("agent_debug_conversations", "draft_type"):
|
||||
op.drop_column("agent_debug_conversations", "draft_type")
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
"""merge skill and agent debug conversation heads
|
||||
|
||||
Revision ID: e9f4a1b2c3d5
|
||||
Revises: a4f8d2c9e1b0, d2825e7b9c10
|
||||
Create Date: 2026-07-23 15:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "e9f4a1b2c3d5"
|
||||
down_revision = ("a4f8d2c9e1b0", "d2825e7b9c10")
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@@ -113,6 +113,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
|
||||
@@ -173,6 +174,7 @@ __all__ = [
|
||||
"AgentRuntimeSessionOwnerType",
|
||||
"AgentRuntimeSessionStatus",
|
||||
"AgentScope",
|
||||
"AgentSkillBinding",
|
||||
"AgentSource",
|
||||
"AgentStatus",
|
||||
"ApiRequest",
|
||||
@@ -246,6 +248,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):
|
||||
|
||||
+1
-1
@@ -2667,7 +2667,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,164 @@
|
||||
"""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]
|
||||
|
||||
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)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentSkillBinding",
|
||||
"Skill",
|
||||
"SkillDraftFile",
|
||||
"SkillFileKind",
|
||||
"SkillFileStorage",
|
||||
"SkillVersion",
|
||||
"SkillVersionManifest",
|
||||
"SkillVersionManifestFile",
|
||||
]
|
||||
@@ -9551,7 +9551,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 | No | string, <br>**Available values:** "", "app", "knowledge", "snippet" |
|
||||
| type | query | Tag type filter | No | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
@@ -10087,6 +10087,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**
|
||||
|
||||
@@ -11988,6 +12020,341 @@ 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> |
|
||||
|
||||
### [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 | **application/json**: [SkillVersionResponse](#skillversionresponse)<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/tool-labels
|
||||
#### Responses
|
||||
|
||||
@@ -14709,6 +15076,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 |
|
||||
|
||||
#### AgentSkillRefConfig
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21665,6 +22063,186 @@ 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 |
|
||||
|
||||
#### SkillAssistMessagePayload
|
||||
|
||||
One user message and optional uploaded context for the read-only Skill Authoring assistant.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| attachments | [ [SkillAssistAttachmentPayload](#skillassistattachmentpayload) ] | | No |
|
||||
| message | string | | Yes |
|
||||
| model | [SkillAssistModelPayload](#skillassistmodelpayload) | | 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 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_version_id | string | | 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 |
|
||||
|
||||
#### 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 |
|
||||
|
||||
#### 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 |
|
||||
|
||||
#### SkillManifest
|
||||
|
||||
Validated metadata extracted from a Skill package.
|
||||
@@ -21678,6 +22256,91 @@ Validated metadata extracted from a Skill package.
|
||||
| name | string | | Yes |
|
||||
| size | integer | | Yes |
|
||||
|
||||
#### 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_version_id | string | | 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 |
|
||||
|
||||
#### SkillToolInferenceResult
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21686,6 +22349,60 @@ Validated metadata extracted from a Skill package.
|
||||
| inferable | boolean | | Yes |
|
||||
| reason | string | | No |
|
||||
|
||||
#### 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 |
|
||||
@@ -22153,7 +22870,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"` | No |
|
||||
| type | [TagType](#tagtype)<br>string | Tag type filter | No |
|
||||
|
||||
#### TagListResponse
|
||||
|
||||
@@ -24339,6 +25056,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 |
|
||||
|
||||
@@ -1599,7 +1599,9 @@ class TenantService:
|
||||
return updated_accounts
|
||||
|
||||
@staticmethod
|
||||
def iter_member_account_id_batches(tenant_id: str, batch_size: int, *, session: Session) -> Iterator[list[str]]:
|
||||
def iter_member_account_id_batches(
|
||||
tenant_id: str, batch_size: int, *, session: Session
|
||||
) -> Iterator[list[str]]:
|
||||
"""Yield workspace member account ids in bounded, ordered batches."""
|
||||
offset = 0
|
||||
while True:
|
||||
|
||||
@@ -19,12 +19,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
|
||||
|
||||
# Bounds — generous but finite so a hostile upload can't exhaust memory/disk.
|
||||
_MAX_ARCHIVE_BYTES = 50 * 1024 * 1024
|
||||
@@ -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 in agent drive."""
|
||||
@@ -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:
|
||||
@@ -280,13 +313,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
|
||||
|
||||
@@ -46,6 +46,7 @@ from models.tools import ToolFile
|
||||
from services.agent.config_skill_normalize_service import ConfigSkillNormalizeService
|
||||
from services.agent.skill_package_service import SkillPackageError
|
||||
from services.agent_drive_service import DriveFileRef
|
||||
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
|
||||
|
||||
|
||||
class AgentConfigVersionKind(StrEnum):
|
||||
@@ -98,6 +99,7 @@ class ConfigPushPayload(BaseModel):
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AgentConfigTarget:
|
||||
tenant_id: str
|
||||
agent_id: str
|
||||
version_id: str
|
||||
kind: AgentConfigVersionKind
|
||||
@@ -146,6 +148,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,
|
||||
@@ -191,7 +194,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),
|
||||
}
|
||||
|
||||
def list_files(
|
||||
@@ -233,10 +236,27 @@ 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)
|
||||
payload, mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
|
||||
return ConfigDownload(filename=f"{skill.name}.zip", mime_type=mime_type or "application/zip", payload=payload)
|
||||
try:
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
file_id = self._available_skill_file_id(skill)
|
||||
payload, mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
|
||||
return ConfigDownload(
|
||||
filename=f"{skill.name}.zip",
|
||||
mime_type=mime_type or "application/zip",
|
||||
payload=payload,
|
||||
)
|
||||
except AgentConfigServiceError as exc:
|
||||
if exc.code != "config_skill_not_found":
|
||||
raise
|
||||
try:
|
||||
result = SkillManagementService().pull_runtime_agent_skill(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
name=name,
|
||||
)
|
||||
return ConfigDownload(filename=result.filename, mime_type=result.mime_type, payload=result.payload)
|
||||
except SkillManagementServiceError as exc:
|
||||
raise AgentConfigServiceError("config_skill_not_found", "config skill not found", status_code=404) from exc
|
||||
|
||||
def download_skill_url(
|
||||
self,
|
||||
@@ -279,9 +299,45 @@ 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,
|
||||
)
|
||||
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:
|
||||
@@ -291,7 +347,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,
|
||||
@@ -839,6 +895,7 @@ class AgentConfigService:
|
||||
status_code=404,
|
||||
)
|
||||
return AgentConfigTarget(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
version_id=version.id,
|
||||
kind=config_version_kind,
|
||||
@@ -1133,9 +1190,7 @@ class AgentConfigService:
|
||||
return {
|
||||
"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]
|
||||
},
|
||||
"skills": {"items": AgentConfigService._skill_items_for_target(target)},
|
||||
"files": {
|
||||
"items": [
|
||||
AgentConfigService._serialize_file_item(file_ref) for file_ref in target.agent_soul.config_files
|
||||
@@ -1145,6 +1200,20 @@ class AgentConfigService:
|
||||
"note": target.agent_soul.config_note,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _skill_items_for_target(target: AgentConfigTarget) -> list[dict[str, object]]:
|
||||
items = [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
|
||||
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,
|
||||
):
|
||||
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 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ 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
|
||||
@@ -282,5 +283,13 @@ 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")
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from inspect import unwrap
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.workspace.skills import (
|
||||
WorkspaceAgentSkillBindingsApi,
|
||||
WorkspaceSkillAssistMessageApi,
|
||||
WorkspaceSkillFilesApi,
|
||||
WorkspaceSkillsApi,
|
||||
WorkspaceSkillTagsApi,
|
||||
WorkspaceSkillVersionApi,
|
||||
)
|
||||
from models.account import Account
|
||||
from services.skill_management_service import SkillAssistAttachmentPayload, SkillManagementServiceError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app() -> Flask:
|
||||
flask_app = Flask("test_workspace_skills")
|
||||
flask_app.config["TESTING"] = True
|
||||
return flask_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def current_user() -> Account:
|
||||
user = Account(name="Test User", email="test@example.com")
|
||||
user.id = "user-1"
|
||||
return user
|
||||
|
||||
|
||||
def _skill_detail() -> dict:
|
||||
return {
|
||||
"id": "skill-1",
|
||||
"name": "finance-sop",
|
||||
"display_name": "Finance SOP",
|
||||
"icon": "📄",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"name_manually_edited": False,
|
||||
"visibility": "workspace",
|
||||
"latest_published_version_id": None,
|
||||
"reference_count": 0,
|
||||
"created_by": "user-1",
|
||||
"created_by_name": "Test User",
|
||||
"updated_by": "user-1",
|
||||
"updated_by_name": "Test User",
|
||||
"created_at": 1,
|
||||
"updated_at": 1,
|
||||
"files": [
|
||||
{
|
||||
"id": "file-1",
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"storage": "text",
|
||||
"mime_type": "text/markdown",
|
||||
"content": "---\nname: finance-sop\n---\n# Body",
|
||||
"tool_file_id": None,
|
||||
"size": 32,
|
||||
"hash": "hash",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_create_skill_validates_payload_and_returns_detail(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillsApi()
|
||||
method = unwrap(api.post)
|
||||
service = MagicMock()
|
||||
service.create_skill.return_value = _skill_detail()
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="POST"),
|
||||
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value={}),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload, status = method(api, "tenant-1", current_user)
|
||||
|
||||
assert status == 201
|
||||
assert payload["id"] == "skill-1"
|
||||
assert payload["files"][0]["path"] == "SKILL.md"
|
||||
service.create_skill.assert_called_once()
|
||||
assert service.create_skill.call_args.kwargs["tenant_id"] == "tenant-1"
|
||||
assert service.create_skill.call_args.kwargs["user_id"] == "user-1"
|
||||
|
||||
|
||||
def test_list_skills_uses_default_pagination_when_query_omits_page_and_limit(app: Flask) -> None:
|
||||
api = WorkspaceSkillsApi()
|
||||
method = unwrap(api.get)
|
||||
service = MagicMock()
|
||||
service.list_skills.return_value = {
|
||||
"data": [],
|
||||
"has_more": False,
|
||||
"limit": 20,
|
||||
"page": 1,
|
||||
"total": 0,
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/?keyword=finance&tag=ops&tag=", method="GET"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1")
|
||||
|
||||
assert payload == {
|
||||
"data": [],
|
||||
"has_more": False,
|
||||
"limit": 20,
|
||||
"page": 1,
|
||||
"total": 0,
|
||||
}
|
||||
service.list_skills.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
keyword="finance",
|
||||
page=1,
|
||||
limit=20,
|
||||
tags=["ops"],
|
||||
)
|
||||
|
||||
|
||||
def test_get_agent_skill_bindings_returns_card_data(app: Flask) -> None:
|
||||
api = WorkspaceAgentSkillBindingsApi()
|
||||
method = unwrap(api.get)
|
||||
service = MagicMock()
|
||||
service.list_agent_bindings.return_value = {
|
||||
"agent_id": "agent-1",
|
||||
"skill_ids": ["skill-1"],
|
||||
"data": [
|
||||
{
|
||||
"id": "skill-1",
|
||||
"priority": 0,
|
||||
"name": "finance-sop",
|
||||
"display_name": "Finance SOP",
|
||||
"icon": "📄",
|
||||
"description": "Handle finance.",
|
||||
"tags": ["Finance"],
|
||||
"status": "published",
|
||||
"file_count": 2,
|
||||
"latest_published_version_id": "version-1",
|
||||
"latest_published_at": 123,
|
||||
"updated_at": 124,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", "agent-1")
|
||||
|
||||
assert payload["skill_ids"] == ["skill-1"]
|
||||
assert payload["data"][0]["display_name"] == "Finance SOP"
|
||||
assert payload["data"][0]["file_count"] == 2
|
||||
service.list_agent_bindings.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
|
||||
|
||||
def test_patch_skill_file_operation_validates_payload_and_returns_detail(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillFilesApi()
|
||||
method = unwrap(api.patch)
|
||||
service = MagicMock()
|
||||
service.apply_draft_file_operation.return_value = _skill_detail()
|
||||
request_payload = {
|
||||
"operation": "upsert_text",
|
||||
"path": "references/policy.md",
|
||||
"content": "Policy",
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="PATCH"),
|
||||
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=request_payload),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", current_user, "skill-1")
|
||||
|
||||
assert payload["id"] == "skill-1"
|
||||
service.apply_draft_file_operation.assert_called_once()
|
||||
call = service.apply_draft_file_operation.call_args.kwargs
|
||||
assert call["tenant_id"] == "tenant-1"
|
||||
assert call["user_id"] == "user-1"
|
||||
assert call["skill_id"] == "skill-1"
|
||||
assert call["payload"].operation == "upsert_text"
|
||||
|
||||
|
||||
def test_patch_skill_file_operation_returns_error_details(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillFilesApi()
|
||||
method = unwrap(api.patch)
|
||||
service = MagicMock()
|
||||
service.apply_draft_file_operation.side_effect = SkillManagementServiceError(
|
||||
"missing_skill_name",
|
||||
"SKILL.md frontmatter name is required",
|
||||
details={"path": "SKILL.md", "field": "name", "line": 2},
|
||||
)
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="PATCH"),
|
||||
patch.object(
|
||||
type(console_ns),
|
||||
"payload",
|
||||
new_callable=PropertyMock,
|
||||
return_value={"operation": "delete", "path": "SKILL.md"},
|
||||
),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload, status = method(api, "tenant-1", current_user, "skill-1")
|
||||
|
||||
assert status == 400
|
||||
assert payload == {
|
||||
"code": "missing_skill_name",
|
||||
"message": "SKILL.md frontmatter name is required",
|
||||
"details": {"path": "SKILL.md", "field": "name", "line": 2},
|
||||
}
|
||||
|
||||
|
||||
def test_list_skill_tags_returns_filter_options(app: Flask) -> None:
|
||||
api = WorkspaceSkillTagsApi()
|
||||
method = unwrap(api.get)
|
||||
service = MagicMock()
|
||||
service.list_tags.return_value = {"data": [{"tag": "finance", "count": 2}]}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1")
|
||||
|
||||
assert payload == {"data": [{"tag": "finance", "count": 2}]}
|
||||
service.list_tags.assert_called_once_with(tenant_id="tenant-1")
|
||||
|
||||
|
||||
def test_get_skill_version_returns_version_detail(app: Flask) -> None:
|
||||
api = WorkspaceSkillVersionApi()
|
||||
method = unwrap(api.get)
|
||||
service = MagicMock()
|
||||
service.get_version.return_value = {
|
||||
"id": "version-1",
|
||||
"skill_id": "skill-1",
|
||||
"version_number": 1,
|
||||
"version_name": "Initial finance policy",
|
||||
"publish_note": "Initial finance policy",
|
||||
"hash_code": "hash-code",
|
||||
"archive_size": 123,
|
||||
"published_by": "user-1",
|
||||
"published_by_name": "Li Wei",
|
||||
"is_latest": True,
|
||||
"created_at": 1,
|
||||
"files": [
|
||||
{
|
||||
"id": None,
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"storage": "text",
|
||||
"mime_type": "text/markdown",
|
||||
"content": "# Version",
|
||||
"tool_file_id": None,
|
||||
"size": 9,
|
||||
"hash": "file-hash",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", "skill-1", "version-1")
|
||||
|
||||
assert payload["files"][0]["content"] == "# Version"
|
||||
service.get_version.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
skill_id="skill-1",
|
||||
version_id="version-1",
|
||||
)
|
||||
|
||||
|
||||
def test_patch_skill_version_renames_version(app: Flask) -> None:
|
||||
api = WorkspaceSkillVersionApi()
|
||||
method = unwrap(api.patch)
|
||||
service = MagicMock()
|
||||
service.update_version.return_value = {
|
||||
"id": "version-1",
|
||||
"skill_id": "skill-1",
|
||||
"version_number": 1,
|
||||
"version_name": "Approval threshold",
|
||||
"publish_note": "",
|
||||
"hash_code": "hash-code",
|
||||
"archive_size": 123,
|
||||
"published_by": "user-1",
|
||||
"published_by_name": "Li Wei",
|
||||
"is_latest": True,
|
||||
"created_at": 1,
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="PATCH"),
|
||||
patch.object(
|
||||
type(console_ns),
|
||||
"payload",
|
||||
new_callable=PropertyMock,
|
||||
return_value={"version_name": "Approval threshold"},
|
||||
),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", "skill-1", "version-1")
|
||||
|
||||
assert payload["version_name"] == "Approval threshold"
|
||||
service.update_version.assert_called_once()
|
||||
assert service.update_version.call_args.kwargs["payload"].version_name == "Approval threshold"
|
||||
|
||||
|
||||
def test_delete_skill_version_returns_new_latest(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillVersionApi()
|
||||
method = unwrap(api.delete)
|
||||
service = MagicMock()
|
||||
service.delete_version.return_value = {
|
||||
"id": "version-2",
|
||||
"deleted": True,
|
||||
"latest_published_version_id": "version-1",
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="DELETE"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", current_user, "skill-1", "version-2")
|
||||
|
||||
assert payload == {"id": "version-2", "deleted": True, "latest_published_version_id": "version-1"}
|
||||
service.delete_version.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
skill_id="skill-1",
|
||||
version_id="version-2",
|
||||
)
|
||||
|
||||
|
||||
def test_skill_assistant_runs_agent_app_stream(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillAssistMessageApi()
|
||||
method = unwrap(api.post)
|
||||
service = MagicMock()
|
||||
assistant_app = MagicMock()
|
||||
assistant_app.id = "assistant-app-1"
|
||||
service.get_or_create_assistant_app.return_value = (assistant_app, "<skill_draft>draft</skill_draft>")
|
||||
app_model = MagicMock()
|
||||
app_response = MagicMock()
|
||||
compact_response = MagicMock()
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="POST"),
|
||||
patch.object(
|
||||
type(console_ns),
|
||||
"payload",
|
||||
new_callable=PropertyMock,
|
||||
return_value={
|
||||
"attachments": [
|
||||
{
|
||||
"tool_file_id": "tool-file-1",
|
||||
"name": "requirements.md",
|
||||
"mime_type": "text/markdown",
|
||||
"size": 128,
|
||||
}
|
||||
],
|
||||
"message": "Create an approval checklist.",
|
||||
},
|
||||
),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
patch(
|
||||
"controllers.console.workspace.skills.db.session",
|
||||
return_value=MagicMock(get=MagicMock(return_value=app_model)),
|
||||
),
|
||||
patch("controllers.console.workspace.skills.AppGenerateService.generate", return_value=app_response),
|
||||
patch("controllers.console.workspace.skills.helper.compact_generate_response", return_value=compact_response),
|
||||
):
|
||||
response = method(api, "tenant-1", current_user, "skill-1")
|
||||
|
||||
assert response is compact_response
|
||||
service.get_or_create_assistant_app.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
skill_id="skill-1",
|
||||
user_id="user-1",
|
||||
attachments=[
|
||||
SkillAssistAttachmentPayload(
|
||||
tool_file_id="tool-file-1",
|
||||
name="requirements.md",
|
||||
mime_type="text/markdown",
|
||||
size=128,
|
||||
)
|
||||
],
|
||||
message="Create an approval checklist.",
|
||||
model_payload=None,
|
||||
)
|
||||
@@ -6,19 +6,13 @@ from unittest.mock import ANY, MagicMock, patch
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from jwt import InvalidTokenError
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
from werkzeug.exceptions import Unauthorized
|
||||
|
||||
import services.errors.account
|
||||
from controllers.console import wraps as console_wraps
|
||||
from controllers.web.login import EmailCodeLoginApi, EmailCodeLoginSendEmailApi, LoginApi, LoginStatusApi, LogoutApi
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from models.model import DifySetup
|
||||
from services.entities.auth_entities import LoginFailureReason
|
||||
|
||||
pytestmark = pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
||||
|
||||
|
||||
def encode_code(code: str) -> str:
|
||||
return base64.b64encode(code.encode("utf-8")).decode()
|
||||
@@ -39,27 +33,17 @@ def app():
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_wraps(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
sqlite_engine: Engine,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
def _patch_wraps():
|
||||
wraps_features = SimpleNamespace(enable_email_password_login=True)
|
||||
console_dify = SimpleNamespace(ENTERPRISE_ENABLED=True, DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
web_dify = SimpleNamespace(ENTERPRISE_ENABLED=True)
|
||||
sqlite_session.add(DifySetup(version="test"))
|
||||
sqlite_session.commit()
|
||||
console_wraps._is_setup_completed.reset_success()
|
||||
session_registry = scoped_session(sessionmaker(bind=sqlite_engine, expire_on_commit=False))
|
||||
monkeypatch.setattr(console_wraps.db, "session", session_registry)
|
||||
with (
|
||||
patch("controllers.console.wraps.db") as mock_db,
|
||||
patch("controllers.console.wraps.dify_config", console_dify),
|
||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
|
||||
patch("controllers.web.login.dify_config", web_dify),
|
||||
):
|
||||
yield
|
||||
session_registry.remove()
|
||||
console_wraps._is_setup_completed.reset_success()
|
||||
|
||||
|
||||
class TestEmailCodeLoginSendEmailApi:
|
||||
|
||||
@@ -7,6 +7,7 @@ from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
@@ -29,6 +30,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 *, tenant_id, agent_id: [],
|
||||
)
|
||||
|
||||
|
||||
def _exec_ctx() -> DifyExecutionContextLayerConfig:
|
||||
return DifyExecutionContextLayerConfig(
|
||||
tenant_id="tenant-1",
|
||||
@@ -514,6 +523,33 @@ 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 *, tenant_id, agent_id: [
|
||||
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(
|
||||
credentials_provider=_FakeCredentialsProvider(),
|
||||
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
|
||||
@@ -40,6 +41,14 @@ 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 *, tenant_id, agent_id: [],
|
||||
)
|
||||
|
||||
|
||||
class FakeCredentialsProvider:
|
||||
def fetch(self, provider_name: str, model_name: str) -> dict[str, object]:
|
||||
assert provider_name == "openai"
|
||||
@@ -1420,6 +1429,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
|
||||
|
||||
@@ -1505,6 +1538,33 @@ def test_workflow_run_request_contains_config_layer():
|
||||
assert any(spec.name == DIFY_CONFIG_LAYER_ID and spec.type == "dify.config" for spec in specs)
|
||||
|
||||
|
||||
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 *, tenant_id, agent_id: [
|
||||
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(credentials_provider=FakeCredentialsProvider()).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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -20,7 +20,7 @@ _AGENT_ID = "22222222-2222-2222-2222-222222222222"
|
||||
_USER_ID = "33333333-3333-3333-3333-333333333333"
|
||||
|
||||
_SKILL_MD = b"""---
|
||||
name: PDF Toolkit
|
||||
name: pdf-toolkit
|
||||
description: Work with PDFs.
|
||||
---
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest(
|
||||
assert skill_row.is_skill is True
|
||||
assert skill_row.skill_metadata is not None
|
||||
skill_metadata = DriveSkillMetadata.model_validate_json(skill_row.skill_metadata)
|
||||
assert skill_metadata.name == "PDF Toolkit"
|
||||
assert skill_metadata.name == "pdf-toolkit"
|
||||
assert skill_metadata.manifest_files == ["SKILL.md", "scripts/run.py"]
|
||||
assert archive_row.file_kind == AgentDriveFileKind.TOOL_FILE
|
||||
assert archive_row.file_id == archive_tool_file.id
|
||||
@@ -132,7 +132,7 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest(
|
||||
# The returned upload response carries only the drive-derived fields the UI needs.
|
||||
skill = result["skill"]
|
||||
assert skill["path"] == "pdf-toolkit"
|
||||
assert skill["name"] == "PDF Toolkit"
|
||||
assert skill["name"] == "pdf-toolkit"
|
||||
assert skill["archive_key"] == "pdf-toolkit/.DIFY-SKILL-FULL.zip"
|
||||
assert skill["skill_md_key"] == "pdf-toolkit/SKILL.md"
|
||||
assert result["manifest"]["entry_path"] == "SKILL.md"
|
||||
|
||||
@@ -7,16 +7,14 @@ update_features persists those flags as a new app_model_config version without
|
||||
touching model / prompt / agent_mode.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.account import Account
|
||||
from models.model import App, AppMode, AppModelConfig
|
||||
from services.agent_app_feature_service import AgentAppFeatureConfigService
|
||||
|
||||
TENANT_ID = "11111111-1111-1111-1111-111111111111"
|
||||
APP_ID = "22222222-2222-2222-2222-222222222222"
|
||||
ACCOUNT_ID = "33333333-3333-3333-3333-333333333333"
|
||||
|
||||
|
||||
class TestValidateFeatures:
|
||||
@@ -73,47 +71,45 @@ class TestValidateFeatures:
|
||||
AgentAppFeatureConfigService.validate_features(TENANT_ID, {"suggested_questions": "nope"})
|
||||
|
||||
|
||||
class _FakeWriteSession:
|
||||
def __init__(self) -> None:
|
||||
self.added: list[Any] = []
|
||||
self.flushed = 0
|
||||
self.committed = 0
|
||||
|
||||
def add(self, obj: Any) -> None:
|
||||
self.added.append(obj)
|
||||
|
||||
def flush(self) -> None:
|
||||
self.flushed += 1
|
||||
|
||||
def commit(self) -> None:
|
||||
self.committed += 1
|
||||
|
||||
|
||||
class TestUpdateFeatures:
|
||||
@pytest.mark.parametrize("sqlite_session", [(Account, App, AppModelConfig)], indirect=True)
|
||||
def test_persists_new_app_model_config_version(self, sqlite_session: Session):
|
||||
app_model = App(
|
||||
id=APP_ID,
|
||||
tenant_id=TENANT_ID,
|
||||
name="Agent App",
|
||||
description="",
|
||||
mode=AppMode.AGENT,
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
max_active_requests=0,
|
||||
def test_persists_new_app_model_config_version(self):
|
||||
session = _FakeWriteSession()
|
||||
app_model = SimpleNamespace(
|
||||
tenant_id=TENANT_ID, id="app-1", app_model_config_id=None, updated_by=None, updated_at=None
|
||||
)
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
account.id = ACCOUNT_ID
|
||||
sqlite_session.add_all([account, app_model])
|
||||
sqlite_session.commit()
|
||||
account = SimpleNamespace(id="acct-1")
|
||||
|
||||
new_config = AgentAppFeatureConfigService.update_features(
|
||||
app_model=app_model,
|
||||
account=account,
|
||||
app_model=app_model, # type: ignore[arg-type]
|
||||
account=account, # type: ignore[arg-type]
|
||||
config={"opening_statement": "Hi!", "suggested_questions_after_answer": {"enabled": True}},
|
||||
session=sqlite_session,
|
||||
session=session,
|
||||
)
|
||||
assert not sqlite_session.in_transaction()
|
||||
|
||||
# New row carries the features but no Soul-owned model/prompt/agent_mode.
|
||||
assert new_config.app_id == APP_ID
|
||||
assert new_config.app_id == "app-1"
|
||||
assert new_config.opening_statement == "Hi!"
|
||||
assert new_config.model is None
|
||||
assert new_config.agent_mode is None
|
||||
# App is repointed at the new version and the write is committed.
|
||||
assert app_model.app_model_config_id == new_config.id
|
||||
assert app_model.updated_by == ACCOUNT_ID
|
||||
sqlite_session.expunge_all()
|
||||
persisted_config = sqlite_session.get(AppModelConfig, new_config.id)
|
||||
persisted_app = sqlite_session.get(App, APP_ID)
|
||||
assert persisted_config is not None
|
||||
assert persisted_config.opening_statement == "Hi!"
|
||||
assert persisted_config.model is None
|
||||
assert persisted_config.agent_mode is None
|
||||
assert persisted_app is not None
|
||||
assert persisted_app.app_model_config_id == new_config.id
|
||||
assert persisted_app.updated_by == ACCOUNT_ID
|
||||
assert app_model.updated_by == "acct-1"
|
||||
assert new_config in session.added
|
||||
assert session.flushed == 1
|
||||
assert session.committed == 1
|
||||
|
||||
@@ -63,6 +63,7 @@ def _target(
|
||||
) -> AgentConfigTarget:
|
||||
agent_soul = soul or _soul()
|
||||
return AgentConfigTarget(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
version_id=version_id,
|
||||
kind=kind,
|
||||
@@ -508,7 +509,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,
|
||||
@@ -557,7 +560,9 @@ def test_manifest_preserves_missing_config_assets_and_pull_rejects_them() -> Non
|
||||
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=soul)
|
||||
service = AgentConfigService()
|
||||
|
||||
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]
|
||||
@@ -606,6 +611,44 @@ 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_preview_skill_file_returns_text_preview() -> None:
|
||||
service = AgentConfigService()
|
||||
target = _target(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -188,7 +188,7 @@ export const get = oc
|
||||
path: '/agent/invite-options',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetAgentInviteOptionsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetAgentInviteOptionsQuery.optional() }))
|
||||
.output(zGetAgentInviteOptionsResponse)
|
||||
|
||||
export const inviteOptions = {
|
||||
@@ -1491,7 +1491,7 @@ export const get36 = oc
|
||||
path: '/agent',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetAgentQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetAgentQuery.optional() }))
|
||||
.output(zGetAgentResponse)
|
||||
|
||||
export const post21 = oc
|
||||
|
||||
@@ -997,11 +997,15 @@ export const zSandboxListResponse = z.object({
|
||||
* Validated metadata extracted from a Skill package.
|
||||
*/
|
||||
export const zSkillManifest = z.object({
|
||||
description: z.string(),
|
||||
description: z.string().min(1).max(1024),
|
||||
entry_path: z.string(),
|
||||
files: z.array(z.string()),
|
||||
hash: z.string(),
|
||||
name: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
@@ -2573,10 +2577,10 @@ export const zSelectInputConfig = z.object({
|
||||
})
|
||||
|
||||
export const zFormInputConfig = z.discriminatedUnion('type', [
|
||||
zParagraphInputConfig,
|
||||
zSelectInputConfig,
|
||||
zFileInputConfig,
|
||||
zFileListInputConfig,
|
||||
zParagraphInputConfig.extend({ type: z.literal('paragraph') }),
|
||||
zSelectInputConfig.extend({ type: z.literal('select') }),
|
||||
zFileInputConfig.extend({ type: z.literal('file') }),
|
||||
zFileListInputConfig.extend({ type: z.literal('file-list') }),
|
||||
])
|
||||
|
||||
/**
|
||||
@@ -2893,10 +2897,7 @@ export const zDeleteAgentByAgentIdApiKeysByApiKeyIdResponse = z.void()
|
||||
|
||||
export const zPostAgentByAgentIdAudioToTextBody = z.object({
|
||||
draft_type: z.enum(['debug_build', 'draft']).optional().default('draft'),
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
export const zPostAgentByAgentIdAudioToTextPath = z.object({
|
||||
@@ -3146,10 +3147,7 @@ export const zGetAgentByAgentIdConfigSkillsQuery = z.object({
|
||||
export const zGetAgentByAgentIdConfigSkillsResponse = zAgentConfigSkillListResponse
|
||||
|
||||
export const zPostAgentByAgentIdConfigSkillsUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
export const zPostAgentByAgentIdConfigSkillsUploadPath = z.object({
|
||||
@@ -3525,10 +3523,7 @@ export const zPostAgentByAgentIdSandboxFilesUploadPath = z.object({
|
||||
export const zPostAgentByAgentIdSandboxFilesUploadResponse = zSandboxUploadResponse
|
||||
|
||||
export const zPostAgentByAgentIdSkillsUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
export const zPostAgentByAgentIdSkillsUploadPath = z.object({
|
||||
|
||||
@@ -12,7 +12,7 @@ export const get = oc
|
||||
path: '/all-workspaces',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetAllWorkspacesQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetAllWorkspacesQuery.optional() }))
|
||||
.output(zGetAllWorkspacesResponse)
|
||||
|
||||
export const allWorkspaces = {
|
||||
|
||||
@@ -567,7 +567,7 @@ export const get2 = oc
|
||||
summary: 'Return the lightweight app cards needed by the Explore home page',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetAppsRecentQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetAppsRecentQuery.optional() }))
|
||||
.output(zGetAppsRecentResponse)
|
||||
|
||||
export const recent = {
|
||||
@@ -586,7 +586,7 @@ export const get3 = oc
|
||||
path: '/apps/starred',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetAppsStarredQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetAppsStarredQuery.optional() }))
|
||||
.output(zGetAppsStarredResponse)
|
||||
|
||||
export const starred = {
|
||||
@@ -4951,7 +4951,7 @@ export const get94 = oc
|
||||
summary: 'Get app list',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetAppsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetAppsQuery.optional() }))
|
||||
.output(zGetAppsResponse)
|
||||
|
||||
/**
|
||||
|
||||
@@ -1373,11 +1373,15 @@ export const zAgentLogMetaResponse = z.object({
|
||||
* Validated metadata extracted from a Skill package.
|
||||
*/
|
||||
export const zSkillManifest = z.object({
|
||||
description: z.string(),
|
||||
description: z.string().min(1).max(1024),
|
||||
entry_path: z.string(),
|
||||
files: z.array(z.string()),
|
||||
hash: z.string(),
|
||||
name: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
@@ -3964,10 +3968,10 @@ export const zSelectInputConfig = z.object({
|
||||
})
|
||||
|
||||
export const zFormInputConfig = z.discriminatedUnion('type', [
|
||||
zParagraphInputConfig,
|
||||
zSelectInputConfig,
|
||||
zFileInputConfig,
|
||||
zFileListInputConfig,
|
||||
zParagraphInputConfig.extend({ type: z.literal('paragraph') }),
|
||||
zSelectInputConfig.extend({ type: z.literal('select') }),
|
||||
zFileInputConfig.extend({ type: z.literal('file') }),
|
||||
zFileListInputConfig.extend({ type: z.literal('file-list') }),
|
||||
])
|
||||
|
||||
/**
|
||||
@@ -4777,10 +4781,7 @@ export const zGetAppsByAppIdAgentConfigSkillsQuery = z.object({
|
||||
export const zGetAppsByAppIdAgentConfigSkillsResponse = zAgentConfigSkillListResponse
|
||||
|
||||
export const zPostAppsByAppIdAgentConfigSkillsUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
export const zPostAppsByAppIdAgentConfigSkillsUploadPath = z.object({
|
||||
@@ -5011,10 +5012,7 @@ export const zGetAppsByAppIdAgentLogsQuery = z.object({
|
||||
export const zGetAppsByAppIdAgentLogsResponse = zAgentLogResponse
|
||||
|
||||
export const zPostAppsByAppIdAgentSkillsUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
export const zPostAppsByAppIdAgentSkillsUploadPath = z.object({
|
||||
@@ -5225,10 +5223,7 @@ export const zPostAppsByAppIdApiEnablePath = z.object({
|
||||
export const zPostAppsByAppIdApiEnableResponse = zAppDetail
|
||||
|
||||
export const zPostAppsByAppIdAudioToTextBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
export const zPostAppsByAppIdAudioToTextPath = z.object({
|
||||
|
||||
@@ -397,7 +397,7 @@ export const get6 = oc
|
||||
path: '/datasets/external-knowledge-api',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetDatasetsExternalKnowledgeApiQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetDatasetsExternalKnowledgeApiQuery.optional() }))
|
||||
.output(zGetDatasetsExternalKnowledgeApiResponse)
|
||||
|
||||
/**
|
||||
@@ -505,7 +505,7 @@ export const get8 = oc
|
||||
path: '/datasets/process-rule',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetDatasetsProcessRuleQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetDatasetsProcessRuleQuery.optional() }))
|
||||
.output(zGetDatasetsProcessRuleResponse)
|
||||
|
||||
export const processRule = {
|
||||
@@ -1186,11 +1186,11 @@ export const segments = {
|
||||
* Returns:
|
||||
* - total_segments: Total number of segments in the document
|
||||
* - summary_status: Dictionary with status counts
|
||||
* - completed: Number of summaries completed
|
||||
* - generating: Number of summaries being generated
|
||||
* - error: Number of summaries with errors
|
||||
* - not_started: Number of segments without summary records
|
||||
* - timeout: Number of summaries that timed out
|
||||
* - completed: Number of summaries completed
|
||||
* - generating: Number of summaries being generated
|
||||
* - error: Number of summaries with errors
|
||||
* - not_started: Number of segments without summary records
|
||||
* - timeout: Number of summaries that timed out
|
||||
* - summaries: List of summary records with status and content preview
|
||||
*/
|
||||
export const get22 = oc
|
||||
@@ -1777,7 +1777,7 @@ export const get36 = oc
|
||||
path: '/datasets',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetDatasetsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetDatasetsQuery.optional() }))
|
||||
.output(zGetDatasetsResponse)
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ export const get = oc
|
||||
path: '/explore/apps/learn-dify',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetExploreAppsLearnDifyQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetExploreAppsLearnDifyQuery.optional() }))
|
||||
.output(zGetExploreAppsLearnDifyResponse)
|
||||
|
||||
export const learnDify = {
|
||||
@@ -51,7 +51,7 @@ export const get3 = oc
|
||||
path: '/explore/apps',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetExploreAppsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetExploreAppsQuery.optional() }))
|
||||
.output(zGetExploreAppsResponse)
|
||||
|
||||
export const apps = {
|
||||
@@ -72,7 +72,7 @@ export const get4 = oc
|
||||
summary: 'Get banner list',
|
||||
tags: ['default'],
|
||||
})
|
||||
.input(z.object({ query: zGetExploreBannersQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetExploreBannersQuery.optional() }))
|
||||
.output(zGetExploreBannersResponse)
|
||||
|
||||
export const banners = {
|
||||
|
||||
@@ -64,10 +64,7 @@ export const zGetFilesSupportTypeResponse = zAllowedExtensionsResponse
|
||||
export const zGetFilesUploadResponse = zUploadConfig
|
||||
|
||||
export const zPostFilesUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
source: z.enum(['datasets']).optional(),
|
||||
})
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ export const get = oc
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* "inputs": {
|
||||
* "content": "User input content"
|
||||
* },
|
||||
* "action": "Approve"
|
||||
* "inputs": {
|
||||
* "content": "User input content"
|
||||
* },
|
||||
* "action": "Approve"
|
||||
* }
|
||||
*/
|
||||
export const post = oc
|
||||
|
||||
@@ -559,7 +559,7 @@ export const get8 = oc
|
||||
path: '/installed-apps',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetInstalledAppsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetInstalledAppsQuery.optional() }))
|
||||
.output(zGetInstalledAppsResponse)
|
||||
|
||||
export const post12 = oc
|
||||
|
||||
@@ -145,8 +145,7 @@ export const zTextToAudioPayload = z.object({
|
||||
* AudioBinaryResponse
|
||||
*/
|
||||
export const zAudioBinaryResponse = z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
(value) => value instanceof Blob || value instanceof File,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -476,10 +475,10 @@ export const zSelectInputConfig = z.object({
|
||||
})
|
||||
|
||||
export const zFormInputConfig = z.discriminatedUnion('type', [
|
||||
zParagraphInputConfig,
|
||||
zSelectInputConfig,
|
||||
zFileInputConfig,
|
||||
zFileListInputConfig,
|
||||
zParagraphInputConfig.extend({ type: z.literal('paragraph') }),
|
||||
zSelectInputConfig.extend({ type: z.literal('select') }),
|
||||
zFileInputConfig.extend({ type: z.literal('file') }),
|
||||
zFileListInputConfig.extend({ type: z.literal('file-list') }),
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
@@ -246,7 +246,7 @@ export const get2 = oc
|
||||
path: '/rag/pipeline/templates',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetRagPipelineTemplatesQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetRagPipelineTemplatesQuery.optional() }))
|
||||
.output(zGetRagPipelineTemplatesResponse)
|
||||
|
||||
export const templates2 = {
|
||||
@@ -338,7 +338,7 @@ export const get5 = oc
|
||||
path: '/rag/pipelines/recommended-plugins',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetRagPipelinesRecommendedPluginsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetRagPipelinesRecommendedPluginsQuery.optional() }))
|
||||
.output(zGetRagPipelinesRecommendedPluginsResponse)
|
||||
|
||||
export const recommendedPlugins = {
|
||||
|
||||
@@ -7,12 +7,12 @@ import { zGetSetupResponse, zPostSetupBody, zPostSetupResponse } from './zod.gen
|
||||
/**
|
||||
* Get system setup status.
|
||||
*
|
||||
* NOTE: This endpoint is unauthenticated by design.
|
||||
* NOTE: This endpoint is unauthenticated by design.
|
||||
*
|
||||
* During first-time bootstrap there is no admin account yet, so frontend initialization must be
|
||||
* able to query setup progress before any login flow exists.
|
||||
* During first-time bootstrap there is no admin account yet, so frontend initialization must be
|
||||
* able to query setup progress before any login flow exists.
|
||||
*
|
||||
* Only bootstrap-safe status information should be returned by this endpoint.
|
||||
* Only bootstrap-safe status information should be returned by this endpoint.
|
||||
*
|
||||
*/
|
||||
export const get = oc
|
||||
@@ -30,9 +30,9 @@ export const get = oc
|
||||
/**
|
||||
* Initialize system setup with admin account.
|
||||
*
|
||||
* NOTE: This endpoint is unauthenticated by design for first-time bootstrap.
|
||||
* Access is restricted by deployment mode (`SELF_HOSTED`), one-time setup guards,
|
||||
* and init-password validation rather than user session authentication.
|
||||
* NOTE: This endpoint is unauthenticated by design for first-time bootstrap.
|
||||
* Access is restricted by deployment mode (`SELF_HOSTED`), one-time setup guards,
|
||||
* and init-password validation rather than user session authentication.
|
||||
*
|
||||
*/
|
||||
export const post = oc
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,7 +50,7 @@ export const get = oc
|
||||
path: '/tags',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ query: zGetTagsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetTagsQuery.optional() }))
|
||||
.output(zGetTagsResponse)
|
||||
|
||||
export const post = oc
|
||||
|
||||
@@ -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']).optional().default(''),
|
||||
type: z.enum(['', 'app', 'knowledge', 'skill', 'snippet']).optional().default(''),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -116,8 +116,7 @@ export const zTextToSpeechRequest = z.object({
|
||||
* AudioBinaryResponse
|
||||
*/
|
||||
export const zAudioBinaryResponse = z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
(value) => value instanceof Blob || value instanceof File,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -460,10 +459,7 @@ export const zGetTrialAppsByAppIdDatasetsQuery = z.object({
|
||||
export const zGetTrialAppsByAppIdDatasetsResponse = zTrialDatasetListResponse
|
||||
|
||||
export const zPostTrialAppsByAppIdFilesUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
source: z.enum(['datasets']).optional(),
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,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
|
||||
@@ -170,7 +180,15 @@ export type MemberInvitePayload = {
|
||||
|
||||
export type MemberInviteResponse = {
|
||||
invitation_results: Array<
|
||||
MemberInviteSuccessResponse | MemberInviteAlreadyMemberResponse | MemberInviteFailedResponse
|
||||
| ({
|
||||
status: 'success'
|
||||
} & MemberInviteSuccessResponse)
|
||||
| ({
|
||||
status: 'already_member'
|
||||
} & MemberInviteAlreadyMemberResponse)
|
||||
| ({
|
||||
status: 'failed'
|
||||
} & MemberInviteFailedResponse)
|
||||
>
|
||||
result: 'success'
|
||||
tenant_id: string
|
||||
@@ -627,6 +645,194 @@ 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_version_id?: string | 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_version_id?: string | 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 SkillAssistModelPayload = {
|
||||
model: string
|
||||
model_settings?: { [key: string]: unknown } | null
|
||||
plugin_id?: string | null
|
||||
provider: string
|
||||
}
|
||||
|
||||
export type SkillAssistAttachmentPayload = {
|
||||
mime_type?: string | null
|
||||
name: string
|
||||
size?: number | null
|
||||
tool_file_id: string
|
||||
}
|
||||
|
||||
export type SkillAssistMessagePayload = {
|
||||
attachments?: Array<SkillAssistAttachmentPayload>
|
||||
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 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 ToolLabelListResponse = Array<ToolLabel>
|
||||
|
||||
export type ApiToolProviderAddPayload = {
|
||||
@@ -1034,6 +1240,21 @@ export type WorkspaceCustomConfigResponse = {
|
||||
replace_webapp_logo?: 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
|
||||
@@ -1494,6 +1715,60 @@ 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 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 SkillReferenceResponse = {
|
||||
agent_id: string
|
||||
agent_icon?: string | null
|
||||
agent_icon_background?: string | null
|
||||
agent_icon_type?: string | null
|
||||
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 ToolLabel = {
|
||||
icon: string
|
||||
label: I18nObject
|
||||
@@ -1970,6 +2245,10 @@ export type PermissionCatalogItem = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type SkillFileKind = 'directory' | 'file'
|
||||
|
||||
export type SkillFileStorage = 'text' | 'tool_file'
|
||||
|
||||
export type ToolParameter = {
|
||||
auto_generate?: PluginParameterAutoGenerate | null
|
||||
default?:
|
||||
@@ -2465,6 +2744,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
|
||||
@@ -4617,6 +4928,369 @@ 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 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: SkillVersionResponse
|
||||
}
|
||||
|
||||
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 GetWorkspacesCurrentToolLabelsData = {
|
||||
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
|
||||
*
|
||||
@@ -246,8 +253,7 @@ export const zWorkspacePermissionResponse = z.object({
|
||||
* BinaryFileResponse
|
||||
*/
|
||||
export const zBinaryFileResponse = z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
(value) => value instanceof Blob || value instanceof File,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -437,6 +443,155 @@ 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_version_id: z.string().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
|
||||
*/
|
||||
@@ -657,6 +812,33 @@ export const zSwitchWorkspaceResponse = z.object({
|
||||
result: 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
|
||||
*
|
||||
@@ -874,9 +1056,21 @@ export const zMemberInviteFailedResponse = z.object({
|
||||
export const zMemberInviteResponse = z.object({
|
||||
invitation_results: z.array(
|
||||
z.union([
|
||||
zMemberInviteSuccessResponse,
|
||||
zMemberInviteAlreadyMemberResponse,
|
||||
zMemberInviteFailedResponse,
|
||||
z
|
||||
.object({
|
||||
status: z.literal('success'),
|
||||
})
|
||||
.and(zMemberInviteSuccessResponse),
|
||||
z
|
||||
.object({
|
||||
status: z.literal('already_member'),
|
||||
})
|
||||
.and(zMemberInviteAlreadyMemberResponse),
|
||||
z
|
||||
.object({
|
||||
status: z.literal('failed'),
|
||||
})
|
||||
.and(zMemberInviteFailedResponse),
|
||||
]),
|
||||
),
|
||||
result: z.literal('success'),
|
||||
@@ -1233,6 +1427,164 @@ 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_version_id: z.string().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),
|
||||
})
|
||||
|
||||
/**
|
||||
* 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 read-only Skill Authoring assistant.
|
||||
*/
|
||||
export const zSkillAssistMessagePayload = z.object({
|
||||
attachments: z.array(zSkillAssistAttachmentPayload).max(10).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(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillReferenceResponse
|
||||
*/
|
||||
export const zSkillReferenceResponse = z.object({
|
||||
agent_id: z.string(),
|
||||
agent_icon: z.string().nullish(),
|
||||
agent_icon_background: z.string().nullish(),
|
||||
agent_icon_type: z.string().nullish(),
|
||||
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(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ToolEmojiIcon
|
||||
*/
|
||||
@@ -2085,6 +2437,42 @@ 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(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Option
|
||||
*/
|
||||
@@ -3082,7 +3470,7 @@ export const zToolParameter = z.object({
|
||||
* ApiToolBundle
|
||||
*
|
||||
* This class is used to store the schema information of an api based tool.
|
||||
* such as the url, the method, the parameters, etc.
|
||||
* such as the url, the method, the parameters, etc.
|
||||
*/
|
||||
export const zApiToolBundle = z.object({
|
||||
author: z.string(),
|
||||
@@ -3410,6 +3798,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(),
|
||||
@@ -4216,10 +4624,7 @@ export const zPostWorkspacesCurrentPluginUploadGithubBody = zParserGithubUpload
|
||||
export const zPostWorkspacesCurrentPluginUploadGithubResponse = zPluginDecodeResponse
|
||||
|
||||
export const zPostWorkspacesCurrentPluginUploadPkgBody = z.object({
|
||||
pkg: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
pkg: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -4686,6 +5091,234 @@ 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 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
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdRestoreResponse = zSkillVersionResponse
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* Tool labels retrieved successfully
|
||||
*/
|
||||
@@ -5247,10 +5880,7 @@ export const zPostWorkspacesCustomConfigBody = zWorkspaceCustomConfigPayload
|
||||
export const zPostWorkspacesCustomConfigResponse = zWorkspaceTenantResultResponse
|
||||
|
||||
export const zPostWorkspacesCustomConfigWebappLogoUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -139,7 +139,7 @@ export const get3 = oc
|
||||
path: '/account/sessions',
|
||||
tags: ['openapi'],
|
||||
})
|
||||
.input(z.object({ query: zGetAccountSessionsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetAccountSessionsQuery.optional() }))
|
||||
.output(zGetAccountSessionsResponse)
|
||||
|
||||
export const sessions = {
|
||||
@@ -468,7 +468,7 @@ export const get13 = oc
|
||||
path: '/permitted-external-apps',
|
||||
tags: ['openapi'],
|
||||
})
|
||||
.input(z.object({ query: zGetPermittedExternalAppsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetPermittedExternalAppsQuery.optional() }))
|
||||
.output(zGetPermittedExternalAppsResponse)
|
||||
|
||||
export const permittedExternalApps = {
|
||||
|
||||
@@ -248,7 +248,7 @@ export const get2 = oc
|
||||
summary: 'List App Feedbacks',
|
||||
tags: ['Feedback'],
|
||||
})
|
||||
.input(z.object({ query: zGetAppFeedbacksQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetAppFeedbacksQuery.optional() }))
|
||||
.output(zGetAppFeedbacksResponse)
|
||||
|
||||
export const feedbacks = {
|
||||
@@ -382,7 +382,7 @@ export const get4 = oc
|
||||
summary: 'List Annotations',
|
||||
tags: ['Annotations'],
|
||||
})
|
||||
.input(z.object({ query: zGetAppsAnnotationsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetAppsAnnotationsQuery.optional() }))
|
||||
.output(zGetAppsAnnotationsResponse)
|
||||
|
||||
/**
|
||||
@@ -669,7 +669,7 @@ export const get6 = oc
|
||||
summary: 'List Conversations',
|
||||
tags: ['Conversations'],
|
||||
})
|
||||
.input(z.object({ query: zGetConversationsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetConversationsQuery.optional() }))
|
||||
.output(zGetConversationsResponse)
|
||||
|
||||
export const conversations = {
|
||||
@@ -1951,7 +1951,7 @@ export const get20 = oc
|
||||
summary: 'List Knowledge Bases',
|
||||
tags: ['Knowledge Bases'],
|
||||
})
|
||||
.input(z.object({ query: zGetDatasetsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetDatasetsQuery.optional() }))
|
||||
.output(zGetDatasetsResponse)
|
||||
|
||||
/**
|
||||
@@ -2367,7 +2367,7 @@ export const get31 = oc
|
||||
summary: 'List Workflow Logs',
|
||||
tags: ['Chatflows', 'Workflows'],
|
||||
})
|
||||
.input(z.object({ query: zGetWorkflowsLogsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetWorkflowsLogsQuery.optional() }))
|
||||
.output(zGetWorkflowsLogsResponse)
|
||||
|
||||
export const logs = {
|
||||
|
||||
@@ -113,8 +113,7 @@ export const zAppMetaResponse = z.object({
|
||||
* AudioBinaryResponse
|
||||
*/
|
||||
export const zAudioBinaryResponse = z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
(value) => value instanceof Blob || value instanceof File,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -128,8 +127,7 @@ export const zAudioTranscriptResponse = z.object({
|
||||
* BinaryFileResponse
|
||||
*/
|
||||
export const zBinaryFileResponse = z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
(value) => value instanceof Blob || value instanceof File,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -1941,10 +1939,10 @@ export const zParagraphInputConfig = z.object({
|
||||
})
|
||||
|
||||
export const zFormInputConfig = z.discriminatedUnion('type', [
|
||||
zParagraphInputConfig,
|
||||
zSelectInputConfig,
|
||||
zFileInputConfig,
|
||||
zFileListInputConfig,
|
||||
zParagraphInputConfig.extend({ type: z.literal('paragraph') }),
|
||||
zSelectInputConfig.extend({ type: z.literal('select') }),
|
||||
zFileInputConfig.extend({ type: z.literal('file') }),
|
||||
zFileListInputConfig.extend({ type: z.literal('file-list') }),
|
||||
])
|
||||
|
||||
/**
|
||||
@@ -2458,10 +2456,7 @@ export const zPutAppsAnnotationsByAnnotationIdPath = z.object({
|
||||
export const zPutAppsAnnotationsByAnnotationIdResponse = zAnnotation
|
||||
|
||||
export const zPostAudioToTextBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
user: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -2600,10 +2595,7 @@ export const zPostDatasetsBody = zDatasetCreatePayload
|
||||
export const zPostDatasetsResponse = zDatasetDetailResponse
|
||||
|
||||
export const zPostDatasetsPipelineFileUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -2682,10 +2674,7 @@ export const zPatchDatasetsByDatasetIdResponse = zDatasetDetailWithPartialMember
|
||||
|
||||
export const zPostDatasetsByDatasetIdDocumentCreateByFileBody = z.object({
|
||||
data: z.string().optional(),
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
export const zPostDatasetsByDatasetIdDocumentCreateByFilePath = z.object({
|
||||
@@ -2710,10 +2699,7 @@ export const zPostDatasetsByDatasetIdDocumentCreateByTextResponse = zDocumentAnd
|
||||
|
||||
export const zPostDatasetsByDatasetIdDocumentCreateByFile2Body = z.object({
|
||||
data: z.string().optional(),
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
export const zPostDatasetsByDatasetIdDocumentCreateByFile2Path = z.object({
|
||||
@@ -2764,8 +2750,7 @@ export const zPostDatasetsByDatasetIdDocumentsDownloadZipPath = z.object({
|
||||
* ZIP archive containing the requested documents.
|
||||
*/
|
||||
export const zPostDatasetsByDatasetIdDocumentsDownloadZipResponse = z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
(value) => value instanceof Blob || value instanceof File,
|
||||
)
|
||||
|
||||
export const zPostDatasetsByDatasetIdDocumentsMetadataBody = zMetadataOperationData
|
||||
@@ -2828,12 +2813,7 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdResponse = zDocumentDet
|
||||
|
||||
export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdBody = z.object({
|
||||
data: z.string().optional(),
|
||||
file: z
|
||||
.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
)
|
||||
.optional(),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File).optional(),
|
||||
})
|
||||
|
||||
export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({
|
||||
@@ -2993,12 +2973,7 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdCh
|
||||
|
||||
export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFileBody = z.object({
|
||||
data: z.string().optional(),
|
||||
file: z
|
||||
.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
)
|
||||
.optional(),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File).optional(),
|
||||
})
|
||||
|
||||
export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFilePath = z.object({
|
||||
@@ -3027,12 +3002,7 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByTextResponse =
|
||||
|
||||
export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Body = z.object({
|
||||
data: z.string().optional(),
|
||||
file: z
|
||||
.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
)
|
||||
.optional(),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File).optional(),
|
||||
})
|
||||
|
||||
export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Path = z.object({
|
||||
@@ -3203,10 +3173,7 @@ export const zGetEndUsersByEndUserIdPath = z.object({
|
||||
export const zGetEndUsersByEndUserIdResponse = zEndUserDetail
|
||||
|
||||
export const zPostFilesUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
),
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
user: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -3228,8 +3195,7 @@ export const zGetFilesByFileIdPreviewQuery = z.object({
|
||||
* Returns the raw file content. The `Content-Type` header is set to the file's MIME type. If `as_attachment` is `true`, the file is returned as a download with `Content-Disposition: attachment`.
|
||||
*/
|
||||
export const zGetFilesByFileIdPreviewResponse = z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
(value) => value instanceof Blob || value instanceof File,
|
||||
)
|
||||
|
||||
export const zGetFormHumanInputByFormTokenPath = z.object({
|
||||
@@ -3314,8 +3280,7 @@ export const zPostTextToAudioBody = zTextToAudioPayloadWithUser
|
||||
* Returns the generated audio. Generator responses are streamed by the service as `audio/mpeg`; otherwise the provider output is returned directly.
|
||||
*/
|
||||
export const zPostTextToAudioResponse = z.custom<Blob | File>(
|
||||
(value) =>
|
||||
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
|
||||
(value) => value instanceof Blob || value instanceof File,
|
||||
)
|
||||
|
||||
export const zGetWorkflowByTaskIdEventsPath = z.object({
|
||||
|
||||
@@ -295,7 +295,7 @@ export const get = oc
|
||||
path: '/conversations',
|
||||
tags: ['web'],
|
||||
})
|
||||
.input(z.object({ query: zGetConversationsQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetConversationsQuery.optional() }))
|
||||
.output(zGetConversationsResponse)
|
||||
|
||||
export const conversations = {
|
||||
@@ -350,23 +350,23 @@ export const emailCodeLogin = {
|
||||
* multiple file types with automatic validation and storage.
|
||||
*
|
||||
* Args:
|
||||
* app_model: The associated application model
|
||||
* end_user: The end user uploading the file
|
||||
* app_model: The associated application model
|
||||
* end_user: The end user uploading the file
|
||||
*
|
||||
* Form Parameters:
|
||||
* file: The file to upload (required)
|
||||
* source: Optional source type (datasets or None)
|
||||
* file: The file to upload (required)
|
||||
* source: Optional source type (datasets or None)
|
||||
*
|
||||
* Returns:
|
||||
* dict: File information including ID, URL, and metadata
|
||||
* int: HTTP status code 201 for success
|
||||
* dict: File information including ID, URL, and metadata
|
||||
* int: HTTP status code 201 for success
|
||||
*
|
||||
* Raises:
|
||||
* NoFileUploadedError: No file provided in request
|
||||
* TooManyFilesError: Multiple files provided (only one allowed)
|
||||
* FilenameNotExistsError: File has no filename
|
||||
* FileTooLargeError: File exceeds size limit
|
||||
* UnsupportedFileTypeError: File type not supported
|
||||
* NoFileUploadedError: No file provided in request
|
||||
* TooManyFilesError: Multiple files provided (only one allowed)
|
||||
* FilenameNotExistsError: File has no filename
|
||||
* FileTooLargeError: File exceeds size limit
|
||||
* UnsupportedFileTypeError: File type not supported
|
||||
*/
|
||||
export const post9 = oc
|
||||
.route({
|
||||
@@ -501,10 +501,10 @@ export const get2 = oc
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* "inputs": {
|
||||
* "content": "User input content"
|
||||
* },
|
||||
* "action": "Approve"
|
||||
* "inputs": {
|
||||
* "content": "User input content"
|
||||
* },
|
||||
* "action": "Approve"
|
||||
* }
|
||||
*/
|
||||
export const post14 = oc
|
||||
@@ -575,7 +575,7 @@ export const get3 = oc
|
||||
path: '/login/status',
|
||||
tags: ['web'],
|
||||
})
|
||||
.input(z.object({ query: zGetLoginStatusQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetLoginStatusQuery.optional() }))
|
||||
.output(zGetLoginStatusResponse)
|
||||
|
||||
export const status = {
|
||||
@@ -771,7 +771,7 @@ export const get9 = oc
|
||||
path: '/passport',
|
||||
tags: ['web'],
|
||||
})
|
||||
.input(z.object({ query: zGetPassportQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetPassportQuery.optional() }))
|
||||
.output(zGetPassportResponse)
|
||||
|
||||
export const passport = {
|
||||
@@ -786,20 +786,20 @@ export const passport = {
|
||||
* to the platform storage for use in web applications.
|
||||
*
|
||||
* Args:
|
||||
* app_model: The associated application model
|
||||
* end_user: The end user making the request
|
||||
* app_model: The associated application model
|
||||
* end_user: The end user making the request
|
||||
*
|
||||
* JSON Parameters:
|
||||
* url: The remote URL to download the file from (required)
|
||||
* url: The remote URL to download the file from (required)
|
||||
*
|
||||
* Returns:
|
||||
* dict: File information including ID, signed URL, and metadata
|
||||
* int: HTTP status code 201 for success
|
||||
* dict: File information including ID, signed URL, and metadata
|
||||
* int: HTTP status code 201 for success
|
||||
*
|
||||
* Raises:
|
||||
* RemoteFileUploadError: Failed to fetch file from remote URL
|
||||
* FileTooLargeError: File exceeds size limit
|
||||
* UnsupportedFileTypeError: File type not supported
|
||||
* RemoteFileUploadError: Failed to fetch file from remote URL
|
||||
* FileTooLargeError: File exceeds size limit
|
||||
* UnsupportedFileTypeError: File type not supported
|
||||
*/
|
||||
export const post19 = oc
|
||||
.route({
|
||||
@@ -828,15 +828,15 @@ export const upload2 = {
|
||||
* including content type and content length.
|
||||
*
|
||||
* Args:
|
||||
* app_model: The associated application model
|
||||
* end_user: The end user making the request
|
||||
* url: URL-encoded path to the remote file
|
||||
* app_model: The associated application model
|
||||
* end_user: The end user making the request
|
||||
* url: URL-encoded path to the remote file
|
||||
*
|
||||
* Returns:
|
||||
* dict: Remote file information including type and length
|
||||
* dict: Remote file information including type and length
|
||||
*
|
||||
* Raises:
|
||||
* HTTPException: If the remote file cannot be accessed
|
||||
* HTTPException: If the remote file cannot be accessed
|
||||
*/
|
||||
export const get10 = oc
|
||||
.route({
|
||||
@@ -893,7 +893,7 @@ export const get11 = oc
|
||||
path: '/saved-messages',
|
||||
tags: ['web'],
|
||||
})
|
||||
.input(z.object({ query: zGetSavedMessagesQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetSavedMessagesQuery.optional() }))
|
||||
.output(zGetSavedMessagesResponse)
|
||||
|
||||
/**
|
||||
@@ -998,7 +998,7 @@ export const get14 = oc
|
||||
path: '/webapp/access-mode',
|
||||
tags: ['web'],
|
||||
})
|
||||
.input(z.object({ query: zGetWebappAccessModeQuery.optional() }).optional())
|
||||
.input(z.object({ query: zGetWebappAccessModeQuery.optional() }))
|
||||
.output(zGetWebappAccessModeResponse)
|
||||
|
||||
export const accessMode = {
|
||||
|
||||
@@ -700,10 +700,10 @@ export const zParagraphInputConfig = z.object({
|
||||
})
|
||||
|
||||
export const zFormInputConfig = z.discriminatedUnion('type', [
|
||||
zParagraphInputConfig,
|
||||
zSelectInputConfig,
|
||||
zFileInputConfig,
|
||||
zFileListInputConfig,
|
||||
zParagraphInputConfig.extend({ type: z.literal('paragraph') }),
|
||||
zSelectInputConfig.extend({ type: z.literal('select') }),
|
||||
zFileInputConfig.extend({ type: z.literal('file') }),
|
||||
zFileListInputConfig.extend({ type: z.literal('file-list') }),
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { $, defineConfig } from '@hey-api/openapi-ts'
|
||||
import ts from 'typescript'
|
||||
|
||||
type JsonObject = Record<string, unknown>
|
||||
|
||||
@@ -39,8 +40,16 @@ type ApiSpec = {
|
||||
}
|
||||
|
||||
type ApiJob = {
|
||||
clean?: boolean
|
||||
document: SwaggerDocument
|
||||
outputPath: string
|
||||
plugins?: UserConfig['plugins']
|
||||
source?: {
|
||||
callback: () => void
|
||||
enabled: true
|
||||
path: null
|
||||
serialize: () => string
|
||||
}
|
||||
}
|
||||
|
||||
type ApiContractOperation = {
|
||||
@@ -405,6 +414,24 @@ const writeConsoleRouterContract = (segments: string[]) => {
|
||||
fs.writeFileSync(routerPath, consoleRouterContractContent(segments))
|
||||
}
|
||||
|
||||
const createConsoleContractEntryJob = (document: SwaggerDocument, segments: string[]): ApiJob => {
|
||||
return {
|
||||
clean: false,
|
||||
document,
|
||||
outputPath: 'generated/api/console',
|
||||
plugins: [],
|
||||
source: {
|
||||
callback: () => {
|
||||
writeConsoleContractEntry(segments)
|
||||
writeConsoleRouterContract(segments)
|
||||
},
|
||||
enabled: true,
|
||||
path: null,
|
||||
serialize: () => '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const splitConsoleDocument = (document: SwaggerDocument) => {
|
||||
const pathsBySegment = new Map<string, Record<string, Record<string, unknown>>>()
|
||||
|
||||
@@ -423,12 +450,7 @@ const splitConsoleDocument = (document: SwaggerDocument) => {
|
||||
}),
|
||||
)
|
||||
|
||||
// An empty plugin list falls back to the generator defaults, so write the root entries
|
||||
// directly instead of creating a no-op job that would also emit a client and SDK.
|
||||
writeConsoleContractEntry(segments)
|
||||
writeConsoleRouterContract(segments)
|
||||
|
||||
return jobs
|
||||
return [...jobs, createConsoleContractEntryJob(document, segments)]
|
||||
}
|
||||
|
||||
const createApiJobs = (spec: ApiSpec): ApiJob[] => {
|
||||
@@ -456,13 +478,15 @@ const createApiConfig = (job: ApiJob): UserConfig => ({
|
||||
file: false,
|
||||
},
|
||||
output: {
|
||||
...(job.clean === undefined ? {} : { clean: job.clean }),
|
||||
entryFile: false,
|
||||
fileName: {
|
||||
suffix: '.gen',
|
||||
},
|
||||
path: job.outputPath,
|
||||
...(job.source ? { source: job.source } : {}),
|
||||
},
|
||||
plugins: [
|
||||
plugins: job.plugins ?? [
|
||||
{
|
||||
comments: false,
|
||||
name: '@hey-api/typescript',
|
||||
@@ -477,10 +501,8 @@ const createApiConfig = (job: ApiJob): UserConfig => ({
|
||||
.call(
|
||||
$.func((predicate) => {
|
||||
const value = $.id('value')
|
||||
const objectValue = $('Object').call(value)
|
||||
// `instanceof` is not exposed by the generator's AST DSL.
|
||||
const isBlob = $('Blob').attr('prototype').attr('isPrototypeOf').call(objectValue)
|
||||
const isFile = $('File').attr('prototype').attr('isPrototypeOf').call(objectValue)
|
||||
const isBlob = $.binary(value, ts.SyntaxKind.InstanceOfKeyword, $.id('Blob'))
|
||||
const isFile = $.binary(value, ts.SyntaxKind.InstanceOfKeyword, $.id('File'))
|
||||
predicate.param('value')
|
||||
predicate.do($.return($.binary(isBlob, '||', isFile)))
|
||||
}),
|
||||
|
||||
Generated
+258
-50
@@ -46,8 +46,8 @@ catalogs:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0
|
||||
'@hey-api/openapi-ts':
|
||||
specifier: 0.0.0-next-20260724070045
|
||||
version: 0.0.0-next-20260724070045
|
||||
specifier: 0.98.2
|
||||
version: 0.98.2
|
||||
'@hono/node-server':
|
||||
specifier: 2.0.12
|
||||
version: 2.0.12
|
||||
@@ -903,7 +903,7 @@ importers:
|
||||
version: link:../tsconfig
|
||||
'@hey-api/openapi-ts':
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.0-next-20260724070045(magicast@0.5.3)
|
||||
version: 0.98.2(@typescript/typescript6@6.0.2)(magicast@0.5.3)
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 25.9.5
|
||||
@@ -1960,12 +1960,18 @@ packages:
|
||||
peerDependencies:
|
||||
tailwindcss: '*'
|
||||
|
||||
'@emnapi/core@1.11.0':
|
||||
resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==}
|
||||
|
||||
'@emnapi/core@1.11.2':
|
||||
resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==}
|
||||
|
||||
'@emnapi/core@1.9.2':
|
||||
resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
|
||||
|
||||
'@emnapi/runtime@1.11.0':
|
||||
resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==}
|
||||
|
||||
'@emnapi/runtime@1.11.1':
|
||||
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
|
||||
|
||||
@@ -2285,29 +2291,27 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>= 16 || ^19.0.0-rc'
|
||||
|
||||
'@hey-api/codegen-cli@0.0.0-next-20260724070045':
|
||||
resolution: {integrity: sha512-bn0ncYXv9PhqsA3u7WJuxTO8uCF/JNUn2SjmNhZkvXf/8E0iAXGkEiLZX2Ed+3cWwiEIQjrP+knrud2b16MBCQ==}
|
||||
'@hey-api/codegen-core@0.9.0':
|
||||
resolution: {integrity: sha512-OK9/R8WuujwgvnrDIPnEiIf6WnfUOi3GaEr6kIngqoI5FUQwYbeDKHE/frTVUl2A76ZQPCrMknHtPx6Gqtwf8Q==}
|
||||
engines: {node: '>=22.18.0'}
|
||||
|
||||
'@hey-api/codegen-core@0.0.0-next-20260724070045':
|
||||
resolution: {integrity: sha512-nzNZVgqrAY86CmjslqKMZM1C3Ai/zaB3lzBvOv5U3NJUO5ZOqU0LDPwMoJGQK/Dpw4TaKPWJ/arjIUS03281iw==}
|
||||
'@hey-api/json-schema-ref-parser@1.4.3':
|
||||
resolution: {integrity: sha512-UzGSDzh3QUhrnwl4atnHc2YqDO6KemYVEOwl1Ynowm/tcr0XlpdHOpyWr5UaWIJfiXTXdYRIC9k2Yxm19pcPzQ==}
|
||||
engines: {node: '>=22.18.0'}
|
||||
|
||||
'@hey-api/json-schema-ref-parser@0.0.0-next-20260724070045':
|
||||
resolution: {integrity: sha512-Od4kqGNUUhh2ZvyGcbijvH/1FNttvFvbwybuGc8mXSTdRVHz8GNMoIoXb3mkugcvuL+myqVFqBpXl/yGNtuF9A==}
|
||||
engines: {node: '>=22.18.0'}
|
||||
|
||||
'@hey-api/openapi-ts@0.0.0-next-20260724070045':
|
||||
resolution: {integrity: sha512-EZFQ2Q20sB5f39mSur1lacSDZdzhhpV/ZWyDzSLLY0fK9GhVbkltW5/y26w+sUA5aC1C2fa78jy+fgLGMra8JA==}
|
||||
'@hey-api/openapi-ts@0.98.2':
|
||||
resolution: {integrity: sha512-2nVJXH8tpFPGTBOhxyjEd1Jw0hsRqJqeTQW3kltAjVdSU4YWxeu97x5sgNOmsbsfeg6Dqz7Wfzs26walBOuswA==}
|
||||
engines: {node: '>=22.18.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc'
|
||||
|
||||
'@hey-api/shared@0.0.0-next-20260724070045':
|
||||
resolution: {integrity: sha512-1l5mWzZ09PLDXtdYWcHweefWqq+6VtXVZjeA3m+YFs+zw9lWcdYIs0LPFMKSqv2sXgnj2c1m5vK1urnDvmmqNQ==}
|
||||
'@hey-api/shared@0.4.8':
|
||||
resolution: {integrity: sha512-29Pg2FB0UW20pplYgcfiQn1hQYpbZ9D2gdDJc7nDK3xh3pvHOTGP0v3R2ueFpFnw9GN1SRhIdhiVuAYWMDimjA==}
|
||||
engines: {node: '>=22.18.0'}
|
||||
|
||||
'@hey-api/spec-types@0.0.0-next-20260724070045':
|
||||
resolution: {integrity: sha512-HkYEwGQlj9X8+KYVFJYAEo4cKsMaZ8z/rF95v8ppIIleZJA+hRaFDZNZPMwCNPXtraTELcc1BUZ4f4rJaBvpLw==}
|
||||
'@hey-api/spec-types@0.2.0':
|
||||
resolution: {integrity: sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==}
|
||||
|
||||
'@hey-api/types@0.1.4':
|
||||
resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==}
|
||||
@@ -3421,104 +3425,207 @@ packages:
|
||||
'@oxc-project/types@0.141.0':
|
||||
resolution: {integrity: sha512-S4as7z0j0xQkXcJlyY5ehntwK8/wRkQb9Cyqw+J/N2rkWGQGK0SxD6X6DhQTc7qsxVTBxXbxZtBJh3mr3PtIzQ==}
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.21.3':
|
||||
resolution: {integrity: sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.24.2':
|
||||
resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@oxc-resolver/binding-android-arm64@11.21.3':
|
||||
resolution: {integrity: sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@oxc-resolver/binding-android-arm64@11.24.2':
|
||||
resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@oxc-resolver/binding-darwin-arm64@11.21.3':
|
||||
resolution: {integrity: sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-resolver/binding-darwin-arm64@11.24.2':
|
||||
resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-resolver/binding-darwin-x64@11.21.3':
|
||||
resolution: {integrity: sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-resolver/binding-darwin-x64@11.24.2':
|
||||
resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-resolver/binding-freebsd-x64@11.21.3':
|
||||
resolution: {integrity: sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@oxc-resolver/binding-freebsd-x64@11.24.2':
|
||||
resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3':
|
||||
resolution: {integrity: sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2':
|
||||
resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf@11.21.3':
|
||||
resolution: {integrity: sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf@11.24.2':
|
||||
resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-gnu@11.21.3':
|
||||
resolution: {integrity: sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.21.3':
|
||||
resolution: {integrity: sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.24.2':
|
||||
resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.21.3':
|
||||
resolution: {integrity: sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.21.3':
|
||||
resolution: {integrity: sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.21.3':
|
||||
resolution: {integrity: sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.24.2':
|
||||
resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.21.3':
|
||||
resolution: {integrity: sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.21.3':
|
||||
resolution: {integrity: sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.24.2':
|
||||
resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.21.3':
|
||||
resolution: {integrity: sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.24.2':
|
||||
resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.21.3':
|
||||
resolution: {integrity: sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.24.2':
|
||||
resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@oxc-resolver/binding-wasm32-wasi@11.21.3':
|
||||
resolution: {integrity: sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@oxc-resolver/binding-wasm32-wasi@11.24.2':
|
||||
resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@oxc-resolver/binding-win32-arm64-msvc@11.21.3':
|
||||
resolution: {integrity: sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxc-resolver/binding-win32-arm64-msvc@11.24.2':
|
||||
resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.21.3':
|
||||
resolution: {integrity: sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.24.2':
|
||||
resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==}
|
||||
cpu: [x64]
|
||||
@@ -5546,9 +5653,6 @@ packages:
|
||||
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
citty@0.2.2:
|
||||
resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
@@ -5596,7 +5700,6 @@ packages:
|
||||
|
||||
color-support@1.1.3:
|
||||
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
||||
hasBin: true
|
||||
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
@@ -6533,7 +6636,6 @@ packages:
|
||||
|
||||
giget@3.3.0:
|
||||
resolution: {integrity: sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==}
|
||||
hasBin: true
|
||||
|
||||
github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
@@ -6914,8 +7016,8 @@ packages:
|
||||
js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
js-yaml@5.2.0:
|
||||
resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==}
|
||||
js-yaml@4.3.0:
|
||||
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
|
||||
hasBin: true
|
||||
|
||||
js-yaml@5.2.2:
|
||||
@@ -7812,6 +7914,9 @@ packages:
|
||||
resolution: {integrity: sha512-h6QFWd6lBMfjESqgQ27GjzrSDb0qbznp7VDQqp2zvgsrWut4vcchyMIzOVXvGQ2GMZgKw9RWrFNWv9WqGL0p7Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
||||
oxc-resolver@11.21.3:
|
||||
resolution: {integrity: sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==}
|
||||
|
||||
oxc-resolver@11.24.2:
|
||||
resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==}
|
||||
|
||||
@@ -8372,6 +8477,11 @@ packages:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.8.2:
|
||||
resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -9392,7 +9502,7 @@ snapshots:
|
||||
|
||||
'@amplitude/rrweb-snapshot@2.1.0':
|
||||
dependencies:
|
||||
postcss: 8.5.23
|
||||
postcss: 8.5.19
|
||||
|
||||
'@amplitude/rrweb-types@2.0.0-alpha.40': {}
|
||||
|
||||
@@ -9768,6 +9878,12 @@ snapshots:
|
||||
'@iconify/utils': 3.1.3
|
||||
tailwindcss: 4.3.3
|
||||
|
||||
'@emnapi/core@1.11.0':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.2.2
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/core@1.11.2':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.2.2
|
||||
@@ -9780,6 +9896,11 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.11.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.11.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -10099,11 +10220,7 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
||||
'@hey-api/codegen-cli@0.0.0-next-20260724070045':
|
||||
dependencies:
|
||||
citty: 0.2.2
|
||||
|
||||
'@hey-api/codegen-core@0.0.0-next-20260724070045(magicast@0.5.3)':
|
||||
'@hey-api/codegen-core@0.9.0(magicast@0.5.3)':
|
||||
dependencies:
|
||||
'@hey-api/types': 0.1.4
|
||||
ansi-colors: 4.1.3
|
||||
@@ -10112,41 +10229,42 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@hey-api/json-schema-ref-parser@0.0.0-next-20260724070045':
|
||||
'@hey-api/json-schema-ref-parser@1.4.3':
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
js-yaml: 5.2.0
|
||||
js-yaml: 4.3.0
|
||||
|
||||
'@hey-api/openapi-ts@0.0.0-next-20260724070045(magicast@0.5.3)':
|
||||
'@hey-api/openapi-ts@0.98.2(@typescript/typescript6@6.0.2)(magicast@0.5.3)':
|
||||
dependencies:
|
||||
'@hey-api/codegen-cli': 0.0.0-next-20260724070045
|
||||
'@hey-api/codegen-core': 0.0.0-next-20260724070045(magicast@0.5.3)
|
||||
'@hey-api/json-schema-ref-parser': 0.0.0-next-20260724070045
|
||||
'@hey-api/shared': 0.0.0-next-20260724070045(magicast@0.5.3)
|
||||
'@hey-api/spec-types': 0.0.0-next-20260724070045
|
||||
'@hey-api/codegen-core': 0.9.0(magicast@0.5.3)
|
||||
'@hey-api/json-schema-ref-parser': 1.4.3
|
||||
'@hey-api/shared': 0.4.8(magicast@0.5.3)
|
||||
'@hey-api/spec-types': 0.2.0
|
||||
'@hey-api/types': 0.1.4
|
||||
'@lukeed/ms': 2.0.2
|
||||
ansi-colors: 4.1.3
|
||||
color-support: 1.1.3
|
||||
commander: 15.0.0
|
||||
get-tsconfig: 4.14.0
|
||||
typescript: '@typescript/typescript6@6.0.2'
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@hey-api/shared@0.0.0-next-20260724070045(magicast@0.5.3)':
|
||||
'@hey-api/shared@0.4.8(magicast@0.5.3)':
|
||||
dependencies:
|
||||
'@hey-api/codegen-core': 0.0.0-next-20260724070045(magicast@0.5.3)
|
||||
'@hey-api/json-schema-ref-parser': 0.0.0-next-20260724070045
|
||||
'@hey-api/spec-types': 0.0.0-next-20260724070045
|
||||
'@hey-api/codegen-core': 0.9.0(magicast@0.5.3)
|
||||
'@hey-api/json-schema-ref-parser': 1.4.3
|
||||
'@hey-api/spec-types': 0.2.0
|
||||
'@hey-api/types': 0.1.4
|
||||
ansi-colors: 4.1.3
|
||||
cross-spawn: 7.0.6
|
||||
open: 11.0.0
|
||||
semver: 7.8.5
|
||||
semver: 7.8.2
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@hey-api/spec-types@0.0.0-next-20260724070045':
|
||||
'@hey-api/spec-types@0.2.0':
|
||||
dependencies:
|
||||
'@hey-api/types': 0.1.4
|
||||
|
||||
@@ -10807,6 +10925,13 @@ snapshots:
|
||||
'@napi-rs/keyring-win32-ia32-msvc': 1.3.0
|
||||
'@napi-rs/keyring-win32-x64-msvc': 1.3.0
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.0
|
||||
'@emnapi/runtime': 1.11.0
|
||||
'@tybys/wasm-util': 0.10.3
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.2
|
||||
@@ -11074,54 +11199,109 @@ snapshots:
|
||||
|
||||
'@oxc-project/types@0.141.0': {}
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-android-arm64@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-android-arm64@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-darwin-arm64@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-darwin-arm64@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-darwin-x64@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-darwin-x64@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-freebsd-x64@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-freebsd-x64@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-gnu@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-gnu@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-wasm32-wasi@11.21.3':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.0
|
||||
'@emnapi/runtime': 1.11.0
|
||||
'@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-wasm32-wasi@11.24.2':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.2
|
||||
@@ -11129,9 +11309,15 @@ snapshots:
|
||||
'@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-win32-arm64-msvc@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-win32-arm64-msvc@11.24.2':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.21.3':
|
||||
optional: true
|
||||
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.24.2':
|
||||
optional: true
|
||||
|
||||
@@ -13198,8 +13384,6 @@ snapshots:
|
||||
|
||||
ci-info@4.4.0: {}
|
||||
|
||||
citty@0.2.2: {}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
@@ -14744,7 +14928,7 @@ snapshots:
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
||||
js-yaml@5.2.0:
|
||||
js-yaml@4.3.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
@@ -15808,6 +15992,28 @@ snapshots:
|
||||
'@oxc-parser/binding-win32-ia32-msvc': 0.140.0
|
||||
'@oxc-parser/binding-win32-x64-msvc': 0.140.0
|
||||
|
||||
oxc-resolver@11.21.3:
|
||||
optionalDependencies:
|
||||
'@oxc-resolver/binding-android-arm-eabi': 11.21.3
|
||||
'@oxc-resolver/binding-android-arm64': 11.21.3
|
||||
'@oxc-resolver/binding-darwin-arm64': 11.21.3
|
||||
'@oxc-resolver/binding-darwin-x64': 11.21.3
|
||||
'@oxc-resolver/binding-freebsd-x64': 11.21.3
|
||||
'@oxc-resolver/binding-linux-arm-gnueabihf': 11.21.3
|
||||
'@oxc-resolver/binding-linux-arm-musleabihf': 11.21.3
|
||||
'@oxc-resolver/binding-linux-arm64-gnu': 11.21.3
|
||||
'@oxc-resolver/binding-linux-arm64-musl': 11.21.3
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu': 11.21.3
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu': 11.21.3
|
||||
'@oxc-resolver/binding-linux-riscv64-musl': 11.21.3
|
||||
'@oxc-resolver/binding-linux-s390x-gnu': 11.21.3
|
||||
'@oxc-resolver/binding-linux-x64-gnu': 11.21.3
|
||||
'@oxc-resolver/binding-linux-x64-musl': 11.21.3
|
||||
'@oxc-resolver/binding-openharmony-arm64': 11.21.3
|
||||
'@oxc-resolver/binding-wasm32-wasi': 11.21.3
|
||||
'@oxc-resolver/binding-win32-arm64-msvc': 11.21.3
|
||||
'@oxc-resolver/binding-win32-x64-msvc': 11.21.3
|
||||
|
||||
oxc-resolver@11.24.2:
|
||||
optionalDependencies:
|
||||
'@oxc-resolver/binding-android-arm-eabi': 11.24.2
|
||||
@@ -16627,6 +16833,8 @@ snapshots:
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.8.2: {}
|
||||
|
||||
semver@7.8.5: {}
|
||||
|
||||
server-only@0.0.1: {}
|
||||
@@ -16821,7 +17029,7 @@ snapshots:
|
||||
jsonc-parser: 3.3.1
|
||||
open: 10.2.0
|
||||
oxc-parser: 0.127.0
|
||||
oxc-resolver: 11.24.2
|
||||
oxc-resolver: 11.21.3
|
||||
recast: 0.23.12
|
||||
semver: 7.8.5
|
||||
use-sync-external-store: 1.6.0(react@19.2.8)
|
||||
@@ -16848,7 +17056,7 @@ snapshots:
|
||||
jsonc-parser: 3.3.1
|
||||
open: 10.2.0
|
||||
oxc-parser: 0.127.0
|
||||
oxc-resolver: 11.24.2
|
||||
oxc-resolver: 11.21.3
|
||||
recast: 0.23.12
|
||||
semver: 7.8.5
|
||||
use-sync-external-store: 1.6.0(react@19.2.8)
|
||||
@@ -17835,7 +18043,7 @@ time:
|
||||
'@floating-ui/react@0.27.20': '2026-07-11T08:41:33.223Z'
|
||||
'@formatjs/intl-localematcher@0.8.13': '2026-07-16T04:15:53.223Z'
|
||||
'@heroicons/react@2.2.0': '2024-11-18T15:33:27.317Z'
|
||||
'@hey-api/openapi-ts@0.0.0-next-20260724070045': '2026-07-24T07:02:37.380Z'
|
||||
'@hey-api/openapi-ts@0.98.2': '2026-06-08T05:37:17.524Z'
|
||||
'@hono/node-server@2.0.12': '2026-07-26T05:02:01.126Z'
|
||||
'@iconify-json/heroicons@1.2.3': '2025-09-20T05:33:02.364Z'
|
||||
'@iconify-json/ri@1.2.10': '2026-02-10T08:41:46.666Z'
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ catalog:
|
||||
'@floating-ui/react': 0.27.20
|
||||
'@formatjs/intl-localematcher': 0.8.13
|
||||
'@heroicons/react': 2.2.0
|
||||
'@hey-api/openapi-ts': 0.0.0-next-20260724070045
|
||||
'@hey-api/openapi-ts': 0.98.2
|
||||
'@hono/node-server': 2.0.12
|
||||
'@iconify-json/heroicons': 1.2.3
|
||||
'@iconify-json/ri': 1.2.10
|
||||
|
||||
@@ -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 />
|
||||
}
|
||||
+12
-6
@@ -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 () => {
|
||||
|
||||
+33
-376
@@ -1,416 +1,73 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import ModelParameterModal from '../index'
|
||||
|
||||
let parameterRules: Array<Record<string, unknown>> | undefined = [
|
||||
{
|
||||
name: 'temperature',
|
||||
label: { en_US: 'Temperature' },
|
||||
type: 'float',
|
||||
default: 0.7,
|
||||
min: 0,
|
||||
max: 1,
|
||||
help: { en_US: 'Control randomness' },
|
||||
},
|
||||
]
|
||||
let isRulesLoading = false
|
||||
let isRulesPending = false
|
||||
let currentProvider: Record<string, unknown> | undefined = {
|
||||
provider: 'openai',
|
||||
label: { en_US: 'OpenAI' },
|
||||
}
|
||||
let currentModel: Record<string, unknown> | undefined = {
|
||||
model: 'gpt-3.5-turbo',
|
||||
status: 'active',
|
||||
model_properties: { mode: 'chat' },
|
||||
}
|
||||
let activeTextGenerationModelList: Array<Record<string, unknown>> = [
|
||||
{
|
||||
provider: 'openai',
|
||||
models: [
|
||||
{
|
||||
model: 'gpt-3.5-turbo',
|
||||
model_properties: { mode: 'chat' },
|
||||
features: ['vision'],
|
||||
},
|
||||
{
|
||||
model: 'gpt-4.1',
|
||||
model_properties: { mode: 'chat' },
|
||||
features: ['vision', 'tool-call'],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openIntegrationsSetting: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
isAPIKeySet: true,
|
||||
}),
|
||||
vi.mock('@/app/components/header/account-setting/use-integrations-setting', () => ({
|
||||
useIntegrationsSetting: () => mocks.openIntegrationsSetting,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useModelParameterRules: () => ({
|
||||
data: {
|
||||
data: parameterRules,
|
||||
data: [],
|
||||
},
|
||||
isLoading: isRulesLoading,
|
||||
isPending: isRulesPending,
|
||||
isLoading: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks', () => ({
|
||||
useTextGenerationCurrentProviderAndModelAndModelList: () => ({
|
||||
currentProvider,
|
||||
currentModel,
|
||||
activeTextGenerationModelList,
|
||||
activeTextGenerationModelList: [],
|
||||
currentModel: undefined,
|
||||
currentProvider: undefined,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../parameter-item', () => ({
|
||||
default: ({
|
||||
parameterRule,
|
||||
onChange,
|
||||
onSwitch,
|
||||
nodesOutputVars,
|
||||
availableNodes,
|
||||
}: {
|
||||
parameterRule: { name: string; label: { en_US: string } }
|
||||
onChange: (v: number) => void
|
||||
onSwitch: (checked: boolean, val: unknown) => void
|
||||
nodesOutputVars?: unknown[]
|
||||
availableNodes?: unknown[]
|
||||
}) => (
|
||||
<div
|
||||
data-testid={`param-${parameterRule.name}`}
|
||||
data-has-nodes-output-vars={!!nodesOutputVars}
|
||||
data-has-available-nodes={!!availableNodes}
|
||||
>
|
||||
{parameterRule.label.en_US}
|
||||
<button onClick={() => onChange(0.9)}>Change</button>
|
||||
<button onClick={() => onSwitch(false, undefined)}>Remove</button>
|
||||
<button onClick={() => onSwitch(true, 'assigned')}>Add</button>
|
||||
</div>
|
||||
vi.mock('../../model-selector', () => ({
|
||||
default: ({ onConfigureEmptyState }: { onConfigureEmptyState?: () => void }) => (
|
||||
<button type="button" onClick={onConfigureEmptyState}>
|
||||
configure-empty-model
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../model-selector', () => ({
|
||||
default: ({
|
||||
onHide,
|
||||
onSelect,
|
||||
}: {
|
||||
onHide: () => void
|
||||
onSelect: (value: { provider: string; model: string }) => void
|
||||
}) => (
|
||||
<div data-testid="model-selector">
|
||||
<button onClick={() => onSelect({ provider: 'openai', model: 'gpt-4.1' })}>
|
||||
Select GPT-4.1
|
||||
</button>
|
||||
<button onClick={onHide}>hide</button>
|
||||
</div>
|
||||
),
|
||||
vi.mock('@/app/components/base/loading', () => ({
|
||||
default: () => <div>loading</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../parameter-item', () => ({
|
||||
default: () => <div>parameter-item</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../presets-parameter', () => ({
|
||||
default: ({
|
||||
onSelect,
|
||||
supportedParameterNames,
|
||||
}: {
|
||||
onSelect: (id: number) => void
|
||||
supportedParameterNames?: string[]
|
||||
}) => {
|
||||
if (supportedParameterNames && !supportedParameterNames.includes('temperature')) return null
|
||||
|
||||
return <button onClick={() => onSelect(1)}>Preset 1</button>
|
||||
},
|
||||
default: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../presets-parameter-utils', () => ({
|
||||
getSupportedPresetConfig: (_toneId: number, supportedParameterNames?: string[]) => {
|
||||
if (supportedParameterNames && !supportedParameterNames.includes('temperature')) return {}
|
||||
|
||||
return { temperature: 0.8 }
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../trigger', () => ({
|
||||
default: () => <button type="button">Open Settings</button>,
|
||||
}))
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return {
|
||||
...actual,
|
||||
PROVIDER_WITH_PRESET_TONE: ['openai'],
|
||||
}
|
||||
})
|
||||
|
||||
describe('ModelParameterModal', () => {
|
||||
const openSettings = () =>
|
||||
fireEvent.click(screen.getByRole('button', { name: /modelProvider\.modelSettings/i }))
|
||||
const defaultProps = {
|
||||
isAdvancedMode: false,
|
||||
modelId: 'gpt-3.5-turbo',
|
||||
provider: 'openai',
|
||||
setModel: vi.fn(),
|
||||
completionParams: { temperature: 0.7 },
|
||||
onCompletionParamsChange: vi.fn(),
|
||||
hideDebugWithMultipleModel: false,
|
||||
debugWithMultipleModel: false,
|
||||
onDebugWithMultipleModelChange: vi.fn(),
|
||||
readonly: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
isRulesLoading = false
|
||||
isRulesPending = false
|
||||
parameterRules = [
|
||||
{
|
||||
name: 'temperature',
|
||||
label: { en_US: 'Temperature' },
|
||||
type: 'float',
|
||||
default: 0.7,
|
||||
min: 0,
|
||||
max: 1,
|
||||
help: { en_US: 'Control randomness' },
|
||||
},
|
||||
]
|
||||
currentProvider = { provider: 'openai', label: { en_US: 'OpenAI' } }
|
||||
currentModel = {
|
||||
model: 'gpt-3.5-turbo',
|
||||
status: 'active',
|
||||
model_properties: { mode: 'chat' },
|
||||
}
|
||||
activeTextGenerationModelList = [
|
||||
{
|
||||
provider: 'openai',
|
||||
models: [
|
||||
{
|
||||
model: 'gpt-3.5-turbo',
|
||||
model_properties: { mode: 'chat' },
|
||||
features: ['vision'],
|
||||
},
|
||||
{
|
||||
model: 'gpt-4.1',
|
||||
model_properties: { mode: 'chat' },
|
||||
features: ['vision', 'tool-call'],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
it('should render trigger and open modal content when trigger is clicked', () => {
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
|
||||
openSettings()
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('param-temperature')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep model selection and model settings as separate actions', () => {
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('param-temperature')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('Select GPT-4.1'))
|
||||
|
||||
expect(defaultProps.setModel).toHaveBeenCalledWith({
|
||||
modelId: 'gpt-4.1',
|
||||
provider: 'openai',
|
||||
mode: 'chat',
|
||||
features: ['vision', 'tool-call'],
|
||||
})
|
||||
expect(screen.queryByTestId('param-temperature')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /modelProvider\.modelSettings/i }))
|
||||
|
||||
expect(screen.getByTestId('param-temperature')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable model settings when no model is selected', () => {
|
||||
render(<ModelParameterModal {...defaultProps} provider="" modelId="" />)
|
||||
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /modelProvider\.modelSettings/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should call onCompletionParamsChange when parameter changes and switch actions happen', () => {
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
|
||||
fireEvent.click(screen.getByText('Change'))
|
||||
expect(defaultProps.onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
...defaultProps.completionParams,
|
||||
temperature: 0.9,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('Remove'))
|
||||
expect(defaultProps.onCompletionParamsChange).toHaveBeenCalledWith({})
|
||||
|
||||
fireEvent.click(screen.getByText('Add'))
|
||||
expect(defaultProps.onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
...defaultProps.completionParams,
|
||||
temperature: 'assigned',
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onCompletionParamsChange when preset is selected', () => {
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
fireEvent.click(screen.getByText('Preset 1'))
|
||||
expect(defaultProps.onCompletionParamsChange).toHaveBeenCalledWith({
|
||||
...defaultProps.completionParams,
|
||||
temperature: 0.8,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render preset control when visible parameters do not support preset keys', () => {
|
||||
parameterRules = [
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: { en_US: 'Max Tokens' },
|
||||
type: 'int',
|
||||
default: 256,
|
||||
min: 1,
|
||||
max: 4096,
|
||||
},
|
||||
]
|
||||
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
|
||||
expect(screen.queryByText('Preset 1')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call setModel when model selector picks another model', () => {
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
fireEvent.click(screen.getByText('Select GPT-4.1'))
|
||||
|
||||
expect(defaultProps.setModel).toHaveBeenCalledWith({
|
||||
modelId: 'gpt-4.1',
|
||||
provider: 'openai',
|
||||
mode: 'chat',
|
||||
features: ['vision', 'tool-call'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should toggle debug mode when debug footer is clicked', () => {
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
fireEvent.click(screen.getByText(/debugAsMultipleModel/i))
|
||||
expect(defaultProps.onDebugWithMultipleModelChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render loading state when parameter rules are loading', () => {
|
||||
isRulesLoading = true
|
||||
isRulesPending = true
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render parameter loading when model is not configured and parameter rules query is pending but disabled', () => {
|
||||
isRulesPending = true
|
||||
parameterRules = []
|
||||
|
||||
render(<ModelParameterModal {...defaultProps} provider="" modelId="" />)
|
||||
openSettings()
|
||||
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not open content when readonly is true', () => {
|
||||
render(<ModelParameterModal {...defaultProps} readonly />)
|
||||
expect(screen.getByRole('button', { name: /modelProvider\.modelSettings/i })).toBeDisabled()
|
||||
expect(screen.queryByTestId('param-temperature')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render no parameter items when rules are undefined', () => {
|
||||
parameterRules = undefined
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
expect(screen.queryByTestId('param-temperature')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass nodesOutputVars and availableNodes to ParameterItem', () => {
|
||||
const mockNodesOutputVars = [{ nodeId: 'n1', title: 'Node', vars: [] }]
|
||||
const mockAvailableNodes = [{ id: 'n1', data: { title: 'Node', type: 'llm' } }]
|
||||
|
||||
it('opens provider settings from the model selector empty state', () => {
|
||||
render(
|
||||
<ModelParameterModal
|
||||
{...defaultProps}
|
||||
isInWorkflow
|
||||
nodesOutputVars={mockNodesOutputVars as never}
|
||||
availableNodes={mockAvailableNodes as never}
|
||||
/>,
|
||||
)
|
||||
|
||||
openSettings()
|
||||
|
||||
const paramEl = screen.getByTestId('param-temperature')
|
||||
expect(paramEl).toHaveAttribute('data-has-nodes-output-vars', 'true')
|
||||
expect(paramEl).toHaveAttribute('data-has-available-nodes', 'true')
|
||||
})
|
||||
|
||||
it('should support custom triggers, workflow mode, and missing default model values', async () => {
|
||||
render(
|
||||
<ModelParameterModal
|
||||
{...defaultProps}
|
||||
provider=""
|
||||
isAdvancedMode
|
||||
modelId=""
|
||||
isInWorkflow
|
||||
renderTrigger={({ open }) => <span>{open ? 'Custom Open' : 'Custom Closed'}</span>}
|
||||
provider=""
|
||||
completionParams={{}}
|
||||
setModel={vi.fn()}
|
||||
onCompletionParamsChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('Custom Closed'))
|
||||
fireEvent.click(screen.getByText('configure-empty-model'))
|
||||
|
||||
expect(screen.getByText('Custom Open')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('hide'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
|
||||
expect(mocks.openIntegrationsSetting).toHaveBeenCalledWith({
|
||||
payload: 'provider',
|
||||
})
|
||||
})
|
||||
|
||||
it('should append the stop parameter in advanced mode and show the single-model debug label', () => {
|
||||
render(<ModelParameterModal {...defaultProps} isAdvancedMode debugWithMultipleModel />)
|
||||
|
||||
openSettings()
|
||||
|
||||
expect(screen.getByTestId('param-stop')).toBeInTheDocument()
|
||||
expect(screen.getByText(/debugAsSingleModel/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the empty loading fallback when rules resolve to an empty list', () => {
|
||||
parameterRules = []
|
||||
isRulesLoading = true
|
||||
isRulesPending = true
|
||||
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('param-temperature')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should support custom trigger placement outside workflow mode', () => {
|
||||
render(
|
||||
<ModelParameterModal
|
||||
{...defaultProps}
|
||||
renderTrigger={({ open }) => <span>{open ? 'Popup Open' : 'Popup Closed'}</span>}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('Popup Closed'))
|
||||
|
||||
expect(screen.getByText('Popup Open')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
+42
-12
@@ -1,5 +1,5 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { DefaultModel, FormValue, ModelParameterRule } from '../declarations'
|
||||
import type { DefaultModel, FormValue, Model, ModelParameterRule } from '../declarations'
|
||||
import type { ParameterValue } from './parameter-item'
|
||||
import type { TriggerProps } from './types'
|
||||
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
@@ -7,8 +7,9 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE } from '@/config'
|
||||
import { useModelParameterRules } from '@/service/use-common'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '../hooks'
|
||||
@@ -34,8 +35,10 @@ export type ModelParameterModalProps = {
|
||||
debugWithMultipleModel?: boolean
|
||||
onDebugWithMultipleModelChange?: () => void
|
||||
renderTrigger?: (v: TriggerProps) => ReactNode
|
||||
triggerContainerClassName?: string
|
||||
readonly?: boolean
|
||||
isInWorkflow?: boolean
|
||||
modelList?: Model[]
|
||||
scope?: string
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
@@ -53,17 +56,25 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
debugWithMultipleModel,
|
||||
onDebugWithMultipleModelChange,
|
||||
renderTrigger,
|
||||
triggerContainerClassName,
|
||||
readonly,
|
||||
isInWorkflow,
|
||||
modelList,
|
||||
nodesOutputVars,
|
||||
availableNodes,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const { data: parameterRulesData, isLoading } = useModelParameterRules(provider, modelId)
|
||||
const isRulesLoading = !!provider && !!modelId && isLoading
|
||||
const { currentProvider, currentModel, activeTextGenerationModelList } =
|
||||
useTextGenerationCurrentProviderAndModelAndModelList({ provider, model: modelId })
|
||||
const availableTextGenerationModelList = modelList ?? activeTextGenerationModelList
|
||||
const selectedProvider =
|
||||
modelList?.find((modelItem) => modelItem.provider === provider) ?? currentProvider
|
||||
const selectedModel =
|
||||
selectedProvider?.models?.find((modelItem) => modelItem.model === modelId) ?? currentModel
|
||||
|
||||
const parameterRules: ModelParameterRule[] = useMemo(() => {
|
||||
return parameterRulesData?.data || []
|
||||
@@ -71,6 +82,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
const supportedPresetParameterNames = useMemo(() => {
|
||||
return parameterRules.map((parameterRule) => parameterRule.name)
|
||||
}, [parameterRules])
|
||||
const hasSelectedModel = !!provider && !!modelId
|
||||
|
||||
const handleParamChange = (key: string, value: ParameterValue) => {
|
||||
onCompletionParamsChange({
|
||||
@@ -80,10 +92,10 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
}
|
||||
|
||||
const handleChangeModel = ({ provider, model }: DefaultModel) => {
|
||||
const targetProvider = activeTextGenerationModelList.find(
|
||||
const targetProvider = availableTextGenerationModelList.find(
|
||||
(modelItem) => modelItem.provider === provider,
|
||||
)
|
||||
const targetModelItem = targetProvider?.models.find((modelItem) => modelItem.model === model)
|
||||
const targetModelItem = targetProvider?.models?.find((modelItem) => modelItem.model === model)
|
||||
setModel({
|
||||
modelId: model,
|
||||
provider,
|
||||
@@ -91,6 +103,15 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
features: targetModelItem?.features || [],
|
||||
})
|
||||
}
|
||||
const handleOpenModelSettings = () => {
|
||||
if (readonly || !hasSelectedModel) return
|
||||
setOpen(true)
|
||||
}
|
||||
const handleConfigureEmptyState = () => {
|
||||
if (readonly) return
|
||||
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
}
|
||||
|
||||
const handleSwitch = (key: string, value: boolean, assignValue: ParameterValue) => {
|
||||
if (!value) {
|
||||
@@ -114,8 +135,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const hasSelectedModel = !!provider && !!modelId
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
@@ -133,8 +152,8 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
>
|
||||
{renderTrigger({
|
||||
open,
|
||||
currentProvider,
|
||||
currentModel,
|
||||
currentProvider: selectedProvider,
|
||||
currentModel: selectedModel,
|
||||
providerName: provider,
|
||||
modelId,
|
||||
})}
|
||||
@@ -142,11 +161,16 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-8 min-w-[296px] items-center gap-px overflow-hidden rounded-lg">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-8 min-w-[296px] items-center gap-px overflow-hidden rounded-lg',
|
||||
triggerContainerClassName,
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<ModelSelector
|
||||
defaultModel={provider || modelId ? { provider, model: modelId } : undefined}
|
||||
modelList={activeTextGenerationModelList}
|
||||
modelList={availableTextGenerationModelList}
|
||||
readonly={readonly}
|
||||
triggerClassName={cn(
|
||||
'h-8! w-full rounded-r-none!',
|
||||
@@ -154,6 +178,8 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
'border border-workflow-block-parma-bg bg-workflow-block-parma-bg hover:bg-workflow-block-parma-bg',
|
||||
)}
|
||||
onSelect={handleChangeModel}
|
||||
onConfigureEmptyState={handleConfigureEmptyState}
|
||||
onOpenProviderSettings={handleOpenModelSettings}
|
||||
/>
|
||||
</div>
|
||||
<PopoverTrigger
|
||||
@@ -187,8 +213,9 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
<div className="px-4 pt-2 pb-4">
|
||||
<ModelSelector
|
||||
defaultModel={hasSelectedModel ? { provider, model: modelId } : undefined}
|
||||
modelList={activeTextGenerationModelList}
|
||||
modelList={availableTextGenerationModelList}
|
||||
onSelect={handleChangeModel}
|
||||
onOpenProviderSettings={handleOpenModelSettings}
|
||||
onHide={() => setOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
@@ -249,7 +276,10 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
{debugWithMultipleModel
|
||||
? t(($) => $.debugAsSingleModel, { ns: 'appDebug' })
|
||||
: t(($) => $.debugAsMultipleModel, { ns: 'appDebug' })}
|
||||
<ArrowNarrowLeft className="size-3 rotate-180" />
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-custom-vender-line-arrows-arrow-narrow-left size-3 rotate-180"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
|
||||
@@ -33,6 +33,7 @@ type ModelSelectorProps = {
|
||||
hideProviderSettingsFooter?: boolean
|
||||
onConfigureEmptyState?: () => void
|
||||
onOpenMarketplace?: () => void
|
||||
onOpenProviderSettings?: () => void
|
||||
providerSettingsSource?: 'agent'
|
||||
showModelMeta?: boolean
|
||||
modelPredicate?: ModelSelectorModelPredicate
|
||||
@@ -52,6 +53,7 @@ function ModelSelector({
|
||||
hideProviderSettingsFooter,
|
||||
onConfigureEmptyState,
|
||||
onOpenMarketplace,
|
||||
onOpenProviderSettings,
|
||||
providerSettingsSource,
|
||||
showModelMeta,
|
||||
modelPredicate,
|
||||
@@ -180,6 +182,7 @@ function ModelSelector({
|
||||
modelSuggestionPredicate={modelSuggestionPredicate}
|
||||
onConfigureEmptyState={onConfigureEmptyState ? handleConfigureEmptyState : undefined}
|
||||
onOpenMarketplace={onOpenMarketplace}
|
||||
onOpenProviderSettings={onOpenProviderSettings}
|
||||
onInputValueChange={setInputValue}
|
||||
onHide={handleHide}
|
||||
/>
|
||||
|
||||
+25
-18
@@ -9,7 +9,6 @@ import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CreditsCoin } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useCredentialPermissions } from '@/hooks/use-credential-permissions'
|
||||
@@ -57,7 +56,8 @@ function PopupItem({
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const currentProvider = modelProviders.find((provider) => provider.provider === model.provider)
|
||||
const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions()
|
||||
const canOpenCredentialDropdown = canUseCredential || canCreateCredential || canManageCredential
|
||||
const canOpenCredentialDropdown =
|
||||
!!currentProvider && (canUseCredential || canCreateCredential || canManageCredential)
|
||||
const handleOpenModelModal = () => {
|
||||
if (!canCreateCredential) return
|
||||
|
||||
@@ -77,7 +77,8 @@ function PopupItem({
|
||||
})
|
||||
}
|
||||
|
||||
const state = useCredentialPanelState(currentProvider)
|
||||
// oxlint-disable-next-line eslint-react/use-state -- This domain hook returns credential panel state, not a React useState tuple.
|
||||
const credentialPanelState = useCredentialPanelState(currentProvider)
|
||||
const { isChangingPriority, handleChangePriority } = useChangeProviderPriority(currentProvider)
|
||||
const groupItems = useMemo(
|
||||
() =>
|
||||
@@ -90,10 +91,11 @@ function PopupItem({
|
||||
[model.models, model.provider],
|
||||
)
|
||||
|
||||
const isUsingCredits = state.priority === 'credits'
|
||||
const hasCredits = !state.isCreditsExhausted
|
||||
const isApiKeyActive = state.variant === 'api-active' || state.variant === 'api-fallback'
|
||||
const { credentialName } = state
|
||||
const isUsingCredits = credentialPanelState.priority === 'credits'
|
||||
const hasCredits = !credentialPanelState.isCreditsExhausted
|
||||
const isApiKeyActive =
|
||||
credentialPanelState.variant === 'api-active' || credentialPanelState.variant === 'api-fallback'
|
||||
const { credentialName } = credentialPanelState
|
||||
|
||||
const handleCloseDropdown = useCallback(() => {
|
||||
setDropdownOpen(false)
|
||||
@@ -129,7 +131,10 @@ function PopupItem({
|
||||
{isUsingCredits ? (
|
||||
hasCredits ? (
|
||||
<>
|
||||
<CreditsCoin className="size-3" />
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-custom-vender-line-financeandecommerce-credits-coin size-3"
|
||||
/>
|
||||
<span className="ml-1 truncate">
|
||||
{t(($) => $['modelProvider.selector.aiCredits'], { ns: 'common' })}
|
||||
</span>
|
||||
@@ -161,15 +166,17 @@ function PopupItem({
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent placement="bottom-end">
|
||||
<DropdownContent
|
||||
provider={currentProvider}
|
||||
state={state}
|
||||
isChangingPriority={isChangingPriority}
|
||||
onChangePriority={handleChangePriority}
|
||||
onClose={handleCloseDropdown}
|
||||
/>
|
||||
</PopoverContent>
|
||||
{currentProvider && (
|
||||
<PopoverContent placement="bottom-end">
|
||||
<DropdownContent
|
||||
provider={currentProvider}
|
||||
state={credentialPanelState}
|
||||
isChangingPriority={isChangingPriority}
|
||||
onChangePriority={handleChangePriority}
|
||||
onClose={handleCloseDropdown}
|
||||
/>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
{!collapsed &&
|
||||
@@ -215,7 +222,7 @@ function PopupItem({
|
||||
</ModelName>
|
||||
</div>
|
||||
{defaultModel?.model === modelItem.model &&
|
||||
defaultModel.provider === currentProvider.provider && (
|
||||
defaultModel.provider === model.provider && (
|
||||
<ComboboxItemIndicator className="shrink-0 text-text-accent">
|
||||
<span
|
||||
className="i-custom-vender-line-general-check size-4"
|
||||
|
||||
+8
-1
@@ -69,6 +69,7 @@ export type PopupProps = {
|
||||
onConfigureEmptyState?: () => void
|
||||
onInputValueChange: (value: string) => void
|
||||
onOpenMarketplace?: () => void
|
||||
onOpenProviderSettings?: () => void
|
||||
onHide: () => void
|
||||
}
|
||||
function Popup({
|
||||
@@ -83,6 +84,7 @@ function Popup({
|
||||
onConfigureEmptyState,
|
||||
onInputValueChange,
|
||||
onOpenMarketplace,
|
||||
onOpenProviderSettings,
|
||||
onHide,
|
||||
}: PopupProps) {
|
||||
const { t } = useTranslation()
|
||||
@@ -255,11 +257,16 @@ function Popup({
|
||||
|
||||
const handleOpenSettings = useCallback(() => {
|
||||
onHide()
|
||||
if (onOpenProviderSettings) {
|
||||
onOpenProviderSettings()
|
||||
return
|
||||
}
|
||||
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
source: providerSettingsSource,
|
||||
})
|
||||
}, [onHide, openIntegrationsSetting, providerSettingsSource])
|
||||
}, [onHide, onOpenProviderSettings, openIntegrationsSetting, providerSettingsSource])
|
||||
const handleClosePreviewCard = useCallback(() => {
|
||||
previewCardHandle.close()
|
||||
}, [previewCardHandle])
|
||||
|
||||
@@ -564,6 +564,10 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
|
||||
expect(screen.getByRole('link', { name: /Agents/ })).toHaveAttribute('href', '/agents')
|
||||
expect(screen.getByRole('link', { name: /Agents common.menus.status/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.skills/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/skills',
|
||||
)
|
||||
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/datasets',
|
||||
@@ -584,6 +588,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', () => {
|
||||
@@ -743,6 +751,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',
|
||||
|
||||
@@ -32,6 +32,8 @@ export type DetailSidebarVisibilityOptions = Pick<
|
||||
const VISIBLE_TO_ALL: MainNavRouteVisibility = () => true
|
||||
const CAN_MANAGE_AGENTS: MainNavRouteVisibility = (options) => options.canManageAgents
|
||||
const CAN_USE_APP_DEPLOY: MainNavRouteVisibility = (options) => options.canUseAppDeploy
|
||||
const NOT_DATASET_OPERATOR: MainNavRouteVisibility = (options) =>
|
||||
!options.isCurrentWorkspaceDatasetOperator
|
||||
|
||||
function isPathUnderRoute(pathname: string, route: string) {
|
||||
return pathname === route || pathname.startsWith(`${route}/`)
|
||||
@@ -69,6 +71,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-ri-box-3-line',
|
||||
activeIcon: 'i-ri-box-3-fill',
|
||||
visibility: NOT_DATASET_OPERATOR,
|
||||
},
|
||||
{
|
||||
key: 'datasets',
|
||||
href: '/datasets',
|
||||
|
||||
+4
-2
@@ -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()
|
||||
|
||||
@@ -16,6 +16,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(),
|
||||
|
||||
@@ -27,6 +27,23 @@ export const viewport: Viewport = {
|
||||
viewportFit: 'cover',
|
||||
}
|
||||
|
||||
const resizeObserverErrorFilterScript = `
|
||||
(() => {
|
||||
const ignoredMessages = new Set([
|
||||
'ResizeObserver loop completed with undelivered notifications.',
|
||||
'ResizeObserver loop limit exceeded',
|
||||
]);
|
||||
const ignore = (event) => {
|
||||
const message = event?.message || event?.reason?.message;
|
||||
if (!ignoredMessages.has(message)) return;
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
};
|
||||
window.addEventListener('error', ignore, true);
|
||||
window.addEventListener('unhandledrejection', ignore, true);
|
||||
})();
|
||||
`
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const datasetMap = getDatasetMap()
|
||||
const queryClient = getQueryClientServer()
|
||||
@@ -43,6 +60,11 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<html lang={locale ?? 'en'} className="h-full" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<script
|
||||
nonce={nonce}
|
||||
// oxlint-disable-next-line eslint-react/dom-no-dangerously-set-innerhtml -- Static early listener must run before the dev error overlay registers.
|
||||
dangerouslySetInnerHTML={{ __html: resizeObserverErrorFilterScript }}
|
||||
/>
|
||||
<meta name="theme-color" content="#1C64F2" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+22
-7
@@ -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 = {
|
||||
@@ -171,10 +174,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: {
|
||||
@@ -191,10 +196,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>,
|
||||
@@ -642,8 +649,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()
|
||||
@@ -651,4 +658,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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,7 +23,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'
|
||||
|
||||
@@ -132,41 +135,43 @@ 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"
|
||||
label={showHeader ? undefined : orchestrateLabel}
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20'),
|
||||
scrollbar: hasBottomAction ? 'z-20' : undefined,
|
||||
}}
|
||||
>
|
||||
<AgentConfigApiContextProvider value={configApiContext}>
|
||||
<AgentOrchestrateAddActionsProvider>
|
||||
<AgentBuildDraftChangedKeysProvider
|
||||
changedKeys={
|
||||
isBuildDraftActive ? buildDraftChangedKeys : EMPTY_BUILD_DRAFT_CHANGED_KEYS
|
||||
}
|
||||
>
|
||||
<AgentModelField
|
||||
currentModel={currentModel}
|
||||
textGenerationModelList={textGenerationModelList}
|
||||
onSelect={onSelectModel}
|
||||
/>
|
||||
<AgentPromptEditor />
|
||||
<AgentSkills />
|
||||
<AgentFiles />
|
||||
<AgentTools />
|
||||
<AgentKnowledgeRetrieval />
|
||||
<AgentAdvancedSettings />
|
||||
</AgentBuildDraftChangedKeysProvider>
|
||||
</AgentOrchestrateAddActionsProvider>
|
||||
</AgentConfigApiContextProvider>
|
||||
</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"
|
||||
label={showHeader ? undefined : orchestrateLabel}
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20'),
|
||||
scrollbar: hasBottomAction ? 'z-20' : undefined,
|
||||
}}
|
||||
>
|
||||
<AgentConfigApiContextProvider value={configApiContext}>
|
||||
<AgentOrchestrateAddActionsProvider>
|
||||
<AgentBuildDraftChangedKeysProvider
|
||||
changedKeys={
|
||||
isBuildDraftActive ? buildDraftChangedKeys : EMPTY_BUILD_DRAFT_CHANGED_KEYS
|
||||
}
|
||||
>
|
||||
<AgentModelField
|
||||
currentModel={currentModel}
|
||||
textGenerationModelList={textGenerationModelList}
|
||||
onSelect={onSelectModel}
|
||||
/>
|
||||
<AgentPromptEditor />
|
||||
<AgentSkills />
|
||||
<AgentFiles />
|
||||
<AgentTools />
|
||||
<AgentKnowledgeRetrieval />
|
||||
<AgentAdvancedSettings />
|
||||
</AgentBuildDraftChangedKeysProvider>
|
||||
</AgentOrchestrateAddActionsProvider>
|
||||
</AgentConfigApiContextProvider>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</AgentOrchestrateReadOnlyContext>
|
||||
</AgentOrchestrateViewingVersionContext>
|
||||
|
||||
{orchestrateBottomAction ? (
|
||||
<AgentOrchestrateBottomActions shrinkOnOpen={!bottomAction}>
|
||||
|
||||
+23
-6
@@ -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', () => {
|
||||
|
||||
+5
@@ -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)
|
||||
}
|
||||
|
||||
+564
-20
@@ -1,3 +1,4 @@
|
||||
import type { SkillResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { AgentConfigApiContext } from '../../config-context'
|
||||
import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@@ -11,7 +12,10 @@ import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-compo
|
||||
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
|
||||
import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store'
|
||||
import { AgentConfigApiContextProvider } from '../../config-context'
|
||||
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
|
||||
import {
|
||||
AgentOrchestrateReadOnlyContext,
|
||||
AgentOrchestrateViewingVersionContext,
|
||||
} from '../../read-only-context'
|
||||
import { AgentSkills } from '../index'
|
||||
|
||||
type ConfigSkillInspectQueryOptionsInput = {
|
||||
@@ -39,10 +43,16 @@ type ConfigSkillDownloadQueryOptionsInput = {
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
agentSkillBindingsKey: vi.fn((_options: unknown): unknown[] => ['workspace-agent-skills']),
|
||||
agentSkillBindingsQueryOptions: vi.fn((_options: unknown) => ({})),
|
||||
deleteSkillMutationFn: vi.fn(async (_input: unknown) => ({
|
||||
removed_names: ['Tender Analyzer'],
|
||||
result: 'success',
|
||||
})),
|
||||
replaceAgentSkillBindingsMutationFn: vi.fn(async (input: { body: { skill_ids?: string[] } }) => ({
|
||||
agent_id: 'agent-1',
|
||||
skill_ids: input.body.skill_ids ?? [],
|
||||
})),
|
||||
uploadSkillMutationFn: vi.fn(async (_input: unknown) => ({
|
||||
config_version: { id: 'draft-1', kind: 'draft', writable: true },
|
||||
skill: {
|
||||
@@ -59,6 +69,8 @@ const mocks = vi.hoisted(() => ({
|
||||
inspectQueryOptions: vi.fn((_options: ConfigSkillInspectQueryOptionsInput) => ({})),
|
||||
previewQueryOptions: vi.fn((_options: ConfigSkillFileQueryOptionsInput) => ({})),
|
||||
downloadQueryOptions: vi.fn((_options: ConfigSkillFileQueryOptionsInput) => ({})),
|
||||
workspaceSkillsQueryOptions: vi.fn((_options: unknown) => ({})),
|
||||
workspaceSkillsInfiniteOptions: vi.fn((_options: unknown) => ({})),
|
||||
downloadBlob: vi.fn(),
|
||||
downloadUrl: vi.fn(),
|
||||
fetch: vi.fn(),
|
||||
@@ -165,9 +177,43 @@ vi.mock('@/service/client', () => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaces: {
|
||||
current: {
|
||||
agents: {
|
||||
byAgentId: {
|
||||
skills: {
|
||||
get: {
|
||||
key: mocks.agentSkillBindingsKey,
|
||||
queryOptions: mocks.agentSkillBindingsQueryOptions,
|
||||
},
|
||||
put: {
|
||||
mutationOptions: () => ({ mutationFn: mocks.replaceAgentSkillBindingsMutationFn }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
get: {
|
||||
queryOptions: mocks.workspaceSkillsQueryOptions,
|
||||
infiniteOptions: mocks.workspaceSkillsInfiniteOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
async function openUploadSkillDialog(user: ReturnType<typeof userEvent.setup>) {
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.upload\.label/i,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function ConfigSnapshotProbe() {
|
||||
const draft = useAtomValue(agentComposerDraftAtom)
|
||||
const configSnapshot = formStateToAgentSoulConfig({ formState: draft })
|
||||
@@ -175,6 +221,23 @@ function ConfigSnapshotProbe() {
|
||||
return <pre aria-label="config snapshot">{JSON.stringify(configSnapshot)}</pre>
|
||||
}
|
||||
|
||||
function createWorkspaceSkill(overrides: Partial<SkillResponse> = {}): SkillResponse {
|
||||
return {
|
||||
id: 'workspace-skill-1',
|
||||
name: 'refund-approval',
|
||||
display_name: 'Refund approval',
|
||||
description: 'Handle refund requests.',
|
||||
icon: '💳',
|
||||
latest_published_version_id: 'version-1',
|
||||
reference_count: 0,
|
||||
tags: [],
|
||||
visibility: 'workspace',
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function renderAgentSkills({
|
||||
initialDraft = {
|
||||
...defaultAgentSoulConfigFormState,
|
||||
@@ -189,10 +252,12 @@ function renderAgentSkills({
|
||||
},
|
||||
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: {
|
||||
@@ -205,10 +270,12 @@ function renderAgentSkills({
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AgentConfigApiContextProvider value={apiContext}>
|
||||
<AgentComposerProvider initialDraft={initialDraft}>
|
||||
<AgentOrchestrateReadOnlyContext value={readOnly}>
|
||||
<AgentSkills />
|
||||
<ConfigSnapshotProbe />
|
||||
</AgentOrchestrateReadOnlyContext>
|
||||
<AgentOrchestrateViewingVersionContext value={viewingVersion}>
|
||||
<AgentOrchestrateReadOnlyContext value={readOnly}>
|
||||
<AgentSkills />
|
||||
<ConfigSnapshotProbe />
|
||||
</AgentOrchestrateReadOnlyContext>
|
||||
</AgentOrchestrateViewingVersionContext>
|
||||
</AgentComposerProvider>
|
||||
</AgentConfigApiContextProvider>
|
||||
</QueryClientProvider>,
|
||||
@@ -225,6 +292,22 @@ describe('AgentSkills', () => {
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
}),
|
||||
)
|
||||
mocks.agentSkillBindingsKey.mockImplementation((options) => {
|
||||
const { input } = options as { input: { params: { agent_id: string } } }
|
||||
return ['workspace-agent-skills', input]
|
||||
})
|
||||
mocks.agentSkillBindingsQueryOptions.mockImplementation((options) => {
|
||||
const { input } = options as { input: { params: { agent_id: string } } }
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-agent-skills', input],
|
||||
queryFn: async () => ({
|
||||
agent_id: input.params.agent_id,
|
||||
skill_ids: [],
|
||||
data: [],
|
||||
}),
|
||||
}
|
||||
})
|
||||
mocks.inspectQueryOptions.mockImplementation(({ input }) => ({
|
||||
queryKey: ['inspect-skill', input],
|
||||
queryFn: async () => ({
|
||||
@@ -293,6 +376,38 @@ describe('AgentSkills', () => {
|
||||
url: `https://example.com/${input.params.name}.skill`,
|
||||
}),
|
||||
}))
|
||||
mocks.workspaceSkillsQueryOptions.mockImplementation((options) => {
|
||||
const { input } = options as { input: { query?: { keyword?: string } } }
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-skills', input],
|
||||
queryFn: async () => ({
|
||||
data: [],
|
||||
}),
|
||||
}
|
||||
})
|
||||
mocks.workspaceSkillsInfiniteOptions.mockImplementation((options) => {
|
||||
const { input, getNextPageParam, initialPageParam } = options as {
|
||||
input: (pageParam: number) => {
|
||||
query?: { keyword?: string; limit?: number; page?: number }
|
||||
}
|
||||
getNextPageParam: (lastPage: { has_more?: boolean; page?: number }) => number | undefined
|
||||
initialPageParam: number
|
||||
}
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-skills', input(initialPageParam)],
|
||||
queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({
|
||||
data: [],
|
||||
has_more: false,
|
||||
limit: input(pageParam).query?.limit ?? 20,
|
||||
page: pageParam,
|
||||
total: 0,
|
||||
}),
|
||||
getNextPageParam,
|
||||
initialPageParam,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -372,9 +487,7 @@ describe('AgentSkills', () => {
|
||||
const user = userEvent.setup()
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
await openUploadSkillDialog(user)
|
||||
|
||||
const input = await waitFor(() => {
|
||||
const element = document.querySelector('input[type="file"]')
|
||||
@@ -418,13 +531,375 @@ describe('AgentSkills', () => {
|
||||
expect(toast.success).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should hide skill package guidance before an upload fails', async () => {
|
||||
it('should bind workspace skills without adding them to inline config skills', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.workspaceSkillsInfiniteOptions.mockImplementation((options) => {
|
||||
const { input, getNextPageParam, initialPageParam } = options as {
|
||||
input: (pageParam: number) => {
|
||||
query?: { keyword?: string; limit?: number; page?: number }
|
||||
}
|
||||
getNextPageParam: (lastPage: { has_more?: boolean; page?: number }) => number | undefined
|
||||
initialPageParam: number
|
||||
}
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-skills', input(initialPageParam)],
|
||||
queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({
|
||||
data: [
|
||||
{
|
||||
id: 'workspace-skill-1',
|
||||
name: 'refund-approval',
|
||||
display_name: 'Refund approval',
|
||||
description: 'Handle refund requests.',
|
||||
icon: '💳',
|
||||
latest_published_version_id: 'version-1',
|
||||
reference_count: 0,
|
||||
tags: [],
|
||||
visibility: 'workspace',
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
limit: 20,
|
||||
page: pageParam,
|
||||
total: 1,
|
||||
}),
|
||||
getNextPageParam,
|
||||
initialPageParam,
|
||||
}
|
||||
})
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i,
|
||||
}),
|
||||
)
|
||||
await user.click(await screen.findByRole('button', { name: /Refund approval/ }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.replaceAgentSkillBindingsMutationFn.mock.calls[0]?.[0]).toEqual({
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
body: {
|
||||
skill_ids: ['workspace-skill-1'],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const snapshot = JSON.parse(screen.getByLabelText('config snapshot').textContent ?? '{}')
|
||||
expect(snapshot.config_skills).toEqual([])
|
||||
})
|
||||
|
||||
it('should allow workflow agent nodes to bind workspace skills', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.workspaceSkillsInfiniteOptions.mockImplementation((options) => {
|
||||
const { input, getNextPageParam, initialPageParam } = options as {
|
||||
input: (pageParam: number) => {
|
||||
query?: { keyword?: string; limit?: number; page?: number }
|
||||
}
|
||||
getNextPageParam: (lastPage: { has_more?: boolean; page?: number }) => number | undefined
|
||||
initialPageParam: number
|
||||
}
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-skills', input(initialPageParam)],
|
||||
queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({
|
||||
data: [
|
||||
{
|
||||
id: 'workspace-skill-1',
|
||||
name: 'refund-approval',
|
||||
display_name: 'Refund approval',
|
||||
description: 'Handle refund requests.',
|
||||
icon: '💳',
|
||||
latest_published_version_id: 'version-1',
|
||||
reference_count: 0,
|
||||
tags: [],
|
||||
visibility: 'workspace',
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
limit: 20,
|
||||
page: pageParam,
|
||||
total: 1,
|
||||
}),
|
||||
getNextPageParam,
|
||||
initialPageParam,
|
||||
}
|
||||
})
|
||||
renderAgentSkills({
|
||||
initialDraft: defaultAgentSoulConfigFormState,
|
||||
apiContext: {
|
||||
agentId: 'workflow-agent-1',
|
||||
draftType: 'draft',
|
||||
workflow: {
|
||||
appId: 'workflow-app-1',
|
||||
nodeId: 'agent-node-1',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
const workspaceMenuItem = screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i,
|
||||
})
|
||||
expect(workspaceMenuItem).not.toBeDisabled()
|
||||
|
||||
await user.click(workspaceMenuItem)
|
||||
await user.click(await screen.findByRole('button', { name: /Refund approval/ }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.replaceAgentSkillBindingsMutationFn.mock.calls[0]?.[0]).toEqual({
|
||||
params: {
|
||||
agent_id: 'workflow-agent-1',
|
||||
},
|
||||
body: {
|
||||
skill_ids: ['workspace-skill-1'],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark already bound workspace skills as added and prevent duplicate binding', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.agentSkillBindingsQueryOptions.mockImplementation((options) => {
|
||||
const { input } = options as { input: { params: { agent_id: string } } }
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-agent-skills', input],
|
||||
queryFn: async () => ({
|
||||
agent_id: input.params.agent_id,
|
||||
skill_ids: ['workspace-skill-1'],
|
||||
data: [
|
||||
{
|
||||
...createWorkspaceSkill(),
|
||||
priority: 0,
|
||||
status: 'published',
|
||||
file_count: 1,
|
||||
latest_published_at: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
})
|
||||
mocks.workspaceSkillsInfiniteOptions.mockImplementation((options) => {
|
||||
const { input, getNextPageParam, initialPageParam } = options as {
|
||||
input: (pageParam: number) => {
|
||||
query?: { keyword?: string; limit?: number; page?: number }
|
||||
}
|
||||
getNextPageParam: (lastPage: { has_more?: boolean; page?: number }) => number | undefined
|
||||
initialPageParam: number
|
||||
}
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-skills', input(initialPageParam)],
|
||||
queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({
|
||||
data: [
|
||||
createWorkspaceSkill(),
|
||||
createWorkspaceSkill({
|
||||
id: 'draft-skill',
|
||||
name: 'draft-skill',
|
||||
display_name: 'Draft skill',
|
||||
latest_published_version_id: null,
|
||||
}),
|
||||
],
|
||||
has_more: false,
|
||||
limit: 20,
|
||||
page: pageParam,
|
||||
total: 2,
|
||||
}),
|
||||
getNextPageParam,
|
||||
initialPageParam,
|
||||
}
|
||||
})
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.findByText('agentV2.agentDetail.configure.skills.workspaceSelector.added'),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('agentV2.agentDetail.configure.skills.workspaceSelector.draft'),
|
||||
).toBeInTheDocument()
|
||||
|
||||
const addedSkillButton = screen
|
||||
.getByText('agentV2.agentDetail.configure.skills.workspaceSelector.added')
|
||||
.closest('button')
|
||||
const draftSkillButton = screen
|
||||
.getByText('agentV2.agentDetail.configure.skills.workspaceSelector.draft')
|
||||
.closest('button')
|
||||
expect(addedSkillButton).toBeDisabled()
|
||||
expect(draftSkillButton).toBeDisabled()
|
||||
|
||||
expect(mocks.replaceAgentSkillBindingsMutationFn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fetch the next workspace skill page when scrolling the selector', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.workspaceSkillsInfiniteOptions.mockImplementation((options) => {
|
||||
const { input, getNextPageParam, initialPageParam } = options as {
|
||||
input: (pageParam: number) => {
|
||||
query?: { keyword?: string; limit?: number; page?: number }
|
||||
}
|
||||
getNextPageParam: (lastPage: { has_more?: boolean; page?: number }) => number | undefined
|
||||
initialPageParam: number
|
||||
}
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-skills', input(initialPageParam)],
|
||||
queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({
|
||||
data:
|
||||
pageParam === 1
|
||||
? [createWorkspaceSkill()]
|
||||
: [
|
||||
createWorkspaceSkill({
|
||||
id: 'workspace-skill-2',
|
||||
name: 'sales-follow-up',
|
||||
display_name: 'Sales follow-up',
|
||||
}),
|
||||
],
|
||||
has_more: pageParam === 1,
|
||||
limit: 20,
|
||||
page: pageParam,
|
||||
total: 2,
|
||||
}),
|
||||
getNextPageParam,
|
||||
initialPageParam,
|
||||
}
|
||||
})
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i,
|
||||
}),
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('Refund approval').length).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
const scrollContainer = document.querySelector('.overflow-y-auto')
|
||||
expect(scrollContainer).not.toBeNull()
|
||||
Object.defineProperties(scrollContainer!, {
|
||||
clientHeight: { configurable: true, value: 100 },
|
||||
scrollHeight: { configurable: true, value: 160 },
|
||||
scrollTop: { configurable: true, value: 80 },
|
||||
})
|
||||
fireEvent.scroll(scrollContainer!)
|
||||
|
||||
expect(await screen.findByText('Sales follow-up')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should remove workspace skill bindings from the configured agent', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.agentSkillBindingsQueryOptions.mockImplementation((options) => {
|
||||
const { input } = options as { input: { params: { agent_id: string } } }
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-agent-skills', input],
|
||||
queryFn: async () => ({
|
||||
agent_id: input.params.agent_id,
|
||||
skill_ids: ['workspace-skill-1'],
|
||||
data: [
|
||||
{
|
||||
...createWorkspaceSkill(),
|
||||
priority: 0,
|
||||
status: 'published',
|
||||
file_count: 1,
|
||||
latest_published_at: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
})
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.skills.moreActions:{"name":"Refund approval"}',
|
||||
}),
|
||||
)
|
||||
await user.click(await screen.findByText('agentV2.agentDetail.configure.skills.removeAction'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.replaceAgentSkillBindingsMutationFn.mock.calls[0]?.[0]).toEqual({
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
body: {
|
||||
skill_ids: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
'agentV2.agentDetail.configure.skills.workspaceSelector.removeSuccess',
|
||||
)
|
||||
})
|
||||
|
||||
it('should open workspace skill details in a new tab from the row menu', async () => {
|
||||
const user = userEvent.setup()
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
mocks.agentSkillBindingsQueryOptions.mockImplementation((options) => {
|
||||
const { input } = options as { input: { params: { agent_id: string } } }
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-agent-skills', input],
|
||||
queryFn: async () => ({
|
||||
agent_id: input.params.agent_id,
|
||||
skill_ids: ['workspace-skill-1'],
|
||||
data: [
|
||||
{
|
||||
...createWorkspaceSkill(),
|
||||
priority: 0,
|
||||
status: 'published',
|
||||
file_count: 1,
|
||||
latest_published_at: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
})
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.skills.moreActions:{"name":"Refund approval"}',
|
||||
}),
|
||||
)
|
||||
await user.click(await screen.findByText('agentV2.agentDetail.configure.skills.openInLibrary'))
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith(
|
||||
'/skills/workspace-skill-1',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
})
|
||||
|
||||
it('should hide skill package guidance before an upload fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await openUploadSkillDialog(user)
|
||||
|
||||
expect(
|
||||
screen.queryByText('agentV2.agentDetail.configure.skills.upload.warning.specification'),
|
||||
@@ -438,9 +913,7 @@ describe('AgentSkills', () => {
|
||||
.mockImplementationOnce(() => new Promise<never>(() => undefined))
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
await openUploadSkillDialog(user)
|
||||
const input = await waitFor(() => {
|
||||
const element = document.querySelector('input[type="file"]')
|
||||
expect(element).not.toBeNull()
|
||||
@@ -477,9 +950,7 @@ describe('AgentSkills', () => {
|
||||
mocks.uploadSkillMutationFn.mockRejectedValueOnce(new Error('Backend upload error'))
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
await openUploadSkillDialog(user)
|
||||
|
||||
const input = await waitFor(() => {
|
||||
const element = document.querySelector('input[type="file"]')
|
||||
@@ -515,9 +986,7 @@ describe('AgentSkills', () => {
|
||||
},
|
||||
})
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
await openUploadSkillDialog(user)
|
||||
const input = await waitFor(() => {
|
||||
const element = document.querySelector('input[type="file"]')
|
||||
expect(element).not.toBeNull()
|
||||
@@ -869,12 +1338,87 @@ describe('AgentSkills', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should disable add and remove actions when the section is read only', () => {
|
||||
const { container } = renderAgentSkills({ readOnly: true })
|
||||
it('should disable add and remove actions when viewing a version', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.agentSkillBindingsQueryOptions.mockImplementation((options) => {
|
||||
const { input } = options as { input: { params: { agent_id: string } } }
|
||||
|
||||
return {
|
||||
queryKey: ['workspace-agent-skills', input],
|
||||
queryFn: async () => ({
|
||||
agent_id: input.params.agent_id,
|
||||
skill_ids: ['workspace-skill-1'],
|
||||
data: [
|
||||
{
|
||||
...createWorkspaceSkill(),
|
||||
priority: 0,
|
||||
status: 'published',
|
||||
file_count: 1,
|
||||
latest_published_at: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
})
|
||||
const { container } = renderAgentSkills({
|
||||
apiContext: {
|
||||
agentId: 'agent-1',
|
||||
draftType: 'draft',
|
||||
versionId: 'version-1',
|
||||
},
|
||||
readOnly: true,
|
||||
viewingVersion: true,
|
||||
})
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(container.querySelector('[data-agent-skill-remove-button]')).toBeNull()
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.skills.moreActions:{"name":"Refund approval"}',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByText('agentV2.agentDetail.configure.skills.openInLibrary'),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('agentV2.agentDetail.configure.skills.removeAction'),
|
||||
).not.toBeInTheDocument()
|
||||
expect(mocks.replaceAgentSkillBindingsMutationFn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should keep the add menu available for build draft skills', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderAgentSkills({
|
||||
apiContext: {
|
||||
agentId: 'agent-1',
|
||||
draftType: 'debug_build',
|
||||
},
|
||||
initialDraft: {
|
||||
...defaultAgentSoulConfigFormState,
|
||||
skills: [],
|
||||
},
|
||||
readOnly: true,
|
||||
})
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.add/i,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i,
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.upload\.label/i,
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
+543
-16
@@ -1,30 +1,391 @@
|
||||
'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,
|
||||
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 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
|
||||
|
||||
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-tertiary', 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({ icon }: { icon?: string }) {
|
||||
return (
|
||||
<span className="flex size-5 shrink-0 items-center justify-center rounded-md border-[0.5px] border-divider-subtle bg-background-default-dodge">
|
||||
{icon ? (
|
||||
<span className="text-[12px] leading-none">{icon}</span>
|
||||
) : (
|
||||
<span aria-hidden className="i-ri-box-3-line size-3.5 text-text-tertiary" />
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkspaceSkillRow({
|
||||
disabled,
|
||||
isAdded,
|
||||
isPending,
|
||||
onSelect,
|
||||
onPreview,
|
||||
selected,
|
||||
skill,
|
||||
}: {
|
||||
disabled: boolean
|
||||
isAdded: boolean
|
||||
isPending: boolean
|
||||
onSelect: (skill: SkillResponse) => void
|
||||
onPreview: (skill: SkillResponse) => void
|
||||
selected: boolean
|
||||
skill: SkillResponse
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled || isAdded || isPending}
|
||||
onClick={() => onSelect(skill)}
|
||||
onFocus={() => onPreview(skill)}
|
||||
onMouseEnter={() => onPreview(skill)}
|
||||
className={cn(
|
||||
'flex h-12 w-full min-w-0 items-center gap-2 rounded-lg px-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-default disabled:opacity-60',
|
||||
selected && 'bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<WorkspaceSkillIcon icon={skill.icon} />
|
||||
<span className="flex w-0 min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="truncate system-sm-medium text-text-secondary">{skill.display_name}</span>
|
||||
<span className="truncate system-xs-regular text-text-tertiary">{skill.name}</span>
|
||||
</span>
|
||||
{isAdded && (
|
||||
<span className="shrink-0 system-xs-medium text-text-tertiary">
|
||||
{t(($) => $['agentDetail.configure.skills.workspaceSelector.added'])}
|
||||
</span>
|
||||
)}
|
||||
{!isAdded && disabled && (
|
||||
<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 h-full 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 h-full flex-col gap-4 p-4">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<WorkspaceSkillIcon icon={skill.icon} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate system-md-semibold text-text-primary">{skill.display_name}</div>
|
||||
<div className="mt-0.5 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="line-clamp-6 system-sm-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 [previewSkillId, setPreviewSkillId] = useState<string | undefined>(undefined)
|
||||
const debouncedKeyword = useDebounce(keyword.trim(), { wait: 300 })
|
||||
const skillsQuery = useInfiniteQuery({
|
||||
...consoleQuery.workspaces.current.skills.get.infiniteOptions({
|
||||
input: (pageParam) => ({
|
||||
query: {
|
||||
limit: WORKSPACE_SKILLS_PAGE_SIZE,
|
||||
page: Number(pageParam),
|
||||
...(debouncedKeyword ? { keyword: debouncedKeyword } : {}),
|
||||
},
|
||||
}),
|
||||
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 ?? []) ?? []
|
||||
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 className="flex h-[520px] w-[560px] overflow-hidden rounded-xl border border-divider-regular bg-components-panel-bg shadow-lg">
|
||||
<div className="flex min-w-0 flex-1 flex-col border-r border-divider-subtle">
|
||||
<div className="border-b border-divider-subtle p-3">
|
||||
<div className="relative">
|
||||
<SearchInput
|
||||
value={keyword}
|
||||
onValueChange={setKeyword}
|
||||
placeholder={t(($) => $['agentDetail.configure.skills.workspaceSelector.search'])}
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute top-1/2 right-8 i-ri-price-tag-3-line size-4 -translate-y-1/2 text-text-tertiary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-1.5" onScroll={handleListScroll}>
|
||||
{skillsQuery.isPending && (
|
||||
<div className="space-y-2 p-1">
|
||||
<SkeletonRectangle className="h-10 rounded-lg" />
|
||||
<SkeletonRectangle className="h-10 rounded-lg" />
|
||||
<SkeletonRectangle className="h-10 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}
|
||||
disabled={!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-2 p-1">
|
||||
<SkeletonRectangle className="h-10 rounded-lg" />
|
||||
<SkeletonRectangle className="h-10 rounded-lg" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
href="/skills"
|
||||
className="flex h-10 items-center justify-between border-t border-divider-subtle px-3 system-sm-medium text-text-secondary outline-hidden hover:bg-state-base-hover 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-4 text-text-tertiary" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="w-[240px] shrink-0 bg-background-default">
|
||||
<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 displayName = skill.display_name || skill.name
|
||||
const handleOpenInLibrary = useCallback(() => {
|
||||
window.open(`/skills/${skill.id}`, '_blank', 'noopener,noreferrer')
|
||||
}, [skill.id])
|
||||
|
||||
return (
|
||||
<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 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm">
|
||||
<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 py-1 pr-8 pl-2 text-left outline-hidden select-none focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
|
||||
>
|
||||
<WorkspaceSkillIcon icon={skill.icon} />
|
||||
<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',
|
||||
)}
|
||||
>
|
||||
{skill.name}
|
||||
</span>
|
||||
</Link>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t(($) => $['agentDetail.configure.skills.moreActions'], {
|
||||
name: displayName,
|
||||
})}
|
||||
className="absolute top-1/2 right-1 z-10 flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-text-tertiary 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:bg-state-base-hover data-popup-open:text-text-secondary"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-48">
|
||||
<DropdownMenuItem className="gap-2" onClick={handleOpenInLibrary}>
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-4 shrink-0" />
|
||||
<span>{t(($) => $['agentDetail.configure.skills.openInLibrary'])}</span>
|
||||
</DropdownMenuItem>
|
||||
{canRemove && (
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
className="gap-2"
|
||||
onClick={() => onRemove(skill.id)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
|
||||
<span>{t(($) => $['agentDetail.configure.skills.removeAction'])}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AgentSkills() {
|
||||
const { t } = useTranslation('agentV2')
|
||||
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,6 +398,68 @@ 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 { 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 replaceWorkspaceSkillBindings = useCallback(
|
||||
(skillIds: string[], onSuccess?: () => void) => {
|
||||
if (isViewingVersion) return
|
||||
|
||||
replaceAgentSkillBindings(
|
||||
{
|
||||
params: {
|
||||
agent_id: apiContext.agentId,
|
||||
},
|
||||
body: {
|
||||
skill_ids: skillIds,
|
||||
},
|
||||
},
|
||||
{
|
||||
onError: () => {
|
||||
toast.error(t(($) => $['agentDetail.configure.skills.workspaceSelector.saveFailed']))
|
||||
},
|
||||
onSuccess: () => {
|
||||
invalidateAgentSkillBindings()
|
||||
onSuccess?.()
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
[
|
||||
apiContext.agentId,
|
||||
invalidateAgentSkillBindings,
|
||||
isViewingVersion,
|
||||
replaceAgentSkillBindings,
|
||||
t,
|
||||
],
|
||||
)
|
||||
|
||||
const handleOpenUpload = useCallback((options?: AgentOrchestrateAddActionOptions) => {
|
||||
promptAddCallbackRef.current = options?.onAdded
|
||||
@@ -44,6 +467,20 @@ export function AgentSkills() {
|
||||
}, [])
|
||||
useRegisterAgentOrchestrateAddAction('skills', handleOpenUpload)
|
||||
|
||||
const handleAddMenuOpenChange = useCallback((open: boolean) => {
|
||||
setAddMenuOpen(open)
|
||||
if (!open) setAddMenuView('menu')
|
||||
}, [])
|
||||
|
||||
const handleOpenWorkspaceSelector = useCallback(() => {
|
||||
setAddMenuView('workspace-selector')
|
||||
}, [])
|
||||
|
||||
const handleOpenUploadFromMenu = useCallback(() => {
|
||||
setAddMenuOpen(false)
|
||||
handleOpenUpload()
|
||||
}, [handleOpenUpload])
|
||||
|
||||
const handleUploaded = useCallback(
|
||||
(skill: AgentSkill) => {
|
||||
upsertAgentSkill(skill)
|
||||
@@ -53,11 +490,36 @@ export function AgentSkills() {
|
||||
[upsertAgentSkill],
|
||||
)
|
||||
|
||||
const handleSelectWorkspaceSkill = useCallback(
|
||||
(skill: SkillResponse) => {
|
||||
if (!skill.latest_published_version_id || boundSkillIds.includes(skill.id)) return
|
||||
|
||||
replaceWorkspaceSkillBindings([...boundSkillIds, skill.id], () => {
|
||||
toast.success(t(($) => $['agentDetail.configure.skills.workspaceSelector.addSuccess']))
|
||||
setAddMenuOpen(false)
|
||||
setAddMenuView('menu')
|
||||
})
|
||||
},
|
||||
[boundSkillIds, 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),
|
||||
() => {
|
||||
toast.success(t(($) => $['agentDetail.configure.skills.workspaceSelector.removeSuccess']))
|
||||
},
|
||||
)
|
||||
},
|
||||
[boundSkillIds, replaceWorkspaceSkillBindings, t],
|
||||
)
|
||||
|
||||
const handleRemoveSkill = useCallback(
|
||||
(skillId: string) => {
|
||||
const skill = skills.find((item) => item.id === skillId)
|
||||
@@ -113,26 +575,91 @@ 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}
|
||||
popupClassName={
|
||||
addMenuView === 'menu'
|
||||
? 'w-[320px] bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]'
|
||||
: 'w-[560px] overflow-hidden border-none bg-transparent p-0 shadow-none'
|
||||
}
|
||||
>
|
||||
{addMenuView === 'menu' ? (
|
||||
<>
|
||||
<AgentSkillAddMenuItem
|
||||
iconClassName="i-custom-public-agent-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={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.length > 0 && (
|
||||
<div className="px-1 pt-1 pb-0.5 system-xs-medium-uppercase text-text-tertiary">
|
||||
{t(($) => $['agentDetail.configure.skills.fromSkillLibrary'])}
|
||||
</div>
|
||||
)}
|
||||
{workspaceSkills.map((skill) => (
|
||||
<WorkspaceAgentSkillItem
|
||||
key={skill.id}
|
||||
canRemove={!isViewingVersion}
|
||||
skill={skill}
|
||||
onRemove={handleRemoveWorkspaceSkill}
|
||||
/>
|
||||
))}
|
||||
{skills.map((skill) => (
|
||||
<AgentSkillItem
|
||||
key={skill.id}
|
||||
apiContext={apiContext}
|
||||
skill={skill}
|
||||
onRemove={handleRemoveSkill}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ConfigureSection>
|
||||
<AgentSkillUploadDialog
|
||||
|
||||
+28
-7
@@ -15,7 +15,10 @@ import {
|
||||
agentComposerSavedDraftAtom,
|
||||
isAgentComposerDirtyAtom,
|
||||
} from '@/features/agent-v2/agent-composer/store'
|
||||
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
|
||||
import {
|
||||
AgentOrchestrateReadOnlyContext,
|
||||
AgentOrchestrateViewingVersionContext,
|
||||
} from '../../read-only-context'
|
||||
import { AgentTools } from '../index'
|
||||
|
||||
const toolProviderState = vi.hoisted(() => ({
|
||||
@@ -340,7 +343,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: {
|
||||
@@ -352,9 +361,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>,
|
||||
)
|
||||
@@ -441,9 +452,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', {
|
||||
@@ -484,6 +495,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()
|
||||
|
||||
|
||||
@@ -36,7 +36,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 {
|
||||
@@ -400,7 +400,7 @@ function AddToolMenu({
|
||||
|
||||
export function AgentTools() {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const readOnly = useAgentOrchestrateReadOnly()
|
||||
const isViewingVersion = useAgentOrchestrateViewingVersion()
|
||||
const setProviderToolCredential = useSetAtom(setProviderToolCredentialAtom)
|
||||
const providerById = useAgentToolProviderMap()
|
||||
const tools = useAtomValue(agentComposerToolsAtom)
|
||||
@@ -533,7 +533,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 })}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Skills
|
||||
|
||||
Workspace Skill management UI. This module owns the Skills list, filters, and list-level actions.
|
||||
|
||||
## Internal Modules
|
||||
|
||||
None.
|
||||
|
||||
## External Modules
|
||||
|
||||
- app/components/base/search-input
|
||||
- app/components/base/skeleton
|
||||
- app/components/base/tooltip
|
||||
- hooks/use-document-title
|
||||
- hooks/use-timestamp
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,488 @@
|
||||
import type {
|
||||
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(),
|
||||
queryState: {
|
||||
keyword: '',
|
||||
tag: [] as string[],
|
||||
},
|
||||
skills: [] as SkillResponse[],
|
||||
skillPages: [] as SkillResponse[][],
|
||||
skillsKey: vi.fn((_options: unknown): unknown[] => ['skills']),
|
||||
skillsQueryOptions: vi.fn((_options: SkillsInfiniteOptions) => ({})),
|
||||
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-timestamp', () => ({
|
||||
default: () => ({
|
||||
formatTime: () => '2026-07-22 10:00',
|
||||
}),
|
||||
}))
|
||||
|
||||
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: {
|
||||
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 }),
|
||||
},
|
||||
duplicate: {
|
||||
post: {
|
||||
mutationOptions: () => ({ mutationFn: mocks.duplicateSkillMutationFn }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
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',
|
||||
reference_count: 2,
|
||||
created_at: 1784631405,
|
||||
updated_at: 1784638487,
|
||||
...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.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.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('agentV2.skillManagement.referenceCount:{"count":2}'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes keyword and selected tags to the list query', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderSkillsPage()
|
||||
|
||||
await user.type(
|
||||
await screen.findByRole('searchbox', {
|
||||
name: 'agentV2.skillManagement.searchLabel',
|
||||
}),
|
||||
'refund',
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const queryOptions = mocks.skillsQueryOptions.mock.lastCall?.[0]
|
||||
expect(queryOptions?.input(1)).toEqual({
|
||||
query: {
|
||||
keyword: 'refund',
|
||||
limit: 20,
|
||||
page: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'agentV2.skillManagement.tags' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('support').length).toBeGreaterThan(1)
|
||||
})
|
||||
await user.click(screen.getAllByText('support').at(-1)!)
|
||||
|
||||
await waitFor(() => {
|
||||
const queryOptions = mocks.skillsQueryOptions.mock.lastCall?.[0]
|
||||
expect(queryOptions?.input(1)).toEqual({
|
||||
query: {
|
||||
keyword: 'refund',
|
||||
limit: 20,
|
||||
page: 1,
|
||||
tag: ['support'],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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: 'agentV2.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()
|
||||
renderSkillsPage()
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'agentV2.skillManagement.create' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.createSkillMutationFn).toHaveBeenCalledWith(
|
||||
{
|
||||
body: {},
|
||||
},
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
expect(toast.success).toHaveBeenCalledWith('agentV2.skillManagement.createSuccess')
|
||||
expect(mocks.push).toHaveBeenCalledWith('/skills/created-skill')
|
||||
})
|
||||
|
||||
it('imports a package file and navigates to the imported skill', async () => {
|
||||
const user = userEvent.setup()
|
||||
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('agentV2.skillManagement.importSuccess')
|
||||
expect(mocks.push).toHaveBeenCalledWith('/skills/imported-skill')
|
||||
})
|
||||
|
||||
it('duplicates a skill from the card action menu', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderSkillsPage()
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'agentV2.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('agentV2.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: 'agentV2.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: 'agentV2.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: 'agentV2.skillManagement.moreActions:{"name":"Refund approval"}',
|
||||
}),
|
||||
)
|
||||
await user.click(await screen.findByText('common.operation.delete'))
|
||||
const dialog = await screen.findByRole('alertdialog')
|
||||
|
||||
expect(
|
||||
within(dialog).getByText(
|
||||
'agentV2.skillManagement.deleteDialog.referencedDescription:{"count":2}',
|
||||
),
|
||||
).toBeInTheDocument()
|
||||
|
||||
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('agentV2.skillManagement.deleteSuccess')
|
||||
})
|
||||
|
||||
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('agentV2.skillManagement.emptySearch')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('agentV2.skillManagement.emptyAction.createTitle'),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('agentV2.skillManagement.emptyAction.importTitle'),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,195 @@
|
||||
import type {
|
||||
SkillAssistAttachmentPayload,
|
||||
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
|
||||
},
|
||||
) {
|
||||
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: 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,
|
||||
}: {
|
||||
attachments?: SkillAssistAttachmentPayload[]
|
||||
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
|
||||
}) {
|
||||
return ssePost(
|
||||
`/workspaces/current/skills/${encodeURIComponent(skillId)}/assist/messages`,
|
||||
{
|
||||
body: {
|
||||
attachments,
|
||||
message,
|
||||
model,
|
||||
target_path: targetPath,
|
||||
},
|
||||
},
|
||||
{
|
||||
getAbortController,
|
||||
onCompleted,
|
||||
onData,
|
||||
onError,
|
||||
onUnhandledEvent,
|
||||
},
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,759 @@
|
||||
'use client'
|
||||
|
||||
import type { SkillResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { UIEvent } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
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 {
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { 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 useDocumentTitle from '@/hooks/use-document-title'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import { fetchSkillArchiveBlob } from './client'
|
||||
import { skillKeywordQueryParser, skillQueryParamNames, skillTagQueryParser } from './query-params'
|
||||
|
||||
const placeholderCardIds = Array.from(
|
||||
{ length: 16 },
|
||||
(_, index) => `skill-placeholder-card-${index}`,
|
||||
)
|
||||
const skeletonRows = ['primary', 'secondary', 'tertiary'] as const
|
||||
const SKILLS_PAGE_SIZE = 20
|
||||
|
||||
function skillsListQueryKey() {
|
||||
return consoleQuery.workspaces.current.skills.get.key({ type: 'query' })
|
||||
}
|
||||
|
||||
function SkillIcon({ icon }: { icon?: string }) {
|
||||
return (
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-[10px] border-[0.5px] border-divider-regular bg-background-default-dodge">
|
||||
{icon ? (
|
||||
<span className="system-lg-medium text-text-secondary">{icon}</span>
|
||||
) : (
|
||||
<span aria-hidden className="i-ri-box-3-line size-5 text-text-tertiary" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SkillTagBadge({ tag }: { tag: string }) {
|
||||
return (
|
||||
<span className="flex min-w-4 shrink-0 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1.5 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
<span className="max-w-28 truncate">{tag}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function SkillCardSkeleton() {
|
||||
return (
|
||||
<>
|
||||
{skeletonRows.map((row) => (
|
||||
<div
|
||||
key={row}
|
||||
className="relative h-42 rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3"
|
||||
>
|
||||
<div className="flex items-center gap-3 px-4 pt-4 pb-2">
|
||||
<SkeletonRectangle className="my-0 size-10 shrink-0 rounded-[10px] opacity-20" />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<SkeletonRectangle className="my-0 h-3 w-36 max-w-full rounded-md opacity-20" />
|
||||
<SkeletonRectangle className="my-0 h-2 w-24 max-w-full rounded-md opacity-12" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-1">
|
||||
<SkeletonRectangle className="my-0 h-2 w-full rounded-md opacity-12" />
|
||||
<SkeletonRectangle className="my-0 mt-2 h-2 w-3/4 rounded-md opacity-10" />
|
||||
</div>
|
||||
<div className="flex gap-1 px-4 pt-2">
|
||||
<SkeletonRectangle className="my-0 h-5 w-14 rounded-md opacity-12" />
|
||||
<SkeletonRectangle className="my-0 h-5 w-20 rounded-md opacity-10" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SkillPlaceholderState({
|
||||
creating,
|
||||
importing,
|
||||
isEmptySearch,
|
||||
onCreate,
|
||||
onImport,
|
||||
title,
|
||||
}: {
|
||||
creating?: boolean
|
||||
importing?: boolean
|
||||
isEmptySearch?: boolean
|
||||
onCreate?: () => void
|
||||
onImport?: () => void
|
||||
title: string
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="skill-placeholder-title"
|
||||
className="relative col-span-full min-h-[calc(100vh-142px)] overflow-hidden"
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] grid-rows-4 gap-3">
|
||||
{placeholderCardIds.map((id) => (
|
||||
<div key={id} className="rounded-xl bg-background-default-lighter opacity-75" />
|
||||
))}
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-background-body/0 to-background-body" />
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden p-2">
|
||||
<div className="flex w-[420px] max-w-full flex-col items-center gap-3">
|
||||
<div className="flex size-14 items-center justify-center rounded-[10px]">
|
||||
<div className="flex size-full min-w-px items-center justify-center overflow-hidden rounded-xl border border-dashed border-divider-regular bg-components-card-bg p-1 backdrop-blur-md">
|
||||
<span aria-hidden className="i-ri-box-3-line size-6 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
<h2
|
||||
id="skill-placeholder-title"
|
||||
className="system-sm-regular whitespace-nowrap text-text-tertiary"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{!isEmptySearch && (
|
||||
<div className="mt-2 flex w-full flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={creating || importing}
|
||||
className="flex h-11 w-full cursor-pointer items-center gap-3 rounded-xl bg-components-card-bg px-4 text-left shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={onCreate}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'size-4 shrink-0 text-text-tertiary',
|
||||
creating ? 'i-ri-loader-4-line animate-spin' : 'i-ri-sparkling-2-line',
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate system-sm-medium text-text-secondary">
|
||||
{t(($) => $['skillManagement.emptyAction.createTitle'])}
|
||||
</span>
|
||||
<span className="block truncate system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['skillManagement.emptyAction.createDescription'])}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={creating || importing}
|
||||
className="flex h-11 w-full cursor-pointer items-center gap-3 rounded-xl bg-components-card-bg px-4 text-left shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={onImport}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'size-4 shrink-0 text-text-tertiary',
|
||||
importing ? 'i-ri-loader-4-line animate-spin' : 'i-ri-upload-line',
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate system-sm-medium text-text-secondary">
|
||||
{t(($) => $['skillManagement.emptyAction.importTitle'])}
|
||||
</span>
|
||||
<span className="block truncate system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['skillManagement.emptyAction.importDescription'])}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function DeleteSkillDialog({
|
||||
open,
|
||||
skill,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean
|
||||
skill: SkillResponse
|
||||
onOpenChange: (open: boolean) => void
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const queryClient = useQueryClient()
|
||||
const deleteMutation = useMutation(
|
||||
consoleQuery.workspaces.current.skills.bySkillId.delete.mutationOptions(),
|
||||
)
|
||||
const referenceCount = skill.reference_count ?? 0
|
||||
const description =
|
||||
referenceCount > 0
|
||||
? t(($) => $['skillManagement.deleteDialog.referencedDescription'], {
|
||||
count: referenceCount,
|
||||
})
|
||||
: t(($) => $['skillManagement.deleteDialog.description'])
|
||||
|
||||
const handleDelete = () => {
|
||||
if (deleteMutation.isPending) return
|
||||
|
||||
deleteMutation.mutate(
|
||||
{
|
||||
params: {
|
||||
skill_id: skill.id,
|
||||
},
|
||||
body: {
|
||||
confirmation_name: skill.name,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t(($) => $['skillManagement.deleteSuccess']))
|
||||
void queryClient.invalidateQueries({ queryKey: skillsListQueryKey() })
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.workspaces.current.skills.tags.get.key({ type: 'query' }),
|
||||
})
|
||||
onOpenChange(false)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t(($) => $['skillManagement.deleteFailed']))
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent className="p-6">
|
||||
<AlertDialogTitle className="truncate title-2xl-semi-bold text-text-primary">
|
||||
{t(($) => $['skillManagement.deleteDialog.title'], { name: skill.display_name })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="mt-2 system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
||||
{description}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogActions className="p-0 pt-6">
|
||||
<AlertDialogCancelButton disabled={deleteMutation.isPending}>
|
||||
{tCommon(($) => $['operation.cancel'])}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton
|
||||
tone="destructive"
|
||||
loading={deleteMutation.isPending}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{tCommon(($) => $['operation.delete'])}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
function SkillCard({ skill }: { skill: SkillResponse }) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { formatTime } = useTimestamp()
|
||||
const queryClient = useQueryClient()
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false)
|
||||
const duplicateMutation = useMutation(
|
||||
consoleQuery.workspaces.current.skills.bySkillId.duplicate.post.mutationOptions(),
|
||||
)
|
||||
const exportMutation = useMutation({
|
||||
mutationFn: () => fetchSkillArchiveBlob(skill.id),
|
||||
onSuccess: (blob) => {
|
||||
downloadBlob({ data: blob, fileName: `${skill.name}.zip` })
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(tCommon(($) => $['operation.downloadFailed']))
|
||||
},
|
||||
})
|
||||
const tags = skill.tags ?? []
|
||||
const isDraft = !skill.latest_published_version_id
|
||||
const updatedAt = formatTime(
|
||||
skill.updated_at,
|
||||
t(($) => $['skillManagement.dateTimeFormat']),
|
||||
)
|
||||
|
||||
const handleDuplicate = () => {
|
||||
if (duplicateMutation.isPending) return
|
||||
|
||||
duplicateMutation.mutate(
|
||||
{
|
||||
params: {
|
||||
skill_id: skill.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t(($) => $['skillManagement.duplicateSuccess']))
|
||||
void queryClient.invalidateQueries({ queryKey: skillsListQueryKey() })
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.workspaces.current.skills.tags.get.key({ type: 'query' }),
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t(($) => $['skillManagement.duplicateFailed']))
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
if (exportMutation.isPending) return
|
||||
|
||||
exportMutation.mutate()
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="group relative col-span-1 h-42 min-w-0 overflow-hidden rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-shadow duration-200 ease-in-out hover:shadow-lg">
|
||||
<div className="flex h-full min-w-0 flex-col">
|
||||
<Link
|
||||
href={`/skills/${skill.id}`}
|
||||
className="block min-w-0 shrink-0 cursor-pointer outline-hidden"
|
||||
>
|
||||
<div className="flex items-center gap-3 px-4 pt-4 pb-2">
|
||||
<SkillIcon icon={skill.icon} />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5 py-px">
|
||||
<h2 className="truncate system-md-semibold text-text-secondary">
|
||||
{skill.display_name}
|
||||
</h2>
|
||||
<p className="truncate system-xs-regular text-text-tertiary">{skill.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-1 system-xs-regular text-text-tertiary">
|
||||
<div className="line-clamp-2 min-h-8">{skill.description}</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="relative h-6 shrink-0 px-3">
|
||||
{tags.length > 0 && (
|
||||
<div className="flex min-w-0 gap-1 overflow-hidden p-1">
|
||||
{tags.slice(0, 4).map((tag) => (
|
||||
<SkillTagBadge key={tag} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="pointer-events-none absolute top-0 right-0 bottom-0 w-14 bg-linear-to-r from-components-card-bg-transparent to-components-card-bg" />
|
||||
</div>
|
||||
<div className="flex min-w-0 shrink-0 items-center px-4 pt-2 pb-3 system-xs-regular text-text-tertiary">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1">
|
||||
<span className="shrink-0">
|
||||
{t(($) => $['skillManagement.referenceCount'], {
|
||||
count: skill.reference_count ?? 0,
|
||||
})}
|
||||
</span>
|
||||
<span aria-hidden className="shrink-0 text-text-quaternary">
|
||||
·
|
||||
</span>
|
||||
<span className="min-w-0 truncate">
|
||||
{isDraft
|
||||
? t(($) => $['skillManagement.editedAt'], { time: updatedAt })
|
||||
: t(($) => $['skillManagement.publishedAt'], { time: updatedAt })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isDraft && (
|
||||
<div className="absolute top-[-0.5px] right-0 flex h-5 items-start overflow-hidden">
|
||||
<div className="h-5 w-3 bg-background-section-burn [clip-path:polygon(0_0,100%_0,100%_100%)]" />
|
||||
<div className="flex h-5 items-center bg-background-section-burn pr-2 pl-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{t(($) => $['skillManagement.draft'])}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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(($) => $['skillManagement.moreActions'], { name: skill.display_name })}
|
||||
className="flex size-8 cursor-pointer items-center justify-center rounded-lg p-1.5 hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-popup-open:bg-state-base-hover"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t(($) => $['skillManagement.moreActions'], { name: skill.display_name })}
|
||||
</span>
|
||||
<span aria-hidden className="i-ri-more-fill size-4.5 text-text-tertiary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-40">
|
||||
<DropdownMenuItem className="gap-2" onClick={handleDuplicate}>
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary"
|
||||
/>
|
||||
<span>{tCommon(($) => $['operation.duplicate'])}</span>
|
||||
</DropdownMenuItem>
|
||||
{skill.latest_published_version_id && (
|
||||
<DropdownMenuItem className="gap-2" onClick={handleExport}>
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-ri-download-2-line size-4 shrink-0 text-text-tertiary"
|
||||
/>
|
||||
<span>{tCommon(($) => $['operation.export'])}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
className="gap-2"
|
||||
onClick={() => setIsDeleteOpen(true)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
|
||||
<span>{tCommon(($) => $['operation.delete'])}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<DeleteSkillDialog skill={skill} open={isDeleteOpen} onOpenChange={setIsDeleteOpen} />
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function SkillTagFilter({ tags }: { tags: string[] }) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const [selectedTags, setSelectedTags] = useQueryState(
|
||||
skillQueryParamNames.tag,
|
||||
skillTagQueryParser,
|
||||
)
|
||||
const selectedTagSet = new Set(selectedTags)
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
const nextTags = selectedTagSet.has(tag)
|
||||
? selectedTags.filter((item) => item !== tag)
|
||||
: [...selectedTags, tag]
|
||||
|
||||
void setSelectedTags(nextTags)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
'flex h-8 shrink-0 cursor-pointer items-center gap-1 rounded-lg bg-components-input-bg-normal px-2 py-1 system-sm-regular text-text-tertiary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
selectedTags.length > 0 && 'text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<span>{t(($) => $['skillManagement.tags'])}</span>
|
||||
{selectedTags.length > 0 && (
|
||||
<span className="flex min-w-4 shrink-0 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary tabular-nums">
|
||||
{selectedTags.length}
|
||||
</span>
|
||||
)}
|
||||
<span aria-hidden className="i-ri-arrow-down-s-line size-4 text-text-tertiary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="bottom-start" sideOffset={4} popupClassName="w-52">
|
||||
{tags.length === 0 ? (
|
||||
<DropdownMenuItem disabled>{t(($) => $['skillManagement.noTags'])}</DropdownMenuItem>
|
||||
) : (
|
||||
tags.map((tag) => (
|
||||
<DropdownMenuItem key={tag} className="gap-2" onClick={() => toggleTag(tag)}>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'i-ri-check-line size-4 shrink-0',
|
||||
selectedTagSet.has(tag) ? 'text-text-accent' : 'text-transparent',
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate">{tag}</span>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
{selectedTags.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2" onClick={() => setSelectedTags([])}>
|
||||
<span aria-hidden className="i-ri-close-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span>{t(($) => $['skillManagement.clearTags'])}</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function SkillsToolbar({
|
||||
creating,
|
||||
importing,
|
||||
onCreate,
|
||||
onImport,
|
||||
tags,
|
||||
}: {
|
||||
creating: boolean
|
||||
importing: boolean
|
||||
onCreate: () => void
|
||||
onImport: () => void
|
||||
tags: string[]
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const [keyword, setKeyword] = useQueryState(skillQueryParamNames.keyword, skillKeywordQueryParser)
|
||||
const isMutating = creating || importing
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<SkillTagFilter tags={tags} />
|
||||
<SearchInput
|
||||
aria-label={t(($) => $['skillManagement.searchLabel'])}
|
||||
className="h-8 w-50 min-w-0 shrink"
|
||||
placeholder={t(($) => $['skillManagement.searchPlaceholder'])}
|
||||
value={keyword}
|
||||
onValueChange={(value) => {
|
||||
void setKeyword(value)
|
||||
}}
|
||||
/>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
className="h-8 gap-1 px-3"
|
||||
disabled={isMutating}
|
||||
loading={importing}
|
||||
onClick={onImport}
|
||||
>
|
||||
<span aria-hidden className="i-ri-upload-line size-4" />
|
||||
<span className="px-0.5 system-sm-medium">{t(($) => $['skillManagement.import'])}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="h-8 gap-0.5 px-3"
|
||||
disabled={isMutating}
|
||||
loading={creating}
|
||||
onClick={onCreate}
|
||||
>
|
||||
<span aria-hidden className="i-ri-add-line size-4" />
|
||||
<span className="px-0.5 system-sm-medium">{t(($) => $['skillManagement.create'])}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SkillGrid({
|
||||
creating,
|
||||
importing,
|
||||
isEmptySearch,
|
||||
isError,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
isPending,
|
||||
onCreate,
|
||||
onImport,
|
||||
skills,
|
||||
}: {
|
||||
creating: boolean
|
||||
importing: boolean
|
||||
isEmptySearch: boolean
|
||||
isError: boolean
|
||||
isFetching: boolean
|
||||
isFetchingNextPage: boolean
|
||||
isPending: boolean
|
||||
onCreate: () => void
|
||||
onImport: () => void
|
||||
skills: SkillResponse[]
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={t(($) => $['skillManagement.listLabel'])}
|
||||
className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5"
|
||||
aria-busy={isFetching || undefined}
|
||||
>
|
||||
{isPending && <SkillCardSkeleton />}
|
||||
{!isPending && isError && (
|
||||
<SkillPlaceholderState title={t(($) => $['skillManagement.loadingError'])} />
|
||||
)}
|
||||
{!isPending && !isError && skills.length === 0 && (
|
||||
<SkillPlaceholderState
|
||||
creating={creating}
|
||||
importing={importing}
|
||||
isEmptySearch={isEmptySearch}
|
||||
onCreate={onCreate}
|
||||
onImport={onImport}
|
||||
title={
|
||||
isEmptySearch
|
||||
? t(($) => $['skillManagement.emptySearch'])
|
||||
: t(($) => $['skillManagement.empty'])
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !isError && skills.map((skill) => <SkillCard key={skill.id} skill={skill} />)}
|
||||
{!isPending && !isError && isFetchingNextPage && <SkillCardSkeleton />}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SkillsPage() {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const importInputRef = useRef<HTMLInputElement>(null)
|
||||
const [keyword] = useQueryState(skillQueryParamNames.keyword, skillKeywordQueryParser)
|
||||
const [selectedTags] = useQueryState(skillQueryParamNames.tag, skillTagQueryParser)
|
||||
const debouncedKeyword = useDebounce(keyword.trim(), { wait: 300 })
|
||||
const createMutation = useMutation(consoleQuery.workspaces.current.skills.post.mutationOptions())
|
||||
const importMutation = useMutation(
|
||||
consoleQuery.workspaces.current.skills.import.post.mutationOptions(),
|
||||
)
|
||||
const skillsQuery = useInfiniteQuery({
|
||||
...consoleQuery.workspaces.current.skills.get.infiniteOptions({
|
||||
input: (pageParam) => ({
|
||||
query: {
|
||||
limit: SKILLS_PAGE_SIZE,
|
||||
page: Number(pageParam),
|
||||
...(debouncedKeyword ? { keyword: debouncedKeyword } : {}),
|
||||
...(selectedTags.length > 0 ? { tag: selectedTags } : {}),
|
||||
},
|
||||
}),
|
||||
getNextPageParam: (lastPage) => (lastPage.has_more ? (lastPage.page ?? 1) + 1 : undefined),
|
||||
initialPageParam: 1,
|
||||
}),
|
||||
})
|
||||
const tagsQuery = useQuery(consoleQuery.workspaces.current.skills.tags.get.queryOptions())
|
||||
const skills = skillsQuery.data?.pages.flatMap((page) => page.data ?? []) ?? []
|
||||
const tags = (tagsQuery.data?.data ?? []).map((tag) => tag.tag)
|
||||
|
||||
useDocumentTitle(t(($) => $['skillManagement.title']))
|
||||
|
||||
const invalidateSkills = () => {
|
||||
void queryClient.invalidateQueries({ queryKey: skillsListQueryKey() })
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.workspaces.current.skills.tags.get.key({ type: 'query' }),
|
||||
})
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (createMutation.isPending) return
|
||||
|
||||
createMutation.mutate(
|
||||
{
|
||||
body: {},
|
||||
},
|
||||
{
|
||||
onSuccess: (skill) => {
|
||||
toast.success(t(($) => $['skillManagement.createSuccess']))
|
||||
invalidateSkills()
|
||||
router.push(`/skills/${skill.id}`)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t(($) => $['skillManagement.createFailed']))
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleFileChange = (file: File | undefined) => {
|
||||
if (!file || importMutation.isPending) return
|
||||
|
||||
importMutation.mutate(
|
||||
{
|
||||
body: {
|
||||
file,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (skill) => {
|
||||
toast.success(t(($) => $['skillManagement.importSuccess']))
|
||||
invalidateSkills()
|
||||
router.push(`/skills/${skill.id}`)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t(($) => $['skillManagement.importFailed']))
|
||||
},
|
||||
onSettled: () => {
|
||||
if (importInputRef.current) importInputRef.current.value = ''
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleListScroll = (event: UIEvent<HTMLDivElement>) => {
|
||||
const target = event.currentTarget
|
||||
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight
|
||||
if (scrollBottom < 80 && skillsQuery.hasNextPage && !skillsQuery.isFetchingNextPage)
|
||||
void skillsQuery.fetchNextPage()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-0 min-w-0 grow flex-col overflow-hidden bg-background-body">
|
||||
<div className="shrink-0 bg-background-body px-8 pt-4 pb-2">
|
||||
<div className="flex h-6 min-w-0 items-center justify-between gap-4">
|
||||
<h1 className="min-w-0 flex-1 truncate text-[18px]/[21.6px] font-semibold text-text-primary">
|
||||
{t(($) => $['skillManagement.title'])}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-3.5">
|
||||
<input
|
||||
ref={importInputRef}
|
||||
type="file"
|
||||
accept=".zip,.skill,application/zip"
|
||||
className="hidden"
|
||||
onChange={(event) => handleFileChange(event.currentTarget.files?.[0])}
|
||||
/>
|
||||
<SkillsToolbar
|
||||
creating={createMutation.isPending}
|
||||
importing={importMutation.isPending}
|
||||
onCreate={handleCreate}
|
||||
onImport={() => importInputRef.current?.click()}
|
||||
tags={tags}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<ScrollAreaRoot className="relative h-full min-h-0 min-w-0 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
tabIndex={-1}
|
||||
className="overscroll-contain"
|
||||
onScroll={handleListScroll}
|
||||
>
|
||||
<ScrollAreaContent className="min-h-full px-8 pt-2 pb-8">
|
||||
<SkillGrid
|
||||
creating={createMutation.isPending}
|
||||
importing={importMutation.isPending}
|
||||
skills={skills}
|
||||
isEmptySearch={!!debouncedKeyword || selectedTags.length > 0}
|
||||
isError={skillsQuery.isError}
|
||||
isFetching={skillsQuery.isFetching}
|
||||
isFetchingNextPage={skillsQuery.isFetchingNextPage}
|
||||
isPending={skillsQuery.isPending}
|
||||
onCreate={handleCreate}
|
||||
onImport={() => importInputRef.current?.click()}
|
||||
/>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { debounce, parseAsArrayOf, parseAsString } from 'nuqs'
|
||||
|
||||
export const skillQueryParamNames = {
|
||||
keyword: 'keyword',
|
||||
tag: 'tag',
|
||||
} as const
|
||||
|
||||
export const skillKeywordQueryParser = parseAsString.withDefault('').withOptions({
|
||||
limitUrlUpdates: debounce(300),
|
||||
})
|
||||
|
||||
export const skillTagQueryParser = parseAsArrayOf(parseAsString, ';').withDefault([])
|
||||
@@ -203,15 +203,22 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "يشغل Preview الوكيل المكتمل كما سيراه المستخدمون، مع ردود واضحة وميزات الدردشة.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "عاين وكيلك",
|
||||
"agentDetail.configure.skills.add": "إضافة مهارة",
|
||||
"agentDetail.configure.skills.addMenu.upload.badge": "EMBEDDED",
|
||||
"agentDetail.configure.skills.addMenu.upload.label": "Upload package",
|
||||
"agentDetail.configure.skills.addMenu.workspace.label": "From skill library",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "محتوى تفاصيل المهارة",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} ملفات",
|
||||
"agentDetail.configure.skills.detail.files": "الملفات",
|
||||
"agentDetail.configure.skills.empty.description": "تمنح المهارات الوكيل خبرة قابلة لإعادة الاستخدام يمكنه استدعاؤها أثناء العمل",
|
||||
"agentDetail.configure.skills.empty.title": "لا توجد مهارات بعد",
|
||||
"agentDetail.configure.skills.fromSkillLibrary": "From skill library",
|
||||
"agentDetail.configure.skills.itemType": "مهارة",
|
||||
"agentDetail.configure.skills.label": "المهارات",
|
||||
"agentDetail.configure.skills.missing": "المهارة غير موجودة",
|
||||
"agentDetail.configure.skills.moreActions": "More actions for {{name}}",
|
||||
"agentDetail.configure.skills.openInLibrary": "Open in Skill library",
|
||||
"agentDetail.configure.skills.remove": "إزالة {{name}}",
|
||||
"agentDetail.configure.skills.removeAction": "Remove",
|
||||
"agentDetail.configure.skills.richTip": "اجمع التعليمات والملفات والبرامج النصية لمهمة متكررة في Skill. أشر إليها باستخدام / في Prompt. <docLink>معرفة المزيد</docLink>\n\nفي وضع Build، يمكن للوكيل إعدادها لك.",
|
||||
"agentDetail.configure.skills.tip": "اجمع التعليمات والملفات والبرامج النصية لمهمة متكررة في Skill. أشر إليها باستخدام / في Prompt. معرفة المزيد\n\nفي وضع Build، يمكن للوكيل إعدادها لك.",
|
||||
"agentDetail.configure.skills.toggle": "تبديل المهارات",
|
||||
@@ -421,5 +428,117 @@
|
||||
"roster.sort.optionsLabel": "خيارات الفرز",
|
||||
"roster.sort.recentlyCreated": "الأحدث إنشاءً",
|
||||
"roster.updateSuccess": "تم تحديث الوكيل.",
|
||||
"roster.usageStatus.draft": "مسودة"
|
||||
"roster.usageStatus.draft": "مسودة",
|
||||
"skillManagement.clearTags": "Clear tags",
|
||||
"skillManagement.create": "Create",
|
||||
"skillManagement.createFailed": "Failed to create skill.",
|
||||
"skillManagement.createSuccess": "Skill created.",
|
||||
"skillManagement.dateTimeFormat": "MMM D, YYYY HH:mm",
|
||||
"skillManagement.deleteDialog.description": "This skill will be removed from the workspace. Agents that reference it may lose access to this capability.",
|
||||
"skillManagement.deleteDialog.title": "Delete {{name}}?",
|
||||
"skillManagement.deleteFailed": "Failed to delete skill.",
|
||||
"skillManagement.deleteSuccess": "Skill deleted.",
|
||||
"skillManagement.detail.addMetadata": "Add metadata",
|
||||
"skillManagement.detail.addMetadataDescription": "Add a frontmatter field to this Markdown file.",
|
||||
"skillManagement.detail.addTag": "Add tag",
|
||||
"skillManagement.detail.addTagDescription": "Create or bind a tag to this Skill. New tags are saved with the Skill metadata.",
|
||||
"skillManagement.detail.addTagSuccess": "Tag added.",
|
||||
"skillManagement.detail.back": "Back to Skills",
|
||||
"skillManagement.detail.cancelAddMetadata": "Cancel adding metadata",
|
||||
"skillManagement.detail.closeFileTab": "Close {{name}}",
|
||||
"skillManagement.detail.collapseSidebar": "طي الشريط الجانبي",
|
||||
"skillManagement.detail.createFile": "New file",
|
||||
"skillManagement.detail.createFileDescription": "Enter a file path relative to the skill root.",
|
||||
"skillManagement.detail.createFileMenu": "New file...",
|
||||
"skillManagement.detail.createFileSuccess": "File created.",
|
||||
"skillManagement.detail.createFolder": "New folder",
|
||||
"skillManagement.detail.createFolderDescription": "Enter a folder path relative to the skill root.",
|
||||
"skillManagement.detail.createFolderMenu": "New folder...",
|
||||
"skillManagement.detail.createFolderSuccess": "Folder created.",
|
||||
"skillManagement.detail.createdBy": "Created by {{name}}",
|
||||
"skillManagement.detail.currentDraft": "Current draft",
|
||||
"skillManagement.detail.deleteFileConfirm": "Delete this file?",
|
||||
"skillManagement.detail.deleteFileSuccess": "File deleted.",
|
||||
"skillManagement.detail.deleteVersionConfirm": "Delete this version?",
|
||||
"skillManagement.detail.deleteVersionFailed": "Failed to delete version.",
|
||||
"skillManagement.detail.deleteVersionSuccess": "Version deleted.",
|
||||
"skillManagement.detail.downloadFile": "Download file",
|
||||
"skillManagement.detail.draft": "Draft",
|
||||
"skillManagement.detail.expandSidebar": "توسيع الشريط الجانبي",
|
||||
"skillManagement.detail.fileCount": "{{count}} FILES",
|
||||
"skillManagement.detail.fileMeta": "{{type}} · {{size}} bytes",
|
||||
"skillManagement.detail.fileMissing": "File not found.",
|
||||
"skillManagement.detail.fileOperationFailed": "File operation failed.",
|
||||
"skillManagement.detail.files": "Files",
|
||||
"skillManagement.detail.latest": "Latest",
|
||||
"skillManagement.detail.loadFailed": "Failed to load skill.",
|
||||
"skillManagement.detail.markdownLiveMode": "Live preview",
|
||||
"skillManagement.detail.markdownSourceMode": "Source editor",
|
||||
"skillManagement.detail.metadataKey": "name",
|
||||
"skillManagement.detail.metadataValue": "value",
|
||||
"skillManagement.detail.moveFileSuccess": "File moved.",
|
||||
"skillManagement.detail.moveFilesSuccess": "Files moved.",
|
||||
"skillManagement.detail.noFileSelected": "Select a file to preview.",
|
||||
"skillManagement.detail.noFiles": "No files.",
|
||||
"skillManagement.detail.noSearchResults": "لا توجد ملفات مطابقة.",
|
||||
"skillManagement.detail.noVersions": "No published versions yet.",
|
||||
"skillManagement.detail.previewUnsupported": "Preview is not supported for this file.",
|
||||
"skillManagement.detail.publish": "Publish",
|
||||
"skillManagement.detail.publishFailed": "Failed to publish skill.",
|
||||
"skillManagement.detail.publishSuccess": "Skill published.",
|
||||
"skillManagement.detail.published": "Published",
|
||||
"skillManagement.detail.readonly": "Read only",
|
||||
"skillManagement.detail.referenceFiles.confirm": "Enter Confirm",
|
||||
"skillManagement.detail.referenceFiles.empty": "No files available.",
|
||||
"skillManagement.detail.referenceFiles.livePlaceholder": "Write instructions for the agent, type / to reference files",
|
||||
"skillManagement.detail.referenceFiles.navigate": "↑↓ Navigate",
|
||||
"skillManagement.detail.referenceFiles.title": "Reference files",
|
||||
"skillManagement.detail.referencedBy": "Referenced by {{count}} Apps",
|
||||
"skillManagement.detail.removeMetadata": "Delete {{name}} metadata",
|
||||
"skillManagement.detail.removeTag": "Remove {{tag}}",
|
||||
"skillManagement.detail.removeTagSuccess": "Tag removed.",
|
||||
"skillManagement.detail.renameFile": "Rename file",
|
||||
"skillManagement.detail.renameFileDescription": "Enter the new path relative to the skill root.",
|
||||
"skillManagement.detail.renameFileSuccess": "File renamed.",
|
||||
"skillManagement.detail.renameVersion": "Rename",
|
||||
"skillManagement.detail.renameVersionFailed": "Failed to rename version.",
|
||||
"skillManagement.detail.renameVersionPrompt": "Version name",
|
||||
"skillManagement.detail.renameVersionSuccess": "Version renamed.",
|
||||
"skillManagement.detail.restoreVersion": "Restore",
|
||||
"skillManagement.detail.restoreVersionFailed": "Failed to restore version.",
|
||||
"skillManagement.detail.restoreVersionSuccess": "Version restored to draft.",
|
||||
"skillManagement.detail.save": "Save",
|
||||
"skillManagement.detail.saveFailed": "Failed to save file.",
|
||||
"skillManagement.detail.saveSuccess": "File saved.",
|
||||
"skillManagement.detail.saved": "Saved",
|
||||
"skillManagement.detail.savedAt": "Saved {{time}}",
|
||||
"skillManagement.detail.saving": "Saving...",
|
||||
"skillManagement.detail.searchFiles": "البحث عن الملفات",
|
||||
"skillManagement.detail.unsavedChanges": "Unsaved changes",
|
||||
"skillManagement.detail.updateTagsFailed": "Failed to update tags.",
|
||||
"skillManagement.detail.uploadFile": "Upload file",
|
||||
"skillManagement.detail.uploadFileFailed": "File upload failed.",
|
||||
"skillManagement.detail.uploadFileSuccess": "File uploaded.",
|
||||
"skillManagement.detail.uploadFilesMenu": "Upload files...",
|
||||
"skillManagement.detail.versionMeta": "{{hash}} · {{time}}",
|
||||
"skillManagement.detail.versions": "Versions",
|
||||
"skillManagement.draft": "Draft",
|
||||
"skillManagement.duplicateFailed": "Failed to duplicate skill.",
|
||||
"skillManagement.duplicateSuccess": "Skill duplicated.",
|
||||
"skillManagement.editedAt": "Edited {{time}}",
|
||||
"skillManagement.empty": "No skills yet",
|
||||
"skillManagement.emptySearch": "No skills found",
|
||||
"skillManagement.import": "Import",
|
||||
"skillManagement.importFailed": "Failed to import skill.",
|
||||
"skillManagement.importSuccess": "Skill imported.",
|
||||
"skillManagement.listLabel": "Workspace skills",
|
||||
"skillManagement.loadingError": "Failed to load skills",
|
||||
"skillManagement.moreActions": "More actions for {{name}}",
|
||||
"skillManagement.noTags": "No tags",
|
||||
"skillManagement.publishedAt": "Published {{time}}",
|
||||
"skillManagement.referenceCount": "{{count}} Refs",
|
||||
"skillManagement.searchLabel": "Search skills",
|
||||
"skillManagement.searchPlaceholder": "Search",
|
||||
"skillManagement.tags": "Tags",
|
||||
"skillManagement.title": "Skills"
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"mainNav.home": "الرئيسية",
|
||||
"mainNav.integrations": "التكاملات",
|
||||
"mainNav.marketplace": "سوق الإضافات",
|
||||
"mainNav.skills": "Skills",
|
||||
"mainNav.webApps.noResults": "لم يتم العثور على تطبيقات ويب",
|
||||
"mainNav.webApps.openApp": "فتح تطبيق الويب {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "البحث في تطبيقات الويب",
|
||||
@@ -462,6 +463,7 @@
|
||||
"operation.downloading": "جارٍ التنزيل...",
|
||||
"operation.duplicate": "تكرار",
|
||||
"operation.edit": "تعديل",
|
||||
"operation.export": "تصدير",
|
||||
"operation.exporting": "جارٍ التصدير",
|
||||
"operation.fill": "ملء تلقائي",
|
||||
"operation.format": "تنسيق",
|
||||
@@ -503,6 +505,7 @@
|
||||
"operation.sure": "أنا متأكد",
|
||||
"operation.toggleFullscreen": "تبديل ملء الشاشة",
|
||||
"operation.toggleMute": "تبديل كتم الصوت",
|
||||
"operation.upload": "Upload",
|
||||
"operation.view": "عرض",
|
||||
"operation.viewDetails": "عرض التفاصيل",
|
||||
"operation.viewMore": "عرض المزيد",
|
||||
|
||||
@@ -203,15 +203,22 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview führt den fertigen Agenten so aus, wie deine Benutzer ihn sehen, mit klaren Antworten und Chat-Funktionen.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Agenten vorschauen",
|
||||
"agentDetail.configure.skills.add": "Skill hinzufügen",
|
||||
"agentDetail.configure.skills.addMenu.upload.badge": "EMBEDDED",
|
||||
"agentDetail.configure.skills.addMenu.upload.label": "Upload package",
|
||||
"agentDetail.configure.skills.addMenu.workspace.label": "From skill library",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Skill-Detailinhalt",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} DATEIEN",
|
||||
"agentDetail.configure.skills.detail.files": "Dateien",
|
||||
"agentDetail.configure.skills.empty.description": "Skills geben dem Agenten wiederverwendbare Fachkenntnisse, die er bei der Arbeit nutzen kann",
|
||||
"agentDetail.configure.skills.empty.title": "Noch keine Skills",
|
||||
"agentDetail.configure.skills.fromSkillLibrary": "From skill library",
|
||||
"agentDetail.configure.skills.itemType": "Skill",
|
||||
"agentDetail.configure.skills.label": "Skills",
|
||||
"agentDetail.configure.skills.missing": "Skill nicht gefunden",
|
||||
"agentDetail.configure.skills.moreActions": "More actions for {{name}}",
|
||||
"agentDetail.configure.skills.openInLibrary": "Open in Skill library",
|
||||
"agentDetail.configure.skills.remove": "{{name}} entfernen",
|
||||
"agentDetail.configure.skills.removeAction": "Remove",
|
||||
"agentDetail.configure.skills.richTip": "Bündeln Sie Anweisungen, Dateien und Skripte für eine wiederkehrende Aufgabe in einer Skill. Verweisen Sie im Prompt mit / darauf. <docLink>Mehr erfahren</docLink>\n\nIm Build-Modus kann der Agent diese Einrichtung für Sie übernehmen.",
|
||||
"agentDetail.configure.skills.tip": "Bündeln Sie Anweisungen, Dateien und Skripte für eine wiederkehrende Aufgabe in einer Skill. Verweisen Sie im Prompt mit / darauf. Mehr erfahren\n\nIm Build-Modus kann der Agent diese Einrichtung für Sie übernehmen.",
|
||||
"agentDetail.configure.skills.toggle": "Skills umschalten",
|
||||
@@ -421,5 +428,117 @@
|
||||
"roster.sort.optionsLabel": "Sortieroptionen",
|
||||
"roster.sort.recentlyCreated": "Zuletzt erstellt",
|
||||
"roster.updateSuccess": "Agent aktualisiert.",
|
||||
"roster.usageStatus.draft": "Entwurf"
|
||||
"roster.usageStatus.draft": "Entwurf",
|
||||
"skillManagement.clearTags": "Clear tags",
|
||||
"skillManagement.create": "Create",
|
||||
"skillManagement.createFailed": "Failed to create skill.",
|
||||
"skillManagement.createSuccess": "Skill created.",
|
||||
"skillManagement.dateTimeFormat": "MMM D, YYYY HH:mm",
|
||||
"skillManagement.deleteDialog.description": "This skill will be removed from the workspace. Agents that reference it may lose access to this capability.",
|
||||
"skillManagement.deleteDialog.title": "Delete {{name}}?",
|
||||
"skillManagement.deleteFailed": "Failed to delete skill.",
|
||||
"skillManagement.deleteSuccess": "Skill deleted.",
|
||||
"skillManagement.detail.addMetadata": "Add metadata",
|
||||
"skillManagement.detail.addMetadataDescription": "Add a frontmatter field to this Markdown file.",
|
||||
"skillManagement.detail.addTag": "Add tag",
|
||||
"skillManagement.detail.addTagDescription": "Create or bind a tag to this Skill. New tags are saved with the Skill metadata.",
|
||||
"skillManagement.detail.addTagSuccess": "Tag added.",
|
||||
"skillManagement.detail.back": "Back to Skills",
|
||||
"skillManagement.detail.cancelAddMetadata": "Cancel adding metadata",
|
||||
"skillManagement.detail.closeFileTab": "Close {{name}}",
|
||||
"skillManagement.detail.collapseSidebar": "Seitenleiste einklappen",
|
||||
"skillManagement.detail.createFile": "New file",
|
||||
"skillManagement.detail.createFileDescription": "Enter a file path relative to the skill root.",
|
||||
"skillManagement.detail.createFileMenu": "New file...",
|
||||
"skillManagement.detail.createFileSuccess": "File created.",
|
||||
"skillManagement.detail.createFolder": "New folder",
|
||||
"skillManagement.detail.createFolderDescription": "Enter a folder path relative to the skill root.",
|
||||
"skillManagement.detail.createFolderMenu": "New folder...",
|
||||
"skillManagement.detail.createFolderSuccess": "Folder created.",
|
||||
"skillManagement.detail.createdBy": "Created by {{name}}",
|
||||
"skillManagement.detail.currentDraft": "Current draft",
|
||||
"skillManagement.detail.deleteFileConfirm": "Delete this file?",
|
||||
"skillManagement.detail.deleteFileSuccess": "File deleted.",
|
||||
"skillManagement.detail.deleteVersionConfirm": "Delete this version?",
|
||||
"skillManagement.detail.deleteVersionFailed": "Failed to delete version.",
|
||||
"skillManagement.detail.deleteVersionSuccess": "Version deleted.",
|
||||
"skillManagement.detail.downloadFile": "Download file",
|
||||
"skillManagement.detail.draft": "Draft",
|
||||
"skillManagement.detail.expandSidebar": "Seitenleiste ausklappen",
|
||||
"skillManagement.detail.fileCount": "{{count}} FILES",
|
||||
"skillManagement.detail.fileMeta": "{{type}} · {{size}} bytes",
|
||||
"skillManagement.detail.fileMissing": "File not found.",
|
||||
"skillManagement.detail.fileOperationFailed": "File operation failed.",
|
||||
"skillManagement.detail.files": "Files",
|
||||
"skillManagement.detail.latest": "Latest",
|
||||
"skillManagement.detail.loadFailed": "Failed to load skill.",
|
||||
"skillManagement.detail.markdownLiveMode": "Live preview",
|
||||
"skillManagement.detail.markdownSourceMode": "Source editor",
|
||||
"skillManagement.detail.metadataKey": "name",
|
||||
"skillManagement.detail.metadataValue": "value",
|
||||
"skillManagement.detail.moveFileSuccess": "File moved.",
|
||||
"skillManagement.detail.moveFilesSuccess": "Files moved.",
|
||||
"skillManagement.detail.noFileSelected": "Select a file to preview.",
|
||||
"skillManagement.detail.noFiles": "No files.",
|
||||
"skillManagement.detail.noSearchResults": "Keine passenden Dateien.",
|
||||
"skillManagement.detail.noVersions": "No published versions yet.",
|
||||
"skillManagement.detail.previewUnsupported": "Preview is not supported for this file.",
|
||||
"skillManagement.detail.publish": "Publish",
|
||||
"skillManagement.detail.publishFailed": "Failed to publish skill.",
|
||||
"skillManagement.detail.publishSuccess": "Skill published.",
|
||||
"skillManagement.detail.published": "Published",
|
||||
"skillManagement.detail.readonly": "Read only",
|
||||
"skillManagement.detail.referenceFiles.confirm": "Enter Confirm",
|
||||
"skillManagement.detail.referenceFiles.empty": "No files available.",
|
||||
"skillManagement.detail.referenceFiles.livePlaceholder": "Write instructions for the agent, type / to reference files",
|
||||
"skillManagement.detail.referenceFiles.navigate": "↑↓ Navigate",
|
||||
"skillManagement.detail.referenceFiles.title": "Reference files",
|
||||
"skillManagement.detail.referencedBy": "Referenced by {{count}} Apps",
|
||||
"skillManagement.detail.removeMetadata": "Delete {{name}} metadata",
|
||||
"skillManagement.detail.removeTag": "Remove {{tag}}",
|
||||
"skillManagement.detail.removeTagSuccess": "Tag removed.",
|
||||
"skillManagement.detail.renameFile": "Rename file",
|
||||
"skillManagement.detail.renameFileDescription": "Enter the new path relative to the skill root.",
|
||||
"skillManagement.detail.renameFileSuccess": "File renamed.",
|
||||
"skillManagement.detail.renameVersion": "Rename",
|
||||
"skillManagement.detail.renameVersionFailed": "Failed to rename version.",
|
||||
"skillManagement.detail.renameVersionPrompt": "Version name",
|
||||
"skillManagement.detail.renameVersionSuccess": "Version renamed.",
|
||||
"skillManagement.detail.restoreVersion": "Restore",
|
||||
"skillManagement.detail.restoreVersionFailed": "Failed to restore version.",
|
||||
"skillManagement.detail.restoreVersionSuccess": "Version restored to draft.",
|
||||
"skillManagement.detail.save": "Save",
|
||||
"skillManagement.detail.saveFailed": "Failed to save file.",
|
||||
"skillManagement.detail.saveSuccess": "File saved.",
|
||||
"skillManagement.detail.saved": "Saved",
|
||||
"skillManagement.detail.savedAt": "Saved {{time}}",
|
||||
"skillManagement.detail.saving": "Saving...",
|
||||
"skillManagement.detail.searchFiles": "Dateien suchen",
|
||||
"skillManagement.detail.unsavedChanges": "Unsaved changes",
|
||||
"skillManagement.detail.updateTagsFailed": "Failed to update tags.",
|
||||
"skillManagement.detail.uploadFile": "Upload file",
|
||||
"skillManagement.detail.uploadFileFailed": "File upload failed.",
|
||||
"skillManagement.detail.uploadFileSuccess": "File uploaded.",
|
||||
"skillManagement.detail.uploadFilesMenu": "Upload files...",
|
||||
"skillManagement.detail.versionMeta": "{{hash}} · {{time}}",
|
||||
"skillManagement.detail.versions": "Versions",
|
||||
"skillManagement.draft": "Draft",
|
||||
"skillManagement.duplicateFailed": "Failed to duplicate skill.",
|
||||
"skillManagement.duplicateSuccess": "Skill duplicated.",
|
||||
"skillManagement.editedAt": "Edited {{time}}",
|
||||
"skillManagement.empty": "No skills yet",
|
||||
"skillManagement.emptySearch": "No skills found",
|
||||
"skillManagement.import": "Import",
|
||||
"skillManagement.importFailed": "Failed to import skill.",
|
||||
"skillManagement.importSuccess": "Skill imported.",
|
||||
"skillManagement.listLabel": "Workspace skills",
|
||||
"skillManagement.loadingError": "Failed to load skills",
|
||||
"skillManagement.moreActions": "More actions for {{name}}",
|
||||
"skillManagement.noTags": "No tags",
|
||||
"skillManagement.publishedAt": "Published {{time}}",
|
||||
"skillManagement.referenceCount": "{{count}} Refs",
|
||||
"skillManagement.searchLabel": "Search skills",
|
||||
"skillManagement.searchPlaceholder": "Search",
|
||||
"skillManagement.tags": "Tags",
|
||||
"skillManagement.title": "Skills"
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"mainNav.home": "Startseite",
|
||||
"mainNav.integrations": "Integrationen",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.skills": "Skills",
|
||||
"mainNav.webApps.noResults": "Keine Web-Apps gefunden",
|
||||
"mainNav.webApps.openApp": "Web-App {{name}} öffnen",
|
||||
"mainNav.webApps.searchPlaceholder": "Web-Apps suchen",
|
||||
@@ -463,6 +464,7 @@
|
||||
"operation.downloading": "Wird heruntergeladen...",
|
||||
"operation.duplicate": "Duplikat",
|
||||
"operation.edit": "Bearbeiten",
|
||||
"operation.export": "Exportieren",
|
||||
"operation.exporting": "Exportiere",
|
||||
"operation.fill": "Automatisch ausfüllen",
|
||||
"operation.format": "Format",
|
||||
@@ -504,6 +506,7 @@
|
||||
"operation.sure": "Ich bin sicher",
|
||||
"operation.toggleFullscreen": "Vollbild umschalten",
|
||||
"operation.toggleMute": "Stummschaltung umschalten",
|
||||
"operation.upload": "Upload",
|
||||
"operation.view": "Ansehen",
|
||||
"operation.viewDetails": "Details anzeigen",
|
||||
"operation.viewMore": "MEHR SEHEN",
|
||||
|
||||
@@ -203,15 +203,24 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview runs the finished agent the way your users will see it, with clean replies and chat features.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Preview your agent",
|
||||
"agentDetail.configure.skills.add": "Add skill",
|
||||
"agentDetail.configure.skills.addMenu.upload.badge": "EMBEDDED",
|
||||
"agentDetail.configure.skills.addMenu.upload.description": "A .zip containing SKILL.md. It's embedded in this app and won't update with the library.",
|
||||
"agentDetail.configure.skills.addMenu.upload.label": "Upload package",
|
||||
"agentDetail.configure.skills.addMenu.workspace.description": "Reuses a shared skill and follows its published updates.",
|
||||
"agentDetail.configure.skills.addMenu.workspace.label": "From skill library",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Skill detail content",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILES",
|
||||
"agentDetail.configure.skills.detail.files": "Files",
|
||||
"agentDetail.configure.skills.empty.description": "Skills give the agent reusable expertise it can call while working",
|
||||
"agentDetail.configure.skills.empty.title": "No skills yet",
|
||||
"agentDetail.configure.skills.fromSkillLibrary": "From skill library",
|
||||
"agentDetail.configure.skills.itemType": "Skill",
|
||||
"agentDetail.configure.skills.label": "Skills",
|
||||
"agentDetail.configure.skills.missing": "Skill not found",
|
||||
"agentDetail.configure.skills.moreActions": "More actions for {{name}}",
|
||||
"agentDetail.configure.skills.openInLibrary": "Open in Skill library",
|
||||
"agentDetail.configure.skills.remove": "Remove {{name}}",
|
||||
"agentDetail.configure.skills.removeAction": "Remove",
|
||||
"agentDetail.configure.skills.richTip": "Package the instructions, files, and scripts for a recurring task into a skill. Reference with / in the Prompt. <docLink>Learn more</docLink>\n\nIn Build mode, the agent can set these up for you.",
|
||||
"agentDetail.configure.skills.tip": "Package the instructions, files, and scripts for a recurring task into a skill. Reference with / in the Prompt. Learn more\n\nIn Build mode, the agent can set these up for you.",
|
||||
"agentDetail.configure.skills.toggle": "Toggle skills",
|
||||
@@ -226,6 +235,15 @@
|
||||
"agentDetail.configure.skills.upload.title": "Upload skill",
|
||||
"agentDetail.configure.skills.upload.warning.files": "If you only need to use Markdown files, upload them to Files and reference them in your prompt.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Uploaded skills must follow the <specificationLink>Agent Skills specification</specificationLink>.",
|
||||
"agentDetail.configure.skills.workspaceItemType": "Workspace",
|
||||
"agentDetail.configure.skills.workspaceSelector.addSuccess": "Workspace skill added.",
|
||||
"agentDetail.configure.skills.workspaceSelector.added": "Added",
|
||||
"agentDetail.configure.skills.workspaceSelector.draft": "Draft",
|
||||
"agentDetail.configure.skills.workspaceSelector.empty": "No workspace skills found.",
|
||||
"agentDetail.configure.skills.workspaceSelector.manage": "Manage in Skills",
|
||||
"agentDetail.configure.skills.workspaceSelector.removeSuccess": "Workspace skill removed.",
|
||||
"agentDetail.configure.skills.workspaceSelector.saveFailed": "Failed to update workspace skills.",
|
||||
"agentDetail.configure.skills.workspaceSelector.search": "Search skills...",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Switch to Preview mode?",
|
||||
"agentDetail.configure.title": "Configure",
|
||||
"agentDetail.configure.tools.add": "Add tool",
|
||||
@@ -421,5 +439,182 @@
|
||||
"roster.sort.optionsLabel": "Sort options",
|
||||
"roster.sort.recentlyCreated": "Recently created",
|
||||
"roster.updateSuccess": "Agent updated.",
|
||||
"roster.usageStatus.draft": "Draft"
|
||||
"roster.usageStatus.draft": "Draft",
|
||||
"skillManagement.clearTags": "Clear tags",
|
||||
"skillManagement.create": "Create",
|
||||
"skillManagement.createFailed": "Failed to create skill.",
|
||||
"skillManagement.createSuccess": "Skill created.",
|
||||
"skillManagement.dateTimeFormat": "MMM D, YYYY HH:mm",
|
||||
"skillManagement.deleteDialog.description": "This skill will be removed from the workspace. Agents that reference it may lose access to this capability.",
|
||||
"skillManagement.deleteDialog.referencedDescription": "This Skill is referenced by {{count}} Agent, Workflow, or Chatflow item. Deleting it will remove the Skill from the workspace and clear those bindings.",
|
||||
"skillManagement.deleteDialog.title": "Delete {{name}}?",
|
||||
"skillManagement.deleteFailed": "Failed to delete skill.",
|
||||
"skillManagement.deleteSuccess": "Skill deleted.",
|
||||
"skillManagement.detail.addMetadata": "Add metadata",
|
||||
"skillManagement.detail.addMetadataDescription": "Add a frontmatter field to this Markdown file.",
|
||||
"skillManagement.detail.addTag": "Add tag",
|
||||
"skillManagement.detail.addTagDescription": "Create or bind a tag to this Skill. New tags are saved with the Skill metadata.",
|
||||
"skillManagement.detail.addTagSuccess": "Tag added.",
|
||||
"skillManagement.detail.back": "Back to Skills",
|
||||
"skillManagement.detail.builder.applyFailed": "Failed to apply draft updates.",
|
||||
"skillManagement.detail.builder.attach": "Attach file",
|
||||
"skillManagement.detail.builder.attachFailed": "Failed to attach file.",
|
||||
"skillManagement.detail.builder.attachUnsupported": "Only text and document files can be attached.",
|
||||
"skillManagement.detail.builder.attachmentOnlyMessage": "Use the attached file to help improve this Skill.",
|
||||
"skillManagement.detail.builder.close": "Close Skill Builder",
|
||||
"skillManagement.detail.builder.compatibleModelsOnly": "Only compatible models are shown",
|
||||
"skillManagement.detail.builder.editIntro": "I've reviewed this Skill. What would you like to adjust?",
|
||||
"skillManagement.detail.builder.exampleIssueTriage": "Customer issue triage",
|
||||
"skillManagement.detail.builder.exampleOnboarding": "New hire onboarding guide",
|
||||
"skillManagement.detail.builder.exampleSalesFollowUp": "Sales lead follow-up strategy",
|
||||
"skillManagement.detail.builder.followUpDisplayName": "Use Refund approval as the display name",
|
||||
"skillManagement.detail.builder.followUpNameIcon": "Apply the suggested name and icon",
|
||||
"skillManagement.detail.builder.fromMarketplace": "From Marketplace",
|
||||
"skillManagement.detail.builder.model": "GPT-4o",
|
||||
"skillManagement.detail.builder.modelCredits.all": "All credits",
|
||||
"skillManagement.detail.builder.modelCredits.configure": "Configure required",
|
||||
"skillManagement.detail.builder.modelCredits.exhausted": "Credits exhausted",
|
||||
"skillManagement.detail.builder.modelProviderSettings": "Model Provider Settings",
|
||||
"skillManagement.detail.builder.modelSearch": "Search models...",
|
||||
"skillManagement.detail.builder.modifyPlaceholder": "Ask AI to modify this skill...",
|
||||
"skillManagement.detail.builder.open": "Open Skill Builder",
|
||||
"skillManagement.detail.builder.placeholder": "Describe the scenario...",
|
||||
"skillManagement.detail.builder.promptDescription": "Describe it and a draft appears in the editor, with steps and files included.",
|
||||
"skillManagement.detail.builder.promptTitle": "What should this Skill handle?",
|
||||
"skillManagement.detail.builder.removeAttachment": "Remove {{name}}",
|
||||
"skillManagement.detail.builder.restart": "Restart builder",
|
||||
"skillManagement.detail.builder.send": "Send message",
|
||||
"skillManagement.detail.builder.sendFailed": "Skill Builder failed to respond.",
|
||||
"skillManagement.detail.builder.title": "Skill Builder",
|
||||
"skillManagement.detail.builder.tryExample": "Try an example",
|
||||
"skillManagement.detail.builder.voice": "Voice input",
|
||||
"skillManagement.detail.builder.voiceUnavailable": "Voice input is not available yet.",
|
||||
"skillManagement.detail.cancelAddMetadata": "Cancel adding metadata",
|
||||
"skillManagement.detail.closeFileTab": "Close {{name}}",
|
||||
"skillManagement.detail.closeVersions": "Close versions",
|
||||
"skillManagement.detail.collapseSidebar": "Collapse sidebar",
|
||||
"skillManagement.detail.copyFile": "Copy",
|
||||
"skillManagement.detail.copyFileSuccess": "File copied to clipboard.",
|
||||
"skillManagement.detail.copyVersionId": "Copy ID",
|
||||
"skillManagement.detail.copyVersionIdSuccess": "ID copied to clipboard.",
|
||||
"skillManagement.detail.createFile": "New file",
|
||||
"skillManagement.detail.createFileDescription": "Enter a file path relative to the skill root.",
|
||||
"skillManagement.detail.createFileMenu": "New file...",
|
||||
"skillManagement.detail.createFileSuccess": "File created.",
|
||||
"skillManagement.detail.createFolder": "New folder",
|
||||
"skillManagement.detail.createFolderDescription": "Enter a folder path relative to the skill root.",
|
||||
"skillManagement.detail.createFolderMenu": "New folder...",
|
||||
"skillManagement.detail.createFolderSuccess": "Folder created.",
|
||||
"skillManagement.detail.createdBy": "Created by {{name}}",
|
||||
"skillManagement.detail.currentDraft": "Current draft",
|
||||
"skillManagement.detail.cutFile": "Cut",
|
||||
"skillManagement.detail.cutFileSuccess": "File cut to clipboard.",
|
||||
"skillManagement.detail.deleteFileConfirm": "Delete this file?",
|
||||
"skillManagement.detail.deleteFileSuccess": "File deleted.",
|
||||
"skillManagement.detail.deleteVersionConfirm": "Delete this version?",
|
||||
"skillManagement.detail.deleteVersionFailed": "Failed to delete version.",
|
||||
"skillManagement.detail.deleteVersionSuccess": "Version deleted.",
|
||||
"skillManagement.detail.downloadFile": "Download file",
|
||||
"skillManagement.detail.draft": "Draft",
|
||||
"skillManagement.detail.editVersionInfo": "Edit version info",
|
||||
"skillManagement.detail.exitVersions": "Exit versions",
|
||||
"skillManagement.detail.expandSidebar": "Expand sidebar",
|
||||
"skillManagement.detail.fileCount": "{{count}} FILES",
|
||||
"skillManagement.detail.fileMeta": "{{type}} · {{size}} bytes",
|
||||
"skillManagement.detail.fileMissing": "File not found.",
|
||||
"skillManagement.detail.fileOperationFailed": "File operation failed.",
|
||||
"skillManagement.detail.files": "Files",
|
||||
"skillManagement.detail.latest": "Latest",
|
||||
"skillManagement.detail.loadFailed": "Failed to load skill.",
|
||||
"skillManagement.detail.markdownLiveMode": "Live preview",
|
||||
"skillManagement.detail.markdownSourceMode": "Source editor",
|
||||
"skillManagement.detail.metadataKey": "name",
|
||||
"skillManagement.detail.metadataValue": "value",
|
||||
"skillManagement.detail.moveFileSuccess": "File moved.",
|
||||
"skillManagement.detail.moveFilesSuccess": "Files moved.",
|
||||
"skillManagement.detail.nameThisVersion": "Name this version",
|
||||
"skillManagement.detail.noFileSelected": "Select a file to preview.",
|
||||
"skillManagement.detail.noFiles": "No files.",
|
||||
"skillManagement.detail.noSearchResults": "No matching files.",
|
||||
"skillManagement.detail.noVersions": "No published versions yet.",
|
||||
"skillManagement.detail.pasteFile": "Paste",
|
||||
"skillManagement.detail.pasteFileSuccess": "File pasted.",
|
||||
"skillManagement.detail.previewUnsupported": "Preview is not supported for this file.",
|
||||
"skillManagement.detail.publish": "Publish",
|
||||
"skillManagement.detail.publishFailed": "Failed to publish skill.",
|
||||
"skillManagement.detail.publishReferencesDescription": "Publishing will make this draft active for {{count}} references using this Skill.",
|
||||
"skillManagement.detail.publishReferencesTitle": "Publish skill",
|
||||
"skillManagement.detail.publishSuccess": "Skill published.",
|
||||
"skillManagement.detail.publishUpdate": "Publish update",
|
||||
"skillManagement.detail.published": "Published",
|
||||
"skillManagement.detail.readonly": "Read only",
|
||||
"skillManagement.detail.referenceFiles.confirm": "Enter Confirm",
|
||||
"skillManagement.detail.referenceFiles.empty": "No files available.",
|
||||
"skillManagement.detail.referenceFiles.livePlaceholder": "Write instructions for the agent, type / to reference files",
|
||||
"skillManagement.detail.referenceFiles.navigate": "↑↓ Navigate",
|
||||
"skillManagement.detail.referenceFiles.title": "Reference files",
|
||||
"skillManagement.detail.referencedBy": "Referenced by {{count}} Apps",
|
||||
"skillManagement.detail.removeMetadata": "Delete {{name}} metadata",
|
||||
"skillManagement.detail.removeTag": "Remove {{tag}}",
|
||||
"skillManagement.detail.removeTagSuccess": "Tag removed.",
|
||||
"skillManagement.detail.renameFile": "Rename file",
|
||||
"skillManagement.detail.renameFileDescription": "Enter the new path relative to the skill root.",
|
||||
"skillManagement.detail.renameFileSuccess": "File renamed.",
|
||||
"skillManagement.detail.renameVersion": "Rename",
|
||||
"skillManagement.detail.renameVersionFailed": "Failed to rename version.",
|
||||
"skillManagement.detail.renameVersionPrompt": "Version name",
|
||||
"skillManagement.detail.renameVersionSuccess": "Version renamed.",
|
||||
"skillManagement.detail.restoreVersion": "Restore",
|
||||
"skillManagement.detail.restoreVersionFailed": "Failed to restore version.",
|
||||
"skillManagement.detail.restoreVersionSuccess": "Version restored to draft.",
|
||||
"skillManagement.detail.save": "Save",
|
||||
"skillManagement.detail.saveConflict": "This Skill changed in another tab. Refreshed the latest draft and will retry saving your changes.",
|
||||
"skillManagement.detail.saveFailed": "Failed to save file.",
|
||||
"skillManagement.detail.saveSuccess": "File saved.",
|
||||
"skillManagement.detail.saved": "Saved",
|
||||
"skillManagement.detail.savedAt": "Saved {{time}}",
|
||||
"skillManagement.detail.saving": "Saving...",
|
||||
"skillManagement.detail.searchFiles": "Search files",
|
||||
"skillManagement.detail.unsavedChanges": "Unsaved changes",
|
||||
"skillManagement.detail.updateTagsFailed": "Failed to update tags.",
|
||||
"skillManagement.detail.uploadFile": "Upload file",
|
||||
"skillManagement.detail.uploadFileFailed": "File upload failed.",
|
||||
"skillManagement.detail.uploadFileSuccess": "File uploaded.",
|
||||
"skillManagement.detail.uploadFilesFailedStatus": "{{count}} file uploads failed.",
|
||||
"skillManagement.detail.uploadFilesMenu": "Upload files...",
|
||||
"skillManagement.detail.uploadFilesProgress": "Uploading {{completed}}/{{total}}",
|
||||
"skillManagement.detail.uploadFilesResult": "{{uploaded}} uploaded · {{failed}} failed",
|
||||
"skillManagement.detail.uploadFilesStatus": "Upload Status",
|
||||
"skillManagement.detail.uploadStatusDismiss": "Dismiss upload status",
|
||||
"skillManagement.detail.versionHistory": "Open version history",
|
||||
"skillManagement.detail.versionMeta": "{{hash}} · {{time}}",
|
||||
"skillManagement.detail.versionPublishNote": "Release notes",
|
||||
"skillManagement.detail.versionPublishNotePlaceholder": "Describe this change",
|
||||
"skillManagement.detail.versionPublishedMeta": "{{time}} · {{name}}",
|
||||
"skillManagement.detail.versionTitle": "Title",
|
||||
"skillManagement.detail.versions": "Versions",
|
||||
"skillManagement.detail.viewOnly": "View only",
|
||||
"skillManagement.draft": "Draft",
|
||||
"skillManagement.duplicateFailed": "Failed to duplicate skill.",
|
||||
"skillManagement.duplicateSuccess": "Skill duplicated.",
|
||||
"skillManagement.editedAt": "Edited {{time}}",
|
||||
"skillManagement.empty": "No skills yet",
|
||||
"skillManagement.emptyAction.createDescription": "Describe a scenario in plain words and get a draft skill to review.",
|
||||
"skillManagement.emptyAction.createTitle": "Create with Skill Builder",
|
||||
"skillManagement.emptyAction.importDescription": "Bring a .zip with SKILL.md, in the agentskills.io format.",
|
||||
"skillManagement.emptyAction.importTitle": "Import a skill package",
|
||||
"skillManagement.emptySearch": "No skills found",
|
||||
"skillManagement.import": "Import",
|
||||
"skillManagement.importFailed": "Failed to import skill.",
|
||||
"skillManagement.importSuccess": "Skill imported.",
|
||||
"skillManagement.listLabel": "Workspace skills",
|
||||
"skillManagement.loadingError": "Failed to load skills",
|
||||
"skillManagement.moreActions": "More actions for {{name}}",
|
||||
"skillManagement.noTags": "No tags",
|
||||
"skillManagement.publishedAt": "Published {{time}}",
|
||||
"skillManagement.referenceCount": "{{count}} Refs",
|
||||
"skillManagement.searchLabel": "Search skills",
|
||||
"skillManagement.searchPlaceholder": "Search",
|
||||
"skillManagement.tags": "Tags",
|
||||
"skillManagement.title": "Skills"
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"mainNav.home": "Home",
|
||||
"mainNav.integrations": "Integrations",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.skills": "Skills",
|
||||
"mainNav.webApps.noResults": "No web apps found",
|
||||
"mainNav.webApps.openApp": "Open {{name}} web app",
|
||||
"mainNav.webApps.searchPlaceholder": "Search web apps",
|
||||
@@ -463,6 +464,7 @@
|
||||
"operation.downloading": "Downloading",
|
||||
"operation.duplicate": "Duplicate",
|
||||
"operation.edit": "Edit",
|
||||
"operation.export": "Export",
|
||||
"operation.exporting": "Exporting",
|
||||
"operation.fill": "Autofill",
|
||||
"operation.format": "Format",
|
||||
@@ -504,6 +506,7 @@
|
||||
"operation.sure": "I'm sure",
|
||||
"operation.toggleFullscreen": "Toggle fullscreen",
|
||||
"operation.toggleMute": "Toggle mute",
|
||||
"operation.upload": "Upload",
|
||||
"operation.view": "View",
|
||||
"operation.viewDetails": "View Details",
|
||||
"operation.viewMore": "VIEW MORE",
|
||||
|
||||
@@ -203,15 +203,22 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview ejecuta el agente terminado como lo verán tus usuarios, con respuestas claras y funciones de chat.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Previsualiza tu agente",
|
||||
"agentDetail.configure.skills.add": "Agregar habilidad",
|
||||
"agentDetail.configure.skills.addMenu.upload.badge": "EMBEDDED",
|
||||
"agentDetail.configure.skills.addMenu.upload.label": "Upload package",
|
||||
"agentDetail.configure.skills.addMenu.workspace.label": "From skill library",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Contenido de los detalles de la habilidad",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} ARCHIVOS",
|
||||
"agentDetail.configure.skills.detail.files": "Archivos",
|
||||
"agentDetail.configure.skills.empty.description": "Las habilidades le dan al agente experiencia reutilizable que puede invocar mientras trabaja",
|
||||
"agentDetail.configure.skills.empty.title": "Aún no hay habilidades",
|
||||
"agentDetail.configure.skills.fromSkillLibrary": "From skill library",
|
||||
"agentDetail.configure.skills.itemType": "Habilidad",
|
||||
"agentDetail.configure.skills.label": "Habilidades",
|
||||
"agentDetail.configure.skills.missing": "Habilidad no encontrada",
|
||||
"agentDetail.configure.skills.moreActions": "More actions for {{name}}",
|
||||
"agentDetail.configure.skills.openInLibrary": "Open in Skill library",
|
||||
"agentDetail.configure.skills.remove": "Eliminar {{name}}",
|
||||
"agentDetail.configure.skills.removeAction": "Remove",
|
||||
"agentDetail.configure.skills.richTip": "Empaqueta las instrucciones, archivos y scripts de una tarea recurrente en una skill. Haz referencia con / en el Prompt. <docLink>Más información</docLink>\n\nEn modo Build, el agente puede configurarlas por ti.",
|
||||
"agentDetail.configure.skills.tip": "Empaqueta las instrucciones, archivos y scripts de una tarea recurrente en una skill. Haz referencia con / en el Prompt. Más información\n\nEn modo Build, el agente puede configurarlas por ti.",
|
||||
"agentDetail.configure.skills.toggle": "Alternar habilidades",
|
||||
@@ -421,5 +428,117 @@
|
||||
"roster.sort.optionsLabel": "Opciones de ordenación",
|
||||
"roster.sort.recentlyCreated": "Creados recientemente",
|
||||
"roster.updateSuccess": "Agente actualizado.",
|
||||
"roster.usageStatus.draft": "Borrador"
|
||||
"roster.usageStatus.draft": "Borrador",
|
||||
"skillManagement.clearTags": "Clear tags",
|
||||
"skillManagement.create": "Create",
|
||||
"skillManagement.createFailed": "Failed to create skill.",
|
||||
"skillManagement.createSuccess": "Skill created.",
|
||||
"skillManagement.dateTimeFormat": "MMM D, YYYY HH:mm",
|
||||
"skillManagement.deleteDialog.description": "This skill will be removed from the workspace. Agents that reference it may lose access to this capability.",
|
||||
"skillManagement.deleteDialog.title": "Delete {{name}}?",
|
||||
"skillManagement.deleteFailed": "Failed to delete skill.",
|
||||
"skillManagement.deleteSuccess": "Skill deleted.",
|
||||
"skillManagement.detail.addMetadata": "Add metadata",
|
||||
"skillManagement.detail.addMetadataDescription": "Add a frontmatter field to this Markdown file.",
|
||||
"skillManagement.detail.addTag": "Add tag",
|
||||
"skillManagement.detail.addTagDescription": "Create or bind a tag to this Skill. New tags are saved with the Skill metadata.",
|
||||
"skillManagement.detail.addTagSuccess": "Tag added.",
|
||||
"skillManagement.detail.back": "Back to Skills",
|
||||
"skillManagement.detail.cancelAddMetadata": "Cancel adding metadata",
|
||||
"skillManagement.detail.closeFileTab": "Close {{name}}",
|
||||
"skillManagement.detail.collapseSidebar": "Contraer barra lateral",
|
||||
"skillManagement.detail.createFile": "New file",
|
||||
"skillManagement.detail.createFileDescription": "Enter a file path relative to the skill root.",
|
||||
"skillManagement.detail.createFileMenu": "New file...",
|
||||
"skillManagement.detail.createFileSuccess": "File created.",
|
||||
"skillManagement.detail.createFolder": "New folder",
|
||||
"skillManagement.detail.createFolderDescription": "Enter a folder path relative to the skill root.",
|
||||
"skillManagement.detail.createFolderMenu": "New folder...",
|
||||
"skillManagement.detail.createFolderSuccess": "Folder created.",
|
||||
"skillManagement.detail.createdBy": "Created by {{name}}",
|
||||
"skillManagement.detail.currentDraft": "Current draft",
|
||||
"skillManagement.detail.deleteFileConfirm": "Delete this file?",
|
||||
"skillManagement.detail.deleteFileSuccess": "File deleted.",
|
||||
"skillManagement.detail.deleteVersionConfirm": "Delete this version?",
|
||||
"skillManagement.detail.deleteVersionFailed": "Failed to delete version.",
|
||||
"skillManagement.detail.deleteVersionSuccess": "Version deleted.",
|
||||
"skillManagement.detail.downloadFile": "Download file",
|
||||
"skillManagement.detail.draft": "Draft",
|
||||
"skillManagement.detail.expandSidebar": "Expandir barra lateral",
|
||||
"skillManagement.detail.fileCount": "{{count}} FILES",
|
||||
"skillManagement.detail.fileMeta": "{{type}} · {{size}} bytes",
|
||||
"skillManagement.detail.fileMissing": "File not found.",
|
||||
"skillManagement.detail.fileOperationFailed": "File operation failed.",
|
||||
"skillManagement.detail.files": "Files",
|
||||
"skillManagement.detail.latest": "Latest",
|
||||
"skillManagement.detail.loadFailed": "Failed to load skill.",
|
||||
"skillManagement.detail.markdownLiveMode": "Live preview",
|
||||
"skillManagement.detail.markdownSourceMode": "Source editor",
|
||||
"skillManagement.detail.metadataKey": "name",
|
||||
"skillManagement.detail.metadataValue": "value",
|
||||
"skillManagement.detail.moveFileSuccess": "File moved.",
|
||||
"skillManagement.detail.moveFilesSuccess": "Files moved.",
|
||||
"skillManagement.detail.noFileSelected": "Select a file to preview.",
|
||||
"skillManagement.detail.noFiles": "No files.",
|
||||
"skillManagement.detail.noSearchResults": "No hay archivos coincidentes.",
|
||||
"skillManagement.detail.noVersions": "No published versions yet.",
|
||||
"skillManagement.detail.previewUnsupported": "Preview is not supported for this file.",
|
||||
"skillManagement.detail.publish": "Publish",
|
||||
"skillManagement.detail.publishFailed": "Failed to publish skill.",
|
||||
"skillManagement.detail.publishSuccess": "Skill published.",
|
||||
"skillManagement.detail.published": "Published",
|
||||
"skillManagement.detail.readonly": "Read only",
|
||||
"skillManagement.detail.referenceFiles.confirm": "Enter Confirm",
|
||||
"skillManagement.detail.referenceFiles.empty": "No files available.",
|
||||
"skillManagement.detail.referenceFiles.livePlaceholder": "Write instructions for the agent, type / to reference files",
|
||||
"skillManagement.detail.referenceFiles.navigate": "↑↓ Navigate",
|
||||
"skillManagement.detail.referenceFiles.title": "Reference files",
|
||||
"skillManagement.detail.referencedBy": "Referenced by {{count}} Apps",
|
||||
"skillManagement.detail.removeMetadata": "Delete {{name}} metadata",
|
||||
"skillManagement.detail.removeTag": "Remove {{tag}}",
|
||||
"skillManagement.detail.removeTagSuccess": "Tag removed.",
|
||||
"skillManagement.detail.renameFile": "Rename file",
|
||||
"skillManagement.detail.renameFileDescription": "Enter the new path relative to the skill root.",
|
||||
"skillManagement.detail.renameFileSuccess": "File renamed.",
|
||||
"skillManagement.detail.renameVersion": "Rename",
|
||||
"skillManagement.detail.renameVersionFailed": "Failed to rename version.",
|
||||
"skillManagement.detail.renameVersionPrompt": "Version name",
|
||||
"skillManagement.detail.renameVersionSuccess": "Version renamed.",
|
||||
"skillManagement.detail.restoreVersion": "Restore",
|
||||
"skillManagement.detail.restoreVersionFailed": "Failed to restore version.",
|
||||
"skillManagement.detail.restoreVersionSuccess": "Version restored to draft.",
|
||||
"skillManagement.detail.save": "Save",
|
||||
"skillManagement.detail.saveFailed": "Failed to save file.",
|
||||
"skillManagement.detail.saveSuccess": "File saved.",
|
||||
"skillManagement.detail.saved": "Saved",
|
||||
"skillManagement.detail.savedAt": "Saved {{time}}",
|
||||
"skillManagement.detail.saving": "Saving...",
|
||||
"skillManagement.detail.searchFiles": "Buscar archivos",
|
||||
"skillManagement.detail.unsavedChanges": "Unsaved changes",
|
||||
"skillManagement.detail.updateTagsFailed": "Failed to update tags.",
|
||||
"skillManagement.detail.uploadFile": "Upload file",
|
||||
"skillManagement.detail.uploadFileFailed": "File upload failed.",
|
||||
"skillManagement.detail.uploadFileSuccess": "File uploaded.",
|
||||
"skillManagement.detail.uploadFilesMenu": "Upload files...",
|
||||
"skillManagement.detail.versionMeta": "{{hash}} · {{time}}",
|
||||
"skillManagement.detail.versions": "Versions",
|
||||
"skillManagement.draft": "Draft",
|
||||
"skillManagement.duplicateFailed": "Failed to duplicate skill.",
|
||||
"skillManagement.duplicateSuccess": "Skill duplicated.",
|
||||
"skillManagement.editedAt": "Edited {{time}}",
|
||||
"skillManagement.empty": "No skills yet",
|
||||
"skillManagement.emptySearch": "No skills found",
|
||||
"skillManagement.import": "Import",
|
||||
"skillManagement.importFailed": "Failed to import skill.",
|
||||
"skillManagement.importSuccess": "Skill imported.",
|
||||
"skillManagement.listLabel": "Workspace skills",
|
||||
"skillManagement.loadingError": "Failed to load skills",
|
||||
"skillManagement.moreActions": "More actions for {{name}}",
|
||||
"skillManagement.noTags": "No tags",
|
||||
"skillManagement.publishedAt": "Published {{time}}",
|
||||
"skillManagement.referenceCount": "{{count}} Refs",
|
||||
"skillManagement.searchLabel": "Search skills",
|
||||
"skillManagement.searchPlaceholder": "Search",
|
||||
"skillManagement.tags": "Tags",
|
||||
"skillManagement.title": "Skills"
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"mainNav.home": "Inicio",
|
||||
"mainNav.integrations": "Integraciones",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.skills": "Skills",
|
||||
"mainNav.webApps.noResults": "No se encontraron aplicaciones web",
|
||||
"mainNav.webApps.openApp": "Abrir la aplicación web {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Buscar aplicaciones web",
|
||||
@@ -463,6 +464,7 @@
|
||||
"operation.downloading": "Descargando...",
|
||||
"operation.duplicate": "Duplicar",
|
||||
"operation.edit": "Editar",
|
||||
"operation.export": "Exportar",
|
||||
"operation.exporting": "Exportando",
|
||||
"operation.fill": "Autocompletar",
|
||||
"operation.format": "Formato",
|
||||
@@ -504,6 +506,7 @@
|
||||
"operation.sure": "Estoy seguro",
|
||||
"operation.toggleFullscreen": "Alternar pantalla completa",
|
||||
"operation.toggleMute": "Alternar silencio",
|
||||
"operation.upload": "Upload",
|
||||
"operation.view": "Vista",
|
||||
"operation.viewDetails": "Ver detalles",
|
||||
"operation.viewMore": "VER MÁS",
|
||||
|
||||
@@ -203,15 +203,22 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview عامل تکمیلشده را همانطور که کاربران میبینند اجرا میکند، با پاسخهای تمیز و قابلیتهای گفتگو.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "پیشنمایش عامل",
|
||||
"agentDetail.configure.skills.add": "افزودن مهارت",
|
||||
"agentDetail.configure.skills.addMenu.upload.badge": "EMBEDDED",
|
||||
"agentDetail.configure.skills.addMenu.upload.label": "Upload package",
|
||||
"agentDetail.configure.skills.addMenu.workspace.label": "From skill library",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "محتوای جزئیات مهارت",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} فایل",
|
||||
"agentDetail.configure.skills.detail.files": "فایلها",
|
||||
"agentDetail.configure.skills.empty.description": "مهارتها به عامل تخصص قابل استفاده مجدد میدهند که هنگام کار میتواند فراخوانی کند",
|
||||
"agentDetail.configure.skills.empty.title": "هنوز مهارتی وجود ندارد",
|
||||
"agentDetail.configure.skills.fromSkillLibrary": "From skill library",
|
||||
"agentDetail.configure.skills.itemType": "مهارت",
|
||||
"agentDetail.configure.skills.label": "مهارتها",
|
||||
"agentDetail.configure.skills.missing": "مهارت یافت نشد",
|
||||
"agentDetail.configure.skills.moreActions": "More actions for {{name}}",
|
||||
"agentDetail.configure.skills.openInLibrary": "Open in Skill library",
|
||||
"agentDetail.configure.skills.remove": "حذف {{name}}",
|
||||
"agentDetail.configure.skills.removeAction": "Remove",
|
||||
"agentDetail.configure.skills.richTip": "دستورالعملها، فایلها و اسکریپتهای یک کار تکرارشونده را در یک Skill بستهبندی کنید. در Prompt با / به آن ارجاع دهید. <docLink>بیشتر بدانید</docLink>\n\nدر حالت Build، عامل میتواند این موارد را برای شما تنظیم کند.",
|
||||
"agentDetail.configure.skills.tip": "دستورالعملها، فایلها و اسکریپتهای یک کار تکرارشونده را در یک Skill بستهبندی کنید. در Prompt با / به آن ارجاع دهید. بیشتر بدانید\n\nدر حالت Build، عامل میتواند این موارد را برای شما تنظیم کند.",
|
||||
"agentDetail.configure.skills.toggle": "تغییر مهارتها",
|
||||
@@ -421,5 +428,117 @@
|
||||
"roster.sort.optionsLabel": "گزینههای مرتبسازی",
|
||||
"roster.sort.recentlyCreated": "تازهترین ایجاد",
|
||||
"roster.updateSuccess": "عامل بهروزرسانی شد.",
|
||||
"roster.usageStatus.draft": "پیشنویس"
|
||||
"roster.usageStatus.draft": "پیشنویس",
|
||||
"skillManagement.clearTags": "Clear tags",
|
||||
"skillManagement.create": "Create",
|
||||
"skillManagement.createFailed": "Failed to create skill.",
|
||||
"skillManagement.createSuccess": "Skill created.",
|
||||
"skillManagement.dateTimeFormat": "MMM D, YYYY HH:mm",
|
||||
"skillManagement.deleteDialog.description": "This skill will be removed from the workspace. Agents that reference it may lose access to this capability.",
|
||||
"skillManagement.deleteDialog.title": "Delete {{name}}?",
|
||||
"skillManagement.deleteFailed": "Failed to delete skill.",
|
||||
"skillManagement.deleteSuccess": "Skill deleted.",
|
||||
"skillManagement.detail.addMetadata": "Add metadata",
|
||||
"skillManagement.detail.addMetadataDescription": "Add a frontmatter field to this Markdown file.",
|
||||
"skillManagement.detail.addTag": "Add tag",
|
||||
"skillManagement.detail.addTagDescription": "Create or bind a tag to this Skill. New tags are saved with the Skill metadata.",
|
||||
"skillManagement.detail.addTagSuccess": "Tag added.",
|
||||
"skillManagement.detail.back": "Back to Skills",
|
||||
"skillManagement.detail.cancelAddMetadata": "Cancel adding metadata",
|
||||
"skillManagement.detail.closeFileTab": "Close {{name}}",
|
||||
"skillManagement.detail.collapseSidebar": "جمع کردن نوار کناری",
|
||||
"skillManagement.detail.createFile": "New file",
|
||||
"skillManagement.detail.createFileDescription": "Enter a file path relative to the skill root.",
|
||||
"skillManagement.detail.createFileMenu": "New file...",
|
||||
"skillManagement.detail.createFileSuccess": "File created.",
|
||||
"skillManagement.detail.createFolder": "New folder",
|
||||
"skillManagement.detail.createFolderDescription": "Enter a folder path relative to the skill root.",
|
||||
"skillManagement.detail.createFolderMenu": "New folder...",
|
||||
"skillManagement.detail.createFolderSuccess": "Folder created.",
|
||||
"skillManagement.detail.createdBy": "Created by {{name}}",
|
||||
"skillManagement.detail.currentDraft": "Current draft",
|
||||
"skillManagement.detail.deleteFileConfirm": "Delete this file?",
|
||||
"skillManagement.detail.deleteFileSuccess": "File deleted.",
|
||||
"skillManagement.detail.deleteVersionConfirm": "Delete this version?",
|
||||
"skillManagement.detail.deleteVersionFailed": "Failed to delete version.",
|
||||
"skillManagement.detail.deleteVersionSuccess": "Version deleted.",
|
||||
"skillManagement.detail.downloadFile": "Download file",
|
||||
"skillManagement.detail.draft": "Draft",
|
||||
"skillManagement.detail.expandSidebar": "باز کردن نوار کناری",
|
||||
"skillManagement.detail.fileCount": "{{count}} FILES",
|
||||
"skillManagement.detail.fileMeta": "{{type}} · {{size}} bytes",
|
||||
"skillManagement.detail.fileMissing": "File not found.",
|
||||
"skillManagement.detail.fileOperationFailed": "File operation failed.",
|
||||
"skillManagement.detail.files": "Files",
|
||||
"skillManagement.detail.latest": "Latest",
|
||||
"skillManagement.detail.loadFailed": "Failed to load skill.",
|
||||
"skillManagement.detail.markdownLiveMode": "Live preview",
|
||||
"skillManagement.detail.markdownSourceMode": "Source editor",
|
||||
"skillManagement.detail.metadataKey": "name",
|
||||
"skillManagement.detail.metadataValue": "value",
|
||||
"skillManagement.detail.moveFileSuccess": "File moved.",
|
||||
"skillManagement.detail.moveFilesSuccess": "Files moved.",
|
||||
"skillManagement.detail.noFileSelected": "Select a file to preview.",
|
||||
"skillManagement.detail.noFiles": "No files.",
|
||||
"skillManagement.detail.noSearchResults": "فایل منطبقی نیست.",
|
||||
"skillManagement.detail.noVersions": "No published versions yet.",
|
||||
"skillManagement.detail.previewUnsupported": "Preview is not supported for this file.",
|
||||
"skillManagement.detail.publish": "Publish",
|
||||
"skillManagement.detail.publishFailed": "Failed to publish skill.",
|
||||
"skillManagement.detail.publishSuccess": "Skill published.",
|
||||
"skillManagement.detail.published": "Published",
|
||||
"skillManagement.detail.readonly": "Read only",
|
||||
"skillManagement.detail.referenceFiles.confirm": "Enter Confirm",
|
||||
"skillManagement.detail.referenceFiles.empty": "No files available.",
|
||||
"skillManagement.detail.referenceFiles.livePlaceholder": "Write instructions for the agent, type / to reference files",
|
||||
"skillManagement.detail.referenceFiles.navigate": "↑↓ Navigate",
|
||||
"skillManagement.detail.referenceFiles.title": "Reference files",
|
||||
"skillManagement.detail.referencedBy": "Referenced by {{count}} Apps",
|
||||
"skillManagement.detail.removeMetadata": "Delete {{name}} metadata",
|
||||
"skillManagement.detail.removeTag": "Remove {{tag}}",
|
||||
"skillManagement.detail.removeTagSuccess": "Tag removed.",
|
||||
"skillManagement.detail.renameFile": "Rename file",
|
||||
"skillManagement.detail.renameFileDescription": "Enter the new path relative to the skill root.",
|
||||
"skillManagement.detail.renameFileSuccess": "File renamed.",
|
||||
"skillManagement.detail.renameVersion": "Rename",
|
||||
"skillManagement.detail.renameVersionFailed": "Failed to rename version.",
|
||||
"skillManagement.detail.renameVersionPrompt": "Version name",
|
||||
"skillManagement.detail.renameVersionSuccess": "Version renamed.",
|
||||
"skillManagement.detail.restoreVersion": "Restore",
|
||||
"skillManagement.detail.restoreVersionFailed": "Failed to restore version.",
|
||||
"skillManagement.detail.restoreVersionSuccess": "Version restored to draft.",
|
||||
"skillManagement.detail.save": "Save",
|
||||
"skillManagement.detail.saveFailed": "Failed to save file.",
|
||||
"skillManagement.detail.saveSuccess": "File saved.",
|
||||
"skillManagement.detail.saved": "Saved",
|
||||
"skillManagement.detail.savedAt": "Saved {{time}}",
|
||||
"skillManagement.detail.saving": "Saving...",
|
||||
"skillManagement.detail.searchFiles": "جستجوی فایلها",
|
||||
"skillManagement.detail.unsavedChanges": "Unsaved changes",
|
||||
"skillManagement.detail.updateTagsFailed": "Failed to update tags.",
|
||||
"skillManagement.detail.uploadFile": "Upload file",
|
||||
"skillManagement.detail.uploadFileFailed": "File upload failed.",
|
||||
"skillManagement.detail.uploadFileSuccess": "File uploaded.",
|
||||
"skillManagement.detail.uploadFilesMenu": "Upload files...",
|
||||
"skillManagement.detail.versionMeta": "{{hash}} · {{time}}",
|
||||
"skillManagement.detail.versions": "Versions",
|
||||
"skillManagement.draft": "Draft",
|
||||
"skillManagement.duplicateFailed": "Failed to duplicate skill.",
|
||||
"skillManagement.duplicateSuccess": "Skill duplicated.",
|
||||
"skillManagement.editedAt": "Edited {{time}}",
|
||||
"skillManagement.empty": "No skills yet",
|
||||
"skillManagement.emptySearch": "No skills found",
|
||||
"skillManagement.import": "Import",
|
||||
"skillManagement.importFailed": "Failed to import skill.",
|
||||
"skillManagement.importSuccess": "Skill imported.",
|
||||
"skillManagement.listLabel": "Workspace skills",
|
||||
"skillManagement.loadingError": "Failed to load skills",
|
||||
"skillManagement.moreActions": "More actions for {{name}}",
|
||||
"skillManagement.noTags": "No tags",
|
||||
"skillManagement.publishedAt": "Published {{time}}",
|
||||
"skillManagement.referenceCount": "{{count}} Refs",
|
||||
"skillManagement.searchLabel": "Search skills",
|
||||
"skillManagement.searchPlaceholder": "Search",
|
||||
"skillManagement.tags": "Tags",
|
||||
"skillManagement.title": "Skills"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user