Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
685f611f86 | ||
|
|
01552e03d6 | ||
|
|
e1bacf098a | ||
|
|
37510f544f | ||
|
|
33e5b84074 | ||
|
|
953b264c07 | ||
|
|
c440fa1beb | ||
|
|
330288ba3d | ||
|
|
d95049cec8 | ||
|
|
615628e4f0 | ||
|
|
7d2a377374 | ||
|
|
080a09b0f5 | ||
|
|
f3df36aec1 | ||
|
|
a87e396b02 | ||
|
|
6e4893f37f | ||
|
|
c7ae155a07 | ||
|
|
a5d8af27d0 | ||
|
|
179968303a | ||
|
|
202f650b7c | ||
|
|
f23739272f | ||
|
|
0958b120e2 | ||
|
|
e6bf7d333c | ||
|
|
98ff51d570 | ||
|
|
b1249dfc79 | ||
|
|
eef27dfc8e | ||
|
|
85dd61de3a | ||
|
|
fee28ca253 | ||
|
|
1cbea4a95f | ||
|
|
b1f11384b5 | ||
|
|
890c062c01 | ||
|
|
ed4dd78210 | ||
|
|
8bafbf7238 | ||
|
|
f6522e6a34 | ||
|
|
a5336da117 | ||
|
|
a0d2d0302e | ||
|
|
f5ea9bbc02 | ||
|
|
3d7c403580 | ||
|
|
c3554ad525 | ||
|
|
f805038d16 | ||
|
|
53542cf84e | ||
|
|
6609ac0b1e | ||
|
|
963410e562 | ||
|
|
5e0d54d4ce | ||
|
|
94cf4c2957 | ||
|
|
aa43aee224 | ||
|
|
9d1a3c5a00 | ||
|
|
ebe0a269ea | ||
|
|
edff7a4471 | ||
|
|
9009960b35 | ||
|
|
9969c9f845 | ||
|
|
2dace438a3 | ||
|
|
19a8683bca | ||
|
|
154926d4ac | ||
|
|
e740e6c423 | ||
|
|
703289fb0b | ||
|
|
89219c479c | ||
|
|
3bbbdd478f | ||
|
|
8bf624401a | ||
|
|
9d5f87f0b4 | ||
|
|
f8ca584a81 | ||
|
|
520999691f | ||
|
|
4b1f4f3223 | ||
|
|
5cc0f16bae | ||
|
|
e9caaa43a3 | ||
|
|
793300066b | ||
|
|
8b456fb4df | ||
|
|
6c052bb9b5 | ||
|
|
bc3ef4de71 | ||
|
|
a14b9be0be | ||
|
|
f39e55caf6 | ||
|
|
d5964397b2 | ||
|
|
0065d0e0f1 | ||
|
|
5053828fad | ||
|
|
fd38ac854c | ||
|
|
3b033f9eb5 | ||
|
|
0451a321cb | ||
|
|
cfe627e894 | ||
|
|
47788f1617 | ||
|
|
dcdf6715a3 | ||
|
|
ea94659e7c | ||
|
|
b3e0f6c5b7 | ||
|
|
4bdc28959d | ||
|
|
3482166f78 | ||
|
|
ff1c935761 | ||
|
|
1a9c03c0af | ||
|
|
a43f2ccc0a | ||
|
|
4e00f708f7 | ||
|
|
c9a0aff2f9 | ||
|
|
5afbf25a59 | ||
|
|
b29abeafb5 | ||
|
|
3a8676371c | ||
|
|
5a13489fdd | ||
|
|
2f5fee324a | ||
|
|
c9eb56c4cb | ||
|
|
6aa5804503 | ||
|
|
bbba892849 | ||
|
|
62d95441d1 | ||
|
|
09479cafa4 | ||
|
|
26a5465c5b | ||
|
|
64558c418c | ||
|
|
785622f7f3 | ||
|
|
43440c6edb | ||
|
|
7a6c9934d7 | ||
|
|
21a4e6fbcc | ||
|
|
a4621041b6 | ||
|
|
7c7d30fe1f | ||
|
|
4ce6b1f620 | ||
|
|
f0fb69f4c7 | ||
|
|
3ac6e1b115 | ||
|
|
8767767794 | ||
|
|
a7ca830fad | ||
|
|
acf56bf154 | ||
|
|
63615b2fab | ||
|
|
7196a370a7 | ||
|
|
f1f9e34683 | ||
|
|
fa7a349a2a | ||
|
|
ac902eb2e9 | ||
|
|
bbef12212f | ||
|
|
b252c0834d | ||
|
|
8b908cbc43 | ||
|
|
0aea96233d | ||
|
|
977b2c4c93 | ||
|
|
b441e3e838 | ||
|
|
b91a3b8c27 | ||
|
|
49695a7575 | ||
|
|
654904fce0 | ||
|
|
2f1276864d | ||
|
|
5ff641a49c | ||
|
|
75bb174719 | ||
|
|
fe6375e39c | ||
|
|
9939df18f5 | ||
|
|
cac9be9d98 | ||
|
|
e3350f7ada | ||
|
|
7526ab3163 | ||
|
|
893fe3bb01 | ||
|
|
851b01578e | ||
|
|
765f1172d5 | ||
|
|
6137c60651 | ||
|
|
975b1a6303 | ||
|
|
045b39c6d6 | ||
|
|
385f2196a9 | ||
|
|
9f73e16373 | ||
|
|
cd48673c20 | ||
|
|
33a8d28d0d | ||
|
|
38a6567bad | ||
|
|
1434758d42 | ||
|
|
2941960545 | ||
|
|
c50ec44bd8 | ||
|
|
3a6b69bfe7 | ||
|
|
78d43c6712 | ||
|
|
fcf05b4b63 | ||
|
|
58abd13450 | ||
|
|
38b1a406bb | ||
|
|
dd6a28ca1f | ||
|
|
f2b0aaeb8f | ||
|
|
977c6ca61f | ||
|
|
a8879e7360 | ||
|
|
80365aec68 | ||
|
|
b1b01630e5 | ||
|
|
ec2e511d0d | ||
|
|
003c7b4fa9 | ||
|
|
5caa2eea03 | ||
|
|
d326d38a0a | ||
|
|
4275c24dc5 | ||
|
|
542544d8eb | ||
|
|
f835f0c59c | ||
|
|
d6fd31c756 | ||
|
|
0ef7bf929e | ||
|
|
64cb61f902 | ||
|
|
779f57902e | ||
|
|
4bfd7fd17c | ||
|
|
ce775fd3f9 | ||
|
|
a0a8ed7594 | ||
|
|
111260fa37 | ||
|
|
f3853f48df | ||
|
|
7fde7b7ca4 | ||
|
|
68c59ad047 | ||
|
|
7e3514adae | ||
|
|
ffbde43b2c | ||
|
|
64b7451003 | ||
|
|
3ec3242a66 | ||
|
|
238887036b | ||
|
|
4ec9fb0a58 | ||
|
|
51b668a2a6 | ||
|
|
ce38c21253 | ||
|
|
f5ba4a8e51 | ||
|
|
52100bbb29 | ||
|
|
65e6d72254 | ||
|
|
48b5dca16a | ||
|
|
fbe4e586f6 | ||
|
|
ca99dda275 | ||
|
|
b28d45bb2d | ||
|
|
404f79f370 | ||
|
|
5a002d8f6a | ||
|
|
606d1d7926 | ||
|
|
277c1ddf9c | ||
|
|
d6fc092795 | ||
|
|
3a422d4dde | ||
|
|
eb8cda6355 | ||
|
|
c81c774b49 | ||
|
|
b6431e82cd | ||
|
|
9d3b0ddf58 | ||
|
|
a6948b57e6 | ||
|
|
4cc89a054b | ||
|
|
03b18bd4e3 | ||
|
|
2907adb12e | ||
|
|
d08ca0e069 | ||
|
|
ca22915bf1 | ||
|
|
a4430fd373 | ||
|
|
90e802d5e4 | ||
|
|
6113893a53 | ||
|
|
5aea0b1489 | ||
|
|
6d59f60f88 | ||
|
|
a5a08455b2 | ||
|
|
bc296aa440 | ||
|
|
d01d63275e | ||
|
|
00e67c1db1 | ||
|
|
946603c513 | ||
|
|
6414d0a020 | ||
|
|
22cab8f4bd | ||
|
|
f08d5343d0 | ||
|
|
8c0fa26938 | ||
|
|
e07c1be932 | ||
|
|
1e4ecbfe0a | ||
|
|
fd242a37de | ||
|
|
2d8440309d | ||
|
|
4bdc2a3d71 | ||
|
|
668b99fab7 | ||
|
|
14942e7f1c | ||
|
|
c1ace7800b | ||
|
|
9a602ab6d6 | ||
|
|
36eddb6a6b | ||
|
|
340cf3a5e8 | ||
|
|
1e20db0758 | ||
|
|
2c8f39c217 | ||
|
|
a87d0bed3a | ||
|
|
c01167b1f4 | ||
|
|
23b06d5710 | ||
|
|
4731f0fd08 | ||
|
|
cf791ff9f4 | ||
|
|
2f2889f957 | ||
|
|
22ba24b6a4 | ||
|
|
bbfbe3f94c | ||
|
|
a8a081486b | ||
|
|
f966ada97b | ||
|
|
ba865c7c9c | ||
|
|
f72b30f5a2 | ||
|
|
30888bfcf9 | ||
|
|
067e95435f | ||
|
|
c0c0b22415 | ||
|
|
b95dd9594e | ||
|
|
fda3de1ec0 | ||
|
|
1c1fd3517c | ||
|
|
7f162a924c | ||
|
|
d3fcb6e045 | ||
|
|
a3d17b5e86 | ||
|
|
e1595f4b55 | ||
|
|
ae20b3b189 | ||
|
|
cd05b1d3e3 | ||
|
|
e61d4ff85d | ||
|
|
8b72e93067 | ||
|
|
ceea0b0222 | ||
|
|
137354df91 | ||
|
|
039ca228fa | ||
|
|
44f7a814c3 | ||
|
|
d5e702894a | ||
|
|
33f8999c60 | ||
|
|
f5f3c59d33 | ||
|
|
a046f66b76 | ||
|
|
05259faaf9 | ||
|
|
6d815cb16c | ||
|
|
7aa44930a7 | ||
|
|
4763a02d55 | ||
|
|
d82c30da65 | ||
|
|
fff55e65ea | ||
|
|
53fe349fa8 | ||
|
|
a05db83783 | ||
|
|
ac5cde8d1e | ||
|
|
2c40f2d3cd | ||
|
|
a006cd91a2 | ||
|
|
136eef8c70 | ||
|
|
b510cdcc48 | ||
|
|
64745080f7 | ||
|
|
b991a91b80 | ||
|
|
5a042854bd | ||
|
|
61cec2e1be | ||
|
|
3589fb3efc | ||
|
|
59295c05bf | ||
|
|
3fd75f6bd5 | ||
|
|
da94a3e334 | ||
|
|
9c96d3d417 | ||
|
|
f7dc60bf90 | ||
|
|
58f17aa5a8 | ||
|
|
95f76e12ef | ||
|
|
3933334a4d | ||
|
|
59e68de46b | ||
|
|
e1e20c9981 | ||
|
|
1495ea9a8f | ||
|
|
1af973bd79 | ||
|
|
15ac8b8954 | ||
|
|
1cd7699660 | ||
|
|
d264aee057 | ||
|
|
8f24e8af40 | ||
|
|
f92e7339f4 | ||
|
|
7908dd69be | ||
|
|
a5ff29eea5 | ||
|
|
2fe4790722 | ||
|
|
35ddd2b6f4 | ||
|
|
94dfaae3d0 | ||
|
|
78154faa62 | ||
|
|
1ff2a7c05e | ||
|
|
bb22386e30 | ||
|
|
18fa5392fe | ||
|
|
33af2faf83 | ||
|
|
e9e2b6e2a9 | ||
|
|
61c43c95e5 | ||
|
|
9316880ecb | ||
|
|
e39a7b10de | ||
|
|
2bb01984ed | ||
|
|
77e41a37ee | ||
|
|
79229b7ede | ||
|
|
fd20d4f536 | ||
|
|
796f32baa2 | ||
|
|
a605790a84 | ||
|
|
36ae648a63 | ||
|
|
59bf9019c9 | ||
|
|
c409d531da | ||
|
|
5fef0cf07c | ||
|
|
511ca69e4e | ||
|
|
4c7e60d7e7 | ||
|
|
068aced02e | ||
|
|
a9e163403d | ||
|
|
bc136b89f9 | ||
|
|
b1bd4f9a8b | ||
|
|
06d3927f05 | ||
|
|
5c3516cae8 | ||
|
|
1c18d8ddbd | ||
|
|
253dfe9351 | ||
|
|
2d3999e984 | ||
|
|
a431cc726d | ||
|
|
08493d2429 | ||
|
|
8dd0969006 | ||
|
|
2d80d3c35c | ||
|
|
4da764904c | ||
|
|
5c4f4fd1ef | ||
|
|
54843971ac | ||
|
|
5ec5d3aeb9 | ||
|
|
d661b53e49 | ||
|
|
fd1e777f85 | ||
|
|
e5f77ce185 | ||
|
|
60bc4eaf3c | ||
|
|
208073e228 | ||
|
|
99929d1c16 | ||
|
|
c699aa11db | ||
|
|
06cd0b56ac | ||
|
|
241a9e1fec | ||
|
|
3d79689fb5 | ||
|
|
9f050f7957 | ||
|
|
cb691d47d3 | ||
|
|
04f5ee58d0 | ||
|
|
f5cff724f4 | ||
|
|
1f4ccafdea | ||
|
|
163049f6ca | ||
|
|
895af9c1d1 | ||
|
|
2bec40d7be | ||
|
|
6d18502041 | ||
|
|
e1432889cb | ||
|
|
0d65a338ef | ||
|
|
3f2fa4d7de | ||
|
|
0a23c0c386 | ||
|
|
49bd13248f | ||
|
|
65e7507ca3 | ||
|
|
ba157f9604 | ||
|
|
c989c3c6ac | ||
|
|
0dc913630e | ||
|
|
875cd30b1f | ||
|
|
aa4a32ae84 | ||
|
|
8573e14777 | ||
|
|
1855be234c | ||
|
|
9bb960ff12 | ||
|
|
1c14c7d467 | ||
|
|
61faec16ca | ||
|
|
57c836e692 | ||
|
|
7d4718d8c5 | ||
|
|
3b9176f538 | ||
|
|
626cc282b1 | ||
|
|
52624d54e3 | ||
|
|
5ce038ef92 | ||
|
|
30f4d4c0c6 | ||
|
|
510679a7d1 | ||
|
|
9237f2a14a | ||
|
|
bd178c7b29 | ||
|
|
d80947aa72 | ||
|
|
1618c37d26 | ||
|
|
701ab64462 | ||
|
|
b3298800e9 | ||
|
|
0f1c6b3f78 | ||
|
|
9b4b246aad | ||
|
|
1bd654a289 | ||
|
|
fc70329bdb | ||
|
|
cd8a82fbd4 | ||
|
|
550cb7eff5 | ||
|
|
2e748c16e9 | ||
|
|
577012b66d | ||
|
|
251c324180 | ||
|
|
865a618fd5 | ||
|
|
991116990a | ||
|
|
eb5d1da0e8 | ||
|
|
dcc660daa2 | ||
|
|
dce3b7a7fc | ||
|
|
be386aba3b | ||
|
|
02e51e7d7c | ||
|
|
96b6d4f2c0 | ||
|
|
4c84c5957d | ||
|
|
34613ecdc5 | ||
|
|
6a14245401 | ||
|
|
a758ca2aef | ||
|
|
9e60d4e213 | ||
|
|
ef29c8442c | ||
|
|
a1b45415ac | ||
|
|
7fc46d75bd | ||
|
|
953a4ef0ca |
@@ -144,6 +144,7 @@ from .workspace import (
|
||||
models,
|
||||
plugin,
|
||||
rbac,
|
||||
skills,
|
||||
snippets,
|
||||
tool_providers,
|
||||
trigger_providers,
|
||||
@@ -225,6 +226,7 @@ __all__ = [
|
||||
"saved_message",
|
||||
"setup",
|
||||
"site",
|
||||
"skills",
|
||||
"snippet_workflow",
|
||||
"snippet_workflow_draft_variable",
|
||||
"snippets",
|
||||
|
||||
@@ -60,7 +60,7 @@ class TagBindingRemovePayload(BaseModel):
|
||||
|
||||
|
||||
class TagListQueryParam(BaseModel):
|
||||
type: Literal["knowledge", "app", "snippet", ""] = Field("", description="Tag type filter")
|
||||
type: TagType | Literal[""] = Field("", description="Tag type filter")
|
||||
keyword: str | None = Field(None, description="Search keyword")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,899 @@
|
||||
"""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 (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
rbac_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,
|
||||
SkillDraftFileCheckPayload,
|
||||
SkillDraftFileOperationPayload,
|
||||
SkillDraftTreePayload,
|
||||
SkillImportPayload,
|
||||
SkillManagementService,
|
||||
SkillManagementServiceError,
|
||||
SkillMetadataPayload,
|
||||
SkillPublishPayload,
|
||||
SkillRestorePayload,
|
||||
SkillVersionUpdatePayload,
|
||||
)
|
||||
|
||||
_FILE_UPLOAD_PARAMS = {
|
||||
"file": {
|
||||
"description": "Skill draft file payload",
|
||||
"in": "formData",
|
||||
"type": "file",
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class WorkspaceSkillsQuery(BaseModel):
|
||||
keyword: str | None = Field(default=None, description="Search keyword matching skill name or description.")
|
||||
page: int = Field(default=1, ge=1, le=99999, description="Page number.")
|
||||
limit: int = Field(default=20, ge=1, le=100, description="Number of items per page.")
|
||||
tag: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Skill tag filters. Repeat the parameter for multiple tags.",
|
||||
)
|
||||
|
||||
|
||||
class SkillDeletePayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
confirmation_name: str | None = Field(
|
||||
default=None,
|
||||
description="Required when deleting a referenced Skill. Must match the Skill display name.",
|
||||
)
|
||||
|
||||
|
||||
class AgentSkillBindingsPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
skill_ids: list[str] = Field(default_factory=list, description="Ordered Skill IDs bound to the Agent.")
|
||||
|
||||
|
||||
class SkillFileQuery(BaseModel):
|
||||
path: str = Field(description="Skill file path relative to the Skill root.")
|
||||
version_id: str | None = Field(default=None, description="Optional published version ID. Omit for current draft.")
|
||||
|
||||
|
||||
class SkillResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
display_name: str
|
||||
icon: str
|
||||
description: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
name_manually_edited: bool = False
|
||||
visibility: str
|
||||
latest_published_version_id: str | None = None
|
||||
latest_published_version_number: int | None = None
|
||||
latest_published_at: int | None = None
|
||||
reference_count: int = 0
|
||||
created_by: str | None = None
|
||||
created_by_name: str | None = None
|
||||
updated_by: str | None = None
|
||||
updated_by_name: str | None = None
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
|
||||
class SkillFileResponse(ResponseModel):
|
||||
id: str | None = None
|
||||
path: str
|
||||
kind: str
|
||||
storage: str | None = None
|
||||
mime_type: str | None = None
|
||||
content: str | None = None
|
||||
tool_file_id: str | None = None
|
||||
size: int | None = None
|
||||
hash: str | None = None
|
||||
|
||||
|
||||
class SkillFilePreviewResponse(ResponseModel):
|
||||
path: str
|
||||
mime_type: str
|
||||
content: str
|
||||
size: int
|
||||
hash: str
|
||||
|
||||
|
||||
class SkillFileUploadResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
mime_type: str
|
||||
size: int
|
||||
hash: str
|
||||
|
||||
|
||||
class SkillFileCheckErrorResponse(ResponseModel):
|
||||
code: str
|
||||
message: str
|
||||
|
||||
|
||||
class SkillFileCheckItemResponse(ResponseModel):
|
||||
path: str
|
||||
filename: str
|
||||
extension: str
|
||||
mime_type: str
|
||||
size: int
|
||||
errors: list[SkillFileCheckErrorResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillFileCheckResponse(ResponseModel):
|
||||
data: dict[str, SkillFileCheckItemResponse] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class SkillDetailResponse(SkillResponse):
|
||||
files: list[SkillFileResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillListResponse(ResponseModel):
|
||||
data: list[SkillResponse] = Field(default_factory=list)
|
||||
has_more: bool = False
|
||||
limit: int = 20
|
||||
page: int = 1
|
||||
total: int = 0
|
||||
|
||||
|
||||
class SkillTagResponse(ResponseModel):
|
||||
tag: str
|
||||
count: int
|
||||
|
||||
|
||||
class SkillTagListResponse(ResponseModel):
|
||||
data: list[SkillTagResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillVersionResponse(ResponseModel):
|
||||
id: str
|
||||
skill_id: str
|
||||
version_number: int
|
||||
version_name: str
|
||||
publish_note: str
|
||||
hash_code: str
|
||||
archive_size: int
|
||||
published_by: str | None = None
|
||||
published_by_name: str | None = None
|
||||
is_latest: bool = False
|
||||
created_at: int
|
||||
|
||||
|
||||
class SkillVersionListResponse(ResponseModel):
|
||||
data: list[SkillVersionResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillVersionDetailResponse(SkillVersionResponse):
|
||||
files: list[SkillFileResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillVersionDeleteResponse(ResponseModel):
|
||||
id: str
|
||||
deleted: bool
|
||||
latest_published_version_id: str | None = None
|
||||
|
||||
|
||||
class SkillReferenceResponse(ResponseModel):
|
||||
type: str
|
||||
agent_id: str
|
||||
agent_icon: str | None = None
|
||||
agent_icon_background: str | None = None
|
||||
agent_icon_type: str | None = None
|
||||
app_id: str | None = None
|
||||
name: str
|
||||
display_name: str
|
||||
workflow_id: str | None = None
|
||||
workflow_name: str | None = None
|
||||
workflow_icon: str | None = None
|
||||
workflow_icon_background: str | None = None
|
||||
workflow_icon_type: str | None = None
|
||||
workflow_version: str | None = None
|
||||
node_id: str | None = None
|
||||
node_name: str | None = None
|
||||
|
||||
|
||||
class SkillReferenceListResponse(ResponseModel):
|
||||
data: list[SkillReferenceResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillDeleteResponse(ResponseModel):
|
||||
id: str
|
||||
deleted: bool
|
||||
|
||||
|
||||
class AgentSkillBindingItemResponse(ResponseModel):
|
||||
id: str
|
||||
priority: int
|
||||
name: str
|
||||
display_name: str
|
||||
icon: str
|
||||
description: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
status: str
|
||||
file_count: int
|
||||
latest_published_version_id: str | None = None
|
||||
latest_published_at: int | None = None
|
||||
updated_at: int
|
||||
|
||||
|
||||
class AgentSkillBindingsResponse(ResponseModel):
|
||||
agent_id: str
|
||||
skill_ids: list[str] = Field(default_factory=list)
|
||||
data: list[AgentSkillBindingItemResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
WorkspaceSkillsQuery,
|
||||
SkillCreatePayload,
|
||||
SkillAssistMessagePayload,
|
||||
SkillMetadataPayload,
|
||||
SkillDraftFileCheckPayload,
|
||||
SkillDraftFileOperationPayload,
|
||||
SkillDraftTreePayload,
|
||||
SkillPublishPayload,
|
||||
SkillRestorePayload,
|
||||
SkillVersionUpdatePayload,
|
||||
SkillDeletePayload,
|
||||
SkillFileQuery,
|
||||
AgentSkillBindingsPayload,
|
||||
)
|
||||
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
SkillResponse,
|
||||
SkillFileResponse,
|
||||
SkillFilePreviewResponse,
|
||||
SkillFileUploadResponse,
|
||||
SkillFileCheckErrorResponse,
|
||||
SkillFileCheckItemResponse,
|
||||
SkillFileCheckResponse,
|
||||
SkillDetailResponse,
|
||||
SkillListResponse,
|
||||
SkillTagResponse,
|
||||
SkillTagListResponse,
|
||||
SkillVersionResponse,
|
||||
SkillVersionListResponse,
|
||||
SkillVersionDetailResponse,
|
||||
SkillVersionDeleteResponse,
|
||||
SkillReferenceResponse,
|
||||
SkillReferenceListResponse,
|
||||
SkillDeleteResponse,
|
||||
AgentSkillBindingItemResponse,
|
||||
AgentSkillBindingsResponse,
|
||||
BinaryFileResponse,
|
||||
)
|
||||
|
||||
|
||||
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, object], int]:
|
||||
body: dict[str, object] = {"code": exc.code, "message": exc.message}
|
||||
if exc.details:
|
||||
body["details"] = exc.details
|
||||
return body, exc.status_code
|
||||
|
||||
|
||||
@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
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_VIEW, resource_required=False)
|
||||
@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
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_EDIT, resource_required=False)
|
||||
@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
|
||||
|
||||
try:
|
||||
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
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/tags")
|
||||
class WorkspaceSkillTagsApi(Resource):
|
||||
@console_ns.response(200, "Workspace Skill tags", console_ns.models[SkillTagListResponse.__name__])
|
||||
@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
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_VIEW, resource_required=False)
|
||||
@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
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_EDIT, resource_required=False)
|
||||
@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
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_DELETE, resource_required=False)
|
||||
@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,
|
||||
history=payload.history,
|
||||
model_payload=payload.model,
|
||||
target_path=payload.target_path,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
return helper.compact_generate_response(response)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/check")
|
||||
class WorkspaceSkillFilesCheckApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SkillDraftFileCheckPayload.__name__])
|
||||
@console_ns.response(200, "Draft files checked", console_ns.models[SkillFileCheckResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
payload = SkillDraftFileCheckPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().check_draft_files(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillFileCheckResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files")
|
||||
class WorkspaceSkillFilesApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SkillDraftFileOperationPayload.__name__])
|
||||
@console_ns.response(200, "Draft file operation applied", console_ns.models[SkillDetailResponse.__name__])
|
||||
@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
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_PUBLISH, resource_required=False)
|
||||
@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 to draft", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_PUBLISH, resource_required=False)
|
||||
@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(SkillDetailResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/references")
|
||||
class WorkspaceSkillReferencesApi(Resource):
|
||||
@console_ns.response(200, "Skill references", console_ns.models[SkillReferenceListResponse.__name__])
|
||||
@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",
|
||||
]
|
||||
@@ -24,6 +24,7 @@ from .knowledge import retrieval as _knowledge_retrieval
|
||||
from .plugin import agent_config as _agent_config
|
||||
from .plugin import agent_drive as _agent_drive
|
||||
from .plugin import plugin as _plugin
|
||||
from .plugin import skills as _skills
|
||||
from .workspace import workspace as _workspace
|
||||
|
||||
api.add_namespace(inner_api_ns)
|
||||
@@ -38,6 +39,7 @@ __all__ = [
|
||||
"_mail",
|
||||
"_plugin",
|
||||
"_runtime_credentials",
|
||||
"_skills",
|
||||
"_workspace",
|
||||
"api",
|
||||
"bp",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Inner API for published workspace Skills.
|
||||
|
||||
These endpoints are called by trusted runtime services. They expose only
|
||||
published Skill artifacts, never draft files or editable metadata.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
from flask import request, send_file
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from controllers.console.wraps import setup_required
|
||||
from controllers.inner_api import inner_api_ns
|
||||
from controllers.inner_api.wraps import plugin_inner_api_only
|
||||
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
|
||||
|
||||
|
||||
class _SkillTargetQuery(BaseModel):
|
||||
tenant_id: str
|
||||
|
||||
|
||||
def _target_query_from_request() -> _SkillTargetQuery:
|
||||
return _SkillTargetQuery.model_validate({"tenant_id": request.args.get("tenant_id")})
|
||||
|
||||
|
||||
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, str], int]:
|
||||
return {"code": exc.code, "message": exc.message}, exc.status_code
|
||||
|
||||
|
||||
@inner_api_ns.route("/skills/<string:skill_id>/pull")
|
||||
class PublishedSkillPullApi(Resource):
|
||||
@setup_required
|
||||
@plugin_inner_api_only
|
||||
@inner_api_ns.doc("published_skill_pull")
|
||||
def get(self, skill_id: str):
|
||||
try:
|
||||
query = _target_query_from_request()
|
||||
result = SkillManagementService().pull_published_archive(tenant_id=query.tenant_id, skill_id=skill_id)
|
||||
return send_file(
|
||||
io.BytesIO(result.payload),
|
||||
mimetype=result.mime_type,
|
||||
as_attachment=True,
|
||||
download_name=result.filename,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
__all__ = ["PublishedSkillPullApi"]
|
||||
@@ -43,6 +43,7 @@ from core.workflow.nodes.agent_v2.runtime_request_builder import (
|
||||
build_config_layer_config,
|
||||
build_knowledge_layer_config,
|
||||
build_shell_layer_config,
|
||||
load_runtime_agent_skill_configs,
|
||||
)
|
||||
from models.agent_config_entities import AgentSoulConfig, AgentSoulToolsConfig
|
||||
from models.provider_ids import ModelProviderID
|
||||
@@ -127,14 +128,22 @@ class AgentAppRuntimeRequestBuilder:
|
||||
"cli_tool_count": len(agent_soul.tools.cli_tools),
|
||||
}
|
||||
|
||||
runtime_config_skills = load_runtime_agent_skill_configs(
|
||||
tenant_id=context.dify_context.tenant_id,
|
||||
agent_id=context.agent_id,
|
||||
)
|
||||
config_layer_config, config_warnings = build_config_layer_config(
|
||||
agent_soul,
|
||||
agent_id=context.agent_id,
|
||||
config_version_id=context.agent_config_snapshot_id,
|
||||
config_version_kind=context.agent_config_version_kind,
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
append_runtime_warnings(metadata, config_warnings)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(
|
||||
agent_soul,
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
knowledge_config = build_knowledge_layer_config(agent_soul)
|
||||
|
||||
request = self._request_builder.build_for_agent_app(
|
||||
|
||||
@@ -1545,7 +1545,7 @@ class ProviderManager:
|
||||
except ValidationError:
|
||||
return SystemConfiguration(enabled=False)
|
||||
|
||||
if plugin_provider_entity.installation_source != PluginInstallationSource.Marketplace:
|
||||
if plugin_provider_entity.installation_source in (None, PluginInstallationSource.Package):
|
||||
return SystemConfiguration(enabled=False)
|
||||
|
||||
from core.plugin.plugin_service import PluginService
|
||||
|
||||
@@ -62,6 +62,10 @@ class RBACPermission(StrEnum):
|
||||
API_EXTENSION_MANAGE = "api_extension_manage"
|
||||
CUSTOMIZATION_MANAGE = "customization_manage"
|
||||
AGENT_MANAGE = "agent_manage"
|
||||
SKILL_VIEW = "skill_view"
|
||||
SKILL_EDIT = "skill_edit"
|
||||
SKILL_PUBLISH = "skill_publish"
|
||||
SKILL_DELETE = "skill_delete"
|
||||
|
||||
SNIPPETS_CREATE_AND_MODIFY = "snippets_create_and_modify"
|
||||
SNIPPETS_MANAGE = "snippets_management"
|
||||
|
||||
@@ -37,6 +37,7 @@ from dify_agent.layers.shell import (
|
||||
)
|
||||
from dify_agent.protocol import CreateRunRequest, DeferredToolResultsPayload
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from clients.agent_backend import (
|
||||
AgentBackendModelConfig,
|
||||
@@ -207,14 +208,22 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
"cli_tool_count": len(agent_soul.tools.cli_tools),
|
||||
}
|
||||
|
||||
runtime_config_skills = load_runtime_agent_skill_configs(
|
||||
tenant_id=context.dify_context.tenant_id,
|
||||
agent_id=context.agent.id,
|
||||
)
|
||||
config_layer_config, config_warnings = build_config_layer_config(
|
||||
agent_soul,
|
||||
agent_id=context.agent.id,
|
||||
config_version_id=context.snapshot.id,
|
||||
config_version_kind="snapshot",
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
append_runtime_warnings(metadata, config_warnings)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(
|
||||
agent_soul,
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
soul_prompt = expand_prompt_mentions(agent_soul.prompt.system_prompt, soul_prompt_resolver).strip()
|
||||
knowledge_config = build_knowledge_layer_config(agent_soul)
|
||||
|
||||
@@ -878,11 +887,16 @@ def append_runtime_warnings(metadata: dict[str, Any], warnings: list[dict[str, s
|
||||
existing.extend(warnings)
|
||||
|
||||
|
||||
def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
|
||||
def build_config_aware_soul_mention_resolver(
|
||||
agent_soul: AgentSoulConfig,
|
||||
*,
|
||||
runtime_config_skills: Sequence[DifyConfigSkillConfig] = (),
|
||||
):
|
||||
"""Resolve config skill/file mentions and delegate the rest to Agent Soul."""
|
||||
|
||||
base_resolver = build_soul_mention_resolver(agent_soul)
|
||||
skill_names = {item.name for item in agent_soul.config_skills if not item.is_missing}
|
||||
skill_names.update(item.name for item in runtime_config_skills)
|
||||
file_names = {item.name for item in agent_soul.config_files if not item.is_missing}
|
||||
|
||||
def _resolve(mention: object) -> str | None:
|
||||
@@ -900,12 +914,34 @@ def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
|
||||
return _resolve
|
||||
|
||||
|
||||
def load_runtime_agent_skill_configs(*, tenant_id: str, agent_id: str) -> list[DifyConfigSkillConfig]:
|
||||
"""Return workspace-bound Skills as prompt-safe runtime config skills."""
|
||||
from services.skill_management_service import SkillManagementService
|
||||
|
||||
try:
|
||||
runtime_skills = SkillManagementService().list_runtime_agent_skills(tenant_id=tenant_id, agent_id=agent_id)
|
||||
except OperationalError as exc:
|
||||
if "no such table: agent_skill_bindings" not in str(exc.orig):
|
||||
raise
|
||||
runtime_skills = []
|
||||
return [
|
||||
DifyConfigSkillConfig(
|
||||
name=str(item["name"]),
|
||||
description=str(item.get("description") or ""),
|
||||
size=cast(int | None, item.get("size")),
|
||||
mime_type=cast(str | None, item.get("mime_type")),
|
||||
)
|
||||
for item in runtime_skills
|
||||
]
|
||||
|
||||
|
||||
def build_config_layer_config(
|
||||
agent_soul: AgentSoulConfig,
|
||||
*,
|
||||
agent_id: str | None = None,
|
||||
config_version_id: str | None = None,
|
||||
config_version_kind: Literal["snapshot", "draft", "build_draft"] = "snapshot",
|
||||
runtime_config_skills: Sequence[DifyConfigSkillConfig] = (),
|
||||
) -> tuple[DifyConfigLayerConfig, list[dict[str, str]]]:
|
||||
"""Build the always-present Agent config layer from Agent Soul state.
|
||||
|
||||
@@ -922,8 +958,23 @@ def build_config_layer_config(
|
||||
)
|
||||
)
|
||||
available_skills = [skill for skill in agent_soul.config_skills if not skill.is_missing]
|
||||
skill_configs = [
|
||||
DifyConfigSkillConfig(
|
||||
name=skill.name,
|
||||
description=skill.description,
|
||||
size=skill.size,
|
||||
mime_type=skill.mime_type,
|
||||
)
|
||||
for skill in available_skills
|
||||
]
|
||||
seen_skill_names = {skill.name for skill in skill_configs}
|
||||
for skill in runtime_config_skills:
|
||||
if skill.name in seen_skill_names:
|
||||
continue
|
||||
seen_skill_names.add(skill.name)
|
||||
skill_configs.append(skill)
|
||||
available_files = [file_ref for file_ref in agent_soul.config_files if not file_ref.is_missing]
|
||||
skill_names = {skill.name for skill in available_skills}
|
||||
skill_names = {skill.name for skill in skill_configs}
|
||||
file_names = {file_ref.name for file_ref in available_files}
|
||||
warnings: list[dict[str, str]] = [
|
||||
{
|
||||
@@ -960,15 +1011,7 @@ def build_config_layer_config(
|
||||
kind=config_version_kind,
|
||||
writable=config_version_kind == "build_draft",
|
||||
),
|
||||
skills=[
|
||||
DifyConfigSkillConfig(
|
||||
name=skill.name,
|
||||
description=skill.description,
|
||||
size=skill.size,
|
||||
mime_type=skill.mime_type,
|
||||
)
|
||||
for skill in available_skills
|
||||
],
|
||||
skills=skill_configs,
|
||||
files=[
|
||||
DifyConfigFileConfig(
|
||||
name=file_ref.name,
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""add workspace skill management
|
||||
|
||||
Revision ID: a4f8d2c9e1b0
|
||||
Revises: e4708db55c1d
|
||||
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 = "e4708db55c1d"
|
||||
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,31 +18,109 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _is_pg(conn) -> bool:
|
||||
return conn.dialect.name == "postgresql"
|
||||
|
||||
|
||||
def _uuid_column(name: str, *, nullable: bool = False, primary_key: bool = False) -> sa.Column:
|
||||
kwargs = {"nullable": nullable, "primary_key": primary_key}
|
||||
if primary_key and _is_pg(op.get_bind()):
|
||||
kwargs["server_default"] = sa.text("uuidv7()")
|
||||
return sa.Column(name, models.types.StringUUID(), **kwargs)
|
||||
|
||||
|
||||
def _has_table(table_name: str) -> bool:
|
||||
return sa.inspect(op.get_bind()).has_table(table_name)
|
||||
|
||||
|
||||
def _has_column(table_name: str, column_name: str) -> bool:
|
||||
return any(
|
||||
column["name"] == column_name for column in sa.inspect(op.get_bind()).get_columns(table_name)
|
||||
)
|
||||
|
||||
|
||||
def _has_unique_constraint(table_name: str, constraint_name: str) -> bool:
|
||||
return any(
|
||||
constraint["name"] == constraint_name
|
||||
for constraint in sa.inspect(op.get_bind()).get_unique_constraints(table_name)
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
if not _has_table("agent_debug_conversations"):
|
||||
op.create_table(
|
||||
"agent_debug_conversations",
|
||||
_uuid_column("id", primary_key=True),
|
||||
sa.Column("tenant_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("agent_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("app_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("account_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("conversation_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column(
|
||||
"draft_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default=sa.text("'debug_build'"),
|
||||
),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("agent_debug_conversation_pkey")),
|
||||
sa.UniqueConstraint(
|
||||
"tenant_id",
|
||||
"agent_id",
|
||||
"account_id",
|
||||
"draft_type",
|
||||
name=op.f("agent_debug_conversation_agent_account_draft_type_unique"),
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"agent_debug_conversation_conversation_idx",
|
||||
"agent_debug_conversations",
|
||||
["conversation_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"agent_debug_conversation_account_idx",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "account_id"],
|
||||
)
|
||||
return
|
||||
|
||||
# Existing pointers have always represented Build chat because the Agent
|
||||
# detail API exposes them as ``debug_conversation_id`` for that surface.
|
||||
op.add_column(
|
||||
if not _has_column("agent_debug_conversations", "draft_type"):
|
||||
op.add_column(
|
||||
"agent_debug_conversations",
|
||||
sa.Column(
|
||||
"draft_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default=sa.text("'debug_build'"),
|
||||
),
|
||||
)
|
||||
if _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
sa.Column(
|
||||
"draft_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default=sa.text("'debug_build'"),
|
||||
),
|
||||
)
|
||||
op.drop_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
):
|
||||
op.drop_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
if not _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id", "draft_type"],
|
||||
)
|
||||
):
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id", "draft_type"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
if not _has_table("agent_debug_conversations"):
|
||||
return
|
||||
|
||||
debug_conversations = sa.table(
|
||||
"agent_debug_conversations",
|
||||
sa.column("tenant_id", models.types.StringUUID()),
|
||||
@@ -64,14 +142,23 @@ def downgrade():
|
||||
),
|
||||
)
|
||||
)
|
||||
op.drop_constraint(
|
||||
if _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
):
|
||||
op.drop_constraint(
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
if not _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id"],
|
||||
)
|
||||
op.drop_column("agent_debug_conversations", "draft_type")
|
||||
):
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id"],
|
||||
)
|
||||
if _has_column("agent_debug_conversations", "draft_type"):
|
||||
op.drop_column("agent_debug_conversations", "draft_type")
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""merge skill and agent debug conversation heads
|
||||
|
||||
Revision ID: e9f4a1b2c3d5
|
||||
Revises: a4f8d2c9e1b0, d2825e7b9c10
|
||||
Create Date: 2026-07-23 15:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "e9f4a1b2c3d5"
|
||||
down_revision = ("a4f8d2c9e1b0", "d2825e7b9c10")
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@@ -114,6 +114,7 @@ from .provider import (
|
||||
TenantDefaultModel,
|
||||
TenantPreferredModelProvider,
|
||||
)
|
||||
from .skill import AgentSkillBinding, Skill, SkillDraftFile, SkillFileKind, SkillFileStorage, SkillVersion
|
||||
from .snippet import CustomizedSnippet, SnippetType
|
||||
from .source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding
|
||||
from .task import CeleryTask, CeleryTaskSet
|
||||
@@ -173,6 +174,7 @@ __all__ = [
|
||||
"AgentIconType",
|
||||
"AgentKind",
|
||||
"AgentScope",
|
||||
"AgentSkillBinding",
|
||||
"AgentSource",
|
||||
"AgentStatus",
|
||||
"AgentWorkingResourceStatus",
|
||||
@@ -250,6 +252,11 @@ __all__ = [
|
||||
"RecommendedApp",
|
||||
"SavedMessage",
|
||||
"Site",
|
||||
"Skill",
|
||||
"SkillDraftFile",
|
||||
"SkillFileKind",
|
||||
"SkillFileStorage",
|
||||
"SkillVersion",
|
||||
"SnippetType",
|
||||
"Tag",
|
||||
"TagBinding",
|
||||
|
||||
@@ -249,6 +249,7 @@ class TagType(StrEnum):
|
||||
KNOWLEDGE = "knowledge"
|
||||
APP = "app"
|
||||
SNIPPET = "snippet"
|
||||
SKILL = "skill"
|
||||
|
||||
|
||||
class DatasetMetadataType(StrEnum):
|
||||
|
||||
@@ -2675,7 +2675,7 @@ class Tag(TypeBase):
|
||||
sa.Index("tag_name_idx", "name"),
|
||||
)
|
||||
|
||||
TAG_TYPE_LIST = ["knowledge", "app", "snippet"]
|
||||
TAG_TYPE_LIST = ["knowledge", "app", "snippet", "skill"]
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Workspace-level Skill Management models.
|
||||
|
||||
These tables are the source of truth for reusable workspace Skills. Agent Soul
|
||||
``config_skills`` and Agent Drive skill rows remain per-agent runtime/config
|
||||
assets; they may consume a published Skill snapshot but do not own the Skill's
|
||||
draft, metadata, version history, or Agent binding priority.
|
||||
"""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
import sqlalchemy as sa
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import Index, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from models.base import Base, DefaultFieldsMixin
|
||||
from models.types import EnumText, JSONModelColumn, LongText, StringUUID
|
||||
|
||||
|
||||
class SkillFileKind(StrEnum):
|
||||
"""Draft file entry kind."""
|
||||
|
||||
FILE = "file"
|
||||
DIRECTORY = "directory"
|
||||
|
||||
|
||||
class SkillFileStorage(StrEnum):
|
||||
"""How a draft file's content is stored."""
|
||||
|
||||
TEXT = "text"
|
||||
TOOL_FILE = "tool_file"
|
||||
|
||||
|
||||
class SkillVersionManifestFile(BaseModel):
|
||||
"""One file entry captured in a published Skill snapshot manifest."""
|
||||
|
||||
path: str
|
||||
mime_type: str | None = None
|
||||
size: int
|
||||
hash: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class SkillVersionManifest(BaseModel):
|
||||
"""Published Skill snapshot file index."""
|
||||
|
||||
files: list[SkillVersionManifestFile]
|
||||
name: str | None = None
|
||||
display_name: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Skill(DefaultFieldsMixin, Base):
|
||||
"""Workspace-level reusable Skill metadata and draft status."""
|
||||
|
||||
__tablename__ = "skills"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="skill_pkey"),
|
||||
UniqueConstraint("tenant_id", "name", name="skill_tenant_name_unique"),
|
||||
Index("skills_tenant_updated_at_idx", "tenant_id", "updated_at"),
|
||||
)
|
||||
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
name: Mapped[str] = mapped_column(sa.String(64), nullable=False)
|
||||
display_name: Mapped[str] = mapped_column(sa.String(128), nullable=False)
|
||||
icon: Mapped[str] = mapped_column(sa.String(16), nullable=False, default="📄", server_default="📄")
|
||||
description: Mapped[str] = mapped_column(sa.String(1024), nullable=False, default="", server_default="")
|
||||
name_manually_edited: Mapped[bool] = mapped_column(
|
||||
sa.Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=sa.false(),
|
||||
)
|
||||
visibility: Mapped[str] = mapped_column(
|
||||
sa.String(32),
|
||||
nullable=False,
|
||||
default="workspace",
|
||||
server_default="workspace",
|
||||
)
|
||||
latest_published_version_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
|
||||
|
||||
class SkillDraftFile(DefaultFieldsMixin, Base):
|
||||
"""One draft file or directory in a workspace Skill."""
|
||||
|
||||
__tablename__ = "skill_draft_files"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="skill_draft_file_pkey"),
|
||||
UniqueConstraint("skill_id", "path", name="skill_draft_file_skill_path_unique"),
|
||||
Index("skill_draft_files_skill_path_idx", "skill_id", "path"),
|
||||
)
|
||||
|
||||
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
path: Mapped[str] = mapped_column(sa.String(512), nullable=False)
|
||||
kind: Mapped[SkillFileKind] = mapped_column(EnumText(SkillFileKind, length=32), nullable=False)
|
||||
storage: Mapped[SkillFileStorage | None] = mapped_column(EnumText(SkillFileStorage, length=32), nullable=True)
|
||||
mime_type: Mapped[str | None] = mapped_column(sa.String(255), nullable=True)
|
||||
content_text: Mapped[str | None] = mapped_column(LongText, nullable=True)
|
||||
tool_file_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
size: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True)
|
||||
hash: Mapped[str | None] = mapped_column(sa.String(255), nullable=True)
|
||||
|
||||
|
||||
class SkillVersion(DefaultFieldsMixin, Base):
|
||||
"""Immutable published Skill snapshot.
|
||||
|
||||
``hash_code`` uniquely identifies a published version for downstream
|
||||
execution audit. It includes Skill identity, version number, and archive
|
||||
content digest instead of being only the archive content hash.
|
||||
"""
|
||||
|
||||
__tablename__ = "skill_versions"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="skill_version_pkey"),
|
||||
UniqueConstraint("skill_id", "version_number", name="skill_version_skill_number_unique"),
|
||||
Index("skill_versions_skill_created_at_idx", "skill_id", "created_at"),
|
||||
)
|
||||
|
||||
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
version_number: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
||||
version_name: Mapped[str] = mapped_column(sa.String(128), nullable=False, default="", server_default="")
|
||||
publish_note: Mapped[str] = mapped_column(sa.String(1024), nullable=False, default="", server_default="")
|
||||
manifest: Mapped[SkillVersionManifest] = mapped_column(JSONModelColumn(SkillVersionManifest), nullable=False)
|
||||
archive_tool_file_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
hash_code: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
||||
archive_size: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
|
||||
published_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
|
||||
|
||||
class AgentSkillBinding(DefaultFieldsMixin, Base):
|
||||
"""Direct Agent-to-workspace-Skill binding.
|
||||
|
||||
``priority`` is retained as an internal ordering column for the current
|
||||
schema constraints. Runtime Skill selection is Agent-driven and must not
|
||||
treat it as a matching priority.
|
||||
"""
|
||||
|
||||
__tablename__ = "agent_skill_bindings"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="agent_skill_binding_pkey"),
|
||||
UniqueConstraint("tenant_id", "agent_id", "skill_id", name="agent_skill_binding_unique"),
|
||||
UniqueConstraint("tenant_id", "agent_id", "priority", name="agent_skill_binding_priority_unique"),
|
||||
Index("agent_skill_bindings_skill_idx", "tenant_id", "skill_id"),
|
||||
)
|
||||
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
priority: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
||||
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentSkillBinding",
|
||||
"Skill",
|
||||
"SkillDraftFile",
|
||||
"SkillFileKind",
|
||||
"SkillFileStorage",
|
||||
"SkillVersion",
|
||||
"SkillVersionManifest",
|
||||
"SkillVersionManifestFile",
|
||||
]
|
||||
@@ -9557,7 +9557,7 @@ Remove one or more tag bindings from a target.
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| keyword | query | Search keyword | No | string |
|
||||
| type | query | Tag type filter | No | string, <br>**Available values:** "", "app", "knowledge", "snippet" |
|
||||
| type | query | Tag type filter | No | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
@@ -10086,6 +10086,38 @@ Get list of available agent providers
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [AgentProviderListResponse](#agentproviderlistresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/agents/{agent_id}/skills
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| agent_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Agent Skill bindings | **application/json**: [AgentSkillBindingsResponse](#agentskillbindingsresponse)<br> |
|
||||
|
||||
### [PUT] /workspaces/current/agents/{agent_id}/skills
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| agent_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [AgentSkillBindingsPayload](#agentskillbindingspayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Agent Skill bindings replaced | **application/json**: [AgentSkillBindingsResponse](#agentskillbindingsresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/customized-snippets
|
||||
**List customized snippets with pagination and search**
|
||||
|
||||
@@ -12017,6 +12049,360 @@ Returns permission flags that control workspace features like member invitations
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceAccessMatrix](#workspaceaccessmatrix)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| keyword | query | Search keyword matching skill name or description. | No | string |
|
||||
| limit | query | Number of items per page. | No | integer, <br>**Default:** 20 |
|
||||
| page | query | Page number. | No | integer, <br>**Default:** 1 |
|
||||
| tag | query | Skill tag filters. Repeat the parameter for multiple tags. | No | [ string ] |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Workspace skills | **application/json**: [SkillListResponse](#skilllistresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillCreatePayload](#skillcreatepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill created | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/files/upload
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **multipart/form-data**: { **"file"**: binary }<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill draft file uploaded | **application/json**: [SkillFileUploadResponse](#skillfileuploadresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/import
|
||||
Import a Skill zip package from multipart form field `file`.
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill imported | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/tags
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Workspace Skill tags | **application/json**: [SkillTagListResponse](#skilltaglistresponse)<br> |
|
||||
|
||||
### [DELETE] /workspaces/current/skills/{skill_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillDeletePayload](#skilldeletepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill deleted | **application/json**: [SkillDeleteResponse](#skilldeleteresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill detail | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [PATCH] /workspaces/current/skills/{skill_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillMetadataPayload](#skillmetadatapayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill updated | **application/json**: [SkillResponse](#skillresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/{skill_id}/assist/messages
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillAssistMessagePayload](#skillassistmessagepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Skill Authoring assistant event stream |
|
||||
|
||||
### [POST] /workspaces/current/skills/{skill_id}/duplicate
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill duplicated | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/export
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Published Skill zip archive |
|
||||
|
||||
### [PATCH] /workspaces/current/skills/{skill_id}/files
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillDraftFileOperationPayload](#skilldraftfileoperationpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Draft file operation applied | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [PUT] /workspaces/current/skills/{skill_id}/files
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillDraftTreePayload](#skilldrafttreepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Draft files replaced | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/{skill_id}/files/check
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillDraftFileCheckPayload](#skilldraftfilecheckpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Draft files checked | **application/json**: [SkillFileCheckResponse](#skillfilecheckresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/files/content
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| download | query | Return as an attachment when 1. | No | string |
|
||||
| path | query | Skill file path relative to the Skill root. | Yes | string |
|
||||
| version_id | query | Optional published version ID. Omit for current draft. | No | string |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill file content | **application/json**: [BinaryFileResponse](#binaryfileresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/files/preview
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| path | query | Skill file path relative to the Skill root. | Yes | string |
|
||||
| version_id | query | Optional published version ID. Omit for current draft. | No | string |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill file text preview | **application/json**: [SkillFilePreviewResponse](#skillfilepreviewresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/{skill_id}/publish
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillPublishPayload](#skillpublishpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill published | **application/json**: [SkillVersionResponse](#skillversionresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/references
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill references | **application/json**: [SkillReferenceListResponse](#skillreferencelistresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/{skill_id}/restore
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillRestorePayload](#skillrestorepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill version restored to draft | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/versions
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill versions | **application/json**: [SkillVersionListResponse](#skillversionlistresponse)<br> |
|
||||
|
||||
### [DELETE] /workspaces/current/skills/{skill_id}/versions/{version_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
| version_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill version deleted | **application/json**: [SkillVersionDeleteResponse](#skillversiondeleteresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/versions/{version_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
| version_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill version detail | **application/json**: [SkillVersionDetailResponse](#skillversiondetailresponse)<br> |
|
||||
|
||||
### [PATCH] /workspaces/current/skills/{skill_id}/versions/{version_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
| version_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillVersionUpdatePayload](#skillversionupdatepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill version updated | **application/json**: [SkillVersionResponse](#skillversionresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/summary
|
||||
#### Responses
|
||||
|
||||
@@ -14760,6 +15146,37 @@ Visibility and lifecycle scope of an Agent record.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | | Yes |
|
||||
|
||||
#### AgentSkillBindingItemResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| description | string | | Yes |
|
||||
| display_name | string | | Yes |
|
||||
| file_count | integer | | Yes |
|
||||
| icon | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| latest_published_at | integer | | No |
|
||||
| latest_published_version_id | string | | No |
|
||||
| name | string | | Yes |
|
||||
| priority | integer | | Yes |
|
||||
| status | string | | Yes |
|
||||
| tags | [ string ] | | No |
|
||||
| updated_at | integer | | Yes |
|
||||
|
||||
#### AgentSkillBindingsPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| skill_ids | [ string ] | Ordered Skill IDs bound to the Agent. | No |
|
||||
|
||||
#### AgentSkillBindingsResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| agent_id | string | | Yes |
|
||||
| data | [ [AgentSkillBindingItemResponse](#agentskillbindingitemresponse) ] | | No |
|
||||
| skill_ids | [ string ] | | No |
|
||||
|
||||
#### AgentSkillRefConfig
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21852,6 +22269,238 @@ Simple provider entity response.
|
||||
| title | string | | Yes |
|
||||
| use_icon_as_answer_icon | boolean | | Yes |
|
||||
|
||||
#### SkillAssistAttachmentPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| mime_type | string | | No |
|
||||
| name | string | | Yes |
|
||||
| size | integer | | No |
|
||||
| tool_file_id | string | | Yes |
|
||||
|
||||
#### SkillAssistHistoryMessagePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| content | string | | Yes |
|
||||
| role | string, <br>**Available values:** "assistant", "user" | *Enum:* `"assistant"`, `"user"` | Yes |
|
||||
| suggested_display_name | string | | No |
|
||||
| suggested_name | string | | No |
|
||||
|
||||
#### SkillAssistMessagePayload
|
||||
|
||||
One user message and optional uploaded context for the Skill Authoring assistant.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| attachments | [ [SkillAssistAttachmentPayload](#skillassistattachmentpayload) ] | | No |
|
||||
| history | [ [SkillAssistHistoryMessagePayload](#skillassisthistorymessagepayload) ] | | No |
|
||||
| message | string | | Yes |
|
||||
| model | [SkillAssistModelPayload](#skillassistmodelpayload) | | No |
|
||||
| target_path | string | | No |
|
||||
|
||||
#### SkillAssistModelPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| model | string | | Yes |
|
||||
| model_settings | object | | No |
|
||||
| plugin_id | string | | No |
|
||||
| provider | string | | Yes |
|
||||
|
||||
#### SkillCreatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| description | string | | No |
|
||||
| display_name | string | | No |
|
||||
| icon | string, <br>**Default:** 📄 | | No |
|
||||
| name | string | | No |
|
||||
| tags | [ string ] | | No |
|
||||
|
||||
#### SkillDeletePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| confirmation_name | string | Required when deleting a referenced Skill. Must match the Skill display name. | No |
|
||||
|
||||
#### SkillDeleteResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| deleted | boolean | | Yes |
|
||||
| id | string | | Yes |
|
||||
|
||||
#### SkillDetailResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| created_at | integer | | Yes |
|
||||
| created_by | string | | No |
|
||||
| created_by_name | string | | No |
|
||||
| description | string | | Yes |
|
||||
| display_name | string | | Yes |
|
||||
| files | [ [SkillFileResponse](#skillfileresponse) ] | | No |
|
||||
| icon | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| latest_published_at | integer | | No |
|
||||
| latest_published_version_id | string | | No |
|
||||
| latest_published_version_number | integer | | No |
|
||||
| name | string | | Yes |
|
||||
| name_manually_edited | boolean | | No |
|
||||
| reference_count | integer | | No |
|
||||
| tags | [ string ] | | No |
|
||||
| updated_at | integer | | Yes |
|
||||
| updated_by | string | | No |
|
||||
| updated_by_name | string | | No |
|
||||
| visibility | string | | Yes |
|
||||
|
||||
#### SkillDraftFileCheckItemPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| filename | string | | Yes |
|
||||
| mime_type | string | | No |
|
||||
| path | string | Target draft path. Defaults to filename. | No |
|
||||
| size | integer | | Yes |
|
||||
|
||||
#### SkillDraftFileCheckPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| files | [ [SkillDraftFileCheckItemPayload](#skilldraftfilecheckitempayload) ] | | No |
|
||||
|
||||
#### SkillDraftFileOperation
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| SkillDraftFileOperation | string | | |
|
||||
|
||||
#### SkillDraftFileOperationPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| content | string | | No |
|
||||
| expected_updated_at | integer | | No |
|
||||
| hash | string | | No |
|
||||
| mime_type | string | | No |
|
||||
| operation | [SkillDraftFileOperation](#skilldraftfileoperation) | | Yes |
|
||||
| path | string | | Yes |
|
||||
| size | integer | | No |
|
||||
| target_path | string | | No |
|
||||
| tool_file_id | string | | No |
|
||||
|
||||
#### SkillDraftTreeItemPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| content | string | | No |
|
||||
| hash | string | | No |
|
||||
| kind | [SkillFileKind](#skillfilekind) | | No |
|
||||
| mime_type | string | | No |
|
||||
| path | string | | Yes |
|
||||
| size | integer | | No |
|
||||
| storage | [SkillFileStorage](#skillfilestorage) | | No |
|
||||
| tool_file_id | string | | No |
|
||||
|
||||
#### SkillDraftTreePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| expected_updated_at | integer | | No |
|
||||
| files | [ [SkillDraftTreeItemPayload](#skilldrafttreeitempayload) ] | | No |
|
||||
|
||||
#### SkillFileCheckErrorResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| code | string | | Yes |
|
||||
| message | string | | Yes |
|
||||
|
||||
#### SkillFileCheckItemResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| errors | [ [SkillFileCheckErrorResponse](#skillfilecheckerrorresponse) ] | | No |
|
||||
| extension | string | | Yes |
|
||||
| filename | string | | Yes |
|
||||
| mime_type | string | | Yes |
|
||||
| path | string | | Yes |
|
||||
| size | integer | | Yes |
|
||||
|
||||
#### SkillFileCheckResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| data | object | | No |
|
||||
|
||||
#### SkillFileKind
|
||||
|
||||
Draft file entry kind.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| SkillFileKind | string | Draft file entry kind. | |
|
||||
|
||||
#### SkillFilePreviewResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| content | string | | Yes |
|
||||
| hash | string | | Yes |
|
||||
| mime_type | string | | Yes |
|
||||
| path | string | | Yes |
|
||||
| size | integer | | Yes |
|
||||
|
||||
#### SkillFileQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| path | string | Skill file path relative to the Skill root. | Yes |
|
||||
| version_id | string | Optional published version ID. Omit for current draft. | No |
|
||||
|
||||
#### SkillFileResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| content | string | | No |
|
||||
| hash | string | | No |
|
||||
| id | string | | No |
|
||||
| kind | string | | Yes |
|
||||
| mime_type | string | | No |
|
||||
| path | string | | Yes |
|
||||
| size | integer | | No |
|
||||
| storage | string | | No |
|
||||
| tool_file_id | string | | No |
|
||||
|
||||
#### SkillFileStorage
|
||||
|
||||
How a draft file's content is stored.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| SkillFileStorage | string | How a draft file's content is stored. | |
|
||||
|
||||
#### SkillFileUploadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| hash | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| mime_type | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| size | integer | | Yes |
|
||||
|
||||
#### SkillListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| data | [ [SkillResponse](#skillresponse) ] | | No |
|
||||
| has_more | boolean | | No |
|
||||
| limit | integer, <br>**Default:** 20 | | No |
|
||||
| page | integer, <br>**Default:** 1 | | No |
|
||||
| total | integer | | No |
|
||||
|
||||
#### SkillManifest
|
||||
|
||||
Validated metadata extracted from a Skill package.
|
||||
@@ -21865,6 +22514,93 @@ 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_at | integer | | No |
|
||||
| latest_published_version_id | string | | No |
|
||||
| latest_published_version_number | integer | | No |
|
||||
| name | string | | Yes |
|
||||
| name_manually_edited | boolean | | No |
|
||||
| reference_count | integer | | No |
|
||||
| tags | [ string ] | | No |
|
||||
| updated_at | integer | | Yes |
|
||||
| updated_by | string | | No |
|
||||
| updated_by_name | string | | No |
|
||||
| visibility | string | | Yes |
|
||||
|
||||
#### SkillRestorePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| publish_note | string | | No |
|
||||
| version_id | string | | Yes |
|
||||
| version_name | string | | No |
|
||||
|
||||
#### SkillTagListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| data | [ [SkillTagResponse](#skilltagresponse) ] | | No |
|
||||
|
||||
#### SkillTagResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| count | integer | | Yes |
|
||||
| tag | string | | Yes |
|
||||
|
||||
#### SkillToolInferenceResult
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21873,6 +22609,60 @@ Validated metadata extracted from a Skill package.
|
||||
| inferable | boolean | | Yes |
|
||||
| reason | string | | No |
|
||||
|
||||
#### SkillVersionDeleteResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| deleted | boolean | | Yes |
|
||||
| id | string | | Yes |
|
||||
| latest_published_version_id | string | | No |
|
||||
|
||||
#### SkillVersionDetailResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| archive_size | integer | | Yes |
|
||||
| created_at | integer | | Yes |
|
||||
| files | [ [SkillFileResponse](#skillfileresponse) ] | | No |
|
||||
| hash_code | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| is_latest | boolean | | No |
|
||||
| publish_note | string | | Yes |
|
||||
| published_by | string | | No |
|
||||
| published_by_name | string | | No |
|
||||
| skill_id | string | | Yes |
|
||||
| version_name | string | | Yes |
|
||||
| version_number | integer | | Yes |
|
||||
|
||||
#### SkillVersionListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| data | [ [SkillVersionResponse](#skillversionresponse) ] | | No |
|
||||
|
||||
#### SkillVersionResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| archive_size | integer | | Yes |
|
||||
| created_at | integer | | Yes |
|
||||
| hash_code | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| is_latest | boolean | | No |
|
||||
| publish_note | string | | Yes |
|
||||
| published_by | string | | No |
|
||||
| published_by_name | string | | No |
|
||||
| skill_id | string | | Yes |
|
||||
| version_name | string | | Yes |
|
||||
| version_number | integer | | Yes |
|
||||
|
||||
#### SkillVersionUpdatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| publish_note | string | | No |
|
||||
| version_name | string | | No |
|
||||
|
||||
#### SnippetDependencyCheckResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -22347,7 +23137,7 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| keyword | string | Search keyword | No |
|
||||
| type | string, <br>**Available values:** "", "app", "knowledge", "snippet" | Tag type filter<br>*Enum:* `""`, `"app"`, `"knowledge"`, `"snippet"` | No |
|
||||
| type | [TagType](#tagtype)<br>string | Tag type filter | No |
|
||||
|
||||
#### TagListResponse
|
||||
|
||||
@@ -24543,6 +25333,15 @@ Workflow tool configuration
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| permission_keys | [ string ] | | No |
|
||||
|
||||
#### WorkspaceSkillsQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| keyword | string | Search keyword matching skill name or description. | No |
|
||||
| limit | integer, <br>**Default:** 20 | Number of items per page. | No |
|
||||
| page | integer, <br>**Default:** 1 | Page number. | No |
|
||||
| tag | [ string ] | Skill tag filters. Repeat the parameter for multiple tags. | No |
|
||||
|
||||
#### WorkspaceTenantResultResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@@ -1614,7 +1614,9 @@ class TenantService:
|
||||
return updated_accounts
|
||||
|
||||
@staticmethod
|
||||
def iter_member_account_id_batches(tenant_id: str, batch_size: int, *, session: Session) -> Iterator[list[str]]:
|
||||
def iter_member_account_id_batches(
|
||||
tenant_id: str, batch_size: int, *, session: Session
|
||||
) -> Iterator[list[str]]:
|
||||
"""Yield workspace member account ids in bounded, ordered batches."""
|
||||
offset = 0
|
||||
while True:
|
||||
|
||||
@@ -19,12 +19,11 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import io
|
||||
import posixpath
|
||||
import re
|
||||
import zipfile
|
||||
import zlib
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field, ValidationError, field_validator
|
||||
|
||||
from configs import dify_config
|
||||
|
||||
@@ -34,7 +33,8 @@ _MAX_SKILL_MD_BYTES = 1 * 1024 * 1024
|
||||
_MAX_ENTRIES = 5000
|
||||
_ALLOWED_EXTENSIONS = (".zip", ".skill")
|
||||
_SKILL_MD_NAME = "SKILL.md"
|
||||
_HEADING_RE = re.compile(r"^\s*#\s+(.+?)\s*$", re.MULTILINE)
|
||||
_SKILL_NAME_PATTERN = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
||||
_MAX_SKILL_DESCRIPTION_LENGTH = 1024
|
||||
|
||||
|
||||
class SkillPackageError(Exception):
|
||||
@@ -54,13 +54,18 @@ class SkillPackageError(Exception):
|
||||
class SkillManifest(BaseModel):
|
||||
"""Validated metadata extracted from a Skill package."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
name: str = Field(min_length=1, max_length=64, pattern=_SKILL_NAME_PATTERN)
|
||||
description: str = Field(min_length=1, max_length=_MAX_SKILL_DESCRIPTION_LENGTH)
|
||||
entry_path: str # path of SKILL.md inside the archive
|
||||
files: list[str] # all (safe) file paths inside the archive
|
||||
size: int # total uncompressed bytes
|
||||
hash: str # sha256 of the archive bytes
|
||||
|
||||
@field_validator("name", "description", mode="before")
|
||||
@classmethod
|
||||
def _strip_required_string(cls, value: object) -> object:
|
||||
return value.strip() if isinstance(value, str) else value
|
||||
|
||||
|
||||
class NormalizedSkillPackage(BaseModel):
|
||||
"""Canonical skill package bytes and metadata ready to store in agent drive."""
|
||||
@@ -109,14 +114,17 @@ class SkillPackageService:
|
||||
normalized_size = sum(max(info.file_size, 0) for info in normalized_members.values())
|
||||
|
||||
name, description = self._parse_skill_md(skill_md)
|
||||
manifest = SkillManifest(
|
||||
name=name,
|
||||
description=description,
|
||||
entry_path=_SKILL_MD_NAME,
|
||||
files=sorted(normalized_members),
|
||||
size=normalized_size,
|
||||
hash=hashlib.sha256(normalized_archive_bytes).hexdigest(),
|
||||
)
|
||||
try:
|
||||
manifest = SkillManifest(
|
||||
name=name,
|
||||
description=description,
|
||||
entry_path=_SKILL_MD_NAME,
|
||||
files=sorted(normalized_members),
|
||||
size=normalized_size,
|
||||
hash=hashlib.sha256(normalized_archive_bytes).hexdigest(),
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise self._manifest_validation_error(exc) from exc
|
||||
return NormalizedSkillPackage(
|
||||
manifest=manifest,
|
||||
archive_bytes=normalized_archive_bytes,
|
||||
@@ -124,6 +132,31 @@ class SkillPackageService:
|
||||
strip_prefix=strip_prefix,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _manifest_validation_error(exc: ValidationError) -> SkillPackageError:
|
||||
first_error = exc.errors()[0]
|
||||
loc = first_error["loc"]
|
||||
field = loc[0] if loc else "manifest"
|
||||
error_type = first_error["type"]
|
||||
if field == "name":
|
||||
code = "missing_skill_name" if error_type == "string_too_short" else "invalid_skill_name"
|
||||
message = (
|
||||
"SKILL.md frontmatter name is required"
|
||||
if code == "missing_skill_name"
|
||||
else "SKILL.md frontmatter name must be lowercase letters, numbers, and hyphens only, "
|
||||
"must not start or end with a hyphen, and must be at most 64 characters"
|
||||
)
|
||||
return SkillPackageError(code, message, status_code=400)
|
||||
if field == "description":
|
||||
code = "missing_skill_description" if error_type == "string_too_short" else "invalid_skill_description"
|
||||
message = (
|
||||
"SKILL.md frontmatter description is required"
|
||||
if code == "missing_skill_description"
|
||||
else f"SKILL.md frontmatter description must be at most {_MAX_SKILL_DESCRIPTION_LENGTH} characters"
|
||||
)
|
||||
return SkillPackageError(code, message, status_code=400)
|
||||
return SkillPackageError("invalid_skill_manifest", "SKILL.md frontmatter is invalid", status_code=400)
|
||||
|
||||
def _open_archive(self, *, content: bytes, filename: str) -> zipfile.ZipFile:
|
||||
self._check_extension(filename)
|
||||
if not content:
|
||||
@@ -282,13 +315,6 @@ class SkillPackageService:
|
||||
frontmatter = cls._parse_frontmatter(content)
|
||||
name = str(frontmatter.get("name") or "").strip()
|
||||
description = str(frontmatter.get("description") or "").strip()
|
||||
if not name:
|
||||
heading = _HEADING_RE.search(content)
|
||||
name = heading.group(1).strip() if heading else ""
|
||||
if not name:
|
||||
raise SkillPackageError(
|
||||
"missing_skill_name", "SKILL.md must declare a name (frontmatter or top heading)", status_code=400
|
||||
)
|
||||
return name, description
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -37,6 +37,7 @@ from core.db.session_factory import session_factory as default_session_factory
|
||||
from core.tools.signature import bind_file_uri, sign_tool_file_uri
|
||||
from core.tools.tool_file_manager import ToolFileManager
|
||||
from extensions.ext_storage import storage
|
||||
from factories import file_factory
|
||||
from models.agent import Agent, AgentConfigDraft, AgentConfigDraftType, AgentConfigSnapshot
|
||||
from models.agent_config_entities import (
|
||||
AgentConfigFileRefConfig,
|
||||
@@ -51,6 +52,7 @@ from models.tools import ToolFile
|
||||
from services.agent.config_skill_normalize_service import ConfigSkillNormalizeService
|
||||
from services.agent.skill_package_service import SkillPackageError
|
||||
from services.agent_drive_service import DriveFileRef
|
||||
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
|
||||
|
||||
|
||||
class AgentConfigVersionKind(StrEnum):
|
||||
@@ -103,6 +105,7 @@ class ConfigPushPayload(BaseModel):
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AgentConfigTarget:
|
||||
tenant_id: str
|
||||
agent_id: str
|
||||
version_id: str
|
||||
kind: AgentConfigVersionKind
|
||||
@@ -129,17 +132,10 @@ class ConfigDownloadRequest:
|
||||
|
||||
|
||||
class AgentConfigService:
|
||||
"""Read and update Agent Soul-backed config assets for one version target.
|
||||
|
||||
The service owns the lifecycle of its database sessions. Callers may inject
|
||||
a session creator for an alternate engine; production defaults to the
|
||||
application-wide session factory.
|
||||
"""
|
||||
"""Read and update Agent Soul-backed config assets for one version target."""
|
||||
|
||||
PREVIEW_MAX_BYTES = 64 * 1024
|
||||
|
||||
_session_factory: Callable[[], Session]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -147,8 +143,6 @@ class AgentConfigService:
|
||||
skill_normalize_service: ConfigSkillNormalizeService | None = None,
|
||||
session_factory: Callable[[], Session] | None = None,
|
||||
) -> None:
|
||||
"""Initialize external collaborators and the service-owned session creator."""
|
||||
|
||||
self._tool_files = tool_file_manager or ToolFileManager()
|
||||
self._skill_normalizer = skill_normalize_service or ConfigSkillNormalizeService()
|
||||
self._session_factory = session_factory or default_session_factory.create_session
|
||||
@@ -172,6 +166,7 @@ class AgentConfigService:
|
||||
user_id=user_id,
|
||||
)
|
||||
return AgentConfigTarget(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=target.agent_id,
|
||||
version_id=target.version_id,
|
||||
kind=target.kind,
|
||||
@@ -217,7 +212,7 @@ class AgentConfigService:
|
||||
return {
|
||||
"agent_id": target.agent_id,
|
||||
"config_version": self._config_version_payload(target),
|
||||
"items": [self._serialize_skill_item(skill) for skill in target.agent_soul.config_skills],
|
||||
"items": self._skill_items_for_target(target, include_runtime_workspace_skills=False),
|
||||
}
|
||||
|
||||
def list_files(
|
||||
@@ -253,8 +248,6 @@ class AgentConfigService:
|
||||
name: str,
|
||||
user_id: str | None = None,
|
||||
) -> ConfigDownloadRequest:
|
||||
"""Authorize one Config reference and return origin-free download metadata."""
|
||||
|
||||
target = self.resolve_target(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
@@ -263,12 +256,39 @@ class AgentConfigService:
|
||||
user_id=user_id,
|
||||
)
|
||||
if kind == "skill":
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
try:
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
return self._resolve_download_request(
|
||||
tenant_id=tenant_id,
|
||||
file_kind=skill.file_kind,
|
||||
file_id=self._available_skill_file_id(skill),
|
||||
filename=f"{skill.name}.zip",
|
||||
default_mime_type="application/zip",
|
||||
missing_code="config_skill_not_found",
|
||||
missing_message="config skill payload is missing",
|
||||
)
|
||||
except AgentConfigServiceError as exc:
|
||||
if exc.code != "config_skill_not_found":
|
||||
raise
|
||||
|
||||
runtime_skill = next(
|
||||
(
|
||||
item
|
||||
for item in SkillManagementService().list_runtime_agent_skills(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
if item["name"] == name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if runtime_skill is None:
|
||||
raise AgentConfigServiceError("config_skill_not_found", "config skill not found", status_code=404)
|
||||
return self._resolve_download_request(
|
||||
tenant_id=tenant_id,
|
||||
file_kind=skill.file_kind,
|
||||
file_id=self._available_skill_file_id(skill),
|
||||
filename=f"{skill.name}.zip",
|
||||
file_kind="tool_file",
|
||||
file_id=str(runtime_skill["file_id"]),
|
||||
filename=f"{runtime_skill['name']}.zip",
|
||||
default_mime_type="application/zip",
|
||||
missing_code="config_skill_not_found",
|
||||
missing_message="config skill payload is missing",
|
||||
@@ -285,6 +305,45 @@ class AgentConfigService:
|
||||
missing_message="config file payload is missing",
|
||||
)
|
||||
|
||||
def pull_skill(
|
||||
self,
|
||||
*,
|
||||
tenant_id: str,
|
||||
agent_id: str,
|
||||
config_version_id: str,
|
||||
config_version_kind: AgentConfigVersionKind,
|
||||
name: str,
|
||||
user_id: str | None = None,
|
||||
) -> ConfigDownload:
|
||||
target = self.resolve_target(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
config_version_id=config_version_id,
|
||||
config_version_kind=config_version_kind,
|
||||
user_id=user_id,
|
||||
)
|
||||
try:
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
file_id = self._available_skill_file_id(skill)
|
||||
payload, mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
|
||||
return ConfigDownload(
|
||||
filename=f"{skill.name}.zip",
|
||||
mime_type=mime_type or "application/zip",
|
||||
payload=payload,
|
||||
)
|
||||
except AgentConfigServiceError as exc:
|
||||
if exc.code != "config_skill_not_found":
|
||||
raise
|
||||
try:
|
||||
result = SkillManagementService().pull_runtime_agent_skill(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
name=name,
|
||||
)
|
||||
return ConfigDownload(filename=result.filename, mime_type=result.mime_type, payload=result.payload)
|
||||
except SkillManagementServiceError as exc:
|
||||
raise AgentConfigServiceError("config_skill_not_found", "config skill not found", status_code=404) from exc
|
||||
|
||||
def download_skill_url(
|
||||
self,
|
||||
*,
|
||||
@@ -323,9 +382,45 @@ class AgentConfigService:
|
||||
config_version_kind=config_version_kind,
|
||||
user_id=user_id,
|
||||
)
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
file_id = self._available_skill_file_id(skill)
|
||||
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
|
||||
try:
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
file_id = self._available_skill_file_id(skill)
|
||||
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
|
||||
skill_item = self._serialize_skill_item(skill)
|
||||
except AgentConfigServiceError as exc:
|
||||
if exc.code != "config_skill_not_found":
|
||||
raise
|
||||
try:
|
||||
workspace_archive = SkillManagementService().pull_runtime_agent_skill(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
name=name,
|
||||
)
|
||||
except SkillManagementServiceError as skill_exc:
|
||||
raise AgentConfigServiceError(
|
||||
"config_skill_not_found",
|
||||
"config skill not found",
|
||||
status_code=404,
|
||||
) from skill_exc
|
||||
archive_bytes = workspace_archive.payload
|
||||
skill_item = next(
|
||||
(
|
||||
item
|
||||
for item in SkillManagementService().list_runtime_agent_skills(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
if item["name"] == name
|
||||
),
|
||||
{
|
||||
"id": name,
|
||||
"name": name,
|
||||
"description": "",
|
||||
"size": None,
|
||||
"hash": None,
|
||||
"mime_type": "application/zip",
|
||||
},
|
||||
)
|
||||
try:
|
||||
archive_items, skill_md = self._inspect_skill_archive(archive_bytes)
|
||||
except (OSError, ValueError, zipfile.BadZipFile) as exc:
|
||||
@@ -335,7 +430,7 @@ class AgentConfigService:
|
||||
status_code=500,
|
||||
) from exc
|
||||
return {
|
||||
**self._serialize_skill_item(skill),
|
||||
**skill_item,
|
||||
"source": "config_skill_zip",
|
||||
"files": archive_items,
|
||||
"skill_md": skill_md,
|
||||
@@ -430,6 +525,34 @@ class AgentConfigService:
|
||||
)
|
||||
return member_path
|
||||
|
||||
def pull_file(
|
||||
self,
|
||||
*,
|
||||
tenant_id: str,
|
||||
agent_id: str,
|
||||
config_version_id: str,
|
||||
config_version_kind: AgentConfigVersionKind,
|
||||
name: str,
|
||||
user_id: str | None = None,
|
||||
) -> ConfigDownload:
|
||||
target = self.resolve_target(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
config_version_id=config_version_id,
|
||||
config_version_kind=config_version_kind,
|
||||
user_id=user_id,
|
||||
)
|
||||
file_ref = self._require_file(target.agent_soul, name=name)
|
||||
file_id = self._available_file_id(file_ref)
|
||||
payload, filename, mime_type = self._load_file_ref_bytes(
|
||||
tenant_id=tenant_id,
|
||||
file_kind=file_ref.file_kind,
|
||||
file_id=file_id,
|
||||
)
|
||||
return ConfigDownload(
|
||||
filename=filename or file_ref.name, mime_type=mime_type or "application/octet-stream", payload=payload
|
||||
)
|
||||
|
||||
def download_file_url(
|
||||
self,
|
||||
*,
|
||||
@@ -852,6 +975,7 @@ class AgentConfigService:
|
||||
status_code=404,
|
||||
)
|
||||
return AgentConfigTarget(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
version_id=version.id,
|
||||
kind=config_version_kind,
|
||||
@@ -1147,7 +1271,7 @@ class AgentConfigService:
|
||||
"agent_id": target.agent_id,
|
||||
"config_version": AgentConfigService._config_version_payload(target),
|
||||
"skills": {
|
||||
"items": [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
|
||||
"items": AgentConfigService._skill_items_for_target(target, include_runtime_workspace_skills=True)
|
||||
},
|
||||
"files": {
|
||||
"items": [
|
||||
@@ -1158,6 +1282,25 @@ class AgentConfigService:
|
||||
"note": target.agent_soul.config_note,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _skill_items_for_target(
|
||||
target: AgentConfigTarget, *, include_runtime_workspace_skills: bool
|
||||
) -> list[dict[str, object]]:
|
||||
items = [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
|
||||
if not include_runtime_workspace_skills:
|
||||
return items
|
||||
|
||||
seen_names = {str(item["name"]) for item in items}
|
||||
for item in SkillManagementService().list_runtime_agent_skills(
|
||||
tenant_id=target.tenant_id,
|
||||
agent_id=target.agent_id,
|
||||
):
|
||||
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 {
|
||||
@@ -1426,12 +1569,37 @@ class AgentConfigService:
|
||||
query.append(("as_attachment", "true"))
|
||||
uri = urllib.parse.urlunsplit(parsed._replace(query=urllib.parse.urlencode(query)))
|
||||
|
||||
return ConfigDownloadRequest(
|
||||
filename=filename,
|
||||
mime_type=mime_type,
|
||||
size=size,
|
||||
download_uri=uri,
|
||||
)
|
||||
return ConfigDownloadRequest(filename=filename, mime_type=mime_type, size=size, download_uri=uri)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_download_url(
|
||||
*, tenant_id: str, file_kind: Literal["upload_file", "tool_file"], file_id: str
|
||||
) -> str | None:
|
||||
controller = DatabaseFileAccessController()
|
||||
from core.app.workflow.file_runtime import DifyWorkflowFileRuntime
|
||||
|
||||
runtime = DifyWorkflowFileRuntime(file_access_controller=controller)
|
||||
try:
|
||||
if file_kind == "upload_file":
|
||||
return runtime.resolve_upload_file_url(
|
||||
upload_file_id=file_id,
|
||||
for_external=True,
|
||||
as_attachment=True,
|
||||
)
|
||||
file = file_factory.build_from_mapping(
|
||||
mapping={"transfer_method": "tool_file", "tool_file_id": file_id},
|
||||
tenant_id=tenant_id,
|
||||
access_controller=controller,
|
||||
)
|
||||
url = runtime.resolve_file_url(file=file, for_external=True)
|
||||
if not url:
|
||||
return None
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
|
||||
query.append(("as_attachment", "true"))
|
||||
return urllib.parse.urlunsplit(parsed._replace(query=urllib.parse.urlencode(query)))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -37,6 +37,7 @@ from models.agent import (
|
||||
AgentWorkspaceBinding,
|
||||
)
|
||||
from models.model import App, AppMode, AppModelConfig, IconType, Site, load_annotation_reply_config
|
||||
from models.skill import AgentSkillBinding
|
||||
from models.tools import ApiToolProvider
|
||||
from models.workflow import Workflow
|
||||
from services.agent.errors import AgentAccessNotReadyError, AgentNameConflictError
|
||||
@@ -999,6 +1000,14 @@ class AppService:
|
||||
Agent.status == AgentStatus.ACTIVE,
|
||||
)
|
||||
).all()
|
||||
agent_ids_to_unbind = [*workflow_agent_ids]
|
||||
if backing_agent is not None:
|
||||
agent_ids_to_unbind.append(backing_agent.id)
|
||||
if agent_ids_to_unbind:
|
||||
session.query(AgentSkillBinding).filter(
|
||||
AgentSkillBinding.tenant_id == app.tenant_id,
|
||||
AgentSkillBinding.agent_id.in_(agent_ids_to_unbind),
|
||||
).delete(synchronize_session=False)
|
||||
account_id = current_user.id if current_user else None
|
||||
if backing_agent is not None:
|
||||
now = naive_utc_now()
|
||||
|
||||
@@ -304,6 +304,10 @@ class MyPermissionsResponse(_RBACModel):
|
||||
# Fallback permission snapshots for legacy Dify tenant roles when external RBAC is disabled.
|
||||
# Keep these keys aligned with langgenius/rbac's built-in workspace roles and access policies.
|
||||
_LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [
|
||||
"skill.view",
|
||||
"skill.edit",
|
||||
"skill.publish",
|
||||
"skill.delete",
|
||||
"workspace.member.manage",
|
||||
"workspace.role.manage",
|
||||
"data_source.manage",
|
||||
@@ -334,6 +338,10 @@ _LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [
|
||||
]
|
||||
|
||||
_LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [
|
||||
"skill.view",
|
||||
"skill.edit",
|
||||
"skill.publish",
|
||||
"skill.delete",
|
||||
"workspace.member.manage",
|
||||
"workspace.role.manage",
|
||||
"data_source.manage",
|
||||
@@ -362,6 +370,10 @@ _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [
|
||||
]
|
||||
|
||||
_LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [
|
||||
"skill.view",
|
||||
"skill.edit",
|
||||
"skill.publish",
|
||||
"skill.delete",
|
||||
"api_extension.manage",
|
||||
"plugin.install",
|
||||
"credential.use",
|
||||
@@ -377,6 +389,7 @@ _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [
|
||||
]
|
||||
|
||||
_LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [
|
||||
"skill.view",
|
||||
"api_extension.manage",
|
||||
"plugin.install",
|
||||
"credential.use",
|
||||
@@ -384,6 +397,7 @@ _LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [
|
||||
]
|
||||
|
||||
_LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS: list[str] = [
|
||||
"skill.view",
|
||||
"plugin.install",
|
||||
"dataset.create_and_management",
|
||||
"dataset.external.connect",
|
||||
|
||||
@@ -12,6 +12,7 @@ from werkzeug.exceptions import NotFound
|
||||
from models.dataset import Dataset
|
||||
from models.enums import TagType
|
||||
from models.model import App, Tag, TagBinding
|
||||
from models.skill import Skill
|
||||
from models.snippet import CustomizedSnippet
|
||||
|
||||
type _TagTypeLike = TagType | str
|
||||
@@ -282,5 +283,13 @@ class TagService:
|
||||
)
|
||||
if not snippet:
|
||||
raise NotFound("Snippet not found")
|
||||
elif type == "skill":
|
||||
skill = session.scalar(
|
||||
select(Skill)
|
||||
.where(Skill.tenant_id == current_user.current_tenant_id, Skill.id == target_id)
|
||||
.limit(1)
|
||||
)
|
||||
if not skill:
|
||||
raise NotFound("Skill not found")
|
||||
else:
|
||||
raise NotFound("Invalid binding type")
|
||||
|
||||
@@ -39,6 +39,7 @@ from core.workflow.system_variables import build_system_variables
|
||||
from graphon.entities import WorkflowStartReason
|
||||
from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from libs.datetime_utils import to_utc_timestamp
|
||||
from models.account import Account
|
||||
from models.enums import CreatorUserRole
|
||||
from models.human_input import HumanInputForm
|
||||
@@ -679,7 +680,7 @@ class TestHitlServiceApi:
|
||||
assert pause_resp.data.reasons[0]["TYPE"] == "human_input_required"
|
||||
assert pause_resp.data.reasons[0]["form_id"] == "form-1"
|
||||
assert pause_resp.data.reasons[0]["form_token"] == "token"
|
||||
assert pause_resp.data.reasons[0]["expiration_time"] == int(expiration_time.timestamp())
|
||||
assert pause_resp.data.reasons[0]["expiration_time"] == to_utc_timestamp(expiration_time)
|
||||
|
||||
assert isinstance(responses[0], HumanInputRequiredResponse)
|
||||
hi_resp = responses[0]
|
||||
@@ -690,7 +691,7 @@ class TestHitlServiceApi:
|
||||
assert hi_resp.data.actions[0].id == "approve"
|
||||
assert hi_resp.data.display_in_ui is True
|
||||
assert hi_resp.data.form_token == "token"
|
||||
assert hi_resp.data.expiration_time == int(expiration_time.timestamp())
|
||||
assert hi_resp.data.expiration_time == to_utc_timestamp(expiration_time)
|
||||
|
||||
# Snapshot payload contract
|
||||
@pytest.mark.parametrize("sqlite_session", [(HumanInputForm,)], indirect=True)
|
||||
@@ -748,13 +749,13 @@ class TestHitlServiceApi:
|
||||
]
|
||||
assert events[2]["data"]["status"] == WorkflowNodeExecutionStatus.PAUSED.value
|
||||
assert events[3]["data"]["form_token"] == "wtok"
|
||||
assert events[3]["data"]["expiration_time"] == int(expiration_time.timestamp())
|
||||
assert events[3]["data"]["expiration_time"] == to_utc_timestamp(expiration_time)
|
||||
pause_data = events[-1]["data"]
|
||||
assert pause_data["paused_nodes"] == ["node-1"]
|
||||
assert pause_data["outputs"] == {"result": "value"}
|
||||
assert pause_data["reasons"][0]["TYPE"] == "human_input_required"
|
||||
assert pause_data["reasons"][0]["form_token"] == "wtok"
|
||||
assert pause_data["reasons"][0]["expiration_time"] == int(expiration_time.timestamp())
|
||||
assert pause_data["reasons"][0]["expiration_time"] == to_utc_timestamp(expiration_time)
|
||||
assert pause_data["status"] == WorkflowExecutionStatus.PAUSED.value
|
||||
assert pause_data["created_at"] == int(workflow_run.created_at.timestamp())
|
||||
assert pause_data["elapsed_time"] == workflow_run.elapsed_time
|
||||
|
||||
@@ -7,6 +7,7 @@ from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from dify_agent.layers.config import DifyConfigSkillConfig
|
||||
from dify_agent.layers.dify_core_tools import DifyCoreToolConfig, DifyCoreToolsLayerConfig
|
||||
from dify_agent.layers.dify_plugin import DifyPluginToolConfig, DifyPluginToolsLayerConfig
|
||||
from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig
|
||||
@@ -29,6 +30,14 @@ from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom
|
||||
from models.agent_config_entities import AgentSoulConfig
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs",
|
||||
lambda **_kwargs: [],
|
||||
)
|
||||
|
||||
|
||||
def _exec_ctx() -> DifyExecutionContextLayerConfig:
|
||||
return DifyExecutionContextLayerConfig(
|
||||
tenant_id="tenant-1",
|
||||
@@ -511,6 +520,33 @@ class TestAgentAppConfigLayer:
|
||||
"mentioned_file_names": [],
|
||||
}
|
||||
|
||||
def test_config_layer_includes_bound_workspace_skills(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs",
|
||||
lambda **_kwargs: [
|
||||
DifyConfigSkillConfig(
|
||||
name="workspace-skill",
|
||||
description="Bound workspace skill.",
|
||||
size=123,
|
||||
mime_type="application/zip",
|
||||
)
|
||||
],
|
||||
)
|
||||
soul = _soul_with_model()
|
||||
soul.prompt.system_prompt = "Use [§skill:workspace-skill:Workspace Skill§]."
|
||||
builder = AgentAppRuntimeRequestBuilder(
|
||||
credentials_provider=_FakeCredentialsProvider(),
|
||||
dify_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
result = builder.build(_ctx(soul))
|
||||
|
||||
config = next(layer for layer in result.request.composition.layers if layer.name == DIFY_CONFIG_LAYER_ID)
|
||||
assert [skill.name for skill in config.config.skills] == ["workspace-skill"]
|
||||
assert config.config.mentioned_skill_names == ["workspace-skill"]
|
||||
prompt_layer = next(layer for layer in result.request.composition.layers if layer.name == "agent_soul_prompt")
|
||||
assert prompt_layer.config.prefix == "Use workspace-skill."
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("system_prompt", "expected_prefix"),
|
||||
[
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import cast
|
||||
|
||||
import pytest
|
||||
from agenton.compositor import CompositorSessionSnapshot
|
||||
from dify_agent.layers.config import DifyConfigSkillConfig
|
||||
from dify_agent.layers.dify_core_tools import DifyCoreToolConfig, DifyCoreToolsLayerConfig
|
||||
from dify_agent.layers.dify_plugin import DifyPluginToolConfig, DifyPluginToolsLayerConfig
|
||||
from dify_agent.protocol import DIFY_AGENT_HISTORY_LAYER_ID, DIFY_AGENT_MODEL_LAYER_ID
|
||||
@@ -40,6 +41,14 @@ from models.agent_config_entities import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs",
|
||||
lambda **_kwargs: [],
|
||||
)
|
||||
|
||||
|
||||
class FakeCredentialsProvider:
|
||||
def fetch(self, provider_name: str, model_name: str) -> dict[str, object]:
|
||||
assert provider_name == "openai"
|
||||
@@ -1422,6 +1431,30 @@ def test_build_config_layer_config_includes_soul_context_and_mentions():
|
||||
assert warnings == []
|
||||
|
||||
|
||||
def test_build_config_layer_config_includes_runtime_agent_skills():
|
||||
from core.workflow.nodes.agent_v2.runtime_request_builder import build_config_layer_config
|
||||
|
||||
soul = AgentSoulConfig(
|
||||
prompt={"system_prompt": "Use [§skill:workspace-skill:Workspace Skill§]."},
|
||||
model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"),
|
||||
)
|
||||
config, warnings = build_config_layer_config(
|
||||
soul,
|
||||
runtime_config_skills=[
|
||||
DifyConfigSkillConfig(
|
||||
name="workspace-skill",
|
||||
description="Bound workspace skill.",
|
||||
size=123,
|
||||
mime_type="application/zip",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
assert [skill.name for skill in config.skills] == ["workspace-skill"]
|
||||
assert config.mentioned_skill_names == ["workspace-skill"]
|
||||
assert warnings == []
|
||||
|
||||
|
||||
def test_build_config_layer_config_returns_empty_config_for_empty_agent_soul():
|
||||
from core.workflow.nodes.agent_v2.runtime_request_builder import build_config_layer_config
|
||||
|
||||
@@ -1505,6 +1538,33 @@ def test_workflow_run_request_contains_config_layer():
|
||||
assert warnings == []
|
||||
|
||||
|
||||
def test_workflow_run_request_includes_bound_workspace_skills(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs",
|
||||
lambda **_kwargs: [
|
||||
DifyConfigSkillConfig(
|
||||
name="workspace-skill",
|
||||
description="Bound workspace skill.",
|
||||
size=123,
|
||||
mime_type="application/zip",
|
||||
)
|
||||
],
|
||||
)
|
||||
context = _context()
|
||||
context.snapshot.config_snapshot = AgentSoulConfig(
|
||||
prompt={"system_prompt": "Use [§skill:workspace-skill:Workspace Skill§]."},
|
||||
model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"),
|
||||
)
|
||||
|
||||
result = WorkflowAgentRuntimeRequestBuilder(credentials_provider=FakeCredentialsProvider()).build(context)
|
||||
|
||||
config = next(layer for layer in result.request.composition.layers if layer.name == DIFY_CONFIG_LAYER_ID)
|
||||
assert [skill.name for skill in config.config.skills] == ["workspace-skill"]
|
||||
assert config.config.mentioned_skill_names == ["workspace-skill"]
|
||||
soul_prompt = next(layer for layer in result.request.composition.layers if layer.name == "agent_soul_prompt")
|
||||
assert soul_prompt.config.prefix == "Use workspace-skill."
|
||||
|
||||
|
||||
def test_workflow_runtime_expands_config_mentions_in_agent_soul_prompt():
|
||||
context = _context()
|
||||
context.snapshot.config_snapshot = _soul_with_config_assets()
|
||||
|
||||
@@ -13,7 +13,7 @@ from services.agent import skill_package_service as skill_package_service_module
|
||||
from services.agent.skill_package_service import NormalizedSkillPackage, SkillPackageError, SkillPackageService
|
||||
|
||||
_SKILL_MD = """---
|
||||
name: PDF Toolkit
|
||||
name: pdf-toolkit
|
||||
description: Tools for working with PDF files.
|
||||
---
|
||||
|
||||
@@ -43,7 +43,7 @@ def _archive_members(content: bytes) -> list[str]:
|
||||
def test_valid_skill_normalizes_manifest():
|
||||
manifest = _normalize({"SKILL.md": _SKILL_MD.encode(), "scripts/run.py": b"print('hi')\n"}).manifest
|
||||
|
||||
assert manifest.name == "PDF Toolkit"
|
||||
assert manifest.name == "pdf-toolkit"
|
||||
assert manifest.description == "Tools for working with PDF files."
|
||||
assert manifest.entry_path == "SKILL.md"
|
||||
assert set(manifest.files) == {"SKILL.md", "scripts/run.py"}
|
||||
@@ -51,10 +51,10 @@ def test_valid_skill_normalizes_manifest():
|
||||
assert len(manifest.hash) == 64
|
||||
|
||||
|
||||
def test_name_falls_back_to_heading_without_frontmatter():
|
||||
manifest = _normalize({"SKILL.md": b"# Heading Name\n\nbody"}).manifest
|
||||
assert manifest.name == "Heading Name"
|
||||
assert manifest.description == ""
|
||||
def test_name_and_description_are_required_in_frontmatter():
|
||||
with pytest.raises(SkillPackageError) as exc_info:
|
||||
_normalize({"SKILL.md": b"# heading-name\n\nbody"})
|
||||
assert exc_info.value.code == "missing_skill_name"
|
||||
|
||||
|
||||
def test_shallowest_skill_md_preferred_during_normalization():
|
||||
@@ -155,7 +155,18 @@ def test_validate_and_normalize_strips_deeper_selected_skill_root():
|
||||
({"README.md": b"x"}, "skill.zip", "missing_skill_md"),
|
||||
({"SKILL.md": _SKILL_MD.encode()}, "skill.tar", "unsupported_extension"),
|
||||
({"SKILL.md": b""}, "skill.zip", "empty_skill_md"),
|
||||
({"SKILL.md": b"no name here"}, "skill.zip", "missing_skill_name"),
|
||||
({"SKILL.md": b"---\ndescription: valid\n---\n# no name here"}, "skill.zip", "missing_skill_name"),
|
||||
({"SKILL.md": b"---\nname: pdf-toolkit\n---\n# no description"}, "skill.zip", "missing_skill_description"),
|
||||
(
|
||||
{"SKILL.md": b"---\nname: PDF Toolkit\ndescription: valid\n---\n# invalid name"},
|
||||
"skill.zip",
|
||||
"invalid_skill_name",
|
||||
),
|
||||
(
|
||||
{"SKILL.md": f"---\nname: pdf-toolkit\ndescription: {'x' * 1025}\n---\n# long".encode()},
|
||||
"skill.zip",
|
||||
"invalid_skill_description",
|
||||
),
|
||||
({"SKILL.md": b"\xff\xfenot utf8"}, "skill.zip", "skill_md_not_utf8"),
|
||||
],
|
||||
)
|
||||
@@ -224,10 +235,10 @@ def test_bad_frontmatter_yaml_rejected():
|
||||
assert exc_info.value.code == "invalid_frontmatter"
|
||||
|
||||
|
||||
def test_unterminated_frontmatter_falls_back_to_heading():
|
||||
# leading '---' with no closing fence -> no frontmatter, use the heading
|
||||
manifest = _normalize({"SKILL.md": b"---\n# Heading Wins\nbody"}).manifest
|
||||
assert manifest.name == "Heading Wins"
|
||||
def test_unterminated_frontmatter_rejected():
|
||||
with pytest.raises(SkillPackageError) as exc_info:
|
||||
_normalize({"SKILL.md": b"---\n# heading-wins\nbody"})
|
||||
assert exc_info.value.code == "missing_skill_name"
|
||||
|
||||
|
||||
def test_validate_and_normalize_rejects_files_outside_selected_skill_root():
|
||||
|
||||
@@ -20,7 +20,7 @@ _AGENT_ID = "22222222-2222-2222-2222-222222222222"
|
||||
_USER_ID = "33333333-3333-3333-3333-333333333333"
|
||||
|
||||
_SKILL_MD = b"""---
|
||||
name: PDF Toolkit
|
||||
name: pdf-toolkit
|
||||
description: Work with PDFs.
|
||||
---
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest(
|
||||
assert skill_row.is_skill is True
|
||||
assert skill_row.skill_metadata is not None
|
||||
skill_metadata = DriveSkillMetadata.model_validate_json(skill_row.skill_metadata)
|
||||
assert skill_metadata.name == "PDF Toolkit"
|
||||
assert skill_metadata.name == "pdf-toolkit"
|
||||
assert skill_metadata.manifest_files == ["SKILL.md", "scripts/run.py"]
|
||||
assert archive_row.file_kind == AgentDriveFileKind.TOOL_FILE
|
||||
assert archive_row.file_id == archive_tool_file.id
|
||||
@@ -132,7 +132,7 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest(
|
||||
# The returned upload response carries only the drive-derived fields the UI needs.
|
||||
skill = result["skill"]
|
||||
assert skill["path"] == "pdf-toolkit"
|
||||
assert skill["name"] == "PDF Toolkit"
|
||||
assert skill["name"] == "pdf-toolkit"
|
||||
assert skill["archive_key"] == "pdf-toolkit/.DIFY-SKILL-FULL.zip"
|
||||
assert skill["skill_md_key"] == "pdf-toolkit/SKILL.md"
|
||||
assert result["manifest"]["entry_path"] == "SKILL.md"
|
||||
|
||||
@@ -40,6 +40,7 @@ from services.agent_config_service import (
|
||||
ConfigPushPayload,
|
||||
ConfigPushSkillItem,
|
||||
)
|
||||
from services.skill_management_service import SkillManagementServiceError
|
||||
|
||||
MODULE = "services.agent_config_service"
|
||||
TENANT = "11111111-1111-1111-1111-111111111111"
|
||||
@@ -138,6 +139,7 @@ def _target(
|
||||
) -> AgentConfigTarget:
|
||||
agent_soul = soul or _soul()
|
||||
return AgentConfigTarget(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
version_id=version_id,
|
||||
kind=kind,
|
||||
@@ -755,6 +757,149 @@ def test_inspect_skill_maps_invalid_archives_to_service_errors(archive_bytes: by
|
||||
assert exc_info.value.status_code == 500
|
||||
|
||||
|
||||
def test_pull_skill_falls_back_to_workspace_runtime_skill() -> None:
|
||||
service = AgentConfigService()
|
||||
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
|
||||
workspace_archive = SimpleNamespace(
|
||||
filename="workspace-skill.zip",
|
||||
mime_type="application/zip",
|
||||
payload=b"zip-bytes",
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(service, "resolve_target", return_value=target),
|
||||
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
|
||||
):
|
||||
skill_management_service.return_value.pull_runtime_agent_skill.return_value = workspace_archive
|
||||
download = service.pull_skill(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
config_version_id="draft-1",
|
||||
config_version_kind=AgentConfigVersionKind.DRAFT,
|
||||
name="workspace-skill",
|
||||
user_id=USER,
|
||||
)
|
||||
|
||||
assert download.filename == "workspace-skill.zip"
|
||||
assert download.mime_type == "application/zip"
|
||||
assert download.payload == b"zip-bytes"
|
||||
skill_management_service.return_value.pull_runtime_agent_skill.assert_called_once_with(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
name="workspace-skill",
|
||||
)
|
||||
|
||||
|
||||
def test_request_download_falls_back_to_workspace_runtime_skill() -> None:
|
||||
service = AgentConfigService()
|
||||
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
|
||||
expected = SimpleNamespace(
|
||||
filename="workspace-skill.zip",
|
||||
mime_type="application/zip",
|
||||
size=123,
|
||||
download_uri="/files/tools/workspace.zip?signature=1",
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(service, "resolve_target", return_value=target),
|
||||
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
|
||||
patch.object(service, "_resolve_download_request", return_value=expected) as resolve_download_request,
|
||||
):
|
||||
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
|
||||
{
|
||||
"name": "workspace-skill",
|
||||
"file_id": "workspace-archive-id",
|
||||
}
|
||||
]
|
||||
download = service.request_download(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
config_version_id="draft-1",
|
||||
config_version_kind=AgentConfigVersionKind.DRAFT,
|
||||
kind="skill",
|
||||
name="workspace-skill",
|
||||
user_id=USER,
|
||||
)
|
||||
|
||||
assert download is expected
|
||||
resolve_download_request.assert_called_once_with(
|
||||
tenant_id=TENANT,
|
||||
file_kind="tool_file",
|
||||
file_id="workspace-archive-id",
|
||||
filename="workspace-skill.zip",
|
||||
default_mime_type="application/zip",
|
||||
missing_code="config_skill_not_found",
|
||||
missing_message="config skill payload is missing",
|
||||
)
|
||||
|
||||
|
||||
def test_pull_skill_maps_missing_workspace_runtime_skill_to_config_error() -> None:
|
||||
service = AgentConfigService()
|
||||
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
|
||||
|
||||
with (
|
||||
patch.object(service, "resolve_target", return_value=target),
|
||||
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
|
||||
):
|
||||
skill_management_service.return_value.pull_runtime_agent_skill.side_effect = SkillManagementServiceError(
|
||||
"skill_not_found",
|
||||
"skill not found",
|
||||
status_code=404,
|
||||
)
|
||||
with pytest.raises(AgentConfigServiceError) as exc_info:
|
||||
service.pull_skill(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
config_version_id="draft-1",
|
||||
config_version_kind=AgentConfigVersionKind.DRAFT,
|
||||
name="workspace-skill",
|
||||
user_id=USER,
|
||||
)
|
||||
|
||||
assert exc_info.value.code == "config_skill_not_found"
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
|
||||
def test_inspect_skill_falls_back_to_workspace_runtime_skill() -> None:
|
||||
service = AgentConfigService()
|
||||
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
|
||||
archive = _zip_bytes(
|
||||
{
|
||||
"SKILL.md": b"---\nname: workspace-skill\ndescription: Workspace skill.\n---\n# Workspace",
|
||||
"references/policy.md": b"Policy",
|
||||
}
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(service, "resolve_target", return_value=target),
|
||||
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
|
||||
):
|
||||
skill_management_service.return_value.pull_runtime_agent_skill.return_value = SimpleNamespace(payload=archive)
|
||||
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
|
||||
{
|
||||
"id": "skill-1",
|
||||
"name": "workspace-skill",
|
||||
"description": "Workspace skill.",
|
||||
"size": len(archive),
|
||||
"hash": "hash",
|
||||
"mime_type": "application/zip",
|
||||
}
|
||||
]
|
||||
result = service.inspect_skill(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
config_version_id="draft-1",
|
||||
config_version_kind=AgentConfigVersionKind.DRAFT,
|
||||
name="workspace-skill",
|
||||
user_id=USER,
|
||||
)
|
||||
|
||||
assert result["id"] == "skill-1"
|
||||
assert result["source"] == "config_skill_zip"
|
||||
assert result["skill_md"]["text"] == "---\nname: workspace-skill\ndescription: Workspace skill.\n---\n# Workspace"
|
||||
assert [item["path"] for item in result["files"]] == ["SKILL.md", "references", "references/policy.md"]
|
||||
|
||||
|
||||
def test_manifest_uses_items_shape_without_download_urls() -> None:
|
||||
target = _target(
|
||||
kind=AgentConfigVersionKind.DRAFT,
|
||||
@@ -766,7 +911,9 @@ def test_manifest_uses_items_shape_without_download_urls() -> None:
|
||||
),
|
||||
)
|
||||
|
||||
manifest = AgentConfigService._manifest_for_target(target)
|
||||
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
|
||||
skill_management_service.return_value.list_runtime_agent_skills.return_value = []
|
||||
manifest = AgentConfigService._manifest_for_target(target)
|
||||
|
||||
assert manifest == {
|
||||
"agent_id": AGENT,
|
||||
@@ -823,7 +970,9 @@ def test_manifest_preserves_missing_config_assets_and_download_rejects_them(sqli
|
||||
user_id=USER,
|
||||
)
|
||||
|
||||
manifest = service._manifest_for_target(target)
|
||||
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
|
||||
skill_management_service.return_value.list_runtime_agent_skills.return_value = []
|
||||
manifest = service._manifest_for_target(target)
|
||||
|
||||
assert manifest["skills"]["items"][0]["is_missing"] is True # type: ignore[index]
|
||||
assert manifest["files"]["items"][0]["is_missing"] is True # type: ignore[index]
|
||||
@@ -873,6 +1022,79 @@ def test_config_asset_refs_require_file_id_unless_marked_missing() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_manifest_appends_published_workspace_skills() -> None:
|
||||
target = _target(
|
||||
kind=AgentConfigVersionKind.DRAFT,
|
||||
writable=False,
|
||||
soul=_soul(
|
||||
config_skills=[AgentConfigSkillRefConfig(name="alpha", description="Alpha skill", file_id="tool-file-1")]
|
||||
),
|
||||
)
|
||||
|
||||
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
|
||||
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
|
||||
{
|
||||
"id": "workspace-skill-id",
|
||||
"name": "beta",
|
||||
"file_id": "tool-file-2",
|
||||
"description": "Beta workspace skill",
|
||||
"size": 123,
|
||||
"hash": "sha256:beta",
|
||||
"mime_type": "application/zip",
|
||||
},
|
||||
{
|
||||
"id": "duplicate",
|
||||
"name": "alpha",
|
||||
"file_id": "tool-file-ignored",
|
||||
"description": "Duplicate workspace skill",
|
||||
"size": 456,
|
||||
"hash": "sha256:ignored",
|
||||
"mime_type": "application/zip",
|
||||
},
|
||||
]
|
||||
manifest = AgentConfigService._manifest_for_target(target)
|
||||
|
||||
assert [item["name"] for item in manifest["skills"]["items"]] == ["alpha", "beta"]
|
||||
assert manifest["skills"]["items"][1]["file_id"] == "tool-file-2"
|
||||
|
||||
|
||||
def test_list_skills_excludes_workspace_skill_bindings() -> None:
|
||||
target = _target(
|
||||
kind=AgentConfigVersionKind.DRAFT,
|
||||
writable=False,
|
||||
soul=_soul(
|
||||
config_skills=[AgentConfigSkillRefConfig(name="alpha", description="Alpha skill", file_id="tool-file-1")]
|
||||
),
|
||||
)
|
||||
|
||||
service = AgentConfigService()
|
||||
with (
|
||||
patch.object(service, "resolve_target", return_value=target),
|
||||
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
|
||||
):
|
||||
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
|
||||
{
|
||||
"id": "workspace-skill-id",
|
||||
"name": "beta",
|
||||
"file_id": "tool-file-2",
|
||||
"description": "Beta workspace skill",
|
||||
"size": 123,
|
||||
"hash": "sha256:beta",
|
||||
"mime_type": "application/zip",
|
||||
}
|
||||
]
|
||||
result = service.list_skills(
|
||||
tenant_id=target.tenant_id,
|
||||
agent_id=target.agent_id,
|
||||
config_version_id=target.version_id,
|
||||
config_version_kind=target.kind,
|
||||
user_id=None,
|
||||
)
|
||||
|
||||
assert [item["name"] for item in result["items"]] == ["alpha"]
|
||||
skill_management_service.return_value.list_runtime_agent_skills.assert_not_called()
|
||||
|
||||
|
||||
def test_preview_skill_file_returns_text_preview() -> None:
|
||||
service = AgentConfigService()
|
||||
target = _target(
|
||||
|
||||
@@ -726,6 +726,7 @@ class TestAgentAppType:
|
||||
assert backing_agent.archived_by == "account-2"
|
||||
assert backing_agent.archived_at is not None
|
||||
mock_db.session.delete.assert_called_once_with(app)
|
||||
mock_db.session.query.return_value.filter.return_value.delete.assert_called_once_with(synchronize_session=False)
|
||||
mock_workflow_retirement.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
agent_ids=["workflow-agent-1", "workflow-agent-2"],
|
||||
|
||||
@@ -2907,14 +2907,6 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx_a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -3059,14 +3051,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/marketplace/plugin-type-switch.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx_a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-auth/authorized/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@@ -3855,14 +3839,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx_a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/add-variable-popup-with-position.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 2
|
||||
|
||||
@@ -926,11 +926,15 @@ export const zSandboxListResponse = z.object({
|
||||
* Validated metadata extracted from a Skill package.
|
||||
*/
|
||||
export const zSkillManifest = z.object({
|
||||
description: z.string(),
|
||||
description: z.string().min(1).max(1024),
|
||||
entry_path: z.string(),
|
||||
files: z.array(z.string()),
|
||||
hash: z.string(),
|
||||
name: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
|
||||
@@ -1361,11 +1361,15 @@ export const zAgentLogMetaResponse = z.object({
|
||||
* Validated metadata extracted from a Skill package.
|
||||
*/
|
||||
export const zSkillManifest = z.object({
|
||||
description: z.string(),
|
||||
description: z.string().min(1).max(1024),
|
||||
entry_path: z.string(),
|
||||
files: z.array(z.string()),
|
||||
hash: z.string(),
|
||||
name: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export type TagBindingRemovePayload = {
|
||||
type: TagType
|
||||
}
|
||||
|
||||
export type TagType = 'app' | 'knowledge' | 'snippet'
|
||||
export type TagType = 'app' | 'knowledge' | 'skill' | 'snippet'
|
||||
|
||||
export type PostTagBindingsData = {
|
||||
body: TagBindingPayload
|
||||
|
||||
@@ -14,7 +14,7 @@ export const zSimpleResultResponse = z.object({
|
||||
*
|
||||
* Tag type
|
||||
*/
|
||||
export const zTagType = z.enum(['app', 'knowledge', 'snippet'])
|
||||
export const zTagType = z.enum(['app', 'knowledge', 'skill', 'snippet'])
|
||||
|
||||
/**
|
||||
* TagBindingPayload
|
||||
|
||||
@@ -22,14 +22,14 @@ export type TagUpdateRequestPayload = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type TagType = 'app' | 'knowledge' | 'snippet'
|
||||
export type TagType = 'app' | 'knowledge' | 'skill' | 'snippet'
|
||||
|
||||
export type GetTagsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
keyword?: string
|
||||
type?: '' | 'app' | 'knowledge' | 'snippet'
|
||||
type?: '' | 'app' | 'knowledge' | 'skill' | 'snippet'
|
||||
}
|
||||
url: '/tags'
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const zTagUpdateRequestPayload = z.object({
|
||||
*
|
||||
* Tag type
|
||||
*/
|
||||
export const zTagType = z.enum(['app', 'knowledge', 'snippet'])
|
||||
export const zTagType = z.enum(['app', 'knowledge', 'skill', 'snippet'])
|
||||
|
||||
/**
|
||||
* TagBasePayload
|
||||
@@ -41,7 +41,7 @@ export const zTagBasePayload = z.object({
|
||||
|
||||
export const zGetTagsQuery = z.object({
|
||||
keyword: z.string().optional(),
|
||||
type: z.enum(['', 'app', 'knowledge', 'snippet']).optional().default(''),
|
||||
type: z.enum(['', 'app', 'knowledge', 'skill', 'snippet']).optional().default(''),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,16 @@ export type AgentProviderListResponse = Array<{
|
||||
[key: string]: unknown
|
||||
}>
|
||||
|
||||
export type AgentSkillBindingsResponse = {
|
||||
agent_id: string
|
||||
data?: Array<AgentSkillBindingItemResponse>
|
||||
skill_ids?: Array<string>
|
||||
}
|
||||
|
||||
export type AgentSkillBindingsPayload = {
|
||||
skill_ids?: Array<string>
|
||||
}
|
||||
|
||||
export type SnippetPaginationResponse = {
|
||||
data: Array<SnippetListItemResponse>
|
||||
has_more: boolean
|
||||
@@ -642,6 +652,209 @@ export type WorkspaceAccessMatrix = {
|
||||
pagination?: Pagination | null
|
||||
}
|
||||
|
||||
export type SkillListResponse = {
|
||||
data?: Array<SkillResponse>
|
||||
has_more?: boolean
|
||||
limit?: number
|
||||
page?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
export type SkillCreatePayload = {
|
||||
description?: string
|
||||
display_name?: string | null
|
||||
icon?: string
|
||||
name?: string | null
|
||||
tags?: Array<string>
|
||||
}
|
||||
|
||||
export type SkillDetailResponse = {
|
||||
created_at: number
|
||||
created_by?: string | null
|
||||
created_by_name?: string | null
|
||||
description: string
|
||||
display_name: string
|
||||
files?: Array<SkillFileResponse>
|
||||
icon: string
|
||||
id: string
|
||||
latest_published_at?: number | null
|
||||
latest_published_version_id?: string | null
|
||||
latest_published_version_number?: number | null
|
||||
name: string
|
||||
name_manually_edited?: boolean
|
||||
reference_count?: number
|
||||
tags?: Array<string>
|
||||
updated_at: number
|
||||
updated_by?: string | null
|
||||
updated_by_name?: string | null
|
||||
visibility: string
|
||||
}
|
||||
|
||||
export type SkillFileUploadResponse = {
|
||||
hash: string
|
||||
id: string
|
||||
mime_type: string
|
||||
name: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export type SkillTagListResponse = {
|
||||
data?: Array<SkillTagResponse>
|
||||
}
|
||||
|
||||
export type SkillDeletePayload = {
|
||||
confirmation_name?: string | null
|
||||
}
|
||||
|
||||
export type SkillDeleteResponse = {
|
||||
deleted: boolean
|
||||
id: string
|
||||
}
|
||||
|
||||
export type SkillMetadataPayload = {
|
||||
display_name?: string | null
|
||||
expected_updated_at?: number | null
|
||||
icon?: string | null
|
||||
tags?: Array<string> | null
|
||||
}
|
||||
|
||||
export type SkillResponse = {
|
||||
created_at: number
|
||||
created_by?: string | null
|
||||
created_by_name?: string | null
|
||||
description: string
|
||||
display_name: string
|
||||
icon: string
|
||||
id: string
|
||||
latest_published_at?: number | null
|
||||
latest_published_version_id?: string | null
|
||||
latest_published_version_number?: number | null
|
||||
name: string
|
||||
name_manually_edited?: boolean
|
||||
reference_count?: number
|
||||
tags?: Array<string>
|
||||
updated_at: number
|
||||
updated_by?: string | null
|
||||
updated_by_name?: string | null
|
||||
visibility: string
|
||||
}
|
||||
|
||||
export type 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>
|
||||
history?: Array<SkillAssistHistoryMessagePayload>
|
||||
message: string
|
||||
model?: SkillAssistModelPayload | null
|
||||
target_path?: string | null
|
||||
}
|
||||
|
||||
export type SkillDraftFileOperationPayload = {
|
||||
content?: string | null
|
||||
expected_updated_at?: number | null
|
||||
hash?: string | null
|
||||
mime_type?: string | null
|
||||
operation: SkillDraftFileOperation
|
||||
path: string
|
||||
size?: number | null
|
||||
target_path?: string | null
|
||||
tool_file_id?: string | null
|
||||
}
|
||||
|
||||
export type SkillDraftTreePayload = {
|
||||
expected_updated_at?: number | null
|
||||
files?: Array<SkillDraftTreeItemPayload>
|
||||
}
|
||||
|
||||
export type SkillDraftFileCheckPayload = {
|
||||
files?: Array<SkillDraftFileCheckItemPayload>
|
||||
}
|
||||
|
||||
export type SkillFileCheckResponse = {
|
||||
data?: {
|
||||
[key: string]: SkillFileCheckItemResponse
|
||||
}
|
||||
}
|
||||
|
||||
export type SkillFilePreviewResponse = {
|
||||
content: string
|
||||
hash: string
|
||||
mime_type: string
|
||||
path: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export type SkillPublishPayload = {
|
||||
publish_note?: string
|
||||
version_name?: string | null
|
||||
}
|
||||
|
||||
export type SkillVersionResponse = {
|
||||
archive_size: number
|
||||
created_at: number
|
||||
hash_code: string
|
||||
id: string
|
||||
is_latest?: boolean
|
||||
publish_note: string
|
||||
published_by?: string | null
|
||||
published_by_name?: string | null
|
||||
skill_id: string
|
||||
version_name: string
|
||||
version_number: number
|
||||
}
|
||||
|
||||
export type SkillReferenceListResponse = {
|
||||
data?: Array<SkillReferenceResponse>
|
||||
}
|
||||
|
||||
export type SkillRestorePayload = {
|
||||
publish_note?: string
|
||||
version_id: string
|
||||
version_name?: string | null
|
||||
}
|
||||
|
||||
export type SkillVersionListResponse = {
|
||||
data?: Array<SkillVersionResponse>
|
||||
}
|
||||
|
||||
export type SkillVersionDeleteResponse = {
|
||||
deleted: boolean
|
||||
id: string
|
||||
latest_published_version_id?: string | null
|
||||
}
|
||||
|
||||
export type SkillVersionDetailResponse = {
|
||||
archive_size: number
|
||||
created_at: number
|
||||
files?: Array<SkillFileResponse>
|
||||
hash_code: string
|
||||
id: string
|
||||
is_latest?: boolean
|
||||
publish_note: string
|
||||
published_by?: string | null
|
||||
published_by_name?: string | null
|
||||
skill_id: string
|
||||
version_name: string
|
||||
version_number: number
|
||||
}
|
||||
|
||||
export type SkillVersionUpdatePayload = {
|
||||
publish_note?: string
|
||||
version_name?: string | null
|
||||
}
|
||||
|
||||
export type CurrentWorkspaceSummaryResponse = {
|
||||
credits: number | null
|
||||
id: string
|
||||
@@ -1057,6 +1270,21 @@ export type TenantListItemResponse = {
|
||||
status?: string | null
|
||||
}
|
||||
|
||||
export type AgentSkillBindingItemResponse = {
|
||||
description: string
|
||||
display_name: string
|
||||
file_count: number
|
||||
icon: string
|
||||
id: string
|
||||
latest_published_at?: number | null
|
||||
latest_published_version_id?: string | null
|
||||
name: string
|
||||
priority: number
|
||||
status: string
|
||||
tags?: Array<string>
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type SnippetListItemResponse = {
|
||||
author_name: string | null
|
||||
created_at: number
|
||||
@@ -1541,6 +1769,98 @@ export type AccessPolicyRole = {
|
||||
role_tag?: string
|
||||
}
|
||||
|
||||
export type SkillFileResponse = {
|
||||
content?: string | null
|
||||
hash?: string | null
|
||||
id?: string | null
|
||||
kind: string
|
||||
mime_type?: string | null
|
||||
path: string
|
||||
size?: number | null
|
||||
storage?: string | null
|
||||
tool_file_id?: string | null
|
||||
}
|
||||
|
||||
export type SkillTagResponse = {
|
||||
count: number
|
||||
tag: string
|
||||
}
|
||||
|
||||
export type SkillAssistAttachmentPayload = {
|
||||
mime_type?: string | null
|
||||
name: string
|
||||
size?: number | null
|
||||
tool_file_id: string
|
||||
}
|
||||
|
||||
export type SkillAssistHistoryMessagePayload = {
|
||||
content: string
|
||||
role: 'assistant' | 'user'
|
||||
suggested_display_name?: string | null
|
||||
suggested_name?: string | null
|
||||
}
|
||||
|
||||
export type SkillAssistModelPayload = {
|
||||
model: string
|
||||
model_settings?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
plugin_id?: string | null
|
||||
provider: string
|
||||
}
|
||||
export type SkillDraftFileOperation =
|
||||
| 'delete'
|
||||
| 'mkdir'
|
||||
| 'rename'
|
||||
| 'upsert_text'
|
||||
| 'upsert_tool_file'
|
||||
|
||||
export type SkillDraftTreeItemPayload = {
|
||||
content?: string | null
|
||||
hash?: string | null
|
||||
kind?: SkillFileKind
|
||||
mime_type?: string | null
|
||||
path: string
|
||||
size?: number | null
|
||||
storage?: SkillFileStorage | null
|
||||
tool_file_id?: string | null
|
||||
}
|
||||
|
||||
export type SkillDraftFileCheckItemPayload = {
|
||||
filename: string
|
||||
mime_type?: string | null
|
||||
path?: string | null
|
||||
size: number
|
||||
}
|
||||
|
||||
export type SkillFileCheckItemResponse = {
|
||||
errors?: Array<SkillFileCheckErrorResponse>
|
||||
extension: string
|
||||
filename: string
|
||||
mime_type: string
|
||||
path: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export type SkillReferenceResponse = {
|
||||
agent_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 TenantAccountRole = 'admin' | 'dataset_operator' | 'editor' | 'normal' | 'owner'
|
||||
|
||||
export type ToolLabel = {
|
||||
@@ -2048,6 +2368,15 @@ export type PermissionCatalogItem = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type SkillFileKind = 'directory' | 'file'
|
||||
|
||||
export type SkillFileStorage = 'text' | 'tool_file'
|
||||
|
||||
export type SkillFileCheckErrorResponse = {
|
||||
code: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export type ToolParameter = {
|
||||
auto_generate?: PluginParameterAutoGenerate | null
|
||||
default?:
|
||||
@@ -2529,6 +2858,38 @@ export type GetWorkspacesCurrentAgentProvidersResponses = {
|
||||
export type GetWorkspacesCurrentAgentProvidersResponse =
|
||||
GetWorkspacesCurrentAgentProvidersResponses[keyof GetWorkspacesCurrentAgentProvidersResponses]
|
||||
|
||||
export type GetWorkspacesCurrentAgentsByAgentIdSkillsData = {
|
||||
body?: never
|
||||
path: {
|
||||
agent_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/agents/{agent_id}/skills'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentAgentsByAgentIdSkillsResponses = {
|
||||
200: AgentSkillBindingsResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentAgentsByAgentIdSkillsResponse =
|
||||
GetWorkspacesCurrentAgentsByAgentIdSkillsResponses[keyof GetWorkspacesCurrentAgentsByAgentIdSkillsResponses]
|
||||
|
||||
export type PutWorkspacesCurrentAgentsByAgentIdSkillsData = {
|
||||
body: AgentSkillBindingsPayload
|
||||
path: {
|
||||
agent_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/agents/{agent_id}/skills'
|
||||
}
|
||||
|
||||
export type PutWorkspacesCurrentAgentsByAgentIdSkillsResponses = {
|
||||
200: AgentSkillBindingsResponse
|
||||
}
|
||||
|
||||
export type PutWorkspacesCurrentAgentsByAgentIdSkillsResponse =
|
||||
PutWorkspacesCurrentAgentsByAgentIdSkillsResponses[keyof PutWorkspacesCurrentAgentsByAgentIdSkillsResponses]
|
||||
|
||||
export type GetWorkspacesCurrentCustomizedSnippetsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -4728,6 +5089,385 @@ export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses = {
|
||||
export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse =
|
||||
GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses[keyof GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
keyword?: string
|
||||
limit?: number
|
||||
page?: number
|
||||
tag?: Array<string>
|
||||
}
|
||||
url: '/workspaces/current/skills'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsResponses = {
|
||||
200: SkillListResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsResponse =
|
||||
GetWorkspacesCurrentSkillsResponses[keyof GetWorkspacesCurrentSkillsResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsData = {
|
||||
body: SkillCreatePayload
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/skills'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsResponses = {
|
||||
201: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsResponse =
|
||||
PostWorkspacesCurrentSkillsResponses[keyof PostWorkspacesCurrentSkillsResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsFilesUploadData = {
|
||||
body: {
|
||||
file: Blob | File
|
||||
}
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/files/upload'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsFilesUploadResponses = {
|
||||
201: SkillFileUploadResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsFilesUploadResponse =
|
||||
PostWorkspacesCurrentSkillsFilesUploadResponses[keyof PostWorkspacesCurrentSkillsFilesUploadResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsImportData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/import'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsImportResponses = {
|
||||
201: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsImportResponse =
|
||||
PostWorkspacesCurrentSkillsImportResponses[keyof PostWorkspacesCurrentSkillsImportResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsTagsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/tags'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsTagsResponses = {
|
||||
200: SkillTagListResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsTagsResponse =
|
||||
GetWorkspacesCurrentSkillsTagsResponses[keyof GetWorkspacesCurrentSkillsTagsResponses]
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdData = {
|
||||
body: SkillDeletePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}'
|
||||
}
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdResponses = {
|
||||
200: SkillDeleteResponse
|
||||
}
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdResponse =
|
||||
DeleteWorkspacesCurrentSkillsBySkillIdResponses[keyof DeleteWorkspacesCurrentSkillsBySkillIdResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdResponses = {
|
||||
200: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdResponses[keyof GetWorkspacesCurrentSkillsBySkillIdResponses]
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdData = {
|
||||
body: SkillMetadataPayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}'
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdResponses = {
|
||||
200: SkillResponse
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdResponse =
|
||||
PatchWorkspacesCurrentSkillsBySkillIdResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesData = {
|
||||
body: SkillAssistMessagePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/assist/messages'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses = {
|
||||
200: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponse =
|
||||
PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses[keyof PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/duplicate'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses = {
|
||||
201: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateResponse =
|
||||
PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses[keyof PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdExportData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/export'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdExportResponses = {
|
||||
200: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdExportResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdExportResponses[keyof GetWorkspacesCurrentSkillsBySkillIdExportResponses]
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdFilesData = {
|
||||
body: SkillDraftFileOperationPayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/files'
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdFilesResponses = {
|
||||
200: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdFilesResponse =
|
||||
PatchWorkspacesCurrentSkillsBySkillIdFilesResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdFilesResponses]
|
||||
|
||||
export type PutWorkspacesCurrentSkillsBySkillIdFilesData = {
|
||||
body: SkillDraftTreePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/files'
|
||||
}
|
||||
|
||||
export type PutWorkspacesCurrentSkillsBySkillIdFilesResponses = {
|
||||
200: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PutWorkspacesCurrentSkillsBySkillIdFilesResponse =
|
||||
PutWorkspacesCurrentSkillsBySkillIdFilesResponses[keyof PutWorkspacesCurrentSkillsBySkillIdFilesResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdFilesCheckData = {
|
||||
body: SkillDraftFileCheckPayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/files/check'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponses = {
|
||||
200: SkillFileCheckResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponse =
|
||||
PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponses[keyof PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query: {
|
||||
download?: string
|
||||
path: string
|
||||
version_id?: string
|
||||
}
|
||||
url: '/workspaces/current/skills/{skill_id}/files/content'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses = {
|
||||
200: BinaryFileResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses[keyof GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query: {
|
||||
path: string
|
||||
version_id?: string
|
||||
}
|
||||
url: '/workspaces/current/skills/{skill_id}/files/preview'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses = {
|
||||
200: SkillFilePreviewResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses[keyof GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdPublishData = {
|
||||
body: SkillPublishPayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/publish'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdPublishResponses = {
|
||||
200: SkillVersionResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdPublishResponse =
|
||||
PostWorkspacesCurrentSkillsBySkillIdPublishResponses[keyof PostWorkspacesCurrentSkillsBySkillIdPublishResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdReferencesData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/references'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdReferencesResponses = {
|
||||
200: SkillReferenceListResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdReferencesResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdReferencesResponses[keyof GetWorkspacesCurrentSkillsBySkillIdReferencesResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdRestoreData = {
|
||||
body: SkillRestorePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/restore'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdRestoreResponses = {
|
||||
200: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdRestoreResponse =
|
||||
PostWorkspacesCurrentSkillsBySkillIdRestoreResponses[keyof PostWorkspacesCurrentSkillsBySkillIdRestoreResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/versions'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsResponses = {
|
||||
200: SkillVersionListResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdVersionsResponses[keyof GetWorkspacesCurrentSkillsBySkillIdVersionsResponses]
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
version_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
|
||||
}
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
|
||||
200: SkillVersionDeleteResponse
|
||||
}
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
version_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
|
||||
200: SkillVersionDetailResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
|
||||
body: SkillVersionUpdatePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
version_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
|
||||
200: SkillVersionResponse
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSummaryData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
@@ -12,6 +12,13 @@ export const zAgentProviderResponse = z.record(z.string(), z.unknown())
|
||||
*/
|
||||
export const zAgentProviderListResponse = z.array(z.record(z.string(), z.unknown()))
|
||||
|
||||
/**
|
||||
* AgentSkillBindingsPayload
|
||||
*/
|
||||
export const zAgentSkillBindingsPayload = z.object({
|
||||
skill_ids: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SnippetImportPayload
|
||||
*
|
||||
@@ -458,6 +465,157 @@ export const zReplaceBindingsRequest = z.object({
|
||||
role_ids: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillCreatePayload
|
||||
*/
|
||||
export const zSkillCreatePayload = z.object({
|
||||
description: z.string().optional().default(''),
|
||||
display_name: z.string().nullish(),
|
||||
icon: z.string().optional().default('📄'),
|
||||
name: z.string().nullish(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFileUploadResponse
|
||||
*/
|
||||
export const zSkillFileUploadResponse = z.object({
|
||||
hash: z.string(),
|
||||
id: z.string(),
|
||||
mime_type: z.string(),
|
||||
name: z.string(),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDeletePayload
|
||||
*/
|
||||
export const zSkillDeletePayload = z.object({
|
||||
confirmation_name: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDeleteResponse
|
||||
*/
|
||||
export const zSkillDeleteResponse = z.object({
|
||||
deleted: z.boolean(),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillMetadataPayload
|
||||
*/
|
||||
export const zSkillMetadataPayload = z.object({
|
||||
display_name: z.string().nullish(),
|
||||
expected_updated_at: z.int().nullish(),
|
||||
icon: z.string().nullish(),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillResponse
|
||||
*/
|
||||
export const zSkillResponse = z.object({
|
||||
created_at: z.int(),
|
||||
created_by: z.string().nullish(),
|
||||
created_by_name: z.string().nullish(),
|
||||
description: z.string(),
|
||||
display_name: z.string(),
|
||||
icon: z.string(),
|
||||
id: z.string(),
|
||||
latest_published_at: z.int().nullish(),
|
||||
latest_published_version_id: z.string().nullish(),
|
||||
latest_published_version_number: z.int().nullish(),
|
||||
name: z.string(),
|
||||
name_manually_edited: z.boolean().optional().default(false),
|
||||
reference_count: z.int().optional().default(0),
|
||||
tags: z.array(z.string()).optional(),
|
||||
updated_at: z.int(),
|
||||
updated_by: z.string().nullish(),
|
||||
updated_by_name: z.string().nullish(),
|
||||
visibility: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillListResponse
|
||||
*/
|
||||
export const zSkillListResponse = z.object({
|
||||
data: z.array(zSkillResponse).optional(),
|
||||
has_more: z.boolean().optional().default(false),
|
||||
limit: z.int().optional().default(20),
|
||||
page: z.int().optional().default(1),
|
||||
total: z.int().optional().default(0),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFilePreviewResponse
|
||||
*/
|
||||
export const zSkillFilePreviewResponse = z.object({
|
||||
content: z.string(),
|
||||
hash: z.string(),
|
||||
mime_type: z.string(),
|
||||
path: z.string(),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillPublishPayload
|
||||
*/
|
||||
export const zSkillPublishPayload = z.object({
|
||||
publish_note: z.string().max(1024).optional().default(''),
|
||||
version_name: z.string().max(128).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionResponse
|
||||
*/
|
||||
export const zSkillVersionResponse = z.object({
|
||||
archive_size: z.int(),
|
||||
created_at: z.int(),
|
||||
hash_code: z.string(),
|
||||
id: z.string(),
|
||||
is_latest: z.boolean().optional().default(false),
|
||||
publish_note: z.string(),
|
||||
published_by: z.string().nullish(),
|
||||
published_by_name: z.string().nullish(),
|
||||
skill_id: z.string(),
|
||||
version_name: z.string(),
|
||||
version_number: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillRestorePayload
|
||||
*/
|
||||
export const zSkillRestorePayload = z.object({
|
||||
publish_note: z.string().max(1024).optional().default(''),
|
||||
version_id: z.string(),
|
||||
version_name: z.string().max(128).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionListResponse
|
||||
*/
|
||||
export const zSkillVersionListResponse = z.object({
|
||||
data: z.array(zSkillVersionResponse).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionDeleteResponse
|
||||
*/
|
||||
export const zSkillVersionDeleteResponse = z.object({
|
||||
deleted: z.boolean(),
|
||||
id: z.string(),
|
||||
latest_published_version_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionUpdatePayload
|
||||
*/
|
||||
export const zSkillVersionUpdatePayload = z.object({
|
||||
publish_note: z.string().max(1024).optional().default(''),
|
||||
version_name: z.string().max(128).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ApiToolProviderDeletePayload
|
||||
*/
|
||||
@@ -643,6 +801,33 @@ export const zTenantListResponse = z.object({
|
||||
workspaces: z.array(zTenantListItemResponse),
|
||||
})
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
@@ -1231,6 +1416,194 @@ export const zWorkspaceAccessMatrix = z.object({
|
||||
pagination: zPagination.nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFileResponse
|
||||
*/
|
||||
export const zSkillFileResponse = z.object({
|
||||
content: z.string().nullish(),
|
||||
hash: z.string().nullish(),
|
||||
id: z.string().nullish(),
|
||||
kind: z.string(),
|
||||
mime_type: z.string().nullish(),
|
||||
path: z.string(),
|
||||
size: z.int().nullish(),
|
||||
storage: z.string().nullish(),
|
||||
tool_file_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDetailResponse
|
||||
*/
|
||||
export const zSkillDetailResponse = z.object({
|
||||
created_at: z.int(),
|
||||
created_by: z.string().nullish(),
|
||||
created_by_name: z.string().nullish(),
|
||||
description: z.string(),
|
||||
display_name: z.string(),
|
||||
files: z.array(zSkillFileResponse).optional(),
|
||||
icon: z.string(),
|
||||
id: z.string(),
|
||||
latest_published_at: z.int().nullish(),
|
||||
latest_published_version_id: z.string().nullish(),
|
||||
latest_published_version_number: z.int().nullish(),
|
||||
name: z.string(),
|
||||
name_manually_edited: z.boolean().optional().default(false),
|
||||
reference_count: z.int().optional().default(0),
|
||||
tags: z.array(z.string()).optional(),
|
||||
updated_at: z.int(),
|
||||
updated_by: z.string().nullish(),
|
||||
updated_by_name: z.string().nullish(),
|
||||
visibility: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionDetailResponse
|
||||
*/
|
||||
export const zSkillVersionDetailResponse = z.object({
|
||||
archive_size: z.int(),
|
||||
created_at: z.int(),
|
||||
files: z.array(zSkillFileResponse).optional(),
|
||||
hash_code: z.string(),
|
||||
id: z.string(),
|
||||
is_latest: z.boolean().optional().default(false),
|
||||
publish_note: z.string(),
|
||||
published_by: z.string().nullish(),
|
||||
published_by_name: z.string().nullish(),
|
||||
skill_id: z.string(),
|
||||
version_name: z.string(),
|
||||
version_number: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillTagResponse
|
||||
*/
|
||||
export const zSkillTagResponse = z.object({
|
||||
count: z.int(),
|
||||
tag: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillTagListResponse
|
||||
*/
|
||||
export const zSkillTagListResponse = z.object({
|
||||
data: z.array(zSkillTagResponse).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillAssistAttachmentPayload
|
||||
*/
|
||||
export const zSkillAssistAttachmentPayload = z.object({
|
||||
mime_type: z.string().min(1).max(255).nullish(),
|
||||
name: z.string().min(1).max(255),
|
||||
size: z.int().gte(0).nullish(),
|
||||
tool_file_id: z.string().min(1),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillAssistHistoryMessagePayload
|
||||
*/
|
||||
export const zSkillAssistHistoryMessagePayload = z.object({
|
||||
content: z.string().min(1).max(8000),
|
||||
role: z.enum(['assistant', 'user']),
|
||||
suggested_display_name: z.string().max(128).nullish(),
|
||||
suggested_name: z.string().max(128).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillAssistModelPayload
|
||||
*/
|
||||
export const zSkillAssistModelPayload = z.object({
|
||||
model: z.string().min(1).max(255),
|
||||
model_settings: z.record(z.string(), z.unknown()).nullish(),
|
||||
plugin_id: z.string().min(1).max(255).nullish(),
|
||||
provider: z.string().min(1).max(255),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillAssistMessagePayload
|
||||
*
|
||||
* One user message and optional uploaded context for the Skill Authoring assistant.
|
||||
*/
|
||||
export const zSkillAssistMessagePayload = z.object({
|
||||
attachments: z.array(zSkillAssistAttachmentPayload).max(10).optional(),
|
||||
history: z.array(zSkillAssistHistoryMessagePayload).max(20).optional(),
|
||||
message: z.string().min(1).max(8000),
|
||||
model: zSkillAssistModelPayload.nullish(),
|
||||
target_path: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDraftFileOperation
|
||||
*/
|
||||
export const zSkillDraftFileOperation = z.enum([
|
||||
'delete',
|
||||
'mkdir',
|
||||
'rename',
|
||||
'upsert_text',
|
||||
'upsert_tool_file',
|
||||
])
|
||||
|
||||
/**
|
||||
* SkillDraftFileOperationPayload
|
||||
*/
|
||||
export const zSkillDraftFileOperationPayload = z.object({
|
||||
content: z.string().nullish(),
|
||||
expected_updated_at: z.int().nullish(),
|
||||
hash: z.string().nullish(),
|
||||
mime_type: z.string().nullish(),
|
||||
operation: zSkillDraftFileOperation,
|
||||
path: z.string(),
|
||||
size: z.int().gte(0).nullish(),
|
||||
target_path: z.string().nullish(),
|
||||
tool_file_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDraftFileCheckItemPayload
|
||||
*/
|
||||
export const zSkillDraftFileCheckItemPayload = z.object({
|
||||
filename: z.string().min(1).max(255),
|
||||
mime_type: z.string().max(255).nullish(),
|
||||
path: z.string().nullish(),
|
||||
size: z.int().gte(0),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDraftFileCheckPayload
|
||||
*/
|
||||
export const zSkillDraftFileCheckPayload = z.object({
|
||||
files: z.array(zSkillDraftFileCheckItemPayload).max(100).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillReferenceResponse
|
||||
*/
|
||||
export const zSkillReferenceResponse = z.object({
|
||||
agent_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(),
|
||||
})
|
||||
|
||||
/**
|
||||
* TenantAccountRole
|
||||
*/
|
||||
@@ -2193,6 +2566,69 @@ export const zPermissionCatalogResponse = z.object({
|
||||
groups: z.array(zPermissionCatalogGroup).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFileKind
|
||||
*
|
||||
* Draft file entry kind.
|
||||
*/
|
||||
export const zSkillFileKind = z.enum(['directory', 'file'])
|
||||
|
||||
/**
|
||||
* SkillFileStorage
|
||||
*
|
||||
* How a draft file's content is stored.
|
||||
*/
|
||||
export const zSkillFileStorage = z.enum(['text', 'tool_file'])
|
||||
|
||||
/**
|
||||
* SkillDraftTreeItemPayload
|
||||
*/
|
||||
export const zSkillDraftTreeItemPayload = z.object({
|
||||
content: z.string().nullish(),
|
||||
hash: z.string().nullish(),
|
||||
kind: zSkillFileKind.optional().default('file'),
|
||||
mime_type: z.string().nullish(),
|
||||
path: z.string(),
|
||||
size: z.int().gte(0).nullish(),
|
||||
storage: zSkillFileStorage.nullish(),
|
||||
tool_file_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDraftTreePayload
|
||||
*/
|
||||
export const zSkillDraftTreePayload = z.object({
|
||||
expected_updated_at: z.int().nullish(),
|
||||
files: z.array(zSkillDraftTreeItemPayload).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFileCheckErrorResponse
|
||||
*/
|
||||
export const zSkillFileCheckErrorResponse = z.object({
|
||||
code: z.string(),
|
||||
message: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFileCheckItemResponse
|
||||
*/
|
||||
export const zSkillFileCheckItemResponse = z.object({
|
||||
errors: z.array(zSkillFileCheckErrorResponse).optional(),
|
||||
extension: z.string(),
|
||||
filename: z.string(),
|
||||
mime_type: z.string(),
|
||||
path: z.string(),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFileCheckResponse
|
||||
*/
|
||||
export const zSkillFileCheckResponse = z.object({
|
||||
data: z.record(z.string(), zSkillFileCheckItemResponse).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Option
|
||||
*/
|
||||
@@ -3513,6 +3949,26 @@ export const zGetWorkspacesCurrentAgentProviderByProviderNameResponse = zAgentPr
|
||||
*/
|
||||
export const zGetWorkspacesCurrentAgentProvidersResponse = zAgentProviderListResponse
|
||||
|
||||
export const zGetWorkspacesCurrentAgentsByAgentIdSkillsPath = z.object({
|
||||
agent_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Agent Skill bindings
|
||||
*/
|
||||
export const zGetWorkspacesCurrentAgentsByAgentIdSkillsResponse = zAgentSkillBindingsResponse
|
||||
|
||||
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsBody = zAgentSkillBindingsPayload
|
||||
|
||||
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsPath = z.object({
|
||||
agent_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Agent Skill bindings replaced
|
||||
*/
|
||||
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsResponse = zAgentSkillBindingsResponse
|
||||
|
||||
export const zGetWorkspacesCurrentCustomizedSnippetsQuery = z.object({
|
||||
creators: z.array(z.string()).optional(),
|
||||
is_published: z.boolean().optional(),
|
||||
@@ -4808,6 +5264,245 @@ export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdR
|
||||
*/
|
||||
export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse = zWorkspaceAccessMatrix
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsQuery = z.object({
|
||||
keyword: z.string().optional(),
|
||||
limit: z.int().gte(1).lte(100).optional().default(20),
|
||||
page: z.int().gte(1).lte(99999).optional().default(1),
|
||||
tag: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Workspace skills
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsResponse = zSkillListResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBody = zSkillCreatePayload
|
||||
|
||||
/**
|
||||
* Skill created
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsResponse = zSkillDetailResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsFilesUploadBody = z.object({
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill draft file uploaded
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsFilesUploadResponse = zSkillFileUploadResponse
|
||||
|
||||
/**
|
||||
* Skill imported
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsImportResponse = zSkillDetailResponse
|
||||
|
||||
/**
|
||||
* Workspace Skill tags
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsTagsResponse = zSkillTagListResponse
|
||||
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdBody = zSkillDeletePayload
|
||||
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill deleted
|
||||
*/
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdResponse = zSkillDeleteResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill detail
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdResponse = zSkillDetailResponse
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdBody = zSkillMetadataPayload
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill updated
|
||||
*/
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdResponse = zSkillResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesBody = zSkillAssistMessagePayload
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill Authoring assistant event stream
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponse = z.record(
|
||||
z.string(),
|
||||
z.unknown(),
|
||||
)
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdDuplicatePath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill duplicated
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdDuplicateResponse = zSkillDetailResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdExportPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Published Skill zip archive
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdExportResponse = z.record(z.string(), z.unknown())
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesBody = zSkillDraftFileOperationPayload
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Draft file operation applied
|
||||
*/
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesResponse = zSkillDetailResponse
|
||||
|
||||
export const zPutWorkspacesCurrentSkillsBySkillIdFilesBody = zSkillDraftTreePayload
|
||||
|
||||
export const zPutWorkspacesCurrentSkillsBySkillIdFilesPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Draft files replaced
|
||||
*/
|
||||
export const zPutWorkspacesCurrentSkillsBySkillIdFilesResponse = zSkillDetailResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdFilesCheckBody = zSkillDraftFileCheckPayload
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdFilesCheckPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Draft files checked
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdFilesCheckResponse = zSkillFileCheckResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentQuery = z.object({
|
||||
download: z.string().optional(),
|
||||
path: z.string(),
|
||||
version_id: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill file content
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentResponse = zBinaryFileResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewQuery = z.object({
|
||||
path: z.string(),
|
||||
version_id: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill file text preview
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse = zSkillFilePreviewResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdPublishBody = zSkillPublishPayload
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdPublishPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill published
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdPublishResponse = zSkillVersionResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdReferencesPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill references
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdReferencesResponse = zSkillReferenceListResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdRestoreBody = zSkillRestorePayload
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdRestorePath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill version restored to draft
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdRestoreResponse = zSkillDetailResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill versions
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsResponse = zSkillVersionListResponse
|
||||
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
version_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill version deleted
|
||||
*/
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
zSkillVersionDeleteResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
version_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill version detail
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
zSkillVersionDetailResponse
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdBody =
|
||||
zSkillVersionUpdatePayload
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
version_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill version updated
|
||||
*/
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
zSkillVersionResponse
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
|
||||
@@ -156,6 +156,21 @@ export type TemplateDetailResponse = {
|
||||
|
||||
export type DownloadPluginResponse = Blob
|
||||
|
||||
const bannerListContract = base
|
||||
.route({
|
||||
path: '/banners',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(
|
||||
type<{
|
||||
query: {
|
||||
page: 'plugins'
|
||||
language: string
|
||||
}
|
||||
}>(),
|
||||
)
|
||||
.output(type<unknown>())
|
||||
|
||||
const collectionsContract = base
|
||||
.route({
|
||||
path: '/collections',
|
||||
@@ -229,6 +244,9 @@ const downloadPluginContract = base
|
||||
.output(type<DownloadPluginResponse>())
|
||||
|
||||
export const marketplaceRouterContract = {
|
||||
banners: {
|
||||
list: bannerListContract,
|
||||
},
|
||||
collections: collectionsContract,
|
||||
collectionPlugins: collectionPluginsContract,
|
||||
searchAdvanced: searchAdvancedContract,
|
||||
|
||||
@@ -18,6 +18,7 @@ The current Flask-RESTX generator still emits these response entries under `appl
|
||||
| service | GET | `/files/{file_id}/preview` | Original file MIME type, optionally attachment | `BinaryFileResponse` |
|
||||
| console | GET | `/workspaces/current/plugin/icon` | Plugin asset MIME type | `BinaryFileResponse` |
|
||||
| console | GET | `/workspaces/current/plugin/asset` | `application/octet-stream` | `BinaryFileResponse` |
|
||||
| console | GET | `/workspaces/current/skills/{skill_id}/files/content` | Skill file MIME type, optionally attachment | `BinaryFileResponse` |
|
||||
| console | GET | `/workspaces/current/tool-provider/builtin/{provider}/icon` | Tool icon MIME type | `BinaryFileResponse` |
|
||||
| console | GET | `/workspaces/current/trigger-provider/{provider}/icon` | Trigger icon response | `BinaryFileResponse` |
|
||||
| console | GET | `/workspaces/{tenant_id}/model-providers/{provider}/{icon_type}/{lang}` | Model provider icon MIME type | `BinaryFileResponse` |
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="translate(-5.75 -7.25)" d="M15.5 10.2912C18.875 10.6643 21.5 13.5256 21.5 17V23.75H8V17C8 13.5256 10.625 10.6643 14 10.2912V8H15.5V10.2912ZM14.75 20.75C16.821 20.75 18.5 19.071 18.5 17C18.5 14.9289 16.821 13.25 14.75 13.25C12.6789 13.25 11 14.9289 11 17C11 19.071 12.6789 20.75 14.75 20.75ZM14.75 19.25C13.5073 19.25 12.5 18.2427 12.5 17C12.5 15.7573 13.5073 14.75 14.75 14.75C15.9927 14.75 17 15.7573 17 17C17 18.2427 15.9927 19.25 14.75 19.25ZM14.75 17.75C15.1642 17.75 15.5 17.4142 15.5 17C15.5 16.5858 15.1642 16.25 14.75 16.25C14.3358 16.25 14 16.5858 14 17C14 17.4142 14.3358 17.75 14.75 17.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 746 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.87891 2.81952C13.2539 3.1926 15.8789 6.05391 15.8789 9.52832V16.2783H2.37891V9.52832C2.37891 6.05391 5.00394 3.1926 8.37891 2.81952V0.52832H9.87891V2.81952ZM14.3789 14.7783V9.52832C14.3789 6.62883 12.0284 4.27832 9.12891 4.27832C6.22941 4.27832 3.87891 6.62883 3.87891 9.52832V14.7783H14.3789ZM9.12891 13.2783C7.05784 13.2783 5.37891 11.5994 5.37891 9.52832C5.37891 7.45727 7.05784 5.77832 9.12891 5.77832C11.2 5.77832 12.8789 7.45727 12.8789 9.52832C12.8789 11.5994 11.2 13.2783 9.12891 13.2783ZM9.12891 11.7783C10.3716 11.7783 11.3789 10.771 11.3789 9.52832C11.3789 8.28565 10.3716 7.27832 9.12891 7.27832C7.88623 7.27832 6.87891 8.28565 6.87891 9.52832C6.87891 10.771 7.88623 11.7783 9.12891 11.7783ZM9.12891 10.2783C8.71468 10.2783 8.37891 9.94254 8.37891 9.52832C8.37891 9.1141 8.71468 8.77832 9.12891 8.77832C9.54313 8.77832 9.87891 9.1141 9.87891 9.52832C9.87891 9.94254 9.54313 10.2783 9.12891 10.2783Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="translate(-5.75 -6.3)" d="M21.5 21.2999C21.5 21.7141 21.1642 22.0499 20.75 22.0499H8.75C8.33579 22.0499 8 21.7141 8 21.2999V13.4167C8 13.1852 8.10685 12.9667 8.28954 12.8247L14.2896 8.15798C14.5604 7.94734 14.9396 7.94734 15.2104 8.15798L21.2104 12.8247C21.3931 12.9667 21.5 13.1852 21.5 13.4167V21.2999ZM14 16.0499V20.5499H15.5V16.0499H14Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 486 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.75 14.2501H14.25V7.48367L9 3.40034L3.75 7.48367V14.2501H8.25V9.75006H9.75V14.2501ZM15.75 15.0001C15.75 15.4143 15.4142 15.7501 15 15.7501H3C2.58579 15.7501 2.25 15.4143 2.25 15.0001V7.11686C2.25 6.88542 2.35685 6.66694 2.53954 6.52485L8.53957 1.85818C8.8104 1.64753 9.1896 1.64753 9.46043 1.85818L15.4604 6.52485C15.6431 6.66694 15.75 6.88542 15.75 7.11686V15.0001Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 506 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-6.39355 -5.66187)" fill="currentColor">
|
||||
<path d="M22.25 11.75C22.6642 11.75 23 12.0858 23 12.5V20C23 20.4142 22.6642 20.75 22.25 20.75H8.75C8.33579 20.75 8 20.4142 8 20V12.5C8 12.0858 8.33579 11.75 8.75 11.75H22.25Z"/>
|
||||
<path d="M14 8C14.4142 8 14.75 8.33579 14.75 8.75V10.25H9.5V8.75C9.5 8.33579 9.83579 8 10.25 8H14Z"/>
|
||||
<path d="M20.75 8C21.1642 8 21.5 8.33579 21.5 8.75V10.25H16.25V8.75C16.25 8.33579 16.5858 8 17 8H20.75Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 577 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 2.25C7.91421 2.25 8.25 2.58579 8.25 3V5.25H9.75V3C9.75 2.58579 10.0858 2.25 10.5 2.25H14.25C14.6642 2.25 15 2.58579 15 3V5.25H15.75C16.1642 5.25 16.5 5.58579 16.5 6V14.25C16.5 14.6642 16.1642 15 15.75 15H2.25C1.83579 15 1.5 14.6642 1.5 14.25V6C1.5 5.58579 1.83579 5.25 2.25 5.25H3V3C3 2.58579 3.33579 2.25 3.75 2.25H7.5ZM3 13.5H15V6.75H3V13.5ZM11.25 5.25H13.5V3.75H11.25V5.25ZM4.5 5.25H6.75V3.75H4.5V5.25Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 588 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="translate(-6 -6.98706)" d="M21.5 20H10.25C9.83579 20 9.5 20.3358 9.5 20.75C9.5 21.1642 9.83579 21.5 10.25 21.5H21.5V23H10.25C9.00736 23 8 21.9927 8 20.75V9.5C8 8.67157 8.67157 8 9.5 8H21.5V20ZM17.75 13.25V11.75H11.75V13.25H17.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 375 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.75 13.5H4.5C4.08579 13.5 3.75 13.8358 3.75 14.25C3.75 14.6642 4.08579 15 4.5 15H15.75V16.5H4.5C3.25736 16.5 2.25 15.4927 2.25 14.25V3C2.25 2.17157 2.92157 1.5 3.75 1.5H15.75V13.5ZM3.75 12.0375C3.87117 12.0129 3.99658 12 4.125 12H14.25V3H3.75V12.0375ZM12 6.75H6V5.25H12V6.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 416 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="translate(-7.25 -6.875)" d="M23.75 21.5V23H8.75V21.5H9.5V16.4317C8.59551 15.8263 8 14.7951 8 13.625C8 13.0046 8.16832 12.4072 8.47491 11.8981L10.509 8.375C10.6429 8.14295 10.8905 8 11.1585 8H21.3415C21.6094 8 21.8571 8.14295 21.991 8.375L24.0181 11.8863C24.3317 12.4072 24.5 13.0046 24.5 13.625C24.5 14.7951 23.9045 15.8263 23 16.4317V21.5H23.75ZM11.5915 9.5L9.76698 12.6599C9.59307 12.9488 9.5 13.2792 9.5 13.625C9.5 14.6605 10.3395 15.5 11.375 15.5C12.1482 15.5 12.8335 15.0277 13.1163 14.3221C13.3681 13.6942 14.257 13.6942 14.5087 14.3221C14.7915 15.0277 15.4767 15.5 16.25 15.5C17.0232 15.5 17.7085 15.0277 17.9914 14.3221C18.2431 13.6942 19.1319 13.6942 19.3836 14.3221C19.6665 15.0277 20.3518 15.5 21.125 15.5C22.1605 15.5 23 14.6605 23 13.625C23 13.2792 22.9069 12.9488 22.726 12.6481L20.9085 9.5H11.5915Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 959 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.75 9.93165V15H16.5V16.5H1.5V15H2.25V9.93165C1.34551 9.32625 0.75 8.29515 0.75 7.125C0.75 6.50464 0.918322 5.90716 1.22491 5.3981L3.25898 1.875C3.39295 1.64295 3.64054 1.5 3.90849 1.5H14.0915C14.3594 1.5 14.6071 1.64295 14.741 1.875L16.7681 5.38629C17.0817 5.90716 17.25 6.50464 17.25 7.125C17.25 8.29515 16.6545 9.32625 15.75 9.93165ZM14.25 10.4794C14.1269 10.493 14.0017 10.5 13.875 10.5C12.9307 10.5 12.0592 10.1085 11.4375 9.4599C10.8158 10.1085 9.94432 10.5 9 10.5C8.05568 10.5 7.18418 10.1085 6.5625 9.4599C5.94082 10.1085 5.06933 10.5 4.125 10.5C3.99825 10.5 3.87313 10.493 3.75 10.4794V15H14.25V10.4794ZM4.34149 3L2.51698 6.15991C2.34307 6.44882 2.25 6.77917 2.25 7.125C2.25 8.16052 3.08947 9 4.125 9C4.89822 9 5.5835 8.52772 5.86634 7.82212C6.11805 7.19417 7.00695 7.19417 7.25866 7.82212C7.54148 8.52772 8.22675 9 9 9C9.77325 9 10.4585 8.52772 10.7414 7.82212C10.9931 7.19417 11.8819 7.19417 12.1336 7.82212C12.4165 8.52772 13.1018 9 13.875 9C14.9105 9 15.75 8.16052 15.75 7.125C15.75 6.77917 15.6569 6.44882 15.476 6.1481L13.6585 3H4.34149Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="translate(-6.2 -6.70433)" d="M8 18.6501C8 19.1406 8 19.3859 8.07199 19.6049C8.13567 19.7987 8.2398 19.9767 8.37747 20.1273C8.53307 20.2974 8.74686 20.4177 9.17444 20.6581L13.76 23.2375V21.0775L8 17.8819V18.6501ZM15.2 21.0775V23.2642L21.422 18.8858C21.78 18.6339 21.9591 18.5079 22.0885 18.3446C22.203 18.2001 22.2888 18.035 22.341 17.8581C22.4 17.6583 22.4 17.4394 22.4 17.0015V16.0375L15.2 21.0775ZM8 16.1582L13.76 19.2889V17.1175L8 13.9066V16.1582ZM15.2 17.1175V19.2775L22.4 14.2375V12.0775L15.2 17.1175ZM8.72 12.4375L14.48 15.6775L21.68 10.9975L17.1357 8.44137C16.6939 8.19286 16.473 8.0686 16.2398 8.02477C16.0335 7.98601 15.8213 7.99281 15.618 8.04468C15.388 8.10333 15.1755 8.24145 14.7505 8.51771L8.72 12.4375Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 863 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.8922 4.70983L10.3274 2.14208C10.1065 2.01781 9.99601 1.95568 9.87944 1.93377C9.77627 1.91439 9.67021 1.91779 9.56848 1.94372C9.45356 1.97304 9.34729 2.04211 9.13475 2.18024L3.04371 6.13944C2.85218 6.26393 2.75642 6.32617 2.68704 6.4092C2.62562 6.4827 2.57949 6.56771 2.55133 6.65926C2.51953 6.76268 2.51953 6.8769 2.51953 7.10533V12.2864C2.51953 12.5317 2.51953 12.6544 2.55552 12.7639C2.58736 12.8607 2.63943 12.9498 2.70826 13.025C2.78607 13.1101 2.89296 13.1702 3.10675 13.2905L7.67163 15.8582C7.8926 15.9825 8.00305 16.0446 8.11962 16.0665C8.22279 16.0859 8.32885 16.0825 8.43059 16.0566C8.5455 16.0273 8.65177 15.9582 8.86424 15.8201L14.9553 11.8609C15.1468 11.7364 15.2426 11.6742 15.312 11.5911C15.3734 11.5176 15.4195 11.4326 15.4476 11.341C15.4795 11.2376 15.4795 11.1234 15.4795 10.895V5.71389C15.4795 5.4686 15.4795 5.34596 15.4435 5.23644C15.4116 5.13955 15.3596 5.05054 15.2907 4.97528C15.2129 4.89022 15.1061 4.8301 14.8922 4.70983Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<path d="M15.1199 5.27417L8.27988 9.72024L2.87988 6.68271" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<path d="M15.4795 8.28003L8.27953 12.96L2.51953 9.72003" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.28027 15.48V9.71997" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-5.75 -6.5)" fill="currentColor">
|
||||
<path d="M11.1062 16.269C10.1983 16.3989 9.50011 17.1812 9.5 18.125C9.50012 19.1604 10.3395 20 11.375 20H17.1304C17.4395 19.1265 18.2706 18.5 19.25 18.5C20.4926 18.5 21.5 19.5074 21.5 20.75C21.5 21.9926 20.4926 23 19.25 23C18.2706 23 17.4395 22.3735 17.1304 21.5H11.375C9.51111 21.5 8.00012 19.9889 8 18.125C8.00011 16.4242 9.25759 15.0177 10.8938 14.7837L11.1062 16.269Z"/>
|
||||
<path d="M14.75 12.5C14.9307 12.5 15.0931 12.6111 15.158 12.7798L15.6802 14.1392C15.7564 14.3371 15.9129 14.4936 16.1108 14.5698L17.4702 15.092C17.6388 15.157 17.75 15.3193 17.75 15.5C17.75 15.6807 17.6388 15.843 17.4702 15.908L16.1108 16.4302C15.9129 16.5064 15.7564 16.6629 15.6802 16.8608L15.158 18.2202C15.0931 18.3888 14.9307 18.5 14.75 18.5C14.5693 18.5 14.4069 18.3888 14.342 18.2202L13.8198 16.8608C13.7436 16.6629 13.5871 16.5064 13.3892 16.4302L12.0298 15.908C11.8612 15.843 11.75 15.6807 11.75 15.5C11.75 15.3193 11.8612 15.157 12.0298 15.092L13.3892 14.5698C13.5871 14.4936 13.7436 14.3371 13.8198 14.1392L14.342 12.7798C14.4069 12.6111 14.5693 12.5 14.75 12.5Z"/>
|
||||
<path d="M10.25 8C11.2294 8 12.0605 8.62649 12.3696 9.5H18.125C19.989 9.5 21.5 11.011 21.5 12.875C21.5 14.5759 20.2425 15.9815 18.6062 16.2156L18.3938 14.731C19.3017 14.6011 20 13.8189 20 12.875C20 11.8395 19.1605 11 18.125 11H12.3696C12.0605 11.8735 11.2294 12.5 10.25 12.5C9.00736 12.5 8 11.4926 8 10.25C8 9.00736 9.00736 8 10.25 8Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.3562 9.76904C4.44832 9.89892 3.75009 10.6811 3.75 11.625C3.75012 12.6604 4.58954 13.5 5.625 13.5H11.3804C11.6895 12.6265 12.5206 12 13.5 12C14.7426 12 15.75 13.0074 15.75 14.25C15.75 15.4926 14.7426 16.5 13.5 16.5C12.5206 16.5 11.6895 15.8735 11.3804 15H5.625C3.76111 15 2.25012 13.4889 2.25 11.625C2.25009 9.92417 3.50758 8.51773 5.1438 8.28369L5.3562 9.76904ZM13.5 13.5C13.0858 13.5 12.75 13.8358 12.75 14.25C12.75 14.6642 13.0858 15 13.5 15C13.9142 15 14.25 14.6642 14.25 14.25C14.25 13.8358 13.9142 13.5 13.5 13.5Z" fill="currentColor"/>
|
||||
<path d="M9 6C9.18068 6 9.34308 6.11115 9.40796 6.27979L9.93018 7.63916C10.0064 7.83706 10.1629 7.99364 10.3608 8.06982L11.7202 8.59204C11.8888 8.65695 12 8.81929 12 9C12 9.18071 11.8888 9.34305 11.7202 9.40796L10.3608 9.93018C10.1629 10.0064 10.0064 10.1629 9.93018 10.3608L9.40796 11.7202C9.34308 11.8888 9.18068 12 9 12C8.81932 12 8.65692 11.8888 8.59204 11.7202L8.06982 10.3608C7.99363 10.1629 7.83707 10.0064 7.63916 9.93018L6.27979 9.40796C6.11119 9.34305 6 9.18071 6 9C6 8.81929 6.11119 8.65695 6.27979 8.59204L7.63916 8.06982C7.83707 7.99364 7.99363 7.83706 8.06982 7.63916L8.59204 6.27979C8.65692 6.11115 8.81932 6 9 6Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 1.5C5.47939 1.5 6.31054 2.12649 6.61963 3H12.375C14.239 3 15.75 4.51104 15.75 6.375C15.75 8.0759 14.4925 9.48151 12.8562 9.71558L12.6438 8.23096C13.5517 8.10109 14.25 7.31895 14.25 6.375C14.25 5.33946 13.4105 4.5 12.375 4.5H6.61963C6.31054 5.37351 5.47939 6 4.5 6C3.25736 6 2.25 4.99264 2.25 3.75C2.25 2.50736 3.25736 1.5 4.5 1.5ZM4.5 3C4.08579 3 3.75 3.33579 3.75 3.75C3.75 4.16421 4.08579 4.5 4.5 4.5C4.91421 4.5 5.25 4.16421 5.25 3.75C5.25 3.33579 4.91421 3 4.5 3Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"prefix": "custom-vender",
|
||||
"lastModified": 1782516559,
|
||||
"lastModified": 1785319384,
|
||||
"icons": {
|
||||
"agent-v2-access-point": {
|
||||
"body": "<g fill=\"none\"><path d=\"M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z\" fill=\"currentColor\"/><path d=\"M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z\" fill=\"currentColor\"/><path d=\"M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z\" fill=\"currentColor\"/><path d=\"M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z\" fill=\"currentColor\"/><path d=\"M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z\" fill=\"currentColor\"/><path d=\"M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 15,
|
||||
"height": 15
|
||||
},
|
||||
"agent-v2-building-blocks": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z\" fill=\"currentColor\"/></g>"
|
||||
},
|
||||
"agent-v2-configure": {
|
||||
"body": "<g fill=\"none\"><path d=\"M7.5 2.2912C10.875 2.66428 13.5 5.52559 13.5 9V15.75H0V9C0 5.52559 2.62504 2.66428 6 2.2912V0H7.5V2.2912ZM12 14.25V9C12 6.10051 9.6495 3.75 6.75 3.75C3.85051 3.75 1.5 6.10051 1.5 9V14.25H12ZM6.75 12.75C4.67893 12.75 3 11.071 3 9C3 6.92895 4.67893 5.25 6.75 5.25C8.82105 5.25 10.5 6.92895 10.5 9C10.5 11.071 8.82105 12.75 6.75 12.75ZM6.75 11.25C7.99268 11.25 9 10.2427 9 9C9 7.75732 7.99268 6.75 6.75 6.75C5.50732 6.75 4.5 7.75732 4.5 9C4.5 10.2427 5.50732 11.25 6.75 11.25ZM6.75 9.75C6.33578 9.75 6 9.41422 6 9C6 8.58578 6.33578 8.25 6.75 8.25C7.16422 8.25 7.5 8.58578 7.5 9C7.5 9.41422 7.16422 9.75 6.75 9.75Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 14
|
||||
@@ -16,8 +19,7 @@
|
||||
"width": 14
|
||||
},
|
||||
"agent-v2-configure-build": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9.66661 1.33333C9.8434 1.33335 10.013 1.40364 10.138 1.52865L11.4713 2.86198C11.5963 2.987 11.6666 3.15654 11.6666 3.33333C11.6666 3.51012 11.5963 3.67967 11.4713 3.80469L9.10932 6.16667L14.3046 11.362C14.5649 11.6223 14.5649 12.0443 14.3046 12.3047L12.3046 14.3047C12.0443 14.565 11.6223 14.565 11.3619 14.3047L6.16661 9.10938L4.4713 10.8047C4.3463 10.9297 4.17673 11 3.99995 11C3.82316 11 3.65361 10.9297 3.52859 10.8047L0.695261 7.97136C0.434913 7.71101 0.434913 7.289 0.695261 7.02865L6.19526 1.52865L6.24409 1.48438C6.36272 1.38718 6.51191 1.33333 6.66661 1.33333H9.66661ZM7.10932 8.16667L11.8333 12.8906L12.8906 11.8333L8.16661 7.10938L7.10932 8.16667ZM2.10932 7.5L3.99995 9.39063L10.0572 3.33333L9.39057 2.66667H6.94266L2.10932 7.5Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 16
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9.66661 1.33333C9.8434 1.33335 10.013 1.40364 10.138 1.52865L11.4713 2.86198C11.5963 2.987 11.6666 3.15654 11.6666 3.33333C11.6666 3.51012 11.5963 3.67967 11.4713 3.80469L9.10932 6.16667L14.3046 11.362C14.5649 11.6223 14.5649 12.0443 14.3046 12.3047L12.3046 14.3047C12.0443 14.565 11.6223 14.565 11.3619 14.3047L6.16661 9.10938L4.4713 10.8047C4.3463 10.9297 4.17673 11 3.99995 11C3.82316 11 3.65361 10.9297 3.52859 10.8047L0.695261 7.97136C0.434913 7.71101 0.434913 7.289 0.695261 7.02865L6.19526 1.52865L6.24409 1.48438C6.36272 1.38718 6.51191 1.33333 6.66661 1.33333H9.66661ZM7.10932 8.16667L11.8333 12.8906L12.8906 11.8333L8.16661 7.10938L7.10932 8.16667ZM2.10932 7.5L3.99995 9.39063L10.0572 3.33333L9.39057 2.66667H6.94266L2.10932 7.5Z\" fill=\"currentColor\"/></g>"
|
||||
},
|
||||
"agent-v2-configure-preview": {
|
||||
"body": "<g fill=\"none\"><path d=\"M12.4756 4.75207L12.3112 5.12919C12.1909 5.40528 11.8091 5.40528 11.6887 5.12919L11.5244 4.75207C11.2314 4.07965 10.7037 3.54427 10.0451 3.25139L9.53867 3.02615C9.26487 2.90435 9.26487 2.50587 9.53867 2.38408L10.0168 2.17143C10.6923 1.87101 11.2295 1.31582 11.5174 0.620554L11.6862 0.21302C11.8039-0.0710068 12.1961-0.0710068 12.3137 0.21302L12.4825 0.620554C12.7705 1.31582 13.3077 1.87101 13.9832 2.17143L14.4613 2.38408C14.7351 2.50587 14.7351 2.90435 14.4613 3.02615L13.9549 3.25139C13.2963 3.54427 12.7686 4.07965 12.4756 4.75207ZM5.33333 1.33333H8V2.66667H5.33333C3.12419 2.66667 1.33333 4.45753 1.33333 6.66667C1.33333 9.07333 2.97472 10.6437 6.66667 12.3199V10.6667H8C10.2091 10.6667 12 8.8758 12 6.66667H13.3333C13.3333 9.6122 10.9455 12 8 12V14.3333C4.66667 13 0 11 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333Z\" fill=\"currentColor\"/></g>",
|
||||
@@ -25,8 +27,7 @@
|
||||
"height": 15
|
||||
},
|
||||
"agent-v2-end-user-auth": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 16
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>"
|
||||
},
|
||||
"agent-v2-plan": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z\" fill=\"currentColor\"/></g>",
|
||||
@@ -35,8 +36,8 @@
|
||||
},
|
||||
"agent-v2-prompt-insert": {
|
||||
"body": "<g fill=\"none\"><path d=\"M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z\" fill=\"currentColor\"/></g>",
|
||||
"height": 14,
|
||||
"width": 14
|
||||
"width": 14,
|
||||
"height": 14
|
||||
},
|
||||
"agent-v2-robot-3": {
|
||||
"body": "<g fill=\"none\"><path d=\"M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z\" fill=\"currentColor\"/><path d=\"M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z\" fill=\"currentColor\"/></g>",
|
||||
@@ -794,6 +795,16 @@
|
||||
"width": 24,
|
||||
"height": 24
|
||||
},
|
||||
"main-nav-agent": {
|
||||
"body": "<g fill=\"none\"><path d=\"M9.87891 2.81952C13.2539 3.1926 15.8789 6.05391 15.8789 9.52832V16.2783H2.37891V9.52832C2.37891 6.05391 5.00394 3.1926 8.37891 2.81952V0.52832H9.87891V2.81952ZM14.3789 14.7783V9.52832C14.3789 6.62883 12.0284 4.27832 9.12891 4.27832C6.22941 4.27832 3.87891 6.62883 3.87891 9.52832V14.7783H14.3789ZM9.12891 13.2783C7.05784 13.2783 5.37891 11.5994 5.37891 9.52832C5.37891 7.45727 7.05784 5.77832 9.12891 5.77832C11.2 5.77832 12.8789 7.45727 12.8789 9.52832C12.8789 11.5994 11.2 13.2783 9.12891 13.2783ZM9.12891 11.7783C10.3716 11.7783 11.3789 10.771 11.3789 9.52832C11.3789 8.28565 10.3716 7.27832 9.12891 7.27832C7.88623 7.27832 6.87891 8.28565 6.87891 9.52832C6.87891 10.771 7.88623 11.7783 9.12891 11.7783ZM9.12891 10.2783C8.71468 10.2783 8.37891 9.94254 8.37891 9.52832C8.37891 9.1141 8.71468 8.77832 9.12891 8.77832C9.54313 8.77832 9.87891 9.1141 9.87891 9.52832C9.87891 9.94254 9.54313 10.2783 9.12891 10.2783Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-agent-active": {
|
||||
"body": "<g fill=\"none\"><path transform=\"translate(-5.75 -7.25)\" d=\"M15.5 10.2912C18.875 10.6643 21.5 13.5256 21.5 17V23.75H8V17C8 13.5256 10.625 10.6643 14 10.2912V8H15.5V10.2912ZM14.75 20.75C16.821 20.75 18.5 19.071 18.5 17C18.5 14.9289 16.821 13.25 14.75 13.25C12.6789 13.25 11 14.9289 11 17C11 19.071 12.6789 20.75 14.75 20.75ZM14.75 19.25C13.5073 19.25 12.5 18.2427 12.5 17C12.5 15.7573 13.5073 14.75 14.75 14.75C15.9927 14.75 17 15.7573 17 17C17 18.2427 15.9927 19.25 14.75 19.25ZM14.75 17.75C15.1642 17.75 15.5 17.4142 15.5 17C15.5 16.5858 15.1642 16.25 14.75 16.25C14.3358 16.25 14 16.5858 14 17C14 17.4142 14.3358 17.75 14.75 17.75Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-app-home": {
|
||||
"body": "<g fill=\"none\"><path d=\"M2.66667 6.17435C2.66667 5.98785 2.66667 5.89459 2.69021 5.80837C2.71107 5.73198 2.74537 5.65992 2.7915 5.59556C2.84357 5.52292 2.91595 5.46411 3.0607 5.3465L7.3274 1.87983C7.56713 1.68502 7.687 1.58762 7.82027 1.55031C7.93787 1.51741 8.06213 1.51741 8.17973 1.55031C8.313 1.58762 8.43287 1.68502 8.6726 1.87983L12.9393 5.3465C13.0841 5.46411 13.1564 5.52292 13.2085 5.59556C13.2547 5.65992 13.2889 5.73198 13.3098 5.80837C13.3333 5.89459 13.3333 5.98785 13.3333 6.17435V12.2667C13.3333 12.64 13.3333 12.8267 13.2607 12.9693C13.1967 13.0947 13.0948 13.1967 12.9693 13.2607C12.8267 13.3333 12.6401 13.3333 12.2667 13.3333H3.73333C3.35997 13.3333 3.17328 13.3333 3.03067 13.2607C2.90523 13.1967 2.80325 13.0947 2.73933 12.9693C2.66667 12.8267 2.66667 12.64 2.66667 12.2667V6.17435Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/><path d=\"M10 13.3333V9.66667C10 9.11438 9.55228 8.66667 9 8.66667H7C6.44772 8.66667 6 9.11438 6 9.66667V13.3333\" stroke=\"currentColor\" stroke-width=\"1.5\"/></g>",
|
||||
"width": 16,
|
||||
@@ -819,6 +830,16 @@
|
||||
"width": 20,
|
||||
"height": 20
|
||||
},
|
||||
"main-nav-home-v2": {
|
||||
"body": "<g fill=\"none\"><path d=\"M9.75 14.2501H14.25V7.48367L9 3.40034L3.75 7.48367V14.2501H8.25V9.75006H9.75V14.2501ZM15.75 15.0001C15.75 15.4143 15.4142 15.7501 15 15.7501H3C2.58579 15.7501 2.25 15.4143 2.25 15.0001V7.11686C2.25 6.88542 2.35685 6.66694 2.53954 6.52485L8.53957 1.85818C8.8104 1.64753 9.1896 1.64753 9.46043 1.85818L15.4604 6.52485C15.6431 6.66694 15.75 6.88542 15.75 7.11686V15.0001Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-home-v2-active": {
|
||||
"body": "<g fill=\"none\"><path transform=\"translate(-5.75 -6.3)\" d=\"M21.5 21.2999C21.5 21.7141 21.1642 22.0499 20.75 22.0499H8.75C8.33579 22.0499 8 21.7141 8 21.2999V13.4167C8 13.1852 8.10685 12.9667 8.28954 12.8247L14.2896 8.15798C14.5604 7.94734 14.9396 7.94734 15.2104 8.15798L21.2104 12.8247C21.3931 12.9667 21.5 13.1852 21.5 13.4167V21.2999ZM14 16.0499V20.5499H15.5V16.0499H14Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-integrations": {
|
||||
"body": "<g fill=\"none\"><path d=\"M2.5 7.50008C2.5 7.03984 2.8731 6.66675 3.33333 6.66675H16.6667C17.1269 6.66675 17.5 7.03985 17.5 7.50008V15.0001C17.5 15.4603 17.1269 15.8334 16.6667 15.8334H3.33333C2.8731 15.8334 2.5 15.4603 2.5 15.0001V7.50008Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M4.16699 6.66659V4.58325C4.16699 3.89289 4.72663 3.33325 5.41699 3.33325H7.08366C7.77402 3.33325 8.33366 3.89289 8.33366 4.58325V6.66659\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M11.667 6.66659V4.16659C11.667 3.70635 12.0401 3.33325 12.5003 3.33325H15.0003C15.4606 3.33325 15.8337 3.70635 15.8337 4.16659V6.66659\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
|
||||
"width": 20,
|
||||
@@ -829,6 +850,16 @@
|
||||
"width": 20,
|
||||
"height": 20
|
||||
},
|
||||
"main-nav-integrations-v2": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M7.5 2.25C7.91421 2.25 8.25 2.58579 8.25 3V5.25H9.75V3C9.75 2.58579 10.0858 2.25 10.5 2.25H14.25C14.6642 2.25 15 2.58579 15 3V5.25H15.75C16.1642 5.25 16.5 5.58579 16.5 6V14.25C16.5 14.6642 16.1642 15 15.75 15H2.25C1.83579 15 1.5 14.6642 1.5 14.25V6C1.5 5.58579 1.83579 5.25 2.25 5.25H3V3C3 2.58579 3.33579 2.25 3.75 2.25H7.5ZM3 13.5H15V6.75H3V13.5ZM11.25 5.25H13.5V3.75H11.25V5.25ZM4.5 5.25H6.75V3.75H4.5V5.25Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-integrations-v2-active": {
|
||||
"body": "<g fill=\"none\"><g transform=\"translate(-6.39355 -5.66187)\" fill=\"currentColor\"><path d=\"M22.25 11.75C22.6642 11.75 23 12.0858 23 12.5V20C23 20.4142 22.6642 20.75 22.25 20.75H8.75C8.33579 20.75 8 20.4142 8 20V12.5C8 12.0858 8.33579 11.75 8.75 11.75H22.25Z\"/><path d=\"M14 8C14.4142 8 14.75 8.33579 14.75 8.75V10.25H9.5V8.75C9.5 8.33579 9.83579 8 10.25 8H14Z\"/><path d=\"M20.75 8C21.1642 8 21.5 8.33579 21.5 8.75V10.25H16.25V8.75C16.25 8.33579 16.5858 8 17 8H20.75Z\"/></g></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-knowledge": {
|
||||
"body": "<g fill=\"none\"><path d=\"M7.5 9.16675H10\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M7.5 5.83325H12.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M4.16699 4.16667C4.16699 3.24619 4.91318 2.5 5.83366 2.5H15.417C15.6471 2.5 15.8337 2.68655 15.8337 2.91667V17.5H5.83366C4.91318 17.5 4.16699 16.7538 4.16699 15.8333V4.16667Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M4.16699 15.8334C4.16699 14.9129 4.91318 14.1667 5.83366 14.1667H15.8337V17.5001H5.83366C4.91318 17.5001 4.16699 16.7539 4.16699 15.8334Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/></g>",
|
||||
"width": 20,
|
||||
@@ -839,6 +870,16 @@
|
||||
"width": 20,
|
||||
"height": 20
|
||||
},
|
||||
"main-nav-knowledge-v2": {
|
||||
"body": "<g fill=\"none\"><path d=\"M15.75 13.5H4.5C4.08579 13.5 3.75 13.8358 3.75 14.25C3.75 14.6642 4.08579 15 4.5 15H15.75V16.5H4.5C3.25736 16.5 2.25 15.4927 2.25 14.25V3C2.25 2.17157 2.92157 1.5 3.75 1.5H15.75V13.5ZM3.75 12.0375C3.87117 12.0129 3.99658 12 4.125 12H14.25V3H3.75V12.0375ZM12 6.75H6V5.25H12V6.75Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-knowledge-v2-active": {
|
||||
"body": "<g fill=\"none\"><path transform=\"translate(-6 -6.98706)\" d=\"M21.5 20H10.25C9.83579 20 9.5 20.3358 9.5 20.75C9.5 21.1642 9.83579 21.5 10.25 21.5H21.5V23H10.25C9.00736 23 8 21.9927 8 20.75V9.5C8 8.67157 8.67157 8 9.5 8H21.5V20ZM17.75 13.25V11.75H11.75V13.25H17.75Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-marketplace": {
|
||||
"body": "<g fill=\"none\"><path d=\"M16.667 9.99992V16.6666H3.33366V9.99992M7.91699 3.33325H12.0837M7.91699 3.33325L7.44543 7.10578C7.25334 8.6425 8.45158 9.99992 10.0003 9.99992C11.5491 9.99992 12.7473 8.6425 12.5552 7.10578L12.0837 3.33325M7.91699 3.33325H3.75033L2.64677 6.86465C2.16081 8.41975 3.32257 9.99992 4.95179 9.99992C6.1697 9.99992 7.19703 9.093 7.34809 7.88451L7.91699 3.33325ZM12.0837 3.33325H16.2503L17.3539 6.86465C17.8398 8.41975 16.6781 9.99992 15.0489 9.99992C13.831 9.99992 12.8037 9.093 12.6526 7.88451L12.0837 3.33325Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/></g>",
|
||||
"width": 20,
|
||||
@@ -849,6 +890,16 @@
|
||||
"width": 20,
|
||||
"height": 20
|
||||
},
|
||||
"main-nav-marketplace-v2": {
|
||||
"body": "<g fill=\"none\"><path d=\"M15.75 9.93165V15H16.5V16.5H1.5V15H2.25V9.93165C1.34551 9.32625 0.75 8.29515 0.75 7.125C0.75 6.50464 0.918322 5.90716 1.22491 5.3981L3.25898 1.875C3.39295 1.64295 3.64054 1.5 3.90849 1.5H14.0915C14.3594 1.5 14.6071 1.64295 14.741 1.875L16.7681 5.38629C17.0817 5.90716 17.25 6.50464 17.25 7.125C17.25 8.29515 16.6545 9.32625 15.75 9.93165ZM14.25 10.4794C14.1269 10.493 14.0017 10.5 13.875 10.5C12.9307 10.5 12.0592 10.1085 11.4375 9.4599C10.8158 10.1085 9.94432 10.5 9 10.5C8.05568 10.5 7.18418 10.1085 6.5625 9.4599C5.94082 10.1085 5.06933 10.5 4.125 10.5C3.99825 10.5 3.87313 10.493 3.75 10.4794V15H14.25V10.4794ZM4.34149 3L2.51698 6.15991C2.34307 6.44882 2.25 6.77917 2.25 7.125C2.25 8.16052 3.08947 9 4.125 9C4.89822 9 5.5835 8.52772 5.86634 7.82212C6.11805 7.19417 7.00695 7.19417 7.25866 7.82212C7.54148 8.52772 8.22675 9 9 9C9.77325 9 10.4585 8.52772 10.7414 7.82212C10.9931 7.19417 11.8819 7.19417 12.1336 7.82212C12.4165 8.52772 13.1018 9 13.875 9C14.9105 9 15.75 8.16052 15.75 7.125C15.75 6.77917 15.6569 6.44882 15.476 6.1481L13.6585 3H4.34149Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-marketplace-v2-active": {
|
||||
"body": "<g fill=\"none\"><path transform=\"translate(-7.25 -6.875)\" d=\"M23.75 21.5V23H8.75V21.5H9.5V16.4317C8.59551 15.8263 8 14.7951 8 13.625C8 13.0046 8.16832 12.4072 8.47491 11.8981L10.509 8.375C10.6429 8.14295 10.8905 8 11.1585 8H21.3415C21.6094 8 21.8571 8.14295 21.991 8.375L24.0181 11.8863C24.3317 12.4072 24.5 13.0046 24.5 13.625C24.5 14.7951 23.9045 15.8263 23 16.4317V21.5H23.75ZM11.5915 9.5L9.76698 12.6599C9.59307 12.9488 9.5 13.2792 9.5 13.625C9.5 14.6605 10.3395 15.5 11.375 15.5C12.1482 15.5 12.8335 15.0277 13.1163 14.3221C13.3681 13.6942 14.257 13.6942 14.5087 14.3221C14.7915 15.0277 15.4767 15.5 16.25 15.5C17.0232 15.5 17.7085 15.0277 17.9914 14.3221C18.2431 13.6942 19.1319 13.6942 19.3836 14.3221C19.6665 15.0277 20.3518 15.5 21.125 15.5C22.1605 15.5 23 14.6605 23 13.625C23 13.2792 22.9069 12.9488 22.726 12.6481L20.9085 9.5H11.5915Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-quick-search": {
|
||||
"body": "<g fill=\"none\"><path d=\"M12.0004 1.875C17.0398 1.87509 21.1246 5.9602 21.1249 10.9995C21.1249 13.1138 20.4037 15.0582 19.1972 16.6055L21.7958 19.2041C22.235 19.6433 22.2348 20.3556 21.7958 20.7949C21.3565 21.2343 20.6443 21.2343 20.205 20.7949L17.6064 18.1963C16.3417 19.1831 14.8123 19.8466 13.14 20.0552C12.5235 20.132 11.9616 19.6932 11.8847 19.0767C11.8081 18.4603 12.2454 17.8981 12.8617 17.8213C16.2516 17.3983 18.8749 14.5044 18.8749 10.9995C18.8746 7.20283 15.7971 4.12509 12.0004 4.125C8.4954 4.12505 5.60139 6.74948 5.17862 10.1396C5.10154 10.7559 4.53955 11.1934 3.92325 11.1167C3.30688 11.0398 2.86963 10.4777 2.9462 9.86133C3.50765 5.35896 7.34631 1.87505 12.0004 1.875Z\" fill=\"currentColor\"/><path d=\"M3.70727 16.1747L7.91781 11.2624C8.24038 10.8861 8.85505 11.158 8.79357 11.6498L8.49979 14.0001H10.9127C11.3399 14.0001 11.5703 14.5012 11.2923 14.8255L7.08177 19.7378C6.7592 20.1141 6.14453 19.8422 6.20601 19.3504L6.49979 17.0001H4.0869C3.65972 17.0001 3.42927 16.499 3.70727 16.1747Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 24,
|
||||
@@ -864,6 +915,16 @@
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-skill": {
|
||||
"body": "<g fill=\"none\"><path d=\"M14.8922 4.70983L10.3274 2.14208C10.1065 2.01781 9.99601 1.95568 9.87944 1.93377C9.77627 1.91439 9.67021 1.91779 9.56848 1.94372C9.45356 1.97304 9.34729 2.04211 9.13475 2.18024L3.04371 6.13944C2.85218 6.26393 2.75642 6.32617 2.68704 6.4092C2.62562 6.4827 2.57949 6.56771 2.55133 6.65926C2.51953 6.76268 2.51953 6.8769 2.51953 7.10533V12.2864C2.51953 12.5317 2.51953 12.6544 2.55552 12.7639C2.58736 12.8607 2.63943 12.9498 2.70826 13.025C2.78607 13.1101 2.89296 13.1702 3.10675 13.2905L7.67163 15.8582C7.8926 15.9825 8.00305 16.0446 8.11962 16.0665C8.22279 16.0859 8.32885 16.0825 8.43059 16.0566C8.5455 16.0273 8.65177 15.9582 8.86424 15.8201L14.9553 11.8609C15.1468 11.7364 15.2426 11.6742 15.312 11.5911C15.3734 11.5176 15.4195 11.4326 15.4476 11.341C15.4795 11.2376 15.4795 11.1234 15.4795 10.895V5.71389C15.4795 5.4686 15.4795 5.34596 15.4435 5.23644C15.4116 5.13955 15.3596 5.05054 15.2907 4.97528C15.2129 4.89022 15.1061 4.8301 14.8922 4.70983Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/><path d=\"M15.1199 5.27417L8.27988 9.72024L2.87988 6.68271\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/><path d=\"M15.4795 8.28003L8.27953 12.96L2.51953 9.72003\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M8.28027 15.48V9.71997\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-skill-active": {
|
||||
"body": "<g fill=\"none\"><path transform=\"translate(-6.2 -6.70433)\" d=\"M8 18.6501C8 19.1406 8 19.3859 8.07199 19.6049C8.13567 19.7987 8.2398 19.9767 8.37747 20.1273C8.53307 20.2974 8.74686 20.4177 9.17444 20.6581L13.76 23.2375V21.0775L8 17.8819V18.6501ZM15.2 21.0775V23.2642L21.422 18.8858C21.78 18.6339 21.9591 18.5079 22.0885 18.3446C22.203 18.2001 22.2888 18.035 22.341 17.8581C22.4 17.6583 22.4 17.4394 22.4 17.0015V16.0375L15.2 21.0775ZM8 16.1582L13.76 19.2889V17.1175L8 13.9066V16.1582ZM15.2 17.1175V19.2775L22.4 14.2375V12.0775L15.2 17.1175ZM8.72 12.4375L14.48 15.6775L21.68 10.9975L17.1357 8.44137C16.6939 8.19286 16.473 8.0686 16.2398 8.02477C16.0335 7.98601 15.8213 7.99281 15.618 8.04468C15.388 8.10333 15.1755 8.24145 14.7505 8.51771L8.72 12.4375Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-studio": {
|
||||
"body": "<g fill=\"none\"><path d=\"M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z\" fill=\"currentColor\"/><path d=\"M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
|
||||
"width": 20,
|
||||
@@ -874,6 +935,16 @@
|
||||
"width": 20,
|
||||
"height": 20
|
||||
},
|
||||
"main-nav-studio-v2": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M5.3562 9.76904C4.44832 9.89892 3.75009 10.6811 3.75 11.625C3.75012 12.6604 4.58954 13.5 5.625 13.5H11.3804C11.6895 12.6265 12.5206 12 13.5 12C14.7426 12 15.75 13.0074 15.75 14.25C15.75 15.4926 14.7426 16.5 13.5 16.5C12.5206 16.5 11.6895 15.8735 11.3804 15H5.625C3.76111 15 2.25012 13.4889 2.25 11.625C2.25009 9.92417 3.50758 8.51773 5.1438 8.28369L5.3562 9.76904ZM13.5 13.5C13.0858 13.5 12.75 13.8358 12.75 14.25C12.75 14.6642 13.0858 15 13.5 15C13.9142 15 14.25 14.6642 14.25 14.25C14.25 13.8358 13.9142 13.5 13.5 13.5Z\" fill=\"currentColor\"/><path d=\"M9 6C9.18068 6 9.34308 6.11115 9.40796 6.27979L9.93018 7.63916C10.0064 7.83706 10.1629 7.99364 10.3608 8.06982L11.7202 8.59204C11.8888 8.65695 12 8.81929 12 9C12 9.18071 11.8888 9.34305 11.7202 9.40796L10.3608 9.93018C10.1629 10.0064 10.0064 10.1629 9.93018 10.3608L9.40796 11.7202C9.34308 11.8888 9.18068 12 9 12C8.81932 12 8.65692 11.8888 8.59204 11.7202L8.06982 10.3608C7.99363 10.1629 7.83707 10.0064 7.63916 9.93018L6.27979 9.40796C6.11119 9.34305 6 9.18071 6 9C6 8.81929 6.11119 8.65695 6.27979 8.59204L7.63916 8.06982C7.83707 7.99364 7.99363 7.83706 8.06982 7.63916L8.59204 6.27979C8.65692 6.11115 8.81932 6 9 6Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M4.5 1.5C5.47939 1.5 6.31054 2.12649 6.61963 3H12.375C14.239 3 15.75 4.51104 15.75 6.375C15.75 8.0759 14.4925 9.48151 12.8562 9.71558L12.6438 8.23096C13.5517 8.10109 14.25 7.31895 14.25 6.375C14.25 5.33946 13.4105 4.5 12.375 4.5H6.61963C6.31054 5.37351 5.47939 6 4.5 6C3.25736 6 2.25 4.99264 2.25 3.75C2.25 2.50736 3.25736 1.5 4.5 1.5ZM4.5 3C4.08579 3 3.75 3.33579 3.75 3.75C3.75 4.16421 4.08579 4.5 4.5 4.5C4.91421 4.5 5.25 4.16421 5.25 3.75C5.25 3.33579 4.91421 3 4.5 3Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-studio-v2-active": {
|
||||
"body": "<g fill=\"none\"><g transform=\"translate(-5.75 -6.5)\" fill=\"currentColor\"><path d=\"M11.1062 16.269C10.1983 16.3989 9.50011 17.1812 9.5 18.125C9.50012 19.1604 10.3395 20 11.375 20H17.1304C17.4395 19.1265 18.2706 18.5 19.25 18.5C20.4926 18.5 21.5 19.5074 21.5 20.75C21.5 21.9926 20.4926 23 19.25 23C18.2706 23 17.4395 22.3735 17.1304 21.5H11.375C9.51111 21.5 8.00012 19.9889 8 18.125C8.00011 16.4242 9.25759 15.0177 10.8938 14.7837L11.1062 16.269Z\"/><path d=\"M14.75 12.5C14.9307 12.5 15.0931 12.6111 15.158 12.7798L15.6802 14.1392C15.7564 14.3371 15.9129 14.4936 16.1108 14.5698L17.4702 15.092C17.6388 15.157 17.75 15.3193 17.75 15.5C17.75 15.6807 17.6388 15.843 17.4702 15.908L16.1108 16.4302C15.9129 16.5064 15.7564 16.6629 15.6802 16.8608L15.158 18.2202C15.0931 18.3888 14.9307 18.5 14.75 18.5C14.5693 18.5 14.4069 18.3888 14.342 18.2202L13.8198 16.8608C13.7436 16.6629 13.5871 16.5064 13.3892 16.4302L12.0298 15.908C11.8612 15.843 11.75 15.6807 11.75 15.5C11.75 15.3193 11.8612 15.157 12.0298 15.092L13.3892 14.5698C13.5871 14.4936 13.7436 14.3371 13.8198 14.1392L14.342 12.7798C14.4069 12.6111 14.5693 12.5 14.75 12.5Z\"/><path d=\"M10.25 8C11.2294 8 12.0605 8.62649 12.3696 9.5H18.125C19.989 9.5 21.5 11.011 21.5 12.875C21.5 14.5759 20.2425 15.9815 18.6062 16.2156L18.3938 14.731C19.3017 14.6011 20 13.8189 20 12.875C20 11.8395 19.1605 11 18.125 11H12.3696C12.0605 11.8735 11.2294 12.5 10.25 12.5C9.00736 12.5 8 11.4926 8 10.25C8 9.00736 9.00736 8 10.25 8Z\"/></g></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-workspace-settings": {
|
||||
"body": "<g fill=\"none\"><g transform=\"translate(2 2.333)\"><path d=\"M1.33333 2.33333C1.33333 1.78105 1.78105 1.33333 2.33333 1.33333C2.88562 1.33333 3.33333 1.78105 3.33333 2.33333C3.33333 2.88562 2.88562 3.33333 2.33333 3.33333C1.78105 3.33333 1.33333 2.88562 1.33333 2.33333ZM2.33333 0C1.04467 0 0 1.04467 0 2.33333C0 3.622 1.04467 4.66667 2.33333 4.66667C3.622 4.66667 4.66667 3.622 4.66667 2.33333C4.66667 1.04467 3.622 0 2.33333 0ZM6 3H11.3333V1.66667H6V3ZM8.66667 9C8.66667 8.44773 9.1144 8 9.66667 8C10.2189 8 10.6667 8.44773 10.6667 9C10.6667 9.55227 10.2189 10 9.66667 10C9.1144 10 8.66667 9.55227 8.66667 9ZM9.66667 6.66667C8.378 6.66667 7.33333 7.71133 7.33333 9C7.33333 10.2887 8.378 11.3333 9.66667 11.3333C10.9553 11.3333 12 10.2887 12 9C12 7.71133 10.9553 6.66667 9.66667 6.66667ZM0.666667 8.33333V9.66667H6V8.33333H0.666667Z\" fill=\"currentColor\"/></g></g>",
|
||||
"width": 16,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"prefix": "custom-vender",
|
||||
"name": "Dify Custom Vender",
|
||||
"total": 331,
|
||||
"total": 346,
|
||||
"version": "0.0.0-private",
|
||||
"author": {
|
||||
"name": "LangGenius, Inc.",
|
||||
@@ -14,11 +14,11 @@
|
||||
},
|
||||
"samples": [
|
||||
"agent-v2-access-point",
|
||||
"agent-v2-building-blocks",
|
||||
"agent-v2-configure",
|
||||
"agent-v2-configure-active",
|
||||
"agent-v2-configure-build",
|
||||
"agent-v2-configure-preview",
|
||||
"agent-v2-end-user-auth"
|
||||
"agent-v2-configure-preview"
|
||||
],
|
||||
"palette": false
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import type { SearchParams } from 'nuqs'
|
||||
import Marketplace from '@/app/components/plugins/marketplace'
|
||||
import AccountDropdown from '@/app/components/header/account-dropdown'
|
||||
import { EmbeddedMarketplace } from '@/app/components/plugins/marketplace/embedded'
|
||||
import { fetchPluginBanners } from '@/app/components/plugins/marketplace/home/banners'
|
||||
import MarketplaceInstallPermissionProvider from '@/app/components/plugins/marketplace/marketplace-install-permission-provider'
|
||||
import { getLocaleOnServer } from '@/i18n-config/server'
|
||||
|
||||
type MarketplacePageProps = {
|
||||
searchParams?: Promise<SearchParams>
|
||||
}
|
||||
const MarketplacePage = async () => {
|
||||
const initialBanners = await getLocaleOnServer()
|
||||
.then(fetchPluginBanners)
|
||||
.catch(() => undefined)
|
||||
|
||||
const MarketplacePage = ({ searchParams }: MarketplacePageProps) => {
|
||||
return (
|
||||
<div
|
||||
id="marketplace-container"
|
||||
className="flex h-full min-h-0 flex-col overflow-y-auto bg-background-default-subtle pr-1"
|
||||
className="flex h-full min-h-0 flex-col overflow-y-auto bg-background-default"
|
||||
>
|
||||
<MarketplaceInstallPermissionProvider>
|
||||
<Marketplace searchParams={searchParams} isMarketplacePlatform showInstallButton />
|
||||
<EmbeddedMarketplace
|
||||
initialBanners={initialBanners}
|
||||
showInstallButton
|
||||
variant="home"
|
||||
homeHeaderActions={
|
||||
<div className="p-0.5">
|
||||
<AccountDropdown />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</MarketplaceInstallPermissionProvider>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import SkillDetailPage from '@/features/skills/detail-page'
|
||||
|
||||
export default function Page() {
|
||||
return <SkillDetailPage />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import SkillsPage from '@/features/skills/page'
|
||||
|
||||
export default function Page() {
|
||||
return <SkillsPage />
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type CarouselProps = Readonly<{
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: 'horizontal' | 'vertical'
|
||||
overlay?: React.ReactNode
|
||||
}>
|
||||
|
||||
type CarouselContextValue = {
|
||||
@@ -49,7 +50,7 @@ type TCarousel = {
|
||||
>
|
||||
|
||||
const Carousel: TCarousel = React.forwardRef(
|
||||
({ orientation = 'horizontal', opts, plugins, className, children, ...props }, ref) => {
|
||||
({ orientation = 'horizontal', opts, plugins, overlay, className, children, ...props }, ref) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{ ...opts, axis: orientation === 'horizontal' ? 'x' : 'y' },
|
||||
plugins,
|
||||
@@ -98,6 +99,35 @@ const Carousel: TCarousel = React.forwardRef(
|
||||
canScrollNext,
|
||||
}))
|
||||
|
||||
const carousel = overlay
|
||||
? (
|
||||
<div
|
||||
className={cn('relative', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{overlay}
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden [border-radius:inherit]"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
@@ -112,16 +142,7 @@ const Carousel: TCarousel = React.forwardRef(
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={carouselRef}
|
||||
// onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{carousel}
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -91,9 +91,12 @@ describe('AgentRosterResponseContent', () => {
|
||||
await user.click(processToggle)
|
||||
|
||||
expect(processToggle).toHaveAttribute('aria-expanded', 'true')
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('history answer')).toBeInTheDocument()
|
||||
})
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText('history answer')).toBeInTheDocument()
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
)
|
||||
|
||||
expect(screen.queryByText('internal thought should not render')).not.toBeInTheDocument()
|
||||
})
|
||||
@@ -122,9 +125,12 @@ describe('AgentRosterResponseContent', () => {
|
||||
render(<AgentRosterResponseContent item={item} />)
|
||||
await user.click(screen.getByRole('button', { name: 'Thinking' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('const answer = 42').tagName).toBe('CODE')
|
||||
})
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText('const answer = 42').tagName).toBe('CODE')
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
)
|
||||
})
|
||||
|
||||
it('should keep one collapsible thinking timeline while response parts interleave', async () => {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { Img } from '..'
|
||||
|
||||
vi.mock('@/app/components/base/image-gallery', () => ({
|
||||
default: ({ srcs }: { srcs: string[] }) => (
|
||||
<div data-testid="image-gallery">
|
||||
{srcs.map((src) => (
|
||||
<span key={src} data-testid="gallery-image" data-src={src} />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Img', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render with the correct wrapper class', () => {
|
||||
@@ -16,9 +27,9 @@ describe('Img', () => {
|
||||
const gallery = screen.getByTestId('image-gallery')
|
||||
expect(gallery).toBeInTheDocument()
|
||||
|
||||
const images = gallery.querySelectorAll('img')
|
||||
const images = gallery.querySelectorAll('[data-testid="gallery-image"]')
|
||||
expect(images).toHaveLength(1)
|
||||
expect(images[0]).toHaveAttribute('src', 'https://example.com/image.png')
|
||||
expect(images[0]).toHaveAttribute('data-src', 'https://example.com/image.png')
|
||||
})
|
||||
|
||||
it('should pass src as single element array to ImageGallery', () => {
|
||||
@@ -26,21 +37,21 @@ describe('Img', () => {
|
||||
render(<Img src={testSrc} />)
|
||||
|
||||
const gallery = screen.getByTestId('image-gallery')
|
||||
const images = gallery.querySelectorAll('img')
|
||||
const images = gallery.querySelectorAll('[data-testid="gallery-image"]')
|
||||
|
||||
expect(images[0]).toHaveAttribute('src', testSrc)
|
||||
expect(images[0]).toHaveAttribute('data-src', testSrc)
|
||||
})
|
||||
|
||||
it('should render with different src values', () => {
|
||||
const { rerender } = render(<Img src="https://example.com/first.png" />)
|
||||
expect(screen.getByTestId('gallery-image')).toHaveAttribute(
|
||||
'src',
|
||||
'data-src',
|
||||
'https://example.com/first.png',
|
||||
)
|
||||
|
||||
rerender(<Img src="https://example.com/second.jpg" />)
|
||||
expect(screen.getByTestId('gallery-image')).toHaveAttribute(
|
||||
'src',
|
||||
'data-src',
|
||||
'https://example.com/second.jpg',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import ModelParameterModal from '../index'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
setSettingsDestination: vi.fn(),
|
||||
}))
|
||||
|
||||
let parameterRules: Array<Record<string, unknown>> | undefined = [
|
||||
{
|
||||
name: 'temperature',
|
||||
@@ -41,6 +45,14 @@ let activeTextGenerationModelList: Array<Record<string, unknown>> = [
|
||||
},
|
||||
]
|
||||
|
||||
vi.mock('nuqs', () => ({
|
||||
parseAsStringLiteral: () => ({
|
||||
withDefault: () => ({}),
|
||||
withOptions: () => ({}),
|
||||
}),
|
||||
useQueryState: () => [null, mocks.setSettingsDestination],
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
isAPIKeySet: true,
|
||||
@@ -94,16 +106,34 @@ vi.mock('../parameter-item', () => ({
|
||||
|
||||
vi.mock('../../model-selector', () => ({
|
||||
default: ({
|
||||
defaultModel,
|
||||
modelList,
|
||||
onConfigureEmptyState,
|
||||
onHide,
|
||||
onSelect,
|
||||
}: {
|
||||
onHide: () => void
|
||||
defaultModel?: { provider: string; model: string }
|
||||
modelList?: Array<{
|
||||
provider: string
|
||||
models: Array<{ model: string }>
|
||||
}>
|
||||
onConfigureEmptyState?: () => void
|
||||
onHide?: () => void
|
||||
onSelect: (value: { provider: string; model: string }) => void
|
||||
}) => (
|
||||
<div data-testid="model-selector">
|
||||
<div
|
||||
data-testid="model-selector"
|
||||
data-default-provider={defaultModel?.provider ?? ''}
|
||||
data-default-model={defaultModel?.model ?? ''}
|
||||
data-provider-count={modelList?.length ?? 0}
|
||||
>
|
||||
<button onClick={() => onSelect({ provider: 'openai', model: 'gpt-4.1' })}>
|
||||
Select GPT-4.1
|
||||
</button>
|
||||
<button onClick={() => onSelect({ provider: 'custom-provider', model: 'custom-model' })}>
|
||||
Select custom model
|
||||
</button>
|
||||
<button onClick={onConfigureEmptyState}>configure-empty-model</button>
|
||||
<button onClick={onHide}>hide</button>
|
||||
</div>
|
||||
),
|
||||
@@ -235,6 +265,14 @@ describe('ModelParameterModal', () => {
|
||||
expect(screen.getByRole('button', { name: /modelProvider\.modelSettings/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('opens provider settings from the model selector empty state', () => {
|
||||
render(<ModelParameterModal {...defaultProps} provider="" modelId="" />)
|
||||
|
||||
fireEvent.click(screen.getByText('configure-empty-model'))
|
||||
|
||||
expect(mocks.setSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
})
|
||||
|
||||
it('should call onCompletionParamsChange when parameter changes and switch actions happen', () => {
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
@@ -296,6 +334,50 @@ describe('ModelParameterModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the provided modelList to resolve selected model metadata', () => {
|
||||
currentProvider = undefined
|
||||
currentModel = undefined
|
||||
activeTextGenerationModelList = []
|
||||
|
||||
render(
|
||||
<ModelParameterModal
|
||||
{...defaultProps}
|
||||
provider="custom-provider"
|
||||
modelId="custom-model"
|
||||
modelList={
|
||||
[
|
||||
{
|
||||
provider: 'custom-provider',
|
||||
label: { en_US: 'Custom Provider' },
|
||||
models: [
|
||||
{
|
||||
model: 'custom-model',
|
||||
label: { en_US: 'Custom Model' },
|
||||
model_properties: { mode: 'completion' },
|
||||
features: ['tool-call'],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as never
|
||||
}
|
||||
/>,
|
||||
)
|
||||
|
||||
const selector = screen.getByTestId('model-selector')
|
||||
expect(selector).toHaveAttribute('data-default-provider', 'custom-provider')
|
||||
expect(selector).toHaveAttribute('data-default-model', 'custom-model')
|
||||
expect(selector).toHaveAttribute('data-provider-count', '1')
|
||||
|
||||
fireEvent.click(screen.getByText('Select custom model'))
|
||||
|
||||
expect(defaultProps.setModel).toHaveBeenCalledWith({
|
||||
provider: 'custom-provider',
|
||||
modelId: 'custom-model',
|
||||
mode: 'completion',
|
||||
features: ['tool-call'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should toggle debug mode when debug footer is clicked', () => {
|
||||
render(<ModelParameterModal {...defaultProps} />)
|
||||
openSettings()
|
||||
|
||||
@@ -5,10 +5,14 @@ import type { TriggerProps } from './types'
|
||||
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useQueryState } from 'nuqs'
|
||||
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 {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE } from '@/config'
|
||||
import { useModelParameterRules } from '@/service/use-common'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '../hooks'
|
||||
@@ -69,6 +73,10 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
const { currentProvider, currentModel, activeTextGenerationModelList } =
|
||||
useTextGenerationCurrentProviderAndModelAndModelList({ provider, model: modelId })
|
||||
const selectableModelList = modelList ?? activeTextGenerationModelList
|
||||
const [settingsDestination, setSettingsDestination] = useQueryState(
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
)
|
||||
|
||||
const parameterRules: ModelParameterRule[] = useMemo(() => {
|
||||
return parameterRulesData?.data || []
|
||||
@@ -95,6 +103,10 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleConfigureEmptyState = () => {
|
||||
if (settingsDestination !== 'provider') void setSettingsDestination('provider')
|
||||
}
|
||||
|
||||
const handleSwitch = (key: string, value: boolean, assignValue: ParameterValue) => {
|
||||
if (!value) {
|
||||
const newCompletionParams = { ...completionParams }
|
||||
@@ -160,6 +172,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
isInWorkflow &&
|
||||
'border border-workflow-block-parma-bg bg-workflow-block-parma-bg hover:bg-workflow-block-parma-bg',
|
||||
)}
|
||||
onConfigureEmptyState={handleConfigureEmptyState}
|
||||
onSelect={handleChangeModel}
|
||||
/>
|
||||
</div>
|
||||
@@ -250,15 +263,19 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
{!hideDebugWithMultipleModel && (
|
||||
<div
|
||||
className="flex h-12.5 cursor-pointer items-center justify-between rounded-b-xl border-t border-t-divider-subtle px-4 system-sm-regular text-text-accent"
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-12.5 w-full cursor-pointer items-center justify-between rounded-b-xl border-t border-t-divider-subtle bg-transparent px-4 text-left system-sm-regular text-text-accent"
|
||||
onClick={() => onDebugWithMultipleModelChange?.()}
|
||||
>
|
||||
{debugWithMultipleModel
|
||||
? t(($) => $.debugAsSingleModel, { ns: 'appDebug' })
|
||||
: t(($) => $.debugAsMultipleModel, { ns: 'appDebug' })}
|
||||
<ArrowNarrowLeft className="size-3 rotate-180" />
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="i-custom-vender-line-arrows-arrow-narrow-left size-3 rotate-180"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -624,22 +624,34 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(
|
||||
Plan.team,
|
||||
)
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
|
||||
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
|
||||
expect(screen.getByRole('link', { name: /Agents/ })).toHaveAttribute('href', '/agents')
|
||||
const homeLink = screen.getByRole('link', { name: /common.mainNav.home/ })
|
||||
expect(homeLink).toHaveAttribute('href', '/')
|
||||
expect(homeLink.querySelector('.i-custom-vender-main-nav-home-v2')).toBeInTheDocument()
|
||||
const studioLink = screen.getByRole('link', { name: /common.menus.apps/ })
|
||||
expect(studioLink).toHaveAttribute('href', '/apps')
|
||||
expect(studioLink.querySelector('.i-custom-vender-main-nav-studio-v2')).toBeInTheDocument()
|
||||
const agentsLink = screen.getByRole('link', { name: /Agents/ })
|
||||
expect(agentsLink).toHaveAttribute('href', '/agents')
|
||||
expect(screen.getByRole('link', { name: /Agents common.menus.status/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/datasets',
|
||||
)
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/integrations/model-provider',
|
||||
)
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/marketplace',
|
||||
)
|
||||
expect(agentsLink.querySelector('.i-custom-vender-main-nav-agent')).toBeInTheDocument()
|
||||
const skillsLink = screen.getByRole('link', { name: /common.mainNav.skills/ })
|
||||
expect(skillsLink).toHaveAttribute('href', '/skills')
|
||||
expect(skillsLink.querySelector('.i-custom-vender-main-nav-skill')).toBeInTheDocument()
|
||||
const knowledgeLink = screen.getByRole('link', { name: /common.menus.datasets/ })
|
||||
expect(knowledgeLink).toHaveAttribute('href', '/datasets')
|
||||
expect(
|
||||
knowledgeLink.querySelector('.i-custom-vender-main-nav-knowledge-v2'),
|
||||
).toBeInTheDocument()
|
||||
const integrationsLink = screen.getByRole('link', { name: /common.mainNav.integrations/ })
|
||||
expect(integrationsLink).toHaveAttribute('href', '/integrations/model-provider')
|
||||
expect(
|
||||
integrationsLink.querySelector('.i-custom-vender-main-nav-integrations-v2'),
|
||||
).toBeInTheDocument()
|
||||
const marketplaceLink = screen.getByRole('link', { name: /common.mainNav.marketplace/ })
|
||||
expect(marketplaceLink).toHaveAttribute('href', '/marketplace')
|
||||
expect(
|
||||
marketplaceLink.querySelector('.i-custom-vender-main-nav-marketplace-v2'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides the roster entry when Agent v2 is disabled', () => {
|
||||
@@ -648,6 +660,10 @@ describe('MainNav', () => {
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.queryByRole('link', { name: /Agents/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.skills/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/skills',
|
||||
)
|
||||
})
|
||||
|
||||
it('hides the roster entry when the user lacks agent.manage', () => {
|
||||
@@ -767,6 +783,7 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
|
||||
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
|
||||
expect(screen.queryByRole('link', { name: /Agents/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.mainNav.skills/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/datasets',
|
||||
|
||||
@@ -196,6 +196,31 @@ describe('MainNavLayout', () => {
|
||||
expect(screen.getByRole('main')).toHaveTextContent('new knowledge detail')
|
||||
})
|
||||
|
||||
it('hides the global main nav on a skill detail route', () => {
|
||||
;(usePathname as Mock).mockReturnValue('/skills/skill-1')
|
||||
|
||||
render(
|
||||
<MainNavLayout>
|
||||
<div>skill detail</div>
|
||||
</MainNavLayout>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('main-nav')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('main')).toHaveTextContent('skill detail')
|
||||
})
|
||||
|
||||
it('keeps the global main nav on the skills collection route', () => {
|
||||
;(usePathname as Mock).mockReturnValue('/skills')
|
||||
|
||||
render(
|
||||
<MainNavLayout>
|
||||
<div>skills collection</div>
|
||||
</MainNavLayout>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('main-nav')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
'/datasets/create',
|
||||
'/datasets/new/create',
|
||||
|
||||
@@ -6,7 +6,9 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import Link from '@/next/link'
|
||||
|
||||
const NavIcon = ({ icon, className }: { icon: string; className?: string }) => (
|
||||
<span aria-hidden className={cn(icon, 'h-5 w-5 shrink-0', className)} />
|
||||
<span aria-hidden className={cn('flex size-5 shrink-0 items-center justify-center', className)}>
|
||||
<span className={cn(icon, 'size-[18px]')} />
|
||||
</span>
|
||||
)
|
||||
|
||||
type MainNavLinkProps = {
|
||||
@@ -30,7 +32,10 @@ const MainNavLink = ({ item, pathname, children }: MainNavLinkProps) => {
|
||||
)}
|
||||
>
|
||||
<NavIcon icon={item.icon} className="group-aria-[current=page]:hidden" />
|
||||
<NavIcon icon={item.activeIcon} className="hidden group-aria-[current=page]:block" />
|
||||
<NavIcon
|
||||
icon={item.activeIcon}
|
||||
className="hidden drop-shadow-[0_0_4px_rgba(49,70,255,0.18)] group-aria-[current=page]:flex"
|
||||
/>
|
||||
<span className="min-w-0 truncate group-aria-[current=page]:text-shadow-[0px_0px_8px_var(--color-components-main-nav-glass-text-glow)]">
|
||||
{item.label}
|
||||
</span>
|
||||
|
||||
@@ -32,6 +32,8 @@ export type DetailSidebarVisibilityOptions = Pick<
|
||||
const VISIBLE_TO_ALL: MainNavRouteVisibility = () => true
|
||||
const CAN_MANAGE_AGENTS: MainNavRouteVisibility = (options) => options.canManageAgents
|
||||
const CAN_USE_APP_DEPLOY: MainNavRouteVisibility = (options) => options.canUseAppDeploy
|
||||
const NOT_DATASET_OPERATOR: MainNavRouteVisibility = (options) =>
|
||||
!options.isCurrentWorkspaceDatasetOperator
|
||||
|
||||
function isPathUnderRoute(pathname: string, route: string) {
|
||||
return pathname === route || pathname.startsWith(`${route}/`)
|
||||
@@ -43,8 +45,8 @@ export const MAIN_NAV_ROUTES = [
|
||||
href: '/',
|
||||
labelKey: 'mainNav.home',
|
||||
active: (path: string) => path === '/' || path === '/explore/apps',
|
||||
icon: 'i-custom-vender-main-nav-home',
|
||||
activeIcon: 'i-custom-vender-main-nav-home-active',
|
||||
icon: 'i-custom-vender-main-nav-home-v2',
|
||||
activeIcon: 'i-custom-vender-main-nav-home-v2-active',
|
||||
visibility: VISIBLE_TO_ALL,
|
||||
},
|
||||
{
|
||||
@@ -55,8 +57,8 @@ export const MAIN_NAV_ROUTES = [
|
||||
isPathUnderRoute(path, '/apps') ||
|
||||
isPathUnderRoute(path, '/app') ||
|
||||
isPathUnderRoute(path, '/snippets'),
|
||||
icon: 'i-custom-vender-main-nav-studio',
|
||||
activeIcon: 'i-custom-vender-main-nav-studio-active',
|
||||
icon: 'i-custom-vender-main-nav-studio-v2',
|
||||
activeIcon: 'i-custom-vender-main-nav-studio-v2-active',
|
||||
visibility: VISIBLE_TO_ALL,
|
||||
},
|
||||
{
|
||||
@@ -64,18 +66,27 @@ export const MAIN_NAV_ROUTES = [
|
||||
href: '/agents',
|
||||
label: 'Agents',
|
||||
active: (path: string) => isPathUnderRoute(path, '/agents'),
|
||||
icon: 'i-custom-vender-main-nav-roster',
|
||||
activeIcon: 'i-custom-vender-main-nav-roster-active',
|
||||
icon: 'i-custom-vender-main-nav-agent',
|
||||
activeIcon: 'i-custom-vender-main-nav-agent-active',
|
||||
visibility: CAN_MANAGE_AGENTS,
|
||||
feature: 'agentV2',
|
||||
},
|
||||
{
|
||||
key: 'skills',
|
||||
href: '/skills',
|
||||
labelKey: 'mainNav.skills',
|
||||
active: (path: string) => isPathUnderRoute(path, '/skills'),
|
||||
icon: 'i-custom-vender-main-nav-skill',
|
||||
activeIcon: 'i-custom-vender-main-nav-skill-active',
|
||||
visibility: NOT_DATASET_OPERATOR,
|
||||
},
|
||||
{
|
||||
key: 'datasets',
|
||||
href: '/datasets',
|
||||
labelKey: 'menus.datasets',
|
||||
active: (path: string) => isPathUnderRoute(path, '/datasets'),
|
||||
icon: 'i-custom-vender-main-nav-knowledge',
|
||||
activeIcon: 'i-custom-vender-main-nav-knowledge-active',
|
||||
icon: 'i-custom-vender-main-nav-knowledge-v2',
|
||||
activeIcon: 'i-custom-vender-main-nav-knowledge-v2-active',
|
||||
visibility: VISIBLE_TO_ALL,
|
||||
},
|
||||
{
|
||||
@@ -84,8 +95,8 @@ export const MAIN_NAV_ROUTES = [
|
||||
labelKey: 'mainNav.integrations',
|
||||
active: (path: string) =>
|
||||
isPathUnderRoute(path, '/integrations') || isPathUnderRoute(path, '/tools'),
|
||||
icon: 'i-custom-vender-main-nav-integrations',
|
||||
activeIcon: 'i-custom-vender-main-nav-integrations-active',
|
||||
icon: 'i-custom-vender-main-nav-integrations-v2',
|
||||
activeIcon: 'i-custom-vender-main-nav-integrations-v2-active',
|
||||
visibility: VISIBLE_TO_ALL,
|
||||
},
|
||||
{
|
||||
@@ -94,8 +105,8 @@ export const MAIN_NAV_ROUTES = [
|
||||
labelKey: 'mainNav.marketplace',
|
||||
active: (path: string) =>
|
||||
isPathUnderRoute(path, '/marketplace') || isPathUnderRoute(path, '/plugins'),
|
||||
icon: 'i-custom-vender-main-nav-marketplace',
|
||||
activeIcon: 'i-custom-vender-main-nav-marketplace-active',
|
||||
icon: 'i-custom-vender-main-nav-marketplace-v2',
|
||||
activeIcon: 'i-custom-vender-main-nav-marketplace-v2-active',
|
||||
visibility: VISIBLE_TO_ALL,
|
||||
feature: 'marketplace',
|
||||
},
|
||||
@@ -140,14 +151,21 @@ function isDatasetDetailPathname(pathname: string) {
|
||||
return true
|
||||
}
|
||||
|
||||
function isSkillDetailPathname(pathname: string) {
|
||||
const [section, skillId] = pathname.split('/').filter(Boolean)
|
||||
|
||||
return section === 'skills' && !!skillId
|
||||
}
|
||||
|
||||
export function shouldHideMainNavigation(pathname: string) {
|
||||
const [section, namespace, knowledgeSpaceId] = pathname.split('/').filter(Boolean)
|
||||
|
||||
return (
|
||||
section === 'datasets' &&
|
||||
namespace === 'new' &&
|
||||
!!knowledgeSpaceId &&
|
||||
knowledgeSpaceId !== 'create'
|
||||
(section === 'datasets' &&
|
||||
namespace === 'new' &&
|
||||
!!knowledgeSpaceId &&
|
||||
knowledgeSpaceId !== 'create') ||
|
||||
isSkillDetailPathname(pathname)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginBanner } from '../home/banners'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockFetchPluginBanners = vi.fn()
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: () => 'zh-Hans',
|
||||
}))
|
||||
|
||||
vi.mock('../home/banners', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../home/banners')>()
|
||||
|
||||
return {
|
||||
...original,
|
||||
fetchPluginBanners: (...args: unknown[]) => mockFetchPluginBanners(...args),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../view', () => ({
|
||||
MarketplaceView: ({
|
||||
banners,
|
||||
showInstallButton,
|
||||
}: {
|
||||
banners: PluginBanner[]
|
||||
showInstallButton: boolean
|
||||
}) => (
|
||||
<div>
|
||||
<p>Trending banners: {banners.length}</p>
|
||||
<p>{showInstallButton ? 'Install enabled' : 'Install disabled'}</p>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
let queryClient: QueryClient
|
||||
|
||||
function Wrapper({ children }: { children: ReactNode }) {
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
}
|
||||
|
||||
describe('EmbeddedMarketplace', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('loads homepage banners on the client for the active locale', async () => {
|
||||
mockFetchPluginBanners.mockResolvedValue([
|
||||
{
|
||||
id: 'banner-1',
|
||||
title: 'Trending',
|
||||
sort: 1,
|
||||
language: 'zh-Hans',
|
||||
style_type: 'blog',
|
||||
content: {
|
||||
blog_title: 'Dify update',
|
||||
link: 'https://dify.ai/blog',
|
||||
link_target_type: 'blog',
|
||||
},
|
||||
},
|
||||
] satisfies PluginBanner[])
|
||||
|
||||
const { EmbeddedMarketplace } = await import('../embedded')
|
||||
|
||||
render(<EmbeddedMarketplace showInstallButton variant="home" />, { wrapper: Wrapper })
|
||||
|
||||
expect(await screen.findByText('Trending banners: 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Install enabled')).toBeInTheDocument()
|
||||
expect(mockFetchPluginBanners).toHaveBeenCalledWith('zh-Hans')
|
||||
})
|
||||
|
||||
it('uses server-rendered homepage banners without requesting them again on hydration', async () => {
|
||||
const initialBanners = [
|
||||
{
|
||||
id: 'banner-1',
|
||||
title: 'Trending',
|
||||
sort: 1,
|
||||
language: 'zh-Hans',
|
||||
style_type: 'blog',
|
||||
content: {
|
||||
blog_title: 'Dify update',
|
||||
link: 'https://dify.ai/blog',
|
||||
link_target_type: 'blog',
|
||||
},
|
||||
},
|
||||
] satisfies PluginBanner[]
|
||||
|
||||
const { EmbeddedMarketplace } = await import('../embedded')
|
||||
|
||||
render(
|
||||
<EmbeddedMarketplace initialBanners={initialBanners} showInstallButton variant="home" />,
|
||||
{ wrapper: Wrapper },
|
||||
)
|
||||
|
||||
expect(screen.getByText('Trending banners: 1')).toBeInTheDocument()
|
||||
expect(mockFetchPluginBanners).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not request homepage banners for the default catalog variant', async () => {
|
||||
const { EmbeddedMarketplace } = await import('../embedded')
|
||||
|
||||
render(<EmbeddedMarketplace variant="default" />, { wrapper: Wrapper })
|
||||
|
||||
expect(screen.getByText('Trending banners: 0')).toBeInTheDocument()
|
||||
expect(mockFetchPluginBanners).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ComponentProps, ReactNode } from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Provider as JotaiProvider } from 'jotai'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||
import PluginTypeSwitch from '../plugin-type-switch'
|
||||
import styles from '../plugin-type-switch.module.css'
|
||||
|
||||
vi.mock('#i18n', async () => {
|
||||
const { withSelectorKey } = await import('@/test/i18n-mock')
|
||||
@@ -13,7 +14,7 @@ vi.mock('#i18n', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
const renderSwitch = (searchParams = '') => {
|
||||
const renderSwitch = (searchParams = '', props?: ComponentProps<typeof PluginTypeSwitch>) => {
|
||||
const { wrapper: NuqsWrapper, onUrlUpdate } = createNuqsTestWrapper({ searchParams })
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<JotaiProvider>
|
||||
@@ -21,7 +22,7 @@ const renderSwitch = (searchParams = '') => {
|
||||
</JotaiProvider>
|
||||
)
|
||||
|
||||
return { ...render(<PluginTypeSwitch />, { wrapper: Wrapper }), onUrlUpdate }
|
||||
return { ...render(<PluginTypeSwitch {...props} />, { wrapper: Wrapper }), onUrlUpdate }
|
||||
}
|
||||
|
||||
describe('PluginTypeSwitch', () => {
|
||||
@@ -47,4 +48,28 @@ describe('PluginTypeSwitch', () => {
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('category')).toBe('model')
|
||||
})
|
||||
|
||||
it('exposes the selected category and updates the URL in the home variant', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onUrlUpdate } = renderSwitch('?category=all', { variant: 'home' })
|
||||
const categoryGroup = screen.getByRole('group', { name: 'marketplace.allPlugins' })
|
||||
|
||||
expect(categoryGroup).toHaveClass('w-full', 'justify-start', 'gap-1')
|
||||
const activeCategory = screen.getByRole('button', { name: 'category.all' })
|
||||
const inactiveCategory = screen.getByRole('button', { name: 'category.models' })
|
||||
|
||||
expect(activeCategory).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(activeCategory).toHaveClass(styles.homeItem, styles.homeItemActive)
|
||||
expect(inactiveCategory).toHaveClass(styles.homeItem)
|
||||
expect(inactiveCategory).not.toHaveClass(styles.homeItemActive)
|
||||
expect(screen.getByRole('button', { name: 'categorySingle.datasource' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'categorySingle.agent' })).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'category.models' }))
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls.at(-1)?.[0]
|
||||
expect(update?.searchParams.get('category')).toBe('model')
|
||||
expect(update?.options.scroll).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginBanner } from '../home/banners'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockFetchPluginBanners, mockGetLocaleOnServer } = vi.hoisted(() => ({
|
||||
mockFetchPluginBanners: vi.fn(),
|
||||
mockGetLocaleOnServer: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n-config/server', () => ({
|
||||
getLocaleOnServer: mockGetLocaleOnServer,
|
||||
}))
|
||||
|
||||
vi.mock('../home/banners', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../home/banners')>()
|
||||
|
||||
return {
|
||||
...original,
|
||||
fetchPluginBanners: mockFetchPluginBanners,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../hydration-server', () => ({
|
||||
HydrateQueryClient: ({ children }: { children: ReactNode }) => children,
|
||||
}))
|
||||
|
||||
vi.mock('../view', () => ({
|
||||
MarketplaceView: ({ banners }: { banners: PluginBanner[] }) => (
|
||||
<p>Server banners: {banners.length}</p>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Marketplace server entry', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('prefetches localized homepage banners before rendering the standalone view', async () => {
|
||||
mockGetLocaleOnServer.mockResolvedValue('en-US')
|
||||
mockFetchPluginBanners.mockResolvedValue([
|
||||
{
|
||||
id: 'banner-1',
|
||||
title: 'Trending',
|
||||
sort: 1,
|
||||
language: 'en-US',
|
||||
style_type: 'blog',
|
||||
content: {
|
||||
blog_title: 'Dify update',
|
||||
link: 'https://dify.ai/blog',
|
||||
link_target_type: 'blog',
|
||||
},
|
||||
},
|
||||
] satisfies PluginBanner[])
|
||||
|
||||
const { default: Marketplace } = await import('../index')
|
||||
const element = await Marketplace({ variant: 'home' })
|
||||
|
||||
render(element)
|
||||
|
||||
expect(screen.getByText('Server banners: 1')).toBeInTheDocument()
|
||||
expect(mockGetLocaleOnServer).toHaveBeenCalledOnce()
|
||||
expect(mockFetchPluginBanners).toHaveBeenCalledWith('en-US')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import MarketplaceDetailDialog from '../index'
|
||||
|
||||
vi.mock('../../utils', () => ({
|
||||
getPluginLinkInMarketplace: (
|
||||
plugin: Plugin,
|
||||
params: { language: string; source?: string; theme?: string; view: string },
|
||||
) => `about:blank?plugin=${plugin.org}/${plugin.name}&language=${params.language}&source=${params.source}&theme=${params.theme}&view=${params.view}`,
|
||||
}))
|
||||
|
||||
const plugin = {
|
||||
type: 'plugin',
|
||||
org: 'dify',
|
||||
name: 'plugin-a',
|
||||
plugin_id: 'plugin-a',
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
latest_package_identifier: 'pkg',
|
||||
icon: 'icon.png',
|
||||
verified: true,
|
||||
label: { 'en-US': 'Plugin A' },
|
||||
brief: { 'en-US': 'Brief' },
|
||||
description: { 'en-US': 'Description' },
|
||||
introduction: 'Intro',
|
||||
repository: 'https://github.com/dify/plugin-a',
|
||||
category: PluginCategoryEnum.tool,
|
||||
install_count: 42,
|
||||
endpoint: { settings: [] },
|
||||
tags: [],
|
||||
badges: [],
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
} as Plugin
|
||||
|
||||
describe('MarketplaceDetailDialog', () => {
|
||||
it('renders the marketplace detail route in modal mode and closes in place', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenChange = vi.fn()
|
||||
|
||||
render(
|
||||
<ThemeProvider forcedTheme="dark">
|
||||
<MarketplaceDetailDialog
|
||||
open
|
||||
plugin={plugin}
|
||||
onOpenChange={onOpenChange}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
)
|
||||
|
||||
const frame = screen.getByTitle('Plugin A · plugin.detailPanel.operation.detail')
|
||||
expect(frame).toHaveAttribute(
|
||||
'src',
|
||||
'about:blank?plugin=dify/plugin-a&language=en-US&source=http://localhost:3000&theme=system&view=modal',
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Dialog,
|
||||
DialogCloseButton,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@langgenius/dify-ui/dialog'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useState } from 'react'
|
||||
import { useLocale, useTranslation } from '#i18n'
|
||||
import { getPluginLinkInMarketplace } from '../utils'
|
||||
|
||||
type MarketplaceDetailDialogProps = {
|
||||
open: boolean
|
||||
plugin: Plugin
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
function MarketplaceDetailDialog({
|
||||
open,
|
||||
plugin,
|
||||
onOpenChange,
|
||||
}: MarketplaceDetailDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const { theme } = useTheme()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const pluginLabel = plugin.label[locale] ?? plugin.label['en-US'] ?? plugin.name
|
||||
const detailLabel = t(($) => $['detailPanel.operation.detail'], { ns: 'plugin' })
|
||||
const detailURL = getPluginLinkInMarketplace(plugin, {
|
||||
language: locale,
|
||||
source: globalThis.location?.origin,
|
||||
theme,
|
||||
view: 'modal',
|
||||
})
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen)
|
||||
setIsLoading(true)
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent
|
||||
className="h-[min(800px,calc(100dvh-48px))] max-h-none! w-[min(1200px,calc(100vw-48px))] max-w-none! overflow-hidden! border-0 p-0 shadow-xl"
|
||||
>
|
||||
<DialogTitle className="sr-only">
|
||||
{pluginLabel}
|
||||
{' · '}
|
||||
{detailLabel}
|
||||
</DialogTitle>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'absolute inset-0 bg-background-default transition-opacity',
|
||||
isLoading ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-[52px] items-center px-6">
|
||||
<div className="h-4 w-40 animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
|
||||
</div>
|
||||
<div className="mx-auto flex w-full max-w-[1000px] gap-8 px-12 py-8">
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<div className="h-16 w-2/3 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="h-4 w-full animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="h-4 w-5/6 animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="mt-8 h-72 w-full animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
</div>
|
||||
<div className="hidden w-60 flex-col gap-4 lg:flex">
|
||||
<div className="h-24 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="h-52 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
className={cn(
|
||||
'size-full border-0 bg-background-default transition-opacity',
|
||||
isLoading ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
src={detailURL}
|
||||
title={`${pluginLabel} · ${detailLabel}`}
|
||||
/>
|
||||
<DialogCloseButton
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
className="top-5 right-5 size-8 rounded-lg"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-12 bg-linear-to-t from-background-default to-transparent"
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarketplaceDetailDialog
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import type { PluginBanner } from './home/banners'
|
||||
import type { MarketplaceViewProps } from './view'
|
||||
import { queryOptions, useQuery } from '@tanstack/react-query'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { marketplaceQuery } from '@/service/client'
|
||||
import { fetchPluginBanners } from './home/banners'
|
||||
import { MarketplaceView } from './view'
|
||||
|
||||
const BANNER_STALE_TIME = 1000 * 60 * 5
|
||||
|
||||
export type EmbeddedMarketplaceProps = Omit<MarketplaceViewProps, 'banners'> & {
|
||||
initialBanners?: PluginBanner[]
|
||||
}
|
||||
|
||||
export function EmbeddedMarketplace({
|
||||
initialBanners,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: EmbeddedMarketplaceProps) {
|
||||
const locale = useLocale()
|
||||
const input = {
|
||||
query: {
|
||||
page: 'plugins' as const,
|
||||
language: locale,
|
||||
},
|
||||
}
|
||||
const { data: banners = [] } = useQuery(
|
||||
queryOptions({
|
||||
queryKey: [...marketplaceQuery.banners.list.queryKey({ input }), locale],
|
||||
queryFn: () => fetchPluginBanners(locale),
|
||||
enabled: variant === 'home',
|
||||
initialData: initialBanners,
|
||||
staleTime: BANNER_STALE_TIME,
|
||||
}),
|
||||
)
|
||||
|
||||
return <MarketplaceView {...props} banners={banners} variant={variant} />
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Marketplace Catalog Home
|
||||
|
||||
The redesigned Marketplace catalog shell provides the shared header, hero, search, trending, tabs, and sticky category navigation used by the Plugins and Templates pages.
|
||||
|
||||
## Internal Modules
|
||||
|
||||
- `marketplace/list/list-wrapper`
|
||||
- `marketplace/plugin-type-switch`
|
||||
|
||||
## External Modules
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,183 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import HomeCatalogNavigation from '../home-catalog-navigation'
|
||||
import HomeCatalogTabs from '../home-catalog-tabs'
|
||||
import { HomeStickyCatalogTabs, HomeStickyStateProvider } from '../home-sticky-state-provider'
|
||||
import styles from '../home-sticky.module.css'
|
||||
|
||||
vi.mock('#i18n', async () => {
|
||||
const { withSelectorKey } = await import('@/test/i18n-mock')
|
||||
return {
|
||||
useTranslation: () => ({
|
||||
t: withSelectorKey((key: string, options?: { ns?: string }) =>
|
||||
options?.ns ? `${options.ns}.${key}` : key,
|
||||
),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../plugin-type-switch', () => ({
|
||||
default: ({ className, variant }: { className?: string; variant?: string }) => (
|
||||
<div data-testid="plugin-type-switch" className={className} data-variant={variant} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (path: string) => `https://marketplace.dify.ai${path}?source=console`,
|
||||
}))
|
||||
|
||||
describe('HomeCatalogNavigation', () => {
|
||||
const renderNavigation = (isMarketplacePlatform: boolean) => {
|
||||
return render(
|
||||
<HomeStickyStateProvider>
|
||||
<HomeStickyCatalogTabs>
|
||||
<div data-testid="header-catalog-tabs" />
|
||||
</HomeStickyCatalogTabs>
|
||||
<HomeCatalogNavigation
|
||||
catalogTabs={<HomeCatalogTabs isMarketplacePlatform={isMarketplacePlatform} />}
|
||||
/>
|
||||
</HomeStickyStateProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
it('keeps template navigation inside the Marketplace platform', () => {
|
||||
renderNavigation(true)
|
||||
|
||||
const navigationSection = screen.getByRole('region', { name: 'common.mainNav.marketplace' })
|
||||
|
||||
expect(navigationSection).toHaveClass(styles.catalogNavigation)
|
||||
expect(navigationSection.firstElementChild).toHaveClass('w-full')
|
||||
expect(navigationSection.firstElementChild).not.toHaveClass('mx-auto', 'max-w-[1200px]')
|
||||
const activeTab = screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })
|
||||
expect(activeTab).toHaveAttribute('aria-current', 'page')
|
||||
expect(activeTab).toHaveAttribute('href', '/plugins')
|
||||
expect(
|
||||
screen.getByRole('link', { name: /plugin\.marketplace\.home\.templates/ }),
|
||||
).toHaveAttribute('href', '/templates')
|
||||
expect(screen.queryByText('plugin.marketplace.home.new')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('plugin-type-switch')).toHaveAttribute('data-variant', 'home')
|
||||
})
|
||||
|
||||
it('keeps header tabs clickable', () => {
|
||||
render(<HomeCatalogTabs isMarketplacePlatform />)
|
||||
|
||||
const pluginsTab = screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })
|
||||
const templatesTab = screen.getByRole('link', { name: 'plugin.marketplace.home.templates' })
|
||||
|
||||
expect(pluginsTab).toHaveAttribute('href', '/plugins')
|
||||
expect(pluginsTab).toHaveClass('cursor-pointer')
|
||||
expect(templatesTab).toHaveAttribute('href', '/templates')
|
||||
expect(templatesTab).toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('marks Templates as active when rendering the Templates catalog', () => {
|
||||
render(<HomeCatalogTabs activeTab="templates" isMarketplacePlatform />)
|
||||
|
||||
const pluginsTab = screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })
|
||||
const templatesTab = screen.getByRole('link', { name: 'plugin.marketplace.home.templates' })
|
||||
|
||||
expect(pluginsTab).not.toHaveAttribute('aria-current')
|
||||
expect(templatesTab).toHaveAttribute('aria-current', 'page')
|
||||
expect(screen.queryByText('plugin.marketplace.home.new')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders a supplied catalog category navigation', () => {
|
||||
render(
|
||||
<HomeStickyStateProvider>
|
||||
<HomeCatalogNavigation
|
||||
catalogTabs={<HomeCatalogTabs activeTab="templates" isMarketplacePlatform />}
|
||||
catalogCategories={<nav aria-label="Template categories">Template categories</nav>}
|
||||
/>
|
||||
</HomeStickyStateProvider>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('navigation', { name: 'Template categories' })).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('plugin-type-switch')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('links Dify users to the hosted Marketplace templates page', () => {
|
||||
renderNavigation(false)
|
||||
|
||||
expect(screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://marketplace.dify.ai/plugins?source=console',
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('link', { name: /plugin\.marketplace\.home\.templates/ }),
|
||||
).toHaveAttribute('href', 'https://marketplace.dify.ai/templates?source=console')
|
||||
})
|
||||
|
||||
it('shows the compact navigation and header tabs after reaching the sticky header', () => {
|
||||
const scrollContainer = document.createElement('div')
|
||||
scrollContainer.id = 'marketplace-container'
|
||||
document.body.appendChild(scrollContainer)
|
||||
|
||||
renderNavigation(true)
|
||||
|
||||
const navigationSection = screen.getByRole('region', { name: 'common.mainNav.marketplace' })
|
||||
const pinTrigger = navigationSection.previousElementSibling as HTMLElement
|
||||
vi.spyOn(scrollContainer, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 100, 100))
|
||||
const triggerRect = vi
|
||||
.spyOn(pinTrigger, 'getBoundingClientRect')
|
||||
.mockReturnValue(new DOMRect(0, 49, 100, 100))
|
||||
|
||||
fireEvent.scroll(scrollContainer)
|
||||
expect(screen.queryByTestId('header-catalog-tabs')).not.toBeInTheDocument()
|
||||
|
||||
triggerRect.mockReturnValue(new DOMRect(0, 48, 100, 100))
|
||||
fireEvent.scroll(scrollContainer)
|
||||
|
||||
expect(navigationSection).toHaveClass(styles.catalogNavigationPinned)
|
||||
expect(
|
||||
screen.getByRole('navigation', { name: 'common.mainNav.marketplace' }).parentElement,
|
||||
).toHaveClass(styles.catalogTabsPinned)
|
||||
expect(screen.getByTestId('plugin-type-switch')).toHaveClass(styles.categoriesPinned)
|
||||
expect(screen.getByTestId('header-catalog-tabs')).toBeInTheDocument()
|
||||
|
||||
triggerRect.mockReturnValue(new DOMRect(0, 49, 100, 100))
|
||||
fireEvent.scroll(scrollContainer)
|
||||
|
||||
expect(navigationSection).not.toHaveClass(styles.catalogNavigationPinned)
|
||||
expect(screen.queryByTestId('header-catalog-tabs')).not.toBeInTheDocument()
|
||||
|
||||
scrollContainer.remove()
|
||||
})
|
||||
|
||||
it('keeps the pinned state when compact styling moves the sticky section', () => {
|
||||
const scrollContainer = document.createElement('div')
|
||||
scrollContainer.id = 'marketplace-container'
|
||||
document.body.appendChild(scrollContainer)
|
||||
|
||||
renderNavigation(true)
|
||||
|
||||
const navigationSection = screen.getByRole('region', { name: 'common.mainNav.marketplace' })
|
||||
const pinTrigger = navigationSection.previousElementSibling as HTMLElement
|
||||
vi.spyOn(scrollContainer, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 100, 100))
|
||||
vi.spyOn(pinTrigger, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 47, 100, 0))
|
||||
vi.spyOn(navigationSection, 'getBoundingClientRect').mockReturnValue(
|
||||
new DOMRect(0, 49, 100, 60),
|
||||
)
|
||||
|
||||
fireEvent.scroll(scrollContainer)
|
||||
|
||||
expect(navigationSection).toHaveClass(styles.catalogNavigationPinned)
|
||||
expect(screen.getByTestId('header-catalog-tabs')).toBeInTheDocument()
|
||||
|
||||
scrollContainer.remove()
|
||||
})
|
||||
|
||||
it('prevents scroll anchoring from reversing the sticky threshold', () => {
|
||||
const scrollContainer = document.createElement('div')
|
||||
scrollContainer.id = 'marketplace-container'
|
||||
document.body.appendChild(scrollContainer)
|
||||
|
||||
const { unmount } = renderNavigation(true)
|
||||
|
||||
expect(scrollContainer.style.overflowAnchor).toBe('none')
|
||||
|
||||
unmount()
|
||||
expect(scrollContainer.style.overflowAnchor).toBe('')
|
||||
|
||||
scrollContainer.remove()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import HomeHeader from '../home-header'
|
||||
|
||||
vi.mock('#i18n', async () => {
|
||||
const { withSelectorKey } = await import('@/test/i18n-mock')
|
||||
return {
|
||||
useTranslation: () => ({
|
||||
t: withSelectorKey((key: string) => key),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => () => 'https://docs.dify.ai/en/home',
|
||||
}))
|
||||
|
||||
vi.mock('../home-sticky-state-provider', () => ({
|
||||
HomeStickyCatalogTabs: ({ children }: { children: React.ReactNode }) => children,
|
||||
}))
|
||||
|
||||
describe('HomeHeader', () => {
|
||||
it('links the Guide action to Dify documentation', () => {
|
||||
render(<HomeHeader isMarketplacePlatform />)
|
||||
|
||||
const brandLink = screen.getByRole('link', { name: 'Dify Marketplace' })
|
||||
const [lightLogo, darkLogo] = brandLink.querySelectorAll('img')
|
||||
expect(lightLogo).toHaveAttribute('src', expect.stringContaining('dify-marketplace-logo.svg'))
|
||||
expect(darkLogo).toHaveAttribute(
|
||||
'src',
|
||||
expect.stringContaining('dify-marketplace-logo-dark.svg'),
|
||||
)
|
||||
expect(lightLogo).toHaveAttribute('width', '141.761')
|
||||
expect(lightLogo).toHaveAttribute('height', '16.386')
|
||||
expect(darkLogo).toHaveAttribute('width', '141.761')
|
||||
expect(darkLogo).toHaveAttribute('height', '16.386')
|
||||
expect(screen.queryByText('mainNav.marketplace')).not.toBeInTheDocument()
|
||||
|
||||
const guideLink = screen.getByRole('link', { name: 'marketplace.home.guide' })
|
||||
expect(guideLink).toHaveAttribute('href', 'https://docs.dify.ai/en/home')
|
||||
expect(guideLink).toHaveAttribute('target', '_blank')
|
||||
expect(guideLink).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('shows Templates as the active compact tab on the Templates catalog', () => {
|
||||
render(<HomeHeader activeTab="templates" isMarketplacePlatform />)
|
||||
|
||||
expect(screen.getByRole('link', { name: 'marketplace.home.plugins' })).not.toHaveAttribute(
|
||||
'aria-current',
|
||||
)
|
||||
expect(screen.getByRole('link', { name: 'marketplace.home.templates' })).toHaveAttribute(
|
||||
'aria-current',
|
||||
'page',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import HomeHero from '../home-hero'
|
||||
|
||||
vi.mock('#i18n', async () => {
|
||||
const { withSelectorKey } = await import('@/test/i18n-mock')
|
||||
return {
|
||||
useTranslation: () => ({
|
||||
t: withSelectorKey((key: string) => key),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('HomeHero', () => {
|
||||
it('renders catalog-specific copy when supplied', () => {
|
||||
render(
|
||||
<HomeHero
|
||||
isMarketplacePlatform
|
||||
title="Discover templates"
|
||||
subtitle="Start faster with ready-to-use workflows."
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Discover templates' })).toBeInTheDocument()
|
||||
expect(screen.getByText('Start faster with ready-to-use workflows.')).toBeInTheDocument()
|
||||
expect(screen.queryByText('marketplace.home.heroTitle')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,208 @@
|
||||
import type { PluginBanner } from '../banners'
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import HomeTrending from '../home-trending'
|
||||
|
||||
vi.mock('#i18n', async () => {
|
||||
const { withSelectorKey } = await import('@/test/i18n-mock')
|
||||
return {
|
||||
useTranslation: (namespace: string) => ({
|
||||
t: withSelectorKey((key: string) => `${namespace}.${key}`),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/plugins/base/badges/partner', () => ({
|
||||
default: () => <span data-testid="partner-badge" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/base/badges/verified', () => ({
|
||||
default: () => <span data-testid="verified-badge" />,
|
||||
}))
|
||||
|
||||
const banners: PluginBanner[] = [
|
||||
{
|
||||
id: 'recommend',
|
||||
style_type: 'recommend',
|
||||
title: 'Trending',
|
||||
sort: 0,
|
||||
language: 'en',
|
||||
content: {
|
||||
theme_type: 'hottest',
|
||||
heading: 'Popular plugins',
|
||||
description: 'Chosen from real usage.',
|
||||
cards: [
|
||||
{
|
||||
item_type: 'plugin',
|
||||
item_id: 'langgenius/dropbox',
|
||||
display_name: 'Dropbox',
|
||||
icon_url: '/api/v1/plugins/langgenius/dropbox/icon',
|
||||
creator: 'langgenius',
|
||||
badges: ['partner', 'verified'],
|
||||
link: '/plugins/langgenius/dropbox',
|
||||
card_position: 0,
|
||||
},
|
||||
{
|
||||
item_type: 'plugin',
|
||||
item_id: 'langgenius/zapier',
|
||||
display_name: 'Zapier',
|
||||
link: '/plugins/langgenius/zapier',
|
||||
card_position: 1,
|
||||
},
|
||||
{
|
||||
item_type: 'plugin',
|
||||
item_id: 'langgenius/notion',
|
||||
display_name: 'Notion',
|
||||
link: '/plugins/langgenius/notion',
|
||||
card_position: 2,
|
||||
},
|
||||
{
|
||||
item_type: 'plugin',
|
||||
item_id: 'langgenius/slack',
|
||||
display_name: 'Slack',
|
||||
link: '/plugins/langgenius/slack',
|
||||
card_position: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'blog',
|
||||
style_type: 'blog',
|
||||
title: 'Dify Updates',
|
||||
sort: 1,
|
||||
language: 'en',
|
||||
content: {
|
||||
blog_title: 'Dify v1.9 new launch',
|
||||
subtitle: 'New Agent node support',
|
||||
description: 'Build agent workflows with the new Agent node.',
|
||||
link: 'https://dify.ai/blog',
|
||||
link_target_type: 'blog',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'event',
|
||||
style_type: 'event',
|
||||
title: 'Duck Duck Go',
|
||||
sort: 2,
|
||||
language: 'en',
|
||||
content: {
|
||||
images: {
|
||||
desktop: '/api/v1/banners/images/banners/duckduckgo.png',
|
||||
mobile: '/api/v1/banners/images/banners/duckduckgo-mobile.png',
|
||||
},
|
||||
link: 'https://marketplace.dify.ai/plugin/langgenius/duckduckgo',
|
||||
alt_text: 'DuckDuckGo plugin',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
describe('HomeTrending', () => {
|
||||
it('renders and switches between the three API-backed banner layouts', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<HomeTrending banners={banners} isMarketplacePlatform />)
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Popular plugins' })).toBeInTheDocument()
|
||||
const recommendationSlide = screen.getByRole('group', { name: 'Trending' })
|
||||
expect(
|
||||
within(recommendationSlide)
|
||||
.getAllByRole('link')
|
||||
.map((link) => link.getAttribute('aria-label')),
|
||||
).toEqual(['Dropbox', 'Zapier', 'Notion', 'Slack'])
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Dify Updates' }))
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Dify v1.9 new launch' })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('link', {
|
||||
name: 'Read more about Dify v1.9 new launch',
|
||||
}),
|
||||
).toHaveAttribute('href', 'https://dify.ai/blog')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Duck Duck Go' }))
|
||||
|
||||
expect(screen.getByRole('link', { name: 'DuckDuckGo plugin' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://marketplace.dify.ai/plugin/langgenius/duckduckgo',
|
||||
)
|
||||
})
|
||||
|
||||
it('switches to the selected slide from the pagination with the keyboard', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<HomeTrending banners={banners} isMarketplacePlatform />)
|
||||
|
||||
const duckDuckGoButton = screen.getByRole('button', { name: 'Duck Duck Go' })
|
||||
|
||||
duckDuckGoButton.focus()
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(duckDuckGoButton).toHaveAttribute('aria-current', 'true')
|
||||
expect(screen.getByRole('button', { name: 'Trending' })).not.toHaveAttribute('aria-current')
|
||||
expect(screen.getByRole('group', { name: 'Duck Duck Go' })).toHaveAttribute(
|
||||
'aria-hidden',
|
||||
'false',
|
||||
)
|
||||
})
|
||||
|
||||
it('toggles the carousel between paused and playing states', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<HomeTrending banners={banners} isMarketplacePlatform />)
|
||||
|
||||
const pauseButton = screen.getByRole('button', {
|
||||
name: 'plugin.marketplace.home.trendingPause',
|
||||
})
|
||||
|
||||
pauseButton.focus()
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
const playButton = screen.getByRole('button', {
|
||||
name: 'plugin.marketplace.home.trendingPlay',
|
||||
})
|
||||
|
||||
playButton.focus()
|
||||
await user.keyboard(' ')
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'plugin.marketplace.home.trendingPause',
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('starts with autoplay paused when reduced motion is enabled', () => {
|
||||
const matchMedia = vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||
matches: true,
|
||||
media: '(prefers-reduced-motion: reduce)',
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})
|
||||
|
||||
render(<HomeTrending banners={banners} isMarketplacePlatform />)
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'plugin.marketplace.home.trendingPlay',
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
|
||||
matchMedia.mockRestore()
|
||||
})
|
||||
|
||||
it('renders no carousel when the API returns no banners', () => {
|
||||
render(<HomeTrending banners={[]} isMarketplacePlatform />)
|
||||
|
||||
expect(
|
||||
screen.queryByRole('region', {
|
||||
name: 'plugin.marketplace.home.trendingTitle',
|
||||
}),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,200 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { marketplaceClient } from '@/service/client'
|
||||
import { fetchPluginBanners } from './banners'
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
marketplaceClient: {
|
||||
banners: {
|
||||
list: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const mockedListBanners = vi.mocked(marketplaceClient.banners.list)
|
||||
|
||||
describe('fetchPluginBanners', () => {
|
||||
beforeEach(() => {
|
||||
mockedListBanners.mockReset()
|
||||
})
|
||||
|
||||
it('normalizes every public banner style in API sort order', async () => {
|
||||
mockedListBanners.mockResolvedValue({
|
||||
code: 0,
|
||||
msg: 'success',
|
||||
data: {
|
||||
banners: [
|
||||
{
|
||||
id: 'event',
|
||||
style_type: 'event',
|
||||
title: 'Dify Event',
|
||||
sort: 3,
|
||||
language: 'en',
|
||||
content: {
|
||||
images: {
|
||||
desktop: '/api/v1/banners/images/banners/event.png',
|
||||
mobile: '/api/v1/banners/images/banners/event-mobile.png',
|
||||
},
|
||||
link: 'https://dify.ai/events',
|
||||
alt_text: 'Dify Event',
|
||||
activity_id: 'event-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'recommend',
|
||||
style_type: 'recommend',
|
||||
title: 'Trending Now',
|
||||
sort: 1,
|
||||
language: 'en',
|
||||
content: {
|
||||
theme_type: 'hottest',
|
||||
heading: 'Popular plugins',
|
||||
description: 'Chosen from real usage.',
|
||||
cards: [
|
||||
{
|
||||
item_type: 'plugin',
|
||||
item_id: 'langgenius/fourth',
|
||||
display_name: 'Fourth',
|
||||
link: '/plugins/langgenius/fourth',
|
||||
card_position: 3,
|
||||
},
|
||||
{
|
||||
item_type: 'plugin',
|
||||
item_id: 'langgenius/first',
|
||||
display_name: 'First',
|
||||
icon_url: '/api/v1/plugins/langgenius/first/icon',
|
||||
creator: 'langgenius',
|
||||
badges: ['verified', 'partner', 'unknown'],
|
||||
link: '/plugins/langgenius/first',
|
||||
card_position: 0,
|
||||
},
|
||||
{
|
||||
item_type: 'plugin',
|
||||
item_id: 'langgenius/third',
|
||||
display_name: 'Third',
|
||||
link: '/plugins/langgenius/third',
|
||||
card_position: 2,
|
||||
},
|
||||
{
|
||||
item_type: 'plugin',
|
||||
item_id: 'langgenius/second',
|
||||
display_name: 'Second',
|
||||
link: '/plugins/langgenius/second',
|
||||
card_position: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ad',
|
||||
style_type: 'ad',
|
||||
title: 'Partner campaign',
|
||||
sort: 4,
|
||||
language: 'en',
|
||||
content: {
|
||||
images: {
|
||||
desktop: '/api/v1/banners/images/banners/ad.webp',
|
||||
},
|
||||
link: 'https://example.com',
|
||||
partner_id: 'partner-1',
|
||||
campaign_id: 'campaign-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'blog',
|
||||
style_type: 'blog',
|
||||
title: 'Dify Updates',
|
||||
sort: 2,
|
||||
language: 'en',
|
||||
content: {
|
||||
blog_title: 'Dify v1.9 new launch',
|
||||
subtitle: 'New Agent node support',
|
||||
description: 'Build agent workflows with the new Agent node.',
|
||||
link: 'https://dify.ai/blog',
|
||||
link_target_type: 'blog',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'unsupported',
|
||||
style_type: 'popup',
|
||||
title: 'Unsupported',
|
||||
sort: 0,
|
||||
language: 'en',
|
||||
content: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const banners = await fetchPluginBanners('en-US')
|
||||
|
||||
expect(mockedListBanners).toHaveBeenCalledWith({
|
||||
query: {
|
||||
page: 'plugins',
|
||||
language: 'en-US',
|
||||
},
|
||||
})
|
||||
expect(banners.map((banner) => banner.id)).toEqual(['recommend', 'blog', 'event', 'ad'])
|
||||
|
||||
const recommend = banners[0]
|
||||
expect(recommend?.style_type).toBe('recommend')
|
||||
if (recommend?.style_type === 'recommend') {
|
||||
expect(recommend.content.cards.map((card) => card.display_name)).toEqual([
|
||||
'First',
|
||||
'Second',
|
||||
'Third',
|
||||
'Fourth',
|
||||
])
|
||||
expect(recommend.content.cards[0]).toMatchObject({
|
||||
creator: 'langgenius',
|
||||
badges: ['verified', 'partner'],
|
||||
})
|
||||
}
|
||||
|
||||
const event = banners[2]
|
||||
expect(event?.style_type).toBe('event')
|
||||
if (event?.style_type === 'event') {
|
||||
expect(event.content.images).toEqual({
|
||||
desktop: '/api/v1/banners/images/banners/event.png',
|
||||
mobile: '/api/v1/banners/images/banners/event-mobile.png',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('drops malformed banners and returns no placeholders for an empty response', async () => {
|
||||
mockedListBanners
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
banners: [
|
||||
{
|
||||
id: 'empty-recommend',
|
||||
style_type: 'recommend',
|
||||
title: 'Empty',
|
||||
sort: 0,
|
||||
language: 'en',
|
||||
content: {
|
||||
theme_type: 'hottest',
|
||||
cards: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'event-without-desktop',
|
||||
style_type: 'event',
|
||||
title: 'Broken',
|
||||
sort: 1,
|
||||
language: 'en',
|
||||
content: {
|
||||
images: {
|
||||
mobile: '/api/v1/banners/images/banners/mobile.png',
|
||||
},
|
||||
link: 'https://example.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce('')
|
||||
|
||||
await expect(fetchPluginBanners('en-US')).resolves.toEqual([])
|
||||
await expect(fetchPluginBanners('en-US')).resolves.toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,282 @@
|
||||
import { marketplaceClient } from '@/service/client'
|
||||
|
||||
const MAX_CARDS_PER_PAGE = 4
|
||||
|
||||
type BannerBase = {
|
||||
id: string
|
||||
title: string
|
||||
sort: number
|
||||
language: string
|
||||
}
|
||||
|
||||
export type BannerRecommendCard = {
|
||||
item_type: 'plugin' | 'template'
|
||||
item_id: string
|
||||
display_name: string
|
||||
icon_url?: string
|
||||
icon?: string
|
||||
icon_background?: string
|
||||
creator?: string
|
||||
badges?: Array<'partner' | 'verified'>
|
||||
link: string
|
||||
card_position: number
|
||||
}
|
||||
|
||||
export type BannerRecommend = BannerBase & {
|
||||
style_type: 'recommend'
|
||||
content: {
|
||||
theme_type: 'newest' | 'hottest' | 'partner'
|
||||
heading?: string
|
||||
subheadings?: string[]
|
||||
description?: string
|
||||
cards: BannerRecommendCard[]
|
||||
}
|
||||
}
|
||||
|
||||
export type BannerBlog = BannerBase & {
|
||||
style_type: 'blog'
|
||||
content: {
|
||||
blog_title: string
|
||||
subtitle?: string
|
||||
description?: string
|
||||
link: string
|
||||
link_target_type: 'blog' | 'github'
|
||||
}
|
||||
}
|
||||
|
||||
type BannerImageContent = {
|
||||
images: {
|
||||
desktop: string
|
||||
tablet?: string
|
||||
mobile?: string
|
||||
}
|
||||
link: string
|
||||
alt_text?: string
|
||||
activity_id?: string
|
||||
}
|
||||
|
||||
export type BannerEvent = BannerBase & {
|
||||
style_type: 'event'
|
||||
content: BannerImageContent
|
||||
}
|
||||
|
||||
export type BannerAd = BannerBase & {
|
||||
style_type: 'ad'
|
||||
content: BannerImageContent & {
|
||||
partner_id?: string
|
||||
campaign_id?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type PluginBanner = BannerRecommend | BannerBlog | BannerEvent | BannerAd
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
const parseBannerBase = (value: Record<string, unknown>): BannerBase | null => {
|
||||
if (
|
||||
typeof value.id !== 'string' ||
|
||||
!value.id ||
|
||||
typeof value.title !== 'string' ||
|
||||
!value.title ||
|
||||
typeof value.sort !== 'number' ||
|
||||
typeof value.language !== 'string' ||
|
||||
!value.language
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: value.id,
|
||||
title: value.title,
|
||||
sort: value.sort,
|
||||
language: value.language,
|
||||
}
|
||||
}
|
||||
|
||||
const parseRecommendCard = (value: unknown): BannerRecommendCard | null => {
|
||||
if (!isRecord(value)) return null
|
||||
|
||||
const itemType = value.item_type
|
||||
const itemId = value.item_id
|
||||
const displayName = value.display_name
|
||||
if (
|
||||
(itemType !== 'plugin' && itemType !== 'template') ||
|
||||
typeof itemId !== 'string' ||
|
||||
!itemId ||
|
||||
typeof displayName !== 'string' ||
|
||||
!displayName
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const badges = Array.isArray(value.badges)
|
||||
? value.badges.filter(
|
||||
(badge): badge is 'partner' | 'verified' => badge === 'partner' || badge === 'verified',
|
||||
)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
item_type: itemType,
|
||||
item_id: itemId,
|
||||
display_name: displayName,
|
||||
icon_url: typeof value.icon_url === 'string' ? value.icon_url : undefined,
|
||||
icon: typeof value.icon === 'string' ? value.icon : undefined,
|
||||
icon_background: typeof value.icon_background === 'string' ? value.icon_background : undefined,
|
||||
creator: typeof value.creator === 'string' ? value.creator : undefined,
|
||||
badges,
|
||||
link: typeof value.link === 'string' ? value.link : '',
|
||||
card_position: typeof value.card_position === 'number' ? value.card_position : 0,
|
||||
}
|
||||
}
|
||||
|
||||
const parseRecommendBanner = (
|
||||
base: BannerBase,
|
||||
content: Record<string, unknown>,
|
||||
): BannerRecommend | null => {
|
||||
const themeType = content.theme_type
|
||||
if (themeType !== 'newest' && themeType !== 'hottest' && themeType !== 'partner') return null
|
||||
|
||||
const cards = Array.isArray(content.cards)
|
||||
? content.cards
|
||||
.map(parseRecommendCard)
|
||||
.filter((card): card is BannerRecommendCard => Boolean(card))
|
||||
.sort((a, b) => a.card_position - b.card_position)
|
||||
.slice(0, MAX_CARDS_PER_PAGE)
|
||||
: []
|
||||
|
||||
if (cards.length === 0) return null
|
||||
|
||||
const subheadings = Array.isArray(content.subheadings)
|
||||
? content.subheadings.filter((item): item is string => typeof item === 'string')
|
||||
: undefined
|
||||
|
||||
return {
|
||||
...base,
|
||||
style_type: 'recommend',
|
||||
content: {
|
||||
theme_type: themeType,
|
||||
heading: typeof content.heading === 'string' ? content.heading : undefined,
|
||||
subheadings,
|
||||
description: typeof content.description === 'string' ? content.description : undefined,
|
||||
cards,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const parseBlogBanner = (base: BannerBase, content: Record<string, unknown>): BannerBlog | null => {
|
||||
const linkTargetType = content.link_target_type
|
||||
if (
|
||||
typeof content.blog_title !== 'string' ||
|
||||
!content.blog_title ||
|
||||
typeof content.link !== 'string' ||
|
||||
!content.link ||
|
||||
(linkTargetType !== 'blog' && linkTargetType !== 'github')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
style_type: 'blog',
|
||||
content: {
|
||||
blog_title: content.blog_title,
|
||||
subtitle: typeof content.subtitle === 'string' ? content.subtitle : undefined,
|
||||
description: typeof content.description === 'string' ? content.description : undefined,
|
||||
link: content.link,
|
||||
link_target_type: linkTargetType,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const parseImageBanner = (
|
||||
base: BannerBase,
|
||||
styleType: 'event' | 'ad',
|
||||
content: Record<string, unknown>,
|
||||
): BannerEvent | BannerAd | null => {
|
||||
if (
|
||||
!isRecord(content.images) ||
|
||||
typeof content.images.desktop !== 'string' ||
|
||||
!content.images.desktop ||
|
||||
typeof content.link !== 'string' ||
|
||||
!content.link
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const imageContent: BannerImageContent = {
|
||||
images: {
|
||||
desktop: content.images.desktop,
|
||||
tablet:
|
||||
typeof content.images.tablet === 'string' && content.images.tablet
|
||||
? content.images.tablet
|
||||
: undefined,
|
||||
mobile:
|
||||
typeof content.images.mobile === 'string' && content.images.mobile
|
||||
? content.images.mobile
|
||||
: undefined,
|
||||
},
|
||||
link: content.link,
|
||||
alt_text: typeof content.alt_text === 'string' ? content.alt_text : undefined,
|
||||
activity_id: typeof content.activity_id === 'string' ? content.activity_id : undefined,
|
||||
}
|
||||
|
||||
if (styleType === 'event') {
|
||||
return {
|
||||
...base,
|
||||
style_type: 'event',
|
||||
content: imageContent,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
style_type: 'ad',
|
||||
content: {
|
||||
...imageContent,
|
||||
partner_id: typeof content.partner_id === 'string' ? content.partner_id : undefined,
|
||||
campaign_id: typeof content.campaign_id === 'string' ? content.campaign_id : undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const parsePluginBanner = (value: unknown): PluginBanner | null => {
|
||||
if (!isRecord(value) || !isRecord(value.content)) return null
|
||||
|
||||
const base = parseBannerBase(value)
|
||||
if (!base) return null
|
||||
|
||||
switch (value.style_type) {
|
||||
case 'recommend':
|
||||
return parseRecommendBanner(base, value.content)
|
||||
case 'blog':
|
||||
return parseBlogBanner(base, value.content)
|
||||
case 'event':
|
||||
case 'ad':
|
||||
return parseImageBanner(base, value.style_type, value.content)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizePluginBanners = (response: unknown): PluginBanner[] => {
|
||||
if (!isRecord(response) || !isRecord(response.data) || !Array.isArray(response.data.banners))
|
||||
return []
|
||||
|
||||
return response.data.banners
|
||||
.map(parsePluginBanner)
|
||||
.filter((banner): banner is PluginBanner => Boolean(banner))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
}
|
||||
|
||||
export const fetchPluginBanners = async (language: string): Promise<PluginBanner[]> => {
|
||||
const response = await marketplaceClient.banners.list({
|
||||
query: {
|
||||
page: 'plugins',
|
||||
language,
|
||||
},
|
||||
})
|
||||
|
||||
return normalizePluginBanners(response)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from '#i18n'
|
||||
import PluginTypeSwitch from '../plugin-type-switch'
|
||||
import { homeCatalogPinnedAtom } from './home-sticky-state'
|
||||
import styles from './home-sticky.module.css'
|
||||
|
||||
type HomeCatalogNavigationProps = {
|
||||
catalogCategories?: ReactNode
|
||||
catalogTabs: ReactNode
|
||||
}
|
||||
|
||||
const STICKY_TOP = 48
|
||||
|
||||
function HomeCatalogNavigation({ catalogCategories, catalogTabs }: HomeCatalogNavigationProps) {
|
||||
const { t } = useTranslation()
|
||||
const isPinned = useAtomValue(homeCatalogPinnedAtom)
|
||||
const setIsPinned = useSetAtom(homeCatalogPinnedAtom)
|
||||
const pinTriggerRef = useRef<HTMLSpanElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = document.getElementById('marketplace-container')
|
||||
if (!scrollContainer) return
|
||||
|
||||
const previousOverflowAnchor = scrollContainer.style.overflowAnchor
|
||||
// The sticky section becomes shorter when its tabs move into the header.
|
||||
// Prevent browser scroll anchoring from moving it back across the pin threshold.
|
||||
scrollContainer.style.overflowAnchor = 'none'
|
||||
|
||||
const updatePinnedState = () => {
|
||||
const pinTrigger = pinTriggerRef.current
|
||||
if (!pinTrigger) return
|
||||
|
||||
const containerTop = scrollContainer.getBoundingClientRect().top
|
||||
const triggerTop = pinTrigger.getBoundingClientRect().top
|
||||
setIsPinned(triggerTop <= containerTop + STICKY_TOP)
|
||||
}
|
||||
|
||||
updatePinnedState()
|
||||
scrollContainer.addEventListener('scroll', updatePinnedState, { passive: true })
|
||||
window.addEventListener('resize', updatePinnedState)
|
||||
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('scroll', updatePinnedState)
|
||||
window.removeEventListener('resize', updatePinnedState)
|
||||
scrollContainer.style.overflowAnchor = previousOverflowAnchor
|
||||
}
|
||||
}, [setIsPinned])
|
||||
|
||||
return (
|
||||
<>
|
||||
<span ref={pinTriggerRef} aria-hidden className={styles.catalogNavigationTrigger} />
|
||||
<section
|
||||
aria-label={t(($) => $['mainNav.marketplace'], { ns: 'common' })}
|
||||
className={cn(
|
||||
'w-full shrink-0 bg-background-default',
|
||||
styles.catalogNavigation,
|
||||
isPinned && styles.catalogNavigationPinned,
|
||||
)}
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className={cn('-ml-2', isPinned && styles.catalogTabsPinned)}>{catalogTabs}</div>
|
||||
{catalogCategories ? (
|
||||
<div className={cn('mt-4', isPinned && styles.categoriesPinned)}>
|
||||
{catalogCategories}
|
||||
</div>
|
||||
) : (
|
||||
<PluginTypeSwitch
|
||||
className={cn('mt-4', isPinned && styles.categoriesPinned)}
|
||||
variant="home"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeCatalogNavigation
|
||||
@@ -0,0 +1,64 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from '#i18n'
|
||||
import Link from '@/next/link'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
export type HomeCatalogTab = 'plugins' | 'templates'
|
||||
|
||||
type HomeCatalogTabsProps = {
|
||||
activeTab?: HomeCatalogTab
|
||||
className?: string
|
||||
isMarketplacePlatform: boolean
|
||||
}
|
||||
|
||||
const HomeCatalogTabs = ({
|
||||
activeTab = 'plugins',
|
||||
className,
|
||||
isMarketplacePlatform,
|
||||
}: HomeCatalogTabsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const pluginsHref = isMarketplacePlatform ? '/plugins' : getMarketplaceUrl('/plugins')
|
||||
const templatesHref = isMarketplacePlatform ? '/templates' : getMarketplaceUrl('/templates')
|
||||
const isPluginsActive = activeTab === 'plugins'
|
||||
const isTemplatesActive = activeTab === 'templates'
|
||||
const pluginsLabel = t(($) => $['marketplace.home.plugins'], { ns: 'plugin' })
|
||||
const templatesLabel = t(($) => $['marketplace.home.templates'], { ns: 'plugin' })
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label={t(($) => $['mainNav.marketplace'], { ns: 'common' })}
|
||||
className={cn('flex h-8 items-center gap-1', className)}
|
||||
>
|
||||
<Link
|
||||
href={pluginsHref}
|
||||
aria-label={pluginsLabel}
|
||||
aria-current={isPluginsActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
'relative flex h-8 cursor-pointer items-start rounded-lg px-[9px] pt-2 outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
isPluginsActive ? 'body-sm-medium' : 'body-sm-regular',
|
||||
isPluginsActive
|
||||
? 'bg-state-base-active text-text-primary'
|
||||
: 'text-text-tertiary hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
{pluginsLabel}
|
||||
</Link>
|
||||
<Link
|
||||
href={templatesHref}
|
||||
aria-label={templatesLabel}
|
||||
aria-current={isTemplatesActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
'relative flex h-8 cursor-pointer items-center rounded-[10px] p-2 outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
isTemplatesActive ? 'body-sm-medium' : 'body-sm-regular',
|
||||
isTemplatesActive
|
||||
? 'bg-state-base-active text-text-primary'
|
||||
: 'text-text-tertiary hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
{templatesLabel}
|
||||
</Link>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeCatalogTabs
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { HomeCatalogTab } from './home-catalog-tabs'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from '#i18n'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import Link from '@/next/link'
|
||||
import MarketplaceLogoDark from '@/public/marketplace/dify-marketplace-logo-dark.svg'
|
||||
import MarketplaceLogo from '@/public/marketplace/dify-marketplace-logo.svg'
|
||||
import HomeCatalogTabs from './home-catalog-tabs'
|
||||
import { HomeStickyCatalogTabs } from './home-sticky-state-provider'
|
||||
import styles from './home-sticky.module.css'
|
||||
|
||||
type HomeHeaderProps = {
|
||||
activeTab?: HomeCatalogTab
|
||||
actions?: React.ReactNode
|
||||
isMarketplacePlatform: boolean
|
||||
}
|
||||
|
||||
function Guide() {
|
||||
const docLink = useDocLink()
|
||||
const { t } = useTranslation('plugin')
|
||||
|
||||
return (
|
||||
<Link href={docLink()} target="_blank" rel="noopener noreferrer" className={styles.guide}>
|
||||
<Button variant="ghost" size="large" className="min-w-[94px] gap-0.5 px-3 text-text-primary">
|
||||
<span aria-hidden className="i-ri-map-2-line size-5" />
|
||||
<span className="px-1 system-md-medium">{t(($) => $['marketplace.home.guide'])}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const HomeHeader = ({ activeTab = 'plugins', actions, isMarketplacePlatform }: HomeHeaderProps) => {
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'sticky top-0 z-50 flex w-full shrink-0 items-center gap-4 border-b border-divider-regular bg-background-default px-4 py-1.5 md:px-9',
|
||||
styles.header,
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="Dify Marketplace"
|
||||
className="flex h-full w-[141.933px] shrink-0 items-center"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'h-[16.386px] w-[141.761px] max-w-none shrink-0',
|
||||
styles.marketplaceLogoLight,
|
||||
)}
|
||||
height="16.386"
|
||||
src={MarketplaceLogo.src}
|
||||
width="141.761"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'h-[16.386px] w-[141.761px] max-w-none shrink-0',
|
||||
styles.marketplaceLogoDark,
|
||||
)}
|
||||
height="16.386"
|
||||
src={MarketplaceLogoDark.src}
|
||||
width="141.761"
|
||||
/>
|
||||
</Link>
|
||||
<HomeStickyCatalogTabs>
|
||||
<HomeCatalogTabs
|
||||
activeTab={activeTab}
|
||||
className={styles.headerCatalogTabs}
|
||||
isMarketplacePlatform={isMarketplacePlatform}
|
||||
/>
|
||||
</HomeStickyCatalogTabs>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full min-w-0 flex-1 items-center justify-end gap-2.5">
|
||||
<Guide />
|
||||
{actions}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeHeader
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from '#i18n'
|
||||
|
||||
type HomeHeroProps = {
|
||||
isMarketplacePlatform: boolean
|
||||
subtitle?: ReactNode
|
||||
title?: ReactNode
|
||||
}
|
||||
|
||||
const HomeHero = ({ isMarketplacePlatform, subtitle, title }: HomeHeroProps) => {
|
||||
const { t } = useTranslation('plugin')
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'relative flex shrink-0 justify-center bg-background-default px-4',
|
||||
!isMarketplacePlatform && 'pt-6',
|
||||
)}
|
||||
>
|
||||
<div className="relative flex h-[162px] w-full max-w-[726px] flex-col items-center pt-[41px]">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1
|
||||
className="text-[28px] leading-[1.2] font-medium tracking-[-0.56px] text-text-primary"
|
||||
style={{ fontFamily: "var(--font-family-brand, 'Söhne', var(--font-sans))" }}
|
||||
>
|
||||
{title ?? t(($) => $['marketplace.home.heroTitle'])}
|
||||
</h1>
|
||||
<p className="text-[13px] leading-4 font-light tracking-[-0.065px] text-text-tertiary">
|
||||
{subtitle ?? t(($) => $['marketplace.home.heroSubtitle'])}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeHero
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from '#i18n'
|
||||
import SearchBoxWrapper from '@/app/components/plugins/marketplace/search-box/search-box-wrapper'
|
||||
import styles from './home-sticky.module.css'
|
||||
|
||||
const HomeSearch = ({ children }: { children?: ReactNode }) => {
|
||||
const searchRef = useRef<HTMLDivElement>(null)
|
||||
const { t } = useTranslation('plugin')
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalSearchShortcut = (event: KeyboardEvent) => {
|
||||
if (event.key.toLowerCase() !== 'k' || (!event.metaKey && !event.ctrlKey)) return
|
||||
|
||||
event.preventDefault()
|
||||
searchRef.current?.querySelector('input')?.focus()
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleGlobalSearchShortcut)
|
||||
return () => document.removeEventListener('keydown', handleGlobalSearchShortcut)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none sticky z-[60] -mt-9 flex h-9 shrink-0 justify-center px-4',
|
||||
styles.search,
|
||||
)}
|
||||
>
|
||||
<div ref={searchRef} className="pointer-events-auto relative w-full max-w-[420px]">
|
||||
{children ?? (
|
||||
<SearchBoxWrapper
|
||||
wrapperClassName="w-full max-w-none"
|
||||
inputClassName="h-9 w-full rounded-[10px] bg-components-input-bg-normal [&>div]:px-2.5"
|
||||
inputElementClassName="text-[14px] leading-5"
|
||||
searchIconName="i-ri-search-line"
|
||||
placeholder={t(($) => $['marketplace.home.searchPlaceholder'])}
|
||||
showTags={false}
|
||||
usedInMarketplace={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeSearch
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { ScopeProvider } from 'jotai-scope'
|
||||
import { homeCatalogPinnedAtom, homeStickyScopedAtoms } from './home-sticky-state'
|
||||
|
||||
export function HomeStickyStateProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ScopeProvider atoms={homeStickyScopedAtoms} name="MarketplaceHomeSticky">
|
||||
{children}
|
||||
</ScopeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export function HomeStickyCatalogTabs({ children }: { children: ReactNode }) {
|
||||
const isCatalogPinned = useAtomValue(homeCatalogPinnedAtom)
|
||||
|
||||
return isCatalogPinned ? children : null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { atom } from 'jotai'
|
||||
|
||||
export const homeCatalogPinnedAtom = atom(false)
|
||||
|
||||
export const homeStickyScopedAtoms = [homeCatalogPinnedAtom]
|
||||
@@ -0,0 +1,73 @@
|
||||
.header {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.headerCatalogTabs {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.marketplaceLogoLight {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.marketplaceLogoDark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(html[data-theme='dark']) .marketplaceLogoLight {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(html[data-theme='dark']) .marketplaceLogoDark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.catalogNavigationTrigger {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.catalogNavigation {
|
||||
position: sticky;
|
||||
top: 48px;
|
||||
z-index: 40;
|
||||
padding: 24px 36px 16px;
|
||||
}
|
||||
|
||||
.catalogNavigationPinned {
|
||||
border-bottom: 1px solid var(--color-divider-regular);
|
||||
padding-top: 16px;
|
||||
padding-bottom: 15px;
|
||||
background-color: var(--color-background-default);
|
||||
}
|
||||
|
||||
.catalogTabsPinned {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.categoriesPinned {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.catalogContent {
|
||||
min-height: calc(100vh - 112px);
|
||||
min-height: calc(100dvh - 112px);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.guide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search {
|
||||
position: relative;
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
.wrapper {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
flex: none;
|
||||
width: 36.9167%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.recommendVisual {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.recommendCards {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
overflow: hidden;
|
||||
padding: 42px 36px;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
top: 208px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contentTrack {
|
||||
transition: transform 400ms ease-out;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1 1 161px;
|
||||
width: auto;
|
||||
min-width: 161px;
|
||||
max-width: 210px;
|
||||
box-shadow: 0 8px 7.2px -6px rgb(0 0 0 / 19%);
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
@container (max-width: 751px) {
|
||||
.recommendCards > .card:nth-child(n + 4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 578px) {
|
||||
.recommendCards > .card:nth-child(n + 3) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 405px) {
|
||||
.recommendCards {
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x proximity;
|
||||
scrollbar-width: none;
|
||||
overscroll-behavior-x: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.recommendCards::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.recommendCards > .card:nth-child(n) {
|
||||
display: flex;
|
||||
flex: 0 0 161px;
|
||||
}
|
||||
}
|
||||
|
||||
.updatesArt {
|
||||
width: 33.3333%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.updatesDescription {
|
||||
display: -webkit-box;
|
||||
max-height: 40px;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.contentTrack {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1232px) {
|
||||
.marketplaceCopy {
|
||||
width: 443px;
|
||||
}
|
||||
|
||||
.updatesArt {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1260px) {
|
||||
.embeddedCopy {
|
||||
width: 431px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
'use client'
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
import type {
|
||||
BannerAd,
|
||||
BannerBlog,
|
||||
BannerEvent,
|
||||
BannerRecommend,
|
||||
BannerRecommendCard,
|
||||
PluginBanner,
|
||||
} from './banners'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from '#i18n'
|
||||
import Partner from '@/app/components/plugins/base/badges/partner'
|
||||
import Verified from '@/app/components/plugins/base/badges/verified'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import Link from '@/next/link'
|
||||
import background from './assets/background.jpg'
|
||||
import difyUpdatesArt from './assets/dify-updates-art.png'
|
||||
import styles from './home-trending.module.css'
|
||||
|
||||
const AUTOPLAY_DELAY = 5000
|
||||
const PAGINATION_DOT_SIZE = 6
|
||||
const PAGINATION_ACTIVE_WIDTH = 40
|
||||
const PAGINATION_GAP = 8
|
||||
const PAGINATION_STEP = PAGINATION_DOT_SIZE + PAGINATION_GAP
|
||||
const PAGINATION_ACTIVE_SHIFT = PAGINATION_ACTIVE_WIDTH - PAGINATION_DOT_SIZE
|
||||
|
||||
const getPaginationItemOffset = (index: number, selectedIndex: number) =>
|
||||
index * PAGINATION_STEP + (index > selectedIndex ? PAGINATION_ACTIVE_SHIFT : 0)
|
||||
|
||||
type AutoplayPauseReason = 'focus' | 'hover' | 'reduced-motion' | 'user' | 'visibility'
|
||||
|
||||
function TrendingCopy({
|
||||
banner,
|
||||
isMarketplacePlatform,
|
||||
}: {
|
||||
banner: BannerRecommend
|
||||
isMarketplacePlatform: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('plugin')
|
||||
const heading = banner.content.heading || t(($) => $['marketplace.home.trendingTitle'])
|
||||
const description =
|
||||
banner.content.description ||
|
||||
banner.content.subheadings?.join(' · ') ||
|
||||
t(($) => $['marketplace.home.trendingDescription'])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
styles.copy,
|
||||
'flex min-w-0 flex-col items-start overflow-hidden p-5',
|
||||
isMarketplacePlatform ? styles.marketplaceCopy : styles.embeddedCopy,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-col items-start gap-2 overflow-hidden">
|
||||
<p className="shrink-0 rounded-sm bg-state-accent-hover-alt px-1.5 py-0.5 text-[10px] leading-3 font-semibold tracking-[-0.2px] text-text-accent">
|
||||
{banner.title}
|
||||
</p>
|
||||
<h2 className="shrink-0 text-xl leading-6 font-semibold tracking-[-0.4px] text-text-primary">
|
||||
{heading}
|
||||
</h2>
|
||||
<p className="w-full text-[13px] leading-5 font-normal tracking-[-0.065px] text-text-tertiary">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getMarketplaceAssetURL = (path?: string) => {
|
||||
if (!path) return ''
|
||||
if (/^https?:\/\//.test(path) || path.startsWith('/_next/')) return path
|
||||
|
||||
try {
|
||||
const apiURL = new URL(MARKETPLACE_API_PREFIX)
|
||||
if (path.startsWith('/api/')) return `${apiURL.origin}${path}`
|
||||
return `${MARKETPLACE_API_PREFIX.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
||||
} catch {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
const getLocalCardHref = (card: BannerRecommendCard) => {
|
||||
if (card.item_type === 'plugin') {
|
||||
const [organization, pluginName] = card.item_id.split('/')
|
||||
if (organization && pluginName)
|
||||
return `/plugin/${encodeURIComponent(organization)}/${encodeURIComponent(pluginName)}`
|
||||
}
|
||||
|
||||
if (card.item_type === 'template') return `/templates?tid=${encodeURIComponent(card.item_id)}`
|
||||
|
||||
return '/'
|
||||
}
|
||||
|
||||
const getCardHref = (card: BannerRecommendCard, isMarketplacePlatform: boolean) => {
|
||||
if (!isMarketplacePlatform && card.link) return card.link
|
||||
return getLocalCardHref(card)
|
||||
}
|
||||
|
||||
const getCardCreator = (card: BannerRecommendCard) => {
|
||||
if (card.creator) return card.creator
|
||||
if (card.item_type !== 'plugin') return ''
|
||||
|
||||
return card.item_id.split('/')[0] || ''
|
||||
}
|
||||
|
||||
function TrendingCard({
|
||||
card,
|
||||
isMarketplacePlatform,
|
||||
}: {
|
||||
card: BannerRecommendCard
|
||||
isMarketplacePlatform: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('plugin')
|
||||
const iconURL = getMarketplaceAssetURL(card.icon_url)
|
||||
const creator = getCardCreator(card)
|
||||
const href = getCardHref(card, isMarketplacePlatform)
|
||||
const opensInNewTab = !isMarketplacePlatform && /^https?:\/\//.test(href)
|
||||
const isPartner = card.badges?.includes('partner')
|
||||
const isVerified = card.badges?.includes('verified')
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
target={opensInNewTab ? '_blank' : undefined}
|
||||
rel={opensInNewTab ? 'noopener noreferrer' : undefined}
|
||||
aria-label={card.display_name}
|
||||
className={cn(
|
||||
styles.card,
|
||||
'flex h-[116px] shrink-0 flex-col items-start justify-between overflow-hidden rounded-lg bg-background-default-dodge p-3.5 outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-[10px] border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge"
|
||||
style={{
|
||||
backgroundColor: !iconURL ? card.icon_background : undefined,
|
||||
}}
|
||||
>
|
||||
{iconURL ? (
|
||||
<img
|
||||
src={iconURL}
|
||||
width={40}
|
||||
height={40}
|
||||
alt=""
|
||||
aria-hidden
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
) : card.icon ? (
|
||||
<span className="text-xl leading-none">{card.icon}</span>
|
||||
) : (
|
||||
<span aria-hidden="true" className="i-ri-image-line size-5 text-text-quaternary" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-end gap-1">
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start gap-[3px]">
|
||||
<div className="flex w-full min-w-0 items-center gap-[3px]">
|
||||
<h3 className="min-w-0 truncate text-sm leading-[normal] font-medium text-text-primary">
|
||||
{card.display_name}
|
||||
</h3>
|
||||
{(isPartner || isVerified) && (
|
||||
<div className="flex shrink-0 items-start gap-[3.5px]">
|
||||
{isPartner && (
|
||||
<Partner className="size-3.5" text={t(($) => $['marketplace.partnerTip'])} />
|
||||
)}
|
||||
{isVerified && (
|
||||
<Verified className="size-3.5" text={t(($) => $['marketplace.verifiedTip'])} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{creator && (
|
||||
<p className="w-full truncate text-xs leading-[normal] font-normal text-text-tertiary">
|
||||
{t(($) => $['marketplace.home.trendingByCreator'], { creator })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="shrink-0 rounded-full bg-background-section-burn px-1.5 py-[3px] text-[10px] leading-3 font-normal text-text-primary">
|
||||
{t(($) => $['marketplace.home.trendingView'])}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function TrendingRecommendationSlide({
|
||||
banner,
|
||||
isMarketplacePlatform,
|
||||
}: {
|
||||
banner: BannerRecommend
|
||||
isMarketplacePlatform: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
styles.recommendSlide,
|
||||
'flex h-[200px] w-full overflow-hidden rounded-2xl bg-background-body',
|
||||
)}
|
||||
>
|
||||
<TrendingCopy banner={banner} isMarketplacePlatform={isMarketplacePlatform} />
|
||||
<div
|
||||
className={cn(
|
||||
styles.recommendVisual,
|
||||
'relative h-[200px] shrink-0 overflow-hidden rounded-xl bg-background-body',
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={background.src}
|
||||
width={3840}
|
||||
height={2160}
|
||||
alt=""
|
||||
aria-hidden
|
||||
className="absolute top-[-173px] left-[-990px] h-[1201px] w-[2135px] max-w-none opacity-80"
|
||||
/>
|
||||
<div aria-hidden className="absolute inset-0 bg-text-accent mix-blend-color" />
|
||||
|
||||
<div className={cn(styles.recommendCards, 'relative z-10 h-full items-center')}>
|
||||
{banner.content.cards.map((card) => (
|
||||
<TrendingCard
|
||||
key={`${card.item_type}:${card.item_id}`}
|
||||
card={card}
|
||||
isMarketplacePlatform={isMarketplacePlatform}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BlogBannerSlide({ banner }: { banner: BannerBlog }) {
|
||||
const opensInNewTab = /^https?:\/\//.test(banner.content.link)
|
||||
|
||||
return (
|
||||
<div className="flex h-[200px] w-full overflow-hidden rounded-2xl bg-background-body">
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start overflow-hidden px-6 py-5">
|
||||
<div className="flex min-h-0 w-full flex-1 flex-col items-start gap-2">
|
||||
<p className="shrink-0 rounded-sm bg-state-success-hover-alt px-1.5 py-0.5 text-[10px] leading-3 font-semibold tracking-[-0.2px] text-text-success">
|
||||
{banner.title}
|
||||
</p>
|
||||
<div className="flex min-h-0 w-full max-w-[800px] flex-1 flex-col items-start gap-3">
|
||||
<h2 className="shrink-0 text-xl leading-6 font-semibold tracking-[-0.4px] text-text-primary">
|
||||
{banner.content.blog_title}
|
||||
</h2>
|
||||
<div className="flex min-h-0 w-full flex-1 flex-col items-start gap-2">
|
||||
{banner.content.subtitle && (
|
||||
<p className="shrink-0 text-[15px] leading-[18px] font-normal tracking-[-0.3px] text-text-primary">
|
||||
{banner.content.subtitle}
|
||||
</p>
|
||||
)}
|
||||
{banner.content.description && (
|
||||
<p className="min-h-0 w-full flex-1 overflow-hidden text-[13px] leading-5 font-normal tracking-[-0.065px] text-text-tertiary">
|
||||
<span className={styles.updatesDescription}>{banner.content.description}</span>
|
||||
</p>
|
||||
)}
|
||||
<Link
|
||||
href={banner.content.link}
|
||||
target={opensInNewTab ? '_blank' : undefined}
|
||||
rel={opensInNewTab ? 'noopener noreferrer' : undefined}
|
||||
aria-label={`Read more about ${banner.content.blog_title}`}
|
||||
className="flex shrink-0 items-center gap-1 text-[13px] leading-[normal] font-medium text-text-accent underline decoration-[10%] underline-offset-2"
|
||||
>
|
||||
<span>Read more</span>
|
||||
<span aria-hidden className="i-ri-arrow-right-s-line size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src={difyUpdatesArt.src}
|
||||
width={400}
|
||||
height={200}
|
||||
alt=""
|
||||
aria-hidden
|
||||
className={cn(styles.updatesArt, 'h-[200px] shrink-0 object-cover')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ImageBannerSlide({ banner }: { banner: BannerEvent | BannerAd }) {
|
||||
const desktopImage = getMarketplaceAssetURL(banner.content.images.desktop)
|
||||
const tabletImage = getMarketplaceAssetURL(banner.content.images.tablet)
|
||||
const mobileImage = getMarketplaceAssetURL(banner.content.images.mobile)
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={banner.content.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={banner.content.alt_text || banner.title}
|
||||
className="block h-[200px] w-full overflow-hidden rounded-2xl outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
>
|
||||
<picture className="block size-full">
|
||||
{mobileImage && <source media="(max-width: 639px)" srcSet={mobileImage} />}
|
||||
{tabletImage && <source media="(max-width: 1023px)" srcSet={tabletImage} />}
|
||||
<img
|
||||
src={desktopImage}
|
||||
width={1200}
|
||||
height={200}
|
||||
alt=""
|
||||
aria-hidden
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeBannerSlide({
|
||||
banner,
|
||||
isMarketplacePlatform,
|
||||
}: {
|
||||
banner: PluginBanner
|
||||
isMarketplacePlatform: boolean
|
||||
}) {
|
||||
if (banner.style_type === 'blog') return <BlogBannerSlide banner={banner} />
|
||||
|
||||
if (banner.style_type === 'event' || banner.style_type === 'ad')
|
||||
return <ImageBannerSlide banner={banner} />
|
||||
|
||||
return (
|
||||
<TrendingRecommendationSlide banner={banner} isMarketplacePlatform={isMarketplacePlatform} />
|
||||
)
|
||||
}
|
||||
|
||||
function TrendingNavigation({
|
||||
banners,
|
||||
selectedIndex,
|
||||
carouselRootRef,
|
||||
onSelect,
|
||||
onNext,
|
||||
}: {
|
||||
banners: PluginBanner[]
|
||||
selectedIndex: number
|
||||
carouselRootRef: RefObject<HTMLDivElement | null>
|
||||
onSelect: (index: number) => void
|
||||
onNext: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('plugin')
|
||||
const progressRef = useRef<HTMLSpanElement>(null)
|
||||
const progressAnimationRef = useRef<Animation | null>(null)
|
||||
const pauseReasonsRef = useRef(new Set<AutoplayPauseReason>())
|
||||
const [isUserPaused, setIsUserPaused] = useState(false)
|
||||
const [isReducedMotionPaused, setIsReducedMotionPaused] = useState(false)
|
||||
const isExplicitlyPaused = isUserPaused || isReducedMotionPaused
|
||||
const paginationWidth =
|
||||
PAGINATION_ACTIVE_WIDTH + Math.max(0, banners.length - 1) * PAGINATION_STEP
|
||||
|
||||
const setPauseReason = useCallback((reason: AutoplayPauseReason, shouldPause: boolean) => {
|
||||
if (shouldPause) pauseReasonsRef.current.add(reason)
|
||||
else pauseReasonsRef.current.delete(reason)
|
||||
|
||||
const progressAnimation = progressAnimationRef.current
|
||||
if (!progressAnimation) return
|
||||
|
||||
if (pauseReasonsRef.current.size > 0) progressAnimation.pause()
|
||||
else progressAnimation.play()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const progressElement = progressRef.current
|
||||
if (!progressElement?.animate) return
|
||||
|
||||
const progressAnimation = progressElement.animate(
|
||||
[{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }],
|
||||
{
|
||||
duration: AUTOPLAY_DELAY,
|
||||
easing: 'linear',
|
||||
fill: 'forwards',
|
||||
},
|
||||
)
|
||||
progressAnimationRef.current = progressAnimation
|
||||
|
||||
if (pauseReasonsRef.current.size > 0) progressAnimation.pause()
|
||||
progressAnimation.onfinish = onNext
|
||||
|
||||
return () => {
|
||||
progressAnimation.onfinish = null
|
||||
progressAnimation.cancel()
|
||||
if (progressAnimationRef.current === progressAnimation) progressAnimationRef.current = null
|
||||
}
|
||||
}, [onNext, selectedIndex])
|
||||
|
||||
useEffect(() => {
|
||||
const carouselRoot = carouselRootRef.current
|
||||
if (!carouselRoot) return
|
||||
|
||||
const handleMouseEnter = () => setPauseReason('hover', true)
|
||||
const handleMouseLeave = () => setPauseReason('hover', false)
|
||||
const handleFocusIn = () => setPauseReason('focus', true)
|
||||
const handleFocusOut = (event: FocusEvent) => {
|
||||
if (carouselRoot.contains(event.relatedTarget as Node | null)) return
|
||||
setPauseReason('focus', false)
|
||||
}
|
||||
const handleVisibilityChange = () =>
|
||||
setPauseReason('visibility', document.visibilityState === 'hidden')
|
||||
|
||||
carouselRoot.addEventListener('mouseenter', handleMouseEnter)
|
||||
carouselRoot.addEventListener('mouseleave', handleMouseLeave)
|
||||
carouselRoot.addEventListener('focusin', handleFocusIn)
|
||||
carouselRoot.addEventListener('focusout', handleFocusOut)
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
return () => {
|
||||
carouselRoot.removeEventListener('mouseenter', handleMouseEnter)
|
||||
carouselRoot.removeEventListener('mouseleave', handleMouseLeave)
|
||||
carouselRoot.removeEventListener('focusin', handleFocusIn)
|
||||
carouselRoot.removeEventListener('focusout', handleFocusOut)
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
}, [carouselRootRef, setPauseReason])
|
||||
|
||||
useEffect(() => {
|
||||
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
|
||||
const syncReducedMotion = () => {
|
||||
// oxlint-disable-next-line eslint-react/set-state-in-effect -- This state mirrors an external media query.
|
||||
setIsReducedMotionPaused(reducedMotionQuery.matches)
|
||||
setPauseReason('reduced-motion', reducedMotionQuery.matches)
|
||||
}
|
||||
|
||||
syncReducedMotion()
|
||||
reducedMotionQuery.addEventListener('change', syncReducedMotion)
|
||||
|
||||
return () => reducedMotionQuery.removeEventListener('change', syncReducedMotion)
|
||||
}, [setPauseReason])
|
||||
|
||||
const toggleAutoplay = () => {
|
||||
if (isExplicitlyPaused) {
|
||||
setIsUserPaused(false)
|
||||
setIsReducedMotionPaused(false)
|
||||
setPauseReason('user', false)
|
||||
setPauseReason('reduced-motion', false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsUserPaused(true)
|
||||
setPauseReason('user', true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label={t(($) => $['marketplace.home.trendingPaginationLabel'])}
|
||||
className={cn(
|
||||
styles.navigation,
|
||||
'absolute right-0 z-10 flex h-[22px] items-center gap-2 px-5 py-2',
|
||||
)}
|
||||
>
|
||||
<div className="relative h-1.5 shrink-0" style={{ width: paginationWidth }}>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute top-0 left-0 z-1 flex h-1.5 w-10 items-center overflow-hidden rounded-full bg-state-base-handle transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
|
||||
style={{
|
||||
transform: `translate3d(${selectedIndex * PAGINATION_STEP}px, 0, 0)`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
key={selectedIndex}
|
||||
ref={progressRef}
|
||||
data-carousel-progress
|
||||
className="h-full w-full rounded-full bg-text-accent"
|
||||
style={{ transform: 'scaleX(0)', transformOrigin: 'left center' }}
|
||||
/>
|
||||
</span>
|
||||
{banners.map((banner, index) => {
|
||||
const isCurrent = index === selectedIndex
|
||||
|
||||
return (
|
||||
<button
|
||||
key={banner.id}
|
||||
type="button"
|
||||
aria-label={banner.title}
|
||||
aria-current={isCurrent ? 'true' : undefined}
|
||||
onClick={() => {
|
||||
if (isCurrent) return
|
||||
onSelect(index)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return
|
||||
event.preventDefault()
|
||||
if (isCurrent) return
|
||||
onSelect(index)
|
||||
}}
|
||||
className={cn(
|
||||
'absolute top-0 left-0 z-2 h-1.5 overflow-hidden rounded-full outline-hidden transition-[transform,width,background-color] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] after:absolute after:-inset-2 hover:bg-state-base-handle-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid motion-reduce:transition-none',
|
||||
isCurrent ? 'bg-transparent' : 'bg-state-base-handle',
|
||||
)}
|
||||
style={{
|
||||
width: isCurrent ? PAGINATION_ACTIVE_WIDTH : PAGINATION_DOT_SIZE,
|
||||
transform: `translate3d(${getPaginationItemOffset(index, selectedIndex)}px, 0, 0)`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1" />
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(
|
||||
($) =>
|
||||
$[
|
||||
isExplicitlyPaused
|
||||
? 'marketplace.home.trendingPlay'
|
||||
: 'marketplace.home.trendingPause'
|
||||
],
|
||||
)}
|
||||
onClick={toggleAutoplay}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return
|
||||
event.preventDefault()
|
||||
toggleAutoplay()
|
||||
}}
|
||||
className="flex size-4 shrink-0 items-center justify-center rounded-full bg-state-base-handle text-text-primary outline-hidden hover:bg-state-base-handle-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
>
|
||||
{isExplicitlyPaused ? (
|
||||
<span aria-hidden className="i-ri-play-large-fill size-2 opacity-30" />
|
||||
) : (
|
||||
<span aria-hidden className="i-ri-pause-large-fill size-2 opacity-30" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeTrending({
|
||||
banners,
|
||||
isMarketplacePlatform,
|
||||
}: {
|
||||
banners: PluginBanner[]
|
||||
isMarketplacePlatform: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('plugin')
|
||||
const carouselRootRef = useRef<HTMLDivElement>(null)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const selectSlide = useCallback((index: number) => setSelectedIndex(index), [])
|
||||
const selectNextSlide = useCallback(
|
||||
() => setSelectedIndex((currentIndex) => (currentIndex + 1) % banners.length),
|
||||
[banners.length],
|
||||
)
|
||||
|
||||
if (banners.length === 0) return null
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={t(($) => $['marketplace.home.trendingTitle'])}
|
||||
className={cn(
|
||||
'shrink-0 bg-background-default pb-6',
|
||||
isMarketplacePlatform ? 'px-4 min-[1232px]:px-0' : 'px-4 md:px-9',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
styles.wrapper,
|
||||
'mx-auto w-full',
|
||||
isMarketplacePlatform ? 'max-w-[1200px]' : 'max-w-[1188px]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
aria-label={t(($) => $['marketplace.home.trendingTitle'])}
|
||||
className="relative h-[200px] w-full rounded-2xl"
|
||||
>
|
||||
<TrendingNavigation
|
||||
banners={banners}
|
||||
selectedIndex={selectedIndex}
|
||||
carouselRootRef={carouselRootRef}
|
||||
onSelect={selectSlide}
|
||||
onNext={selectNextSlide}
|
||||
/>
|
||||
<div ref={carouselRootRef} className="h-full overflow-hidden rounded-2xl">
|
||||
<div
|
||||
aria-live="polite"
|
||||
className={cn(styles.contentTrack, 'flex h-full')}
|
||||
style={{ transform: `translate3d(-${selectedIndex * 100}%, 0, 0)` }}
|
||||
>
|
||||
{banners.map((banner, index) => {
|
||||
const isActive = index === selectedIndex
|
||||
|
||||
return (
|
||||
<div
|
||||
key={banner.id}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
aria-label={banner.title}
|
||||
aria-hidden={!isActive}
|
||||
inert={!isActive}
|
||||
className="h-full min-w-0 shrink-0 grow-0 basis-full"
|
||||
>
|
||||
<HomeBannerSlide
|
||||
banner={banner}
|
||||
isMarketplacePlatform={isMarketplacePlatform}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeTrending
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { PluginBanner } from './banners'
|
||||
import ListWrapper from '../list/list-wrapper'
|
||||
import HomeCatalogNavigation from './home-catalog-navigation'
|
||||
import HomeCatalogTabs from './home-catalog-tabs'
|
||||
import HomeHeader from './home-header'
|
||||
import HomeHero from './home-hero'
|
||||
import HomeSearch from './home-search'
|
||||
import { HomeStickyStateProvider } from './home-sticky-state-provider'
|
||||
import styles from './home-sticky.module.css'
|
||||
import HomeTrending from './home-trending'
|
||||
|
||||
type MarketplaceHomeProps = {
|
||||
actions?: React.ReactNode
|
||||
banners: PluginBanner[]
|
||||
isMarketplacePlatform: boolean
|
||||
linkToMarketplaceDetail: boolean
|
||||
showInstallButton: boolean
|
||||
}
|
||||
|
||||
const MarketplaceHome = ({
|
||||
actions,
|
||||
banners,
|
||||
isMarketplacePlatform,
|
||||
linkToMarketplaceDetail,
|
||||
showInstallButton,
|
||||
}: MarketplaceHomeProps) => {
|
||||
return (
|
||||
<HomeStickyStateProvider>
|
||||
<div className="flex min-h-full w-full flex-col bg-background-default">
|
||||
<HomeHeader actions={actions} isMarketplacePlatform={isMarketplacePlatform} />
|
||||
<div className="relative flex w-full flex-col">
|
||||
<HomeHero isMarketplacePlatform={isMarketplacePlatform} />
|
||||
<HomeSearch />
|
||||
{banners.length > 0 && (
|
||||
<>
|
||||
<div aria-hidden="true" className="h-12 shrink-0" />
|
||||
<HomeTrending banners={banners} isMarketplacePlatform={isMarketplacePlatform} />
|
||||
</>
|
||||
)}
|
||||
<HomeCatalogNavigation
|
||||
catalogTabs={<HomeCatalogTabs isMarketplacePlatform={isMarketplacePlatform} />}
|
||||
/>
|
||||
<div className="contents [&>div]:bg-background-default!">
|
||||
<ListWrapper
|
||||
className={styles.catalogContent}
|
||||
showInstallButton={showInstallButton}
|
||||
linkToMarketplaceDetail={linkToMarketplaceDetail}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HomeStickyStateProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarketplaceHome
|
||||