Compare commits

..
Author SHA1 Message Date
Stephen Zhou dec469a306 chore: upgrade openapi-ts preview 2026-07-28 14:55:40 +08:00
Stephen Zhou 212402e561 Merge remote-tracking branch 'origin/main' into pr-38152 2026-07-28 14:54:54 +08:00
Stephen Zhou 1b90369f3f Merge remote-tracking branch 'origin/main' into pr-38152
# Conflicts:
#	packages/contracts/openapi-ts.api.config.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
2026-07-28 14:43:55 +08:00
Asuka MinatoGitHubByron.wangautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
3b040e6bed test: use sqlite3 session in test_web_login (#38725)
Co-authored-by: Byron.wang <byron@dify.ai>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-28 06:37:45 +00:00
Asuka MinatoandGitHub 9300be03f0 test: use sqlite3 session in test_agent_app_feature_service (#38724) 2026-07-28 06:32:07 +00:00
copilot-swe-agent[bot]andGitHub 0f73a1b656 Apply remaining changes 2026-07-27 09:00:49 +00:00
copilot-swe-agent[bot]andGitHub 6193c10e66 fix: update pnpm-lock.yaml to use @typescript/typescript6@6.0.2 peer for openapi-ts 0.99.0 2026-07-27 04:52:18 +00:00
copilot-swe-agent[bot]andGitHub 2dec9c2cce chore: sync branch with main to resolve merge conflicts 2026-07-27 04:51:10 +00:00
Stephen ZhouandGitHub e6567a0d7c Merge branch 'main' into codex/upgrade-openapi-ts-autofix 2026-07-02 16:50:09 +08:00
Stephen ZhouandGitHub b2683977a7 Merge branch 'main' into codex/upgrade-openapi-ts-autofix 2026-07-02 16:46:33 +08:00
copilot-swe-agent[bot]andGitHub ee580d6f10 Merge remote-tracking branch 'origin/main' into codex/upgrade-openapi-ts-autofix
# Conflicts:
#	.github/workflows/autofix.yml
#	packages/contracts/openapi-ts.api.config.ts
2026-07-02 08:22:14 +00:00
Stephen Zhou dde1e5c73c fix: generate openapi specs in contract autofix 2026-06-29 15:37:29 +08:00
Stephen Zhou 508ae6dceb chore: remove codegen smoke test 2026-06-29 15:21:33 +08:00
Stephen Zhou c8d7e3f4ab test: expose openapi-ts discriminator issue 2026-06-29 15:18:18 +08:00
Stephen Zhou 9df5a7c373 fix: support openapi-ts 0.99.0 2026-06-29 15:12:02 +08:00
137 changed files with 1240 additions and 24431 deletions
-2
View File
@@ -144,7 +144,6 @@ from .workspace import (
models,
plugin,
rbac,
skills,
snippets,
tool_providers,
trigger_providers,
@@ -226,7 +225,6 @@ __all__ = [
"saved_message",
"setup",
"site",
"skills",
"snippet_workflow",
"snippet_workflow_draft_variable",
"snippets",
+1 -1
View File
@@ -59,7 +59,7 @@ class TagBindingRemovePayload(BaseModel):
class TagListQueryParam(BaseModel):
type: TagType | Literal[""] = Field("", description="Tag type filter")
type: Literal["knowledge", "app", "snippet", ""] = Field("", description="Tag type filter")
keyword: str | None = Field(None, description="Search keyword")
-834
View File
@@ -1,834 +0,0 @@
"""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",
]
-2
View File
@@ -23,7 +23,6 @@ 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)
@@ -37,7 +36,6 @@ __all__ = [
"_mail",
"_plugin",
"_runtime_credentials",
"_skills",
"_workspace",
"api",
"bp",
@@ -1,54 +0,0 @@
"""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,7 +43,6 @@ 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
@@ -126,22 +125,14 @@ 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,
runtime_config_skills=runtime_config_skills,
)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
knowledge_config = build_knowledge_layer_config(agent_soul)
request = self._request_builder.build_for_agent_app(
@@ -38,7 +38,6 @@ 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,
@@ -207,22 +206,14 @@ 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,
runtime_config_skills=runtime_config_skills,
)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
soul_prompt = expand_prompt_mentions(agent_soul.prompt.system_prompt, soul_prompt_resolver).strip()
knowledge_config = build_knowledge_layer_config(agent_soul)
@@ -892,16 +883,11 @@ 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,
*,
runtime_config_skills: Sequence[DifyConfigSkillConfig] = (),
):
def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
"""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:
@@ -919,34 +905,12 @@ def build_config_aware_soul_mention_resolver(
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.
@@ -963,23 +927,8 @@ 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 skill_configs}
skill_names = {skill.name for skill in available_skills}
file_names = {file_ref.name for file_ref in available_files}
warnings: list[dict[str, str]] = [
{
@@ -1016,7 +965,15 @@ def build_config_layer_config(
kind=config_version_kind,
writable=config_version_kind == "build_draft",
),
skills=skill_configs,
skills=[
DifyConfigSkillConfig(
name=skill.name,
description=skill.description,
size=skill.size,
mime_type=skill.mime_type,
)
for skill in available_skills
],
files=[
DifyConfigFileConfig(
name=file_ref.name,
@@ -1,114 +0,0 @@
"""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")
@@ -18,109 +18,31 @@ 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.
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(
op.add_column(
"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",
):
op.create_unique_constraint(
"agent_debug_conversation_agent_account_draft_type_unique",
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id", "draft_type"],
)
"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()),
@@ -142,23 +64,14 @@ def downgrade():
),
)
)
if _has_unique_constraint(
"agent_debug_conversations",
op.drop_constraint(
"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",
):
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")
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id"],
)
op.drop_column("agent_debug_conversations", "draft_type")
@@ -1,21 +0,0 @@
"""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
-7
View File
@@ -113,7 +113,6 @@ 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
@@ -174,7 +173,6 @@ __all__ = [
"AgentRuntimeSessionOwnerType",
"AgentRuntimeSessionStatus",
"AgentScope",
"AgentSkillBinding",
"AgentSource",
"AgentStatus",
"ApiRequest",
@@ -248,11 +246,6 @@ __all__ = [
"RecommendedApp",
"SavedMessage",
"Site",
"Skill",
"SkillDraftFile",
"SkillFileKind",
"SkillFileStorage",
"SkillVersion",
"SnippetType",
"Tag",
"TagBinding",
-1
View File
@@ -249,7 +249,6 @@ class TagType(StrEnum):
KNOWLEDGE = "knowledge"
APP = "app"
SNIPPET = "snippet"
SKILL = "skill"
class DatasetMetadataType(StrEnum):
+1 -1
View File
@@ -2667,7 +2667,7 @@ class Tag(TypeBase):
sa.Index("tag_name_idx", "name"),
)
TAG_TYPE_LIST = ["knowledge", "app", "snippet", "skill"]
TAG_TYPE_LIST = ["knowledge", "app", "snippet"]
id: Mapped[str] = mapped_column(
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
-164
View File
@@ -1,164 +0,0 @@
"""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",
]
+2 -728
View File
@@ -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 |
| type | query | Tag type filter | No | string, <br>**Available values:** "", "app", "knowledge", "snippet" |
#### Responses
@@ -10087,38 +10087,6 @@ 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**
@@ -12020,341 +11988,6 @@ 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
@@ -15076,37 +14709,6 @@ 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 |
@@ -22063,186 +21665,6 @@ 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.
@@ -22256,91 +21678,6 @@ 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 |
@@ -22349,60 +21686,6 @@ 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 |
@@ -22870,7 +22153,7 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| keyword | string | Search keyword | No |
| type | [TagType](#tagtype)<br>string | Tag type filter | No |
| type | string, <br>**Available values:** "", "app", "knowledge", "snippet" | Tag type filter<br>*Enum:* `""`, `"app"`, `"knowledge"`, `"snippet"` | No |
#### TagListResponse
@@ -25056,15 +24339,6 @@ 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 |
+1 -3
View File
@@ -1599,9 +1599,7 @@ 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:
+20 -46
View File
@@ -19,11 +19,12 @@ from __future__ import annotations
import hashlib
import io
import posixpath
import re
import zipfile
import zlib
import yaml
from pydantic import BaseModel, Field, ValidationError, field_validator
from pydantic import BaseModel
# Bounds — generous but finite so a hostile upload can't exhaust memory/disk.
_MAX_ARCHIVE_BYTES = 50 * 1024 * 1024
@@ -32,8 +33,7 @@ _MAX_SKILL_MD_BYTES = 1 * 1024 * 1024
_MAX_ENTRIES = 5000
_ALLOWED_EXTENSIONS = (".zip", ".skill")
_SKILL_MD_NAME = "SKILL.md"
_SKILL_NAME_PATTERN = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
_MAX_SKILL_DESCRIPTION_LENGTH = 1024
_HEADING_RE = re.compile(r"^\s*#\s+(.+?)\s*$", re.MULTILINE)
class SkillPackageError(Exception):
@@ -53,18 +53,13 @@ class SkillPackageError(Exception):
class SkillManifest(BaseModel):
"""Validated metadata extracted from a Skill package."""
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)
name: str
description: str
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."""
@@ -113,17 +108,14 @@ class SkillPackageService:
normalized_size = sum(max(info.file_size, 0) for info in normalized_members.values())
name, description = self._parse_skill_md(skill_md)
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
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(),
)
return NormalizedSkillPackage(
manifest=manifest,
archive_bytes=normalized_archive_bytes,
@@ -131,31 +123,6 @@ 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:
@@ -313,6 +280,13 @@ 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
+12 -81
View File
@@ -46,7 +46,6 @@ 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):
@@ -99,7 +98,6 @@ class ConfigPushPayload(BaseModel):
@dataclass(slots=True)
class AgentConfigTarget:
tenant_id: str
agent_id: str
version_id: str
kind: AgentConfigVersionKind
@@ -148,7 +146,6 @@ 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,
@@ -194,7 +191,7 @@ class AgentConfigService:
return {
"agent_id": target.agent_id,
"config_version": self._config_version_payload(target),
"items": self._skill_items_for_target(target),
"items": [self._serialize_skill_item(skill) for skill in target.agent_soul.config_skills],
}
def list_files(
@@ -236,27 +233,10 @@ class AgentConfigService:
config_version_kind=config_version_kind,
user_id=user_id,
)
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
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)
def download_skill_url(
self,
@@ -299,45 +279,9 @@ class AgentConfigService:
config_version_kind=config_version_kind,
user_id=user_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",
},
)
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:
archive_items, skill_md = self._inspect_skill_archive(archive_bytes)
except (OSError, ValueError, zipfile.BadZipFile) as exc:
@@ -347,7 +291,7 @@ class AgentConfigService:
status_code=500,
) from exc
return {
**skill_item,
**self._serialize_skill_item(skill),
"source": "config_skill_zip",
"files": archive_items,
"skill_md": skill_md,
@@ -895,7 +839,6 @@ class AgentConfigService:
status_code=404,
)
return AgentConfigTarget(
tenant_id=tenant_id,
agent_id=agent_id,
version_id=version.id,
kind=config_version_kind,
@@ -1190,7 +1133,9 @@ class AgentConfigService:
return {
"agent_id": target.agent_id,
"config_version": AgentConfigService._config_version_payload(target),
"skills": {"items": AgentConfigService._skill_items_for_target(target)},
"skills": {
"items": [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
},
"files": {
"items": [
AgentConfigService._serialize_file_item(file_ref) for file_ref in target.agent_soul.config_files
@@ -1200,20 +1145,6 @@ 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
-9
View File
@@ -12,7 +12,6 @@ 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
@@ -283,13 +282,5 @@ 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")
@@ -1,394 +0,0 @@
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,13 +6,19 @@ 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()
@@ -33,17 +39,27 @@ def app():
@pytest.fixture(autouse=True)
def _patch_wraps():
def _patch_wraps(
monkeypatch: pytest.MonkeyPatch,
sqlite_engine: Engine,
sqlite_session: Session,
):
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,7 +7,6 @@ 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
@@ -30,14 +29,6 @@ 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",
@@ -523,33 +514,6 @@ 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,7 +5,6 @@ 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
@@ -41,14 +40,6 @@ 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"
@@ -1429,30 +1420,6 @@ 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
@@ -1538,33 +1505,6 @@ 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_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_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_shallowest_skill_md_preferred_during_normalization():
@@ -155,18 +155,7 @@ 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"---\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"no name here"}, "skill.zip", "missing_skill_name"),
({"SKILL.md": b"\xff\xfenot utf8"}, "skill.zip", "skill_md_not_utf8"),
],
)
@@ -235,10 +224,10 @@ def test_bad_frontmatter_yaml_rejected():
assert exc_info.value.code == "invalid_frontmatter"
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_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_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,14 +7,16 @@ 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:
@@ -71,45 +73,47 @@ 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:
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
@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,
)
account = SimpleNamespace(id="acct-1")
account = Account(name="Test User", email="test@example.com")
account.id = ACCOUNT_ID
sqlite_session.add_all([account, app_model])
sqlite_session.commit()
new_config = AgentAppFeatureConfigService.update_features(
app_model=app_model, # type: ignore[arg-type]
account=account, # type: ignore[arg-type]
app_model=app_model,
account=account,
config={"opening_statement": "Hi!", "suggested_questions_after_answer": {"enabled": True}},
session=session,
session=sqlite_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-1"
assert new_config.app_id == APP_ID
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 == "acct-1"
assert new_config in session.added
assert session.flushed == 1
assert session.committed == 1
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
@@ -63,7 +63,6 @@ def _target(
) -> AgentConfigTarget:
agent_soul = soul or _soul()
return AgentConfigTarget(
tenant_id=TENANT,
agent_id=AGENT,
version_id=version_id,
kind=kind,
@@ -509,9 +508,7 @@ def test_manifest_uses_items_shape_without_download_urls() -> None:
),
)
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)
manifest = AgentConfigService._manifest_for_target(target)
assert manifest == {
"agent_id": AGENT,
@@ -560,9 +557,7 @@ def test_manifest_preserves_missing_config_assets_and_pull_rejects_them() -> Non
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=soul)
service = AgentConfigService()
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)
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]
@@ -611,44 +606,6 @@ 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() }))
.input(z.object({ query: zGetAgentInviteOptionsQuery.optional() }).optional())
.output(zGetAgentInviteOptionsResponse)
export const inviteOptions = {
@@ -1491,7 +1491,7 @@ export const get36 = oc
path: '/agent',
tags: ['console'],
})
.input(z.object({ query: zGetAgentQuery.optional() }))
.input(z.object({ query: zGetAgentQuery.optional() }).optional())
.output(zGetAgentResponse)
export const post21 = oc
@@ -997,15 +997,11 @@ export const zSandboxListResponse = z.object({
* Validated metadata extracted from a Skill package.
*/
export const zSkillManifest = z.object({
description: z.string().min(1).max(1024),
description: z.string(),
entry_path: z.string(),
files: z.array(z.string()),
hash: z.string(),
name: z
.string()
.min(1)
.max(64)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
name: z.string(),
size: z.int(),
})
@@ -2577,10 +2573,10 @@ export const zSelectInputConfig = z.object({
})
export const zFormInputConfig = z.discriminatedUnion('type', [
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') }),
zParagraphInputConfig,
zSelectInputConfig,
zFileInputConfig,
zFileListInputConfig,
])
/**
@@ -2897,7 +2893,10 @@ 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) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
export const zPostAgentByAgentIdAudioToTextPath = z.object({
@@ -3147,7 +3146,10 @@ export const zGetAgentByAgentIdConfigSkillsQuery = z.object({
export const zGetAgentByAgentIdConfigSkillsResponse = zAgentConfigSkillListResponse
export const zPostAgentByAgentIdConfigSkillsUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
export const zPostAgentByAgentIdConfigSkillsUploadPath = z.object({
@@ -3523,7 +3525,10 @@ export const zPostAgentByAgentIdSandboxFilesUploadPath = z.object({
export const zPostAgentByAgentIdSandboxFilesUploadResponse = zSandboxUploadResponse
export const zPostAgentByAgentIdSkillsUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
export const zPostAgentByAgentIdSkillsUploadPath = z.object({
@@ -12,7 +12,7 @@ export const get = oc
path: '/all-workspaces',
tags: ['console'],
})
.input(z.object({ query: zGetAllWorkspacesQuery.optional() }))
.input(z.object({ query: zGetAllWorkspacesQuery.optional() }).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() }))
.input(z.object({ query: zGetAppsRecentQuery.optional() }).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() }))
.input(z.object({ query: zGetAppsStarredQuery.optional() }).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() }))
.input(z.object({ query: zGetAppsQuery.optional() }).optional())
.output(zGetAppsResponse)
/**
@@ -1373,15 +1373,11 @@ export const zAgentLogMetaResponse = z.object({
* Validated metadata extracted from a Skill package.
*/
export const zSkillManifest = z.object({
description: z.string().min(1).max(1024),
description: z.string(),
entry_path: z.string(),
files: z.array(z.string()),
hash: z.string(),
name: z
.string()
.min(1)
.max(64)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
name: z.string(),
size: z.int(),
})
@@ -3968,10 +3964,10 @@ export const zSelectInputConfig = z.object({
})
export const zFormInputConfig = z.discriminatedUnion('type', [
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') }),
zParagraphInputConfig,
zSelectInputConfig,
zFileInputConfig,
zFileListInputConfig,
])
/**
@@ -4781,7 +4777,10 @@ export const zGetAppsByAppIdAgentConfigSkillsQuery = z.object({
export const zGetAppsByAppIdAgentConfigSkillsResponse = zAgentConfigSkillListResponse
export const zPostAppsByAppIdAgentConfigSkillsUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
export const zPostAppsByAppIdAgentConfigSkillsUploadPath = z.object({
@@ -5012,7 +5011,10 @@ export const zGetAppsByAppIdAgentLogsQuery = z.object({
export const zGetAppsByAppIdAgentLogsResponse = zAgentLogResponse
export const zPostAppsByAppIdAgentSkillsUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
export const zPostAppsByAppIdAgentSkillsUploadPath = z.object({
@@ -5223,7 +5225,10 @@ export const zPostAppsByAppIdApiEnablePath = z.object({
export const zPostAppsByAppIdApiEnableResponse = zAppDetail
export const zPostAppsByAppIdAudioToTextBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
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() }))
.input(z.object({ query: zGetDatasetsExternalKnowledgeApiQuery.optional() }).optional())
.output(zGetDatasetsExternalKnowledgeApiResponse)
/**
@@ -505,7 +505,7 @@ export const get8 = oc
path: '/datasets/process-rule',
tags: ['console'],
})
.input(z.object({ query: zGetDatasetsProcessRuleQuery.optional() }))
.input(z.object({ query: zGetDatasetsProcessRuleQuery.optional() }).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() }))
.input(z.object({ query: zGetDatasetsQuery.optional() }).optional())
.output(zGetDatasetsResponse)
/**
@@ -21,7 +21,7 @@ export const get = oc
path: '/explore/apps/learn-dify',
tags: ['console'],
})
.input(z.object({ query: zGetExploreAppsLearnDifyQuery.optional() }))
.input(z.object({ query: zGetExploreAppsLearnDifyQuery.optional() }).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() }))
.input(z.object({ query: zGetExploreAppsQuery.optional() }).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() }))
.input(z.object({ query: zGetExploreBannersQuery.optional() }).optional())
.output(zGetExploreBannersResponse)
export const banners = {
@@ -64,7 +64,10 @@ export const zGetFilesSupportTypeResponse = zAllowedExtensionsResponse
export const zGetFilesUploadResponse = zUploadConfig
export const zPostFilesUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
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() }))
.input(z.object({ query: zGetInstalledAppsQuery.optional() }).optional())
.output(zGetInstalledAppsResponse)
export const post12 = oc
@@ -145,7 +145,8 @@ export const zTextToAudioPayload = z.object({
* AudioBinaryResponse
*/
export const zAudioBinaryResponse = z.custom<Blob | File>(
(value) => value instanceof Blob || value instanceof File,
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
/**
@@ -475,10 +476,10 @@ export const zSelectInputConfig = z.object({
})
export const zFormInputConfig = z.discriminatedUnion('type', [
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') }),
zParagraphInputConfig,
zSelectInputConfig,
zFileInputConfig,
zFileListInputConfig,
])
/**
@@ -246,7 +246,7 @@ export const get2 = oc
path: '/rag/pipeline/templates',
tags: ['console'],
})
.input(z.object({ query: zGetRagPipelineTemplatesQuery.optional() }))
.input(z.object({ query: zGetRagPipelineTemplatesQuery.optional() }).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() }))
.input(z.object({ query: zGetRagPipelinesRecommendedPluginsQuery.optional() }).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' | 'skill' | 'snippet'
export type TagType = 'app' | 'knowledge' | 'snippet'
export type PostTagBindingsData = {
body: TagBindingPayload
@@ -14,7 +14,7 @@ export const zSimpleResultResponse = z.object({
*
* Tag type
*/
export const zTagType = z.enum(['app', 'knowledge', 'skill', 'snippet'])
export const zTagType = z.enum(['app', 'knowledge', 'snippet'])
/**
* TagBindingPayload
@@ -50,7 +50,7 @@ export const get = oc
path: '/tags',
tags: ['console'],
})
.input(z.object({ query: zGetTagsQuery.optional() }))
.input(z.object({ query: zGetTagsQuery.optional() }).optional())
.output(zGetTagsResponse)
export const post = oc
@@ -22,14 +22,14 @@ export type TagUpdateRequestPayload = {
name: string
}
export type TagType = 'app' | 'knowledge' | 'skill' | 'snippet'
export type TagType = 'app' | 'knowledge' | 'snippet'
export type GetTagsData = {
body?: never
path?: never
query?: {
keyword?: string
type?: '' | 'app' | 'knowledge' | 'skill' | 'snippet'
type?: '' | 'app' | 'knowledge' | 'snippet'
}
url: '/tags'
}
@@ -29,7 +29,7 @@ export const zTagUpdateRequestPayload = z.object({
*
* Tag type
*/
export const zTagType = z.enum(['app', 'knowledge', 'skill', 'snippet'])
export const zTagType = z.enum(['app', 'knowledge', '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', 'skill', 'snippet']).optional().default(''),
type: z.enum(['', 'app', 'knowledge', 'snippet']).optional().default(''),
})
/**
@@ -116,7 +116,8 @@ export const zTextToSpeechRequest = z.object({
* AudioBinaryResponse
*/
export const zAudioBinaryResponse = z.custom<Blob | File>(
(value) => value instanceof Blob || value instanceof File,
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
/**
@@ -459,7 +460,10 @@ export const zGetTrialAppsByAppIdDatasetsQuery = z.object({
export const zGetTrialAppsByAppIdDatasetsResponse = zTrialDatasetListResponse
export const zPostTrialAppsByAppIdFilesUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
source: z.enum(['datasets']).optional(),
})
File diff suppressed because it is too large Load Diff
@@ -32,16 +32,6 @@ 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
@@ -180,15 +170,7 @@ export type MemberInvitePayload = {
export type MemberInviteResponse = {
invitation_results: Array<
| ({
status: 'success'
} & MemberInviteSuccessResponse)
| ({
status: 'already_member'
} & MemberInviteAlreadyMemberResponse)
| ({
status: 'failed'
} & MemberInviteFailedResponse)
MemberInviteSuccessResponse | MemberInviteAlreadyMemberResponse | MemberInviteFailedResponse
>
result: 'success'
tenant_id: string
@@ -645,194 +627,6 @@ 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 = {
@@ -1240,21 +1034,6 @@ 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
@@ -1715,60 +1494,6 @@ 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
@@ -2245,10 +1970,6 @@ export type PermissionCatalogItem = {
name: string
}
export type SkillFileKind = 'directory' | 'file'
export type SkillFileStorage = 'text' | 'tool_file'
export type ToolParameter = {
auto_generate?: PluginParameterAutoGenerate | null
default?:
@@ -2744,38 +2465,6 @@ 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
@@ -4928,369 +4617,6 @@ 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,13 +12,6 @@ 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
*
@@ -253,7 +246,8 @@ export const zWorkspacePermissionResponse = z.object({
* BinaryFileResponse
*/
export const zBinaryFileResponse = z.custom<Blob | File>(
(value) => value instanceof Blob || value instanceof File,
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
/**
@@ -443,155 +437,6 @@ 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
*/
@@ -812,33 +657,6 @@ 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
*
@@ -1056,21 +874,9 @@ export const zMemberInviteFailedResponse = z.object({
export const zMemberInviteResponse = z.object({
invitation_results: z.array(
z.union([
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),
zMemberInviteSuccessResponse,
zMemberInviteAlreadyMemberResponse,
zMemberInviteFailedResponse,
]),
),
result: z.literal('success'),
@@ -1427,164 +1233,6 @@ 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
*/
@@ -2437,42 +2085,6 @@ 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
*/
@@ -3470,7 +3082,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(),
@@ -3798,26 +3410,6 @@ 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(),
@@ -4624,7 +4216,10 @@ export const zPostWorkspacesCurrentPluginUploadGithubBody = zParserGithubUpload
export const zPostWorkspacesCurrentPluginUploadGithubResponse = zPluginDecodeResponse
export const zPostWorkspacesCurrentPluginUploadPkgBody = z.object({
pkg: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
pkg: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
/**
@@ -5091,234 +4686,6 @@ 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
*/
@@ -5880,7 +5247,10 @@ export const zPostWorkspacesCustomConfigBody = zWorkspaceCustomConfigPayload
export const zPostWorkspacesCustomConfigResponse = zWorkspaceTenantResultResponse
export const zPostWorkspacesCustomConfigWebappLogoUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
/**
@@ -139,7 +139,7 @@ export const get3 = oc
path: '/account/sessions',
tags: ['openapi'],
})
.input(z.object({ query: zGetAccountSessionsQuery.optional() }))
.input(z.object({ query: zGetAccountSessionsQuery.optional() }).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() }))
.input(z.object({ query: zGetPermittedExternalAppsQuery.optional() }).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() }))
.input(z.object({ query: zGetAppFeedbacksQuery.optional() }).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() }))
.input(z.object({ query: zGetAppsAnnotationsQuery.optional() }).optional())
.output(zGetAppsAnnotationsResponse)
/**
@@ -669,7 +669,7 @@ export const get6 = oc
summary: 'List Conversations',
tags: ['Conversations'],
})
.input(z.object({ query: zGetConversationsQuery.optional() }))
.input(z.object({ query: zGetConversationsQuery.optional() }).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() }))
.input(z.object({ query: zGetDatasetsQuery.optional() }).optional())
.output(zGetDatasetsResponse)
/**
@@ -2367,7 +2367,7 @@ export const get31 = oc
summary: 'List Workflow Logs',
tags: ['Chatflows', 'Workflows'],
})
.input(z.object({ query: zGetWorkflowsLogsQuery.optional() }))
.input(z.object({ query: zGetWorkflowsLogsQuery.optional() }).optional())
.output(zGetWorkflowsLogsResponse)
export const logs = {
@@ -113,7 +113,8 @@ export const zAppMetaResponse = z.object({
* AudioBinaryResponse
*/
export const zAudioBinaryResponse = z.custom<Blob | File>(
(value) => value instanceof Blob || value instanceof File,
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
/**
@@ -127,7 +128,8 @@ export const zAudioTranscriptResponse = z.object({
* BinaryFileResponse
*/
export const zBinaryFileResponse = z.custom<Blob | File>(
(value) => value instanceof Blob || value instanceof File,
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
/**
@@ -1939,10 +1941,10 @@ export const zParagraphInputConfig = z.object({
})
export const zFormInputConfig = z.discriminatedUnion('type', [
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') }),
zParagraphInputConfig,
zSelectInputConfig,
zFileInputConfig,
zFileListInputConfig,
])
/**
@@ -2456,7 +2458,10 @@ export const zPutAppsAnnotationsByAnnotationIdPath = z.object({
export const zPutAppsAnnotationsByAnnotationIdResponse = zAnnotation
export const zPostAudioToTextBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
user: z.string().optional(),
})
@@ -2595,7 +2600,10 @@ export const zPostDatasetsBody = zDatasetCreatePayload
export const zPostDatasetsResponse = zDatasetDetailResponse
export const zPostDatasetsPipelineFileUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
/**
@@ -2674,7 +2682,10 @@ export const zPatchDatasetsByDatasetIdResponse = zDatasetDetailWithPartialMember
export const zPostDatasetsByDatasetIdDocumentCreateByFileBody = z.object({
data: z.string().optional(),
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
export const zPostDatasetsByDatasetIdDocumentCreateByFilePath = z.object({
@@ -2699,7 +2710,10 @@ export const zPostDatasetsByDatasetIdDocumentCreateByTextResponse = zDocumentAnd
export const zPostDatasetsByDatasetIdDocumentCreateByFile2Body = z.object({
data: z.string().optional(),
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
})
export const zPostDatasetsByDatasetIdDocumentCreateByFile2Path = z.object({
@@ -2750,7 +2764,8 @@ export const zPostDatasetsByDatasetIdDocumentsDownloadZipPath = z.object({
* ZIP archive containing the requested documents.
*/
export const zPostDatasetsByDatasetIdDocumentsDownloadZipResponse = z.custom<Blob | File>(
(value) => value instanceof Blob || value instanceof File,
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
export const zPostDatasetsByDatasetIdDocumentsMetadataBody = zMetadataOperationData
@@ -2813,7 +2828,12 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdResponse = zDocumentDet
export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdBody = z.object({
data: z.string().optional(),
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File).optional(),
file: z
.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
.optional(),
})
export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({
@@ -2973,7 +2993,12 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdCh
export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFileBody = z.object({
data: z.string().optional(),
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File).optional(),
file: z
.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
.optional(),
})
export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFilePath = z.object({
@@ -3002,7 +3027,12 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByTextResponse =
export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Body = z.object({
data: z.string().optional(),
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File).optional(),
file: z
.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
.optional(),
})
export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Path = z.object({
@@ -3173,7 +3203,10 @@ export const zGetEndUsersByEndUserIdPath = z.object({
export const zGetEndUsersByEndUserIdResponse = zEndUserDetail
export const zPostFilesUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
file: z.custom<Blob | File>(
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
),
user: z.string().optional(),
})
@@ -3195,7 +3228,8 @@ 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) => value instanceof Blob || value instanceof File,
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
export const zGetFormHumanInputByFormTokenPath = z.object({
@@ -3280,7 +3314,8 @@ 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) => value instanceof Blob || value instanceof File,
(value) =>
Blob.prototype.isPrototypeOf(Object(value)) || File.prototype.isPrototypeOf(Object(value)),
)
export const zGetWorkflowByTaskIdEventsPath = z.object({
@@ -295,7 +295,7 @@ export const get = oc
path: '/conversations',
tags: ['web'],
})
.input(z.object({ query: zGetConversationsQuery.optional() }))
.input(z.object({ query: zGetConversationsQuery.optional() }).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() }))
.input(z.object({ query: zGetLoginStatusQuery.optional() }).optional())
.output(zGetLoginStatusResponse)
export const status = {
@@ -771,7 +771,7 @@ export const get9 = oc
path: '/passport',
tags: ['web'],
})
.input(z.object({ query: zGetPassportQuery.optional() }))
.input(z.object({ query: zGetPassportQuery.optional() }).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() }))
.input(z.object({ query: zGetSavedMessagesQuery.optional() }).optional())
.output(zGetSavedMessagesResponse)
/**
@@ -998,7 +998,7 @@ export const get14 = oc
path: '/webapp/access-mode',
tags: ['web'],
})
.input(z.object({ query: zGetWebappAccessModeQuery.optional() }))
.input(z.object({ query: zGetWebappAccessModeQuery.optional() }).optional())
.output(zGetWebappAccessModeResponse)
export const accessMode = {
@@ -700,10 +700,10 @@ export const zParagraphInputConfig = z.object({
})
export const zFormInputConfig = z.discriminatedUnion('type', [
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') }),
zParagraphInputConfig,
zSelectInputConfig,
zFileInputConfig,
zFileListInputConfig,
])
/**
@@ -18,7 +18,6 @@ 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` |
+11 -33
View File
@@ -3,7 +3,6 @@ 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>
@@ -40,16 +39,8 @@ type ApiSpec = {
}
type ApiJob = {
clean?: boolean
document: SwaggerDocument
outputPath: string
plugins?: UserConfig['plugins']
source?: {
callback: () => void
enabled: true
path: null
serialize: () => string
}
}
type ApiContractOperation = {
@@ -414,24 +405,6 @@ 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>>>()
@@ -450,7 +423,12 @@ const splitConsoleDocument = (document: SwaggerDocument) => {
}),
)
return [...jobs, createConsoleContractEntryJob(document, segments)]
// 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
}
const createApiJobs = (spec: ApiSpec): ApiJob[] => {
@@ -478,15 +456,13 @@ 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: job.plugins ?? [
plugins: [
{
comments: false,
name: '@hey-api/typescript',
@@ -501,8 +477,10 @@ const createApiConfig = (job: ApiJob): UserConfig => ({
.call(
$.func((predicate) => {
const value = $.id('value')
const isBlob = $.binary(value, ts.SyntaxKind.InstanceOfKeyword, $.id('Blob'))
const isFile = $.binary(value, ts.SyntaxKind.InstanceOfKeyword, $.id('File'))
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)
predicate.param('value')
predicate.do($.return($.binary(isBlob, '||', isFile)))
}),
+50 -258
View File
@@ -46,8 +46,8 @@ catalogs:
specifier: 2.2.0
version: 2.2.0
'@hey-api/openapi-ts':
specifier: 0.98.2
version: 0.98.2
specifier: 0.0.0-next-20260724070045
version: 0.0.0-next-20260724070045
'@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.98.2(@typescript/typescript6@6.0.2)(magicast@0.5.3)
version: 0.0.0-next-20260724070045(magicast@0.5.3)
'@types/node':
specifier: 'catalog:'
version: 25.9.5
@@ -1960,18 +1960,12 @@ 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==}
@@ -2291,27 +2285,29 @@ packages:
peerDependencies:
react: '>= 16 || ^19.0.0-rc'
'@hey-api/codegen-core@0.9.0':
resolution: {integrity: sha512-OK9/R8WuujwgvnrDIPnEiIf6WnfUOi3GaEr6kIngqoI5FUQwYbeDKHE/frTVUl2A76ZQPCrMknHtPx6Gqtwf8Q==}
'@hey-api/codegen-cli@0.0.0-next-20260724070045':
resolution: {integrity: sha512-bn0ncYXv9PhqsA3u7WJuxTO8uCF/JNUn2SjmNhZkvXf/8E0iAXGkEiLZX2Ed+3cWwiEIQjrP+knrud2b16MBCQ==}
engines: {node: '>=22.18.0'}
'@hey-api/json-schema-ref-parser@1.4.3':
resolution: {integrity: sha512-UzGSDzh3QUhrnwl4atnHc2YqDO6KemYVEOwl1Ynowm/tcr0XlpdHOpyWr5UaWIJfiXTXdYRIC9k2Yxm19pcPzQ==}
'@hey-api/codegen-core@0.0.0-next-20260724070045':
resolution: {integrity: sha512-nzNZVgqrAY86CmjslqKMZM1C3Ai/zaB3lzBvOv5U3NJUO5ZOqU0LDPwMoJGQK/Dpw4TaKPWJ/arjIUS03281iw==}
engines: {node: '>=22.18.0'}
'@hey-api/openapi-ts@0.98.2':
resolution: {integrity: sha512-2nVJXH8tpFPGTBOhxyjEd1Jw0hsRqJqeTQW3kltAjVdSU4YWxeu97x5sgNOmsbsfeg6Dqz7Wfzs26walBOuswA==}
'@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==}
engines: {node: '>=22.18.0'}
hasBin: true
peerDependencies:
typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc'
'@hey-api/shared@0.4.8':
resolution: {integrity: sha512-29Pg2FB0UW20pplYgcfiQn1hQYpbZ9D2gdDJc7nDK3xh3pvHOTGP0v3R2ueFpFnw9GN1SRhIdhiVuAYWMDimjA==}
'@hey-api/shared@0.0.0-next-20260724070045':
resolution: {integrity: sha512-1l5mWzZ09PLDXtdYWcHweefWqq+6VtXVZjeA3m+YFs+zw9lWcdYIs0LPFMKSqv2sXgnj2c1m5vK1urnDvmmqNQ==}
engines: {node: '>=22.18.0'}
'@hey-api/spec-types@0.2.0':
resolution: {integrity: sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==}
'@hey-api/spec-types@0.0.0-next-20260724070045':
resolution: {integrity: sha512-HkYEwGQlj9X8+KYVFJYAEo4cKsMaZ8z/rF95v8ppIIleZJA+hRaFDZNZPMwCNPXtraTELcc1BUZ4f4rJaBvpLw==}
'@hey-api/types@0.1.4':
resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==}
@@ -3425,207 +3421,104 @@ 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]
@@ -5653,6 +5546,9 @@ 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==}
@@ -5700,6 +5596,7 @@ 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==}
@@ -6636,6 +6533,7 @@ 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==}
@@ -7016,8 +6914,8 @@ packages:
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.3.0:
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
js-yaml@5.2.0:
resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==}
hasBin: true
js-yaml@5.2.2:
@@ -7914,9 +7812,6 @@ 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==}
@@ -8477,11 +8372,6 @@ 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'}
@@ -9502,7 +9392,7 @@ snapshots:
'@amplitude/rrweb-snapshot@2.1.0':
dependencies:
postcss: 8.5.19
postcss: 8.5.23
'@amplitude/rrweb-types@2.0.0-alpha.40': {}
@@ -9878,12 +9768,6 @@ 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
@@ -9896,11 +9780,6 @@ 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
@@ -10220,7 +10099,11 @@ snapshots:
dependencies:
react: 19.2.8
'@hey-api/codegen-core@0.9.0(magicast@0.5.3)':
'@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)':
dependencies:
'@hey-api/types': 0.1.4
ansi-colors: 4.1.3
@@ -10229,42 +10112,41 @@ snapshots:
transitivePeerDependencies:
- magicast
'@hey-api/json-schema-ref-parser@1.4.3':
'@hey-api/json-schema-ref-parser@0.0.0-next-20260724070045':
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
js-yaml: 4.3.0
js-yaml: 5.2.0
'@hey-api/openapi-ts@0.98.2(@typescript/typescript6@6.0.2)(magicast@0.5.3)':
'@hey-api/openapi-ts@0.0.0-next-20260724070045(magicast@0.5.3)':
dependencies:
'@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/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/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.4.8(magicast@0.5.3)':
'@hey-api/shared@0.0.0-next-20260724070045(magicast@0.5.3)':
dependencies:
'@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/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/types': 0.1.4
ansi-colors: 4.1.3
cross-spawn: 7.0.6
open: 11.0.0
semver: 7.8.2
semver: 7.8.5
transitivePeerDependencies:
- magicast
'@hey-api/spec-types@0.2.0':
'@hey-api/spec-types@0.0.0-next-20260724070045':
dependencies:
'@hey-api/types': 0.1.4
@@ -10925,13 +10807,6 @@ 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
@@ -11199,109 +11074,54 @@ 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
@@ -11309,15 +11129,9 @@ 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
@@ -13384,6 +13198,8 @@ snapshots:
ci-info@4.4.0: {}
citty@0.2.2: {}
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@@ -14928,7 +14744,7 @@ snapshots:
js-tokens@9.0.1: {}
js-yaml@4.3.0:
js-yaml@5.2.0:
dependencies:
argparse: 2.0.1
@@ -15992,28 +15808,6 @@ 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
@@ -16833,8 +16627,6 @@ snapshots:
semver@6.3.1: {}
semver@7.8.2: {}
semver@7.8.5: {}
server-only@0.0.1: {}
@@ -17029,7 +16821,7 @@ snapshots:
jsonc-parser: 3.3.1
open: 10.2.0
oxc-parser: 0.127.0
oxc-resolver: 11.21.3
oxc-resolver: 11.24.2
recast: 0.23.12
semver: 7.8.5
use-sync-external-store: 1.6.0(react@19.2.8)
@@ -17056,7 +16848,7 @@ snapshots:
jsonc-parser: 3.3.1
open: 10.2.0
oxc-parser: 0.127.0
oxc-resolver: 11.21.3
oxc-resolver: 11.24.2
recast: 0.23.12
semver: 7.8.5
use-sync-external-store: 1.6.0(react@19.2.8)
@@ -18043,7 +17835,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.98.2': '2026-06-08T05:37:17.524Z'
'@hey-api/openapi-ts@0.0.0-next-20260724070045': '2026-07-24T07:02:37.380Z'
'@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
View File
@@ -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.98.2
'@hey-api/openapi-ts': 0.0.0-next-20260724070045
'@hono/node-server': 2.0.12
'@iconify-json/heroicons': 1.2.3
'@iconify-json/ri': 1.2.10
@@ -1,5 +0,0 @@
import SkillDetailPage from '@/features/skills/detail-page'
export default function Page() {
return <SkillDetailPage />
}
-5
View File
@@ -1,5 +0,0 @@
import SkillsPage from '@/features/skills/page'
export default function Page() {
return <SkillsPage />
}
@@ -91,12 +91,9 @@ describe('AgentRosterResponseContent', () => {
await user.click(processToggle)
expect(processToggle).toHaveAttribute('aria-expanded', 'true')
await waitFor(
() => {
expect(screen.getByText('history answer')).toBeInTheDocument()
},
{ timeout: 5000 },
)
await waitFor(() => {
expect(screen.getByText('history answer')).toBeInTheDocument()
})
expect(screen.queryByText('internal thought should not render')).not.toBeInTheDocument()
})
@@ -125,12 +122,9 @@ 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')
},
{ timeout: 5000 },
)
await waitFor(() => {
expect(screen.getByText('const answer = 42').tagName).toBe('CODE')
})
})
it('should keep one collapsible thinking timeline while response parts interleave', async () => {
@@ -1,73 +1,416 @@
import type { ReactNode } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import ModelParameterModal from '../index'
const mocks = vi.hoisted(() => ({
openIntegrationsSetting: vi.fn(),
}))
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'],
},
],
},
]
vi.mock('@/app/components/header/account-setting/use-integrations-setting', () => ({
useIntegrationsSetting: () => mocks.openIntegrationsSetting,
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
isAPIKeySet: true,
}),
}))
vi.mock('@/service/use-common', () => ({
useModelParameterRules: () => ({
data: {
data: [],
data: parameterRules,
},
isLoading: false,
isLoading: isRulesLoading,
isPending: isRulesPending,
}),
}))
vi.mock('../../hooks', () => ({
useTextGenerationCurrentProviderAndModelAndModelList: () => ({
activeTextGenerationModelList: [],
currentModel: undefined,
currentProvider: undefined,
currentProvider,
currentModel,
activeTextGenerationModelList,
}),
}))
vi.mock('../../model-selector', () => ({
default: ({ onConfigureEmptyState }: { onConfigureEmptyState?: () => void }) => (
<button type="button" onClick={onConfigureEmptyState}>
configure-empty-model
</button>
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('@/app/components/base/loading', () => ({
default: () => <div>loading</div>,
}))
vi.mock('../parameter-item', () => ({
default: () => <div>parameter-item</div>,
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('../presets-parameter', () => ({
default: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
default: ({
onSelect,
supportedParameterNames,
}: {
onSelect: (id: number) => void
supportedParameterNames?: string[]
}) => {
if (supportedParameterNames && !supportedParameterNames.includes('temperature')) return null
return <button onClick={() => onSelect(1)}>Preset 1</button>
},
}))
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('opens provider settings from the model selector empty state', () => {
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' } }]
render(
<ModelParameterModal
isAdvancedMode
modelId=""
provider=""
completionParams={{}}
setModel={vi.fn()}
onCompletionParamsChange={vi.fn()}
{...defaultProps}
isInWorkflow
nodesOutputVars={mockNodesOutputVars as never}
availableNodes={mockAvailableNodes as never}
/>,
)
fireEvent.click(screen.getByText('configure-empty-model'))
openSettings()
expect(mocks.openIntegrationsSetting).toHaveBeenCalledWith({
payload: 'provider',
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=""
modelId=""
isInWorkflow
renderTrigger={({ open }) => <span>{open ? 'Custom Open' : 'Custom Closed'}</span>}
/>,
)
fireEvent.click(screen.getByText('Custom Closed'))
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()
})
})
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()
})
})
@@ -1,5 +1,5 @@
import type { FC, ReactNode } from 'react'
import type { DefaultModel, FormValue, Model, ModelParameterRule } from '../declarations'
import type { DefaultModel, FormValue, ModelParameterRule } from '../declarations'
import type { ParameterValue } from './parameter-item'
import type { TriggerProps } from './types'
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
@@ -7,9 +7,8 @@ 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'
@@ -35,10 +34,8 @@ 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[]
@@ -56,25 +53,17 @@ 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 || []
@@ -82,7 +71,6 @@ 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({
@@ -92,10 +80,10 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
}
const handleChangeModel = ({ provider, model }: DefaultModel) => {
const targetProvider = availableTextGenerationModelList.find(
const targetProvider = activeTextGenerationModelList.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,
@@ -103,15 +91,6 @@ 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) {
@@ -135,6 +114,8 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
})
}
const hasSelectedModel = !!provider && !!modelId
return (
<Popover
open={open}
@@ -152,8 +133,8 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
>
{renderTrigger({
open,
currentProvider: selectedProvider,
currentModel: selectedModel,
currentProvider,
currentModel,
providerName: provider,
modelId,
})}
@@ -161,16 +142,11 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
}
/>
) : (
<div
className={cn(
'flex h-8 min-w-[296px] items-center gap-px overflow-hidden rounded-lg',
triggerContainerClassName,
)}
>
<div className="flex h-8 min-w-[296px] items-center gap-px overflow-hidden rounded-lg">
<div className="min-w-0 flex-1">
<ModelSelector
defaultModel={provider || modelId ? { provider, model: modelId } : undefined}
modelList={availableTextGenerationModelList}
modelList={activeTextGenerationModelList}
readonly={readonly}
triggerClassName={cn(
'h-8! w-full rounded-r-none!',
@@ -178,8 +154,6 @@ 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
@@ -213,9 +187,8 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
<div className="px-4 pt-2 pb-4">
<ModelSelector
defaultModel={hasSelectedModel ? { provider, model: modelId } : undefined}
modelList={availableTextGenerationModelList}
modelList={activeTextGenerationModelList}
onSelect={handleChangeModel}
onOpenProviderSettings={handleOpenModelSettings}
onHide={() => setOpen(false)}
/>
</div>
@@ -276,10 +249,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
{debugWithMultipleModel
? t(($) => $.debugAsSingleModel, { ns: 'appDebug' })
: t(($) => $.debugAsMultipleModel, { ns: 'appDebug' })}
<span
aria-hidden
className="i-custom-vender-line-arrows-arrow-narrow-left size-3 rotate-180"
/>
<ArrowNarrowLeft className="size-3 rotate-180" />
</div>
)}
</PopoverContent>
@@ -33,7 +33,6 @@ type ModelSelectorProps = {
hideProviderSettingsFooter?: boolean
onConfigureEmptyState?: () => void
onOpenMarketplace?: () => void
onOpenProviderSettings?: () => void
providerSettingsSource?: 'agent'
showModelMeta?: boolean
modelPredicate?: ModelSelectorModelPredicate
@@ -53,7 +52,6 @@ function ModelSelector({
hideProviderSettingsFooter,
onConfigureEmptyState,
onOpenMarketplace,
onOpenProviderSettings,
providerSettingsSource,
showModelMeta,
modelPredicate,
@@ -182,7 +180,6 @@ function ModelSelector({
modelSuggestionPredicate={modelSuggestionPredicate}
onConfigureEmptyState={onConfigureEmptyState ? handleConfigureEmptyState : undefined}
onOpenMarketplace={onOpenMarketplace}
onOpenProviderSettings={onOpenProviderSettings}
onInputValueChange={setInputValue}
onHide={handleHide}
/>
@@ -9,6 +9,7 @@ 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'
@@ -56,8 +57,7 @@ function PopupItem({
const updateModelProviders = useUpdateModelProviders()
const currentProvider = modelProviders.find((provider) => provider.provider === model.provider)
const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions()
const canOpenCredentialDropdown =
!!currentProvider && (canUseCredential || canCreateCredential || canManageCredential)
const canOpenCredentialDropdown = canUseCredential || canCreateCredential || canManageCredential
const handleOpenModelModal = () => {
if (!canCreateCredential) return
@@ -77,8 +77,7 @@ function PopupItem({
})
}
// 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 state = useCredentialPanelState(currentProvider)
const { isChangingPriority, handleChangePriority } = useChangeProviderPriority(currentProvider)
const groupItems = useMemo(
() =>
@@ -91,11 +90,10 @@ function PopupItem({
[model.models, model.provider],
)
const isUsingCredits = credentialPanelState.priority === 'credits'
const hasCredits = !credentialPanelState.isCreditsExhausted
const isApiKeyActive =
credentialPanelState.variant === 'api-active' || credentialPanelState.variant === 'api-fallback'
const { credentialName } = credentialPanelState
const isUsingCredits = state.priority === 'credits'
const hasCredits = !state.isCreditsExhausted
const isApiKeyActive = state.variant === 'api-active' || state.variant === 'api-fallback'
const { credentialName } = state
const handleCloseDropdown = useCallback(() => {
setDropdownOpen(false)
@@ -131,10 +129,7 @@ function PopupItem({
{isUsingCredits ? (
hasCredits ? (
<>
<span
aria-hidden
className="i-custom-vender-line-financeandecommerce-credits-coin size-3"
/>
<CreditsCoin className="size-3" />
<span className="ml-1 truncate">
{t(($) => $['modelProvider.selector.aiCredits'], { ns: 'common' })}
</span>
@@ -166,17 +161,15 @@ function PopupItem({
</button>
}
/>
{currentProvider && (
<PopoverContent placement="bottom-end">
<DropdownContent
provider={currentProvider}
state={credentialPanelState}
isChangingPriority={isChangingPriority}
onChangePriority={handleChangePriority}
onClose={handleCloseDropdown}
/>
</PopoverContent>
)}
<PopoverContent placement="bottom-end">
<DropdownContent
provider={currentProvider}
state={state}
isChangingPriority={isChangingPriority}
onChangePriority={handleChangePriority}
onClose={handleCloseDropdown}
/>
</PopoverContent>
</Popover>
</div>
{!collapsed &&
@@ -222,7 +215,7 @@ function PopupItem({
</ModelName>
</div>
{defaultModel?.model === modelItem.model &&
defaultModel.provider === model.provider && (
defaultModel.provider === currentProvider.provider && (
<ComboboxItemIndicator className="shrink-0 text-text-accent">
<span
className="i-custom-vender-line-general-check size-4"
@@ -69,7 +69,6 @@ export type PopupProps = {
onConfigureEmptyState?: () => void
onInputValueChange: (value: string) => void
onOpenMarketplace?: () => void
onOpenProviderSettings?: () => void
onHide: () => void
}
function Popup({
@@ -84,7 +83,6 @@ function Popup({
onConfigureEmptyState,
onInputValueChange,
onOpenMarketplace,
onOpenProviderSettings,
onHide,
}: PopupProps) {
const { t } = useTranslation()
@@ -257,16 +255,11 @@ function Popup({
const handleOpenSettings = useCallback(() => {
onHide()
if (onOpenProviderSettings) {
onOpenProviderSettings()
return
}
openIntegrationsSetting({
payload: ACCOUNT_SETTING_TAB.PROVIDER,
source: providerSettingsSource,
})
}, [onHide, onOpenProviderSettings, openIntegrationsSetting, providerSettingsSource])
}, [onHide, openIntegrationsSetting, providerSettingsSource])
const handleClosePreviewCard = useCallback(() => {
previewCardHandle.close()
}, [previewCardHandle])
@@ -564,10 +564,6 @@ 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',
@@ -588,10 +584,6 @@ 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', () => {
@@ -751,7 +743,6 @@ 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',
-11
View File
@@ -32,8 +32,6 @@ 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}/`)
@@ -71,15 +69,6 @@ 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',
@@ -119,10 +119,8 @@ describe('ConversationVariableModal', () => {
})
expect(screen.getAllByText('session_state')).toHaveLength(2)
expect(
await screen.findByText((content) => content.includes('formatted-100')),
).toBeInTheDocument()
expect(await screen.findByTestId('conversation-code-editor')).toHaveTextContent('{"latest":1}')
expect(screen.getByText((content) => content.includes('formatted-100'))).toBeInTheDocument()
expect(screen.getByTestId('conversation-code-editor')).toHaveTextContent('{"latest":1}')
await user.click(screen.getByText('summary'))
expect(screen.getByText('latest text')).toBeInTheDocument()
-4
View File
@@ -16,10 +16,6 @@ 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(),
-22
View File
@@ -27,23 +27,6 @@ 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()
@@ -60,11 +43,6 @@ 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 { useAgentOrchestrateViewingVersion } from './read-only-context'
import { useAgentOrchestrateReadOnly } from './read-only-context'
export function AgentOrchestrateAddActionsProvider({ children }: { children: ReactNode }) {
const isViewingVersion = useAgentOrchestrateViewingVersion()
const readOnly = useAgentOrchestrateReadOnly()
const [actions, setActions] = useState<AgentOrchestrateAddActions>({})
const registerAction = useCallback(
(key: AgentOrchestrateAddActionKey, action: AgentOrchestrateAddAction) => {
if (isViewingVersion) return () => undefined
if (readOnly) return () => undefined
setActions((currentActions) => {
if (currentActions[key] === action) return currentActions
@@ -37,15 +37,15 @@ export function AgentOrchestrateAddActionsProvider({ children }: { children: Rea
})
}
},
[isViewingVersion],
[readOnly],
)
const value = useMemo(
() => ({
actions: isViewingVersion ? {} : actions,
actions: readOnly ? {} : actions,
registerAction,
}),
[actions, isViewingVersion, registerAction],
[actions, readOnly, registerAction],
)
return (
@@ -4,7 +4,7 @@ import type { ButtonProps } from '@langgenius/dify-ui/button'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
import { useAgentOrchestrateViewingVersion } from '../read-only-context'
import { useAgentOrchestrateReadOnly } from '../read-only-context'
type ConfigureSectionAddButtonProps = Omit<
ButtonProps,
@@ -19,9 +19,9 @@ export function ConfigureSectionAddButton({
...props
}: ConfigureSectionAddButtonProps) {
const { t } = useTranslation('common')
const isViewingVersion = useAgentOrchestrateViewingVersion()
const readOnly = useAgentOrchestrateReadOnly()
if (isViewingVersion) return null
if (readOnly) return null
return (
<Button
@@ -13,10 +13,7 @@ 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,
AgentOrchestrateViewingVersionContext,
} from '../../read-only-context'
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
import { AgentFiles } from '../index'
type ConfigFileQueryOptionsInput = {
@@ -174,12 +171,10 @@ 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: {
@@ -196,12 +191,10 @@ function renderAgentFiles({
<QueryClientTestProvider queryClient={queryClient}>
<AgentConfigApiContextProvider value={apiContext}>
<AgentComposerProvider initialDraft={initialDraft}>
<AgentOrchestrateViewingVersionContext value={viewingVersion}>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentFiles />
<ConfigSnapshotProbe />
</AgentOrchestrateReadOnlyContext>
</AgentOrchestrateViewingVersionContext>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentFiles />
<ConfigSnapshotProbe />
</AgentOrchestrateReadOnlyContext>
</AgentComposerProvider>
</AgentConfigApiContextProvider>
</QueryClientTestProvider>,
@@ -649,8 +642,8 @@ describe('AgentFiles', () => {
expect(snapshot.config_note).toBe('')
})
it('should keep flat config files visible without drive-prefix filtering and disable add when viewing a version', () => {
renderAgentFiles({ readOnly: true, viewingVersion: true })
it('should keep flat config files visible without drive-prefix filtering and disable add in read-only mode', () => {
renderAgentFiles({ readOnly: true })
expect(screen.getByText('diagram.png')).toBeInTheDocument()
expect(screen.getByText('brief.md')).toBeInTheDocument()
@@ -658,12 +651,4 @@ 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,10 +23,7 @@ import { AgentKnowledgeRetrieval } from './knowledge'
import { AgentModelField } from './model-config/field'
import { AgentPromptEditor } from './prompt-editor'
import { AgentConfigurePublishBar } from './publish-bar'
import {
AgentOrchestrateReadOnlyContext,
AgentOrchestrateViewingVersionContext,
} from './read-only-context'
import { AgentOrchestrateReadOnlyContext } from './read-only-context'
import { AgentSkills } from './skills'
import { AgentTools } from './tools'
@@ -135,43 +132,41 @@ export function AgentOrchestratePanel({
/>
)}
<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>
<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>
{orchestrateBottomAction ? (
<AgentOrchestrateBottomActions shrinkOnOpen={!bottomAction}>
@@ -10,10 +10,7 @@ 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,
AgentOrchestrateViewingVersionContext,
} from '../../read-only-context'
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
import { AgentKnowledgeRetrieval } from '../index'
vi.mock('@/context/workspace-state', async () => {
@@ -110,21 +107,17 @@ 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}>
<AgentOrchestrateViewingVersionContext value={viewingVersion}>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentKnowledgeRetrieval />
</AgentOrchestrateReadOnlyContext>
</AgentOrchestrateViewingVersionContext>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentKnowledgeRetrieval />
</AgentOrchestrateReadOnlyContext>
{showConfigSnapshot && <ConfigSnapshotPreview />}
</AgentComposerProvider>,
)
@@ -161,8 +154,8 @@ describe('AgentKnowledgeRetrieval', () => {
).not.toBeInTheDocument()
})
it('should hide add, edit, and remove actions when viewing a version', () => {
renderKnowledgeRetrieval({ readOnly: true, viewingVersion: true })
it('should hide add, edit, and remove actions when readonly', () => {
renderKnowledgeRetrieval({ readOnly: true })
expect(
screen.getByText('agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne'),
@@ -183,16 +176,6 @@ 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', () => {
@@ -1,12 +1,7 @@
import { createContext, use } from 'react'
export const AgentOrchestrateReadOnlyContext = createContext(false)
export const AgentOrchestrateViewingVersionContext = createContext(false)
export function useAgentOrchestrateReadOnly() {
return use(AgentOrchestrateReadOnlyContext)
}
export function useAgentOrchestrateViewingVersion() {
return use(AgentOrchestrateViewingVersionContext)
}
@@ -1,4 +1,3 @@
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'
@@ -12,10 +11,7 @@ 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,
AgentOrchestrateViewingVersionContext,
} from '../../read-only-context'
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
import { AgentSkills } from '../index'
type ConfigSkillInspectQueryOptionsInput = {
@@ -43,16 +39,10 @@ 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: {
@@ -69,8 +59,6 @@ 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(),
@@ -177,43 +165,9 @@ 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 })
@@ -221,23 +175,6 @@ 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,
@@ -252,12 +189,10 @@ 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: {
@@ -270,12 +205,10 @@ function renderAgentSkills({
<QueryClientProvider client={queryClient}>
<AgentConfigApiContextProvider value={apiContext}>
<AgentComposerProvider initialDraft={initialDraft}>
<AgentOrchestrateViewingVersionContext value={viewingVersion}>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentSkills />
<ConfigSnapshotProbe />
</AgentOrchestrateReadOnlyContext>
</AgentOrchestrateViewingVersionContext>
<AgentOrchestrateReadOnlyContext value={readOnly}>
<AgentSkills />
<ConfigSnapshotProbe />
</AgentOrchestrateReadOnlyContext>
</AgentComposerProvider>
</AgentConfigApiContextProvider>
</QueryClientProvider>,
@@ -292,22 +225,6 @@ 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 () => ({
@@ -376,38 +293,6 @@ 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(() => {
@@ -487,7 +372,9 @@ describe('AgentSkills', () => {
const user = userEvent.setup()
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
await openUploadSkillDialog(user)
await user.click(
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
)
const input = await waitFor(() => {
const element = document.querySelector('input[type="file"]')
@@ -531,375 +418,13 @@ describe('AgentSkills', () => {
expect(toast.success).toHaveBeenCalled()
})
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)
await user.click(
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
)
expect(
screen.queryByText('agentV2.agentDetail.configure.skills.upload.warning.specification'),
@@ -913,7 +438,9 @@ describe('AgentSkills', () => {
.mockImplementationOnce(() => new Promise<never>(() => undefined))
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
await openUploadSkillDialog(user)
await user.click(
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
)
const input = await waitFor(() => {
const element = document.querySelector('input[type="file"]')
expect(element).not.toBeNull()
@@ -950,7 +477,9 @@ describe('AgentSkills', () => {
mocks.uploadSkillMutationFn.mockRejectedValueOnce(new Error('Backend upload error'))
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
await openUploadSkillDialog(user)
await user.click(
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
)
const input = await waitFor(() => {
const element = document.querySelector('input[type="file"]')
@@ -986,7 +515,9 @@ describe('AgentSkills', () => {
},
})
await openUploadSkillDialog(user)
await user.click(
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
)
const input = await waitFor(() => {
const element = document.querySelector('input[type="file"]')
expect(element).not.toBeNull()
@@ -1338,87 +869,12 @@ describe('AgentSkills', () => {
)
})
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,
})
it('should disable add and remove actions when the section is read only', () => {
const { container } = renderAgentSkills({ readOnly: 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()
})
})
@@ -1,391 +1,30 @@
'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 { 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 { useMutation } from '@tanstack/react-query'
import { useAtomValue, useSetAtom } from 'jotai'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useCallback, 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()
@@ -398,68 +37,6 @@ 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
@@ -467,20 +44,6 @@ 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)
@@ -490,36 +53,11 @@ 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)
@@ -575,91 +113,26 @@ export function AgentSkills() {
rootClassName="border-b border-divider-subtle pt-4"
panelContentClassName="flex flex-col gap-1 pb-4"
actions={
!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>
)
<ConfigureSectionAddButton
ariaLabel={t(($) => $['agentDetail.configure.skills.add'])}
onClick={() => handleOpenUpload()}
/>
}
>
{!hasSkills ? (
{skills.length === 0 ? (
<ConfigureSectionEmpty
title={t(($) => $['agentDetail.configure.skills.empty.title'])}
description={t(($) => $['agentDetail.configure.skills.empty.description'])}
/>
) : (
<>
{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}
/>
))}
</>
skills.map((skill) => (
<AgentSkillItem
key={skill.id}
apiContext={apiContext}
skill={skill}
onRemove={handleRemoveSkill}
/>
))
)}
</ConfigureSection>
<AgentSkillUploadDialog
@@ -15,10 +15,7 @@ import {
agentComposerSavedDraftAtom,
isAgentComposerDirtyAtom,
} from '@/features/agent-v2/agent-composer/store'
import {
AgentOrchestrateReadOnlyContext,
AgentOrchestrateViewingVersionContext,
} from '../../read-only-context'
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
import { AgentTools } from '../index'
const toolProviderState = vi.hoisted(() => ({
@@ -343,13 +340,7 @@ function renderAgentToolsWithStore(initialDraft: AgentSoulConfigFormState = agen
}
}
function renderReadonlyAgentTools({
initialDraft = agentToolsDraft,
viewingVersion = false,
}: {
initialDraft?: AgentSoulConfigFormState
viewingVersion?: boolean
} = {}) {
function renderReadonlyAgentTools(initialDraft: AgentSoulConfigFormState = agentToolsDraft) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -361,11 +352,9 @@ function renderReadonlyAgentTools({
return render(
<QueryClientProvider client={queryClient}>
<AgentComposerProvider initialDraft={initialDraft}>
<AgentOrchestrateViewingVersionContext value={viewingVersion}>
<AgentOrchestrateReadOnlyContext value>
<AgentTools />
</AgentOrchestrateReadOnlyContext>
</AgentOrchestrateViewingVersionContext>
<AgentOrchestrateReadOnlyContext value>
<AgentTools />
</AgentOrchestrateReadOnlyContext>
</AgentComposerProvider>
</QueryClientProvider>,
)
@@ -452,9 +441,9 @@ describe('AgentTools', () => {
).toBeInTheDocument()
})
it('should hide add, edit, and remove actions when viewing a version', async () => {
it('should hide add, edit, and remove actions when readonly', async () => {
const user = userEvent.setup()
renderReadonlyAgentTools({ viewingVersion: true })
renderReadonlyAgentTools()
expect(
screen.queryByRole('button', {
@@ -495,16 +484,6 @@ 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 { useAgentOrchestrateViewingVersion } from '../read-only-context'
import { useAgentOrchestrateReadOnly } 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 isViewingVersion = useAgentOrchestrateViewingVersion()
const readOnly = useAgentOrchestrateReadOnly()
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={
!isViewingVersion ? (
!readOnly ? (
<AddToolMenu
onAddCliTool={openCliToolDialog}
onAddTools={addTools}
@@ -2,7 +2,6 @@
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,
@@ -225,12 +224,7 @@ function AgentRosterItem({ agent }: { agent: AgentAppPartial }) {
</div>
</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',
)}
>
<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">
<DropdownMenu modal={false}>
<DropdownMenuTrigger
aria-label={t(($) => $['roster.moreActions'], { name: agent.name })}
-15
View File
@@ -1,15 +0,0 @@
# 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
-488
View File
@@ -1,488 +0,0 @@
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()
})
})
-195
View File
@@ -1,195 +0,0 @@
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
-759
View File
@@ -1,759 +0,0 @@
'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>
)
}
-12
View File
@@ -1,12 +0,0 @@
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([])
+1 -120
View File
@@ -203,22 +203,15 @@
"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": "تبديل المهارات",
@@ -428,117 +421,5 @@
"roster.sort.optionsLabel": "خيارات الفرز",
"roster.sort.recentlyCreated": "الأحدث إنشاءً",
"roster.updateSuccess": "تم تحديث الوكيل.",
"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"
"roster.usageStatus.draft": "مسودة"
}
-3
View File
@@ -201,7 +201,6 @@
"mainNav.home": "الرئيسية",
"mainNav.integrations": "التكاملات",
"mainNav.marketplace": "سوق الإضافات",
"mainNav.skills": "Skills",
"mainNav.webApps.noResults": "لم يتم العثور على تطبيقات ويب",
"mainNav.webApps.openApp": "فتح تطبيق الويب {{name}}",
"mainNav.webApps.searchPlaceholder": "البحث في تطبيقات الويب",
@@ -463,7 +462,6 @@
"operation.downloading": "جارٍ التنزيل...",
"operation.duplicate": "تكرار",
"operation.edit": "تعديل",
"operation.export": "تصدير",
"operation.exporting": "جارٍ التصدير",
"operation.fill": "ملء تلقائي",
"operation.format": "تنسيق",
@@ -505,7 +503,6 @@
"operation.sure": "أنا متأكد",
"operation.toggleFullscreen": "تبديل ملء الشاشة",
"operation.toggleMute": "تبديل كتم الصوت",
"operation.upload": "Upload",
"operation.view": "عرض",
"operation.viewDetails": "عرض التفاصيل",
"operation.viewMore": "عرض المزيد",
+1 -120
View File
@@ -203,22 +203,15 @@
"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",
@@ -428,117 +421,5 @@
"roster.sort.optionsLabel": "Sortieroptionen",
"roster.sort.recentlyCreated": "Zuletzt erstellt",
"roster.updateSuccess": "Agent aktualisiert.",
"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"
"roster.usageStatus.draft": "Entwurf"
}
-3
View File
@@ -202,7 +202,6 @@
"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",
@@ -464,7 +463,6 @@
"operation.downloading": "Wird heruntergeladen...",
"operation.duplicate": "Duplikat",
"operation.edit": "Bearbeiten",
"operation.export": "Exportieren",
"operation.exporting": "Exportiere",
"operation.fill": "Automatisch ausfüllen",
"operation.format": "Format",
@@ -506,7 +504,6 @@
"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",
+1 -196
View File
@@ -203,24 +203,15 @@
"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",
@@ -235,15 +226,6 @@
"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",
@@ -439,182 +421,5 @@
"roster.sort.optionsLabel": "Sort options",
"roster.sort.recentlyCreated": "Recently created",
"roster.updateSuccess": "Agent updated.",
"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"
"roster.usageStatus.draft": "Draft"
}
-3
View File
@@ -202,7 +202,6 @@
"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",
@@ -464,7 +463,6 @@
"operation.downloading": "Downloading",
"operation.duplicate": "Duplicate",
"operation.edit": "Edit",
"operation.export": "Export",
"operation.exporting": "Exporting",
"operation.fill": "Autofill",
"operation.format": "Format",
@@ -506,7 +504,6 @@
"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",
+1 -120
View File
@@ -203,22 +203,15 @@
"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",
@@ -428,117 +421,5 @@
"roster.sort.optionsLabel": "Opciones de ordenación",
"roster.sort.recentlyCreated": "Creados recientemente",
"roster.updateSuccess": "Agente actualizado.",
"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"
"roster.usageStatus.draft": "Borrador"
}
-3
View File
@@ -202,7 +202,6 @@
"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",
@@ -464,7 +463,6 @@
"operation.downloading": "Descargando...",
"operation.duplicate": "Duplicar",
"operation.edit": "Editar",
"operation.export": "Exportar",
"operation.exporting": "Exportando",
"operation.fill": "Autocompletar",
"operation.format": "Formato",
@@ -506,7 +504,6 @@
"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",
+1 -120
View File
@@ -203,22 +203,15 @@
"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": "تغییر مهارت‌ها",
@@ -428,117 +421,5 @@
"roster.sort.optionsLabel": "گزینه‌های مرتب‌سازی",
"roster.sort.recentlyCreated": "تازه‌ترین ایجاد",
"roster.updateSuccess": "عامل به‌روزرسانی شد.",
"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"
"roster.usageStatus.draft": "پیش‌نویس"
}

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