Compare commits
84
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bb01984ed | ||
|
|
77e41a37ee | ||
|
|
79229b7ede | ||
|
|
36ae648a63 | ||
|
|
c409d531da | ||
|
|
511ca69e4e | ||
|
|
4c7e60d7e7 | ||
|
|
a9e163403d | ||
|
|
bc136b89f9 | ||
|
|
b1bd4f9a8b | ||
|
|
06d3927f05 | ||
|
|
5c3516cae8 | ||
|
|
1c18d8ddbd | ||
|
|
253dfe9351 | ||
|
|
2d3999e984 | ||
|
|
a431cc726d | ||
|
|
08493d2429 | ||
|
|
8dd0969006 | ||
|
|
2d80d3c35c | ||
|
|
4da764904c | ||
|
|
5c4f4fd1ef | ||
|
|
54843971ac | ||
|
|
5ec5d3aeb9 | ||
|
|
d661b53e49 | ||
|
|
fd1e777f85 | ||
|
|
e5f77ce185 | ||
|
|
99929d1c16 | ||
|
|
c699aa11db | ||
|
|
06cd0b56ac | ||
|
|
241a9e1fec | ||
|
|
3d79689fb5 | ||
|
|
9f050f7957 | ||
|
|
cb691d47d3 | ||
|
|
04f5ee58d0 | ||
|
|
f5cff724f4 | ||
|
|
1f4ccafdea | ||
|
|
163049f6ca | ||
|
|
65e7507ca3 | ||
|
|
ba157f9604 | ||
|
|
0dc913630e | ||
|
|
875cd30b1f | ||
|
|
aa4a32ae84 | ||
|
|
8573e14777 | ||
|
|
1855be234c | ||
|
|
9bb960ff12 | ||
|
|
1c14c7d467 | ||
|
|
61faec16ca | ||
|
|
57c836e692 | ||
|
|
626cc282b1 | ||
|
|
52624d54e3 | ||
|
|
5ce038ef92 | ||
|
|
30f4d4c0c6 | ||
|
|
510679a7d1 | ||
|
|
9237f2a14a | ||
|
|
bd178c7b29 | ||
|
|
d80947aa72 | ||
|
|
1618c37d26 | ||
|
|
701ab64462 | ||
|
|
b3298800e9 | ||
|
|
0f1c6b3f78 | ||
|
|
9b4b246aad | ||
|
|
1bd654a289 | ||
|
|
fc70329bdb | ||
|
|
cd8a82fbd4 | ||
|
|
550cb7eff5 | ||
|
|
2e748c16e9 | ||
|
|
577012b66d | ||
|
|
251c324180 | ||
|
|
865a618fd5 | ||
|
|
991116990a | ||
|
|
eb5d1da0e8 | ||
|
|
dce3b7a7fc | ||
|
|
be386aba3b | ||
|
|
02e51e7d7c | ||
|
|
96b6d4f2c0 | ||
|
|
4c84c5957d | ||
|
|
34613ecdc5 | ||
|
|
6a14245401 | ||
|
|
a758ca2aef | ||
|
|
9e60d4e213 | ||
|
|
ef29c8442c | ||
|
|
a1b45415ac | ||
|
|
7fc46d75bd | ||
|
|
953a4ef0ca |
@@ -1,7 +1,6 @@
|
||||
name: Deploy Knowledge
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
on:
|
||||
@@ -19,48 +18,6 @@ jobs:
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_branch == 'deploy/konwledge'
|
||||
steps:
|
||||
- name: Wait for KnowledgeFS CI
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
timeout-minutes: 35
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const workflowId = "knowledge-fs-ci.yml";
|
||||
const headBranch = context.payload.workflow_run.head_branch;
|
||||
const headSha = context.payload.workflow_run.head_sha;
|
||||
const deadline = Date.now() + 30 * 60 * 1000;
|
||||
const pollIntervalMs = 15 * 1000;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const { data } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: workflowId,
|
||||
branch: headBranch,
|
||||
event: "push",
|
||||
head_sha: headSha,
|
||||
per_page: 10,
|
||||
});
|
||||
const run = data.workflow_runs[0];
|
||||
|
||||
if (!run) {
|
||||
core.info(`Waiting for ${workflowId} to start for ${headSha}.`);
|
||||
} else if (run.status !== "completed") {
|
||||
core.info(`Waiting for ${run.html_url}; current status is ${run.status}.`);
|
||||
} else if (run.conclusion !== "success") {
|
||||
throw new Error(
|
||||
`${workflowId} did not succeed for ${headSha}: ${run.conclusion} (${run.html_url})`,
|
||||
);
|
||||
} else {
|
||||
core.info(`KnowledgeFS CI succeeded for ${headSha}: ${run.html_url}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for ${workflowId} to succeed for ${headSha}.`);
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
||||
with:
|
||||
|
||||
+2
-2
@@ -99,9 +99,9 @@ ENV VIRTUAL_ENV=/app/api/.venv
|
||||
COPY --from=packages --chown=dify:dify ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
|
||||
|
||||
# Download nltk data
|
||||
RUN mkdir -p /usr/local/share/nltk_data \
|
||||
&& NLTK_DATA=/usr/local/share/nltk_data python -m nltk.downloader punkt_tab averaged_perceptron_tagger_eng stopwords \
|
||||
&& NLTK_DATA=/usr/local/share/nltk_data python -c "import nltk; nltk.data.find('tokenizers/punkt_tab'); nltk.data.find('taggers/averaged_perceptron_tagger_eng'); nltk.data.find('corpora/stopwords')" \
|
||||
&& NLTK_DATA=/usr/local/share/nltk_data python -c "import nltk; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger'); nltk.download('stopwords')" \
|
||||
&& chmod -R 755 /usr/local/share/nltk_data
|
||||
|
||||
ENV TIKTOKEN_CACHE_DIR=/app/api/.tiktoken_cache
|
||||
|
||||
@@ -52,7 +52,7 @@ def with_session[T, **P, R](
|
||||
session.commit()
|
||||
return result
|
||||
except Exception:
|
||||
session.rollback() # guard-ignore: no-new-controller-sqlalchemy -- decorator owns rollback
|
||||
session.rollback() # noqa: no-new-controller-sqlalchemy decorator owns transaction rollback
|
||||
raise
|
||||
|
||||
with session_factory.create_session() as session:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -317,7 +317,7 @@ class _WorkflowResponseSource:
|
||||
self._session = session
|
||||
|
||||
def __getattr__(self, name: str) -> object:
|
||||
return getattr(self._workflow, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self._workflow, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
@property
|
||||
def created_by_account(self) -> Account | None:
|
||||
|
||||
@@ -218,7 +218,7 @@ class _DatasetQueryResponseSource:
|
||||
return self.query.get_queries(session=self.session)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self.query, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self.query, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
|
||||
class DatasetQueryListResponse(ResponseModel):
|
||||
@@ -257,7 +257,7 @@ class _RelatedAppResponseSource:
|
||||
return self.app.mode_compatible_with_agent_with_session(session=self.session)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self.app, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self.app, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
|
||||
class RelatedAppListResponse(ResponseModel):
|
||||
|
||||
@@ -106,7 +106,7 @@ class ExternalKnowledgeApiResponseSource:
|
||||
return self.external_knowledge_api.get_dataset_bindings(session=self.session)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self.external_knowledge_api, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self.external_knowledge_api, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
|
||||
def external_knowledge_api_response(
|
||||
|
||||
@@ -404,7 +404,7 @@ class TrialWorkflowResponseSource:
|
||||
return self.workflow.get_tool_published(session=self.session)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self.workflow, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self.workflow, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
|
||||
register_schema_models(
|
||||
|
||||
@@ -59,7 +59,7 @@ class TagBindingRemovePayload(BaseModel):
|
||||
|
||||
|
||||
class TagListQueryParam(BaseModel):
|
||||
type: Literal["knowledge", "app", "snippet", ""] = Field("", description="Tag type filter")
|
||||
type: TagType | Literal[""] = Field("", description="Tag type filter")
|
||||
keyword: str | None = Field(None, description="Search keyword")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,834 @@
|
||||
"""Console API for workspace-level Skill Management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
from flask import request, send_file
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
from controllers.common.fields import BinaryFileResponse
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
from fields.base import ResponseModel
|
||||
from libs import helper
|
||||
from libs.helper import dump_response
|
||||
from libs.login import login_required
|
||||
from models.account import Account
|
||||
from services.skill_management_service import (
|
||||
SkillAssistMessagePayload,
|
||||
SkillCreatePayload,
|
||||
SkillDraftFileOperationPayload,
|
||||
SkillDraftTreePayload,
|
||||
SkillImportPayload,
|
||||
SkillManagementService,
|
||||
SkillManagementServiceError,
|
||||
SkillMetadataPayload,
|
||||
SkillPublishPayload,
|
||||
SkillRestorePayload,
|
||||
SkillVersionUpdatePayload,
|
||||
)
|
||||
|
||||
_FILE_UPLOAD_PARAMS = {
|
||||
"file": {
|
||||
"description": "Skill draft file payload",
|
||||
"in": "formData",
|
||||
"type": "file",
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class WorkspaceSkillsQuery(BaseModel):
|
||||
keyword: str | None = Field(default=None, description="Search keyword matching skill name or description.")
|
||||
page: int = Field(default=1, ge=1, le=99999, description="Page number.")
|
||||
limit: int = Field(default=20, ge=1, le=100, description="Number of items per page.")
|
||||
tag: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Skill tag filters. Repeat the parameter for multiple tags.",
|
||||
)
|
||||
|
||||
|
||||
class SkillDeletePayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
confirmation_name: str | None = Field(
|
||||
default=None,
|
||||
description="Required when deleting a referenced Skill. Must match the Skill name.",
|
||||
)
|
||||
|
||||
|
||||
class AgentSkillBindingsPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
skill_ids: list[str] = Field(default_factory=list, description="Ordered Skill IDs bound to the Agent.")
|
||||
|
||||
|
||||
class SkillFileQuery(BaseModel):
|
||||
path: str = Field(description="Skill file path relative to the Skill root.")
|
||||
version_id: str | None = Field(default=None, description="Optional published version ID. Omit for current draft.")
|
||||
|
||||
|
||||
class SkillResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
display_name: str
|
||||
icon: str
|
||||
description: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
name_manually_edited: bool = False
|
||||
visibility: str
|
||||
latest_published_version_id: str | None = None
|
||||
reference_count: int = 0
|
||||
created_by: str | None = None
|
||||
created_by_name: str | None = None
|
||||
updated_by: str | None = None
|
||||
updated_by_name: str | None = None
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
|
||||
class SkillFileResponse(ResponseModel):
|
||||
id: str | None = None
|
||||
path: str
|
||||
kind: str
|
||||
storage: str | None = None
|
||||
mime_type: str | None = None
|
||||
content: str | None = None
|
||||
tool_file_id: str | None = None
|
||||
size: int | None = None
|
||||
hash: str | None = None
|
||||
|
||||
|
||||
class SkillFilePreviewResponse(ResponseModel):
|
||||
path: str
|
||||
mime_type: str
|
||||
content: str
|
||||
size: int
|
||||
hash: str
|
||||
|
||||
|
||||
class SkillFileUploadResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
mime_type: str
|
||||
size: int
|
||||
hash: str
|
||||
|
||||
|
||||
class SkillDetailResponse(SkillResponse):
|
||||
files: list[SkillFileResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillListResponse(ResponseModel):
|
||||
data: list[SkillResponse] = Field(default_factory=list)
|
||||
has_more: bool = False
|
||||
limit: int = 20
|
||||
page: int = 1
|
||||
total: int = 0
|
||||
|
||||
|
||||
class SkillTagResponse(ResponseModel):
|
||||
tag: str
|
||||
count: int
|
||||
|
||||
|
||||
class SkillTagListResponse(ResponseModel):
|
||||
data: list[SkillTagResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillVersionResponse(ResponseModel):
|
||||
id: str
|
||||
skill_id: str
|
||||
version_number: int
|
||||
version_name: str
|
||||
publish_note: str
|
||||
hash_code: str
|
||||
archive_size: int
|
||||
published_by: str | None = None
|
||||
published_by_name: str | None = None
|
||||
is_latest: bool = False
|
||||
created_at: int
|
||||
|
||||
|
||||
class SkillVersionListResponse(ResponseModel):
|
||||
data: list[SkillVersionResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillVersionDetailResponse(SkillVersionResponse):
|
||||
files: list[SkillFileResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillVersionDeleteResponse(ResponseModel):
|
||||
id: str
|
||||
deleted: bool
|
||||
latest_published_version_id: str | None = None
|
||||
|
||||
|
||||
class SkillReferenceResponse(ResponseModel):
|
||||
type: str
|
||||
agent_id: str
|
||||
agent_icon: str | None = None
|
||||
agent_icon_background: str | None = None
|
||||
agent_icon_type: str | None = None
|
||||
app_id: str | None = None
|
||||
name: str
|
||||
display_name: str
|
||||
workflow_id: str | None = None
|
||||
workflow_name: str | None = None
|
||||
workflow_icon: str | None = None
|
||||
workflow_icon_background: str | None = None
|
||||
workflow_icon_type: str | None = None
|
||||
workflow_version: str | None = None
|
||||
node_id: str | None = None
|
||||
node_name: str | None = None
|
||||
|
||||
|
||||
class SkillReferenceListResponse(ResponseModel):
|
||||
data: list[SkillReferenceResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SkillDeleteResponse(ResponseModel):
|
||||
id: str
|
||||
deleted: bool
|
||||
|
||||
|
||||
class AgentSkillBindingItemResponse(ResponseModel):
|
||||
id: str
|
||||
priority: int
|
||||
name: str
|
||||
display_name: str
|
||||
icon: str
|
||||
description: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
status: str
|
||||
file_count: int
|
||||
latest_published_version_id: str | None = None
|
||||
latest_published_at: int | None = None
|
||||
updated_at: int
|
||||
|
||||
|
||||
class AgentSkillBindingsResponse(ResponseModel):
|
||||
agent_id: str
|
||||
skill_ids: list[str] = Field(default_factory=list)
|
||||
data: list[AgentSkillBindingItemResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
WorkspaceSkillsQuery,
|
||||
SkillCreatePayload,
|
||||
SkillAssistMessagePayload,
|
||||
SkillMetadataPayload,
|
||||
SkillDraftFileOperationPayload,
|
||||
SkillDraftTreePayload,
|
||||
SkillPublishPayload,
|
||||
SkillRestorePayload,
|
||||
SkillVersionUpdatePayload,
|
||||
SkillDeletePayload,
|
||||
SkillFileQuery,
|
||||
AgentSkillBindingsPayload,
|
||||
)
|
||||
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
SkillResponse,
|
||||
SkillFileResponse,
|
||||
SkillFilePreviewResponse,
|
||||
SkillFileUploadResponse,
|
||||
SkillDetailResponse,
|
||||
SkillListResponse,
|
||||
SkillTagResponse,
|
||||
SkillTagListResponse,
|
||||
SkillVersionResponse,
|
||||
SkillVersionListResponse,
|
||||
SkillVersionDetailResponse,
|
||||
SkillVersionDeleteResponse,
|
||||
SkillReferenceResponse,
|
||||
SkillReferenceListResponse,
|
||||
SkillDeleteResponse,
|
||||
AgentSkillBindingItemResponse,
|
||||
AgentSkillBindingsResponse,
|
||||
BinaryFileResponse,
|
||||
)
|
||||
|
||||
|
||||
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, object], int]:
|
||||
body: dict[str, object] = {"code": exc.code, "message": exc.message}
|
||||
if exc.details:
|
||||
body["details"] = exc.details
|
||||
return body, exc.status_code
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills")
|
||||
class WorkspaceSkillsApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(WorkspaceSkillsQuery))
|
||||
@console_ns.response(200, "Workspace skills", console_ns.models[SkillListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str):
|
||||
query_input: dict[str, object] = {
|
||||
"keyword": request.args.get("keyword"),
|
||||
"tag": request.args.getlist("tag"),
|
||||
}
|
||||
if "limit" in request.args:
|
||||
query_input["limit"] = request.args.get("limit")
|
||||
if "page" in request.args:
|
||||
query_input["page"] = request.args.get("page")
|
||||
query = WorkspaceSkillsQuery.model_validate(query_input)
|
||||
result = SkillManagementService().list_skills(
|
||||
tenant_id=current_tenant_id,
|
||||
keyword=query.keyword,
|
||||
page=query.page,
|
||||
limit=query.limit,
|
||||
tags=[tag for tag in query.tag if tag],
|
||||
)
|
||||
return dump_response(SkillListResponse, result)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillCreatePayload.__name__])
|
||||
@console_ns.response(201, "Skill created", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
try:
|
||||
payload = SkillCreatePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().create_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result), 201
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/files/upload")
|
||||
class WorkspaceSkillFileUploadApi(Resource):
|
||||
@console_ns.doc(consumes=["multipart/form-data"], params=_FILE_UPLOAD_PARAMS)
|
||||
@console_ns.response(201, "Skill draft file uploaded", console_ns.models[SkillFileUploadResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
if "file" not in request.files:
|
||||
return {"code": "no_file_uploaded", "message": "no file uploaded"}, 400
|
||||
|
||||
file = request.files["file"]
|
||||
if not file.filename:
|
||||
return {"code": "filename_missing", "message": "filename is required"}, 400
|
||||
|
||||
result = SkillManagementService().upload_file(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
filename=file.filename,
|
||||
content=file.stream.read(),
|
||||
mime_type=file.mimetype,
|
||||
)
|
||||
return dump_response(SkillFileUploadResponse, result), 201
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/tags")
|
||||
class WorkspaceSkillTagsApi(Resource):
|
||||
@console_ns.response(200, "Workspace Skill tags", console_ns.models[SkillTagListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str):
|
||||
result = SkillManagementService().list_tags(tenant_id=current_tenant_id)
|
||||
return dump_response(SkillTagListResponse, result)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/import")
|
||||
class WorkspaceSkillImportApi(Resource):
|
||||
@console_ns.doc(description="Import a Skill zip package from multipart form field `file`.")
|
||||
@console_ns.response(201, "Skill imported", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
upload = request.files.get("file")
|
||||
if upload is None:
|
||||
return {"code": "invalid_request", "message": "file is required"}, 400
|
||||
try:
|
||||
payload = SkillImportPayload(content=upload.read(), filename=upload.filename or "skill.zip")
|
||||
result = SkillManagementService().import_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result), 201
|
||||
except (ValidationError, ValueError) as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>")
|
||||
class WorkspaceSkillApi(Resource):
|
||||
@console_ns.response(200, "Skill detail", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().get_skill(tenant_id=current_tenant_id, skill_id=skill_id)
|
||||
return dump_response(SkillDetailResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillMetadataPayload.__name__])
|
||||
@console_ns.response(200, "Skill updated", console_ns.models[SkillResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillMetadataPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().update_metadata(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillDeletePayload.__name__])
|
||||
@console_ns.response(200, "Skill deleted", console_ns.models[SkillDeleteResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
payload = SkillDeletePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().delete_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
confirmation_name=payload.confirmation_name,
|
||||
)
|
||||
return dump_response(SkillDeleteResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/duplicate")
|
||||
class WorkspaceSkillDuplicateApi(Resource):
|
||||
@console_ns.response(201, "Skill duplicated", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().duplicate_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result), 201
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/export")
|
||||
class WorkspaceSkillExportApi(Resource):
|
||||
@console_ns.response(200, "Published Skill zip archive")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().pull_published_archive(tenant_id=current_tenant_id, skill_id=skill_id)
|
||||
return send_file(
|
||||
io.BytesIO(result.payload),
|
||||
mimetype=result.mime_type,
|
||||
as_attachment=True,
|
||||
download_name=result.filename,
|
||||
)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/assist/messages")
|
||||
class WorkspaceSkillAssistMessageApi(Resource):
|
||||
"""Stream read-only Skill Authoring suggestions from the default workspace model."""
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillAssistMessagePayload.__name__])
|
||||
@console_ns.response(200, "Skill Authoring assistant event stream")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillAssistMessagePayload.model_validate(console_ns.payload or {})
|
||||
response = SkillManagementService().create_assistant_action_stream(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
user_id=current_user.id,
|
||||
message=payload.message,
|
||||
attachments=payload.attachments,
|
||||
model_payload=payload.model,
|
||||
target_path=payload.target_path,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
return helper.compact_generate_response(response)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files")
|
||||
class WorkspaceSkillFilesApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SkillDraftFileOperationPayload.__name__])
|
||||
@console_ns.response(200, "Draft file operation applied", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillDraftFileOperationPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().apply_draft_file_operation(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillDraftTreePayload.__name__])
|
||||
@console_ns.response(200, "Draft files replaced", console_ns.models[SkillDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillDraftTreePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().replace_draft_tree(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillDetailResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/preview")
|
||||
class WorkspaceSkillFilePreviewApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(SkillFileQuery))
|
||||
@console_ns.response(200, "Skill file text preview", console_ns.models[SkillFilePreviewResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
query = SkillFileQuery.model_validate(
|
||||
{
|
||||
"path": request.args.get("path"),
|
||||
"version_id": request.args.get("version_id"),
|
||||
}
|
||||
)
|
||||
result = SkillManagementService().preview_file(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
path=query.path,
|
||||
version_id=query.version_id,
|
||||
)
|
||||
return dump_response(SkillFilePreviewResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/content")
|
||||
class WorkspaceSkillFileContentApi(Resource):
|
||||
@console_ns.doc(params={**query_params_from_model(SkillFileQuery), "download": "Return as an attachment when 1."})
|
||||
@console_ns.response(200, "Skill file content", console_ns.models[BinaryFileResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
query = SkillFileQuery.model_validate(
|
||||
{
|
||||
"path": request.args.get("path"),
|
||||
"version_id": request.args.get("version_id"),
|
||||
}
|
||||
)
|
||||
result = SkillManagementService().pull_file(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
path=query.path,
|
||||
version_id=query.version_id,
|
||||
)
|
||||
return send_file(
|
||||
io.BytesIO(result.payload),
|
||||
mimetype=result.mime_type,
|
||||
as_attachment=request.args.get("download") == "1",
|
||||
download_name=result.filename,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except ValueError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/publish")
|
||||
class WorkspaceSkillPublishApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SkillPublishPayload.__name__])
|
||||
@console_ns.response(200, "Skill published", console_ns.models[SkillVersionResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillPublishPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().publish_skill(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillVersionResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/restore")
|
||||
class WorkspaceSkillRestoreApi(Resource):
|
||||
@console_ns.expect(console_ns.models[SkillRestorePayload.__name__])
|
||||
@console_ns.response(200, "Skill version restored", console_ns.models[SkillVersionResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
|
||||
try:
|
||||
payload = SkillRestorePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().restore_version(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillVersionResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/references")
|
||||
class WorkspaceSkillReferencesApi(Resource):
|
||||
@console_ns.response(200, "Skill references", console_ns.models[SkillReferenceListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().list_skill_references(tenant_id=current_tenant_id, skill_id=skill_id)
|
||||
return dump_response(SkillReferenceListResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/versions")
|
||||
class WorkspaceSkillVersionsApi(Resource):
|
||||
@console_ns.response(200, "Skill versions", console_ns.models[SkillVersionListResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str):
|
||||
try:
|
||||
result = SkillManagementService().list_versions(tenant_id=current_tenant_id, skill_id=skill_id)
|
||||
return dump_response(SkillVersionListResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/skills/<string:skill_id>/versions/<string:version_id>")
|
||||
class WorkspaceSkillVersionApi(Resource):
|
||||
@console_ns.response(200, "Skill version detail", console_ns.models[SkillVersionDetailResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, skill_id: str, version_id: str):
|
||||
try:
|
||||
result = SkillManagementService().get_version(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
return dump_response(SkillVersionDetailResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.expect(console_ns.models[SkillVersionUpdatePayload.__name__])
|
||||
@console_ns.response(200, "Skill version updated", console_ns.models[SkillVersionResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, skill_id: str, version_id: str):
|
||||
try:
|
||||
payload = SkillVersionUpdatePayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().update_version(
|
||||
tenant_id=current_tenant_id,
|
||||
skill_id=skill_id,
|
||||
version_id=version_id,
|
||||
payload=payload,
|
||||
)
|
||||
return dump_response(SkillVersionResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
@console_ns.response(200, "Skill version deleted", console_ns.models[SkillVersionDeleteResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, current_user: Account, skill_id: str, version_id: str):
|
||||
try:
|
||||
result = SkillManagementService().delete_version(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
skill_id=skill_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
return dump_response(SkillVersionDeleteResponse, result)
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/agents/<string:agent_id>/skills")
|
||||
class WorkspaceAgentSkillBindingsApi(Resource):
|
||||
@console_ns.response(200, "Agent Skill bindings", console_ns.models[AgentSkillBindingsResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, agent_id: str):
|
||||
result = SkillManagementService().list_agent_bindings(tenant_id=current_tenant_id, agent_id=agent_id)
|
||||
return dump_response(AgentSkillBindingsResponse, result)
|
||||
|
||||
@console_ns.expect(console_ns.models[AgentSkillBindingsPayload.__name__])
|
||||
@console_ns.response(200, "Agent Skill bindings replaced", console_ns.models[AgentSkillBindingsResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, current_user: Account, agent_id: str):
|
||||
try:
|
||||
payload = AgentSkillBindingsPayload.model_validate(console_ns.payload or {})
|
||||
result = SkillManagementService().replace_agent_bindings(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
agent_id=agent_id,
|
||||
skill_ids=payload.skill_ids,
|
||||
)
|
||||
return dump_response(AgentSkillBindingsResponse, result)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"WorkspaceAgentSkillBindingsApi",
|
||||
"WorkspaceSkillApi",
|
||||
"WorkspaceSkillDuplicateApi",
|
||||
"WorkspaceSkillExportApi",
|
||||
"WorkspaceSkillFilesApi",
|
||||
"WorkspaceSkillImportApi",
|
||||
"WorkspaceSkillPublishApi",
|
||||
"WorkspaceSkillReferencesApi",
|
||||
"WorkspaceSkillRestoreApi",
|
||||
"WorkspaceSkillTagsApi",
|
||||
"WorkspaceSkillVersionApi",
|
||||
"WorkspaceSkillVersionsApi",
|
||||
"WorkspaceSkillsApi",
|
||||
]
|
||||
@@ -23,6 +23,7 @@ from .knowledge import retrieval as _knowledge_retrieval
|
||||
from .plugin import agent_config as _agent_config
|
||||
from .plugin import agent_drive as _agent_drive
|
||||
from .plugin import plugin as _plugin
|
||||
from .plugin import skills as _skills
|
||||
from .workspace import workspace as _workspace
|
||||
|
||||
api.add_namespace(inner_api_ns)
|
||||
@@ -36,6 +37,7 @@ __all__ = [
|
||||
"_mail",
|
||||
"_plugin",
|
||||
"_runtime_credentials",
|
||||
"_skills",
|
||||
"_workspace",
|
||||
"api",
|
||||
"bp",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Inner API for published workspace Skills.
|
||||
|
||||
These endpoints are called by trusted runtime services. They expose only
|
||||
published Skill artifacts, never draft files or editable metadata.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
from flask import request, send_file
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from controllers.console.wraps import setup_required
|
||||
from controllers.inner_api import inner_api_ns
|
||||
from controllers.inner_api.wraps import plugin_inner_api_only
|
||||
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
|
||||
|
||||
|
||||
class _SkillTargetQuery(BaseModel):
|
||||
tenant_id: str
|
||||
|
||||
|
||||
def _target_query_from_request() -> _SkillTargetQuery:
|
||||
return _SkillTargetQuery.model_validate({"tenant_id": request.args.get("tenant_id")})
|
||||
|
||||
|
||||
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, str], int]:
|
||||
return {"code": exc.code, "message": exc.message}, exc.status_code
|
||||
|
||||
|
||||
@inner_api_ns.route("/skills/<string:skill_id>/pull")
|
||||
class PublishedSkillPullApi(Resource):
|
||||
@setup_required
|
||||
@plugin_inner_api_only
|
||||
@inner_api_ns.doc("published_skill_pull")
|
||||
def get(self, skill_id: str):
|
||||
try:
|
||||
query = _target_query_from_request()
|
||||
result = SkillManagementService().pull_published_archive(tenant_id=query.tenant_id, skill_id=skill_id)
|
||||
return send_file(
|
||||
io.BytesIO(result.payload),
|
||||
mimetype=result.mime_type,
|
||||
as_attachment=True,
|
||||
download_name=result.filename,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except SkillManagementServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
__all__ = ["PublishedSkillPullApi"]
|
||||
@@ -43,6 +43,7 @@ from core.workflow.nodes.agent_v2.runtime_request_builder import (
|
||||
build_config_layer_config,
|
||||
build_knowledge_layer_config,
|
||||
build_shell_layer_config,
|
||||
load_runtime_agent_skill_configs,
|
||||
)
|
||||
from models.agent_config_entities import AgentSoulConfig, AgentSoulToolsConfig
|
||||
from models.provider_ids import ModelProviderID
|
||||
@@ -125,14 +126,22 @@ class AgentAppRuntimeRequestBuilder:
|
||||
"cli_tool_count": len(agent_soul.tools.cli_tools),
|
||||
}
|
||||
|
||||
runtime_config_skills = load_runtime_agent_skill_configs(
|
||||
tenant_id=context.dify_context.tenant_id,
|
||||
agent_id=context.agent_id,
|
||||
)
|
||||
config_layer_config, config_warnings = build_config_layer_config(
|
||||
agent_soul,
|
||||
agent_id=context.agent_id,
|
||||
config_version_id=context.agent_config_snapshot_id,
|
||||
config_version_kind=context.agent_config_version_kind,
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
append_runtime_warnings(metadata, config_warnings)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(
|
||||
agent_soul,
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
knowledge_config = build_knowledge_layer_config(agent_soul)
|
||||
|
||||
request = self._request_builder.build_for_agent_app(
|
||||
|
||||
@@ -198,23 +198,8 @@ class AgentRuntimeSupport:
|
||||
if model_schema:
|
||||
model_schema = self._remove_unsupported_model_features_for_old_version(model_schema)
|
||||
value["entity"] = model_schema.model_dump(mode="json")
|
||||
# The model selector value from the workflow frontend only
|
||||
# carries provider/model/mode — it does NOT include
|
||||
# completion_params. AgentStrategy plugins (cot_agent,
|
||||
# function_calling) read completion_params to build the
|
||||
# LLMModelConfig that is backwards-invoked, and some model
|
||||
# providers raise KeyError('required') when
|
||||
# completion_params is empty because their parameter_rules
|
||||
# declare required fields with no default. Populate
|
||||
# completion_params with the defaults declared in the model
|
||||
# schema so the plugin daemon always receives a valid set
|
||||
# of model parameters.
|
||||
if "completion_params" not in value:
|
||||
value["completion_params"] = self._extract_default_completion_params(model_schema)
|
||||
else:
|
||||
value["entity"] = None
|
||||
if "completion_params" not in value:
|
||||
value["completion_params"] = {}
|
||||
result[parameter_name] = value
|
||||
|
||||
return result
|
||||
@@ -290,24 +275,6 @@ class AgentRuntimeSupport:
|
||||
model_schema.features.remove(feature)
|
||||
return model_schema
|
||||
|
||||
@staticmethod
|
||||
def _extract_default_completion_params(model_schema: AIModelEntity) -> dict[str, Any]:
|
||||
"""Build a completion_params dict from the model schema's parameter_rules.
|
||||
|
||||
The workflow Agent node's model-selector parameter only stores
|
||||
provider/model/mode — it never carries completion_params. When the
|
||||
value is forwarded to the plugin daemon, AgentModelConfig defaults
|
||||
completion_params to ``{}``, which causes some model providers to fail
|
||||
because their parameter_rules declare required fields. This helper
|
||||
collects the ``default`` value of every parameter_rule that has one so
|
||||
the plugin daemon receives a valid, non-empty set of model parameters.
|
||||
"""
|
||||
completion_params: dict[str, Any] = {}
|
||||
for rule in model_schema.parameter_rules:
|
||||
if rule.default is not None:
|
||||
completion_params[rule.name] = rule.default
|
||||
return completion_params
|
||||
|
||||
@staticmethod
|
||||
def _filter_mcp_type_tool(
|
||||
strategy: ResolvedAgentStrategy,
|
||||
|
||||
@@ -38,6 +38,7 @@ from dify_agent.layers.shell import (
|
||||
)
|
||||
from dify_agent.protocol import CreateRunRequest, DeferredToolResultsPayload
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from clients.agent_backend import (
|
||||
AgentBackendModelConfig,
|
||||
@@ -206,14 +207,22 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
"cli_tool_count": len(agent_soul.tools.cli_tools),
|
||||
}
|
||||
|
||||
runtime_config_skills = load_runtime_agent_skill_configs(
|
||||
tenant_id=context.dify_context.tenant_id,
|
||||
agent_id=context.agent.id,
|
||||
)
|
||||
config_layer_config, config_warnings = build_config_layer_config(
|
||||
agent_soul,
|
||||
agent_id=context.agent.id,
|
||||
config_version_id=context.snapshot.id,
|
||||
config_version_kind="snapshot",
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
append_runtime_warnings(metadata, config_warnings)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(
|
||||
agent_soul,
|
||||
runtime_config_skills=runtime_config_skills,
|
||||
)
|
||||
soul_prompt = expand_prompt_mentions(agent_soul.prompt.system_prompt, soul_prompt_resolver).strip()
|
||||
knowledge_config = build_knowledge_layer_config(agent_soul)
|
||||
|
||||
@@ -883,11 +892,16 @@ def append_runtime_warnings(metadata: dict[str, Any], warnings: list[dict[str, s
|
||||
existing.extend(warnings)
|
||||
|
||||
|
||||
def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
|
||||
def build_config_aware_soul_mention_resolver(
|
||||
agent_soul: AgentSoulConfig,
|
||||
*,
|
||||
runtime_config_skills: Sequence[DifyConfigSkillConfig] = (),
|
||||
):
|
||||
"""Resolve config skill/file mentions and delegate the rest to Agent Soul."""
|
||||
|
||||
base_resolver = build_soul_mention_resolver(agent_soul)
|
||||
skill_names = {item.name for item in agent_soul.config_skills if not item.is_missing}
|
||||
skill_names.update(item.name for item in runtime_config_skills)
|
||||
file_names = {item.name for item in agent_soul.config_files if not item.is_missing}
|
||||
|
||||
def _resolve(mention: object) -> str | None:
|
||||
@@ -905,12 +919,34 @@ def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
|
||||
return _resolve
|
||||
|
||||
|
||||
def load_runtime_agent_skill_configs(*, tenant_id: str, agent_id: str) -> list[DifyConfigSkillConfig]:
|
||||
"""Return workspace-bound Skills as prompt-safe runtime config skills."""
|
||||
from services.skill_management_service import SkillManagementService
|
||||
|
||||
try:
|
||||
runtime_skills = SkillManagementService().list_runtime_agent_skills(tenant_id=tenant_id, agent_id=agent_id)
|
||||
except OperationalError as exc:
|
||||
if "no such table: agent_skill_bindings" not in str(exc.orig):
|
||||
raise
|
||||
runtime_skills = []
|
||||
return [
|
||||
DifyConfigSkillConfig(
|
||||
name=str(item["name"]),
|
||||
description=str(item.get("description") or ""),
|
||||
size=cast(int | None, item.get("size")),
|
||||
mime_type=cast(str | None, item.get("mime_type")),
|
||||
)
|
||||
for item in runtime_skills
|
||||
]
|
||||
|
||||
|
||||
def build_config_layer_config(
|
||||
agent_soul: AgentSoulConfig,
|
||||
*,
|
||||
agent_id: str | None = None,
|
||||
config_version_id: str | None = None,
|
||||
config_version_kind: Literal["snapshot", "draft", "build_draft"] = "snapshot",
|
||||
runtime_config_skills: Sequence[DifyConfigSkillConfig] = (),
|
||||
) -> tuple[DifyConfigLayerConfig, list[dict[str, str]]]:
|
||||
"""Build the always-present Agent config layer from Agent Soul state.
|
||||
|
||||
@@ -927,8 +963,23 @@ def build_config_layer_config(
|
||||
)
|
||||
)
|
||||
available_skills = [skill for skill in agent_soul.config_skills if not skill.is_missing]
|
||||
skill_configs = [
|
||||
DifyConfigSkillConfig(
|
||||
name=skill.name,
|
||||
description=skill.description,
|
||||
size=skill.size,
|
||||
mime_type=skill.mime_type,
|
||||
)
|
||||
for skill in available_skills
|
||||
]
|
||||
seen_skill_names = {skill.name for skill in skill_configs}
|
||||
for skill in runtime_config_skills:
|
||||
if skill.name in seen_skill_names:
|
||||
continue
|
||||
seen_skill_names.add(skill.name)
|
||||
skill_configs.append(skill)
|
||||
available_files = [file_ref for file_ref in agent_soul.config_files if not file_ref.is_missing]
|
||||
skill_names = {skill.name for skill in available_skills}
|
||||
skill_names = {skill.name for skill in skill_configs}
|
||||
file_names = {file_ref.name for file_ref in available_files}
|
||||
warnings: list[dict[str, str]] = [
|
||||
{
|
||||
@@ -965,15 +1016,7 @@ def build_config_layer_config(
|
||||
kind=config_version_kind,
|
||||
writable=config_version_kind == "build_draft",
|
||||
),
|
||||
skills=[
|
||||
DifyConfigSkillConfig(
|
||||
name=skill.name,
|
||||
description=skill.description,
|
||||
size=skill.size,
|
||||
mime_type=skill.mime_type,
|
||||
)
|
||||
for skill in available_skills
|
||||
],
|
||||
skills=skill_configs,
|
||||
files=[
|
||||
DifyConfigFileConfig(
|
||||
name=file_ref.name,
|
||||
|
||||
@@ -34,7 +34,7 @@ class _SessionResponseSource[SourceT]:
|
||||
self._session = session
|
||||
|
||||
def __getattr__(self, name: str) -> object:
|
||||
return getattr(self._source, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self._source, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
|
||||
class _FeedbackResponseSource(_SessionResponseSource[MessageFeedback]):
|
||||
|
||||
@@ -227,7 +227,7 @@ class DatasetDetailResponseSource:
|
||||
return self.dataset.get_total_available_documents(session=self.session)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self.dataset, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self.dataset, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
|
||||
def dataset_detail_response_source(dataset: Any, *, session: Session) -> DatasetDetailResponseSource:
|
||||
|
||||
@@ -90,7 +90,7 @@ class DocumentWithSession:
|
||||
return self.document.get_doc_metadata_details(session=self.session)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self.document, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self.document, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
|
||||
def document_response(document: Document, *, session: Session) -> DocumentResponse:
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
"""add workspace skill management
|
||||
|
||||
Revision ID: a4f8d2c9e1b0
|
||||
Revises: 6f5a9c2d8e1b
|
||||
Create Date: 2026-07-09 12:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
from models.types import StringUUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a4f8d2c9e1b0"
|
||||
down_revision = "6f5a9c2d8e1b"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _uuid_column(name: str, *, nullable: bool = False) -> sa.Column:
|
||||
return sa.Column(name, StringUUID(), nullable=nullable)
|
||||
|
||||
|
||||
def _long_text() -> sa.types.TypeEngine:
|
||||
return sa.Text().with_variant(mysql.LONGTEXT(), "mysql")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"skills",
|
||||
_uuid_column("id"),
|
||||
_uuid_column("tenant_id"),
|
||||
sa.Column("name", sa.String(length=64), nullable=False),
|
||||
sa.Column("display_name", sa.String(length=128), nullable=False),
|
||||
sa.Column("icon", sa.String(length=16), nullable=False, server_default="📄"),
|
||||
sa.Column("description", sa.String(length=1024), nullable=False, server_default=""),
|
||||
sa.Column("name_manually_edited", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("visibility", sa.String(length=32), nullable=False, server_default="workspace"),
|
||||
_uuid_column("latest_published_version_id", nullable=True),
|
||||
_uuid_column("created_by", nullable=True),
|
||||
_uuid_column("updated_by", nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
|
||||
sa.PrimaryKeyConstraint("id", name="skill_pkey"),
|
||||
sa.UniqueConstraint("tenant_id", "name", name="skill_tenant_name_unique"),
|
||||
)
|
||||
op.create_index("skills_tenant_updated_at_idx", "skills", ["tenant_id", "updated_at"])
|
||||
|
||||
op.create_table(
|
||||
"skill_draft_files",
|
||||
_uuid_column("id"),
|
||||
_uuid_column("skill_id"),
|
||||
sa.Column("path", sa.String(length=512), nullable=False),
|
||||
sa.Column("kind", sa.String(length=32), nullable=False),
|
||||
sa.Column("storage", sa.String(length=32), nullable=True),
|
||||
sa.Column("mime_type", sa.String(length=255), nullable=True),
|
||||
sa.Column("content_text", _long_text(), nullable=True),
|
||||
_uuid_column("tool_file_id", nullable=True),
|
||||
sa.Column("size", sa.BigInteger(), nullable=True),
|
||||
sa.Column("hash", sa.String(length=255), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
|
||||
sa.PrimaryKeyConstraint("id", name="skill_draft_file_pkey"),
|
||||
sa.UniqueConstraint("skill_id", "path", name="skill_draft_file_skill_path_unique"),
|
||||
)
|
||||
op.create_index("skill_draft_files_skill_path_idx", "skill_draft_files", ["skill_id", "path"])
|
||||
|
||||
op.create_table(
|
||||
"skill_versions",
|
||||
_uuid_column("id"),
|
||||
_uuid_column("skill_id"),
|
||||
sa.Column("version_number", sa.Integer(), nullable=False),
|
||||
sa.Column("version_name", sa.String(length=128), nullable=False, server_default=""),
|
||||
sa.Column("publish_note", sa.String(length=1024), nullable=False, server_default=""),
|
||||
sa.Column("manifest", _long_text(), nullable=False),
|
||||
_uuid_column("archive_tool_file_id"),
|
||||
sa.Column("hash_code", sa.String(length=255), nullable=False),
|
||||
sa.Column("archive_size", sa.BigInteger(), nullable=False),
|
||||
_uuid_column("published_by", nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
|
||||
sa.PrimaryKeyConstraint("id", name="skill_version_pkey"),
|
||||
sa.UniqueConstraint("skill_id", "version_number", name="skill_version_skill_number_unique"),
|
||||
)
|
||||
op.create_index("skill_versions_skill_created_at_idx", "skill_versions", ["skill_id", "created_at"])
|
||||
|
||||
op.create_table(
|
||||
"agent_skill_bindings",
|
||||
_uuid_column("id"),
|
||||
_uuid_column("tenant_id"),
|
||||
_uuid_column("agent_id"),
|
||||
_uuid_column("skill_id"),
|
||||
sa.Column("priority", sa.Integer(), nullable=False),
|
||||
_uuid_column("created_by", nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
|
||||
sa.PrimaryKeyConstraint("id", name="agent_skill_binding_pkey"),
|
||||
sa.UniqueConstraint("tenant_id", "agent_id", "skill_id", name="agent_skill_binding_unique"),
|
||||
sa.UniqueConstraint("tenant_id", "agent_id", "priority", name="agent_skill_binding_priority_unique"),
|
||||
)
|
||||
op.create_index("agent_skill_bindings_skill_idx", "agent_skill_bindings", ["tenant_id", "skill_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("agent_skill_bindings_skill_idx", table_name="agent_skill_bindings")
|
||||
op.drop_table("agent_skill_bindings")
|
||||
op.drop_index("skill_versions_skill_created_at_idx", table_name="skill_versions")
|
||||
op.drop_table("skill_versions")
|
||||
op.drop_index("skill_draft_files_skill_path_idx", table_name="skill_draft_files")
|
||||
op.drop_table("skill_draft_files")
|
||||
op.drop_index("skills_tenant_updated_at_idx", table_name="skills")
|
||||
op.drop_table("skills")
|
||||
+110
-23
@@ -18,31 +18,109 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _is_pg(conn) -> bool:
|
||||
return conn.dialect.name == "postgresql"
|
||||
|
||||
|
||||
def _uuid_column(name: str, *, nullable: bool = False, primary_key: bool = False) -> sa.Column:
|
||||
kwargs = {"nullable": nullable, "primary_key": primary_key}
|
||||
if primary_key and _is_pg(op.get_bind()):
|
||||
kwargs["server_default"] = sa.text("uuidv7()")
|
||||
return sa.Column(name, models.types.StringUUID(), **kwargs)
|
||||
|
||||
|
||||
def _has_table(table_name: str) -> bool:
|
||||
return sa.inspect(op.get_bind()).has_table(table_name)
|
||||
|
||||
|
||||
def _has_column(table_name: str, column_name: str) -> bool:
|
||||
return any(
|
||||
column["name"] == column_name for column in sa.inspect(op.get_bind()).get_columns(table_name)
|
||||
)
|
||||
|
||||
|
||||
def _has_unique_constraint(table_name: str, constraint_name: str) -> bool:
|
||||
return any(
|
||||
constraint["name"] == constraint_name
|
||||
for constraint in sa.inspect(op.get_bind()).get_unique_constraints(table_name)
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
if not _has_table("agent_debug_conversations"):
|
||||
op.create_table(
|
||||
"agent_debug_conversations",
|
||||
_uuid_column("id", primary_key=True),
|
||||
sa.Column("tenant_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("agent_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("app_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("account_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("conversation_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column(
|
||||
"draft_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default=sa.text("'debug_build'"),
|
||||
),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("agent_debug_conversation_pkey")),
|
||||
sa.UniqueConstraint(
|
||||
"tenant_id",
|
||||
"agent_id",
|
||||
"account_id",
|
||||
"draft_type",
|
||||
name=op.f("agent_debug_conversation_agent_account_draft_type_unique"),
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"agent_debug_conversation_conversation_idx",
|
||||
"agent_debug_conversations",
|
||||
["conversation_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"agent_debug_conversation_account_idx",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "account_id"],
|
||||
)
|
||||
return
|
||||
|
||||
# Existing pointers have always represented Build chat because the Agent
|
||||
# detail API exposes them as ``debug_conversation_id`` for that surface.
|
||||
op.add_column(
|
||||
if not _has_column("agent_debug_conversations", "draft_type"):
|
||||
op.add_column(
|
||||
"agent_debug_conversations",
|
||||
sa.Column(
|
||||
"draft_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default=sa.text("'debug_build'"),
|
||||
),
|
||||
)
|
||||
if _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
sa.Column(
|
||||
"draft_type",
|
||||
sa.String(length=32),
|
||||
nullable=False,
|
||||
server_default=sa.text("'debug_build'"),
|
||||
),
|
||||
)
|
||||
op.drop_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
):
|
||||
op.drop_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
if not _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id", "draft_type"],
|
||||
)
|
||||
):
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id", "draft_type"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
if not _has_table("agent_debug_conversations"):
|
||||
return
|
||||
|
||||
debug_conversations = sa.table(
|
||||
"agent_debug_conversations",
|
||||
sa.column("tenant_id", models.types.StringUUID()),
|
||||
@@ -64,14 +142,23 @@ def downgrade():
|
||||
),
|
||||
)
|
||||
)
|
||||
op.drop_constraint(
|
||||
if _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
):
|
||||
op.drop_constraint(
|
||||
"agent_debug_conversation_agent_account_draft_type_unique",
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
if not _has_unique_constraint(
|
||||
"agent_debug_conversations",
|
||||
type_="unique",
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id"],
|
||||
)
|
||||
op.drop_column("agent_debug_conversations", "draft_type")
|
||||
):
|
||||
op.create_unique_constraint(
|
||||
"agent_debug_conversation_agent_account_unique",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "agent_id", "account_id"],
|
||||
)
|
||||
if _has_column("agent_debug_conversations", "draft_type"):
|
||||
op.drop_column("agent_debug_conversations", "draft_type")
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
"""merge skill and agent debug conversation heads
|
||||
|
||||
Revision ID: e9f4a1b2c3d5
|
||||
Revises: a4f8d2c9e1b0, d2825e7b9c10
|
||||
Create Date: 2026-07-23 15:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "e9f4a1b2c3d5"
|
||||
down_revision = ("a4f8d2c9e1b0", "d2825e7b9c10")
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@@ -113,6 +113,7 @@ from .provider import (
|
||||
TenantDefaultModel,
|
||||
TenantPreferredModelProvider,
|
||||
)
|
||||
from .skill import AgentSkillBinding, Skill, SkillDraftFile, SkillFileKind, SkillFileStorage, SkillVersion
|
||||
from .snippet import CustomizedSnippet, SnippetType
|
||||
from .source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding
|
||||
from .task import CeleryTask, CeleryTaskSet
|
||||
@@ -173,6 +174,7 @@ __all__ = [
|
||||
"AgentRuntimeSessionOwnerType",
|
||||
"AgentRuntimeSessionStatus",
|
||||
"AgentScope",
|
||||
"AgentSkillBinding",
|
||||
"AgentSource",
|
||||
"AgentStatus",
|
||||
"ApiRequest",
|
||||
@@ -246,6 +248,11 @@ __all__ = [
|
||||
"RecommendedApp",
|
||||
"SavedMessage",
|
||||
"Site",
|
||||
"Skill",
|
||||
"SkillDraftFile",
|
||||
"SkillFileKind",
|
||||
"SkillFileStorage",
|
||||
"SkillVersion",
|
||||
"SnippetType",
|
||||
"Tag",
|
||||
"TagBinding",
|
||||
|
||||
@@ -249,6 +249,7 @@ class TagType(StrEnum):
|
||||
KNOWLEDGE = "knowledge"
|
||||
APP = "app"
|
||||
SNIPPET = "snippet"
|
||||
SKILL = "skill"
|
||||
|
||||
|
||||
class DatasetMetadataType(StrEnum):
|
||||
|
||||
+1
-1
@@ -2667,7 +2667,7 @@ class Tag(TypeBase):
|
||||
sa.Index("tag_name_idx", "name"),
|
||||
)
|
||||
|
||||
TAG_TYPE_LIST = ["knowledge", "app", "snippet"]
|
||||
TAG_TYPE_LIST = ["knowledge", "app", "snippet", "skill"]
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Workspace-level Skill Management models.
|
||||
|
||||
These tables are the source of truth for reusable workspace Skills. Agent Soul
|
||||
``config_skills`` and Agent Drive skill rows remain per-agent runtime/config
|
||||
assets; they may consume a published Skill snapshot but do not own the Skill's
|
||||
draft, metadata, version history, or Agent binding priority.
|
||||
"""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
import sqlalchemy as sa
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import Index, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from models.base import Base, DefaultFieldsMixin
|
||||
from models.types import EnumText, JSONModelColumn, LongText, StringUUID
|
||||
|
||||
|
||||
class SkillFileKind(StrEnum):
|
||||
"""Draft file entry kind."""
|
||||
|
||||
FILE = "file"
|
||||
DIRECTORY = "directory"
|
||||
|
||||
|
||||
class SkillFileStorage(StrEnum):
|
||||
"""How a draft file's content is stored."""
|
||||
|
||||
TEXT = "text"
|
||||
TOOL_FILE = "tool_file"
|
||||
|
||||
|
||||
class SkillVersionManifestFile(BaseModel):
|
||||
"""One file entry captured in a published Skill snapshot manifest."""
|
||||
|
||||
path: str
|
||||
mime_type: str | None = None
|
||||
size: int
|
||||
hash: str
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class SkillVersionManifest(BaseModel):
|
||||
"""Published Skill snapshot file index."""
|
||||
|
||||
files: list[SkillVersionManifestFile]
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Skill(DefaultFieldsMixin, Base):
|
||||
"""Workspace-level reusable Skill metadata and draft status."""
|
||||
|
||||
__tablename__ = "skills"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="skill_pkey"),
|
||||
UniqueConstraint("tenant_id", "name", name="skill_tenant_name_unique"),
|
||||
Index("skills_tenant_updated_at_idx", "tenant_id", "updated_at"),
|
||||
)
|
||||
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
name: Mapped[str] = mapped_column(sa.String(64), nullable=False)
|
||||
display_name: Mapped[str] = mapped_column(sa.String(128), nullable=False)
|
||||
icon: Mapped[str] = mapped_column(sa.String(16), nullable=False, default="📄", server_default="📄")
|
||||
description: Mapped[str] = mapped_column(sa.String(1024), nullable=False, default="", server_default="")
|
||||
name_manually_edited: Mapped[bool] = mapped_column(
|
||||
sa.Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=sa.false(),
|
||||
)
|
||||
visibility: Mapped[str] = mapped_column(
|
||||
sa.String(32),
|
||||
nullable=False,
|
||||
default="workspace",
|
||||
server_default="workspace",
|
||||
)
|
||||
latest_published_version_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
|
||||
|
||||
class SkillDraftFile(DefaultFieldsMixin, Base):
|
||||
"""One draft file or directory in a workspace Skill."""
|
||||
|
||||
__tablename__ = "skill_draft_files"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="skill_draft_file_pkey"),
|
||||
UniqueConstraint("skill_id", "path", name="skill_draft_file_skill_path_unique"),
|
||||
Index("skill_draft_files_skill_path_idx", "skill_id", "path"),
|
||||
)
|
||||
|
||||
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
path: Mapped[str] = mapped_column(sa.String(512), nullable=False)
|
||||
kind: Mapped[SkillFileKind] = mapped_column(EnumText(SkillFileKind, length=32), nullable=False)
|
||||
storage: Mapped[SkillFileStorage | None] = mapped_column(EnumText(SkillFileStorage, length=32), nullable=True)
|
||||
mime_type: Mapped[str | None] = mapped_column(sa.String(255), nullable=True)
|
||||
content_text: Mapped[str | None] = mapped_column(LongText, nullable=True)
|
||||
tool_file_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
size: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True)
|
||||
hash: Mapped[str | None] = mapped_column(sa.String(255), nullable=True)
|
||||
|
||||
|
||||
class SkillVersion(DefaultFieldsMixin, Base):
|
||||
"""Immutable published Skill snapshot.
|
||||
|
||||
``hash_code`` uniquely identifies a published version for downstream
|
||||
execution audit. It includes Skill identity, version number, and archive
|
||||
content digest instead of being only the archive content hash.
|
||||
"""
|
||||
|
||||
__tablename__ = "skill_versions"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="skill_version_pkey"),
|
||||
UniqueConstraint("skill_id", "version_number", name="skill_version_skill_number_unique"),
|
||||
Index("skill_versions_skill_created_at_idx", "skill_id", "created_at"),
|
||||
)
|
||||
|
||||
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
version_number: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
||||
version_name: Mapped[str] = mapped_column(sa.String(128), nullable=False, default="", server_default="")
|
||||
publish_note: Mapped[str] = mapped_column(sa.String(1024), nullable=False, default="", server_default="")
|
||||
manifest: Mapped[SkillVersionManifest] = mapped_column(JSONModelColumn(SkillVersionManifest), nullable=False)
|
||||
archive_tool_file_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
hash_code: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
||||
archive_size: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
|
||||
published_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
|
||||
|
||||
class AgentSkillBinding(DefaultFieldsMixin, Base):
|
||||
"""Direct Agent-to-workspace-Skill binding.
|
||||
|
||||
``priority`` is retained as an internal ordering column for the current
|
||||
schema constraints. Runtime Skill selection is Agent-driven and must not
|
||||
treat it as a matching priority.
|
||||
"""
|
||||
|
||||
__tablename__ = "agent_skill_bindings"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="agent_skill_binding_pkey"),
|
||||
UniqueConstraint("tenant_id", "agent_id", "skill_id", name="agent_skill_binding_unique"),
|
||||
UniqueConstraint("tenant_id", "agent_id", "priority", name="agent_skill_binding_priority_unique"),
|
||||
Index("agent_skill_bindings_skill_idx", "tenant_id", "skill_id"),
|
||||
)
|
||||
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
priority: Mapped[int] = mapped_column(sa.Integer, nullable=False)
|
||||
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentSkillBinding",
|
||||
"Skill",
|
||||
"SkillDraftFile",
|
||||
"SkillFileKind",
|
||||
"SkillFileStorage",
|
||||
"SkillVersion",
|
||||
"SkillVersionManifest",
|
||||
"SkillVersionManifestFile",
|
||||
]
|
||||
@@ -9551,7 +9551,7 @@ Remove one or more tag bindings from a target.
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| keyword | query | Search keyword | No | string |
|
||||
| type | query | Tag type filter | No | string, <br>**Available values:** "", "app", "knowledge", "snippet" |
|
||||
| type | query | Tag type filter | No | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
@@ -10087,6 +10087,38 @@ Get list of available agent providers
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [AgentProviderListResponse](#agentproviderlistresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/agents/{agent_id}/skills
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| agent_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Agent Skill bindings | **application/json**: [AgentSkillBindingsResponse](#agentskillbindingsresponse)<br> |
|
||||
|
||||
### [PUT] /workspaces/current/agents/{agent_id}/skills
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| agent_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [AgentSkillBindingsPayload](#agentskillbindingspayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Agent Skill bindings replaced | **application/json**: [AgentSkillBindingsResponse](#agentskillbindingsresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/customized-snippets
|
||||
**List customized snippets with pagination and search**
|
||||
|
||||
@@ -11988,6 +12020,341 @@ Returns permission flags that control workspace features like member invitations
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceAccessMatrix](#workspaceaccessmatrix)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| keyword | query | Search keyword matching skill name or description. | No | string |
|
||||
| limit | query | Number of items per page. | No | integer, <br>**Default:** 20 |
|
||||
| page | query | Page number. | No | integer, <br>**Default:** 1 |
|
||||
| tag | query | Skill tag filters. Repeat the parameter for multiple tags. | No | [ string ] |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Workspace skills | **application/json**: [SkillListResponse](#skilllistresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillCreatePayload](#skillcreatepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill created | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/files/upload
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **multipart/form-data**: { **"file"**: binary }<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill draft file uploaded | **application/json**: [SkillFileUploadResponse](#skillfileuploadresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/import
|
||||
Import a Skill zip package from multipart form field `file`.
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill imported | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/tags
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Workspace Skill tags | **application/json**: [SkillTagListResponse](#skilltaglistresponse)<br> |
|
||||
|
||||
### [DELETE] /workspaces/current/skills/{skill_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillDeletePayload](#skilldeletepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill deleted | **application/json**: [SkillDeleteResponse](#skilldeleteresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill detail | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [PATCH] /workspaces/current/skills/{skill_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillMetadataPayload](#skillmetadatapayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill updated | **application/json**: [SkillResponse](#skillresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/{skill_id}/assist/messages
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillAssistMessagePayload](#skillassistmessagepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Skill Authoring assistant event stream |
|
||||
|
||||
### [POST] /workspaces/current/skills/{skill_id}/duplicate
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill duplicated | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/export
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Published Skill zip archive |
|
||||
|
||||
### [PATCH] /workspaces/current/skills/{skill_id}/files
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillDraftFileOperationPayload](#skilldraftfileoperationpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Draft file operation applied | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [PUT] /workspaces/current/skills/{skill_id}/files
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillDraftTreePayload](#skilldrafttreepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Draft files replaced | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/files/content
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| download | query | Return as an attachment when 1. | No | string |
|
||||
| path | query | Skill file path relative to the Skill root. | Yes | string |
|
||||
| version_id | query | Optional published version ID. Omit for current draft. | No | string |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill file content | **application/json**: [BinaryFileResponse](#binaryfileresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/files/preview
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| path | query | Skill file path relative to the Skill root. | Yes | string |
|
||||
| version_id | query | Optional published version ID. Omit for current draft. | No | string |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill file text preview | **application/json**: [SkillFilePreviewResponse](#skillfilepreviewresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/{skill_id}/publish
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillPublishPayload](#skillpublishpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill published | **application/json**: [SkillVersionResponse](#skillversionresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/references
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill references | **application/json**: [SkillReferenceListResponse](#skillreferencelistresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current/skills/{skill_id}/restore
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillRestorePayload](#skillrestorepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill version restored | **application/json**: [SkillVersionResponse](#skillversionresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/versions
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill versions | **application/json**: [SkillVersionListResponse](#skillversionlistresponse)<br> |
|
||||
|
||||
### [DELETE] /workspaces/current/skills/{skill_id}/versions/{version_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
| version_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill version deleted | **application/json**: [SkillVersionDeleteResponse](#skillversiondeleteresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/skills/{skill_id}/versions/{version_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
| version_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill version detail | **application/json**: [SkillVersionDetailResponse](#skillversiondetailresponse)<br> |
|
||||
|
||||
### [PATCH] /workspaces/current/skills/{skill_id}/versions/{version_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| skill_id | path | | Yes | string |
|
||||
| version_id | path | | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SkillVersionUpdatePayload](#skillversionupdatepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Skill version updated | **application/json**: [SkillVersionResponse](#skillversionresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/tool-labels
|
||||
#### Responses
|
||||
|
||||
@@ -14709,6 +15076,37 @@ Visibility and lifecycle scope of an Agent record.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | | Yes |
|
||||
|
||||
#### AgentSkillBindingItemResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| description | string | | Yes |
|
||||
| display_name | string | | Yes |
|
||||
| file_count | integer | | Yes |
|
||||
| icon | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| latest_published_at | integer | | No |
|
||||
| latest_published_version_id | string | | No |
|
||||
| name | string | | Yes |
|
||||
| priority | integer | | Yes |
|
||||
| status | string | | Yes |
|
||||
| tags | [ string ] | | No |
|
||||
| updated_at | integer | | Yes |
|
||||
|
||||
#### AgentSkillBindingsPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| skill_ids | [ string ] | Ordered Skill IDs bound to the Agent. | No |
|
||||
|
||||
#### AgentSkillBindingsResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| agent_id | string | | Yes |
|
||||
| data | [ [AgentSkillBindingItemResponse](#agentskillbindingitemresponse) ] | | No |
|
||||
| skill_ids | [ string ] | | No |
|
||||
|
||||
#### AgentSkillRefConfig
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21665,6 +22063,186 @@ Simple provider entity response.
|
||||
| title | string | | Yes |
|
||||
| use_icon_as_answer_icon | boolean | | Yes |
|
||||
|
||||
#### SkillAssistAttachmentPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| mime_type | string | | No |
|
||||
| name | string | | Yes |
|
||||
| size | integer | | No |
|
||||
| tool_file_id | string | | Yes |
|
||||
|
||||
#### SkillAssistMessagePayload
|
||||
|
||||
One user message and optional uploaded context for the read-only Skill Authoring assistant.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| attachments | [ [SkillAssistAttachmentPayload](#skillassistattachmentpayload) ] | | No |
|
||||
| message | string | | Yes |
|
||||
| model | [SkillAssistModelPayload](#skillassistmodelpayload) | | No |
|
||||
|
||||
#### SkillAssistModelPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| model | string | | Yes |
|
||||
| model_settings | object | | No |
|
||||
| plugin_id | string | | No |
|
||||
| provider | string | | Yes |
|
||||
|
||||
#### SkillCreatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| description | string | | No |
|
||||
| display_name | string | | No |
|
||||
| icon | string, <br>**Default:** 📄 | | No |
|
||||
| name | string | | No |
|
||||
| tags | [ string ] | | No |
|
||||
|
||||
#### SkillDeletePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| confirmation_name | string | Required when deleting a referenced Skill. Must match the Skill name. | No |
|
||||
|
||||
#### SkillDeleteResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| deleted | boolean | | Yes |
|
||||
| id | string | | Yes |
|
||||
|
||||
#### SkillDetailResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| created_at | integer | | Yes |
|
||||
| created_by | string | | No |
|
||||
| created_by_name | string | | No |
|
||||
| description | string | | Yes |
|
||||
| display_name | string | | Yes |
|
||||
| files | [ [SkillFileResponse](#skillfileresponse) ] | | No |
|
||||
| icon | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| latest_published_version_id | string | | No |
|
||||
| name | string | | Yes |
|
||||
| name_manually_edited | boolean | | No |
|
||||
| reference_count | integer | | No |
|
||||
| tags | [ string ] | | No |
|
||||
| updated_at | integer | | Yes |
|
||||
| updated_by | string | | No |
|
||||
| updated_by_name | string | | No |
|
||||
| visibility | string | | Yes |
|
||||
|
||||
#### SkillDraftFileOperation
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| SkillDraftFileOperation | string | | |
|
||||
|
||||
#### SkillDraftFileOperationPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| content | string | | No |
|
||||
| expected_updated_at | integer | | No |
|
||||
| hash | string | | No |
|
||||
| mime_type | string | | No |
|
||||
| operation | [SkillDraftFileOperation](#skilldraftfileoperation) | | Yes |
|
||||
| path | string | | Yes |
|
||||
| size | integer | | No |
|
||||
| target_path | string | | No |
|
||||
| tool_file_id | string | | No |
|
||||
|
||||
#### SkillDraftTreeItemPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| content | string | | No |
|
||||
| hash | string | | No |
|
||||
| kind | [SkillFileKind](#skillfilekind) | | No |
|
||||
| mime_type | string | | No |
|
||||
| path | string | | Yes |
|
||||
| size | integer | | No |
|
||||
| storage | [SkillFileStorage](#skillfilestorage) | | No |
|
||||
| tool_file_id | string | | No |
|
||||
|
||||
#### SkillDraftTreePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| expected_updated_at | integer | | No |
|
||||
| files | [ [SkillDraftTreeItemPayload](#skilldrafttreeitempayload) ] | | No |
|
||||
|
||||
#### SkillFileKind
|
||||
|
||||
Draft file entry kind.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| SkillFileKind | string | Draft file entry kind. | |
|
||||
|
||||
#### SkillFilePreviewResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| content | string | | Yes |
|
||||
| hash | string | | Yes |
|
||||
| mime_type | string | | Yes |
|
||||
| path | string | | Yes |
|
||||
| size | integer | | Yes |
|
||||
|
||||
#### SkillFileQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| path | string | Skill file path relative to the Skill root. | Yes |
|
||||
| version_id | string | Optional published version ID. Omit for current draft. | No |
|
||||
|
||||
#### SkillFileResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| content | string | | No |
|
||||
| hash | string | | No |
|
||||
| id | string | | No |
|
||||
| kind | string | | Yes |
|
||||
| mime_type | string | | No |
|
||||
| path | string | | Yes |
|
||||
| size | integer | | No |
|
||||
| storage | string | | No |
|
||||
| tool_file_id | string | | No |
|
||||
|
||||
#### SkillFileStorage
|
||||
|
||||
How a draft file's content is stored.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| SkillFileStorage | string | How a draft file's content is stored. | |
|
||||
|
||||
#### SkillFileUploadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| hash | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| mime_type | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| size | integer | | Yes |
|
||||
|
||||
#### SkillListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| data | [ [SkillResponse](#skillresponse) ] | | No |
|
||||
| has_more | boolean | | No |
|
||||
| limit | integer, <br>**Default:** 20 | | No |
|
||||
| page | integer, <br>**Default:** 1 | | No |
|
||||
| total | integer | | No |
|
||||
|
||||
#### SkillManifest
|
||||
|
||||
Validated metadata extracted from a Skill package.
|
||||
@@ -21678,6 +22256,91 @@ Validated metadata extracted from a Skill package.
|
||||
| name | string | | Yes |
|
||||
| size | integer | | Yes |
|
||||
|
||||
#### SkillMetadataPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| display_name | string | | No |
|
||||
| expected_updated_at | integer | | No |
|
||||
| icon | string | | No |
|
||||
| tags | [ string ] | | No |
|
||||
|
||||
#### SkillPublishPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| publish_note | string | | No |
|
||||
| version_name | string | | No |
|
||||
|
||||
#### SkillReferenceListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| data | [ [SkillReferenceResponse](#skillreferenceresponse) ] | | No |
|
||||
|
||||
#### SkillReferenceResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| agent_icon | string | | No |
|
||||
| agent_icon_background | string | | No |
|
||||
| agent_icon_type | string | | No |
|
||||
| agent_id | string | | Yes |
|
||||
| app_id | string | | No |
|
||||
| display_name | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| node_id | string | | No |
|
||||
| node_name | string | | No |
|
||||
| type | string | | Yes |
|
||||
| workflow_icon | string | | No |
|
||||
| workflow_icon_background | string | | No |
|
||||
| workflow_icon_type | string | | No |
|
||||
| workflow_id | string | | No |
|
||||
| workflow_name | string | | No |
|
||||
| workflow_version | string | | No |
|
||||
|
||||
#### SkillResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| created_at | integer | | Yes |
|
||||
| created_by | string | | No |
|
||||
| created_by_name | string | | No |
|
||||
| description | string | | Yes |
|
||||
| display_name | string | | Yes |
|
||||
| icon | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| latest_published_version_id | string | | No |
|
||||
| name | string | | Yes |
|
||||
| name_manually_edited | boolean | | No |
|
||||
| reference_count | integer | | No |
|
||||
| tags | [ string ] | | No |
|
||||
| updated_at | integer | | Yes |
|
||||
| updated_by | string | | No |
|
||||
| updated_by_name | string | | No |
|
||||
| visibility | string | | Yes |
|
||||
|
||||
#### SkillRestorePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| publish_note | string | | No |
|
||||
| version_id | string | | Yes |
|
||||
| version_name | string | | No |
|
||||
|
||||
#### SkillTagListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| data | [ [SkillTagResponse](#skilltagresponse) ] | | No |
|
||||
|
||||
#### SkillTagResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| count | integer | | Yes |
|
||||
| tag | string | | Yes |
|
||||
|
||||
#### SkillToolInferenceResult
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21686,6 +22349,60 @@ Validated metadata extracted from a Skill package.
|
||||
| inferable | boolean | | Yes |
|
||||
| reason | string | | No |
|
||||
|
||||
#### SkillVersionDeleteResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| deleted | boolean | | Yes |
|
||||
| id | string | | Yes |
|
||||
| latest_published_version_id | string | | No |
|
||||
|
||||
#### SkillVersionDetailResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| archive_size | integer | | Yes |
|
||||
| created_at | integer | | Yes |
|
||||
| files | [ [SkillFileResponse](#skillfileresponse) ] | | No |
|
||||
| hash_code | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| is_latest | boolean | | No |
|
||||
| publish_note | string | | Yes |
|
||||
| published_by | string | | No |
|
||||
| published_by_name | string | | No |
|
||||
| skill_id | string | | Yes |
|
||||
| version_name | string | | Yes |
|
||||
| version_number | integer | | Yes |
|
||||
|
||||
#### SkillVersionListResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| data | [ [SkillVersionResponse](#skillversionresponse) ] | | No |
|
||||
|
||||
#### SkillVersionResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| archive_size | integer | | Yes |
|
||||
| created_at | integer | | Yes |
|
||||
| hash_code | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| is_latest | boolean | | No |
|
||||
| publish_note | string | | Yes |
|
||||
| published_by | string | | No |
|
||||
| published_by_name | string | | No |
|
||||
| skill_id | string | | Yes |
|
||||
| version_name | string | | Yes |
|
||||
| version_number | integer | | Yes |
|
||||
|
||||
#### SkillVersionUpdatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| publish_note | string | | No |
|
||||
| version_name | string | | No |
|
||||
|
||||
#### SnippetDependencyCheckResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -22153,7 +22870,7 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| keyword | string | Search keyword | No |
|
||||
| type | string, <br>**Available values:** "", "app", "knowledge", "snippet" | Tag type filter<br>*Enum:* `""`, `"app"`, `"knowledge"`, `"snippet"` | No |
|
||||
| type | [TagType](#tagtype)<br>string | Tag type filter | No |
|
||||
|
||||
#### TagListResponse
|
||||
|
||||
@@ -24339,6 +25056,15 @@ Workflow tool configuration
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| permission_keys | [ string ] | | No |
|
||||
|
||||
#### WorkspaceSkillsQuery
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| keyword | string | Search keyword matching skill name or description. | No |
|
||||
| limit | integer, <br>**Default:** 20 | Number of items per page. | No |
|
||||
| page | integer, <br>**Default:** 1 | Page number. | No |
|
||||
| tag | [ string ] | Skill tag filters. Repeat the parameter for multiple tags. | No |
|
||||
|
||||
#### WorkspaceTenantResultResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@@ -316,10 +316,10 @@ class OracleVector(BaseVector):
|
||||
entities.append(current_entity)
|
||||
else:
|
||||
try:
|
||||
nltk.data.find("tokenizers/punkt_tab")
|
||||
nltk.data.find("tokenizers/punkt")
|
||||
nltk.data.find("corpora/stopwords")
|
||||
except LookupError:
|
||||
raise LookupError("Unable to find the required NLTK data package: punkt_tab and stopwords")
|
||||
raise LookupError("Unable to find the required NLTK data package: punkt and stopwords")
|
||||
e_str = re.sub(r"[^\w ]", "", query)
|
||||
all_tokens = nltk.word_tokenize(e_str)
|
||||
stop_words = stopwords.words("english")
|
||||
|
||||
+1
-1
@@ -206,7 +206,7 @@ storage = [
|
||||
############################################################
|
||||
# [ Tools ] dependency group
|
||||
############################################################
|
||||
tools = ["cloudscraper>=1.2.71,<2.0.0", "nltk>=3.10.0,<4.0.0"]
|
||||
tools = ["cloudscraper>=1.2.71,<2.0.0", "nltk>=3.9.1,<4.0.0"]
|
||||
|
||||
############################################################
|
||||
# [ VDB ] workspace plugins — hollow packages under providers/vdb/*
|
||||
|
||||
@@ -1599,7 +1599,9 @@ class TenantService:
|
||||
return updated_accounts
|
||||
|
||||
@staticmethod
|
||||
def iter_member_account_id_batches(tenant_id: str, batch_size: int, *, session: Session) -> Iterator[list[str]]:
|
||||
def iter_member_account_id_batches(
|
||||
tenant_id: str, batch_size: int, *, session: Session
|
||||
) -> Iterator[list[str]]:
|
||||
"""Yield workspace member account ids in bounded, ordered batches."""
|
||||
offset = 0
|
||||
while True:
|
||||
|
||||
@@ -19,12 +19,11 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import io
|
||||
import posixpath
|
||||
import re
|
||||
import zipfile
|
||||
import zlib
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field, ValidationError, field_validator
|
||||
|
||||
# Bounds — generous but finite so a hostile upload can't exhaust memory/disk.
|
||||
_MAX_ARCHIVE_BYTES = 50 * 1024 * 1024
|
||||
@@ -33,7 +32,8 @@ _MAX_SKILL_MD_BYTES = 1 * 1024 * 1024
|
||||
_MAX_ENTRIES = 5000
|
||||
_ALLOWED_EXTENSIONS = (".zip", ".skill")
|
||||
_SKILL_MD_NAME = "SKILL.md"
|
||||
_HEADING_RE = re.compile(r"^\s*#\s+(.+?)\s*$", re.MULTILINE)
|
||||
_SKILL_NAME_PATTERN = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
||||
_MAX_SKILL_DESCRIPTION_LENGTH = 1024
|
||||
|
||||
|
||||
class SkillPackageError(Exception):
|
||||
@@ -53,13 +53,18 @@ class SkillPackageError(Exception):
|
||||
class SkillManifest(BaseModel):
|
||||
"""Validated metadata extracted from a Skill package."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
name: str = Field(min_length=1, max_length=64, pattern=_SKILL_NAME_PATTERN)
|
||||
description: str = Field(min_length=1, max_length=_MAX_SKILL_DESCRIPTION_LENGTH)
|
||||
entry_path: str # path of SKILL.md inside the archive
|
||||
files: list[str] # all (safe) file paths inside the archive
|
||||
size: int # total uncompressed bytes
|
||||
hash: str # sha256 of the archive bytes
|
||||
|
||||
@field_validator("name", "description", mode="before")
|
||||
@classmethod
|
||||
def _strip_required_string(cls, value: object) -> object:
|
||||
return value.strip() if isinstance(value, str) else value
|
||||
|
||||
|
||||
class NormalizedSkillPackage(BaseModel):
|
||||
"""Canonical skill package bytes and metadata ready to store in agent drive."""
|
||||
@@ -108,14 +113,17 @@ class SkillPackageService:
|
||||
normalized_size = sum(max(info.file_size, 0) for info in normalized_members.values())
|
||||
|
||||
name, description = self._parse_skill_md(skill_md)
|
||||
manifest = SkillManifest(
|
||||
name=name,
|
||||
description=description,
|
||||
entry_path=_SKILL_MD_NAME,
|
||||
files=sorted(normalized_members),
|
||||
size=normalized_size,
|
||||
hash=hashlib.sha256(normalized_archive_bytes).hexdigest(),
|
||||
)
|
||||
try:
|
||||
manifest = SkillManifest(
|
||||
name=name,
|
||||
description=description,
|
||||
entry_path=_SKILL_MD_NAME,
|
||||
files=sorted(normalized_members),
|
||||
size=normalized_size,
|
||||
hash=hashlib.sha256(normalized_archive_bytes).hexdigest(),
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise self._manifest_validation_error(exc) from exc
|
||||
return NormalizedSkillPackage(
|
||||
manifest=manifest,
|
||||
archive_bytes=normalized_archive_bytes,
|
||||
@@ -123,6 +131,31 @@ class SkillPackageService:
|
||||
strip_prefix=strip_prefix,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _manifest_validation_error(exc: ValidationError) -> SkillPackageError:
|
||||
first_error = exc.errors()[0]
|
||||
loc = first_error["loc"]
|
||||
field = loc[0] if loc else "manifest"
|
||||
error_type = first_error["type"]
|
||||
if field == "name":
|
||||
code = "missing_skill_name" if error_type == "string_too_short" else "invalid_skill_name"
|
||||
message = (
|
||||
"SKILL.md frontmatter name is required"
|
||||
if code == "missing_skill_name"
|
||||
else "SKILL.md frontmatter name must be lowercase letters, numbers, and hyphens only, "
|
||||
"must not start or end with a hyphen, and must be at most 64 characters"
|
||||
)
|
||||
return SkillPackageError(code, message, status_code=400)
|
||||
if field == "description":
|
||||
code = "missing_skill_description" if error_type == "string_too_short" else "invalid_skill_description"
|
||||
message = (
|
||||
"SKILL.md frontmatter description is required"
|
||||
if code == "missing_skill_description"
|
||||
else f"SKILL.md frontmatter description must be at most {_MAX_SKILL_DESCRIPTION_LENGTH} characters"
|
||||
)
|
||||
return SkillPackageError(code, message, status_code=400)
|
||||
return SkillPackageError("invalid_skill_manifest", "SKILL.md frontmatter is invalid", status_code=400)
|
||||
|
||||
def _open_archive(self, *, content: bytes, filename: str) -> zipfile.ZipFile:
|
||||
self._check_extension(filename)
|
||||
if not content:
|
||||
@@ -280,13 +313,6 @@ class SkillPackageService:
|
||||
frontmatter = cls._parse_frontmatter(content)
|
||||
name = str(frontmatter.get("name") or "").strip()
|
||||
description = str(frontmatter.get("description") or "").strip()
|
||||
if not name:
|
||||
heading = _HEADING_RE.search(content)
|
||||
name = heading.group(1).strip() if heading else ""
|
||||
if not name:
|
||||
raise SkillPackageError(
|
||||
"missing_skill_name", "SKILL.md must declare a name (frontmatter or top heading)", status_code=400
|
||||
)
|
||||
return name, description
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -46,6 +46,7 @@ from models.tools import ToolFile
|
||||
from services.agent.config_skill_normalize_service import ConfigSkillNormalizeService
|
||||
from services.agent.skill_package_service import SkillPackageError
|
||||
from services.agent_drive_service import DriveFileRef
|
||||
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
|
||||
|
||||
|
||||
class AgentConfigVersionKind(StrEnum):
|
||||
@@ -98,6 +99,7 @@ class ConfigPushPayload(BaseModel):
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AgentConfigTarget:
|
||||
tenant_id: str
|
||||
agent_id: str
|
||||
version_id: str
|
||||
kind: AgentConfigVersionKind
|
||||
@@ -146,6 +148,7 @@ class AgentConfigService:
|
||||
user_id=user_id,
|
||||
)
|
||||
return AgentConfigTarget(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=target.agent_id,
|
||||
version_id=target.version_id,
|
||||
kind=target.kind,
|
||||
@@ -191,7 +194,7 @@ class AgentConfigService:
|
||||
return {
|
||||
"agent_id": target.agent_id,
|
||||
"config_version": self._config_version_payload(target),
|
||||
"items": [self._serialize_skill_item(skill) for skill in target.agent_soul.config_skills],
|
||||
"items": self._skill_items_for_target(target),
|
||||
}
|
||||
|
||||
def list_files(
|
||||
@@ -233,10 +236,27 @@ class AgentConfigService:
|
||||
config_version_kind=config_version_kind,
|
||||
user_id=user_id,
|
||||
)
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
file_id = self._available_skill_file_id(skill)
|
||||
payload, mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
|
||||
return ConfigDownload(filename=f"{skill.name}.zip", mime_type=mime_type or "application/zip", payload=payload)
|
||||
try:
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
file_id = self._available_skill_file_id(skill)
|
||||
payload, mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
|
||||
return ConfigDownload(
|
||||
filename=f"{skill.name}.zip",
|
||||
mime_type=mime_type or "application/zip",
|
||||
payload=payload,
|
||||
)
|
||||
except AgentConfigServiceError as exc:
|
||||
if exc.code != "config_skill_not_found":
|
||||
raise
|
||||
try:
|
||||
result = SkillManagementService().pull_runtime_agent_skill(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
name=name,
|
||||
)
|
||||
return ConfigDownload(filename=result.filename, mime_type=result.mime_type, payload=result.payload)
|
||||
except SkillManagementServiceError as exc:
|
||||
raise AgentConfigServiceError("config_skill_not_found", "config skill not found", status_code=404) from exc
|
||||
|
||||
def download_skill_url(
|
||||
self,
|
||||
@@ -279,9 +299,45 @@ class AgentConfigService:
|
||||
config_version_kind=config_version_kind,
|
||||
user_id=user_id,
|
||||
)
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
file_id = self._available_skill_file_id(skill)
|
||||
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
|
||||
try:
|
||||
skill = self._require_skill(target.agent_soul, name=name)
|
||||
file_id = self._available_skill_file_id(skill)
|
||||
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
|
||||
skill_item = self._serialize_skill_item(skill)
|
||||
except AgentConfigServiceError as exc:
|
||||
if exc.code != "config_skill_not_found":
|
||||
raise
|
||||
try:
|
||||
workspace_archive = SkillManagementService().pull_runtime_agent_skill(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
name=name,
|
||||
)
|
||||
except SkillManagementServiceError as skill_exc:
|
||||
raise AgentConfigServiceError(
|
||||
"config_skill_not_found",
|
||||
"config skill not found",
|
||||
status_code=404,
|
||||
) from skill_exc
|
||||
archive_bytes = workspace_archive.payload
|
||||
skill_item = next(
|
||||
(
|
||||
item
|
||||
for item in SkillManagementService().list_runtime_agent_skills(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
if item["name"] == name
|
||||
),
|
||||
{
|
||||
"id": name,
|
||||
"name": name,
|
||||
"description": "",
|
||||
"size": None,
|
||||
"hash": None,
|
||||
"mime_type": "application/zip",
|
||||
},
|
||||
)
|
||||
try:
|
||||
archive_items, skill_md = self._inspect_skill_archive(archive_bytes)
|
||||
except (OSError, ValueError, zipfile.BadZipFile) as exc:
|
||||
@@ -291,7 +347,7 @@ class AgentConfigService:
|
||||
status_code=500,
|
||||
) from exc
|
||||
return {
|
||||
**self._serialize_skill_item(skill),
|
||||
**skill_item,
|
||||
"source": "config_skill_zip",
|
||||
"files": archive_items,
|
||||
"skill_md": skill_md,
|
||||
@@ -839,6 +895,7 @@ class AgentConfigService:
|
||||
status_code=404,
|
||||
)
|
||||
return AgentConfigTarget(
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
version_id=version.id,
|
||||
kind=config_version_kind,
|
||||
@@ -1133,9 +1190,7 @@ class AgentConfigService:
|
||||
return {
|
||||
"agent_id": target.agent_id,
|
||||
"config_version": AgentConfigService._config_version_payload(target),
|
||||
"skills": {
|
||||
"items": [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
|
||||
},
|
||||
"skills": {"items": AgentConfigService._skill_items_for_target(target)},
|
||||
"files": {
|
||||
"items": [
|
||||
AgentConfigService._serialize_file_item(file_ref) for file_ref in target.agent_soul.config_files
|
||||
@@ -1145,6 +1200,20 @@ class AgentConfigService:
|
||||
"note": target.agent_soul.config_note,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _skill_items_for_target(target: AgentConfigTarget) -> list[dict[str, object]]:
|
||||
items = [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
|
||||
seen_names = {str(item["name"]) for item in items}
|
||||
for item in SkillManagementService().list_runtime_agent_skills(
|
||||
tenant_id=target.tenant_id,
|
||||
agent_id=target.agent_id,
|
||||
):
|
||||
if item["name"] in seen_names:
|
||||
continue
|
||||
seen_names.add(str(item["name"]))
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def _config_version_payload(target: AgentConfigTarget) -> dict[str, object]:
|
||||
return {
|
||||
|
||||
@@ -114,7 +114,7 @@ class AppModelConfigResponseView:
|
||||
self._session = session
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._app_model_config, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self._app_model_config, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
@property
|
||||
def annotation_reply_dict(self) -> Any:
|
||||
@@ -129,7 +129,7 @@ class AppResponseView:
|
||||
self._session = session
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._app, name) # guard-ignore: no-new-getattr -- delegates model fields
|
||||
return getattr(self._app, name) # noqa: no-new-getattr response adapter delegates model fields
|
||||
|
||||
@property
|
||||
def desc_or_prompt(self) -> str:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ from werkzeug.exceptions import NotFound
|
||||
from models.dataset import Dataset
|
||||
from models.enums import TagType
|
||||
from models.model import App, Tag, TagBinding
|
||||
from models.skill import Skill
|
||||
from models.snippet import CustomizedSnippet
|
||||
|
||||
type _TagTypeLike = TagType | str
|
||||
@@ -282,5 +283,13 @@ class TagService:
|
||||
)
|
||||
if not snippet:
|
||||
raise NotFound("Snippet not found")
|
||||
elif type == "skill":
|
||||
skill = session.scalar(
|
||||
select(Skill)
|
||||
.where(Skill.tenant_id == current_user.current_tenant_id, Skill.id == target_id)
|
||||
.limit(1)
|
||||
)
|
||||
if not skill:
|
||||
raise NotFound("Skill not found")
|
||||
else:
|
||||
raise NotFound("Invalid binding type")
|
||||
|
||||
@@ -2,94 +2,114 @@ extend = "../../.ruff.toml"
|
||||
src = ["../.."]
|
||||
|
||||
[lint]
|
||||
extend-select = ["ANN401", "ARG"]
|
||||
extend-select = ["ANN401", "ARG", "TID251"]
|
||||
|
||||
# Existing strict-mode debt. Remove a file entry when bringing it under strict checking.
|
||||
[lint.per-file-ignores]
|
||||
"controllers/console/test_apikey.py" = ["ARG002"]
|
||||
"controllers/openapi/test_app_dsl.py" = ["ARG002"]
|
||||
"controllers/service_api/dataset/test_dataset.py" = ["ARG002"]
|
||||
"controllers/web/test_conversation.py" = ["ARG002"]
|
||||
"controllers/web/test_human_input_form.py" = ["ARG001"]
|
||||
"controllers/web/test_wraps.py" = ["ARG002"]
|
||||
"core/app/layers/test_pause_state_persist_layer.py" = ["ARG002"]
|
||||
"core/rag/pipeline/test_queue_integration.py" = ["ARG002", "TID251"]
|
||||
"core/rag/retrieval/test_dataset_retrieval_integration.py" = ["ARG002"]
|
||||
"models/test_conversation_message_inputs.py" = ["ARG001"]
|
||||
"core/rag/pipeline/test_queue_integration.py" = ["ANN401", "TID251", "ARG"]
|
||||
"models/test_types_enum_text.py" = ["ANN401", "TID251"]
|
||||
"repositories/test_sqlalchemy_api_workflow_run_repository.py" = ["ARG002", "ARG005"]
|
||||
"repositories/test_workflow_run_repository.py" = ["ARG002"]
|
||||
"services/auth/test_auth_integration.py" = ["ARG002"]
|
||||
"services/dataset_collection_binding.py" = ["ARG002"]
|
||||
"services/document_service_status.py" = ["ARG002"]
|
||||
"services/rag_pipeline/test_rag_pipeline_service_db.py" = ["ARG002"]
|
||||
"services/recommend_app/test_database_retrieval.py" = ["ARG002"]
|
||||
"services/test_account_service.py" = ["ARG002"]
|
||||
"services/test_advanced_prompt_template_service.py" = ["ARG002"]
|
||||
"services/test_app_dsl_service.py" = ["ANN401", "ARG001", "ARG002", "ARG005", "TID251"]
|
||||
"services/test_app_service.py" = ["ARG002"]
|
||||
"services/test_attachment_service.py" = ["ARG002"]
|
||||
"services/test_conversation_variable_updater.py" = ["ARG002"]
|
||||
"services/test_dataset_service_batch_update_document_status.py" = ["ARG002"]
|
||||
"services/test_delete_archived_workflow_run.py" = ["ARG002"]
|
||||
"services/test_document_service_rename_document.py" = ["ARG001"]
|
||||
"services/test_end_user_service.py" = ["ARG002"]
|
||||
"services/test_feature_service.py" = ["ARG002"]
|
||||
"services/test_file_service.py" = ["ARG002"]
|
||||
"services/test_file_service_zip_and_lookup.py" = ["TID251"]
|
||||
"services/test_messages_clean_service.py" = ["ARG002", "S110"]
|
||||
"services/test_metadata_partial_update.py" = ["ARG002"]
|
||||
"services/test_metadata_service.py" = ["ARG002"]
|
||||
"services/test_model_load_balancing_service.py" = ["ARG002"]
|
||||
"services/test_model_provider_service.py" = ["ARG002"]
|
||||
"services/test_ops_service.py" = ["ARG002"]
|
||||
"services/test_webapp_auth_service.py" = ["ARG002"]
|
||||
"services/test_webhook_service.py" = ["ARG002"]
|
||||
"services/test_workflow_draft_variable_service.py" = ["ARG002"]
|
||||
"services/test_workflow_run_service.py" = ["ARG002"]
|
||||
"services/test_workflow_service.py" = ["ARG002"]
|
||||
"services/test_workspace_service.py" = ["ARG002"]
|
||||
"services/tools/test_api_tools_manage_service.py" = ["ARG002"]
|
||||
"services/tools/test_mcp_tools_manage_service.py" = ["ARG002", "ARG005"]
|
||||
"services/tools/test_tools_transform_service.py" = ["ARG002"]
|
||||
"services/workflow/test_workflow_converter.py" = ["ARG002"]
|
||||
"tasks/test_add_document_to_index_task.py" = ["ARG002"]
|
||||
"tasks/test_batch_clean_document_task.py" = ["ARG002"]
|
||||
"tasks/test_batch_create_segment_to_index_task.py" = ["ARG001", "ARG002"]
|
||||
"tasks/test_clean_dataset_task.py" = ["T201"]
|
||||
"tasks/test_clean_notion_document_task.py" = ["ARG002"]
|
||||
"tasks/test_create_segment_to_index_task.py" = ["ARG002"]
|
||||
"tasks/test_dataset_indexing_task.py" = ["ARG002"]
|
||||
"tasks/test_deal_dataset_vector_index_task.py" = ["ARG002"]
|
||||
"tasks/test_delete_segment_from_index_task.py" = ["ARG002"]
|
||||
"tasks/test_disable_segment_from_index_task.py" = ["ARG002"]
|
||||
"tasks/test_disable_segments_from_index_task.py" = ["ARG002"]
|
||||
"tasks/test_document_indexing_sync_task.py" = ["ARG002"]
|
||||
"tasks/test_document_indexing_task.py" = ["ARG002"]
|
||||
"tasks/test_document_indexing_update_task.py" = ["ARG002"]
|
||||
"tasks/test_duplicate_document_indexing_task.py" = ["ARG002"]
|
||||
"tasks/test_enable_segments_to_index_task.py" = ["ARG002"]
|
||||
"tasks/test_mail_change_mail_task.py" = ["ARG002"]
|
||||
"tasks/test_mail_email_code_login_task.py" = ["ARG002"]
|
||||
"tasks/test_mail_human_input_delivery_task.py" = ["ARG001"]
|
||||
"tasks/test_mail_inner_task.py" = ["ARG002"]
|
||||
"tasks/test_mail_invite_member_task.py" = ["ARG002"]
|
||||
"tasks/test_mail_owner_transfer_task.py" = ["ARG002"]
|
||||
"tasks/test_mail_register_task.py" = ["ARG002"]
|
||||
"tasks/test_rag_pipeline_run_tasks.py" = ["ARG002"]
|
||||
"test_workflow_pause_integration.py" = ["T201"]
|
||||
"services/test_app_dsl_service.py" = ["ANN401", "TID251", "ARG"]
|
||||
"services/test_file_service_zip_and_lookup.py" = ["ANN401", "TID251", "ARG"]
|
||||
"trigger/conftest.py" = ["ANN401", "TID251"]
|
||||
"trigger/test_trigger_e2e.py" = ["ANN401", "ARG001", "TID251"]
|
||||
"workflow/nodes/code_executor/test_code_javascript.py" = ["ARG002"]
|
||||
"workflow/nodes/code_executor/test_code_jinja2.py" = ["ARG002"]
|
||||
"workflow/nodes/code_executor/test_code_python3.py" = ["ARG002"]
|
||||
"trigger/test_trigger_e2e.py" = ["ANN401", "TID251", "ARG"]
|
||||
"controllers/console/app/test_app_apis.py" = ["ARG"]
|
||||
"controllers/console/app/test_app_import_api.py" = ["ARG"]
|
||||
"controllers/console/auth/test_oauth.py" = ["ARG"]
|
||||
"controllers/console/auth/test_password_reset.py" = ["ARG"]
|
||||
"controllers/console/datasets/test_data_source.py" = ["ARG"]
|
||||
"controllers/console/test_apikey.py" = ["ARG"]
|
||||
"controllers/console/workspace/test_tool_provider.py" = ["ARG"]
|
||||
"controllers/mcp/test_mcp.py" = ["ARG"]
|
||||
"controllers/openapi/test_app_dsl.py" = ["ARG"]
|
||||
"controllers/openapi/test_workspaces.py" = ["ARG"]
|
||||
"controllers/service_api/dataset/test_dataset.py" = ["ARG"]
|
||||
"controllers/web/test_conversation.py" = ["ARG"]
|
||||
"controllers/web/test_human_input_form.py" = ["ARG"]
|
||||
"controllers/web/test_wraps.py" = ["ARG"]
|
||||
"core/app/layers/test_pause_state_persist_layer.py" = ["ARG"]
|
||||
"core/rag/retrieval/test_dataset_retrieval_integration.py" = ["ARG"]
|
||||
"models/test_conversation_message_inputs.py" = ["ARG"]
|
||||
"models/test_conversation_status_count.py" = ["ARG"]
|
||||
"repositories/test_sqlalchemy_api_workflow_run_repository.py" = ["ARG"]
|
||||
"repositories/test_workflow_run_repository.py" = ["ARG"]
|
||||
"services/auth/test_api_key_auth_service.py" = ["ARG"]
|
||||
"services/auth/test_auth_integration.py" = ["ARG"]
|
||||
"services/dataset_collection_binding.py" = ["ARG"]
|
||||
"services/dataset_service_update_delete.py" = ["ARG"]
|
||||
"services/document_service_status.py" = ["ARG"]
|
||||
"services/enterprise/test_account_deletion_sync.py" = ["ARG"]
|
||||
"services/plugin/test_plugin_parameter_service.py" = ["ARG"]
|
||||
"services/plugin/test_plugin_service.py" = ["ARG"]
|
||||
"services/rag_pipeline/test_rag_pipeline_service_db.py" = ["ARG"]
|
||||
"services/recommend_app/test_database_retrieval.py" = ["ARG"]
|
||||
"services/test_account_service.py" = ["ARG"]
|
||||
"services/test_advanced_prompt_template_service.py" = ["ARG"]
|
||||
"services/test_annotation_service.py" = ["ARG"]
|
||||
"services/test_api_based_extension_service.py" = ["ARG"]
|
||||
"services/test_api_token_service.py" = ["ARG"]
|
||||
"services/test_app_generate_service.py" = ["ARG"]
|
||||
"services/test_app_service.py" = ["ARG"]
|
||||
"services/test_attachment_service.py" = ["ARG"]
|
||||
"services/test_conversation_variable_updater.py" = ["ARG"]
|
||||
"services/test_dataset_permission_service.py" = ["ARG"]
|
||||
"services/test_dataset_service_batch_update_document_status.py" = ["ARG"]
|
||||
"services/test_dataset_service_retrieval.py" = ["ARG"]
|
||||
"services/test_delete_archived_workflow_run.py" = ["ARG"]
|
||||
"services/test_document_service_rename_document.py" = ["ARG"]
|
||||
"services/test_end_user_service.py" = ["ARG"]
|
||||
"services/test_feature_service.py" = ["ARG"]
|
||||
"services/test_feedback_service.py" = ["ARG"]
|
||||
"services/test_file_service.py" = ["ARG"]
|
||||
"services/test_human_input_delivery_test_service.py" = ["ARG"]
|
||||
"services/test_message_service.py" = ["ARG"]
|
||||
"services/test_messages_clean_service.py" = ["ARG", "S110"]
|
||||
"services/test_metadata_partial_update.py" = ["ARG"]
|
||||
"services/test_metadata_service.py" = ["ARG"]
|
||||
"services/test_model_load_balancing_service.py" = ["ARG"]
|
||||
"services/test_model_provider_service.py" = ["ARG"]
|
||||
"services/test_oauth_server_service.py" = ["ARG"]
|
||||
"services/test_ops_service.py" = ["ARG"]
|
||||
"services/test_saved_message_service.py" = ["ARG"]
|
||||
"services/test_web_conversation_service.py" = ["ARG"]
|
||||
"services/test_webapp_auth_service.py" = ["ARG"]
|
||||
"services/test_webhook_service.py" = ["ARG"]
|
||||
"services/test_workflow_app_service.py" = ["ARG"]
|
||||
"services/test_workflow_draft_variable_service.py" = ["ARG"]
|
||||
"services/test_workflow_run_service.py" = ["ARG"]
|
||||
"services/test_workflow_service.py" = ["ARG"]
|
||||
"services/test_workspace_service.py" = ["ARG"]
|
||||
"services/tools/test_api_tools_manage_service.py" = ["ARG"]
|
||||
"services/tools/test_mcp_tools_manage_service.py" = ["ARG"]
|
||||
"services/tools/test_tools_transform_service.py" = ["ARG"]
|
||||
"services/workflow/test_workflow_converter.py" = ["ARG"]
|
||||
"tasks/test_add_document_to_index_task.py" = ["ARG"]
|
||||
"tasks/test_batch_clean_document_task.py" = ["ARG"]
|
||||
"tasks/test_batch_create_segment_to_index_task.py" = ["ARG"]
|
||||
"tasks/test_clean_dataset_task.py" = ["T201"]
|
||||
"tasks/test_clean_notion_document_task.py" = ["ARG"]
|
||||
"tasks/test_create_segment_to_index_task.py" = ["ARG"]
|
||||
"tasks/test_dataset_indexing_task.py" = ["ARG"]
|
||||
"tasks/test_deal_dataset_vector_index_task.py" = ["ARG"]
|
||||
"tasks/test_delete_segment_from_index_task.py" = ["ARG"]
|
||||
"tasks/test_disable_segment_from_index_task.py" = ["ARG"]
|
||||
"tasks/test_disable_segments_from_index_task.py" = ["ARG"]
|
||||
"tasks/test_document_indexing_sync_task.py" = ["ARG"]
|
||||
"tasks/test_document_indexing_task.py" = ["ARG"]
|
||||
"tasks/test_document_indexing_update_task.py" = ["ARG"]
|
||||
"tasks/test_duplicate_document_indexing_task.py" = ["ARG"]
|
||||
"tasks/test_enable_segments_to_index_task.py" = ["ARG"]
|
||||
"tasks/test_mail_change_mail_task.py" = ["ARG"]
|
||||
"tasks/test_mail_email_code_login_task.py" = ["ARG"]
|
||||
"tasks/test_mail_human_input_delivery_task.py" = ["ARG"]
|
||||
"tasks/test_mail_inner_task.py" = ["ARG"]
|
||||
"tasks/test_mail_invite_member_task.py" = ["ARG"]
|
||||
"tasks/test_mail_owner_transfer_task.py" = ["ARG"]
|
||||
"tasks/test_mail_register_task.py" = ["ARG"]
|
||||
"tasks/test_rag_pipeline_run_tasks.py" = ["ARG"]
|
||||
"test_workflow_pause_integration.py" = ["T201"]
|
||||
"workflow/nodes/code_executor/test_code_javascript.py" = ["ARG"]
|
||||
"workflow/nodes/code_executor/test_code_jinja2.py" = ["ARG"]
|
||||
"workflow/nodes/code_executor/test_code_python3.py" = ["ARG"]
|
||||
"workflow/nodes/code_executor/test_utils.py" = ["T201"]
|
||||
|
||||
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse"]
|
||||
msg = "Use Pydantic payload/query models instead of reqparse."
|
||||
|
||||
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse.RequestParser"]
|
||||
msg = "Use Pydantic payload/query models instead of reqparse."
|
||||
|
||||
[lint.flake8-tidy-imports.banned-api."typing.Any"]
|
||||
msg = "Use object, Protocol, TypedDict, TypeVar, ParamSpec, or a localized cast instead."
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
preset = "strict"
|
||||
strict-callable-subtyping = true
|
||||
project-includes = ["."]
|
||||
search-path = ["../.."]
|
||||
python-platform = "linux"
|
||||
python-version = "3.12.0"
|
||||
infer-with-first-use = true
|
||||
min-severity = "warn"
|
||||
|
||||
# Existing strict-mode debt. Remove a file when bringing it under strict checking.
|
||||
# Verify project-excludes from the repo root:
|
||||
# tmp_config=$(mktemp --tmpdir=api/tests/test_containers_integration_tests pyrefly-no-excludes.XXXXXX.toml)
|
||||
# awk 'BEGIN {skip=0} /^project-excludes = \[/ {skip=1; next} skip && /^\]/ {skip=0; next} !skip {print}' api/tests/test_containers_integration_tests/pyrefly.toml > "$tmp_config"
|
||||
# tmp_name=$(basename "$tmp_config")
|
||||
# comm -3 <(sed -n 's/^ "\(.*\)",$/\1/p' api/tests/test_containers_integration_tests/pyrefly.toml | sort) <(uv --directory api run pyrefly check --config "tests/test_containers_integration_tests/$tmp_name" --summary=none --output-format=min-text 2>/dev/null | rg '^ERROR ' | sed -E 's#^ERROR (tests/test_containers_integration_tests/[^:]+):.*#\1#' | sed 's#^tests/test_containers_integration_tests/##' | sort -u)
|
||||
# rm --force "$tmp_config"
|
||||
project-excludes = [
|
||||
"commands/test_legacy_model_type_migration.py",
|
||||
"controllers/console/app/test_app_apis.py",
|
||||
"controllers/console/app/test_app_import_api.py",
|
||||
"controllers/console/app/test_chat_conversation_status_count_api.py",
|
||||
"controllers/console/app/test_conversation_read_timestamp.py",
|
||||
"controllers/console/app/test_workflow_draft_variable.py",
|
||||
"controllers/console/auth/test_email_register.py",
|
||||
"controllers/console/auth/test_forgot_password.py",
|
||||
"controllers/console/auth/test_oauth.py",
|
||||
"controllers/console/auth/test_password_reset.py",
|
||||
"controllers/console/datasets/rag_pipeline/test_rag_pipeline.py",
|
||||
"controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py",
|
||||
"controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py",
|
||||
"controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py",
|
||||
"controllers/console/datasets/test_data_source.py",
|
||||
"controllers/console/explore/test_conversation.py",
|
||||
"controllers/console/test_api_based_extension.py",
|
||||
"controllers/console/test_apikey.py",
|
||||
"controllers/console/workspace/test_tool_provider.py",
|
||||
"controllers/console/workspace/test_trigger_providers.py",
|
||||
"controllers/console/workspace/test_workspace_wraps.py",
|
||||
"controllers/mcp/test_mcp.py",
|
||||
"controllers/service_api/dataset/test_dataset.py",
|
||||
"controllers/service_api/test_site.py",
|
||||
"controllers/web/test_conversation.py",
|
||||
"controllers/web/test_site.py",
|
||||
"controllers/web/test_web_forgot_password.py",
|
||||
"controllers/web/test_wraps.py",
|
||||
"core/app/layers/test_pause_state_persist_layer.py",
|
||||
"core/rag/pipeline/test_queue_integration.py",
|
||||
@@ -39,16 +57,22 @@ project-excludes = [
|
||||
"repositories/test_sqlalchemy_execution_extra_content_repository.py",
|
||||
"repositories/test_sqlalchemy_workflow_node_execution_repository.py",
|
||||
"repositories/test_workflow_run_repository.py",
|
||||
"services/auth/test_api_key_auth_service.py",
|
||||
"services/auth/test_auth_integration.py",
|
||||
"services/dataset_collection_binding.py",
|
||||
"services/dataset_service_update_delete.py",
|
||||
"services/document_service_status.py",
|
||||
"services/enterprise/test_account_deletion_sync.py",
|
||||
"services/plugin/test_plugin_parameter_service.py",
|
||||
"services/plugin/test_plugin_service.py",
|
||||
"services/rag_pipeline/test_rag_pipeline_service_db.py",
|
||||
"services/recommend_app/test_database_retrieval.py",
|
||||
"services/test_account_service.py",
|
||||
"services/test_advanced_prompt_template_service.py",
|
||||
"services/test_agent_service.py",
|
||||
"services/test_annotation_service.py",
|
||||
"services/test_api_based_extension_service.py",
|
||||
"services/test_api_token_service.py",
|
||||
"services/test_app_dsl_service.py",
|
||||
"services/test_app_generate_service.py",
|
||||
"services/test_app_service.py",
|
||||
@@ -73,15 +97,20 @@ project-excludes = [
|
||||
"services/test_document_service_rename_document.py",
|
||||
"services/test_end_user_service.py",
|
||||
"services/test_feature_service.py",
|
||||
"services/test_feedback_service.py",
|
||||
"services/test_file_service.py",
|
||||
"services/test_human_input_delivery_test.py",
|
||||
"services/test_human_input_delivery_test_service.py",
|
||||
"services/test_message_export_service.py",
|
||||
"services/test_message_service.py",
|
||||
"services/test_message_service_execution_extra_content.py",
|
||||
"services/test_message_service_extra_contents.py",
|
||||
"services/test_messages_clean_service.py",
|
||||
"services/test_metadata_partial_update.py",
|
||||
"services/test_metadata_service.py",
|
||||
"services/test_model_load_balancing_service.py",
|
||||
"services/test_model_provider_service.py",
|
||||
"services/test_oauth_server_service.py",
|
||||
"services/test_ops_service.py",
|
||||
"services/test_restore_archived_workflow_run.py",
|
||||
"services/test_saved_message_service.py",
|
||||
@@ -95,6 +124,7 @@ project-excludes = [
|
||||
"services/test_workflow_run_service.py",
|
||||
"services/test_workflow_service.py",
|
||||
"services/test_workspace_service.py",
|
||||
"services/tools/test_api_tools_manage_service.py",
|
||||
"services/tools/test_mcp_tools_manage_service.py",
|
||||
"services/tools/test_tools_transform_service.py",
|
||||
"services/tools/test_workflow_tools_manage_service.py",
|
||||
@@ -131,6 +161,7 @@ project-excludes = [
|
||||
"test_workflow_pause_integration.py",
|
||||
"trigger/conftest.py",
|
||||
"trigger/test_trigger_e2e.py",
|
||||
"workflow/nodes/code_executor/test_code_executor.py",
|
||||
"workflow/nodes/code_executor/test_code_javascript.py",
|
||||
"workflow/nodes/code_executor/test_code_jinja2.py",
|
||||
"workflow/nodes/code_executor/test_code_python3.py",
|
||||
@@ -138,7 +169,6 @@ project-excludes = [
|
||||
]
|
||||
|
||||
[errors]
|
||||
missing-override-decorator = "error"
|
||||
redundant-cast = true
|
||||
unannotated-return = true
|
||||
unnecessary-type-conversion = true
|
||||
|
||||
@@ -1,430 +0,0 @@
|
||||
extend = "../../.ruff.toml"
|
||||
src = ["../.."]
|
||||
|
||||
[lint]
|
||||
extend-select = ["ANN401", "ARG"]
|
||||
|
||||
# Existing strict-mode debt. Remove a file entry when bringing it under strict checking.
|
||||
[lint.per-file-ignores]
|
||||
"clients/agent_backend/test_request_builder.py" = ["TID251"]
|
||||
"commands/test_archive_workflow_runs.py" = ["ARG005"]
|
||||
"commands/test_data_migration_wizard.py" = ["ARG005"]
|
||||
"commands/test_legacy_model_type_migration.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"controllers/common/test_agent_app_parameters.py" = ["ARG005", "TID251"]
|
||||
"controllers/common/test_app_access.py" = ["ARG005"]
|
||||
"controllers/console/agent/test_agent_controllers.py" = ["ARG001", "ARG002", "ARG003", "ARG005", "TID251"]
|
||||
"controllers/console/app/test_agent_app_sandbox.py" = ["ARG002", "ARG005"]
|
||||
"controllers/console/app/test_agent_config_inspector.py" = ["ARG005"]
|
||||
"controllers/console/app/test_agent_drive_inspector.py" = ["ARG005"]
|
||||
"controllers/console/app/test_agent_manage_guard.py" = ["ARG001"]
|
||||
"controllers/console/app/test_agent_skills.py" = ["ARG005"]
|
||||
"controllers/console/app/test_annotation_security.py" = ["ARG002"]
|
||||
"controllers/console/app/test_app_apis.py" = ["ARG001", "ARG002"]
|
||||
"controllers/console/app/test_app_import_api.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"controllers/console/app/test_app_response_models.py" = ["ARG002", "ARG004", "ARG005"]
|
||||
"controllers/console/app/test_conversation_api.py" = ["ARG001"]
|
||||
"controllers/console/app/test_generator_api.py" = ["ARG001"]
|
||||
"controllers/console/app/test_mcp_server_response.py" = ["ARG002"]
|
||||
"controllers/console/app/test_message_api.py" = ["ARG001"]
|
||||
"controllers/console/app/test_statistic_api.py" = ["ANN401", "ARG001", "TID251"]
|
||||
"controllers/console/app/test_workflow.py" = ["ARG001", "ARG005"]
|
||||
"controllers/console/app/test_workflow_convert_api.py" = ["ARG005"]
|
||||
"controllers/console/app/test_workflow_node_output_inspector.py" = ["ANN401", "ARG001", "TID251"]
|
||||
"controllers/console/app/test_workflow_run_api.py" = ["ANN401", "TID251"]
|
||||
"controllers/console/app/workflow_draft_variables_test.py" = ["TID251"]
|
||||
"controllers/console/auth/test_account_activation.py" = ["ARG002"]
|
||||
"controllers/console/auth/test_authentication_security.py" = ["ARG002"]
|
||||
"controllers/console/auth/test_email_verification.py" = ["ARG002"]
|
||||
"controllers/console/auth/test_login_logout.py" = ["ARG002"]
|
||||
"controllers/console/auth/test_oauth.py" = ["ARG002"]
|
||||
"controllers/console/auth/test_oauth_timezone.py" = ["ARG001"]
|
||||
"controllers/console/auth/test_password_reset.py" = ["ARG002"]
|
||||
"controllers/console/auth/test_token_refresh.py" = ["ARG002"]
|
||||
"controllers/console/billing/test_billing.py" = ["ARG002"]
|
||||
"controllers/console/datasets/test_datasets.py" = ["ARG005"]
|
||||
"controllers/console/datasets/test_datasets_document.py" = ["ARG002", "ARG005"]
|
||||
"controllers/console/datasets/test_datasets_document_download.py" = ["ARG005"]
|
||||
"controllers/console/datasets/test_datasets_segments.py" = ["TID251"]
|
||||
"controllers/console/datasets/test_external.py" = ["TID251"]
|
||||
"controllers/console/datasets/test_wraps.py" = ["ARG001"]
|
||||
"controllers/console/explore/test_trial.py" = ["ANN401", "ARG001", "ARG005", "TID251"]
|
||||
"controllers/console/explore/test_wraps.py" = ["ARG001"]
|
||||
"controllers/console/snippets/test_snippet_workflow.py" = ["ARG001"]
|
||||
"controllers/console/tag/test_tags.py" = ["ARG002"]
|
||||
"controllers/console/test_files.py" = ["ARG001", "ARG002"]
|
||||
"controllers/console/test_human_input_form.py" = ["ARG001", "ARG005"]
|
||||
"controllers/console/test_init_validate.py" = ["ARG005"]
|
||||
"controllers/console/test_workspace_account.py" = ["ARG002"]
|
||||
"controllers/console/test_workspace_members.py" = ["ARG002", "ARG005"]
|
||||
"controllers/console/test_wraps.py" = ["ARG001", "ARG002"]
|
||||
"controllers/console/workspace/test_load_balancing_config.py" = ["ARG001"]
|
||||
"controllers/console/workspace/test_plugin.py" = ["ARG002", "TID251"]
|
||||
"controllers/console/workspace/test_snippets.py" = ["ARG002"]
|
||||
"controllers/console/workspace/test_tool_providers.py" = ["ARG001", "ARG005"]
|
||||
"controllers/console/workspace/test_trigger_providers.py" = ["ARG001"]
|
||||
"controllers/console/workspace/test_workspace.py" = ["ARG005"]
|
||||
"controllers/files/test_image_preview.py" = ["ARG005"]
|
||||
"controllers/files/test_tool_files.py" = ["ARG002", "ARG005"]
|
||||
"controllers/files/test_upload.py" = ["ARG002", "ARG005"]
|
||||
"controllers/inner_api/plugin/test_plugin.py" = ["ARG002"]
|
||||
"controllers/inner_api/plugin/test_plugin_wraps.py" = ["ARG001", "ARG002", "ARG003", "TID251"]
|
||||
"controllers/inner_api/test_runtime_credentials.py" = ["ARG001"]
|
||||
"controllers/mcp/test_mcp.py" = ["ARG002"]
|
||||
"controllers/openapi/auth/test_conditions.py" = ["ARG005"]
|
||||
"controllers/openapi/auth/test_flow.py" = ["ARG005"]
|
||||
"controllers/openapi/auth/test_pipeline.py" = ["ARG001"]
|
||||
"controllers/openapi/conftest.py" = ["ARG001"]
|
||||
"controllers/openapi/test_account.py" = ["ARG005"]
|
||||
"controllers/openapi/test_app_describe_builder.py" = ["ARG001"]
|
||||
"controllers/openapi/test_app_run_streaming.py" = ["ARG001"]
|
||||
"controllers/openapi/test_contract.py" = ["ARG001", "TID251"]
|
||||
"controllers/openapi/test_error_contract.py" = ["ARG002"]
|
||||
"controllers/openapi/test_human_input_form.py" = ["ARG002"]
|
||||
"controllers/openapi/test_oauth_sso_claims.py" = ["ARG002"]
|
||||
"controllers/openapi/test_workflow_events_openapi.py" = ["ARG002", "ARG005"]
|
||||
"controllers/openapi/test_workspaces_members.py" = ["ARG001"]
|
||||
"controllers/service_api/app/test_app.py" = ["ARG002"]
|
||||
"controllers/service_api/app/test_completion.py" = ["ARG002"]
|
||||
"controllers/service_api/app/test_file.py" = ["ARG002"]
|
||||
"controllers/service_api/app/test_hitl_service_api.py" = ["ARG002", "ARG005"]
|
||||
"controllers/service_api/app/test_workflow_events.py" = ["ARG005"]
|
||||
"controllers/service_api/dataset/rag_pipeline/test_rag_pipeline_workflow.py" = ["ARG002"]
|
||||
"controllers/service_api/dataset/test_dataset_segment.py" = ["ARG002"]
|
||||
"controllers/service_api/dataset/test_document.py" = ["ARG001", "ARG002"]
|
||||
"controllers/service_api/dataset/test_metadata.py" = ["ARG002"]
|
||||
"controllers/service_api/test_trace_session_id_parsing.py" = ["ARG001"]
|
||||
"controllers/service_api/test_wraps.py" = ["ARG001", "ARG002"]
|
||||
"controllers/trigger/test_trigger.py" = ["ARG002"]
|
||||
"controllers/trigger/test_webhook.py" = ["ARG002"]
|
||||
"controllers/web/conftest.py" = ["ANN401", "TID251"]
|
||||
"controllers/web/test_app.py" = ["ARG002", "ARG005"]
|
||||
"controllers/web/test_audio.py" = ["ARG002"]
|
||||
"controllers/web/test_completion.py" = ["ARG002"]
|
||||
"controllers/web/test_feature.py" = ["ARG002"]
|
||||
"controllers/web/test_human_input_form.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"controllers/web/test_message_endpoints.py" = ["ARG002"]
|
||||
"controllers/web/test_remote_files.py" = ["ARG002"]
|
||||
"controllers/web/test_saved_message.py" = ["ARG002"]
|
||||
"controllers/web/test_web_login.py" = ["ARG002"]
|
||||
"controllers/web/test_web_passport.py" = ["ARG002"]
|
||||
"controllers/web/test_workflow.py" = ["ARG002"]
|
||||
"core/agent/test_base_agent_runner.py" = ["ARG002"]
|
||||
"core/agent/test_cot_agent_runner.py" = ["ARG001"]
|
||||
"core/agent/test_cot_chat_agent_runner.py" = ["ARG002"]
|
||||
"core/agent/test_fc_agent_runner.py" = ["TID251"]
|
||||
"core/app/app_config/common/test_parameters_mapping.py" = ["ARG002"]
|
||||
"core/app/app_config/easy_ui_based_app/test_dataset_manager.py" = ["ARG001", "ARG002"]
|
||||
"core/app/app_config/easy_ui_based_app/test_model_config_converter.py" = ["ARG002"]
|
||||
"core/app/app_config/easy_ui_based_app/test_variables_manager.py" = ["ARG002"]
|
||||
"core/app/apps/advanced_chat/test_app_generator.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/app/apps/advanced_chat/test_app_runner_input_moderation.py" = ["ARG001", "ARG005"]
|
||||
"core/app/apps/advanced_chat/test_generate_task_pipeline.py" = ["ARG005"]
|
||||
"core/app/apps/advanced_chat/test_generate_task_pipeline_core.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/app/apps/agent_app/test_app_generator.py" = ["ARG001", "ARG005"]
|
||||
"core/app/apps/agent_app/test_app_runner.py" = ["ANN401", "ARG002", "ARG005", "TID251"]
|
||||
"core/app/apps/agent_app/test_input_guards.py" = ["ANN401", "ARG002", "TID251"]
|
||||
"core/app/apps/agent_app/test_resolve_agent.py" = ["ANN401", "TID251"]
|
||||
"core/app/apps/agent_app/test_runtime_request_builder.py" = ["ARG002", "TID251"]
|
||||
"core/app/apps/agent_chat/test_agent_chat_app_config_manager.py" = ["ARG005"]
|
||||
"core/app/apps/agent_chat/test_agent_chat_app_generator.py" = ["ARG001"]
|
||||
"core/app/apps/chat/test_app_config_manager.py" = ["ARG001"]
|
||||
"core/app/apps/chat/test_app_generator_and_runner.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/app/apps/common/test_workflow_response_converter_truncation.py" = ["TID251"]
|
||||
"core/app/apps/completion/test_app_runner.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/app/apps/pipeline/test_pipeline_generator.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/app/apps/pipeline/test_pipeline_runner.py" = ["ARG001", "ARG002"]
|
||||
"core/app/apps/test_advanced_chat_app_generator.py" = ["ARG001"]
|
||||
"core/app/apps/test_base_app_generator.py" = ["ARG005"]
|
||||
"core/app/apps/test_base_app_runner.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/app/apps/test_pause_resume.py" = ["ANN401", "TID251"]
|
||||
"core/app/apps/test_streaming_utils.py" = ["ARG001"]
|
||||
"core/app/apps/test_workflow_app_generator.py" = ["ARG005"]
|
||||
"core/app/apps/test_workflow_app_runner_core.py" = ["ARG001", "ARG002", "ARG004", "ARG005"]
|
||||
"core/app/apps/test_workflow_app_runner_single_node.py" = ["ANN401", "TID251"]
|
||||
"core/app/apps/test_workflow_pause_events.py" = ["ARG005"]
|
||||
"core/app/apps/workflow/test_app_generator_extra.py" = ["ARG005"]
|
||||
"core/app/apps/workflow/test_generate_task_pipeline_core.py" = ["ARG002", "ARG005"]
|
||||
"core/app/features/rate_limiting/test_rate_limit.py" = ["ARG001"]
|
||||
"core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py" = ["ARG002"]
|
||||
"core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/app/task_pipeline/test_message_cycle_manager_optimization.py" = ["ARG002"]
|
||||
"core/app/test_easy_ui_model_config_manager.py" = ["ARG005"]
|
||||
"core/app/workflow/layers/test_persistence_inspector_publish.py" = ["ANN401", "ARG005", "TID251"]
|
||||
"core/app/workflow/test_file_runtime.py" = ["ARG001", "ARG005"]
|
||||
"core/app/workflow/test_observability_layer_extra.py" = ["ARG005"]
|
||||
"core/app/workflow/test_persistence_layer.py" = ["ARG001"]
|
||||
"core/base/test_app_generator_tts_publisher.py" = ["ARG002"]
|
||||
"core/callback_handler/test_agent_tool_callback_handler.py" = ["ARG002"]
|
||||
"core/callback_handler/test_workflow_tool_callback_handler.py" = ["ARG002"]
|
||||
"core/datasource/__base/test_datasource_provider.py" = ["ARG002"]
|
||||
"core/datasource/test_datasource_file_manager.py" = ["ARG001", "ARG002"]
|
||||
"core/datasource/test_notion_provider.py" = ["ARG002", "TID251"]
|
||||
"core/datasource/test_website_crawl.py" = ["ARG002"]
|
||||
"core/datasource/utils/test_message_transformer.py" = ["ARG002"]
|
||||
"core/entities/test_entities_mcp_provider.py" = ["ARG001"]
|
||||
"core/entities/test_entities_provider_configuration.py" = ["ANN401", "ARG001", "ARG005", "TID251"]
|
||||
"core/extension/test_extensible.py" = ["ARG002", "ARG005"]
|
||||
"core/external_data_tool/api/test_api.py" = ["ARG001"]
|
||||
"core/external_data_tool/test_base.py" = ["TID251"]
|
||||
"core/external_data_tool/test_external_data_fetch.py" = ["ARG001"]
|
||||
"core/helper/code_executor/test_code_executor.py" = ["TID251"]
|
||||
"core/helper/code_executor/test_template_transformer.py" = ["ANN401", "TID251"]
|
||||
"core/llm_generator/test_llm_generator.py" = ["ARG002"]
|
||||
"core/mcp/auth/test_auth_flow.py" = ["ARG002"]
|
||||
"core/mcp/client/test_session.py" = ["ARG001", "TID251"]
|
||||
"core/mcp/client/test_sse.py" = ["ARG001", "TID251"]
|
||||
"core/mcp/client/test_streamable_http.py" = ["ARG001", "ARG005", "S110", "TID251"]
|
||||
"core/mcp/session/test_base_session.py" = ["S110"]
|
||||
"core/mcp/session/test_client_session.py" = ["ARG005"]
|
||||
"core/mcp/test_mcp_client.py" = ["ARG002"]
|
||||
"core/memory/test_token_buffer_memory.py" = ["ARG002"]
|
||||
"core/moderation/test_content_moderation.py" = ["TID251"]
|
||||
"core/moderation/test_output_moderation.py" = ["ARG001", "ARG002"]
|
||||
"core/ops/test_base_trace_instance.py" = ["ARG001"]
|
||||
"core/ops/test_lookup_helpers.py" = ["ARG002"]
|
||||
"core/ops/test_ops_trace_manager.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/ops/test_trace_queue_manager.py" = ["ARG004"]
|
||||
"core/ops/test_trace_session_metadata.py" = ["ARG001", "ARG005"]
|
||||
"core/plugin/impl/test_agent_client.py" = ["ARG001"]
|
||||
"core/plugin/impl/test_datasource_manager.py" = ["ARG001"]
|
||||
"core/plugin/impl/test_oauth_handler.py" = ["ARG001"]
|
||||
"core/plugin/impl/test_tool_manager.py" = ["ARG001"]
|
||||
"core/plugin/impl/test_trigger_client.py" = ["ARG001"]
|
||||
"core/plugin/test_endpoint_client.py" = ["ARG002"]
|
||||
"core/plugin/test_model_runtime_adapter.py" = ["ARG002"]
|
||||
"core/plugin/test_plugin_runtime.py" = ["ARG001", "ARG002", "TID251"]
|
||||
"core/prompt/test_advanced_prompt_transform.py" = ["ARG005"]
|
||||
"core/prompt/test_prompt_transform.py" = ["ARG005"]
|
||||
"core/rag/datasource/keyword/jieba/test_jieba.py" = ["ARG001", "TID251"]
|
||||
"core/rag/datasource/keyword/jieba/test_jieba_keyword_table_handler.py" = ["ARG004"]
|
||||
"core/rag/datasource/keyword/test_keyword_factory.py" = ["ARG005"]
|
||||
"core/rag/datasource/test_datasource_retrieval.py" = ["ARG001", "ARG002", "ARG005", "TID251"]
|
||||
"core/rag/datasource/test_retrieval_attachment_access.py" = ["ARG005"]
|
||||
"core/rag/datasource/vdb/test_vector_factory.py" = ["ARG002"]
|
||||
"core/rag/embedding/test_embedding_base.py" = ["TID251"]
|
||||
"core/rag/embedding/test_embedding_service.py" = ["ARG001"]
|
||||
"core/rag/extractor/firecrawl/test_firecrawl.py" = ["TID251"]
|
||||
"core/rag/extractor/test_csv_extractor.py" = ["ARG001", "ARG005"]
|
||||
"core/rag/extractor/test_excel_extractor.py" = ["ARG002", "ARG005"]
|
||||
"core/rag/extractor/test_extract_processor.py" = ["ARG001", "ARG005"]
|
||||
"core/rag/extractor/test_helpers.py" = ["ARG002"]
|
||||
"core/rag/extractor/test_markdown_extractor.py" = ["ARG001"]
|
||||
"core/rag/extractor/test_notion_extractor.py" = ["ARG002", "ARG005"]
|
||||
"core/rag/extractor/test_pdf_extractor.py" = ["ARG001", "ARG005"]
|
||||
"core/rag/extractor/test_text_extractor.py" = ["ARG001"]
|
||||
"core/rag/extractor/test_word_extractor.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/rag/extractor/unstructured/test_unstructured_extractors.py" = ["ARG001", "ARG005"]
|
||||
"core/rag/extractor/watercrawl/test_watercrawl.py" = ["ARG001", "ARG005", "TID251"]
|
||||
"core/rag/indexing/processor/conftest.py" = ["ANN401", "ARG002", "TID251"]
|
||||
"core/rag/indexing/processor/test_paragraph_index_processor.py" = ["ARG002", "TID251"]
|
||||
"core/rag/indexing/processor/test_qa_index_processor.py" = ["ARG001", "TID251"]
|
||||
"core/rag/indexing/test_index_processor.py" = ["ARG005"]
|
||||
"core/rag/indexing/test_indexing_runner.py" = ["ARG005", "TID251"]
|
||||
"core/rag/pipeline/test_queue.py" = ["ARG002"]
|
||||
"core/rag/retrieval/test_dataset_retrieval.py" = ["ARG001", "ARG002", "ARG005", "TID251"]
|
||||
"core/rag/splitter/test_text_splitter.py" = ["ARG002", "ARG005"]
|
||||
"core/repositories/test_celery_workflow_execution_repository.py" = ["ARG002"]
|
||||
"core/repositories/test_celery_workflow_node_execution_repository.py" = ["ARG002"]
|
||||
"core/repositories/test_human_input_form_repository_impl.py" = ["ARG001", "ARG005"]
|
||||
"core/repositories/test_human_input_repository.py" = ["ANN401", "ARG001", "ARG005", "TID251"]
|
||||
"core/repositories/test_sqlalchemy_workflow_node_execution_repository.py" = ["ANN401", "ARG002", "ARG005", "TID251"]
|
||||
"core/repositories/test_workflow_node_execution_truncation.py" = ["TID251"]
|
||||
"core/schemas/test_resolver.py" = ["ARG005", "T201"]
|
||||
"core/telemetry/test_facade.py" = ["ARG002", "ARG004"]
|
||||
"core/telemetry/test_gateway_integration.py" = ["ARG002"]
|
||||
"core/test_model_manager.py" = ["ARG001"]
|
||||
"core/test_trigger_debug_event_selectors.py" = ["ARG002"]
|
||||
"core/tools/test_base_tool.py" = ["ANN401", "ARG002", "TID251"]
|
||||
"core/tools/test_builtin_tool_base.py" = ["ARG001", "ARG002", "TID251"]
|
||||
"core/tools/test_builtin_tool_provider.py" = ["ARG001", "ARG005", "TID251"]
|
||||
"core/tools/test_builtin_tools_extra.py" = ["ARG005"]
|
||||
"core/tools/test_custom_tool.py" = ["ARG001", "ARG005", "TID251"]
|
||||
"core/tools/test_dataset_retriever_tool.py" = ["ARG005"]
|
||||
"core/tools/test_mcp_tool.py" = ["S110"]
|
||||
"core/tools/test_tool_engine.py" = ["ANN401", "ARG002", "ARG005", "TID251"]
|
||||
"core/tools/test_tool_file_manager.py" = ["ARG001"]
|
||||
"core/tools/test_tool_label_manager.py" = ["TID251"]
|
||||
"core/tools/test_tool_manager.py" = ["ANN401", "ARG001", "ARG005", "TID251"]
|
||||
"core/tools/test_tool_provider_controller.py" = ["TID251"]
|
||||
"core/tools/utils/test_configuration.py" = ["ARG001", "ARG002", "TID251"]
|
||||
"core/tools/utils/test_encryption.py" = ["ANN401", "TID251"]
|
||||
"core/tools/utils/test_message_transformer.py" = ["TID251"]
|
||||
"core/tools/utils/test_misc_utils_extra.py" = ["ARG002"]
|
||||
"core/tools/utils/test_model_invocation_utils.py" = ["ARG005", "TID251"]
|
||||
"core/tools/utils/test_parser.py" = ["TID251"]
|
||||
"core/tools/utils/test_web_reader_tool.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/tools/workflow_as_tool/test_provider.py" = ["TID251"]
|
||||
"core/tools/workflow_as_tool/test_tool.py" = ["ANN401", "ARG001", "ARG005", "TID251"]
|
||||
"core/trigger/conftest.py" = ["ANN401", "TID251"]
|
||||
"core/trigger/debug/test_debug_event_selectors.py" = ["ARG002", "TID251"]
|
||||
"core/variables/test_segment_type_validation.py" = ["TID251"]
|
||||
"core/workflow/context/test_execution_context.py" = ["ANN401", "ARG002", "S110", "TID251"]
|
||||
"core/workflow/context/test_flask_app_context.py" = ["ARG002"]
|
||||
"core/workflow/generator/test_runner.py" = ["ARG001", "ARG002", "TID251"]
|
||||
"core/workflow/generator/test_runner_missing.py" = ["ARG003"]
|
||||
"core/workflow/generator/test_tool_catalogue.py" = ["ARG002"]
|
||||
"core/workflow/graph_engine/layers/test_observability.py" = ["ARG002"]
|
||||
"core/workflow/graph_engine/test_mock_config.py" = ["TID251"]
|
||||
"core/workflow/graph_engine/test_mock_factory.py" = ["TID251"]
|
||||
"core/workflow/graph_engine/test_mock_nodes.py" = ["ANN401", "S110", "TID251"]
|
||||
"core/workflow/graph_engine/test_parallel_human_input_join_resume.py" = ["ARG002", "TID251"]
|
||||
"core/workflow/graph_engine/test_table_runner.py" = ["ARG001", "TID251"]
|
||||
"core/workflow/nodes/agent_v2/test_agent_node.py" = ["ARG001", "ARG002", "ARG005"]
|
||||
"core/workflow/nodes/agent_v2/test_ask_human_hitl.py" = ["ANN401", "TID251"]
|
||||
"core/workflow/nodes/agent_v2/test_dify_tools_builder.py" = ["ANN401", "ARG001", "ARG002", "ARG005", "TID251"]
|
||||
"core/workflow/nodes/agent_v2/test_output_adapter.py" = ["ARG005"]
|
||||
"core/workflow/nodes/agent_v2/test_runtime_request_builder.py" = ["ARG002"]
|
||||
"core/workflow/nodes/agent_v2/test_validators.py" = ["ARG001"]
|
||||
"core/workflow/nodes/http_request/test_http_request_node.py" = ["ANN401", "ARG002", "TID251"]
|
||||
"core/workflow/nodes/human_input/test_entities.py" = ["TID251"]
|
||||
"core/workflow/nodes/human_input/test_human_input_form_filled_event.py" = ["TID251"]
|
||||
"core/workflow/nodes/iteration/test_iteration_child_engine_errors.py" = ["ARG002", "TID251"]
|
||||
"core/workflow/nodes/knowledge_index/test_knowledge_index_node.py" = ["ARG002"]
|
||||
"core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py" = ["ARG002"]
|
||||
"core/workflow/nodes/llm/test_node.py" = ["ARG002"]
|
||||
"core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py" = ["TID251"]
|
||||
"core/workflow/nodes/test_document_extractor_node.py" = ["ARG001"]
|
||||
"core/workflow/nodes/tool/test_tool_node.py" = ["ANN401", "ARG002", "TID251"]
|
||||
"core/workflow/nodes/webhook/test_webhook_file_conversion.py" = ["TID251"]
|
||||
"core/workflow/nodes/webhook/test_webhook_node.py" = ["TID251"]
|
||||
"core/workflow/test_form_input_serialization_compat.py" = ["ANN401", "TID251"]
|
||||
"core/workflow/test_human_input_adapter.py" = ["ARG005"]
|
||||
"core/workflow/test_node_factory.py" = ["ARG002"]
|
||||
"core/workflow/test_workflow_entry.py" = ["ARG001"]
|
||||
"enterprise/telemetry/test_enterprise_trace.py" = ["ARG002", "TID251"]
|
||||
"enterprise/telemetry/test_exporter.py" = ["ARG001"]
|
||||
"enterprise/telemetry/test_gateway.py" = ["ARG002"]
|
||||
"events/event_handlers/test_delete_tool_parameters_cache_when_sync_draft_workflow.py" = ["ARG005"]
|
||||
"extensions/logstore/test_sql_escape.py" = ["ARG001", "ARG002"]
|
||||
"extensions/otel/decorators/handlers/test_generate_handler.py" = ["ARG001", "ARG002"]
|
||||
"extensions/otel/decorators/handlers/test_workflow_app_runner_handler.py" = ["ARG001"]
|
||||
"extensions/otel/decorators/test_base.py" = ["ARG002"]
|
||||
"extensions/otel/decorators/test_handler.py" = ["ARG002"]
|
||||
"extensions/otel/test_retrieval_tracing.py" = ["ARG001"]
|
||||
"extensions/test_ext_request_logging.py" = ["ARG002"]
|
||||
"extensions/test_redis.py" = ["ARG001"]
|
||||
"factories/test_build_from_mapping.py" = ["ARG001"]
|
||||
"factories/test_file_factory.py" = ["ARG001"]
|
||||
"factories/test_variable_factory.py" = ["TID251"]
|
||||
"fields/test_file_fields.py" = ["ARG005"]
|
||||
"libs/_human_input/support.py" = ["TID251"]
|
||||
"libs/broadcast_channel/redis/test_channel_unit_tests.py" = ["ARG002", "ARG005"]
|
||||
"libs/broadcast_channel/redis/test_streams_channel_unit_tests.py" = ["ARG001", "ARG002", "TID251"]
|
||||
"libs/test_cron_compatibility.py" = ["S110"]
|
||||
"libs/test_email_i18n.py" = ["ANN401", "TID251"]
|
||||
"libs/test_oauth_bearer_rate_limit_ordering.py" = ["ARG001"]
|
||||
"libs/test_pyrefly_type_coverage.py" = ["TID251"]
|
||||
"libs/test_schedule_utils_enhanced.py" = ["S110"]
|
||||
"libs/test_sendgrid_client.py" = ["ARG001", "TID251"]
|
||||
"libs/test_smtp_client.py" = ["TID251"]
|
||||
"models/test_dataset_models.py" = ["ARG005"]
|
||||
"models/test_plugin_entities.py" = ["TID251"]
|
||||
"models/test_snippet.py" = ["ARG001"]
|
||||
"oss/__mock/aliyun_oss.py" = ["ARG002"]
|
||||
"oss/__mock/baidu_obs.py" = ["ARG002"]
|
||||
"oss/__mock/base.py" = ["ARG002"]
|
||||
"oss/__mock/tencent_cos.py" = ["ARG002"]
|
||||
"oss/__mock/volcengine_tos.py" = ["ARG002"]
|
||||
"oss/aliyun_oss/aliyun_oss/test_aliyun_oss.py" = ["ARG002"]
|
||||
"oss/baidu_obs/test_baidu_obs.py" = ["ARG002"]
|
||||
"oss/opendal/test_opendal.py" = ["ARG002"]
|
||||
"oss/tencent_cos/test_tencent_cos.py" = ["ARG002"]
|
||||
"oss/volcengine_tos/test_volcengine_tos.py" = ["ARG002"]
|
||||
"services/agent/test_agent_observability_service.py" = ["ARG002", "ARG005"]
|
||||
"services/agent/test_agent_services.py" = ["ARG001", "ARG002", "ARG003", "ARG005"]
|
||||
"services/agent/test_composer_candidates.py" = ["ARG005"]
|
||||
"services/agent/test_prompt_mentions.py" = ["ARG005"]
|
||||
"services/agent/test_skill_tool_inference_service.py" = ["ARG001", "ARG005"]
|
||||
"services/auth/test_jina_auth_standalone_module.py" = ["TID251"]
|
||||
"services/controller_api.py" = ["ARG002"]
|
||||
"services/data_migration/test_import_service.py" = ["ARG002", "ARG005"]
|
||||
"services/dataset_service_test_helpers.py" = ["TID251"]
|
||||
"services/enterprise/test_account_deletion_sync.py" = ["ARG001"]
|
||||
"services/enterprise/test_rbac_service.py" = ["ARG002"]
|
||||
"services/enterprise/test_traceparent_propagation.py" = ["ARG002"]
|
||||
"services/hit_service.py" = ["TID251"]
|
||||
"services/plugin/test_plugin_parameter_service.py" = ["ARG002"]
|
||||
"services/rag_pipeline/pipeline_template/test_built_in_retrieval.py" = ["ARG001"]
|
||||
"services/rag_pipeline/test_rag_pipeline_dsl_service.py" = ["ARG001", "ARG005", "T201", "TID251"]
|
||||
"services/rag_pipeline/test_rag_pipeline_service.py" = ["ARG001", "ARG005"]
|
||||
"services/rag_pipeline/test_rag_pipeline_task_proxy.py" = ["ARG001", "ARG005"]
|
||||
"services/rag_pipeline/test_rag_pipeline_transform_service.py" = ["ARG001"]
|
||||
"services/recommend_app/test_remote_retrieval.py" = ["ARG002"]
|
||||
"services/retention/workflow_run/test_archive_download_preparation.py" = ["ARG002"]
|
||||
"services/retention/workflow_run/test_archive_log_service.py" = ["ARG001", "ARG002"]
|
||||
"services/retention/workflow_run/test_bundle_archive_maintenance.py" = ["TID251"]
|
||||
"services/retention/workflow_run/test_restore_archived_workflow_run.py" = ["ARG002"]
|
||||
"services/test_account_service.py" = ["ARG001", "ARG002"]
|
||||
"services/test_annotation_service.py" = ["ANN401", "TID251"]
|
||||
"services/test_api_token_service.py" = ["ARG002"]
|
||||
"services/test_app_generate_service.py" = ["ARG001", "ARG002", "ARG004"]
|
||||
"services/test_app_generate_service_streaming_integration.py" = ["ARG002", "TID251"]
|
||||
"services/test_archive_workflow_run_logs.py" = ["ARG002"]
|
||||
"services/test_audio_service.py" = ["ARG002", "TID251"]
|
||||
"services/test_batch_indexing_base.py" = ["ANN401", "TID251"]
|
||||
"services/test_billing_service.py" = ["ARG001", "ARG002"]
|
||||
"services/test_clear_free_plan_expired_workflow_run_logs.py" = ["ANN401", "ARG002", "ARG005", "TID251"]
|
||||
"services/test_clear_free_plan_tenant_expired_logs.py" = ["ARG002", "ARG003"]
|
||||
"services/test_dataset_service_document.py" = ["ARG002"]
|
||||
"services/test_dataset_service_lock_not_owned.py" = ["ARG001", "ARG005"]
|
||||
"services/test_dataset_service_segment.py" = ["ARG002"]
|
||||
"services/test_datasource_provider_service.py" = ["ARG002"]
|
||||
"services/test_external_dataset_service.py" = ["ARG002", "TID251"]
|
||||
"services/test_feature_service_human_input_email_delivery.py" = ["ARG005"]
|
||||
"services/test_feedback_service.py" = ["ARG002"]
|
||||
"services/test_human_input_delivery_test_service.py" = ["ARG005"]
|
||||
"services/test_knowledge_service.py" = ["TID251"]
|
||||
"services/test_message_service.py" = ["ARG002"]
|
||||
"services/test_messages_clean_service.py" = ["TID251"]
|
||||
"services/test_model_load_balancing_service.py" = ["ANN401", "ARG001", "ARG005", "TID251"]
|
||||
"services/test_model_provider_service.py" = ["ANN401", "TID251"]
|
||||
"services/test_model_provider_service_sanitization.py" = ["ARG002", "ARG005"]
|
||||
"services/test_oauth_server_service.py" = ["ARG002"]
|
||||
"services/test_operation_service.py" = ["TID251"]
|
||||
"services/test_rag_pipeline_task_proxy.py" = ["ARG002"]
|
||||
"services/test_recommended_app_service.py" = ["ARG001"]
|
||||
"services/test_schedule_service.py" = ["ANN401", "TID251"]
|
||||
"services/test_snippet_service.py" = ["ARG001", "ARG002"]
|
||||
"services/test_summary_index_service.py" = ["ARG001"]
|
||||
"services/test_telemetry_service.py" = ["ARG001", "ARG005"]
|
||||
"services/test_variable_truncator.py" = ["ARG002", "TID251"]
|
||||
"services/test_variable_truncator_additional.py" = ["ANN401", "TID251"]
|
||||
"services/test_vector_service.py" = ["ARG001", "TID251"]
|
||||
"services/test_webhook_service_additional.py" = ["ANN401", "ARG002", "TID251"]
|
||||
"services/test_website_service.py" = ["TID251"]
|
||||
"services/test_workflow_comment_service.py" = ["ARG001", "ARG002"]
|
||||
"services/test_workflow_run_service.py" = ["ANN401", "ARG002", "TID251"]
|
||||
"services/test_workflow_service.py" = ["ANN401", "ARG002", "TID251"]
|
||||
"services/tools/test_builtin_tools_manage_service.py" = ["ARG001", "ARG002"]
|
||||
"services/tools/test_tools_manage_service.py" = ["ARG002"]
|
||||
"services/workflow/test_inspector_events.py" = ["ANN401", "TID251"]
|
||||
"services/workflow/test_node_output_inspector_service.py" = ["TID251"]
|
||||
"services/workflow/test_workflow_converter_additional.py" = ["ANN401", "ARG001", "ARG005", "TID251"]
|
||||
"services/workflow/test_workflow_event_snapshot_service.py" = ["ANN401", "ARG002", "ARG005", "TID251"]
|
||||
"services/workflow/test_workflow_event_snapshot_service_additional.py" = ["ANN401", "ARG002", "ARG005", "TID251"]
|
||||
"tasks/test_agent_backend_session_cleanup_task.py" = ["ARG005"]
|
||||
"tasks/test_clean_dataset_task.py" = ["ARG002"]
|
||||
"tasks/test_clean_document_task.py" = ["ARG002"]
|
||||
"tasks/test_dataset_indexing_task.py" = ["ARG001", "ARG002"]
|
||||
"tasks/test_document_indexing_sync_task.py" = ["ARG002"]
|
||||
"tasks/test_duplicate_document_indexing_task.py" = ["ARG002"]
|
||||
"tasks/test_human_input_timeout_tasks.py" = ["ARG001", "ARG002", "ARG005", "TID251"]
|
||||
"tasks/test_initialize_created_app_rbac_access_task.py" = ["ARG005"]
|
||||
"tasks/test_mail_send_task.py" = ["ARG002"]
|
||||
"tasks/test_ops_trace_task.py" = ["ARG004"]
|
||||
"tasks/test_process_tenant_plugin_autoupgrade_check_task.py" = ["ARG001"]
|
||||
"tasks/test_remove_app_and_related_data_task.py" = ["ARG002"]
|
||||
"tasks/test_trigger_processing_tasks.py" = ["ARG002"]
|
||||
"tasks/test_workflow_execute_task.py" = ["ARG005"]
|
||||
"test_app_factory.py" = ["ARG001"]
|
||||
"test_pytest_dify.py" = ["ARG001"]
|
||||
"tools/test_mcp_tool.py" = ["TID251"]
|
||||
|
||||
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse"]
|
||||
msg = "Use Pydantic payload/query models instead of reqparse."
|
||||
|
||||
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse.RequestParser"]
|
||||
msg = "Use Pydantic payload/query models instead of reqparse."
|
||||
|
||||
[lint.flake8-tidy-imports.banned-api."typing.Any"]
|
||||
msg = "Use object, Protocol, TypedDict, TypeVar, ParamSpec, or a localized cast instead."
|
||||
@@ -85,42 +85,6 @@ def main_branch_rev(repo: Path) -> str:
|
||||
return git(repo, "rev-parse", "main")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source_line", "rule_id"),
|
||||
[
|
||||
(
|
||||
"value = getattr(module, name) # guard-ignore: no-new-getattr -- lazy export proxy",
|
||||
"no-new-getattr",
|
||||
),
|
||||
(
|
||||
"session.rollback() # guard-ignore: no-new-controller-sqlalchemy -- decorator owns rollback",
|
||||
"no-new-controller-sqlalchemy",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_has_reasoned_guard_ignore_accepts_custom_rules(source_line: str, rule_id: str) -> None:
|
||||
module = load_guard_module()
|
||||
|
||||
assert module.has_reasoned_guard_ignore(source_line, rule_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source_line", "rule_id"),
|
||||
[
|
||||
("value = getattr(module, name) # noqa: no-new-getattr legacy marker", "no-new-getattr"),
|
||||
("value = getattr(module, name) # guard-ignore: no-new-getattr", "no-new-getattr"),
|
||||
(
|
||||
"value = getattr(module, name) # guard-ignore: another-rule -- wrong rule",
|
||||
"no-new-getattr",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_has_reasoned_guard_ignore_rejects_invalid_markers(source_line: str, rule_id: str) -> None:
|
||||
module = load_guard_module()
|
||||
|
||||
assert not module.has_reasoned_guard_ignore(source_line, rule_id)
|
||||
|
||||
|
||||
def test_resolve_ast_grep_command_prefers_ast_grep(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
module = load_guard_module()
|
||||
monkeypatch.setattr(
|
||||
@@ -812,7 +776,7 @@ def test_modified_hunk_with_increased_getattr_count_fails(tmp_path: Path) -> Non
|
||||
assert "net-new getattr" in result.stderr
|
||||
|
||||
|
||||
def test_inline_guard_ignore_with_explanatory_text_skips_added_getattr(tmp_path: Path) -> None:
|
||||
def test_inline_noqa_suppression_with_explanatory_text_skips_added_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -831,20 +795,20 @@ def test_inline_guard_ignore_with_explanatory_text_skips_added_getattr(tmp_path:
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "dynamic_name", None) # guard-ignore: no-new-getattr -- plugin-defined attributes
|
||||
return getattr(obj, "dynamic_name", None) # noqa: no-new-getattr needed for plugin-defined attributes
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add suppressed getattr")
|
||||
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
assert "guard-ignore: no-new-getattr -- plugin-defined attributes" in (tmp_path / "pkg/existing.py").read_text(
|
||||
assert "no-new-getattr needed for plugin-defined attributes" in (tmp_path / "pkg/existing.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert result.returncode == 0, stderr_lines(result)
|
||||
|
||||
|
||||
def test_inline_guard_ignore_without_explanatory_text_is_not_sufficient(tmp_path: Path) -> None:
|
||||
def test_inline_noqa_without_explanatory_text_is_not_sufficient(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
@@ -863,10 +827,10 @@ def test_inline_guard_ignore_without_explanatory_text_is_not_sufficient(tmp_path
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "dynamic_name", None) # guard-ignore: no-new-getattr
|
||||
return getattr(obj, "dynamic_name", None) # noqa: no-new-getattr
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add bare guard ignore getattr")
|
||||
commit_all(tmp_path, "add bare noqa getattr")
|
||||
|
||||
result = run_script(tmp_path, "--base-rev", base_rev)
|
||||
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from inspect import unwrap
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.workspace.skills import (
|
||||
WorkspaceAgentSkillBindingsApi,
|
||||
WorkspaceSkillAssistMessageApi,
|
||||
WorkspaceSkillFilesApi,
|
||||
WorkspaceSkillsApi,
|
||||
WorkspaceSkillTagsApi,
|
||||
WorkspaceSkillVersionApi,
|
||||
)
|
||||
from models.account import Account
|
||||
from services.skill_management_service import SkillAssistAttachmentPayload, SkillManagementServiceError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app() -> Flask:
|
||||
flask_app = Flask("test_workspace_skills")
|
||||
flask_app.config["TESTING"] = True
|
||||
return flask_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def current_user() -> Account:
|
||||
user = Account(name="Test User", email="test@example.com")
|
||||
user.id = "user-1"
|
||||
return user
|
||||
|
||||
|
||||
def _skill_detail() -> dict:
|
||||
return {
|
||||
"id": "skill-1",
|
||||
"name": "finance-sop",
|
||||
"display_name": "Finance SOP",
|
||||
"icon": "📄",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"name_manually_edited": False,
|
||||
"visibility": "workspace",
|
||||
"latest_published_version_id": None,
|
||||
"reference_count": 0,
|
||||
"created_by": "user-1",
|
||||
"created_by_name": "Test User",
|
||||
"updated_by": "user-1",
|
||||
"updated_by_name": "Test User",
|
||||
"created_at": 1,
|
||||
"updated_at": 1,
|
||||
"files": [
|
||||
{
|
||||
"id": "file-1",
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"storage": "text",
|
||||
"mime_type": "text/markdown",
|
||||
"content": "---\nname: finance-sop\n---\n# Body",
|
||||
"tool_file_id": None,
|
||||
"size": 32,
|
||||
"hash": "hash",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_create_skill_validates_payload_and_returns_detail(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillsApi()
|
||||
method = unwrap(api.post)
|
||||
service = MagicMock()
|
||||
service.create_skill.return_value = _skill_detail()
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="POST"),
|
||||
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value={}),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload, status = method(api, "tenant-1", current_user)
|
||||
|
||||
assert status == 201
|
||||
assert payload["id"] == "skill-1"
|
||||
assert payload["files"][0]["path"] == "SKILL.md"
|
||||
service.create_skill.assert_called_once()
|
||||
assert service.create_skill.call_args.kwargs["tenant_id"] == "tenant-1"
|
||||
assert service.create_skill.call_args.kwargs["user_id"] == "user-1"
|
||||
|
||||
|
||||
def test_list_skills_uses_default_pagination_when_query_omits_page_and_limit(app: Flask) -> None:
|
||||
api = WorkspaceSkillsApi()
|
||||
method = unwrap(api.get)
|
||||
service = MagicMock()
|
||||
service.list_skills.return_value = {
|
||||
"data": [],
|
||||
"has_more": False,
|
||||
"limit": 20,
|
||||
"page": 1,
|
||||
"total": 0,
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/?keyword=finance&tag=ops&tag=", method="GET"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1")
|
||||
|
||||
assert payload == {
|
||||
"data": [],
|
||||
"has_more": False,
|
||||
"limit": 20,
|
||||
"page": 1,
|
||||
"total": 0,
|
||||
}
|
||||
service.list_skills.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
keyword="finance",
|
||||
page=1,
|
||||
limit=20,
|
||||
tags=["ops"],
|
||||
)
|
||||
|
||||
|
||||
def test_get_agent_skill_bindings_returns_card_data(app: Flask) -> None:
|
||||
api = WorkspaceAgentSkillBindingsApi()
|
||||
method = unwrap(api.get)
|
||||
service = MagicMock()
|
||||
service.list_agent_bindings.return_value = {
|
||||
"agent_id": "agent-1",
|
||||
"skill_ids": ["skill-1"],
|
||||
"data": [
|
||||
{
|
||||
"id": "skill-1",
|
||||
"priority": 0,
|
||||
"name": "finance-sop",
|
||||
"display_name": "Finance SOP",
|
||||
"icon": "📄",
|
||||
"description": "Handle finance.",
|
||||
"tags": ["Finance"],
|
||||
"status": "published",
|
||||
"file_count": 2,
|
||||
"latest_published_version_id": "version-1",
|
||||
"latest_published_at": 123,
|
||||
"updated_at": 124,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", "agent-1")
|
||||
|
||||
assert payload["skill_ids"] == ["skill-1"]
|
||||
assert payload["data"][0]["display_name"] == "Finance SOP"
|
||||
assert payload["data"][0]["file_count"] == 2
|
||||
service.list_agent_bindings.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
|
||||
|
||||
def test_patch_skill_file_operation_validates_payload_and_returns_detail(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillFilesApi()
|
||||
method = unwrap(api.patch)
|
||||
service = MagicMock()
|
||||
service.apply_draft_file_operation.return_value = _skill_detail()
|
||||
request_payload = {
|
||||
"operation": "upsert_text",
|
||||
"path": "references/policy.md",
|
||||
"content": "Policy",
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="PATCH"),
|
||||
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=request_payload),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", current_user, "skill-1")
|
||||
|
||||
assert payload["id"] == "skill-1"
|
||||
service.apply_draft_file_operation.assert_called_once()
|
||||
call = service.apply_draft_file_operation.call_args.kwargs
|
||||
assert call["tenant_id"] == "tenant-1"
|
||||
assert call["user_id"] == "user-1"
|
||||
assert call["skill_id"] == "skill-1"
|
||||
assert call["payload"].operation == "upsert_text"
|
||||
|
||||
|
||||
def test_patch_skill_file_operation_returns_error_details(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillFilesApi()
|
||||
method = unwrap(api.patch)
|
||||
service = MagicMock()
|
||||
service.apply_draft_file_operation.side_effect = SkillManagementServiceError(
|
||||
"missing_skill_name",
|
||||
"SKILL.md frontmatter name is required",
|
||||
details={"path": "SKILL.md", "field": "name", "line": 2},
|
||||
)
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="PATCH"),
|
||||
patch.object(
|
||||
type(console_ns),
|
||||
"payload",
|
||||
new_callable=PropertyMock,
|
||||
return_value={"operation": "delete", "path": "SKILL.md"},
|
||||
),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload, status = method(api, "tenant-1", current_user, "skill-1")
|
||||
|
||||
assert status == 400
|
||||
assert payload == {
|
||||
"code": "missing_skill_name",
|
||||
"message": "SKILL.md frontmatter name is required",
|
||||
"details": {"path": "SKILL.md", "field": "name", "line": 2},
|
||||
}
|
||||
|
||||
|
||||
def test_list_skill_tags_returns_filter_options(app: Flask) -> None:
|
||||
api = WorkspaceSkillTagsApi()
|
||||
method = unwrap(api.get)
|
||||
service = MagicMock()
|
||||
service.list_tags.return_value = {"data": [{"tag": "finance", "count": 2}]}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1")
|
||||
|
||||
assert payload == {"data": [{"tag": "finance", "count": 2}]}
|
||||
service.list_tags.assert_called_once_with(tenant_id="tenant-1")
|
||||
|
||||
|
||||
def test_get_skill_version_returns_version_detail(app: Flask) -> None:
|
||||
api = WorkspaceSkillVersionApi()
|
||||
method = unwrap(api.get)
|
||||
service = MagicMock()
|
||||
service.get_version.return_value = {
|
||||
"id": "version-1",
|
||||
"skill_id": "skill-1",
|
||||
"version_number": 1,
|
||||
"version_name": "Initial finance policy",
|
||||
"publish_note": "Initial finance policy",
|
||||
"hash_code": "hash-code",
|
||||
"archive_size": 123,
|
||||
"published_by": "user-1",
|
||||
"published_by_name": "Li Wei",
|
||||
"is_latest": True,
|
||||
"created_at": 1,
|
||||
"files": [
|
||||
{
|
||||
"id": None,
|
||||
"path": "SKILL.md",
|
||||
"kind": "file",
|
||||
"storage": "text",
|
||||
"mime_type": "text/markdown",
|
||||
"content": "# Version",
|
||||
"tool_file_id": None,
|
||||
"size": 9,
|
||||
"hash": "file-hash",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", "skill-1", "version-1")
|
||||
|
||||
assert payload["files"][0]["content"] == "# Version"
|
||||
service.get_version.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
skill_id="skill-1",
|
||||
version_id="version-1",
|
||||
)
|
||||
|
||||
|
||||
def test_patch_skill_version_renames_version(app: Flask) -> None:
|
||||
api = WorkspaceSkillVersionApi()
|
||||
method = unwrap(api.patch)
|
||||
service = MagicMock()
|
||||
service.update_version.return_value = {
|
||||
"id": "version-1",
|
||||
"skill_id": "skill-1",
|
||||
"version_number": 1,
|
||||
"version_name": "Approval threshold",
|
||||
"publish_note": "",
|
||||
"hash_code": "hash-code",
|
||||
"archive_size": 123,
|
||||
"published_by": "user-1",
|
||||
"published_by_name": "Li Wei",
|
||||
"is_latest": True,
|
||||
"created_at": 1,
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="PATCH"),
|
||||
patch.object(
|
||||
type(console_ns),
|
||||
"payload",
|
||||
new_callable=PropertyMock,
|
||||
return_value={"version_name": "Approval threshold"},
|
||||
),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", "skill-1", "version-1")
|
||||
|
||||
assert payload["version_name"] == "Approval threshold"
|
||||
service.update_version.assert_called_once()
|
||||
assert service.update_version.call_args.kwargs["payload"].version_name == "Approval threshold"
|
||||
|
||||
|
||||
def test_delete_skill_version_returns_new_latest(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillVersionApi()
|
||||
method = unwrap(api.delete)
|
||||
service = MagicMock()
|
||||
service.delete_version.return_value = {
|
||||
"id": "version-2",
|
||||
"deleted": True,
|
||||
"latest_published_version_id": "version-1",
|
||||
}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="DELETE"),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
):
|
||||
payload = method(api, "tenant-1", current_user, "skill-1", "version-2")
|
||||
|
||||
assert payload == {"id": "version-2", "deleted": True, "latest_published_version_id": "version-1"}
|
||||
service.delete_version.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
skill_id="skill-1",
|
||||
version_id="version-2",
|
||||
)
|
||||
|
||||
|
||||
def test_skill_assistant_runs_agent_app_stream(app: Flask, current_user: Account) -> None:
|
||||
api = WorkspaceSkillAssistMessageApi()
|
||||
method = unwrap(api.post)
|
||||
service = MagicMock()
|
||||
assistant_app = MagicMock()
|
||||
assistant_app.id = "assistant-app-1"
|
||||
service.get_or_create_assistant_app.return_value = (assistant_app, "<skill_draft>draft</skill_draft>")
|
||||
app_model = MagicMock()
|
||||
app_response = MagicMock()
|
||||
compact_response = MagicMock()
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="POST"),
|
||||
patch.object(
|
||||
type(console_ns),
|
||||
"payload",
|
||||
new_callable=PropertyMock,
|
||||
return_value={
|
||||
"attachments": [
|
||||
{
|
||||
"tool_file_id": "tool-file-1",
|
||||
"name": "requirements.md",
|
||||
"mime_type": "text/markdown",
|
||||
"size": 128,
|
||||
}
|
||||
],
|
||||
"message": "Create an approval checklist.",
|
||||
},
|
||||
),
|
||||
patch("controllers.console.workspace.skills.SkillManagementService", return_value=service),
|
||||
patch(
|
||||
"controllers.console.workspace.skills.db.session",
|
||||
return_value=MagicMock(get=MagicMock(return_value=app_model)),
|
||||
),
|
||||
patch("controllers.console.workspace.skills.AppGenerateService.generate", return_value=app_response),
|
||||
patch("controllers.console.workspace.skills.helper.compact_generate_response", return_value=compact_response),
|
||||
):
|
||||
response = method(api, "tenant-1", current_user, "skill-1")
|
||||
|
||||
assert response is compact_response
|
||||
service.get_or_create_assistant_app.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
skill_id="skill-1",
|
||||
user_id="user-1",
|
||||
attachments=[
|
||||
SkillAssistAttachmentPayload(
|
||||
tool_file_id="tool-file-1",
|
||||
name="requirements.md",
|
||||
mime_type="text/markdown",
|
||||
size=128,
|
||||
)
|
||||
],
|
||||
message="Create an approval checklist.",
|
||||
model_payload=None,
|
||||
)
|
||||
@@ -6,19 +6,13 @@ from unittest.mock import ANY, MagicMock, patch
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from jwt import InvalidTokenError
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
||||
from werkzeug.exceptions import Unauthorized
|
||||
|
||||
import services.errors.account
|
||||
from controllers.console import wraps as console_wraps
|
||||
from controllers.web.login import EmailCodeLoginApi, EmailCodeLoginSendEmailApi, LoginApi, LoginStatusApi, LogoutApi
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from models.model import DifySetup
|
||||
from services.entities.auth_entities import LoginFailureReason
|
||||
|
||||
pytestmark = pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
|
||||
|
||||
|
||||
def encode_code(code: str) -> str:
|
||||
return base64.b64encode(code.encode("utf-8")).decode()
|
||||
@@ -39,27 +33,17 @@ def app():
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_wraps(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
sqlite_engine: Engine,
|
||||
sqlite_session: Session,
|
||||
):
|
||||
def _patch_wraps():
|
||||
wraps_features = SimpleNamespace(enable_email_password_login=True)
|
||||
console_dify = SimpleNamespace(ENTERPRISE_ENABLED=True, DEPLOYMENT_EDITION=DeploymentEdition.CLOUD)
|
||||
web_dify = SimpleNamespace(ENTERPRISE_ENABLED=True)
|
||||
sqlite_session.add(DifySetup(version="test"))
|
||||
sqlite_session.commit()
|
||||
console_wraps._is_setup_completed.reset_success()
|
||||
session_registry = scoped_session(sessionmaker(bind=sqlite_engine, expire_on_commit=False))
|
||||
monkeypatch.setattr(console_wraps.db, "session", session_registry)
|
||||
with (
|
||||
patch("controllers.console.wraps.db") as mock_db,
|
||||
patch("controllers.console.wraps.dify_config", console_dify),
|
||||
patch("controllers.console.wraps.FeatureService.get_system_features", return_value=wraps_features),
|
||||
patch("controllers.web.login.dify_config", web_dify),
|
||||
):
|
||||
yield
|
||||
session_registry.remove()
|
||||
console_wraps._is_setup_completed.reset_success()
|
||||
|
||||
|
||||
class TestEmailCodeLoginSendEmailApi:
|
||||
|
||||
@@ -7,6 +7,7 @@ from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from dify_agent.layers.config import DifyConfigSkillConfig
|
||||
from dify_agent.layers.dify_core_tools import DifyCoreToolConfig, DifyCoreToolsLayerConfig
|
||||
from dify_agent.layers.dify_plugin import DifyPluginToolConfig, DifyPluginToolsLayerConfig
|
||||
from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig
|
||||
@@ -29,6 +30,14 @@ from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom
|
||||
from models.agent_config_entities import AgentSoulConfig
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs",
|
||||
lambda *, tenant_id, agent_id: [],
|
||||
)
|
||||
|
||||
|
||||
def _exec_ctx() -> DifyExecutionContextLayerConfig:
|
||||
return DifyExecutionContextLayerConfig(
|
||||
tenant_id="tenant-1",
|
||||
@@ -514,6 +523,33 @@ class TestAgentAppConfigLayer:
|
||||
"mentioned_file_names": [],
|
||||
}
|
||||
|
||||
def test_config_layer_includes_bound_workspace_skills(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs",
|
||||
lambda *, tenant_id, agent_id: [
|
||||
DifyConfigSkillConfig(
|
||||
name="workspace-skill",
|
||||
description="Bound workspace skill.",
|
||||
size=123,
|
||||
mime_type="application/zip",
|
||||
)
|
||||
],
|
||||
)
|
||||
soul = _soul_with_model()
|
||||
soul.prompt.system_prompt = "Use [§skill:workspace-skill:Workspace Skill§]."
|
||||
builder = AgentAppRuntimeRequestBuilder(
|
||||
credentials_provider=_FakeCredentialsProvider(),
|
||||
dify_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
result = builder.build(_ctx(soul))
|
||||
|
||||
config = next(layer for layer in result.request.composition.layers if layer.name == DIFY_CONFIG_LAYER_ID)
|
||||
assert [skill.name for skill in config.config.skills] == ["workspace-skill"]
|
||||
assert config.config.mentioned_skill_names == ["workspace-skill"]
|
||||
prompt_layer = next(layer for layer in result.request.composition.layers if layer.name == "agent_soul_prompt")
|
||||
assert prompt_layer.config.prefix == "Use workspace-skill."
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("system_prompt", "expected_prefix"),
|
||||
[
|
||||
|
||||
@@ -2,16 +2,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from core.workflow.nodes.agent.runtime_support import AgentRuntimeSupport
|
||||
from graphon.model_runtime.entities.common_entities import I18nObject
|
||||
from graphon.model_runtime.entities.model_entities import (
|
||||
AIModelEntity,
|
||||
FetchFrom,
|
||||
ModelFeature,
|
||||
ModelPropertyKey,
|
||||
ModelType,
|
||||
ParameterRule,
|
||||
ParameterType,
|
||||
)
|
||||
from graphon.model_runtime.entities.model_entities import ModelType
|
||||
|
||||
|
||||
def test_fetch_model_reuses_single_model_assembly():
|
||||
@@ -56,98 +47,3 @@ def test_fetch_model_reuses_single_model_assembly():
|
||||
model_type=ModelType.LLM,
|
||||
model="gpt-4o-mini",
|
||||
)
|
||||
|
||||
|
||||
def _make_model_schema_with_defaults() -> AIModelEntity:
|
||||
"""Return a minimal AIModelEntity whose parameter_rules carry defaults."""
|
||||
return AIModelEntity(
|
||||
model="qwen-max",
|
||||
label=I18nObject(en_US="Qwen Max"),
|
||||
model_type=ModelType.LLM,
|
||||
features=[ModelFeature.AGENT_THOUGHT, ModelFeature.MULTI_TOOL_CALL],
|
||||
fetch_from=FetchFrom.PREDEFINED_MODEL,
|
||||
model_properties={
|
||||
ModelPropertyKey.MODE: "chat",
|
||||
ModelPropertyKey.CONTEXT_SIZE: 32768,
|
||||
},
|
||||
parameter_rules=[
|
||||
ParameterRule(
|
||||
name="temperature",
|
||||
use_template="temperature",
|
||||
label=I18nObject(en_US="Temperature"),
|
||||
type=ParameterType.FLOAT,
|
||||
required=False,
|
||||
default=0.7,
|
||||
min=0.0,
|
||||
max=2.0,
|
||||
precision=2,
|
||||
),
|
||||
ParameterRule(
|
||||
name="max_tokens",
|
||||
use_template="max_tokens",
|
||||
label=I18nObject(en_US="Max Tokens"),
|
||||
type=ParameterType.INT,
|
||||
required=False,
|
||||
default=2048,
|
||||
min=1,
|
||||
max=32768,
|
||||
),
|
||||
ParameterRule(
|
||||
name="top_p",
|
||||
use_template="top_p",
|
||||
label=I18nObject(en_US="Top P"),
|
||||
type=ParameterType.FLOAT,
|
||||
required=False,
|
||||
default=1.0,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_extract_default_completion_params_collects_rule_defaults():
|
||||
"""_extract_default_completion_params should gather every rule.default."""
|
||||
schema = _make_model_schema_with_defaults()
|
||||
params = AgentRuntimeSupport._extract_default_completion_params(schema)
|
||||
assert params == {"temperature": 0.7, "max_tokens": 2048, "top_p": 1.0}
|
||||
|
||||
|
||||
def test_extract_default_completion_params_skips_rules_without_default():
|
||||
"""Rules whose default is None must not appear in the result."""
|
||||
schema = AIModelEntity(
|
||||
model="test-model",
|
||||
label=I18nObject(en_US="Test"),
|
||||
model_type=ModelType.LLM,
|
||||
fetch_from=FetchFrom.PREDEFINED_MODEL,
|
||||
model_properties={ModelPropertyKey.MODE: "chat"},
|
||||
parameter_rules=[
|
||||
ParameterRule(
|
||||
name="seed",
|
||||
label=I18nObject(en_US="Seed"),
|
||||
type=ParameterType.INT,
|
||||
required=False,
|
||||
default=None,
|
||||
),
|
||||
ParameterRule(
|
||||
name="temperature",
|
||||
label=I18nObject(en_US="Temperature"),
|
||||
type=ParameterType.FLOAT,
|
||||
required=False,
|
||||
default=0.5,
|
||||
),
|
||||
],
|
||||
)
|
||||
params = AgentRuntimeSupport._extract_default_completion_params(schema)
|
||||
assert params == {"temperature": 0.5}
|
||||
|
||||
|
||||
def test_extract_default_completion_params_empty_when_no_defaults():
|
||||
"""An empty parameter_rules list yields an empty dict."""
|
||||
schema = AIModelEntity(
|
||||
model="test-model",
|
||||
label=I18nObject(en_US="Test"),
|
||||
model_type=ModelType.LLM,
|
||||
fetch_from=FetchFrom.PREDEFINED_MODEL,
|
||||
model_properties={ModelPropertyKey.MODE: "chat"},
|
||||
parameter_rules=[],
|
||||
)
|
||||
assert AgentRuntimeSupport._extract_default_completion_params(schema) == {}
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import cast
|
||||
|
||||
import pytest
|
||||
from agenton.compositor import CompositorSessionSnapshot
|
||||
from dify_agent.layers.config import DifyConfigSkillConfig
|
||||
from dify_agent.layers.dify_core_tools import DifyCoreToolConfig, DifyCoreToolsLayerConfig
|
||||
from dify_agent.layers.dify_plugin import DifyPluginToolConfig, DifyPluginToolsLayerConfig
|
||||
from dify_agent.protocol import DIFY_AGENT_HISTORY_LAYER_ID, DIFY_AGENT_MODEL_LAYER_ID
|
||||
@@ -40,6 +41,14 @@ from models.agent_config_entities import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs",
|
||||
lambda *, tenant_id, agent_id: [],
|
||||
)
|
||||
|
||||
|
||||
class FakeCredentialsProvider:
|
||||
def fetch(self, provider_name: str, model_name: str) -> dict[str, object]:
|
||||
assert provider_name == "openai"
|
||||
@@ -1420,6 +1429,30 @@ def test_build_config_layer_config_includes_soul_context_and_mentions():
|
||||
assert warnings == []
|
||||
|
||||
|
||||
def test_build_config_layer_config_includes_runtime_agent_skills():
|
||||
from core.workflow.nodes.agent_v2.runtime_request_builder import build_config_layer_config
|
||||
|
||||
soul = AgentSoulConfig(
|
||||
prompt={"system_prompt": "Use [§skill:workspace-skill:Workspace Skill§]."},
|
||||
model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"),
|
||||
)
|
||||
config, warnings = build_config_layer_config(
|
||||
soul,
|
||||
runtime_config_skills=[
|
||||
DifyConfigSkillConfig(
|
||||
name="workspace-skill",
|
||||
description="Bound workspace skill.",
|
||||
size=123,
|
||||
mime_type="application/zip",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
assert [skill.name for skill in config.skills] == ["workspace-skill"]
|
||||
assert config.mentioned_skill_names == ["workspace-skill"]
|
||||
assert warnings == []
|
||||
|
||||
|
||||
def test_build_config_layer_config_returns_empty_config_for_empty_agent_soul():
|
||||
from core.workflow.nodes.agent_v2.runtime_request_builder import build_config_layer_config
|
||||
|
||||
@@ -1505,6 +1538,33 @@ def test_workflow_run_request_contains_config_layer():
|
||||
assert any(spec.name == DIFY_CONFIG_LAYER_ID and spec.type == "dify.config" for spec in specs)
|
||||
|
||||
|
||||
def test_workflow_run_request_includes_bound_workspace_skills(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(
|
||||
"core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs",
|
||||
lambda *, tenant_id, agent_id: [
|
||||
DifyConfigSkillConfig(
|
||||
name="workspace-skill",
|
||||
description="Bound workspace skill.",
|
||||
size=123,
|
||||
mime_type="application/zip",
|
||||
)
|
||||
],
|
||||
)
|
||||
context = _context()
|
||||
context.snapshot.config_snapshot = AgentSoulConfig(
|
||||
prompt={"system_prompt": "Use [§skill:workspace-skill:Workspace Skill§]."},
|
||||
model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"),
|
||||
)
|
||||
|
||||
result = WorkflowAgentRuntimeRequestBuilder(credentials_provider=FakeCredentialsProvider()).build(context)
|
||||
|
||||
config = next(layer for layer in result.request.composition.layers if layer.name == DIFY_CONFIG_LAYER_ID)
|
||||
assert [skill.name for skill in config.config.skills] == ["workspace-skill"]
|
||||
assert config.config.mentioned_skill_names == ["workspace-skill"]
|
||||
soul_prompt = next(layer for layer in result.request.composition.layers if layer.name == "agent_soul_prompt")
|
||||
assert soul_prompt.config.prefix == "Use workspace-skill."
|
||||
|
||||
|
||||
def test_workflow_runtime_expands_config_mentions_in_agent_soul_prompt():
|
||||
context = _context()
|
||||
context.snapshot.config_snapshot = _soul_with_config_assets()
|
||||
|
||||
@@ -1,961 +0,0 @@
|
||||
preset = "strict"
|
||||
project-includes = ["."]
|
||||
search-path = ["../.."]
|
||||
python-platform = "linux"
|
||||
python-version = "3.12.0"
|
||||
infer-with-first-use = true
|
||||
min-severity = "warn"
|
||||
|
||||
# Existing strict-mode debt. Remove a file when bringing it under strict checking.
|
||||
project-excludes = [
|
||||
"clients/agent_backend/test_cleanup_composition_compositor_integration.py",
|
||||
"clients/agent_backend/test_client.py",
|
||||
"clients/agent_backend/test_event_adapter.py",
|
||||
"clients/agent_backend/test_fake_client.py",
|
||||
"clients/agent_backend/test_request_builder.py",
|
||||
"clients/agent_backend/test_session_cleanup.py",
|
||||
"commands/test_archive_workflow_runs.py",
|
||||
"commands/test_clean_expired_messages.py",
|
||||
"commands/test_data_migration_commands.py",
|
||||
"commands/test_data_migration_wizard.py",
|
||||
"commands/test_fix_app_site_missing.py",
|
||||
"commands/test_generate_swagger_markdown_docs.py",
|
||||
"commands/test_generate_swagger_specs.py",
|
||||
"commands/test_legacy_model_type_migration.py",
|
||||
"commands/test_lint_response_contracts.py",
|
||||
"commands/test_reset_encrypt_key_pair.py",
|
||||
"commands/test_upgrade_db.py",
|
||||
"configs/test_dify_config.py",
|
||||
"configs/test_env_consistency.py",
|
||||
"configs/test_nacos_http_client.py",
|
||||
"conftest.py",
|
||||
"controllers/common/test_agent_app_parameters.py",
|
||||
"controllers/common/test_app_access.py",
|
||||
"controllers/common/test_errors.py",
|
||||
"controllers/common/test_fields.py",
|
||||
"controllers/common/test_file_response.py",
|
||||
"controllers/common/test_helpers.py",
|
||||
"controllers/common/test_schema.py",
|
||||
"controllers/common/test_session.py",
|
||||
"controllers/console/agent/test_agent_controllers.py",
|
||||
"controllers/console/app/test_agent_app_sandbox.py",
|
||||
"controllers/console/app/test_agent_config_inspector.py",
|
||||
"controllers/console/app/test_agent_drive_inspector.py",
|
||||
"controllers/console/app/test_agent_manage_guard.py",
|
||||
"controllers/console/app/test_agent_skills.py",
|
||||
"controllers/console/app/test_annotation_api.py",
|
||||
"controllers/console/app/test_annotation_security.py",
|
||||
"controllers/console/app/test_app_apis.py",
|
||||
"controllers/console/app/test_app_import_api.py",
|
||||
"controllers/console/app/test_app_response_models.py",
|
||||
"controllers/console/app/test_audio.py",
|
||||
"controllers/console/app/test_conversation_api.py",
|
||||
"controllers/console/app/test_conversation_variables_api.py",
|
||||
"controllers/console/app/test_create_app_payload.py",
|
||||
"controllers/console/app/test_description_validation.py",
|
||||
"controllers/console/app/test_generator_api.py",
|
||||
"controllers/console/app/test_generator_api_missing.py",
|
||||
"controllers/console/app/test_mcp_server_response.py",
|
||||
"controllers/console/app/test_message_api.py",
|
||||
"controllers/console/app/test_model_config_api.py",
|
||||
"controllers/console/app/test_ops_trace_api.py",
|
||||
"controllers/console/app/test_statistic_api.py",
|
||||
"controllers/console/app/test_workflow.py",
|
||||
"controllers/console/app/test_workflow_app_log_api.py",
|
||||
"controllers/console/app/test_workflow_comment_api.py",
|
||||
"controllers/console/app/test_workflow_convert_api.py",
|
||||
"controllers/console/app/test_workflow_node_output_inspector.py",
|
||||
"controllers/console/app/test_workflow_pause_details_api.py",
|
||||
"controllers/console/app/test_workflow_run_api.py",
|
||||
"controllers/console/app/test_workflow_trigger_api.py",
|
||||
"controllers/console/app/test_wraps.py",
|
||||
"controllers/console/app/workflow_draft_variables_test.py",
|
||||
"controllers/console/auth/test_account_activation.py",
|
||||
"controllers/console/auth/test_authentication_security.py",
|
||||
"controllers/console/auth/test_data_source_bearer_auth.py",
|
||||
"controllers/console/auth/test_email_register.py",
|
||||
"controllers/console/auth/test_email_register_language.py",
|
||||
"controllers/console/auth/test_email_verification.py",
|
||||
"controllers/console/auth/test_forgot_password.py",
|
||||
"controllers/console/auth/test_login_logout.py",
|
||||
"controllers/console/auth/test_oauth.py",
|
||||
"controllers/console/auth/test_oauth_timezone.py",
|
||||
"controllers/console/auth/test_password_reset.py",
|
||||
"controllers/console/auth/test_token_refresh.py",
|
||||
"controllers/console/billing/test_billing.py",
|
||||
"controllers/console/datasets/rag_pipeline/test_datasource_auth.py",
|
||||
"controllers/console/datasets/rag_pipeline/test_datasource_content_preview.py",
|
||||
"controllers/console/datasets/rag_pipeline/test_rag_pipeline.py",
|
||||
"controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py",
|
||||
"controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py",
|
||||
"controllers/console/datasets/test_data_source.py",
|
||||
"controllers/console/datasets/test_datasets.py",
|
||||
"controllers/console/datasets/test_datasets_document.py",
|
||||
"controllers/console/datasets/test_datasets_document_download.py",
|
||||
"controllers/console/datasets/test_datasets_segments.py",
|
||||
"controllers/console/datasets/test_external.py",
|
||||
"controllers/console/datasets/test_hit_testing.py",
|
||||
"controllers/console/datasets/test_hit_testing_base.py",
|
||||
"controllers/console/datasets/test_metadata.py",
|
||||
"controllers/console/datasets/test_website.py",
|
||||
"controllers/console/datasets/test_wraps.py",
|
||||
"controllers/console/explore/test_audio.py",
|
||||
"controllers/console/explore/test_banner.py",
|
||||
"controllers/console/explore/test_completion.py",
|
||||
"controllers/console/explore/test_installed_app.py",
|
||||
"controllers/console/explore/test_message.py",
|
||||
"controllers/console/explore/test_parameter.py",
|
||||
"controllers/console/explore/test_recommended_app.py",
|
||||
"controllers/console/explore/test_saved_message.py",
|
||||
"controllers/console/explore/test_trial.py",
|
||||
"controllers/console/explore/test_workflow.py",
|
||||
"controllers/console/explore/test_wraps.py",
|
||||
"controllers/console/snippets/test_snippet_workflow.py",
|
||||
"controllers/console/snippets/test_snippet_workflow_draft_variable.py",
|
||||
"controllers/console/tag/test_tags.py",
|
||||
"controllers/console/test_document_detail_api_data_source_info.py",
|
||||
"controllers/console/test_extension.py",
|
||||
"controllers/console/test_fastopenapi_ping.py",
|
||||
"controllers/console/test_fastopenapi_setup.py",
|
||||
"controllers/console/test_fastopenapi_version.py",
|
||||
"controllers/console/test_feature.py",
|
||||
"controllers/console/test_files.py",
|
||||
"controllers/console/test_files_security.py",
|
||||
"controllers/console/test_human_input_form.py",
|
||||
"controllers/console/test_knowledge_fs_proxy.py",
|
||||
"controllers/console/test_remote_files.py",
|
||||
"controllers/console/test_spec.py",
|
||||
"controllers/console/test_version.py",
|
||||
"controllers/console/test_workflow_run_archive.py",
|
||||
"controllers/console/test_workspace_account.py",
|
||||
"controllers/console/test_workspace_members.py",
|
||||
"controllers/console/test_wraps.py",
|
||||
"controllers/console/workspace/test_accounts.py",
|
||||
"controllers/console/workspace/test_agent_providers.py",
|
||||
"controllers/console/workspace/test_endpoint.py",
|
||||
"controllers/console/workspace/test_load_balancing_config.py",
|
||||
"controllers/console/workspace/test_members.py",
|
||||
"controllers/console/workspace/test_model_providers.py",
|
||||
"controllers/console/workspace/test_models.py",
|
||||
"controllers/console/workspace/test_plugin.py",
|
||||
"controllers/console/workspace/test_rbac.py",
|
||||
"controllers/console/workspace/test_snippets.py",
|
||||
"controllers/console/workspace/test_tool_providers.py",
|
||||
"controllers/console/workspace/test_workspace.py",
|
||||
"controllers/files/test_image_preview.py",
|
||||
"controllers/files/test_tool_files.py",
|
||||
"controllers/files/test_upload.py",
|
||||
"controllers/inner_api/app/test_dsl.py",
|
||||
"controllers/inner_api/plugin/test_agent_config.py",
|
||||
"controllers/inner_api/plugin/test_agent_drive.py",
|
||||
"controllers/inner_api/plugin/test_plugin.py",
|
||||
"controllers/inner_api/plugin/test_plugin_wraps.py",
|
||||
"controllers/inner_api/test_auth_wraps.py",
|
||||
"controllers/inner_api/test_knowledge_retrieval.py",
|
||||
"controllers/inner_api/test_mail.py",
|
||||
"controllers/inner_api/test_runtime_credentials.py",
|
||||
"controllers/inner_api/workspace/test_workspace.py",
|
||||
"controllers/mcp/test_mcp.py",
|
||||
"controllers/openapi/auth/test_composition.py",
|
||||
"controllers/openapi/auth/test_conditions.py",
|
||||
"controllers/openapi/auth/test_data.py",
|
||||
"controllers/openapi/auth/test_flow.py",
|
||||
"controllers/openapi/auth/test_pipeline.py",
|
||||
"controllers/openapi/auth/test_prepare.py",
|
||||
"controllers/openapi/auth/test_verify.py",
|
||||
"controllers/openapi/conftest.py",
|
||||
"controllers/openapi/test_account.py",
|
||||
"controllers/openapi/test_app_describe_builder.py",
|
||||
"controllers/openapi/test_app_list_query.py",
|
||||
"controllers/openapi/test_app_payloads.py",
|
||||
"controllers/openapi/test_app_run_dispatch.py",
|
||||
"controllers/openapi/test_app_run_rate_limit.py",
|
||||
"controllers/openapi/test_app_run_streaming.py",
|
||||
"controllers/openapi/test_apps_permitted_external_query.py",
|
||||
"controllers/openapi/test_audit_app_run.py",
|
||||
"controllers/openapi/test_contract.py",
|
||||
"controllers/openapi/test_cors.py",
|
||||
"controllers/openapi/test_device_approve_deny.py",
|
||||
"controllers/openapi/test_device_code.py",
|
||||
"controllers/openapi/test_device_lookup.py",
|
||||
"controllers/openapi/test_device_sso.py",
|
||||
"controllers/openapi/test_device_token.py",
|
||||
"controllers/openapi/test_error_contract.py",
|
||||
"controllers/openapi/test_health.py",
|
||||
"controllers/openapi/test_human_input_form.py",
|
||||
"controllers/openapi/test_input_schema.py",
|
||||
"controllers/openapi/test_meta_version.py",
|
||||
"controllers/openapi/test_models.py",
|
||||
"controllers/openapi/test_oauth_sso_claims.py",
|
||||
"controllers/openapi/test_oauth_sso_csrf.py",
|
||||
"controllers/openapi/test_oauth_sso_host_header.py",
|
||||
"controllers/openapi/test_pagination_envelope.py",
|
||||
"controllers/openapi/test_supported_app_type.py",
|
||||
"controllers/openapi/test_version_gate.py",
|
||||
"controllers/openapi/test_workflow_events_openapi.py",
|
||||
"controllers/openapi/test_workspaces.py",
|
||||
"controllers/openapi/test_workspaces_members.py",
|
||||
"controllers/service_api/app/test_annotation.py",
|
||||
"controllers/service_api/app/test_app.py",
|
||||
"controllers/service_api/app/test_audio.py",
|
||||
"controllers/service_api/app/test_chat_request_payload.py",
|
||||
"controllers/service_api/app/test_completion.py",
|
||||
"controllers/service_api/app/test_conversation.py",
|
||||
"controllers/service_api/app/test_file.py",
|
||||
"controllers/service_api/app/test_file_preview.py",
|
||||
"controllers/service_api/app/test_hitl_service_api.py",
|
||||
"controllers/service_api/app/test_human_input_form.py",
|
||||
"controllers/service_api/app/test_message.py",
|
||||
"controllers/service_api/app/test_workflow.py",
|
||||
"controllers/service_api/app/test_workflow_events.py",
|
||||
"controllers/service_api/conftest.py",
|
||||
"controllers/service_api/dataset/rag_pipeline/test_rag_pipeline_workflow.py",
|
||||
"controllers/service_api/dataset/test_dataset_segment.py",
|
||||
"controllers/service_api/dataset/test_document.py",
|
||||
"controllers/service_api/dataset/test_hit_testing.py",
|
||||
"controllers/service_api/dataset/test_metadata.py",
|
||||
"controllers/service_api/dataset/test_rag_pipeline_file_upload_serialization.py",
|
||||
"controllers/service_api/dataset/test_rag_pipeline_route_registration.py",
|
||||
"controllers/service_api/test_index.py",
|
||||
"controllers/service_api/test_trace_session_id_parsing.py",
|
||||
"controllers/service_api/test_wraps.py",
|
||||
"controllers/test_compare_versions.py",
|
||||
"controllers/test_conversation_rename_payload.py",
|
||||
"controllers/test_swagger.py",
|
||||
"controllers/trigger/test_trigger.py",
|
||||
"controllers/trigger/test_webhook.py",
|
||||
"controllers/web/conftest.py",
|
||||
"controllers/web/test_app.py",
|
||||
"controllers/web/test_completion.py",
|
||||
"controllers/web/test_human_input_file_upload.py",
|
||||
"controllers/web/test_human_input_form.py",
|
||||
"controllers/web/test_message_endpoints.py",
|
||||
"controllers/web/test_message_list.py",
|
||||
"controllers/web/test_pydantic_models.py",
|
||||
"controllers/web/test_web_forgot_password.py",
|
||||
"controllers/web/test_web_login.py",
|
||||
"controllers/web/test_web_passport.py",
|
||||
"controllers/web/test_workflow.py",
|
||||
"controllers/web/test_wraps.py",
|
||||
"core/agent/conftest.py",
|
||||
"core/agent/output_parser/test_cot_output_parser.py",
|
||||
"core/agent/strategy/test_base.py",
|
||||
"core/agent/strategy/test_plugin.py",
|
||||
"core/agent/test_base_agent_runner.py",
|
||||
"core/agent/test_cot_agent_runner.py",
|
||||
"core/agent/test_cot_chat_agent_runner.py",
|
||||
"core/agent/test_cot_completion_agent_runner.py",
|
||||
"core/agent/test_fc_agent_runner.py",
|
||||
"core/agent/test_plugin_entities.py",
|
||||
"core/app/app_config/common/test_parameters_mapping.py",
|
||||
"core/app/app_config/common/test_sensitive_word_avoidance_manager.py",
|
||||
"core/app/app_config/easy_ui_based_app/test_agent_manager.py",
|
||||
"core/app/app_config/easy_ui_based_app/test_dataset_manager.py",
|
||||
"core/app/app_config/easy_ui_based_app/test_model_config_converter.py",
|
||||
"core/app/app_config/easy_ui_based_app/test_model_config_manager.py",
|
||||
"core/app/app_config/easy_ui_based_app/test_prompt_template_manager.py",
|
||||
"core/app/app_config/easy_ui_based_app/test_variables_manager.py",
|
||||
"core/app/app_config/features/file_upload/test_manager.py",
|
||||
"core/app/app_config/features/test_additional_feature_managers.py",
|
||||
"core/app/app_config/test_base_app_config_manager.py",
|
||||
"core/app/app_config/test_entities.py",
|
||||
"core/app/app_config/workflow_ui_based_app/test_workflow_ui_based_app_manager.py",
|
||||
"core/app/apps/advanced_chat/test_app_config_manager.py",
|
||||
"core/app/apps/advanced_chat/test_app_generator.py",
|
||||
"core/app/apps/advanced_chat/test_app_runner_conversation_variables.py",
|
||||
"core/app/apps/advanced_chat/test_app_runner_input_moderation.py",
|
||||
"core/app/apps/advanced_chat/test_generate_response_converter.py",
|
||||
"core/app/apps/advanced_chat/test_generate_task_pipeline.py",
|
||||
"core/app/apps/advanced_chat/test_generate_task_pipeline_core.py",
|
||||
"core/app/apps/agent_app/test_app_config_manager.py",
|
||||
"core/app/apps/agent_app/test_app_generator.py",
|
||||
"core/app/apps/agent_app/test_app_runner.py",
|
||||
"core/app/apps/agent_app/test_input_guards.py",
|
||||
"core/app/apps/agent_app/test_resolve_agent.py",
|
||||
"core/app/apps/agent_app/test_runtime_request_builder.py",
|
||||
"core/app/apps/agent_app/test_session_store.py",
|
||||
"core/app/apps/agent_chat/test_agent_chat_app_config_manager.py",
|
||||
"core/app/apps/agent_chat/test_agent_chat_app_generator.py",
|
||||
"core/app/apps/agent_chat/test_agent_chat_app_runner.py",
|
||||
"core/app/apps/agent_chat/test_agent_chat_generate_response_converter.py",
|
||||
"core/app/apps/chat/test_app_config_manager.py",
|
||||
"core/app/apps/chat/test_app_generator_and_runner.py",
|
||||
"core/app/apps/chat/test_base_app_runner_multimodal.py",
|
||||
"core/app/apps/chat/test_generate_response_converter.py",
|
||||
"core/app/apps/common/test_graph_runtime_state_support.py",
|
||||
"core/app/apps/common/test_workflow_response_converter.py",
|
||||
"core/app/apps/common/test_workflow_response_converter_human_input.py",
|
||||
"core/app/apps/common/test_workflow_response_converter_resumption.py",
|
||||
"core/app/apps/common/test_workflow_response_converter_truncation.py",
|
||||
"core/app/apps/completion/test_app_runner.py",
|
||||
"core/app/apps/completion/test_completion_app_config_manager.py",
|
||||
"core/app/apps/completion/test_completion_completion_app_generator.py",
|
||||
"core/app/apps/completion/test_completion_generate_response_converter.py",
|
||||
"core/app/apps/pipeline/test_pipeline_config_manager.py",
|
||||
"core/app/apps/pipeline/test_pipeline_generate_response_converter.py",
|
||||
"core/app/apps/pipeline/test_pipeline_generator.py",
|
||||
"core/app/apps/pipeline/test_pipeline_queue_manager.py",
|
||||
"core/app/apps/pipeline/test_pipeline_runner.py",
|
||||
"core/app/apps/test_advanced_chat_app_generator.py",
|
||||
"core/app/apps/test_base_app_generate_response_converter.py",
|
||||
"core/app/apps/test_base_app_generator.py",
|
||||
"core/app/apps/test_base_app_queue_manager.py",
|
||||
"core/app/apps/test_base_app_runner.py",
|
||||
"core/app/apps/test_exc.py",
|
||||
"core/app/apps/test_message_based_app_generator.py",
|
||||
"core/app/apps/test_message_based_app_queue_manager.py",
|
||||
"core/app/apps/test_message_generator.py",
|
||||
"core/app/apps/test_pause_resume.py",
|
||||
"core/app/apps/test_streaming_utils.py",
|
||||
"core/app/apps/test_trace_session_id_generate_extras.py",
|
||||
"core/app/apps/test_workflow_app_generator.py",
|
||||
"core/app/apps/test_workflow_app_runner_core.py",
|
||||
"core/app/apps/test_workflow_app_runner_notifications.py",
|
||||
"core/app/apps/test_workflow_app_runner_single_node.py",
|
||||
"core/app/apps/test_workflow_pause_events.py",
|
||||
"core/app/apps/workflow/test_active_workflow_tasks.py",
|
||||
"core/app/apps/workflow/test_app_config_manager.py",
|
||||
"core/app/apps/workflow/test_app_generator_extra.py",
|
||||
"core/app/apps/workflow/test_app_queue_manager.py",
|
||||
"core/app/apps/workflow/test_command_channels.py",
|
||||
"core/app/apps/workflow/test_errors.py",
|
||||
"core/app/apps/workflow/test_generate_response_converter.py",
|
||||
"core/app/apps/workflow/test_generate_task_pipeline.py",
|
||||
"core/app/apps/workflow/test_generate_task_pipeline_core.py",
|
||||
"core/app/entities/test_app_invoke_entities.py",
|
||||
"core/app/entities/test_queue_entities.py",
|
||||
"core/app/entities/test_rag_pipeline_invoke_entities.py",
|
||||
"core/app/entities/test_task_entities.py",
|
||||
"core/app/features/rate_limiting/conftest.py",
|
||||
"core/app/features/rate_limiting/test_rate_limit.py",
|
||||
"core/app/features/test_annotation_reply.py",
|
||||
"core/app/features/test_hosting_moderation.py",
|
||||
"core/app/layers/test_conversation_variable_persist_layer.py",
|
||||
"core/app/layers/test_pause_state_persist_layer.py",
|
||||
"core/app/layers/test_suspend_layer.py",
|
||||
"core/app/layers/test_timeslice_layer.py",
|
||||
"core/app/layers/test_trigger_post_layer.py",
|
||||
"core/app/task_pipeline/test_based_generate_task_pipeline.py",
|
||||
"core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py",
|
||||
"core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py",
|
||||
"core/app/task_pipeline/test_exc.py",
|
||||
"core/app/task_pipeline/test_message_cycle_manager_optimization.py",
|
||||
"core/app/test_easy_ui_model_config_manager.py",
|
||||
"core/app/test_invoke_from.py",
|
||||
"core/app/test_llm_quota.py",
|
||||
"core/app/workflow/layers/test_persistence.py",
|
||||
"core/app/workflow/layers/test_persistence_inspector_publish.py",
|
||||
"core/app/workflow/test_file_runtime.py",
|
||||
"core/app/workflow/test_node_factory.py",
|
||||
"core/app/workflow/test_observability_layer_extra.py",
|
||||
"core/app/workflow/test_persistence_layer.py",
|
||||
"core/base/test_app_generator_tts_publisher.py",
|
||||
"core/callback_handler/test_agent_tool_callback_handler.py",
|
||||
"core/callback_handler/test_index_tool_callback_handler.py",
|
||||
"core/callback_handler/test_workflow_tool_callback_handler.py",
|
||||
"core/datasource/__base/test_datasource_plugin.py",
|
||||
"core/datasource/__base/test_datasource_provider.py",
|
||||
"core/datasource/__base/test_datasource_runtime.py",
|
||||
"core/datasource/entities/test_api_entities.py",
|
||||
"core/datasource/entities/test_common_entities.py",
|
||||
"core/datasource/entities/test_datasource_entities.py",
|
||||
"core/datasource/local_file/test_local_file_plugin.py",
|
||||
"core/datasource/local_file/test_local_file_provider.py",
|
||||
"core/datasource/online_document/test_online_document_plugin.py",
|
||||
"core/datasource/online_document/test_online_document_provider.py",
|
||||
"core/datasource/online_drive/test_online_drive_plugin.py",
|
||||
"core/datasource/online_drive/test_online_drive_provider.py",
|
||||
"core/datasource/test_datasource_file_manager.py",
|
||||
"core/datasource/test_datasource_manager.py",
|
||||
"core/datasource/test_errors.py",
|
||||
"core/datasource/test_file_upload.py",
|
||||
"core/datasource/test_notion_provider.py",
|
||||
"core/datasource/test_website_crawl.py",
|
||||
"core/datasource/utils/test_message_transformer.py",
|
||||
"core/datasource/website_crawl/test_website_crawl_plugin.py",
|
||||
"core/datasource/website_crawl/test_website_crawl_provider.py",
|
||||
"core/entities/test_entities_mcp_provider.py",
|
||||
"core/entities/test_entities_provider_configuration.py",
|
||||
"core/extension/test_api_based_extension_requestor.py",
|
||||
"core/extension/test_extensible.py",
|
||||
"core/extension/test_extension.py",
|
||||
"core/external_data_tool/api/test_api.py",
|
||||
"core/external_data_tool/test_base.py",
|
||||
"core/external_data_tool/test_external_data_fetch.py",
|
||||
"core/external_data_tool/test_factory.py",
|
||||
"core/file/test_models.py",
|
||||
"core/file/test_remote_fetcher.py",
|
||||
"core/helper/code_executor/javascript/test_javascript_transformer.py",
|
||||
"core/helper/code_executor/jinja2/test_jinja2_sandbox.py",
|
||||
"core/helper/code_executor/python3/test_python3_transformer.py",
|
||||
"core/helper/code_executor/test_code_executor.py",
|
||||
"core/helper/code_executor/test_code_node_provider.py",
|
||||
"core/helper/code_executor/test_template_transformer.py",
|
||||
"core/helper/test_creators.py",
|
||||
"core/helper/test_credential_utils.py",
|
||||
"core/helper/test_csv_sanitizer.py",
|
||||
"core/helper/test_encrypter.py",
|
||||
"core/helper/test_ssrf_proxy.py",
|
||||
"core/helper/test_trace_id_helper.py",
|
||||
"core/llm_generator/output_parser/test_rule_config_generator.py",
|
||||
"core/llm_generator/output_parser/test_structured_output.py",
|
||||
"core/llm_generator/test_llm_generator.py",
|
||||
"core/llm_generator/test_llm_generator_missing.py",
|
||||
"core/logging/test_context.py",
|
||||
"core/logging/test_filters.py",
|
||||
"core/logging/test_structured_formatter.py",
|
||||
"core/logging/test_trace_helpers.py",
|
||||
"core/mcp/auth/test_auth_flow.py",
|
||||
"core/mcp/client/test_session.py",
|
||||
"core/mcp/client/test_sse.py",
|
||||
"core/mcp/client/test_streamable_http.py",
|
||||
"core/mcp/server/test_streamable_http.py",
|
||||
"core/mcp/session/test_base_session.py",
|
||||
"core/mcp/session/test_client_session.py",
|
||||
"core/mcp/test_auth_client_inheritance.py",
|
||||
"core/mcp/test_entities.py",
|
||||
"core/mcp/test_error.py",
|
||||
"core/mcp/test_mcp_client.py",
|
||||
"core/mcp/test_types.py",
|
||||
"core/mcp/test_utils.py",
|
||||
"core/memory/test_token_buffer_memory.py",
|
||||
"core/model_runtime/test_model_provider_factory.py",
|
||||
"core/moderation/api/test_api.py",
|
||||
"core/moderation/test_content_moderation.py",
|
||||
"core/moderation/test_input_moderation.py",
|
||||
"core/moderation/test_output_moderation.py",
|
||||
"core/moderation/test_sensitive_word_filter.py",
|
||||
"core/ops/test_base_trace_instance.py",
|
||||
"core/ops/test_config_entity.py",
|
||||
"core/ops/test_lookup_helpers.py",
|
||||
"core/ops/test_ops_trace_manager.py",
|
||||
"core/ops/test_trace_queue_manager.py",
|
||||
"core/ops/test_trace_session_metadata.py",
|
||||
"core/ops/test_utils.py",
|
||||
"core/plugin/impl/test_agent_client.py",
|
||||
"core/plugin/impl/test_asset_manager.py",
|
||||
"core/plugin/impl/test_base_client_impl.py",
|
||||
"core/plugin/impl/test_datasource_manager.py",
|
||||
"core/plugin/impl/test_debugging_client.py",
|
||||
"core/plugin/impl/test_endpoint_client_impl.py",
|
||||
"core/plugin/impl/test_exc_impl.py",
|
||||
"core/plugin/impl/test_model_client.py",
|
||||
"core/plugin/impl/test_model_runtime_factory.py",
|
||||
"core/plugin/impl/test_oauth_handler.py",
|
||||
"core/plugin/impl/test_tool_manager.py",
|
||||
"core/plugin/impl/test_trigger_client.py",
|
||||
"core/plugin/test_backwards_invocation_app.py",
|
||||
"core/plugin/test_backwards_invocation_model.py",
|
||||
"core/plugin/test_endpoint_client.py",
|
||||
"core/plugin/test_model_runtime_adapter.py",
|
||||
"core/plugin/test_plugin_entities.py",
|
||||
"core/plugin/test_plugin_manager.py",
|
||||
"core/plugin/test_plugin_runtime.py",
|
||||
"core/plugin/utils/test_chunk_merger.py",
|
||||
"core/plugin/utils/test_http_parser.py",
|
||||
"core/prompt/test_advanced_prompt_transform.py",
|
||||
"core/prompt/test_agent_history_prompt_transform.py",
|
||||
"core/prompt/test_extract_thread_messages.py",
|
||||
"core/prompt/test_prompt_message.py",
|
||||
"core/prompt/test_prompt_transform.py",
|
||||
"core/prompt/test_simple_prompt_transform.py",
|
||||
"core/rag/cleaner/test_clean_processor.py",
|
||||
"core/rag/data_post_processor/test_data_post_processor.py",
|
||||
"core/rag/datasource/keyword/jieba/test_jieba.py",
|
||||
"core/rag/datasource/keyword/jieba/test_jieba_keyword_table_handler.py",
|
||||
"core/rag/datasource/keyword/jieba/test_stopwords.py",
|
||||
"core/rag/datasource/keyword/test_keyword_base.py",
|
||||
"core/rag/datasource/keyword/test_keyword_factory.py",
|
||||
"core/rag/datasource/test_datasource_retrieval.py",
|
||||
"core/rag/datasource/vdb/test_field.py",
|
||||
"core/rag/datasource/vdb/test_vector_base.py",
|
||||
"core/rag/datasource/vdb/test_vector_factory.py",
|
||||
"core/rag/docstore/test_dataset_docstore.py",
|
||||
"core/rag/embedding/test_cached_embedding.py",
|
||||
"core/rag/embedding/test_embedding_base.py",
|
||||
"core/rag/embedding/test_embedding_service.py",
|
||||
"core/rag/extractor/blob/test_blob.py",
|
||||
"core/rag/extractor/firecrawl/test_firecrawl.py",
|
||||
"core/rag/extractor/test_csv_extractor.py",
|
||||
"core/rag/extractor/test_excel_extractor.py",
|
||||
"core/rag/extractor/test_extract_processor.py",
|
||||
"core/rag/extractor/test_extractor_base.py",
|
||||
"core/rag/extractor/test_helpers.py",
|
||||
"core/rag/extractor/test_html_extractor.py",
|
||||
"core/rag/extractor/test_jina_reader_extractor.py",
|
||||
"core/rag/extractor/test_markdown_extractor.py",
|
||||
"core/rag/extractor/test_notion_extractor.py",
|
||||
"core/rag/extractor/test_pdf_extractor.py",
|
||||
"core/rag/extractor/test_text_extractor.py",
|
||||
"core/rag/extractor/test_word_extractor.py",
|
||||
"core/rag/extractor/unstructured/test_unstructured_extractors.py",
|
||||
"core/rag/extractor/watercrawl/test_watercrawl.py",
|
||||
"core/rag/indexing/processor/test_paragraph_index_processor.py",
|
||||
"core/rag/indexing/processor/test_qa_index_processor.py",
|
||||
"core/rag/indexing/test_index_processor.py",
|
||||
"core/rag/indexing/test_index_processor_base.py",
|
||||
"core/rag/indexing/test_indexing_runner.py",
|
||||
"core/rag/pipeline/test_queue.py",
|
||||
"core/rag/rerank/test_reranker.py",
|
||||
"core/rag/retrieval/test_dataset_retrieval.py",
|
||||
"core/rag/retrieval/test_dataset_retrieval_methods.py",
|
||||
"core/rag/retrieval/test_multi_dataset_function_call_router.py",
|
||||
"core/rag/retrieval/test_multi_dataset_react_route.py",
|
||||
"core/rag/splitter/test_text_splitter.py",
|
||||
"core/repositories/test_celery_workflow_execution_repository.py",
|
||||
"core/repositories/test_celery_workflow_node_execution_repository.py",
|
||||
"core/repositories/test_factory.py",
|
||||
"core/repositories/test_human_input_form_repository_impl.py",
|
||||
"core/repositories/test_human_input_repository.py",
|
||||
"core/repositories/test_sqlalchemy_workflow_execution_repository.py",
|
||||
"core/repositories/test_sqlalchemy_workflow_node_execution_repository.py",
|
||||
"core/repositories/test_workflow_node_execution_conflict_handling.py",
|
||||
"core/repositories/test_workflow_node_execution_truncation.py",
|
||||
"core/schemas/test_registry.py",
|
||||
"core/schemas/test_resolver.py",
|
||||
"core/schemas/test_schema_manager.py",
|
||||
"core/telemetry/test_facade.py",
|
||||
"core/telemetry/test_gateway_integration.py",
|
||||
"core/test_file.py",
|
||||
"core/test_model_manager.py",
|
||||
"core/test_provider_configuration.py",
|
||||
"core/test_provider_manager.py",
|
||||
"core/test_trigger_debug_event_selectors.py",
|
||||
"core/tools/entities/test_api_entities.py",
|
||||
"core/tools/test_base_tool.py",
|
||||
"core/tools/test_builtin_tool_base.py",
|
||||
"core/tools/test_builtin_tool_provider.py",
|
||||
"core/tools/test_builtin_tools_extra.py",
|
||||
"core/tools/test_custom_tool.py",
|
||||
"core/tools/test_custom_tool_provider.py",
|
||||
"core/tools/test_dataset_retriever_tool.py",
|
||||
"core/tools/test_mcp_tool.py",
|
||||
"core/tools/test_mcp_tool_provider.py",
|
||||
"core/tools/test_plugin_tool.py",
|
||||
"core/tools/test_plugin_tool_provider.py",
|
||||
"core/tools/test_tool_engine.py",
|
||||
"core/tools/test_tool_entities.py",
|
||||
"core/tools/test_tool_file_manager.py",
|
||||
"core/tools/test_tool_label_manager.py",
|
||||
"core/tools/test_tool_manager.py",
|
||||
"core/tools/test_tool_parameter_type.py",
|
||||
"core/tools/test_tool_provider_controller.py",
|
||||
"core/tools/utils/test_configuration.py",
|
||||
"core/tools/utils/test_encryption.py",
|
||||
"core/tools/utils/test_message_transformer.py",
|
||||
"core/tools/utils/test_misc_utils_extra.py",
|
||||
"core/tools/utils/test_model_invocation_utils.py",
|
||||
"core/tools/utils/test_parser.py",
|
||||
"core/tools/utils/test_system_oauth_encryption.py",
|
||||
"core/tools/utils/test_tool_engine_serialization.py",
|
||||
"core/tools/utils/test_web_reader_tool.py",
|
||||
"core/tools/utils/test_workflow_configuration_sync.py",
|
||||
"core/tools/workflow_as_tool/test_provider.py",
|
||||
"core/tools/workflow_as_tool/test_tool.py",
|
||||
"core/trigger/conftest.py",
|
||||
"core/trigger/debug/test_debug_event_bus.py",
|
||||
"core/trigger/debug/test_debug_event_selectors.py",
|
||||
"core/trigger/test_provider.py",
|
||||
"core/trigger/test_trigger_manager.py",
|
||||
"core/trigger/utils/test_utils_encryption.py",
|
||||
"core/trigger/utils/test_utils_endpoint.py",
|
||||
"core/trigger/utils/test_utils_locks.py",
|
||||
"core/variables/test_segment.py",
|
||||
"core/variables/test_segment_type.py",
|
||||
"core/variables/test_segment_type_validation.py",
|
||||
"core/variables/test_variables.py",
|
||||
"core/workflow/context/test_execution_context.py",
|
||||
"core/workflow/context/test_flask_app_context.py",
|
||||
"core/workflow/entities/test_private_workflow_pause.py",
|
||||
"core/workflow/generator/test_prompts.py",
|
||||
"core/workflow/generator/test_runner.py",
|
||||
"core/workflow/generator/test_runner_missing.py",
|
||||
"core/workflow/generator/test_tool_catalogue.py",
|
||||
"core/workflow/graph_engine/layers/conftest.py",
|
||||
"core/workflow/graph_engine/layers/test_llm_quota.py",
|
||||
"core/workflow/graph_engine/layers/test_observability.py",
|
||||
"core/workflow/graph_engine/test_mock_config.py",
|
||||
"core/workflow/graph_engine/test_mock_factory.py",
|
||||
"core/workflow/graph_engine/test_mock_nodes.py",
|
||||
"core/workflow/graph_engine/test_parallel_human_input_join_resume.py",
|
||||
"core/workflow/graph_engine/test_table_runner.py",
|
||||
"core/workflow/graph_engine/test_tool_in_chatflow.py",
|
||||
"core/workflow/nodes/agent/test_message_transformer.py",
|
||||
"core/workflow/nodes/agent/test_runtime_support.py",
|
||||
"core/workflow/nodes/agent_v2/test_agent_node.py",
|
||||
"core/workflow/nodes/agent_v2/test_binding_resolver.py",
|
||||
"core/workflow/nodes/agent_v2/test_dify_tools_builder.py",
|
||||
"core/workflow/nodes/agent_v2/test_file_tenant_validator.py",
|
||||
"core/workflow/nodes/agent_v2/test_output_adapter.py",
|
||||
"core/workflow/nodes/agent_v2/test_output_failure_orchestrator.py",
|
||||
"core/workflow/nodes/agent_v2/test_output_file_rebacker.py",
|
||||
"core/workflow/nodes/agent_v2/test_output_type_checker.py",
|
||||
"core/workflow/nodes/agent_v2/test_runtime_request_builder.py",
|
||||
"core/workflow/nodes/agent_v2/test_session_cleanup_layer.py",
|
||||
"core/workflow/nodes/agent_v2/test_session_store.py",
|
||||
"core/workflow/nodes/agent_v2/test_validators.py",
|
||||
"core/workflow/nodes/answer/test_answer.py",
|
||||
"core/workflow/nodes/base/test_base_node.py",
|
||||
"core/workflow/nodes/base/test_get_node_type_classes_mapping.py",
|
||||
"core/workflow/nodes/code/code_node_spec.py",
|
||||
"core/workflow/nodes/datasource/test_datasource_node.py",
|
||||
"core/workflow/nodes/http_request/test_http_request_executor.py",
|
||||
"core/workflow/nodes/http_request/test_http_request_node.py",
|
||||
"core/workflow/nodes/human_input/test_dify_owned_contracts.py",
|
||||
"core/workflow/nodes/human_input/test_email_delivery_config.py",
|
||||
"core/workflow/nodes/human_input/test_entities.py",
|
||||
"core/workflow/nodes/human_input/test_human_input_form_filled_event.py",
|
||||
"core/workflow/nodes/iteration/test_iteration_child_engine_errors.py",
|
||||
"core/workflow/nodes/knowledge_index/test_knowledge_index_node.py",
|
||||
"core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py",
|
||||
"core/workflow/nodes/list_operator/node_spec.py",
|
||||
"core/workflow/nodes/llm/test_llm_utils.py",
|
||||
"core/workflow/nodes/llm/test_node.py",
|
||||
"core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py",
|
||||
"core/workflow/nodes/template_transform/template_transform_node_spec.py",
|
||||
"core/workflow/nodes/template_transform/test_template_transform_node.py",
|
||||
"core/workflow/nodes/test_base_node.py",
|
||||
"core/workflow/nodes/test_document_extractor_node.py",
|
||||
"core/workflow/nodes/test_if_else.py",
|
||||
"core/workflow/nodes/test_list_operator.py",
|
||||
"core/workflow/nodes/test_start_node_json_object.py",
|
||||
"core/workflow/nodes/tool/test_tool_node.py",
|
||||
"core/workflow/nodes/tool/test_tool_node_runtime.py",
|
||||
"core/workflow/nodes/webhook/test_entities.py",
|
||||
"core/workflow/nodes/webhook/test_exceptions.py",
|
||||
"core/workflow/nodes/webhook/test_webhook_file_conversion.py",
|
||||
"core/workflow/nodes/webhook/test_webhook_node.py",
|
||||
"core/workflow/test_enrich_pause_reasons.py",
|
||||
"core/workflow/test_form_input_serialization_compat.py",
|
||||
"core/workflow/test_graph_topology.py",
|
||||
"core/workflow/test_human_input_adapter.py",
|
||||
"core/workflow/test_human_input_callback.py",
|
||||
"core/workflow/test_human_input_forms.py",
|
||||
"core/workflow/test_node_factory.py",
|
||||
"core/workflow/test_node_mapping_bootstrap.py",
|
||||
"core/workflow/test_node_runtime.py",
|
||||
"core/workflow/test_system_variable.py",
|
||||
"core/workflow/test_variable_pool.py",
|
||||
"core/workflow/test_workflow_entry.py",
|
||||
"core/workflow/test_workflow_entry_helpers.py",
|
||||
"core/workflow/test_workflow_entry_redis_channel.py",
|
||||
"dev/test_generate_knowledge_fs_contract.py",
|
||||
"enterprise/telemetry/test_contracts.py",
|
||||
"enterprise/telemetry/test_draft_trace.py",
|
||||
"enterprise/telemetry/test_enterprise_trace.py",
|
||||
"enterprise/telemetry/test_event_handlers.py",
|
||||
"enterprise/telemetry/test_exporter.py",
|
||||
"enterprise/telemetry/test_gateway.py",
|
||||
"enterprise/telemetry/test_metric_handler.py",
|
||||
"enums/test_quota_type.py",
|
||||
"events/event_handlers/test_clean_when_document_deleted.py",
|
||||
"events/event_handlers/test_delete_tool_parameters_cache_when_sync_draft_workflow.py",
|
||||
"events/test_app_event_signals.py",
|
||||
"events/test_events_package_compat.py",
|
||||
"events/test_update_provider_when_message_created.py",
|
||||
"extensions/logstore/repositories/test_logstore_api_workflow_node_execution_repository.py",
|
||||
"extensions/logstore/test_sql_escape.py",
|
||||
"extensions/otel/conftest.py",
|
||||
"extensions/otel/decorators/handlers/test_generate_handler.py",
|
||||
"extensions/otel/decorators/handlers/test_workflow_app_runner_handler.py",
|
||||
"extensions/otel/decorators/test_base.py",
|
||||
"extensions/otel/decorators/test_handler.py",
|
||||
"extensions/otel/test_celery_sqlcommenter.py",
|
||||
"extensions/otel/test_context.py",
|
||||
"extensions/otel/test_retrieval_tracing.py",
|
||||
"extensions/otel/test_runtime.py",
|
||||
"extensions/storage/test_supabase_storage.py",
|
||||
"extensions/test_celery_ssl.py",
|
||||
"extensions/test_ext_blueprints_openapi.py",
|
||||
"extensions/test_ext_login.py",
|
||||
"extensions/test_ext_request_logging.py",
|
||||
"extensions/test_ext_socketio.py",
|
||||
"extensions/test_pubsub_channel.py",
|
||||
"extensions/test_redis.py",
|
||||
"extensions/test_set_secretkey.py",
|
||||
"extensions/test_workflow_warm_shutdown.py",
|
||||
"factories/test_build_from_mapping.py",
|
||||
"factories/test_file_factory.py",
|
||||
"factories/test_file_validation.py",
|
||||
"factories/test_variable_factory.py",
|
||||
"fields/test_dataset_fields.py",
|
||||
"fields/test_file_fields.py",
|
||||
"fields/test_message_fields.py",
|
||||
"libs/_human_input/support.py",
|
||||
"libs/_human_input/test_form_service.py",
|
||||
"libs/_human_input/test_models.py",
|
||||
"libs/broadcast_channel/redis/test_channel_unit_tests.py",
|
||||
"libs/broadcast_channel/redis/test_streams_channel_unit_tests.py",
|
||||
"libs/test_api_token_cache.py",
|
||||
"libs/test_archive_storage.py",
|
||||
"libs/test_cron_compatibility.py",
|
||||
"libs/test_custom_inputs.py",
|
||||
"libs/test_datetime_utils.py",
|
||||
"libs/test_email.py",
|
||||
"libs/test_email_i18n.py",
|
||||
"libs/test_encryption.py",
|
||||
"libs/test_external_api.py",
|
||||
"libs/test_file_utils.py",
|
||||
"libs/test_flask_utils.py",
|
||||
"libs/test_helper.py",
|
||||
"libs/test_json_in_md_parser.py",
|
||||
"libs/test_jwt_imports.py",
|
||||
"libs/test_login.py",
|
||||
"libs/test_oauth_base.py",
|
||||
"libs/test_oauth_bearer.py",
|
||||
"libs/test_oauth_bearer_layer0_cache.py",
|
||||
"libs/test_oauth_bearer_rate_limit_ordering.py",
|
||||
"libs/test_oauth_bearer_require_scope.py",
|
||||
"libs/test_oauth_clients.py",
|
||||
"libs/test_orjson.py",
|
||||
"libs/test_pagination.py",
|
||||
"libs/test_pandas.py",
|
||||
"libs/test_passport.py",
|
||||
"libs/test_password.py",
|
||||
"libs/test_rate_limit_bearer.py",
|
||||
"libs/test_rate_limiter.py",
|
||||
"libs/test_rsa.py",
|
||||
"libs/test_schedule_utils_enhanced.py",
|
||||
"libs/test_sendgrid_client.py",
|
||||
"libs/test_smtp_client.py",
|
||||
"libs/test_time_parser.py",
|
||||
"libs/test_token.py",
|
||||
"libs/test_token_manager.py",
|
||||
"libs/test_uuid_utils.py",
|
||||
"libs/test_workspace_member_helper.py",
|
||||
"libs/test_workspace_permission.py",
|
||||
"libs/test_yarl.py",
|
||||
"migrations/test_agent_drive_skill_metadata_refactor.py",
|
||||
"migrations/test_uuidv7_pg18_migration.py",
|
||||
"models/test_account_models.py",
|
||||
"models/test_agent.py",
|
||||
"models/test_app_models.py",
|
||||
"models/test_base.py",
|
||||
"models/test_conversation_variable.py",
|
||||
"models/test_dataset_models.py",
|
||||
"models/test_end_user_type.py",
|
||||
"models/test_enums_creator_user_role.py",
|
||||
"models/test_model.py",
|
||||
"models/test_plugin_entities.py",
|
||||
"models/test_provider_models.py",
|
||||
"models/test_tool_models.py",
|
||||
"models/test_types.py",
|
||||
"models/test_workflow.py",
|
||||
"models/test_workflow_models.py",
|
||||
"models/test_workflow_node_execution_offload.py",
|
||||
"oss/__mock/aliyun_oss.py",
|
||||
"oss/__mock/baidu_obs.py",
|
||||
"oss/__mock/base.py",
|
||||
"oss/__mock/local.py",
|
||||
"oss/__mock/tencent_cos.py",
|
||||
"oss/__mock/volcengine_tos.py",
|
||||
"oss/aliyun_oss/aliyun_oss/test_aliyun_oss.py",
|
||||
"oss/baidu_obs/test_baidu_obs.py",
|
||||
"oss/opendal/test_opendal.py",
|
||||
"oss/tencent_cos/test_tencent_cos.py",
|
||||
"oss/volcengine_tos/test_volcengine_tos.py",
|
||||
"repositories/test_sqlalchemy_api_workflow_run_repository.py",
|
||||
"services/agent/test_agent_composer_entities.py",
|
||||
"services/agent/test_agent_dsl_service.py",
|
||||
"services/agent/test_agent_observability_service.py",
|
||||
"services/agent/test_agent_services.py",
|
||||
"services/agent/test_composer_candidates.py",
|
||||
"services/agent/test_composer_mention_validation.py",
|
||||
"services/agent/test_prompt_mentions.py",
|
||||
"services/agent/test_skill_package_service.py",
|
||||
"services/agent/test_skill_standardize_service.py",
|
||||
"services/agent/test_skill_tool_inference_service.py",
|
||||
"services/agent/test_workflow_publish_service.py",
|
||||
"services/auth/test_api_key_auth_base.py",
|
||||
"services/auth/test_api_key_auth_factory.py",
|
||||
"services/auth/test_api_key_auth_service.py",
|
||||
"services/auth/test_auth_type.py",
|
||||
"services/auth/test_firecrawl_auth.py",
|
||||
"services/auth/test_jina_auth.py",
|
||||
"services/auth/test_jina_auth_standalone_module.py",
|
||||
"services/auth/test_watercrawl_auth.py",
|
||||
"services/controller_api.py",
|
||||
"services/data_migration/test_dependency_discovery_service.py",
|
||||
"services/data_migration/test_entities.py",
|
||||
"services/data_migration/test_export_service.py",
|
||||
"services/data_migration/test_import_service.py",
|
||||
"services/data_migration/test_package_service.py",
|
||||
"services/data_migration/test_report_service.py",
|
||||
"services/dataset_service_test_helpers.py",
|
||||
"services/document_service_validation.py",
|
||||
"services/enterprise/test_account_deletion_sync.py",
|
||||
"services/enterprise/test_app_permitted_service.py",
|
||||
"services/enterprise/test_enterprise_service.py",
|
||||
"services/enterprise/test_plugin_manager_service.py",
|
||||
"services/enterprise/test_rbac_service.py",
|
||||
"services/enterprise/test_traceparent_propagation.py",
|
||||
"services/hit_service.py",
|
||||
"services/openapi/test_mint_policy.py",
|
||||
"services/plugin/conftest.py",
|
||||
"services/plugin/test_dependencies_analysis.py",
|
||||
"services/plugin/test_endpoint_service.py",
|
||||
"services/plugin/test_oauth_service.py",
|
||||
"services/plugin/test_plugin_migration.py",
|
||||
"services/plugin/test_plugin_parameter_service.py",
|
||||
"services/plugin/test_plugin_service.py",
|
||||
"services/plugin/test_plugin_service_installation.py",
|
||||
"services/rag_pipeline/pipeline_template/test_built_in_retrieval.py",
|
||||
"services/rag_pipeline/pipeline_template/test_pipeline_template_base.py",
|
||||
"services/rag_pipeline/test_pipeline_generate_service.py",
|
||||
"services/rag_pipeline/test_rag_pipeline_dsl_service.py",
|
||||
"services/rag_pipeline/test_rag_pipeline_service.py",
|
||||
"services/rag_pipeline/test_rag_pipeline_task_proxy.py",
|
||||
"services/rag_pipeline/test_rag_pipeline_transform_service.py",
|
||||
"services/recommend_app/test_buildin_retrieval.py",
|
||||
"services/recommend_app/test_category_order.py",
|
||||
"services/recommend_app/test_recommend_app_factory.py",
|
||||
"services/recommend_app/test_recommend_app_type.py",
|
||||
"services/recommend_app/test_remote_retrieval.py",
|
||||
"services/retention/test_messages_clean_policy.py",
|
||||
"services/retention/workflow_run/test_archive_download_preparation.py",
|
||||
"services/retention/workflow_run/test_archive_download_task_cache.py",
|
||||
"services/retention/workflow_run/test_archive_log_service.py",
|
||||
"services/retention/workflow_run/test_bundle_archive_maintenance.py",
|
||||
"services/retention/workflow_run/test_clear_free_plan_expired_workflow_run_logs.py",
|
||||
"services/retention/workflow_run/test_restore_archived_workflow_run.py",
|
||||
"services/test_account_service.py",
|
||||
"services/test_agent_app_feature_service.py",
|
||||
"services/test_agent_app_sandbox_service.py",
|
||||
"services/test_agent_config_service.py",
|
||||
"services/test_agent_drive_service.py",
|
||||
"services/test_agent_file_request_service.py",
|
||||
"services/test_annotation_service.py",
|
||||
"services/test_api_token_service.py",
|
||||
"services/test_app_dsl_service.py",
|
||||
"services/test_app_generate_service.py",
|
||||
"services/test_app_generate_service_streaming_integration.py",
|
||||
"services/test_app_model_config_service.py",
|
||||
"services/test_app_service.py",
|
||||
"services/test_app_task_service.py",
|
||||
"services/test_archive_workflow_run_logs.py",
|
||||
"services/test_async_workflow_service.py",
|
||||
"services/test_audio_service.py",
|
||||
"services/test_billing_service.py",
|
||||
"services/test_clear_free_plan_expired_workflow_run_logs.py",
|
||||
"services/test_clear_free_plan_tenant_expired_logs.py",
|
||||
"services/test_code_based_extension_service.py",
|
||||
"services/test_conversation_service.py",
|
||||
"services/test_credential_permission_service.py",
|
||||
"services/test_credit_pool_service.py",
|
||||
"services/test_dataset_service_dataset.py",
|
||||
"services/test_dataset_service_document.py",
|
||||
"services/test_dataset_service_lock_not_owned.py",
|
||||
"services/test_dataset_service_segment.py",
|
||||
"services/test_datasource_provider_service.py",
|
||||
"services/test_document_indexing_task_proxy.py",
|
||||
"services/test_duplicate_document_indexing_task_proxy.py",
|
||||
"services/test_export_app_messages.py",
|
||||
"services/test_external_dataset_service.py",
|
||||
"services/test_feature_service_app_dsl_version.py",
|
||||
"services/test_feature_service_enable_app_deploy.py",
|
||||
"services/test_feature_service_human_input_email_delivery.py",
|
||||
"services/test_feature_service_learn_app.py",
|
||||
"services/test_feature_service_licensed_seats.py",
|
||||
"services/test_feature_service_trial_models.py",
|
||||
"services/test_feature_service_vector_space.py",
|
||||
"services/test_feature_service_webapp_public_access.py",
|
||||
"services/test_feedback_service.py",
|
||||
"services/test_file_service.py",
|
||||
"services/test_human_input_delivery_test_service.py",
|
||||
"services/test_human_input_file_upload_service.py",
|
||||
"services/test_human_input_service.py",
|
||||
"services/test_knowledge_retrieval_inner_service.py",
|
||||
"services/test_knowledge_service.py",
|
||||
"services/test_message_service.py",
|
||||
"services/test_messages_clean_service.py",
|
||||
"services/test_metadata_nullable_bug.py",
|
||||
"services/test_metadata_service_session_boundary.py",
|
||||
"services/test_model_load_balancing_service.py",
|
||||
"services/test_model_provider_service.py",
|
||||
"services/test_model_provider_service_sanitization.py",
|
||||
"services/test_oauth_device_flow.py",
|
||||
"services/test_oauth_server_service.py",
|
||||
"services/test_operation_service.py",
|
||||
"services/test_rag_pipeline_task_proxy.py",
|
||||
"services/test_schedule_service.py",
|
||||
"services/test_snippet_dsl_service.py",
|
||||
"services/test_snippet_generate_service.py",
|
||||
"services/test_snippet_service.py",
|
||||
"services/test_step_by_step_tour_service.py",
|
||||
"services/test_summary_index_service.py",
|
||||
"services/test_telemetry_service.py",
|
||||
"services/test_trigger_provider_service.py",
|
||||
"services/test_variable_truncator.py",
|
||||
"services/test_vector_service.py",
|
||||
"services/test_webhook_service.py",
|
||||
"services/test_webhook_service_additional.py",
|
||||
"services/test_website_service.py",
|
||||
"services/test_workflow_app_service_metadata.py",
|
||||
"services/test_workflow_collaboration_service.py",
|
||||
"services/test_workflow_comment_service.py",
|
||||
"services/test_workflow_generator_service.py",
|
||||
"services/test_workflow_node_execution_trace_service.py",
|
||||
"services/test_workflow_run_service.py",
|
||||
"services/test_workflow_run_service_pause.py",
|
||||
"services/test_workflow_service.py",
|
||||
"services/tools/test_api_tools_manage_service.py",
|
||||
"services/tools/test_builtin_tools_manage_service.py",
|
||||
"services/tools/test_mcp_tools_transform.py",
|
||||
"services/tools/test_tool_labels_service.py",
|
||||
"services/tools/test_tools_manage_service.py",
|
||||
"services/tools/test_tools_transform_service.py",
|
||||
"services/workflow/test_draft_var_loader_simple.py",
|
||||
"services/workflow/test_inspector_events.py",
|
||||
"services/workflow/test_node_output_inspector_service.py",
|
||||
"services/workflow/test_queue_dispatcher.py",
|
||||
"services/workflow/test_scheduler.py",
|
||||
"services/workflow/test_workflow_converter_additional.py",
|
||||
"services/workflow/test_workflow_draft_variable_service.py",
|
||||
"services/workflow/test_workflow_event_snapshot_service.py",
|
||||
"services/workflow/test_workflow_event_snapshot_service_additional.py",
|
||||
"services/workflow/test_workflow_human_input_delivery.py",
|
||||
"services/workflow/test_workflow_restore.py",
|
||||
"tasks/test_agent_backend_session_cleanup_task.py",
|
||||
"tasks/test_async_workflow_tasks.py",
|
||||
"tasks/test_batch_clean_document_task.py",
|
||||
"tasks/test_clean_dataset_task.py",
|
||||
"tasks/test_clean_document_task.py",
|
||||
"tasks/test_community_telemetry_task.py",
|
||||
"tasks/test_dataset_indexing_task.py",
|
||||
"tasks/test_document_indexing_sync_task.py",
|
||||
"tasks/test_document_indexing_update_task.py",
|
||||
"tasks/test_duplicate_document_indexing_task.py",
|
||||
"tasks/test_enable_segment_index_tasks.py",
|
||||
"tasks/test_enterprise_telemetry_task.py",
|
||||
"tasks/test_human_input_timeout_tasks.py",
|
||||
"tasks/test_initialize_created_app_rbac_access_task.py",
|
||||
"tasks/test_install_default_plugins_task.py",
|
||||
"tasks/test_mail_human_input_delivery_task.py",
|
||||
"tasks/test_mail_send_task.py",
|
||||
"tasks/test_ops_trace_task.py",
|
||||
"tasks/test_process_tenant_plugin_autoupgrade_check_task.py",
|
||||
"tasks/test_refresh_billing_vector_space_task.py",
|
||||
"tasks/test_remove_app_and_related_data_task.py",
|
||||
"tasks/test_resume_agent_app_task.py",
|
||||
"tasks/test_summary_queue_isolation.py",
|
||||
"tasks/test_trigger_processing_tasks.py",
|
||||
"tasks/test_workflow_execute_task.py",
|
||||
"test_app_factory.py",
|
||||
"test_makefile_backend_tests.py",
|
||||
"test_pytest_dify.py",
|
||||
"tools/test_api_tool.py",
|
||||
"tools/test_mcp_tool.py",
|
||||
"utils/encryption/test_system_encryption.py",
|
||||
"utils/http_parser/test_oauth_convert_request_to_raw_data.py",
|
||||
"utils/position_helper/test_position_helper.py",
|
||||
"utils/structured_output_parser/test_structured_output_parser.py",
|
||||
"utils/test_text_processing.py",
|
||||
"utils/yaml/test_yaml_utils.py",
|
||||
]
|
||||
|
||||
[errors]
|
||||
missing-override-decorator = "error"
|
||||
redundant-cast = true
|
||||
unannotated-return = true
|
||||
unnecessary-type-conversion = true
|
||||
unused-ignore = true
|
||||
@@ -13,7 +13,7 @@ from services.agent import skill_package_service as skill_package_service_module
|
||||
from services.agent.skill_package_service import NormalizedSkillPackage, SkillPackageError, SkillPackageService
|
||||
|
||||
_SKILL_MD = """---
|
||||
name: PDF Toolkit
|
||||
name: pdf-toolkit
|
||||
description: Tools for working with PDF files.
|
||||
---
|
||||
|
||||
@@ -43,7 +43,7 @@ def _archive_members(content: bytes) -> list[str]:
|
||||
def test_valid_skill_normalizes_manifest():
|
||||
manifest = _normalize({"SKILL.md": _SKILL_MD.encode(), "scripts/run.py": b"print('hi')\n"}).manifest
|
||||
|
||||
assert manifest.name == "PDF Toolkit"
|
||||
assert manifest.name == "pdf-toolkit"
|
||||
assert manifest.description == "Tools for working with PDF files."
|
||||
assert manifest.entry_path == "SKILL.md"
|
||||
assert set(manifest.files) == {"SKILL.md", "scripts/run.py"}
|
||||
@@ -51,10 +51,10 @@ def test_valid_skill_normalizes_manifest():
|
||||
assert len(manifest.hash) == 64
|
||||
|
||||
|
||||
def test_name_falls_back_to_heading_without_frontmatter():
|
||||
manifest = _normalize({"SKILL.md": b"# Heading Name\n\nbody"}).manifest
|
||||
assert manifest.name == "Heading Name"
|
||||
assert manifest.description == ""
|
||||
def test_name_and_description_are_required_in_frontmatter():
|
||||
with pytest.raises(SkillPackageError) as exc_info:
|
||||
_normalize({"SKILL.md": b"# heading-name\n\nbody"})
|
||||
assert exc_info.value.code == "missing_skill_name"
|
||||
|
||||
|
||||
def test_shallowest_skill_md_preferred_during_normalization():
|
||||
@@ -155,7 +155,18 @@ def test_validate_and_normalize_strips_deeper_selected_skill_root():
|
||||
({"README.md": b"x"}, "skill.zip", "missing_skill_md"),
|
||||
({"SKILL.md": _SKILL_MD.encode()}, "skill.tar", "unsupported_extension"),
|
||||
({"SKILL.md": b""}, "skill.zip", "empty_skill_md"),
|
||||
({"SKILL.md": b"no name here"}, "skill.zip", "missing_skill_name"),
|
||||
({"SKILL.md": b"---\ndescription: valid\n---\n# no name here"}, "skill.zip", "missing_skill_name"),
|
||||
({"SKILL.md": b"---\nname: pdf-toolkit\n---\n# no description"}, "skill.zip", "missing_skill_description"),
|
||||
(
|
||||
{"SKILL.md": b"---\nname: PDF Toolkit\ndescription: valid\n---\n# invalid name"},
|
||||
"skill.zip",
|
||||
"invalid_skill_name",
|
||||
),
|
||||
(
|
||||
{"SKILL.md": f"---\nname: pdf-toolkit\ndescription: {'x' * 1025}\n---\n# long".encode()},
|
||||
"skill.zip",
|
||||
"invalid_skill_description",
|
||||
),
|
||||
({"SKILL.md": b"\xff\xfenot utf8"}, "skill.zip", "skill_md_not_utf8"),
|
||||
],
|
||||
)
|
||||
@@ -224,10 +235,10 @@ def test_bad_frontmatter_yaml_rejected():
|
||||
assert exc_info.value.code == "invalid_frontmatter"
|
||||
|
||||
|
||||
def test_unterminated_frontmatter_falls_back_to_heading():
|
||||
# leading '---' with no closing fence -> no frontmatter, use the heading
|
||||
manifest = _normalize({"SKILL.md": b"---\n# Heading Wins\nbody"}).manifest
|
||||
assert manifest.name == "Heading Wins"
|
||||
def test_unterminated_frontmatter_rejected():
|
||||
with pytest.raises(SkillPackageError) as exc_info:
|
||||
_normalize({"SKILL.md": b"---\n# heading-wins\nbody"})
|
||||
assert exc_info.value.code == "missing_skill_name"
|
||||
|
||||
|
||||
def test_validate_and_normalize_rejects_files_outside_selected_skill_root():
|
||||
|
||||
@@ -20,7 +20,7 @@ _AGENT_ID = "22222222-2222-2222-2222-222222222222"
|
||||
_USER_ID = "33333333-3333-3333-3333-333333333333"
|
||||
|
||||
_SKILL_MD = b"""---
|
||||
name: PDF Toolkit
|
||||
name: pdf-toolkit
|
||||
description: Work with PDFs.
|
||||
---
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest(
|
||||
assert skill_row.is_skill is True
|
||||
assert skill_row.skill_metadata is not None
|
||||
skill_metadata = DriveSkillMetadata.model_validate_json(skill_row.skill_metadata)
|
||||
assert skill_metadata.name == "PDF Toolkit"
|
||||
assert skill_metadata.name == "pdf-toolkit"
|
||||
assert skill_metadata.manifest_files == ["SKILL.md", "scripts/run.py"]
|
||||
assert archive_row.file_kind == AgentDriveFileKind.TOOL_FILE
|
||||
assert archive_row.file_id == archive_tool_file.id
|
||||
@@ -132,7 +132,7 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest(
|
||||
# The returned upload response carries only the drive-derived fields the UI needs.
|
||||
skill = result["skill"]
|
||||
assert skill["path"] == "pdf-toolkit"
|
||||
assert skill["name"] == "PDF Toolkit"
|
||||
assert skill["name"] == "pdf-toolkit"
|
||||
assert skill["archive_key"] == "pdf-toolkit/.DIFY-SKILL-FULL.zip"
|
||||
assert skill["skill_md_key"] == "pdf-toolkit/SKILL.md"
|
||||
assert result["manifest"]["entry_path"] == "SKILL.md"
|
||||
|
||||
@@ -7,16 +7,14 @@ update_features persists those flags as a new app_model_config version without
|
||||
touching model / prompt / agent_mode.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.account import Account
|
||||
from models.model import App, AppMode, AppModelConfig
|
||||
from services.agent_app_feature_service import AgentAppFeatureConfigService
|
||||
|
||||
TENANT_ID = "11111111-1111-1111-1111-111111111111"
|
||||
APP_ID = "22222222-2222-2222-2222-222222222222"
|
||||
ACCOUNT_ID = "33333333-3333-3333-3333-333333333333"
|
||||
|
||||
|
||||
class TestValidateFeatures:
|
||||
@@ -73,47 +71,45 @@ class TestValidateFeatures:
|
||||
AgentAppFeatureConfigService.validate_features(TENANT_ID, {"suggested_questions": "nope"})
|
||||
|
||||
|
||||
class _FakeWriteSession:
|
||||
def __init__(self) -> None:
|
||||
self.added: list[Any] = []
|
||||
self.flushed = 0
|
||||
self.committed = 0
|
||||
|
||||
def add(self, obj: Any) -> None:
|
||||
self.added.append(obj)
|
||||
|
||||
def flush(self) -> None:
|
||||
self.flushed += 1
|
||||
|
||||
def commit(self) -> None:
|
||||
self.committed += 1
|
||||
|
||||
|
||||
class TestUpdateFeatures:
|
||||
@pytest.mark.parametrize("sqlite_session", [(Account, App, AppModelConfig)], indirect=True)
|
||||
def test_persists_new_app_model_config_version(self, sqlite_session: Session):
|
||||
app_model = App(
|
||||
id=APP_ID,
|
||||
tenant_id=TENANT_ID,
|
||||
name="Agent App",
|
||||
description="",
|
||||
mode=AppMode.AGENT,
|
||||
enable_site=True,
|
||||
enable_api=True,
|
||||
max_active_requests=0,
|
||||
def test_persists_new_app_model_config_version(self):
|
||||
session = _FakeWriteSession()
|
||||
app_model = SimpleNamespace(
|
||||
tenant_id=TENANT_ID, id="app-1", app_model_config_id=None, updated_by=None, updated_at=None
|
||||
)
|
||||
account = Account(name="Test User", email="test@example.com")
|
||||
account.id = ACCOUNT_ID
|
||||
sqlite_session.add_all([account, app_model])
|
||||
sqlite_session.commit()
|
||||
account = SimpleNamespace(id="acct-1")
|
||||
|
||||
new_config = AgentAppFeatureConfigService.update_features(
|
||||
app_model=app_model,
|
||||
account=account,
|
||||
app_model=app_model, # type: ignore[arg-type]
|
||||
account=account, # type: ignore[arg-type]
|
||||
config={"opening_statement": "Hi!", "suggested_questions_after_answer": {"enabled": True}},
|
||||
session=sqlite_session,
|
||||
session=session,
|
||||
)
|
||||
assert not sqlite_session.in_transaction()
|
||||
|
||||
# New row carries the features but no Soul-owned model/prompt/agent_mode.
|
||||
assert new_config.app_id == APP_ID
|
||||
assert new_config.app_id == "app-1"
|
||||
assert new_config.opening_statement == "Hi!"
|
||||
assert new_config.model is None
|
||||
assert new_config.agent_mode is None
|
||||
# App is repointed at the new version and the write is committed.
|
||||
assert app_model.app_model_config_id == new_config.id
|
||||
assert app_model.updated_by == ACCOUNT_ID
|
||||
sqlite_session.expunge_all()
|
||||
persisted_config = sqlite_session.get(AppModelConfig, new_config.id)
|
||||
persisted_app = sqlite_session.get(App, APP_ID)
|
||||
assert persisted_config is not None
|
||||
assert persisted_config.opening_statement == "Hi!"
|
||||
assert persisted_config.model is None
|
||||
assert persisted_config.agent_mode is None
|
||||
assert persisted_app is not None
|
||||
assert persisted_app.app_model_config_id == new_config.id
|
||||
assert persisted_app.updated_by == ACCOUNT_ID
|
||||
assert app_model.updated_by == "acct-1"
|
||||
assert new_config in session.added
|
||||
assert session.flushed == 1
|
||||
assert session.committed == 1
|
||||
|
||||
@@ -63,6 +63,7 @@ def _target(
|
||||
) -> AgentConfigTarget:
|
||||
agent_soul = soul or _soul()
|
||||
return AgentConfigTarget(
|
||||
tenant_id=TENANT,
|
||||
agent_id=AGENT,
|
||||
version_id=version_id,
|
||||
kind=kind,
|
||||
@@ -508,7 +509,9 @@ def test_manifest_uses_items_shape_without_download_urls() -> None:
|
||||
),
|
||||
)
|
||||
|
||||
manifest = AgentConfigService._manifest_for_target(target)
|
||||
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
|
||||
skill_management_service.return_value.list_runtime_agent_skills.return_value = []
|
||||
manifest = AgentConfigService._manifest_for_target(target)
|
||||
|
||||
assert manifest == {
|
||||
"agent_id": AGENT,
|
||||
@@ -557,7 +560,9 @@ def test_manifest_preserves_missing_config_assets_and_pull_rejects_them() -> Non
|
||||
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=soul)
|
||||
service = AgentConfigService()
|
||||
|
||||
manifest = service._manifest_for_target(target)
|
||||
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
|
||||
skill_management_service.return_value.list_runtime_agent_skills.return_value = []
|
||||
manifest = service._manifest_for_target(target)
|
||||
|
||||
assert manifest["skills"]["items"][0]["is_missing"] is True # type: ignore[index]
|
||||
assert manifest["files"]["items"][0]["is_missing"] is True # type: ignore[index]
|
||||
@@ -606,6 +611,44 @@ def test_config_asset_refs_require_file_id_unless_marked_missing() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_manifest_appends_published_workspace_skills() -> None:
|
||||
target = _target(
|
||||
kind=AgentConfigVersionKind.DRAFT,
|
||||
writable=False,
|
||||
soul=_soul(
|
||||
config_skills=[
|
||||
AgentConfigSkillRefConfig(name="alpha", description="Alpha skill", file_id="tool-file-1")
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
|
||||
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
|
||||
{
|
||||
"id": "workspace-skill-id",
|
||||
"name": "beta",
|
||||
"file_id": "tool-file-2",
|
||||
"description": "Beta workspace skill",
|
||||
"size": 123,
|
||||
"hash": "sha256:beta",
|
||||
"mime_type": "application/zip",
|
||||
},
|
||||
{
|
||||
"id": "duplicate",
|
||||
"name": "alpha",
|
||||
"file_id": "tool-file-ignored",
|
||||
"description": "Duplicate workspace skill",
|
||||
"size": 456,
|
||||
"hash": "sha256:ignored",
|
||||
"mime_type": "application/zip",
|
||||
},
|
||||
]
|
||||
manifest = AgentConfigService._manifest_for_target(target)
|
||||
|
||||
assert [item["name"] for item in manifest["skills"]["items"]] == ["alpha", "beta"]
|
||||
assert manifest["skills"]["items"][1]["file_id"] == "tool-file-2"
|
||||
|
||||
|
||||
def test_preview_skill_file_returns_text_preview() -> None:
|
||||
service = AgentConfigService()
|
||||
target = _target(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+4
-5
@@ -1738,7 +1738,7 @@ storage = [
|
||||
]
|
||||
tools = [
|
||||
{ name = "cloudscraper", specifier = ">=1.2.71,<2.0.0" },
|
||||
{ name = "nltk", specifier = ">=3.10.0,<4.0.0" },
|
||||
{ name = "nltk", specifier = ">=3.9.1,<4.0.0" },
|
||||
]
|
||||
trace-aliyun = [{ name = "dify-trace-aliyun", editable = "providers/trace/trace-aliyun" }]
|
||||
trace-all = [
|
||||
@@ -4113,18 +4113,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "nltk"
|
||||
version = "3.10.0"
|
||||
version = "3.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "defusedxml" },
|
||||
{ name = "joblib" },
|
||||
{ name = "regex" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/02/df4f105b28a7c16b0e41423bc09cf0f1b8a305df4ef0b10ca74a2e4c648c/nltk-3.10.0.tar.gz", hash = "sha256:4fbac1d98203cbcd1b5d94a2877fb822300072d80604a5e7fae49d2c5f84e8c1", size = 3089244, upload-time = "2026-07-08T02:39:13.562Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/89/a0b0f35e2820d6a99d75ea1c11977ee6d5c9e6658eceb45b0c7620881faa/nltk-3.10.0-py3-none-any.whl", hash = "sha256:54ff84d4916d3ef127e8953bee0023f6a6b320b75d634a19e06ef056d3d244bf", size = 1716144, upload-time = "2026-07-08T02:39:09.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -7,7 +7,6 @@ REPO_ROOT="$SCRIPT_DIR/.."
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
EXCLUDES_FILE="api/pyrefly-local-excludes.txt"
|
||||
UNIT_TESTS_CONFIG="tests/unit_tests/pyrefly.toml"
|
||||
TEST_CONTAINERS_DIR="tests/test_containers_integration_tests"
|
||||
TEST_CONTAINERS_CONFIG="$TEST_CONTAINERS_DIR/pyrefly.toml"
|
||||
|
||||
@@ -75,19 +74,6 @@ fi
|
||||
run_pyrefly "${pyrefly_command[@]}" || status=$?
|
||||
|
||||
if (( ${#target_paths[@]} == 0 )); then
|
||||
unit_tests_args=(
|
||||
"--summary=none"
|
||||
"--use-ignore-files=false"
|
||||
"--config=$UNIT_TESTS_CONFIG"
|
||||
)
|
||||
if [[ "${PYREFLY_OUTPUT_FORMAT:-}" == "github" ]]; then
|
||||
unit_tests_args+=("--output-format=github")
|
||||
fi
|
||||
run_pyrefly \
|
||||
uv run --directory api --dev pyrefly check \
|
||||
"${unit_tests_args[@]}" \
|
||||
|| status=$?
|
||||
|
||||
test_containers_args=(
|
||||
"--summary=none"
|
||||
"--use-ignore-files=false"
|
||||
|
||||
@@ -115,7 +115,7 @@ def __getattr__(name: str) -> Any:
|
||||
if name not in _EXPORTS:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
module = import_module(_EXPORTS[name])
|
||||
value = getattr(module, name) # guard-ignore: no-new-getattr -- lazy export proxy
|
||||
value = getattr(module, name) # noqa: no-new-getattr lazy export proxy
|
||||
globals()[name] = value
|
||||
return value
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ def __getattr__(name: str) -> Any:
|
||||
if name not in _EXPORTS:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
module = import_module(_EXPORTS[name])
|
||||
value = getattr(module, name) # guard-ignore: no-new-getattr -- lazy export proxy
|
||||
value = getattr(module, name) # noqa: no-new-getattr lazy export proxy
|
||||
globals()[name] = value
|
||||
return value
|
||||
|
||||
|
||||
@@ -363,6 +363,14 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/app-publisher/sections.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx_a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
|
||||
@@ -997,11 +997,15 @@ export const zSandboxListResponse = z.object({
|
||||
* Validated metadata extracted from a Skill package.
|
||||
*/
|
||||
export const zSkillManifest = z.object({
|
||||
description: z.string(),
|
||||
description: z.string().min(1).max(1024),
|
||||
entry_path: z.string(),
|
||||
files: z.array(z.string()),
|
||||
hash: z.string(),
|
||||
name: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
|
||||
@@ -1373,11 +1373,15 @@ export const zAgentLogMetaResponse = z.object({
|
||||
* Validated metadata extracted from a Skill package.
|
||||
*/
|
||||
export const zSkillManifest = z.object({
|
||||
description: z.string(),
|
||||
description: z.string().min(1).max(1024),
|
||||
entry_path: z.string(),
|
||||
files: z.array(z.string()),
|
||||
hash: z.string(),
|
||||
name: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
|
||||
@@ -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(''),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,16 @@ export type AgentProviderListResponse = Array<{
|
||||
[key: string]: unknown
|
||||
}>
|
||||
|
||||
export type AgentSkillBindingsResponse = {
|
||||
agent_id: string
|
||||
data?: Array<AgentSkillBindingItemResponse>
|
||||
skill_ids?: Array<string>
|
||||
}
|
||||
|
||||
export type AgentSkillBindingsPayload = {
|
||||
skill_ids?: Array<string>
|
||||
}
|
||||
|
||||
export type SnippetPaginationResponse = {
|
||||
data: Array<SnippetListItemResponse>
|
||||
has_more: boolean
|
||||
@@ -635,6 +645,194 @@ export type WorkspaceAccessMatrix = {
|
||||
pagination?: Pagination | null
|
||||
}
|
||||
|
||||
export type SkillListResponse = {
|
||||
data?: Array<SkillResponse>
|
||||
has_more?: boolean
|
||||
limit?: number
|
||||
page?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
export type SkillCreatePayload = {
|
||||
description?: string
|
||||
display_name?: string | null
|
||||
icon?: string
|
||||
name?: string | null
|
||||
tags?: Array<string>
|
||||
}
|
||||
|
||||
export type SkillDetailResponse = {
|
||||
created_at: number
|
||||
created_by?: string | null
|
||||
created_by_name?: string | null
|
||||
description: string
|
||||
display_name: string
|
||||
files?: Array<SkillFileResponse>
|
||||
icon: string
|
||||
id: string
|
||||
latest_published_version_id?: string | null
|
||||
name: string
|
||||
name_manually_edited?: boolean
|
||||
reference_count?: number
|
||||
tags?: Array<string>
|
||||
updated_at: number
|
||||
updated_by?: string | null
|
||||
updated_by_name?: string | null
|
||||
visibility: string
|
||||
}
|
||||
|
||||
export type SkillFileUploadResponse = {
|
||||
hash: string
|
||||
id: string
|
||||
mime_type: string
|
||||
name: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export type SkillTagListResponse = {
|
||||
data?: Array<SkillTagResponse>
|
||||
}
|
||||
|
||||
export type SkillDeletePayload = {
|
||||
confirmation_name?: string | null
|
||||
}
|
||||
|
||||
export type SkillDeleteResponse = {
|
||||
deleted: boolean
|
||||
id: string
|
||||
}
|
||||
|
||||
export type SkillMetadataPayload = {
|
||||
display_name?: string | null
|
||||
expected_updated_at?: number | null
|
||||
icon?: string | null
|
||||
tags?: Array<string> | null
|
||||
}
|
||||
|
||||
export type SkillResponse = {
|
||||
created_at: number
|
||||
created_by?: string | null
|
||||
created_by_name?: string | null
|
||||
description: string
|
||||
display_name: string
|
||||
icon: string
|
||||
id: string
|
||||
latest_published_version_id?: string | null
|
||||
name: string
|
||||
name_manually_edited?: boolean
|
||||
reference_count?: number
|
||||
tags?: Array<string>
|
||||
updated_at: number
|
||||
updated_by?: string | null
|
||||
updated_by_name?: string | null
|
||||
visibility: string
|
||||
}
|
||||
|
||||
export type SkillAssistModelPayload = {
|
||||
model: string
|
||||
model_settings?: { [key: string]: unknown } | null
|
||||
plugin_id?: string | null
|
||||
provider: string
|
||||
}
|
||||
|
||||
export type SkillAssistAttachmentPayload = {
|
||||
mime_type?: string | null
|
||||
name: string
|
||||
size?: number | null
|
||||
tool_file_id: string
|
||||
}
|
||||
|
||||
export type SkillAssistMessagePayload = {
|
||||
attachments?: Array<SkillAssistAttachmentPayload>
|
||||
message: string
|
||||
model?: SkillAssistModelPayload | null
|
||||
target_path?: string | null
|
||||
}
|
||||
|
||||
export type SkillDraftFileOperationPayload = {
|
||||
content?: string | null
|
||||
expected_updated_at?: number | null
|
||||
hash?: string | null
|
||||
mime_type?: string | null
|
||||
operation: SkillDraftFileOperation
|
||||
path: string
|
||||
size?: number | null
|
||||
target_path?: string | null
|
||||
tool_file_id?: string | null
|
||||
}
|
||||
|
||||
export type SkillDraftTreePayload = {
|
||||
expected_updated_at?: number | null
|
||||
files?: Array<SkillDraftTreeItemPayload>
|
||||
}
|
||||
|
||||
export type SkillFilePreviewResponse = {
|
||||
content: string
|
||||
hash: string
|
||||
mime_type: string
|
||||
path: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export type SkillPublishPayload = {
|
||||
publish_note?: string
|
||||
version_name?: string | null
|
||||
}
|
||||
|
||||
export type SkillVersionResponse = {
|
||||
archive_size: number
|
||||
created_at: number
|
||||
hash_code: string
|
||||
id: string
|
||||
is_latest?: boolean
|
||||
publish_note: string
|
||||
published_by?: string | null
|
||||
published_by_name?: string | null
|
||||
skill_id: string
|
||||
version_name: string
|
||||
version_number: number
|
||||
}
|
||||
|
||||
export type SkillReferenceListResponse = {
|
||||
data?: Array<SkillReferenceResponse>
|
||||
}
|
||||
|
||||
export type SkillRestorePayload = {
|
||||
publish_note?: string
|
||||
version_id: string
|
||||
version_name?: string | null
|
||||
}
|
||||
|
||||
export type SkillVersionListResponse = {
|
||||
data?: Array<SkillVersionResponse>
|
||||
}
|
||||
|
||||
export type SkillVersionDeleteResponse = {
|
||||
deleted: boolean
|
||||
id: string
|
||||
latest_published_version_id?: string | null
|
||||
}
|
||||
|
||||
export type SkillVersionDetailResponse = {
|
||||
archive_size: number
|
||||
created_at: number
|
||||
files?: Array<SkillFileResponse>
|
||||
hash_code: string
|
||||
id: string
|
||||
is_latest?: boolean
|
||||
publish_note: string
|
||||
published_by?: string | null
|
||||
published_by_name?: string | null
|
||||
skill_id: string
|
||||
version_name: string
|
||||
version_number: number
|
||||
}
|
||||
|
||||
export type SkillVersionUpdatePayload = {
|
||||
publish_note?: string
|
||||
version_name?: string | null
|
||||
}
|
||||
|
||||
export type ToolLabelListResponse = Array<ToolLabel>
|
||||
|
||||
export type ApiToolProviderAddPayload = {
|
||||
@@ -1042,6 +1240,21 @@ export type WorkspaceCustomConfigResponse = {
|
||||
replace_webapp_logo?: string | null
|
||||
}
|
||||
|
||||
export type AgentSkillBindingItemResponse = {
|
||||
description: string
|
||||
display_name: string
|
||||
file_count: number
|
||||
icon: string
|
||||
id: string
|
||||
latest_published_at?: number | null
|
||||
latest_published_version_id?: string | null
|
||||
name: string
|
||||
priority: number
|
||||
status: string
|
||||
tags?: Array<string>
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type SnippetListItemResponse = {
|
||||
author_name: string | null
|
||||
created_at: number
|
||||
@@ -1502,6 +1715,60 @@ export type AccessPolicyRole = {
|
||||
role_tag?: string
|
||||
}
|
||||
|
||||
export type SkillFileResponse = {
|
||||
content?: string | null
|
||||
hash?: string | null
|
||||
id?: string | null
|
||||
kind: string
|
||||
mime_type?: string | null
|
||||
path: string
|
||||
size?: number | null
|
||||
storage?: string | null
|
||||
tool_file_id?: string | null
|
||||
}
|
||||
|
||||
export type SkillTagResponse = {
|
||||
count: number
|
||||
tag: string
|
||||
}
|
||||
|
||||
export type SkillDraftFileOperation =
|
||||
| 'delete'
|
||||
| 'mkdir'
|
||||
| 'rename'
|
||||
| 'upsert_text'
|
||||
| 'upsert_tool_file'
|
||||
|
||||
export type SkillDraftTreeItemPayload = {
|
||||
content?: string | null
|
||||
hash?: string | null
|
||||
kind?: SkillFileKind
|
||||
mime_type?: string | null
|
||||
path: string
|
||||
size?: number | null
|
||||
storage?: SkillFileStorage | null
|
||||
tool_file_id?: string | null
|
||||
}
|
||||
|
||||
export type SkillReferenceResponse = {
|
||||
agent_id: string
|
||||
agent_icon?: string | null
|
||||
agent_icon_background?: string | null
|
||||
agent_icon_type?: string | null
|
||||
app_id?: string | null
|
||||
display_name: string
|
||||
name: string
|
||||
node_id?: string | null
|
||||
node_name?: string | null
|
||||
type: string
|
||||
workflow_icon?: string | null
|
||||
workflow_icon_background?: string | null
|
||||
workflow_icon_type?: string | null
|
||||
workflow_id?: string | null
|
||||
workflow_name?: string | null
|
||||
workflow_version?: string | null
|
||||
}
|
||||
|
||||
export type ToolLabel = {
|
||||
icon: string
|
||||
label: I18nObject
|
||||
@@ -1978,6 +2245,10 @@ export type PermissionCatalogItem = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type SkillFileKind = 'directory' | 'file'
|
||||
|
||||
export type SkillFileStorage = 'text' | 'tool_file'
|
||||
|
||||
export type ToolParameter = {
|
||||
auto_generate?: PluginParameterAutoGenerate | null
|
||||
default?:
|
||||
@@ -2473,6 +2744,38 @@ export type GetWorkspacesCurrentAgentProvidersResponses = {
|
||||
export type GetWorkspacesCurrentAgentProvidersResponse =
|
||||
GetWorkspacesCurrentAgentProvidersResponses[keyof GetWorkspacesCurrentAgentProvidersResponses]
|
||||
|
||||
export type GetWorkspacesCurrentAgentsByAgentIdSkillsData = {
|
||||
body?: never
|
||||
path: {
|
||||
agent_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/agents/{agent_id}/skills'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentAgentsByAgentIdSkillsResponses = {
|
||||
200: AgentSkillBindingsResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentAgentsByAgentIdSkillsResponse =
|
||||
GetWorkspacesCurrentAgentsByAgentIdSkillsResponses[keyof GetWorkspacesCurrentAgentsByAgentIdSkillsResponses]
|
||||
|
||||
export type PutWorkspacesCurrentAgentsByAgentIdSkillsData = {
|
||||
body: AgentSkillBindingsPayload
|
||||
path: {
|
||||
agent_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/agents/{agent_id}/skills'
|
||||
}
|
||||
|
||||
export type PutWorkspacesCurrentAgentsByAgentIdSkillsResponses = {
|
||||
200: AgentSkillBindingsResponse
|
||||
}
|
||||
|
||||
export type PutWorkspacesCurrentAgentsByAgentIdSkillsResponse =
|
||||
PutWorkspacesCurrentAgentsByAgentIdSkillsResponses[keyof PutWorkspacesCurrentAgentsByAgentIdSkillsResponses]
|
||||
|
||||
export type GetWorkspacesCurrentCustomizedSnippetsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -4625,6 +4928,369 @@ export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses = {
|
||||
export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse =
|
||||
GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses[keyof GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
keyword?: string
|
||||
limit?: number
|
||||
page?: number
|
||||
tag?: Array<string>
|
||||
}
|
||||
url: '/workspaces/current/skills'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsResponses = {
|
||||
200: SkillListResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsResponse =
|
||||
GetWorkspacesCurrentSkillsResponses[keyof GetWorkspacesCurrentSkillsResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsData = {
|
||||
body: SkillCreatePayload
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/skills'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsResponses = {
|
||||
201: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsResponse =
|
||||
PostWorkspacesCurrentSkillsResponses[keyof PostWorkspacesCurrentSkillsResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsFilesUploadData = {
|
||||
body: {
|
||||
file: Blob | File
|
||||
}
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/files/upload'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsFilesUploadResponses = {
|
||||
201: SkillFileUploadResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsFilesUploadResponse =
|
||||
PostWorkspacesCurrentSkillsFilesUploadResponses[keyof PostWorkspacesCurrentSkillsFilesUploadResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsImportData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/import'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsImportResponses = {
|
||||
201: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsImportResponse =
|
||||
PostWorkspacesCurrentSkillsImportResponses[keyof PostWorkspacesCurrentSkillsImportResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsTagsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/tags'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsTagsResponses = {
|
||||
200: SkillTagListResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsTagsResponse =
|
||||
GetWorkspacesCurrentSkillsTagsResponses[keyof GetWorkspacesCurrentSkillsTagsResponses]
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdData = {
|
||||
body: SkillDeletePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}'
|
||||
}
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdResponses = {
|
||||
200: SkillDeleteResponse
|
||||
}
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdResponse =
|
||||
DeleteWorkspacesCurrentSkillsBySkillIdResponses[keyof DeleteWorkspacesCurrentSkillsBySkillIdResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdResponses = {
|
||||
200: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdResponses[keyof GetWorkspacesCurrentSkillsBySkillIdResponses]
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdData = {
|
||||
body: SkillMetadataPayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}'
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdResponses = {
|
||||
200: SkillResponse
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdResponse =
|
||||
PatchWorkspacesCurrentSkillsBySkillIdResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesData = {
|
||||
body: SkillAssistMessagePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/assist/messages'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses = {
|
||||
200: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponse =
|
||||
PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses[keyof PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/duplicate'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses = {
|
||||
201: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateResponse =
|
||||
PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses[keyof PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdExportData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/export'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdExportResponses = {
|
||||
200: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdExportResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdExportResponses[keyof GetWorkspacesCurrentSkillsBySkillIdExportResponses]
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdFilesData = {
|
||||
body: SkillDraftFileOperationPayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/files'
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdFilesResponses = {
|
||||
200: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdFilesResponse =
|
||||
PatchWorkspacesCurrentSkillsBySkillIdFilesResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdFilesResponses]
|
||||
|
||||
export type PutWorkspacesCurrentSkillsBySkillIdFilesData = {
|
||||
body: SkillDraftTreePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/files'
|
||||
}
|
||||
|
||||
export type PutWorkspacesCurrentSkillsBySkillIdFilesResponses = {
|
||||
200: SkillDetailResponse
|
||||
}
|
||||
|
||||
export type PutWorkspacesCurrentSkillsBySkillIdFilesResponse =
|
||||
PutWorkspacesCurrentSkillsBySkillIdFilesResponses[keyof PutWorkspacesCurrentSkillsBySkillIdFilesResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query: {
|
||||
download?: string
|
||||
path: string
|
||||
version_id?: string
|
||||
}
|
||||
url: '/workspaces/current/skills/{skill_id}/files/content'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses = {
|
||||
200: BinaryFileResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses[keyof GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query: {
|
||||
path: string
|
||||
version_id?: string
|
||||
}
|
||||
url: '/workspaces/current/skills/{skill_id}/files/preview'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses = {
|
||||
200: SkillFilePreviewResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses[keyof GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdPublishData = {
|
||||
body: SkillPublishPayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/publish'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdPublishResponses = {
|
||||
200: SkillVersionResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdPublishResponse =
|
||||
PostWorkspacesCurrentSkillsBySkillIdPublishResponses[keyof PostWorkspacesCurrentSkillsBySkillIdPublishResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdReferencesData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/references'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdReferencesResponses = {
|
||||
200: SkillReferenceListResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdReferencesResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdReferencesResponses[keyof GetWorkspacesCurrentSkillsBySkillIdReferencesResponses]
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdRestoreData = {
|
||||
body: SkillRestorePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/restore'
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdRestoreResponses = {
|
||||
200: SkillVersionResponse
|
||||
}
|
||||
|
||||
export type PostWorkspacesCurrentSkillsBySkillIdRestoreResponse =
|
||||
PostWorkspacesCurrentSkillsBySkillIdRestoreResponses[keyof PostWorkspacesCurrentSkillsBySkillIdRestoreResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/versions'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsResponses = {
|
||||
200: SkillVersionListResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdVersionsResponses[keyof GetWorkspacesCurrentSkillsBySkillIdVersionsResponses]
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
version_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
|
||||
}
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
|
||||
200: SkillVersionDeleteResponse
|
||||
}
|
||||
|
||||
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
|
||||
body?: never
|
||||
path: {
|
||||
skill_id: string
|
||||
version_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
|
||||
200: SkillVersionDetailResponse
|
||||
}
|
||||
|
||||
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
|
||||
body: SkillVersionUpdatePayload
|
||||
path: {
|
||||
skill_id: string
|
||||
version_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
|
||||
200: SkillVersionResponse
|
||||
}
|
||||
|
||||
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
|
||||
|
||||
export type GetWorkspacesCurrentToolLabelsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
@@ -12,6 +12,13 @@ export const zAgentProviderResponse = z.record(z.string(), z.unknown())
|
||||
*/
|
||||
export const zAgentProviderListResponse = z.array(z.record(z.string(), z.unknown()))
|
||||
|
||||
/**
|
||||
* AgentSkillBindingsPayload
|
||||
*/
|
||||
export const zAgentSkillBindingsPayload = z.object({
|
||||
skill_ids: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SnippetImportPayload
|
||||
*
|
||||
@@ -436,6 +443,155 @@ export const zReplaceBindingsRequest = z.object({
|
||||
role_ids: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillCreatePayload
|
||||
*/
|
||||
export const zSkillCreatePayload = z.object({
|
||||
description: z.string().optional().default(''),
|
||||
display_name: z.string().nullish(),
|
||||
icon: z.string().optional().default('📄'),
|
||||
name: z.string().nullish(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFileUploadResponse
|
||||
*/
|
||||
export const zSkillFileUploadResponse = z.object({
|
||||
hash: z.string(),
|
||||
id: z.string(),
|
||||
mime_type: z.string(),
|
||||
name: z.string(),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDeletePayload
|
||||
*/
|
||||
export const zSkillDeletePayload = z.object({
|
||||
confirmation_name: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDeleteResponse
|
||||
*/
|
||||
export const zSkillDeleteResponse = z.object({
|
||||
deleted: z.boolean(),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillMetadataPayload
|
||||
*/
|
||||
export const zSkillMetadataPayload = z.object({
|
||||
display_name: z.string().nullish(),
|
||||
expected_updated_at: z.int().nullish(),
|
||||
icon: z.string().nullish(),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillResponse
|
||||
*/
|
||||
export const zSkillResponse = z.object({
|
||||
created_at: z.int(),
|
||||
created_by: z.string().nullish(),
|
||||
created_by_name: z.string().nullish(),
|
||||
description: z.string(),
|
||||
display_name: z.string(),
|
||||
icon: z.string(),
|
||||
id: z.string(),
|
||||
latest_published_version_id: z.string().nullish(),
|
||||
name: z.string(),
|
||||
name_manually_edited: z.boolean().optional().default(false),
|
||||
reference_count: z.int().optional().default(0),
|
||||
tags: z.array(z.string()).optional(),
|
||||
updated_at: z.int(),
|
||||
updated_by: z.string().nullish(),
|
||||
updated_by_name: z.string().nullish(),
|
||||
visibility: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillListResponse
|
||||
*/
|
||||
export const zSkillListResponse = z.object({
|
||||
data: z.array(zSkillResponse).optional(),
|
||||
has_more: z.boolean().optional().default(false),
|
||||
limit: z.int().optional().default(20),
|
||||
page: z.int().optional().default(1),
|
||||
total: z.int().optional().default(0),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFilePreviewResponse
|
||||
*/
|
||||
export const zSkillFilePreviewResponse = z.object({
|
||||
content: z.string(),
|
||||
hash: z.string(),
|
||||
mime_type: z.string(),
|
||||
path: z.string(),
|
||||
size: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillPublishPayload
|
||||
*/
|
||||
export const zSkillPublishPayload = z.object({
|
||||
publish_note: z.string().max(1024).optional().default(''),
|
||||
version_name: z.string().max(128).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionResponse
|
||||
*/
|
||||
export const zSkillVersionResponse = z.object({
|
||||
archive_size: z.int(),
|
||||
created_at: z.int(),
|
||||
hash_code: z.string(),
|
||||
id: z.string(),
|
||||
is_latest: z.boolean().optional().default(false),
|
||||
publish_note: z.string(),
|
||||
published_by: z.string().nullish(),
|
||||
published_by_name: z.string().nullish(),
|
||||
skill_id: z.string(),
|
||||
version_name: z.string(),
|
||||
version_number: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillRestorePayload
|
||||
*/
|
||||
export const zSkillRestorePayload = z.object({
|
||||
publish_note: z.string().max(1024).optional().default(''),
|
||||
version_id: z.string(),
|
||||
version_name: z.string().max(128).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionListResponse
|
||||
*/
|
||||
export const zSkillVersionListResponse = z.object({
|
||||
data: z.array(zSkillVersionResponse).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionDeleteResponse
|
||||
*/
|
||||
export const zSkillVersionDeleteResponse = z.object({
|
||||
deleted: z.boolean(),
|
||||
id: z.string(),
|
||||
latest_published_version_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionUpdatePayload
|
||||
*/
|
||||
export const zSkillVersionUpdatePayload = z.object({
|
||||
publish_note: z.string().max(1024).optional().default(''),
|
||||
version_name: z.string().max(128).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ApiToolProviderDeletePayload
|
||||
*/
|
||||
@@ -656,6 +812,33 @@ export const zSwitchWorkspaceResponse = z.object({
|
||||
result: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AgentSkillBindingItemResponse
|
||||
*/
|
||||
export const zAgentSkillBindingItemResponse = z.object({
|
||||
description: z.string(),
|
||||
display_name: z.string(),
|
||||
file_count: z.int(),
|
||||
icon: z.string(),
|
||||
id: z.string(),
|
||||
latest_published_at: z.int().nullish(),
|
||||
latest_published_version_id: z.string().nullish(),
|
||||
name: z.string(),
|
||||
priority: z.int(),
|
||||
status: z.string(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
updated_at: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AgentSkillBindingsResponse
|
||||
*/
|
||||
export const zAgentSkillBindingsResponse = z.object({
|
||||
agent_id: z.string(),
|
||||
data: z.array(zAgentSkillBindingItemResponse).optional(),
|
||||
skill_ids: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* IconInfo
|
||||
*
|
||||
@@ -1244,6 +1427,164 @@ export const zWorkspaceAccessMatrix = z.object({
|
||||
pagination: zPagination.nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFileResponse
|
||||
*/
|
||||
export const zSkillFileResponse = z.object({
|
||||
content: z.string().nullish(),
|
||||
hash: z.string().nullish(),
|
||||
id: z.string().nullish(),
|
||||
kind: z.string(),
|
||||
mime_type: z.string().nullish(),
|
||||
path: z.string(),
|
||||
size: z.int().nullish(),
|
||||
storage: z.string().nullish(),
|
||||
tool_file_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDetailResponse
|
||||
*/
|
||||
export const zSkillDetailResponse = z.object({
|
||||
created_at: z.int(),
|
||||
created_by: z.string().nullish(),
|
||||
created_by_name: z.string().nullish(),
|
||||
description: z.string(),
|
||||
display_name: z.string(),
|
||||
files: z.array(zSkillFileResponse).optional(),
|
||||
icon: z.string(),
|
||||
id: z.string(),
|
||||
latest_published_version_id: z.string().nullish(),
|
||||
name: z.string(),
|
||||
name_manually_edited: z.boolean().optional().default(false),
|
||||
reference_count: z.int().optional().default(0),
|
||||
tags: z.array(z.string()).optional(),
|
||||
updated_at: z.int(),
|
||||
updated_by: z.string().nullish(),
|
||||
updated_by_name: z.string().nullish(),
|
||||
visibility: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillVersionDetailResponse
|
||||
*/
|
||||
export const zSkillVersionDetailResponse = z.object({
|
||||
archive_size: z.int(),
|
||||
created_at: z.int(),
|
||||
files: z.array(zSkillFileResponse).optional(),
|
||||
hash_code: z.string(),
|
||||
id: z.string(),
|
||||
is_latest: z.boolean().optional().default(false),
|
||||
publish_note: z.string(),
|
||||
published_by: z.string().nullish(),
|
||||
published_by_name: z.string().nullish(),
|
||||
skill_id: z.string(),
|
||||
version_name: z.string(),
|
||||
version_number: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillTagResponse
|
||||
*/
|
||||
export const zSkillTagResponse = z.object({
|
||||
count: z.int(),
|
||||
tag: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillTagListResponse
|
||||
*/
|
||||
export const zSkillTagListResponse = z.object({
|
||||
data: z.array(zSkillTagResponse).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillAssistAttachmentPayload
|
||||
*/
|
||||
export const zSkillAssistAttachmentPayload = z.object({
|
||||
mime_type: z.string().min(1).max(255).nullish(),
|
||||
name: z.string().min(1).max(255),
|
||||
size: z.int().gte(0).nullish(),
|
||||
tool_file_id: z.string().min(1),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillAssistModelPayload
|
||||
*/
|
||||
export const zSkillAssistModelPayload = z.object({
|
||||
model: z.string().min(1).max(255),
|
||||
model_settings: z.record(z.string(), z.unknown()).nullish(),
|
||||
plugin_id: z.string().min(1).max(255).nullish(),
|
||||
provider: z.string().min(1).max(255),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillAssistMessagePayload
|
||||
*
|
||||
* One user message and optional uploaded context for the read-only Skill Authoring assistant.
|
||||
*/
|
||||
export const zSkillAssistMessagePayload = z.object({
|
||||
attachments: z.array(zSkillAssistAttachmentPayload).max(10).optional(),
|
||||
message: z.string().min(1).max(8000),
|
||||
model: zSkillAssistModelPayload.nullish(),
|
||||
target_path: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDraftFileOperation
|
||||
*/
|
||||
export const zSkillDraftFileOperation = z.enum([
|
||||
'delete',
|
||||
'mkdir',
|
||||
'rename',
|
||||
'upsert_text',
|
||||
'upsert_tool_file',
|
||||
])
|
||||
|
||||
/**
|
||||
* SkillDraftFileOperationPayload
|
||||
*/
|
||||
export const zSkillDraftFileOperationPayload = z.object({
|
||||
content: z.string().nullish(),
|
||||
expected_updated_at: z.int().nullish(),
|
||||
hash: z.string().nullish(),
|
||||
mime_type: z.string().nullish(),
|
||||
operation: zSkillDraftFileOperation,
|
||||
path: z.string(),
|
||||
size: z.int().gte(0).nullish(),
|
||||
target_path: z.string().nullish(),
|
||||
tool_file_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillReferenceResponse
|
||||
*/
|
||||
export const zSkillReferenceResponse = z.object({
|
||||
agent_id: z.string(),
|
||||
agent_icon: z.string().nullish(),
|
||||
agent_icon_background: z.string().nullish(),
|
||||
agent_icon_type: z.string().nullish(),
|
||||
app_id: z.string().nullish(),
|
||||
display_name: z.string(),
|
||||
name: z.string(),
|
||||
node_id: z.string().nullish(),
|
||||
node_name: z.string().nullish(),
|
||||
type: z.string(),
|
||||
workflow_icon: z.string().nullish(),
|
||||
workflow_icon_background: z.string().nullish(),
|
||||
workflow_icon_type: z.string().nullish(),
|
||||
workflow_id: z.string().nullish(),
|
||||
workflow_name: z.string().nullish(),
|
||||
workflow_version: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillReferenceListResponse
|
||||
*/
|
||||
export const zSkillReferenceListResponse = z.object({
|
||||
data: z.array(zSkillReferenceResponse).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ToolEmojiIcon
|
||||
*/
|
||||
@@ -2096,6 +2437,42 @@ export const zPermissionCatalogResponse = z.object({
|
||||
groups: z.array(zPermissionCatalogGroup).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillFileKind
|
||||
*
|
||||
* Draft file entry kind.
|
||||
*/
|
||||
export const zSkillFileKind = z.enum(['directory', 'file'])
|
||||
|
||||
/**
|
||||
* SkillFileStorage
|
||||
*
|
||||
* How a draft file's content is stored.
|
||||
*/
|
||||
export const zSkillFileStorage = z.enum(['text', 'tool_file'])
|
||||
|
||||
/**
|
||||
* SkillDraftTreeItemPayload
|
||||
*/
|
||||
export const zSkillDraftTreeItemPayload = z.object({
|
||||
content: z.string().nullish(),
|
||||
hash: z.string().nullish(),
|
||||
kind: zSkillFileKind.optional().default('file'),
|
||||
mime_type: z.string().nullish(),
|
||||
path: z.string(),
|
||||
size: z.int().gte(0).nullish(),
|
||||
storage: zSkillFileStorage.nullish(),
|
||||
tool_file_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SkillDraftTreePayload
|
||||
*/
|
||||
export const zSkillDraftTreePayload = z.object({
|
||||
expected_updated_at: z.int().nullish(),
|
||||
files: z.array(zSkillDraftTreeItemPayload).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Option
|
||||
*/
|
||||
@@ -3421,6 +3798,26 @@ export const zGetWorkspacesCurrentAgentProviderByProviderNameResponse = zAgentPr
|
||||
*/
|
||||
export const zGetWorkspacesCurrentAgentProvidersResponse = zAgentProviderListResponse
|
||||
|
||||
export const zGetWorkspacesCurrentAgentsByAgentIdSkillsPath = z.object({
|
||||
agent_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Agent Skill bindings
|
||||
*/
|
||||
export const zGetWorkspacesCurrentAgentsByAgentIdSkillsResponse = zAgentSkillBindingsResponse
|
||||
|
||||
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsBody = zAgentSkillBindingsPayload
|
||||
|
||||
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsPath = z.object({
|
||||
agent_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Agent Skill bindings replaced
|
||||
*/
|
||||
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsResponse = zAgentSkillBindingsResponse
|
||||
|
||||
export const zGetWorkspacesCurrentCustomizedSnippetsQuery = z.object({
|
||||
creators: z.array(z.string()).optional(),
|
||||
is_published: z.boolean().optional(),
|
||||
@@ -4694,6 +5091,234 @@ export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdR
|
||||
*/
|
||||
export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse = zWorkspaceAccessMatrix
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsQuery = z.object({
|
||||
keyword: z.string().optional(),
|
||||
limit: z.int().gte(1).lte(100).optional().default(20),
|
||||
page: z.int().gte(1).lte(99999).optional().default(1),
|
||||
tag: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Workspace skills
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsResponse = zSkillListResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBody = zSkillCreatePayload
|
||||
|
||||
/**
|
||||
* Skill created
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsResponse = zSkillDetailResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsFilesUploadBody = z.object({
|
||||
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill draft file uploaded
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsFilesUploadResponse = zSkillFileUploadResponse
|
||||
|
||||
/**
|
||||
* Skill imported
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsImportResponse = zSkillDetailResponse
|
||||
|
||||
/**
|
||||
* Workspace Skill tags
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsTagsResponse = zSkillTagListResponse
|
||||
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdBody = zSkillDeletePayload
|
||||
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill deleted
|
||||
*/
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdResponse = zSkillDeleteResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill detail
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdResponse = zSkillDetailResponse
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdBody = zSkillMetadataPayload
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill updated
|
||||
*/
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdResponse = zSkillResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesBody = zSkillAssistMessagePayload
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill Authoring assistant event stream
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponse = z.record(
|
||||
z.string(),
|
||||
z.unknown(),
|
||||
)
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdDuplicatePath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill duplicated
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdDuplicateResponse = zSkillDetailResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdExportPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Published Skill zip archive
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdExportResponse = z.record(z.string(), z.unknown())
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesBody = zSkillDraftFileOperationPayload
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Draft file operation applied
|
||||
*/
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesResponse = zSkillDetailResponse
|
||||
|
||||
export const zPutWorkspacesCurrentSkillsBySkillIdFilesBody = zSkillDraftTreePayload
|
||||
|
||||
export const zPutWorkspacesCurrentSkillsBySkillIdFilesPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Draft files replaced
|
||||
*/
|
||||
export const zPutWorkspacesCurrentSkillsBySkillIdFilesResponse = zSkillDetailResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentQuery = z.object({
|
||||
download: z.string().optional(),
|
||||
path: z.string(),
|
||||
version_id: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill file content
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentResponse = zBinaryFileResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewQuery = z.object({
|
||||
path: z.string(),
|
||||
version_id: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill file text preview
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse = zSkillFilePreviewResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdPublishBody = zSkillPublishPayload
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdPublishPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill published
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdPublishResponse = zSkillVersionResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdReferencesPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill references
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdReferencesResponse = zSkillReferenceListResponse
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdRestoreBody = zSkillRestorePayload
|
||||
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdRestorePath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill version restored
|
||||
*/
|
||||
export const zPostWorkspacesCurrentSkillsBySkillIdRestoreResponse = zSkillVersionResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsPath = z.object({
|
||||
skill_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill versions
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsResponse = zSkillVersionListResponse
|
||||
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
version_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill version deleted
|
||||
*/
|
||||
export const zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
zSkillVersionDeleteResponse
|
||||
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
version_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill version detail
|
||||
*/
|
||||
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
zSkillVersionDetailResponse
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdBody =
|
||||
zSkillVersionUpdatePayload
|
||||
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
|
||||
skill_id: z.string(),
|
||||
version_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill version updated
|
||||
*/
|
||||
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
|
||||
zSkillVersionResponse
|
||||
|
||||
/**
|
||||
* Tool labels retrieved successfully
|
||||
*/
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -245,13 +245,13 @@ def extract_meta_variables(raw_match: dict[str, Any]) -> dict[str, str]:
|
||||
return result
|
||||
|
||||
|
||||
def has_reasoned_guard_ignore(source_line: str, rule_id: str) -> bool:
|
||||
pattern = re.compile(rf"# guard-ignore: {re.escape(rule_id)} -- (?P<reason>\S.*)\s*$")
|
||||
def has_reasoned_noqa(source_line: str, rule_id: str) -> bool:
|
||||
pattern = re.compile(rf"# noqa: {re.escape(rule_id)}(?:\s+(?P<reason>\S.*))?\s*$")
|
||||
match = pattern.search(source_line)
|
||||
if not match:
|
||||
return False
|
||||
reason = match.group("reason")
|
||||
return bool(reason.strip())
|
||||
return reason is not None and bool(reason.strip())
|
||||
|
||||
|
||||
def collect_hunk_violations(
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from ast_grep_guard import Match, has_reasoned_guard_ignore, rule_path, run_guard
|
||||
from ast_grep_guard import Match, has_reasoned_noqa, rule_path, run_guard
|
||||
|
||||
|
||||
RULE_ID = "no-new-controller-sqlalchemy"
|
||||
@@ -40,7 +40,7 @@ def is_flask_session_get(match: Match) -> bool:
|
||||
|
||||
|
||||
def is_suppressed(match: Match) -> bool:
|
||||
return has_reasoned_guard_ignore(match.source_line, RULE_ID)
|
||||
return has_reasoned_noqa(match.source_line, RULE_ID)
|
||||
|
||||
|
||||
def is_reportable_match(match: Match) -> bool:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ast_grep_guard import Match, has_reasoned_guard_ignore, is_python_source_path, rule_path, run_guard
|
||||
from ast_grep_guard import Match, has_reasoned_noqa, is_python_source_path, rule_path, run_guard
|
||||
|
||||
|
||||
RULE_ID = "no-new-getattr"
|
||||
@@ -12,7 +12,7 @@ VIOLATION_MESSAGE = "no-new-getattr net-new getattr() in added code"
|
||||
|
||||
|
||||
def is_reportable_match(match: Match) -> bool:
|
||||
return not has_reasoned_guard_ignore(match.source_line, RULE_ID)
|
||||
return not has_reasoned_noqa(match.source_line, RULE_ID)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
@@ -44,7 +44,7 @@ def parse_args() -> argparse.Namespace:
|
||||
help="Files or directories to scan. Defaults to api/controllers.",
|
||||
)
|
||||
parser.add_argument("--include-allowed", action="store_true", help="Print allowed flush()/commit() findings.")
|
||||
parser.add_argument("--include-suppressed", action="store_true", help="Print reasoned guard suppressions.")
|
||||
parser.add_argument("--include-suppressed", action="store_true", help="Print reasoned noqa suppressions.")
|
||||
parser.add_argument("--summary-only", action="store_true", help="Print counts without per-finding details.")
|
||||
parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON.")
|
||||
parser.add_argument(
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { AppDetailSidebarSlot } from '../sidebar-page'
|
||||
|
||||
export default function AppAccessPointDetailSidebarSlot() {
|
||||
return <AppDetailSidebarSlot />
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { AppDetailSidebarSlot } from '../sidebar-page'
|
||||
|
||||
export default function AppDeployDetailSidebarSlot() {
|
||||
return <AppDetailSidebarSlot />
|
||||
}
|
||||
@@ -1,20 +1,10 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Dependency } from '@/app/components/plugins/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { useStore as usePluginDependencyStore } from '@/app/components/workflow/plugin-dependency/store'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
guardAgentV2Route: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/install-plugin/install-bundle', () => ({
|
||||
default: ({ fromDSLPayload }: { fromDSLPayload: Dependency[] }) => (
|
||||
<div role="dialog" aria-label="Install missing plugins">
|
||||
{`bundle-size:${fromDSLPayload.length}`}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../feature-guard', () => ({
|
||||
guardAgentV2Route: () => mocks.guardAgentV2Route(),
|
||||
}))
|
||||
@@ -28,7 +18,6 @@ vi.mock('../agents-access-guard', () => ({
|
||||
describe('RosterLayout', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
usePluginDependencyStore.setState({ dependencies: [] })
|
||||
})
|
||||
|
||||
it('should render children when Agent v2 is enabled', async () => {
|
||||
@@ -44,34 +33,6 @@ describe('RosterLayout', () => {
|
||||
expect(screen.getByText('Roster content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show the missing-plugin installer across Agent routes', async () => {
|
||||
usePluginDependencyStore.setState({
|
||||
dependencies: [
|
||||
{
|
||||
type: 'marketplace',
|
||||
value: {
|
||||
organization: 'langgenius',
|
||||
plugin: 'sample-plugin',
|
||||
version: '1.0.0',
|
||||
plugin_unique_identifier: 'langgenius/sample-plugin:1.0.0',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
const { default: RosterLayout } = await import('../layout')
|
||||
|
||||
render(
|
||||
<RosterLayout>
|
||||
<div>Agent route content</div>
|
||||
</RosterLayout>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('dialog', { name: 'Install missing plugins' })).toHaveTextContent(
|
||||
'bundle-size:1',
|
||||
)
|
||||
expect(screen.getByText('Agent route content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should block rendering when the roster guard throws notFound', async () => {
|
||||
mocks.guardAgentV2Route.mockImplementation(() => {
|
||||
throw new Error('NEXT_NOT_FOUND')
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import PluginDependency from '@/app/components/workflow/plugin-dependency'
|
||||
import { AgentsAccessGuard } from './agents-access-guard'
|
||||
import { guardAgentV2Route } from './feature-guard'
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
guardAgentV2Route()
|
||||
|
||||
return (
|
||||
<AgentsAccessGuard>
|
||||
<PluginDependency />
|
||||
{children}
|
||||
</AgentsAccessGuard>
|
||||
)
|
||||
return <AgentsAccessGuard>{children}</AgentsAccessGuard>
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function AppAccessPointPage() {
|
||||
return null
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function AppDeployPage() {
|
||||
return null
|
||||
}
|
||||
@@ -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 />
|
||||
}
|
||||
@@ -7,12 +7,10 @@ let mockAppMode = 'chat'
|
||||
let mockPathname = '/app/app-1/logs'
|
||||
let mockAppPermissionKeys: string[] = []
|
||||
let mockIsRbacEnabled = true
|
||||
let mockEnableAppDeploy = false
|
||||
const mockConsoleState = vi.hoisted(() => ({
|
||||
current: {
|
||||
userProfile: { id: 'user-1' },
|
||||
workspacePermissionKeys: [] as string[],
|
||||
isCurrentWorkspaceEditor: true,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -20,7 +18,6 @@ const render = (ui: Parameters<typeof renderWithConsoleQuery>[0]) =>
|
||||
renderWithConsoleQuery(ui, {
|
||||
systemFeatures: {
|
||||
rbac_enabled: mockIsRbacEnabled,
|
||||
enable_app_deploy: mockEnableAppDeploy,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -47,10 +44,6 @@ vi.mock('@/context/permission-state', async () => {
|
||||
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
return createPermissionStateModuleMock(() => mockConsoleState.current)
|
||||
})
|
||||
vi.mock('@/context/workspace-state', async () => {
|
||||
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
return createWorkspaceStateModuleMock(() => mockConsoleState.current)
|
||||
})
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
usePathname: () => mockPathname,
|
||||
@@ -79,8 +72,6 @@ describe('AppDetailSection', () => {
|
||||
mockPathname = '/app/app-1/logs'
|
||||
mockAppPermissionKeys = [AppACLPermission.Monitor]
|
||||
mockIsRbacEnabled = true
|
||||
mockEnableAppDeploy = false
|
||||
mockConsoleState.current.isCurrentWorkspaceEditor = true
|
||||
})
|
||||
|
||||
// Rendering behavior for app detail navigation entries.
|
||||
@@ -192,72 +183,6 @@ describe('AppDetailSection', () => {
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render access point navigation using its app route', () => {
|
||||
// Act
|
||||
render(<AppDetailSection />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('link', { name: 'common.appMenus.accessPoint' })).toHaveAttribute(
|
||||
'href',
|
||||
'/app/app-1/access-point',
|
||||
)
|
||||
expect(
|
||||
screen.queryByRole('link', { name: 'common.appMenus.apiAccess' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render deploy navigation for workflow apps when app deploy is available', () => {
|
||||
// Arrange
|
||||
mockAppMode = 'workflow'
|
||||
mockEnableAppDeploy = true
|
||||
|
||||
// Act
|
||||
render(<AppDetailSection />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('link', { name: 'common.appMenus.deploy' })).toHaveAttribute(
|
||||
'href',
|
||||
'/app/app-1/deploy',
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: 'the app is not a workflow app',
|
||||
mode: 'chat',
|
||||
enableAppDeploy: true,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
},
|
||||
{
|
||||
label: 'app deploy is disabled',
|
||||
mode: 'workflow',
|
||||
enableAppDeploy: false,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
},
|
||||
{
|
||||
label: 'the current workspace role cannot use app deploy',
|
||||
mode: 'workflow',
|
||||
enableAppDeploy: true,
|
||||
isCurrentWorkspaceEditor: false,
|
||||
},
|
||||
])(
|
||||
'should hide deploy navigation when $label',
|
||||
({ mode, enableAppDeploy, isCurrentWorkspaceEditor }) => {
|
||||
// Arrange
|
||||
mockAppMode = mode
|
||||
mockEnableAppDeploy = enableAppDeploy
|
||||
mockConsoleState.current.isCurrentWorkspaceEditor = isCurrentWorkspaceEditor
|
||||
|
||||
// Act
|
||||
render(<AppDetailSection />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.queryByRole('link', { name: 'common.appMenus.deploy' }),
|
||||
).not.toBeInTheDocument()
|
||||
},
|
||||
)
|
||||
|
||||
it('should render resource access navigation when app access config permission is granted', () => {
|
||||
// Arrange
|
||||
mockAppPermissionKeys = [AppACLPermission.AccessConfig]
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { NavIcon } from './nav-link'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiDashboard2Fill,
|
||||
RiDashboard2Line,
|
||||
RiFileList3Fill,
|
||||
RiFileList3Line,
|
||||
RiLock2Fill,
|
||||
RiLock2Line,
|
||||
RiTerminalBoxFill,
|
||||
RiTerminalBoxLine,
|
||||
RiTerminalWindowFill,
|
||||
RiTerminalWindowLine,
|
||||
} from '@remixicon/react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { Fragment, useMemo } from 'react'
|
||||
@@ -12,7 +24,6 @@ import Divider from '@/app/components/base/divider'
|
||||
import Annotations from '@/app/components/base/icons/src/vender/Annotations'
|
||||
import { userProfileIdAtom } from '@/context/account-state'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { isCurrentWorkspaceEditorAtom } from '@/context/workspace-state'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@@ -34,28 +45,6 @@ const AnnotationNavIcon = ({ className, ...props }: ComponentProps<typeof Annota
|
||||
|
||||
AnnotationNavIcon.displayName = 'Annotations'
|
||||
|
||||
const createClassNameNavIcon = (iconClassName: string) => {
|
||||
const ClassNameNavIcon = ({ className }: ComponentProps<'svg'>) => (
|
||||
<span aria-hidden className={cn(iconClassName, className)} />
|
||||
)
|
||||
|
||||
ClassNameNavIcon.displayName = 'ClassNameNavIcon'
|
||||
|
||||
return ClassNameNavIcon
|
||||
}
|
||||
|
||||
const accessPointNavIcon = createClassNameNavIcon('i-custom-vender-agent-v2-access-point')
|
||||
const terminalWindowLineNavIcon = createClassNameNavIcon('i-ri-terminal-window-line')
|
||||
const terminalWindowFillNavIcon = createClassNameNavIcon('i-ri-terminal-window-fill')
|
||||
const instanceLineNavIcon = createClassNameNavIcon('i-ri-instance-line')
|
||||
const instanceFillNavIcon = createClassNameNavIcon('i-ri-instance-fill')
|
||||
const fileListLineNavIcon = createClassNameNavIcon('i-ri-file-list-3-line')
|
||||
const fileListFillNavIcon = createClassNameNavIcon('i-ri-file-list-3-fill')
|
||||
const dashboardLineNavIcon = createClassNameNavIcon('i-ri-dashboard-2-line')
|
||||
const dashboardFillNavIcon = createClassNameNavIcon('i-ri-dashboard-2-fill')
|
||||
const lockLineNavIcon = createClassNameNavIcon('i-ri-lock-2-line')
|
||||
const lockFillNavIcon = createClassNameNavIcon('i-ri-lock-2-fill')
|
||||
|
||||
const isLogsNavItem = (item: AppDetailNavItem) => item.href.endsWith('/logs')
|
||||
const isAnnotationsNavItem = (item: AppDetailNavItem) => item.href.endsWith('/annotations')
|
||||
|
||||
@@ -84,7 +73,6 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const isCurrentWorkspaceEditor = useAtomValue(isCurrentWorkspaceEditorAtom)
|
||||
const isRbacEnabled = systemFeatures.rbac_enabled
|
||||
const appDetail = useStore((state) => state.appDetail)
|
||||
const appInfoActions = useAppInfoActions({
|
||||
@@ -97,7 +85,6 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
|
||||
const appId = appDetail.id
|
||||
const isWorkflowApp =
|
||||
appDetail.mode === AppModeEnum.WORKFLOW || appDetail.mode === AppModeEnum.ADVANCED_CHAT
|
||||
const supportsAppDeploy = appDetail.mode === AppModeEnum.WORKFLOW
|
||||
const supportsAnnotations =
|
||||
appDetail.mode !== AppModeEnum.WORKFLOW && appDetail.mode !== AppModeEnum.COMPLETION
|
||||
const appACLCapabilities = getAppACLCapabilities(appDetail.permission_keys, {
|
||||
@@ -113,34 +100,24 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
|
||||
{
|
||||
name: t(($) => $['appMenus.promptEng'], { ns: 'common' }),
|
||||
href: `/app/${appId}/${isWorkflowApp ? 'workflow' : 'configuration'}`,
|
||||
icon: terminalWindowLineNavIcon,
|
||||
selectedIcon: terminalWindowFillNavIcon,
|
||||
icon: RiTerminalWindowLine,
|
||||
selectedIcon: RiTerminalWindowFill,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: t(($) => $['appMenus.accessPoint'], { ns: 'common' }),
|
||||
href: `/app/${appId}/access-point`,
|
||||
icon: accessPointNavIcon,
|
||||
selectedIcon: accessPointNavIcon,
|
||||
name: t(($) => $['appMenus.apiAccess'], { ns: 'common' }),
|
||||
href: `/app/${appId}/develop`,
|
||||
icon: RiTerminalBoxLine,
|
||||
selectedIcon: RiTerminalBoxFill,
|
||||
},
|
||||
...(supportsAppDeploy && isCurrentWorkspaceEditor && systemFeatures.enable_app_deploy
|
||||
? [
|
||||
{
|
||||
name: t(($) => $['appMenus.deploy'], { ns: 'common' }),
|
||||
href: `/app/${appId}/deploy`,
|
||||
icon: instanceLineNavIcon,
|
||||
selectedIcon: instanceFillNavIcon,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appACLCapabilities.canAccessLogAndAnnotation
|
||||
? [
|
||||
{
|
||||
name: t(($) => $['appMenus.logs'], { ns: 'common' }),
|
||||
href: `/app/${appId}/logs`,
|
||||
icon: fileListLineNavIcon,
|
||||
selectedIcon: fileListFillNavIcon,
|
||||
icon: RiFileList3Line,
|
||||
selectedIcon: RiFileList3Fill,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -159,8 +136,8 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
|
||||
{
|
||||
name: t(($) => $['appMenus.overview'], { ns: 'common' }),
|
||||
href: `/app/${appId}/overview`,
|
||||
icon: dashboardLineNavIcon,
|
||||
selectedIcon: dashboardFillNavIcon,
|
||||
icon: RiDashboard2Line,
|
||||
selectedIcon: RiDashboard2Fill,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -169,21 +146,13 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
|
||||
{
|
||||
name: t(($) => $['settings.resourceAccess'], { ns: 'common' }),
|
||||
href: `/app/${appId}/access-config`,
|
||||
icon: lockLineNavIcon,
|
||||
selectedIcon: lockFillNavIcon,
|
||||
icon: RiLock2Line,
|
||||
selectedIcon: RiLock2Fill,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
}, [
|
||||
appDetail,
|
||||
t,
|
||||
currentUserId,
|
||||
workspacePermissionKeys,
|
||||
isCurrentWorkspaceEditor,
|
||||
isRbacEnabled,
|
||||
systemFeatures.enable_app_deploy,
|
||||
])
|
||||
}, [appDetail, t, currentUserId, workspacePermissionKeys, isRbacEnabled])
|
||||
|
||||
if (!appDetail) return null
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
}))
|
||||
|
||||
describe('FeaturesWrappedAppPublisher', () => {
|
||||
const resetAppConfig = vi.fn()
|
||||
const publishedConfig = {
|
||||
modelConfig: {
|
||||
more_like_this: { enabled: true },
|
||||
@@ -82,6 +81,7 @@ describe('FeaturesWrappedAppPublisher', () => {
|
||||
allowed_file_upload_methods: ['remote_url'],
|
||||
number_limits: 5,
|
||||
},
|
||||
resetAppConfig: vi.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -106,18 +106,13 @@ describe('FeaturesWrappedAppPublisher', () => {
|
||||
})
|
||||
|
||||
it('should restore published features after confirmation', async () => {
|
||||
render(
|
||||
<FeaturesWrappedAppPublisher
|
||||
publishedConfig={publishedConfig as any}
|
||||
resetAppConfig={resetAppConfig}
|
||||
/>,
|
||||
)
|
||||
render(<FeaturesWrappedAppPublisher publishedConfig={publishedConfig as any} />)
|
||||
|
||||
fireEvent.click(screen.getByText('restore-through-wrapper'))
|
||||
fireEvent.click(screen.getByRole('button', { name: /(?:^|\.)operation\.confirm(?=$|:)/ }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resetAppConfig).toHaveBeenCalledTimes(1)
|
||||
expect(publishedConfig.modelConfig.resetAppConfig).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetFeatures).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
moreLikeThis: { enabled: true },
|
||||
@@ -133,18 +128,12 @@ describe('FeaturesWrappedAppPublisher', () => {
|
||||
citation: { enabled: true },
|
||||
annotationReply: { enabled: true },
|
||||
}),
|
||||
{ silent: true },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should close restore confirmation without restoring when cancelled', async () => {
|
||||
render(
|
||||
<FeaturesWrappedAppPublisher
|
||||
publishedConfig={publishedConfig as any}
|
||||
resetAppConfig={resetAppConfig}
|
||||
/>,
|
||||
)
|
||||
render(<FeaturesWrappedAppPublisher publishedConfig={publishedConfig as any} />)
|
||||
|
||||
fireEvent.click(screen.getByText('restore-through-wrapper'))
|
||||
const dialog = screen.getByRole('alertdialog')
|
||||
@@ -156,7 +145,7 @@ describe('FeaturesWrappedAppPublisher', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(resetAppConfig).not.toHaveBeenCalled()
|
||||
expect(publishedConfig.modelConfig.resetAppConfig).not.toHaveBeenCalled()
|
||||
expect(mockSetFeatures).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/* oxlint-disable typescript/no-explicit-any */
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@@ -20,14 +18,12 @@ const mockSetAppDetail = vi.fn()
|
||||
const mockTrackEvent = vi.fn()
|
||||
const mockRefetch = vi.fn()
|
||||
const mockUseGetUserCanAccessApp = vi.fn()
|
||||
const mockOpenAsyncWindow = vi.fn()
|
||||
const mockFetchInstalledAppList = vi.fn()
|
||||
const mockFetchAppDetail = vi.fn()
|
||||
const mockToastError = vi.fn()
|
||||
const mockToastSuccess = vi.fn()
|
||||
const mockWindowOpen = vi.fn()
|
||||
const mockInvalidateAppWorkflow = vi.fn()
|
||||
const mockUpdateWorkflow = vi.fn()
|
||||
const mockFetchPublishedWorkflow = vi.fn()
|
||||
let mockPublishedWorkflow: Record<string, any> | null = null
|
||||
|
||||
const sectionProps = vi.hoisted(() => ({
|
||||
summary: null as null | Record<string, any>,
|
||||
@@ -38,20 +34,9 @@ const hotkeyMocks = vi.hoisted(() => ({
|
||||
hotkeys: [] as string[],
|
||||
handlers: [] as Array<(event: { preventDefault: () => void }) => void>,
|
||||
}))
|
||||
const collaborationMocks = vi.hoisted(() => ({
|
||||
handler: undefined as
|
||||
| ((update: {
|
||||
type: 'app_publish_update'
|
||||
userId: string
|
||||
data: Record<string, unknown>
|
||||
timestamp: number
|
||||
}) => void)
|
||||
| undefined,
|
||||
}))
|
||||
|
||||
let mockAppDetail: Record<string, any> | null = null
|
||||
let mockWorkspacePermissionKeys: string[] = ['tool.manage']
|
||||
let mockIsCurrentWorkspaceEditor = true
|
||||
|
||||
vi.mock('@tanstack/react-hotkeys', () => ({
|
||||
useHotkey: (hotkey: string, handler: (event: { preventDefault: () => void }) => void) => {
|
||||
@@ -79,6 +64,10 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-async-window-open', () => ({
|
||||
useAsyncWindowOpen: () => mockOpenAsyncWindow,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/access-control/use-app-access-control', () => ({
|
||||
useGetUserCanAccessApp: (params: unknown) => {
|
||||
mockUseGetUserCanAccessApp(params)
|
||||
@@ -94,6 +83,10 @@ vi.mock('@/service/access-control/use-app-access-control', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/explore', () => ({
|
||||
fetchInstalledAppList: (...args: unknown[]) => mockFetchInstalledAppList(...args),
|
||||
}))
|
||||
|
||||
const mockPublishToCreatorsPlatform = vi.fn()
|
||||
|
||||
vi.mock('@/service/apps', () => ({
|
||||
@@ -102,22 +95,7 @@ vi.mock('@/service/apps', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
appWorkflowQueryOptions: (appId: string) => ({
|
||||
queryKey: ['workflow', 'publish', appId],
|
||||
queryFn: () => mockFetchPublishedWorkflow(appId),
|
||||
}),
|
||||
useAppWorkflow: () => ({ data: mockPublishedWorkflow, isSuccess: true }),
|
||||
useInvalidateAppWorkflow: () => mockInvalidateAppWorkflow,
|
||||
useUpdateWorkflow: () => ({ mutate: mockUpdateWorkflow }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () => ({
|
||||
collaborationManager: {
|
||||
onAppPublishUpdate: (handler: NonNullable<(typeof collaborationMocks)['handler']>) => {
|
||||
collaborationMocks.handler = handler
|
||||
return vi.fn()
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
@@ -132,7 +110,6 @@ vi.mock('@/service/use-tools', () => ({
|
||||
vi.mock('@/context/workspace-state', async () => {
|
||||
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
return createWorkspaceStateModuleMock(() => ({
|
||||
isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor,
|
||||
isCurrentWorkspaceManager: true,
|
||||
workspacePermissionKeys: mockWorkspacePermissionKeys,
|
||||
}))
|
||||
@@ -148,7 +125,6 @@ vi.mock('@/context/permission-state', async () => {
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: {
|
||||
error: (...args: unknown[]) => mockToastError(...args),
|
||||
success: (...args: unknown[]) => mockToastSuccess(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -156,6 +132,16 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/overview/embedded', () => ({
|
||||
default: ({ isShow, onClose }: { isShow: boolean; onClose: () => void }) =>
|
||||
isShow ? (
|
||||
<div data-testid="embedded-modal">
|
||||
embedded modal
|
||||
<button onClick={onClose}>close-embedded-modal</button>
|
||||
</div>
|
||||
) : null,
|
||||
}))
|
||||
|
||||
vi.mock('../../app-access-control', () => {
|
||||
const MockAccessControl = ({
|
||||
onConfirm,
|
||||
@@ -194,7 +180,6 @@ vi.mock('../sections', () => ({
|
||||
<div>
|
||||
<button onClick={() => void props.handlePublish()}>publisher-summary-publish</button>
|
||||
<button onClick={() => void props.handleRestore()}>publisher-summary-restore</button>
|
||||
<button onClick={props.onEditVersion}>publisher-summary-edit-version</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -205,18 +190,18 @@ vi.mock('../sections', () => ({
|
||||
PublisherActionsSection: (props: Record<string, any>) => {
|
||||
sectionProps.actions = props
|
||||
return (
|
||||
<div data-testid="publisher-actions">
|
||||
{props.showRunConfig && props.handleOpenRunConfig && (
|
||||
<button onClick={() => props.handleOpenRunConfig(props.appURL)}>
|
||||
publisher-run-config
|
||||
</button>
|
||||
)}
|
||||
{props.showMarketplaceAction && (
|
||||
<button disabled={props.marketplaceActionDisabled} onClick={props.onPublishToMarketplace}>
|
||||
{props.publishingToMarketplace
|
||||
? 'workflow.common.publishingToMarketplace'
|
||||
: 'workflow.common.publishToMarketplace'}
|
||||
</button>
|
||||
<div>
|
||||
<button onClick={props.handleEmbed}>publisher-embed</button>
|
||||
<button onClick={() => void props.handleOpenInExplore()}>publisher-open-in-explore</button>
|
||||
{props.handleOpenRunConfig && (
|
||||
<>
|
||||
<button onClick={() => props.handleOpenRunConfig(props.appURL)}>
|
||||
publisher-run-config
|
||||
</button>
|
||||
<button onClick={() => props.handleOpenRunConfig(`${props.appURL}?mode=batch`)}>
|
||||
publisher-batch-run-config
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button onClick={props.onConfigureWorkflowTool}>publisher-workflow-tool</button>
|
||||
</div>
|
||||
@@ -229,14 +214,10 @@ describe('AppPublisher', () => {
|
||||
vi.clearAllMocks()
|
||||
hotkeyMocks.hotkeys.length = 0
|
||||
hotkeyMocks.handlers.length = 0
|
||||
collaborationMocks.handler = undefined
|
||||
sectionProps.summary = null
|
||||
sectionProps.access = null
|
||||
sectionProps.actions = null
|
||||
mockPublishedWorkflow = null
|
||||
mockFetchPublishedWorkflow.mockResolvedValue(null)
|
||||
mockWorkspacePermissionKeys = ['tool.manage']
|
||||
mockIsCurrentWorkspaceEditor = true
|
||||
mockAppDetail = {
|
||||
id: 'app-1',
|
||||
name: 'Demo App',
|
||||
@@ -247,12 +228,17 @@ describe('AppPublisher', () => {
|
||||
access_token: 'token-1',
|
||||
},
|
||||
}
|
||||
mockFetchInstalledAppList.mockResolvedValue({
|
||||
installed_apps: [{ id: 'installed-1' }],
|
||||
})
|
||||
mockFetchAppDetail.mockResolvedValue({
|
||||
id: 'app-1',
|
||||
access_mode: AccessMode.PUBLIC,
|
||||
})
|
||||
mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise<string>) => {
|
||||
return resolver()
|
||||
})
|
||||
Object.defineProperty(window, 'open', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: mockWindowOpen,
|
||||
})
|
||||
@@ -296,74 +282,16 @@ describe('AppPublisher', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should edit the current workflow version from the publish summary', () => {
|
||||
mockAppDetail = {
|
||||
...mockAppDetail,
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
}
|
||||
mockPublishedWorkflow = {
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
id: 'workflow-version-5',
|
||||
marked_name: 'Release 5',
|
||||
marked_comment: 'Initial notes',
|
||||
}
|
||||
|
||||
it('should open the embedded modal from the actions section', () => {
|
||||
render(<AppPublisher publishedAt={Date.now()} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
expect(sectionProps.summary).toEqual(
|
||||
expect.objectContaining({
|
||||
isWorkflowApp: true,
|
||||
versionInfo: mockPublishedWorkflow,
|
||||
}),
|
||||
)
|
||||
fireEvent.click(screen.getByText('publisher-summary-edit-version'))
|
||||
fireEvent.click(screen.getByText('publisher-embed'))
|
||||
|
||||
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
|
||||
const [titleInput, notesInput] = screen.getAllByRole('textbox')
|
||||
fireEvent.change(titleInput!, { target: { value: 'Release 6' } })
|
||||
fireEvent.change(notesInput!, { target: { value: 'Updated notes' } })
|
||||
const publishButtons = screen.getAllByRole('button', {
|
||||
name: /(?:^|\.)common\.publish(?=$|:)/,
|
||||
})
|
||||
fireEvent.click(publishButtons.at(-1)!)
|
||||
|
||||
expect(mockUpdateWorkflow).toHaveBeenCalledWith(
|
||||
{
|
||||
url: '/apps/app-1/workflows/workflow-version-5',
|
||||
title: 'Release 6',
|
||||
releaseNotes: 'Updated notes',
|
||||
},
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
onError: expect.any(Function),
|
||||
onSettled: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
|
||||
const mutationCallbacks = mockUpdateWorkflow.mock.calls[0]![1]
|
||||
mutationCallbacks.onSuccess()
|
||||
expect(mockInvalidateAppWorkflow).toHaveBeenCalledWith('app-1')
|
||||
expect(mockToastSuccess).toHaveBeenCalled()
|
||||
expect(screen.getByTestId('embedded-modal'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose the Deploy quick link for editable workflow apps when enabled', () => {
|
||||
mockAppDetail = {
|
||||
...mockAppDetail,
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
}
|
||||
|
||||
renderWithConsoleQuery(<AppPublisher publishedAt={Date.now()} />, {
|
||||
systemFeatures: { webapp_auth: { enabled: true }, enable_app_deploy: true },
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
|
||||
expect(sectionProps.actions?.showDeployAction).toBe(true)
|
||||
expect(sectionProps.actions?.appURL).toContain('/workflow/token-1')
|
||||
})
|
||||
|
||||
it('should collect hidden inputs before opening the web app from its config action', async () => {
|
||||
it('should collect hidden inputs before opening published run links from config actions', async () => {
|
||||
render(
|
||||
<AppPublisher
|
||||
publishedAt={Date.now()}
|
||||
@@ -381,8 +309,6 @@ describe('AppPublisher', () => {
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
|
||||
expect(sectionProps.actions?.showRunConfig).toBe(true)
|
||||
fireEvent.click(screen.getByText('publisher-run-config'))
|
||||
|
||||
expect(
|
||||
@@ -404,21 +330,55 @@ describe('AppPublisher', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should open batch run config links with the configured hidden inputs', async () => {
|
||||
mockAppDetail = {
|
||||
...mockAppDetail,
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
}
|
||||
|
||||
render(
|
||||
<AppPublisher
|
||||
publishedAt={Date.now()}
|
||||
inputs={[
|
||||
{
|
||||
variable: 'batch_secret',
|
||||
label: 'Batch Secret',
|
||||
type: 'text-input',
|
||||
required: true,
|
||||
hide: true,
|
||||
default: '',
|
||||
} as any,
|
||||
]}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
fireEvent.click(screen.getByText('publisher-batch-run-config'))
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Batch Secret'), {
|
||||
target: { value: 'batch-value' },
|
||||
})
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: /(?:^|\.)overview\.appInfo\.launch(?=$|:)/ }),
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
`https://example.com${basePath}/workflow/token-1?mode=batch&batch_secret=${encodeURIComponent('batch-value')}`,
|
||||
'_blank',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep workflow tool drawer mounted after closing the publish popover', () => {
|
||||
mockAppDetail = {
|
||||
...mockAppDetail,
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
}
|
||||
|
||||
render(<AppPublisher publishedAt={Date.now()} toolPublished />)
|
||||
render(<AppPublisher publishedAt={Date.now()} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
expect(sectionProps.actions).toEqual(
|
||||
expect.objectContaining({
|
||||
toolPublished: true,
|
||||
workflowToolOutdated: false,
|
||||
}),
|
||||
)
|
||||
fireEvent.click(screen.getByText('publisher-workflow-tool'))
|
||||
|
||||
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
|
||||
@@ -441,9 +401,14 @@ describe('AppPublisher', () => {
|
||||
expect(sectionProps.actions?.workflowToolAvailable).toBe(false)
|
||||
})
|
||||
|
||||
it('should close access control through its child callback', async () => {
|
||||
it('should close embedded and access control panels through child callbacks', async () => {
|
||||
render(<AppPublisher publishedAt={Date.now()} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
fireEvent.click(screen.getByText('publisher-embed'))
|
||||
fireEvent.click(screen.getByText('close-embedded-modal'))
|
||||
expect(screen.queryByTestId('embedded-modal')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
fireEvent.click(screen.getByText('publisher-access-control'))
|
||||
expect(screen.getByTestId('access-control'))!.toBeInTheDocument()
|
||||
@@ -478,6 +443,25 @@ describe('AppPublisher', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should open the installed explore page through the async window helper', async () => {
|
||||
let openedUrl = ''
|
||||
mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise<string>) => {
|
||||
openedUrl = await resolver()
|
||||
})
|
||||
|
||||
render(<AppPublisher publishedAt={Date.now()} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
fireEvent.click(screen.getByText('publisher-open-in-explore'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOpenAsyncWindow).toHaveBeenCalledTimes(1)
|
||||
expect(mockFetchInstalledAppList).toHaveBeenCalledWith('app-1')
|
||||
expect(openedUrl).toBe('/installed/installed-1')
|
||||
expect(sectionProps.actions?.appURL).toBe(`https://example.com${basePath}/chat/token-1`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore the trigger when the publish button is disabled', () => {
|
||||
render(<AppPublisher disabled publishedAt={Date.now()} onToggle={mockOnToggle} />)
|
||||
|
||||
@@ -495,13 +479,8 @@ describe('AppPublisher', () => {
|
||||
const onRestore = vi.fn().mockResolvedValue(undefined)
|
||||
mockOnPublish.mockResolvedValue(undefined)
|
||||
|
||||
const { rerender } = render(
|
||||
<AppPublisher
|
||||
hasUnpublishedChanges
|
||||
publishedAt={Date.now()}
|
||||
onPublish={mockOnPublish}
|
||||
onRestore={onRestore}
|
||||
/>,
|
||||
render(
|
||||
<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} onRestore={onRestore} />,
|
||||
)
|
||||
|
||||
expect(hotkeyMocks.hotkeys).toContain('Mod+Shift+P')
|
||||
@@ -512,30 +491,6 @@ describe('AppPublisher', () => {
|
||||
expect(mockOnPublish).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
rerender(
|
||||
<AppPublisher
|
||||
hasUnpublishedChanges={false}
|
||||
publishedAt={Date.now()}
|
||||
onPublish={mockOnPublish}
|
||||
onRestore={onRestore}
|
||||
/>,
|
||||
)
|
||||
hotkeyMocks.handlers.at(-1)!({ preventDefault })
|
||||
expect(mockOnPublish).toHaveBeenCalledTimes(1)
|
||||
|
||||
rerender(
|
||||
<AppPublisher
|
||||
hasUnpublishedChanges
|
||||
publishedAt={Date.now()}
|
||||
onPublish={mockOnPublish}
|
||||
onRestore={onRestore}
|
||||
/>,
|
||||
)
|
||||
hotkeyMocks.handlers.at(-1)!({ preventDefault })
|
||||
await waitFor(() => {
|
||||
expect(mockOnPublish).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
fireEvent.click(screen.getByText('publisher-summary-restore'))
|
||||
|
||||
@@ -545,44 +500,13 @@ describe('AppPublisher', () => {
|
||||
expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should require an explicit model selection when publishing in multiple model mode', () => {
|
||||
const preventDefault = vi.fn()
|
||||
|
||||
render(
|
||||
<AppPublisher
|
||||
debugWithMultipleModel
|
||||
hasUnpublishedChanges
|
||||
multipleModelConfigs={[
|
||||
{
|
||||
id: 'model-1',
|
||||
model: 'gpt-4o',
|
||||
provider: 'openai',
|
||||
parameters: {},
|
||||
},
|
||||
]}
|
||||
publishedAt={Date.now()}
|
||||
onPublish={mockOnPublish}
|
||||
/>,
|
||||
)
|
||||
|
||||
hotkeyMocks.handlers[0]!({ preventDefault })
|
||||
|
||||
expect(preventDefault).not.toHaveBeenCalled()
|
||||
expect(mockOnPublish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should keep the popover open when restore and publish fail', async () => {
|
||||
it('should keep the popover open when restore fails and reset published state after publish failures', async () => {
|
||||
const preventDefault = vi.fn()
|
||||
const onRestore = vi.fn().mockRejectedValue(new Error('restore failed'))
|
||||
mockOnPublish.mockRejectedValueOnce(new Error('publish failed'))
|
||||
|
||||
render(
|
||||
<AppPublisher
|
||||
hasUnpublishedChanges
|
||||
publishedAt={Date.now()}
|
||||
onPublish={mockOnPublish}
|
||||
onRestore={onRestore}
|
||||
/>,
|
||||
<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} onRestore={onRestore} />,
|
||||
)
|
||||
|
||||
hotkeyMocks.handlers[0]!({ preventDefault })
|
||||
@@ -602,6 +526,57 @@ describe('AppPublisher', () => {
|
||||
expect(screen.getByText('publisher-summary-publish'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should report missing explore installations', async () => {
|
||||
mockFetchInstalledAppList.mockResolvedValueOnce({
|
||||
installed_apps: [],
|
||||
})
|
||||
mockOpenAsyncWindow.mockImplementation(
|
||||
async (resolver: () => Promise<string>, options: { onError: (error: Error) => void }) => {
|
||||
try {
|
||||
await resolver()
|
||||
} catch (error) {
|
||||
options.onError(error as Error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
render(<AppPublisher publishedAt={Date.now()} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
fireEvent.click(screen.getByText('publisher-open-in-explore'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastError).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/(?:^|\.)notPublishedYet(?=$|:)/),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should report explore errors when the app cannot be opened', async () => {
|
||||
mockAppDetail = {
|
||||
...mockAppDetail,
|
||||
id: undefined,
|
||||
}
|
||||
mockOpenAsyncWindow.mockImplementation(
|
||||
async (resolver: () => Promise<string>, options: { onError: (error: Error) => void }) => {
|
||||
try {
|
||||
await resolver()
|
||||
} catch (error) {
|
||||
options.onError(error as Error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
render(<AppPublisher publishedAt={Date.now()} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
fireEvent.click(screen.getByText('publisher-open-in-explore'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastError).toHaveBeenCalledWith('App not found')
|
||||
})
|
||||
})
|
||||
|
||||
it('should show marketplace button and open redirect URL on success', async () => {
|
||||
mockPublishToCreatorsPlatform.mockResolvedValue({
|
||||
redirect_url: 'https://marketplace.example.com/publish?code=abc',
|
||||
@@ -613,12 +588,6 @@ describe('AppPublisher', () => {
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
expect(sectionProps.actions).toEqual(
|
||||
expect.objectContaining({
|
||||
marketplaceActionDisabled: false,
|
||||
showMarketplaceAction: true,
|
||||
}),
|
||||
)
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/))
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -655,12 +624,6 @@ describe('AppPublisher', () => {
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
expect(sectionProps.actions).toEqual(
|
||||
expect.objectContaining({
|
||||
marketplaceActionDisabled: true,
|
||||
showMarketplaceAction: true,
|
||||
}),
|
||||
)
|
||||
const marketplaceButton = screen
|
||||
.getByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/)
|
||||
.closest('a, button, div[role="button"]') as HTMLElement
|
||||
@@ -674,7 +637,6 @@ describe('AppPublisher', () => {
|
||||
render(<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
expect(sectionProps.actions?.showMarketplaceAction).toBe(false)
|
||||
expect(
|
||||
screen.queryByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/),
|
||||
).not.toBeInTheDocument()
|
||||
@@ -694,116 +656,4 @@ describe('AppPublisher', () => {
|
||||
})
|
||||
expect(screen.getByTestId('access-control'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not infer an app mode when app detail is unavailable', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockAppDetail = null
|
||||
|
||||
render(<AppPublisher publishedAt={Date.now()} />)
|
||||
|
||||
await user.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
|
||||
expect(sectionProps.summary).toEqual(
|
||||
expect.objectContaining({
|
||||
isChatApp: false,
|
||||
isWorkflowApp: false,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should derive workflow changes from draft and published hashes', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockAppDetail = {
|
||||
...mockAppDetail,
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
}
|
||||
mockPublishedWorkflow = {
|
||||
created_at: 1_710_000_100,
|
||||
hash: 'published-hash',
|
||||
}
|
||||
|
||||
const { rerender } = render(
|
||||
<AppPublisher
|
||||
draftHash="published-hash"
|
||||
draftUpdatedAt={1_710_000_200_000}
|
||||
publishedAt={1_710_000_100_000}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
expect(sectionProps.summary?.hasUnpublishedChanges).toBe(false)
|
||||
|
||||
rerender(
|
||||
<AppPublisher
|
||||
draftHash="changed-draft-hash"
|
||||
draftUpdatedAt={1_710_000_100_000}
|
||||
publishedAt={1_710_000_200_000}
|
||||
/>,
|
||||
)
|
||||
expect(sectionProps.summary?.hasUnpublishedChanges).toBe(true)
|
||||
})
|
||||
|
||||
it('should keep workflow publishing available when the published hash is unavailable', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockAppDetail = {
|
||||
...mockAppDetail,
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
}
|
||||
mockPublishedWorkflow = {
|
||||
created_at: 1_710_000_100,
|
||||
hash: '',
|
||||
}
|
||||
|
||||
render(
|
||||
<AppPublisher
|
||||
draftHash="draft-hash"
|
||||
draftUpdatedAt={1_710_000_200_000}
|
||||
publishedAt={1_710_000_100_000}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
|
||||
|
||||
expect(sectionProps.summary).toEqual(
|
||||
expect.objectContaining({
|
||||
hasUnpublishedChanges: true,
|
||||
publishedAt: 1_710_000_100_000,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should refresh the shared workflow query and store after a collaborator publishes', async () => {
|
||||
const setPublishedAt = vi.fn()
|
||||
const workflowStore = {
|
||||
getState: () => ({ setPublishedAt }),
|
||||
}
|
||||
mockAppDetail = {
|
||||
...mockAppDetail,
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
}
|
||||
mockFetchPublishedWorkflow.mockResolvedValue({
|
||||
created_at: 1_710_000_300,
|
||||
hash: 'published-hash',
|
||||
})
|
||||
|
||||
render(
|
||||
<WorkflowContext value={workflowStore as any}>
|
||||
<AppPublisher draftHash="draft-hash" publishedAt={1_710_000_100_000} />
|
||||
</WorkflowContext>,
|
||||
)
|
||||
|
||||
act(() => {
|
||||
collaborationMocks.handler?.({
|
||||
type: 'app_publish_update',
|
||||
userId: 'collaborator-1',
|
||||
data: { action: 'published' },
|
||||
timestamp: 1_710_000_300_000,
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchPublishedWorkflow).toHaveBeenCalledWith('app-1')
|
||||
expect(setPublishedAt).toHaveBeenCalledWith(1_710_000_300)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -143,27 +143,6 @@ describe('PublishWithMultipleModel', () => {
|
||||
expect(screen.queryByText(/(?:^|\.)publishAs(?=$|:)/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable the trigger when publishing is unavailable', () => {
|
||||
render(
|
||||
<PublishWithMultipleModel
|
||||
disabled
|
||||
multipleModelConfigs={[
|
||||
{
|
||||
id: 'config-1',
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
parameters: {},
|
||||
},
|
||||
]}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /(?:^|\.)operation\.applyConfig(?=$|:)/ }),
|
||||
).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should open matching model options and call onSelect', () => {
|
||||
const handleSelect = vi.fn()
|
||||
const modelConfig = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* oxlint-disable typescript/no-explicit-any */
|
||||
import type { VersionHistory } from '@/types/workflow'
|
||||
import type { ReactNode } from 'react'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@@ -13,19 +12,45 @@ import {
|
||||
} from '../sections'
|
||||
|
||||
vi.mock('../publish-with-multiple-model', () => ({
|
||||
default: ({
|
||||
disabled,
|
||||
onSelect,
|
||||
}: {
|
||||
disabled?: boolean
|
||||
onSelect: (item: Record<string, unknown>) => void
|
||||
}) => (
|
||||
<button type="button" disabled={disabled} onClick={() => onSelect({ model: 'gpt-4o' })}>
|
||||
default: ({ onSelect }: { onSelect: (item: Record<string, unknown>) => void }) => (
|
||||
<button type="button" onClick={() => onSelect({ model: 'gpt-4o' })}>
|
||||
publish-multiple-model
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../suggested-action', () => ({
|
||||
default: ({
|
||||
children,
|
||||
onClick,
|
||||
link,
|
||||
disabled,
|
||||
actionButton,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
link?: string
|
||||
disabled?: boolean
|
||||
actionButton?: { ariaLabel: string; onClick: () => void }
|
||||
}) => (
|
||||
<div>
|
||||
<button type="button" data-link={link} disabled={disabled} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
{actionButton && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={actionButton.ariaLabel}
|
||||
disabled={disabled}
|
||||
onClick={actionButton.onClick}
|
||||
>
|
||||
{actionButton.ariaLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/tools/workflow-tool/configure-button', () => ({
|
||||
default: (props: Record<string, unknown>) => (
|
||||
<div>
|
||||
@@ -35,32 +60,6 @@ vi.mock('@/app/components/tools/workflow-tool/configure-button', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
const createVersionInfo = (overrides: Partial<VersionHistory> = {}): VersionHistory => ({
|
||||
id: 'workflow-version-1',
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
created_at: 1_710_000_000,
|
||||
created_by: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
},
|
||||
hash: 'hash-1',
|
||||
updated_at: 1_710_000_000,
|
||||
updated_by: {
|
||||
id: 'user-1',
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
},
|
||||
tool_published: false,
|
||||
version: '2024-03-09T16:00:00Z',
|
||||
marked_name: '',
|
||||
marked_comment: '',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('app-publisher sections', () => {
|
||||
it('should render restore controls for published chat apps', () => {
|
||||
const handleRestore = vi.fn()
|
||||
@@ -72,10 +71,10 @@ describe('app-publisher sections', () => {
|
||||
formatTimeFromNow={() => '3 minutes ago'}
|
||||
handlePublish={vi.fn()}
|
||||
handleRestore={handleRestore}
|
||||
hasUnpublishedChanges
|
||||
isChatApp
|
||||
multipleModelConfigs={[]}
|
||||
publishDisabled={false}
|
||||
published={false}
|
||||
publishedAt={Date.now()}
|
||||
startNodeLimitExceeded={false}
|
||||
upgradeHighlightStyle={{}}
|
||||
@@ -86,35 +85,6 @@ describe('app-publisher sections', () => {
|
||||
expect(handleRestore).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should disable restore for published chat apps without unpublished changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleRestore = vi.fn()
|
||||
|
||||
render(
|
||||
<PublisherSummarySection
|
||||
debugWithMultipleModel={false}
|
||||
draftUpdatedAt={Date.now()}
|
||||
formatTimeFromNow={() => '3 minutes ago'}
|
||||
handlePublish={vi.fn()}
|
||||
handleRestore={handleRestore}
|
||||
hasUnpublishedChanges={false}
|
||||
isChatApp
|
||||
multipleModelConfigs={[]}
|
||||
publishDisabled={false}
|
||||
publishedAt={Date.now()}
|
||||
startNodeLimitExceeded={false}
|
||||
upgradeHighlightStyle={{}}
|
||||
/>,
|
||||
)
|
||||
|
||||
const restoreButton = screen.getByRole('button', {
|
||||
name: /(?:^|\.)common\.restore(?=$|:)/,
|
||||
})
|
||||
expect(restoreButton).toBeDisabled()
|
||||
await user.click(restoreButton)
|
||||
expect(handleRestore).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should expose the access control warning when subjects are missing', () => {
|
||||
render(
|
||||
<PublisherAccessSection
|
||||
@@ -130,7 +100,7 @@ describe('app-publisher sections', () => {
|
||||
expect(screen.getByText(/(?:^|\.)publishApp\.notSetDesc(?=$|:)/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the initial publish action when the draft has not been published yet', () => {
|
||||
it('should render the publish update action when the draft has not been published yet', () => {
|
||||
render(
|
||||
<PublisherSummarySection
|
||||
debugWithMultipleModel={false}
|
||||
@@ -141,118 +111,17 @@ describe('app-publisher sections', () => {
|
||||
isChatApp={false}
|
||||
multipleModelConfigs={[]}
|
||||
publishDisabled={false}
|
||||
published={false}
|
||||
publishedAt={undefined}
|
||||
startNodeLimitExceeded={false}
|
||||
upgradeHighlightStyle={{}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/(?:^|\.)common\.notPublishedYet(?=$|:)/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/)).toBeInTheDocument()
|
||||
expect(screen.getByText('P')).toBeInTheDocument()
|
||||
expect(screen.getByText(/(?:^|\.)common\.unpublishedChanges(?=$|:)/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/(?:^|\.)common\.publishUpdate(?=$|:)/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose naming for an unnamed published workflow with no draft changes', () => {
|
||||
const onEditVersion = vi.fn()
|
||||
|
||||
render(
|
||||
<PublisherSummarySection
|
||||
debugWithMultipleModel={false}
|
||||
draftUpdatedAt={1_710_000_000_000}
|
||||
formatTimeFromNow={() => '17 days ago'}
|
||||
handlePublish={vi.fn()}
|
||||
handleRestore={vi.fn()}
|
||||
hasUnpublishedChanges={false}
|
||||
isChatApp={false}
|
||||
isWorkflowApp
|
||||
multipleModelConfigs={[]}
|
||||
onEditVersion={onEditVersion}
|
||||
publishDisabled={false}
|
||||
publishedAt={1_710_000_100_000}
|
||||
startNodeLimitExceeded={false}
|
||||
upgradeHighlightStyle={{}}
|
||||
versionInfo={createVersionInfo()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('2024-03-09T16:00:00Z')).toBeInTheDocument()
|
||||
const nameButton = screen.getByRole('button', {
|
||||
name: /versionHistory\.nameIt\b/,
|
||||
})
|
||||
fireEvent.click(nameButton)
|
||||
expect(onEditVersion).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByRole('button', { name: /common\.published\b/ })).toBeDisabled()
|
||||
expect(screen.queryByText('P')).not.toBeInTheDocument()
|
||||
expect(screen.getByText(/common\.noChanges\b/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show named workflow metadata and publish update when its draft changed', () => {
|
||||
const onEditVersion = vi.fn()
|
||||
|
||||
render(
|
||||
<PublisherSummarySection
|
||||
debugWithMultipleModel={false}
|
||||
draftUpdatedAt={1_710_000_200_000}
|
||||
formatTimeFromNow={() => '2 minutes ago'}
|
||||
handlePublish={vi.fn()}
|
||||
handleRestore={vi.fn()}
|
||||
hasUnpublishedChanges
|
||||
isChatApp={false}
|
||||
isWorkflowApp
|
||||
multipleModelConfigs={[]}
|
||||
onEditVersion={onEditVersion}
|
||||
publishDisabled={false}
|
||||
publishedAt={1_710_000_100_000}
|
||||
startNodeLimitExceeded={false}
|
||||
upgradeHighlightStyle={{}}
|
||||
versionInfo={createVersionInfo({
|
||||
marked_name: 'Sprint-42',
|
||||
marked_comment: 'Fixed data synchronization and page loading.',
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Sprint-42')).toBeInTheDocument()
|
||||
expect(screen.getByText('Fixed data synchronization and page loading.')).toBeInTheDocument()
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: /versionHistory\.editVersionInfo\b/,
|
||||
}),
|
||||
)
|
||||
expect(onEditVersion).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByRole('button', { name: /common\.publishUpdate\b/ })).toBeEnabled()
|
||||
expect(screen.getByText(/common\.unpublishedChanges\b/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/common\.savedAt\b/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep non-workflow apps free of workflow version details and saved time', () => {
|
||||
render(
|
||||
<PublisherSummarySection
|
||||
debugWithMultipleModel={false}
|
||||
draftUpdatedAt={1_710_000_200_000}
|
||||
formatTimeFromNow={() => '2 minutes ago'}
|
||||
handlePublish={vi.fn()}
|
||||
handleRestore={vi.fn()}
|
||||
hasUnpublishedChanges
|
||||
isChatApp
|
||||
isWorkflowApp={false}
|
||||
multipleModelConfigs={[]}
|
||||
publishDisabled={false}
|
||||
publishedAt={1_710_000_100_000}
|
||||
startNodeLimitExceeded={false}
|
||||
upgradeHighlightStyle={{}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/common\.latestPublished\b/)).toBeInTheDocument()
|
||||
expect(screen.queryByText('#5')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/versionHistory\.nameIt\b/)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(/common\.unpublishedChanges\b/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/common\.savedAt\b/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep multiple-model publishing available without publish config changes', () => {
|
||||
it('should render multiple-model publishing', () => {
|
||||
const handlePublish = vi.fn()
|
||||
|
||||
render(
|
||||
@@ -262,11 +131,11 @@ describe('app-publisher sections', () => {
|
||||
formatTimeFromNow={() => '1 minute ago'}
|
||||
handlePublish={handlePublish}
|
||||
handleRestore={vi.fn()}
|
||||
hasUnpublishedChanges={false}
|
||||
isChatApp={false}
|
||||
multipleModelConfigs={[{ id: '1' } as any]}
|
||||
publishDisabled={false}
|
||||
publishedAt={Date.now()}
|
||||
published={false}
|
||||
publishedAt={undefined}
|
||||
startNodeLimitExceeded={false}
|
||||
upgradeHighlightStyle={{}}
|
||||
/>,
|
||||
@@ -277,27 +146,6 @@ describe('app-publisher sections', () => {
|
||||
expect(handlePublish).toHaveBeenCalledWith({ model: 'gpt-4o' })
|
||||
})
|
||||
|
||||
it('should disable multiple-model publishing when publishing is unavailable', () => {
|
||||
render(
|
||||
<PublisherSummarySection
|
||||
debugWithMultipleModel
|
||||
draftUpdatedAt={Date.now()}
|
||||
formatTimeFromNow={() => '1 minute ago'}
|
||||
handlePublish={vi.fn()}
|
||||
handleRestore={vi.fn()}
|
||||
hasUnpublishedChanges={false}
|
||||
isChatApp={false}
|
||||
multipleModelConfigs={[{ id: '1' } as any]}
|
||||
publishDisabled
|
||||
publishedAt={Date.now()}
|
||||
startNodeLimitExceeded={false}
|
||||
upgradeHighlightStyle={{}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'publish-multiple-model' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should render the upgrade hint when the start node limit is exceeded', () => {
|
||||
render(
|
||||
<PublisherSummarySection
|
||||
@@ -309,6 +157,7 @@ describe('app-publisher sections', () => {
|
||||
isChatApp={false}
|
||||
multipleModelConfigs={[]}
|
||||
publishDisabled={false}
|
||||
published={false}
|
||||
publishedAt={undefined}
|
||||
startNodeLimitExceeded
|
||||
upgradeHighlightStyle={{}}
|
||||
@@ -364,13 +213,12 @@ describe('app-publisher sections', () => {
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the published workflow actions with Workflow as Tool after Marketplace', async () => {
|
||||
const user = userEvent.setup()
|
||||
it('should render workflow actions, batch run links, and workflow tool configuration', () => {
|
||||
const handleOpenInExplore = vi.fn()
|
||||
const handleEmbed = vi.fn()
|
||||
const handleOpenRunConfig = vi.fn()
|
||||
const onConfigureWorkflowTool = vi.fn()
|
||||
const onPublishToMarketplace = vi.fn()
|
||||
|
||||
render(
|
||||
const { rerender } = render(
|
||||
<PublisherActionsSection
|
||||
appDetail={{
|
||||
id: 'workflow-app',
|
||||
@@ -384,280 +232,92 @@ describe('app-publisher sections', () => {
|
||||
appURL="https://example.com/app"
|
||||
disabledFunctionButton={false}
|
||||
disabledFunctionTooltip="disabled"
|
||||
handleEmbed={handleEmbed}
|
||||
handleOpenInExplore={handleOpenInExplore}
|
||||
handleOpenRunConfig={handleOpenRunConfig}
|
||||
handlePublish={vi.fn()}
|
||||
hasHumanInputNode={false}
|
||||
hasTriggerNode={false}
|
||||
missingStartNode={false}
|
||||
published={false}
|
||||
publishedAt={Date.now()}
|
||||
showDeployAction
|
||||
showMarketplaceAction
|
||||
showBatchRunConfig
|
||||
showRunConfig
|
||||
workflowToolAvailable
|
||||
workflowToolIsLoading={false}
|
||||
onPublishToMarketplace={onPublishToMarketplace}
|
||||
onConfigureWorkflowTool={onConfigureWorkflowTool}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('link', { name: /common\.openWebApp\b/ })).toHaveAttribute(
|
||||
'href',
|
||||
'https://example.com/app',
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: /(?:^|\.)operation\.config(?=$|:)/ }))
|
||||
expect(handleOpenRunConfig).toHaveBeenCalledWith('https://example.com/app')
|
||||
expect(screen.getByRole('link', { name: /appMenus\.accessPoint\b/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/app/workflow-app/access-point',
|
||||
)
|
||||
expect(screen.getByRole('link', { name: /appMenus\.deploy\b/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/app/workflow-app/deploy',
|
||||
)
|
||||
|
||||
const marketplaceAction = screen.getByRole('button', {
|
||||
name: /common\.publishToMarketplace\b/,
|
||||
})
|
||||
const workflowToolAction = screen.getByRole('button', {
|
||||
name: /common\.workflowAsTool\b/,
|
||||
})
|
||||
expect(
|
||||
marketplaceAction.compareDocumentPosition(workflowToolAction) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy()
|
||||
expect(screen.getByRole('status', { name: /common\.configureRequired\b/ })).toBeInTheDocument()
|
||||
|
||||
await user.click(marketplaceAction)
|
||||
expect(onPublishToMarketplace).toHaveBeenCalledTimes(1)
|
||||
|
||||
await user.click(workflowToolAction)
|
||||
expect(onConfigureWorkflowTool).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should expose Configure and Manage in Tools actions for a ready workflow tool', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfigureWorkflowTool = vi.fn()
|
||||
|
||||
render(
|
||||
<PublisherActionsSection
|
||||
appDetail={{
|
||||
id: 'workflow-app',
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
name: 'Workflow App',
|
||||
}}
|
||||
appURL="https://example.com/app"
|
||||
disabledFunctionButton={false}
|
||||
hasHumanInputNode={false}
|
||||
hasTriggerNode={false}
|
||||
publishedAt={Date.now()}
|
||||
showDeployAction
|
||||
toolPublished
|
||||
workflowToolAvailable
|
||||
workflowToolAvailable={false}
|
||||
workflowToolIsLoading={false}
|
||||
workflowToolOutdated={false}
|
||||
onConfigureWorkflowTool={onConfigureWorkflowTool}
|
||||
workflowToolMessage="workflow-disabled"
|
||||
onConfigureWorkflowTool={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByRole('status', { name: /common\.workflowAsToolReady\b/ }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common\.manageInTools\b/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/integrations/tools/workflow',
|
||||
expect(screen.getByText(/(?:^|\.)common\.batchRunApp(?=$|:)/)).toHaveAttribute(
|
||||
'data-link',
|
||||
'https://example.com/app?mode=batch',
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /common\.configure\b/ }))
|
||||
expect(onConfigureWorkflowTool).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should show the disabled reason below setup and configured workflow tool actions', () => {
|
||||
const commonProps = {
|
||||
appDetail: {
|
||||
id: 'workflow-app',
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
},
|
||||
appURL: 'https://example.com/app',
|
||||
disabledFunctionButton: false,
|
||||
hasHumanInputNode: false,
|
||||
hasTriggerNode: false,
|
||||
onConfigureWorkflowTool: vi.fn(),
|
||||
publishedAt: Date.now(),
|
||||
workflowToolAvailable: false,
|
||||
workflowToolIsLoading: false,
|
||||
workflowToolMessage: 'Workflow tool unavailable',
|
||||
}
|
||||
const { rerender } = render(<PublisherActionsSection {...commonProps} toolPublished={false} />)
|
||||
|
||||
const setupAction = screen.getByRole('button', { name: /common\.workflowAsTool\b/ })
|
||||
const setupReason = screen.getByText('Workflow tool unavailable')
|
||||
expect(setupAction).toBeDisabled()
|
||||
expect(setupReason).toBeVisible()
|
||||
expect(
|
||||
setupAction.compareDocumentPosition(setupReason) & Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy()
|
||||
|
||||
rerender(<PublisherActionsSection {...commonProps} toolPublished />)
|
||||
|
||||
const configureAction = screen.getByRole('button', { name: /common\.configure\b/ })
|
||||
const manageAction = screen.getByRole('button', { name: /common\.manageInTools\b/ })
|
||||
const configuredReason = screen.getByText('Workflow tool unavailable')
|
||||
expect(configureAction).toBeDisabled()
|
||||
expect(manageAction).toBeDisabled()
|
||||
expect(configuredReason).toBeVisible()
|
||||
expect(
|
||||
manageAction.compareDocumentPosition(configuredReason) & Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should surface update-needed and loading states for a configured workflow tool', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfigureWorkflowTool = vi.fn()
|
||||
const commonProps = {
|
||||
appDetail: {
|
||||
id: 'workflow-app',
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
},
|
||||
appURL: 'https://example.com/app',
|
||||
disabledFunctionButton: false,
|
||||
hasHumanInputNode: false,
|
||||
hasTriggerNode: false,
|
||||
onConfigureWorkflowTool,
|
||||
publishedAt: Date.now(),
|
||||
toolPublished: true,
|
||||
workflowToolAvailable: true,
|
||||
}
|
||||
const { rerender } = render(
|
||||
<PublisherActionsSection
|
||||
{...commonProps}
|
||||
workflowToolIsLoading={false}
|
||||
workflowToolOutdated
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByRole('status', { name: /common\.workflowAsToolUpdateNeeded\b/ }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText(/common\.workflowAsToolTip\b/)).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /common\.workflowAsToolReconfigure\b/ }))
|
||||
expect(onConfigureWorkflowTool).toHaveBeenCalledTimes(1)
|
||||
fireEvent.click(screen.getAllByRole('button', { name: /(?:^|\.)operation\.config(?=$|:)/ })[0]!)
|
||||
expect(handleOpenRunConfig).toHaveBeenCalledWith('https://example.com/app')
|
||||
fireEvent.click(screen.getAllByRole('button', { name: /(?:^|\.)operation\.config(?=$|:)/ })[1]!)
|
||||
expect(handleOpenRunConfig).toHaveBeenCalledWith('https://example.com/app?mode=batch')
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.openInExplore(?=$|:)/))
|
||||
expect(handleOpenInExplore).toHaveBeenCalled()
|
||||
expect(screen.getByText('workflow-tool-configure')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow-disabled')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<PublisherActionsSection
|
||||
{...commonProps}
|
||||
workflowToolIsLoading
|
||||
appDetail={{
|
||||
id: 'chat-app',
|
||||
mode: AppModeEnum.CHAT,
|
||||
name: 'Chat App',
|
||||
}}
|
||||
appURL="https://example.com/app?foo=bar"
|
||||
disabledFunctionButton
|
||||
disabledFunctionTooltip="disabled"
|
||||
handleEmbed={handleEmbed}
|
||||
handleOpenInExplore={handleOpenInExplore}
|
||||
handleOpenRunConfig={handleOpenRunConfig}
|
||||
handlePublish={vi.fn()}
|
||||
hasHumanInputNode={false}
|
||||
hasTriggerNode={false}
|
||||
missingStartNode
|
||||
published={false}
|
||||
publishedAt={Date.now()}
|
||||
toolPublished={false}
|
||||
workflowToolAvailable
|
||||
workflowToolIsLoading={false}
|
||||
workflowToolOutdated={false}
|
||||
onConfigureWorkflowTool={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /common\.workflowAsTool\b/ })).toBeDisabled()
|
||||
expect(screen.getByRole('status', { name: /loading\b/ })).toBeInTheDocument()
|
||||
expect(screen.queryByText(/common\.workflowAsToolTip\b/)).not.toBeInTheDocument()
|
||||
})
|
||||
fireEvent.click(screen.getByText(/(?:^|\.)common\.embedIntoSite(?=$|:)/))
|
||||
expect(handleEmbed).toHaveBeenCalled()
|
||||
expect(screen.getByText(/(?:^|\.)common\.accessAPIReference(?=$|:)/)).toBeDisabled()
|
||||
|
||||
it('should keep Access Point and Deploy available for trigger workflows', () => {
|
||||
render(
|
||||
rerender(
|
||||
<PublisherActionsSection
|
||||
appDetail={{
|
||||
id: 'trigger-app',
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
}}
|
||||
appDetail={{ id: 'trigger-app', mode: AppModeEnum.WORKFLOW }}
|
||||
appURL="https://example.com/app"
|
||||
disabledFunctionButton={false}
|
||||
handleEmbed={handleEmbed}
|
||||
handleOpenInExplore={handleOpenInExplore}
|
||||
handleOpenRunConfig={handleOpenRunConfig}
|
||||
handlePublish={vi.fn()}
|
||||
hasHumanInputNode={false}
|
||||
hasTriggerNode
|
||||
publishedAt={Date.now()}
|
||||
showDeployAction
|
||||
workflowToolAvailable
|
||||
workflowToolIsLoading={false}
|
||||
onConfigureWorkflowTool={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText(/(?:^|\.)common\.openWebApp(?=$|:)/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/(?:^|\.)common\.workflowAsTool(?=$|:)/)).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /appMenus\.accessPoint\b/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/app/trigger-app/access-point',
|
||||
)
|
||||
expect(screen.getByRole('link', { name: /appMenus\.deploy\b/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/app/trigger-app/deploy',
|
||||
)
|
||||
})
|
||||
|
||||
it('should expose unavailable quick links as disabled buttons before the first publish', () => {
|
||||
render(
|
||||
<PublisherActionsSection
|
||||
appDetail={{ id: 'workflow-app', mode: AppModeEnum.WORKFLOW }}
|
||||
appURL="https://example.com/app"
|
||||
disabledFunctionButton
|
||||
hasHumanInputNode={false}
|
||||
hasTriggerNode={false}
|
||||
missingStartNode={false}
|
||||
published={false}
|
||||
publishedAt={undefined}
|
||||
showDeployAction
|
||||
toolPublished={false}
|
||||
workflowToolAvailable
|
||||
workflowToolIsLoading={false}
|
||||
workflowToolOutdated={false}
|
||||
onConfigureWorkflowTool={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/(?:^|\.)common\.openWebApp(?=$|:)/).closest('button')).toBeDisabled()
|
||||
expect(
|
||||
screen.getByText(/(?:^|\.)appMenus\.accessPoint(?=$|:)/).closest('button'),
|
||||
).toBeDisabled()
|
||||
expect(screen.getByText(/(?:^|\.)appMenus\.deploy(?=$|:)/).closest('button')).toBeDisabled()
|
||||
expect(
|
||||
screen.getByText(/(?:^|\.)common\.workflowAsTool(?=$|:)/).closest('button'),
|
||||
).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should show the disabled reason when hovering an unavailable action', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<PublisherActionsSection
|
||||
appDetail={{ id: 'workflow-app', mode: AppModeEnum.WORKFLOW }}
|
||||
appURL="https://example.com/app"
|
||||
disabledFunctionButton
|
||||
disabledFunctionTooltip="Open web app unavailable"
|
||||
hasHumanInputNode={false}
|
||||
hasTriggerNode={false}
|
||||
publishedAt={undefined}
|
||||
workflowToolAvailable
|
||||
workflowToolIsLoading={false}
|
||||
onConfigureWorkflowTool={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.hover(screen.getByRole('button', { name: /common\.openWebApp\b/ }))
|
||||
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Open web app unavailable')
|
||||
})
|
||||
|
||||
it('should keep an unavailable action with a tooltip keyboard focusable', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<PublisherActionsSection
|
||||
appDetail={{ id: 'workflow-app', mode: AppModeEnum.WORKFLOW }}
|
||||
appURL="https://example.com/app"
|
||||
disabledFunctionButton
|
||||
disabledFunctionTooltip="Open web app unavailable"
|
||||
hasHumanInputNode={false}
|
||||
hasTriggerNode={false}
|
||||
publishedAt={undefined}
|
||||
workflowToolAvailable
|
||||
workflowToolIsLoading={false}
|
||||
onConfigureWorkflowTool={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.tab()
|
||||
|
||||
const action = screen.getByRole('button', { name: /common\.openWebApp\b/ })
|
||||
expect(action).toHaveFocus()
|
||||
expect(action).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(action).toHaveAccessibleDescription('Open web app unavailable')
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent('Open web app unavailable')
|
||||
expect(screen.queryByText(/(?:^|\.)common\.runApp(?=$|:)/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,149 +1,91 @@
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import SuggestedAction from '../suggested-action'
|
||||
|
||||
describe('SuggestedAction', () => {
|
||||
it('should render an enabled external link with supporting copy', () => {
|
||||
render(
|
||||
<SuggestedAction
|
||||
link="https://example.com/docs"
|
||||
external
|
||||
description="Read the documentation"
|
||||
>
|
||||
Open docs
|
||||
</SuggestedAction>,
|
||||
)
|
||||
it('should render an enabled external link', () => {
|
||||
render(<SuggestedAction link="https://example.com/docs">Open docs</SuggestedAction>)
|
||||
|
||||
const link = screen.getByRole('link', { name: 'Open docs' })
|
||||
expect(link).toHaveAttribute('href', 'https://example.com/docs')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAccessibleDescription('Read the documentation')
|
||||
expect(screen.getByText('Read the documentation')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render internal destinations without opening a new tab', () => {
|
||||
it('should block clicks when disabled', () => {
|
||||
const handleClick = vi.fn()
|
||||
|
||||
render(
|
||||
<SuggestedAction link="/app/app-1/deploy" description="Push versions to environments">
|
||||
Deploy
|
||||
<SuggestedAction link="https://example.com/docs" disabled onClick={handleClick}>
|
||||
Disabled action
|
||||
</SuggestedAction>,
|
||||
)
|
||||
|
||||
const link = screen.getByRole('link', { name: 'Deploy' })
|
||||
expect(link).toHaveAttribute('href', '/app/app-1/deploy')
|
||||
expect(link).not.toHaveAttribute('target')
|
||||
expect(link).toHaveAccessibleDescription('Push versions to environments')
|
||||
const link = screen.getByText('Disabled action').closest('a') as HTMLAnchorElement
|
||||
fireEvent.click(link)
|
||||
|
||||
expect(link).not.toHaveAttribute('href')
|
||||
expect(handleClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use native disabled button semantics for unavailable links', () => {
|
||||
const handleClick = vi.fn()
|
||||
it('should forward click events when enabled', () => {
|
||||
const handleClick = vi.fn((event: ReactMouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
render(
|
||||
<SuggestedAction link="https://example.com/docs" onClick={handleClick}>
|
||||
Enabled action
|
||||
</SuggestedAction>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Enabled action' }))
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render and trigger the trailing action button when configured', () => {
|
||||
const handleActionClick = vi.fn()
|
||||
|
||||
render(
|
||||
<SuggestedAction
|
||||
link="https://example.com/docs"
|
||||
actionButton={{
|
||||
ariaLabel: 'Configure action',
|
||||
icon: <span>config</span>,
|
||||
onClick: handleActionClick,
|
||||
}}
|
||||
>
|
||||
Configurable action
|
||||
</SuggestedAction>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Configure action' }))
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Configurable action' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://example.com/docs',
|
||||
)
|
||||
expect(handleActionClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should block action button clicks when disabled', () => {
|
||||
const handleActionClick = vi.fn()
|
||||
|
||||
render(
|
||||
<SuggestedAction
|
||||
link="https://example.com/docs"
|
||||
disabled
|
||||
description="Unavailable until published"
|
||||
onClick={handleClick}
|
||||
>
|
||||
Disabled action
|
||||
</SuggestedAction>,
|
||||
)
|
||||
|
||||
const action = screen.getByRole('button', { name: 'Disabled action' })
|
||||
fireEvent.click(action)
|
||||
|
||||
expect(action).toBeDisabled()
|
||||
expect(screen.queryByRole('link', { name: /Disabled action/ })).not.toBeInTheDocument()
|
||||
expect(handleClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should keep an explained disabled action in the keyboard tab order', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleClick = vi.fn()
|
||||
|
||||
render(
|
||||
<SuggestedAction disabled focusableWhenDisabled onClick={handleClick}>
|
||||
Disabled action with explanation
|
||||
</SuggestedAction>,
|
||||
)
|
||||
|
||||
await user.tab()
|
||||
|
||||
const action = screen.getByRole('button', { name: 'Disabled action with explanation' })
|
||||
expect(action).toHaveFocus()
|
||||
expect(action).toHaveAttribute('aria-disabled', 'true')
|
||||
|
||||
await user.keyboard('{Enter}')
|
||||
expect(handleClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render and trigger an enabled button action', () => {
|
||||
const handleClick = vi.fn()
|
||||
|
||||
render(
|
||||
<SuggestedAction
|
||||
description="Use as a tool in other apps"
|
||||
endIcon={<span data-testid="configure-icon" />}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Workflow as Tool
|
||||
</SuggestedAction>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Workflow as Tool' }))
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByTestId('configure-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep the main link separate from a trailing action button', () => {
|
||||
const handleActionClick = vi.fn()
|
||||
|
||||
render(
|
||||
<SuggestedAction
|
||||
link="https://example.com/app"
|
||||
external
|
||||
actionButton={{
|
||||
ariaLabel: 'Configure action',
|
||||
icon: <span>config</span>,
|
||||
onClick: handleActionClick,
|
||||
}}
|
||||
>
|
||||
Open web app
|
||||
Disabled with action
|
||||
</SuggestedAction>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Open web app' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://example.com/app',
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Configure action' }))
|
||||
expect(handleActionClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should disable both controls when an action with a trailing button is unavailable', () => {
|
||||
const handleActionClick = vi.fn()
|
||||
|
||||
render(
|
||||
<SuggestedAction
|
||||
link="https://example.com/app"
|
||||
external
|
||||
disabled
|
||||
actionButton={{
|
||||
ariaLabel: 'Configure action',
|
||||
icon: <span>config</span>,
|
||||
onClick: handleActionClick,
|
||||
}}
|
||||
>
|
||||
Open web app
|
||||
</SuggestedAction>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Open web app' })).toBeDisabled()
|
||||
|
||||
const actionButton = screen.getByRole('button', { name: 'Configure action' })
|
||||
fireEvent.click(actionButton)
|
||||
expect(actionButton).toBeDisabled()
|
||||
expect(handleActionClick).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
|
||||
type ActionTooltipProps = {
|
||||
children: ReactNode
|
||||
disabled: boolean
|
||||
tooltip?: ReactNode
|
||||
}
|
||||
|
||||
const ActionTooltip = ({ children, disabled, tooltip }: ActionTooltipProps) => {
|
||||
if (!tooltip) return <>{children}</>
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div
|
||||
className={cn('flex w-full', disabled && 'cursor-not-allowed *:pointer-events-none')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent role="tooltip">{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionTooltip
|
||||
@@ -1 +0,0 @@
|
||||
export const APP_PUBLISH_DRAFT_CHANGED = 'APP_PUBLISH_DRAFT_CHANGED'
|
||||
@@ -2,8 +2,8 @@ import type {
|
||||
AppPublisherProps,
|
||||
AppPublisherPublishParams,
|
||||
} from '@/app/components/app/app-publisher'
|
||||
import type { ConfigurationPublishConfig } from '@/app/components/app/configuration/hooks/use-configuration-utils'
|
||||
import type { Features, FileUpload } from '@/app/components/base/features/types'
|
||||
import type { ModelConfig } from '@/models/debug'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AppPublisher } from '@/app/components/app/app-publisher'
|
||||
@@ -22,12 +23,18 @@ import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { Resolution } from '@/types/app'
|
||||
|
||||
type PublishedModelConfig = ModelConfig & {
|
||||
resetAppConfig?: () => void
|
||||
}
|
||||
|
||||
type Props = Omit<AppPublisherProps, 'onPublish'> & {
|
||||
onPublish?: (
|
||||
params?: AppPublisherPublishParams,
|
||||
features?: Features,
|
||||
) => Promise<unknown> | unknown
|
||||
publishedConfig: ConfigurationPublishConfig
|
||||
publishedConfig: {
|
||||
modelConfig: PublishedModelConfig
|
||||
}
|
||||
resetAppConfig?: () => void
|
||||
}
|
||||
|
||||
@@ -36,7 +43,6 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
|
||||
const features = useFeatures((s) => s.features)
|
||||
const featuresStore = useFeaturesStore()
|
||||
const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
|
||||
const { resetAppConfig } = props
|
||||
const {
|
||||
more_like_this,
|
||||
opening_statement,
|
||||
@@ -48,9 +54,10 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
|
||||
retriever_resource,
|
||||
annotation_reply,
|
||||
file_upload,
|
||||
resetAppConfig,
|
||||
} = props.publishedConfig.modelConfig
|
||||
|
||||
const handleConfirm = () => {
|
||||
const handleConfirm = useCallback(() => {
|
||||
resetAppConfig?.()
|
||||
const { features, setFeatures } = featuresStore!.getState()
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
@@ -83,9 +90,9 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
|
||||
number_limits: file_upload?.number_limits || file_upload?.image?.number_limits || 3,
|
||||
} as FileUpload
|
||||
})
|
||||
setFeatures(newFeatures, { silent: true })
|
||||
setFeatures(newFeatures)
|
||||
setRestoreConfirmOpen(false)
|
||||
}
|
||||
}, [featuresStore, props])
|
||||
|
||||
const handlePublish = useCallback(
|
||||
(params?: AppPublisherPublishParams) => {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useHotkey } from '@tanstack/react-hotkeys'
|
||||
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { use, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { WorkflowLaunchDialog } from '@/app/components/app/overview/app-card-sections'
|
||||
@@ -21,8 +20,10 @@ import {
|
||||
createWorkflowLaunchInitialValues,
|
||||
isWorkflowLaunchInputSupported,
|
||||
} from '@/app/components/app/overview/app-card-utils'
|
||||
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { buildInstalledAppPath } from '@/app/components/explore/installed-app/routes'
|
||||
import { useCanManageTools } from '@/app/components/tools/hooks/use-tool-permissions'
|
||||
import { WorkflowToolDrawer } from '@/app/components/tools/workflow-tool'
|
||||
import { useConfigureButton } from '@/app/components/tools/workflow-tool/hooks/use-configure-button'
|
||||
@@ -30,20 +31,18 @@ import { collaborationManager } from '@/app/components/workflow/collaboration/co
|
||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import { isCurrentWorkspaceEditorAtom } from '@/context/workspace-state'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { fetchAppDetail, publishToCreatorsPlatform } from '@/service/apps'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import { appDetailQueryKeyPrefix } from '@/service/use-apps'
|
||||
import {
|
||||
appWorkflowQueryOptions,
|
||||
useAppWorkflow,
|
||||
useInvalidateAppWorkflow,
|
||||
useUpdateWorkflow,
|
||||
} from '@/service/use-workflow'
|
||||
import { useInvalidateAppWorkflow } from '@/service/use-workflow'
|
||||
import { fetchPublishedWorkflow } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { basePath } from '@/utils/var'
|
||||
import AccessControl from '../app-access-control'
|
||||
import { APP_PUBLISH_HOTKEY } from './hotkeys'
|
||||
import {
|
||||
@@ -51,12 +50,12 @@ import {
|
||||
PublisherActionsSection,
|
||||
PublisherSummarySection,
|
||||
} from './sections'
|
||||
import SuggestedAction from './suggested-action'
|
||||
import {
|
||||
getDisabledFunctionTooltip,
|
||||
getPublisherAppUrl,
|
||||
isPublisherAccessConfigured,
|
||||
} from './utils'
|
||||
import VersionInfoModal from './version-info-modal'
|
||||
|
||||
export type AppPublisherProps = {
|
||||
disabled?: boolean
|
||||
@@ -64,10 +63,6 @@ export type AppPublisherProps = {
|
||||
publishedAt?: number
|
||||
/** only needed in workflow / chatflow mode */
|
||||
draftUpdatedAt?: number
|
||||
/** Current persisted workflow draft hash, used to compare with the published workflow. */
|
||||
draftHash?: string
|
||||
/** Non-workflow editors should pass their local dirty state. */
|
||||
hasUnpublishedChanges?: boolean
|
||||
debugWithMultipleModel?: boolean
|
||||
multipleModelConfigs?: ModelAndParameter[]
|
||||
/** modelAndParameter is passed when debugWithMultipleModel is true */
|
||||
@@ -99,8 +94,6 @@ export function AppPublisher({
|
||||
publishDisabled = false,
|
||||
publishedAt,
|
||||
draftUpdatedAt,
|
||||
draftHash,
|
||||
hasUnpublishedChanges,
|
||||
debugWithMultipleModel = false,
|
||||
multipleModelConfigs = [],
|
||||
onPublish,
|
||||
@@ -119,44 +112,32 @@ export function AppPublisher({
|
||||
}: AppPublisherProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [published, setPublished] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
|
||||
const [workflowToolDrawerOpen, setWorkflowToolDrawerOpen] = useState(false)
|
||||
|
||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||
const [workflowLaunchDialogOpen, setWorkflowLaunchDialogOpen] = useState(false)
|
||||
const [workflowLaunchTargetUrl, setWorkflowLaunchTargetUrl] = useState('')
|
||||
const [workflowLaunchValues, setWorkflowLaunchValues] = useState<
|
||||
Record<string, WorkflowLaunchInputValue>
|
||||
>({})
|
||||
const [publishingToMarketplace, setPublishingToMarketplace] = useState(false)
|
||||
const [editVersionInfoOpen, setEditVersionInfoOpen] = useState(false)
|
||||
|
||||
const workflowStore = use(WorkflowContext)
|
||||
const appDetail = useAppStore((state) => state.appDetail)
|
||||
const setAppDetail = useAppStore((state) => state.setAppDetail)
|
||||
const canManageTools = useCanManageTools()
|
||||
const isCurrentWorkspaceEditor = useAtomValue(isCurrentWorkspaceEditorAtom)
|
||||
const queryClient = useQueryClient()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
||||
|
||||
const appURL = getPublisherAppUrl({ appBaseUrl: appBaseURL, accessToken, mode: appDetail?.mode })
|
||||
const appMode = appDetail?.mode
|
||||
const isWorkflowApp = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
|
||||
const isChatApp =
|
||||
appMode === AppModeEnum.CHAT ||
|
||||
appMode === AppModeEnum.AGENT_CHAT ||
|
||||
appMode === AppModeEnum.COMPLETION
|
||||
const { data: publishedWorkflow, isSuccess: isPublishedWorkflowSuccess } = useAppWorkflow(
|
||||
isWorkflowApp ? (appDetail?.id ?? '') : '',
|
||||
const isChatApp = [AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION].includes(
|
||||
appDetail?.mode || AppModeEnum.CHAT,
|
||||
)
|
||||
const currentPublishedAt =
|
||||
isWorkflowApp && isPublishedWorkflowSuccess
|
||||
? publishedWorkflow?.created_at
|
||||
? publishedWorkflow.created_at * 1000
|
||||
: undefined
|
||||
: publishedAt
|
||||
const { mutate: updateWorkflow } = useUpdateWorkflow()
|
||||
const hiddenLaunchVariables: WorkflowHiddenStartVariable[] = (inputs ?? []).filter(
|
||||
(input) => input.hide === true,
|
||||
)
|
||||
@@ -185,6 +166,7 @@ export function AppPublisher({
|
||||
appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
)
|
||||
const invalidateAppWorkflow = useInvalidateAppWorkflow()
|
||||
const openAsyncWindow = useAsyncWindowOpen()
|
||||
|
||||
const isAppAccessSet = isPublisherAccessConfigured(appDetail, appAccessSubjects)
|
||||
|
||||
@@ -194,10 +176,10 @@ export function AppPublisher({
|
||||
appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS &&
|
||||
!userCanAccessApp?.result,
|
||||
)
|
||||
const disabledFunctionButton = !currentPublishedAt || missingStartNode || noAccessPermission
|
||||
const disabledFunctionButton = !publishedAt || missingStartNode || noAccessPermission
|
||||
const disabledFunctionTooltip = getDisabledFunctionTooltip({
|
||||
t,
|
||||
publishedAt: currentPublishedAt,
|
||||
publishedAt,
|
||||
missingStartNode,
|
||||
noAccessPermission,
|
||||
})
|
||||
@@ -205,6 +187,7 @@ export function AppPublisher({
|
||||
async function handlePublish(params?: ModelAndParameter | PublishWorkflowParams) {
|
||||
try {
|
||||
await onPublish?.(params)
|
||||
setPublished(true)
|
||||
|
||||
const appId = appDetail?.id
|
||||
const socket = appId ? webSocketClient.getSocket(appId) : null
|
||||
@@ -231,6 +214,7 @@ export function AppPublisher({
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('[app-publisher] publish failed', error)
|
||||
setPublished(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +233,25 @@ export function AppPublisher({
|
||||
|
||||
onToggle?.(nextOpen)
|
||||
setOpen(nextOpen)
|
||||
|
||||
if (nextOpen) setPublished(false)
|
||||
}
|
||||
|
||||
async function handleOpenInExplore() {
|
||||
await openAsyncWindow(
|
||||
async () => {
|
||||
if (!appDetail?.id) throw new Error('App not found')
|
||||
const { installed_apps } = await fetchInstalledAppList(appDetail.id)
|
||||
if (installed_apps?.length > 0)
|
||||
return `${basePath}${buildInstalledAppPath(installed_apps[0]!.id)}`
|
||||
throw new Error(t(($) => $.notPublishedYet, { ns: 'app' }))
|
||||
},
|
||||
{
|
||||
onError: (err) => {
|
||||
toast.error(`${err.message || err}`)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async function handleAccessControlUpdate() {
|
||||
@@ -301,50 +304,9 @@ export function AppPublisher({
|
||||
}
|
||||
}
|
||||
|
||||
const hasPublishedVersion = Boolean(currentPublishedAt)
|
||||
const workflowHasUnpublishedChanges =
|
||||
!currentPublishedAt ||
|
||||
!draftHash ||
|
||||
!publishedWorkflow?.hash ||
|
||||
draftHash !== publishedWorkflow.hash
|
||||
const resolvedHasUnpublishedChanges =
|
||||
hasUnpublishedChanges ?? (isWorkflowApp ? workflowHasUnpublishedChanges : !currentPublishedAt)
|
||||
|
||||
function handleOpenVersionInfo() {
|
||||
if (!publishedWorkflow) return
|
||||
|
||||
handleOpenChange(false)
|
||||
setEditVersionInfoOpen(true)
|
||||
}
|
||||
|
||||
function handleUpdateVersionInfo(params: { id?: string; title: string; releaseNotes: string }) {
|
||||
if (!appDetail?.id || !params.id) return
|
||||
|
||||
updateWorkflow(
|
||||
{
|
||||
url: `/apps/${appDetail.id}/workflows/${params.id}`,
|
||||
title: params.title,
|
||||
releaseNotes: params.releaseNotes,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t(($) => $['versionHistory.action.updateSuccess'], { ns: 'workflow' }))
|
||||
invalidateAppWorkflow(appDetail.id)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t(($) => $['versionHistory.action.updateFailure'], { ns: 'workflow' }))
|
||||
},
|
||||
onSettled: () => {
|
||||
setEditVersionInfoOpen(false)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
useHotkey(APP_PUBLISH_HOTKEY, (e) => {
|
||||
if (debugWithMultipleModel) return
|
||||
e.preventDefault()
|
||||
if (publishDisabled || (hasPublishedVersion && !resolvedHasUnpublishedChanges)) return
|
||||
if (publishDisabled || published) return
|
||||
handlePublish()
|
||||
})
|
||||
|
||||
@@ -355,10 +317,11 @@ export function AppPublisher({
|
||||
const unsubscribe = collaborationManager.onAppPublishUpdate((update: CollaborationUpdate) => {
|
||||
const action = typeof update.data.action === 'string' ? update.data.action : undefined
|
||||
if (action === 'published') {
|
||||
void queryClient
|
||||
.fetchQuery(appWorkflowQueryOptions(appId))
|
||||
invalidateAppWorkflow(appId)
|
||||
fetchPublishedWorkflow(`/apps/${appId}/workflows/publish`)
|
||||
.then((publishedWorkflow) => {
|
||||
workflowStore?.getState().setPublishedAt(publishedWorkflow?.created_at ?? 0)
|
||||
if (publishedWorkflow?.created_at)
|
||||
workflowStore?.getState().setPublishedAt(publishedWorkflow.created_at)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('[app-publisher] refresh published workflow failed', error)
|
||||
@@ -367,8 +330,9 @@ export function AppPublisher({
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [appDetail?.id, queryClient, workflowStore])
|
||||
}, [appDetail?.id, invalidateAppWorkflow, workflowStore])
|
||||
|
||||
const hasPublishedVersion = !!publishedAt
|
||||
const workflowToolVisible =
|
||||
appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && !hasTriggerNode
|
||||
const workflowToolAvailableForUser = workflowToolAvailable && canManageTools
|
||||
@@ -389,7 +353,7 @@ export function AppPublisher({
|
||||
const workflowTool = useConfigureButton({
|
||||
enabled: workflowToolVisible && canManageTools,
|
||||
published: workflowToolPublished,
|
||||
detailNeedUpdate: workflowToolPublished && !resolvedHasUnpublishedChanges,
|
||||
detailNeedUpdate: workflowToolPublished && published,
|
||||
workflowAppId: appDetail?.id ?? '',
|
||||
icon: workflowToolIcon,
|
||||
name: appDetail?.name ?? '',
|
||||
@@ -431,23 +395,20 @@ export function AppPublisher({
|
||||
alignOffset={crossAxisOffset}
|
||||
popupClassName="border-none bg-transparent shadow-none"
|
||||
>
|
||||
<div className="w-98 rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5">
|
||||
<div className="w-[320px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5">
|
||||
<PublisherSummarySection
|
||||
debugWithMultipleModel={debugWithMultipleModel}
|
||||
draftUpdatedAt={draftUpdatedAt}
|
||||
formatTimeFromNow={formatTimeFromNow}
|
||||
handlePublish={handlePublish}
|
||||
handleRestore={handleRestore}
|
||||
hasUnpublishedChanges={resolvedHasUnpublishedChanges}
|
||||
isChatApp={isChatApp}
|
||||
isWorkflowApp={isWorkflowApp}
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onEditVersion={handleOpenVersionInfo}
|
||||
publishDisabled={publishDisabled}
|
||||
publishedAt={currentPublishedAt}
|
||||
published={published}
|
||||
publishedAt={publishedAt}
|
||||
startNodeLimitExceeded={startNodeLimitExceeded}
|
||||
upgradeHighlightStyle={upgradeHighlightStyle}
|
||||
versionInfo={publishedWorkflow}
|
||||
/>
|
||||
<PublisherAccessSection
|
||||
enabled={systemFeatures.webapp_auth.enabled}
|
||||
@@ -467,29 +428,57 @@ export function AppPublisher({
|
||||
appURL={appURL}
|
||||
disabledFunctionButton={disabledFunctionButton}
|
||||
disabledFunctionTooltip={disabledFunctionTooltip}
|
||||
handleEmbed={() => {
|
||||
setEmbeddingModalOpen(true)
|
||||
handleOpenChange(false)
|
||||
}}
|
||||
handleOpenInExplore={() => {
|
||||
handleOpenChange(false)
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
handleOpenRunConfig={handleOpenWorkflowLaunchDialog}
|
||||
handlePublish={handlePublish}
|
||||
hasHumanInputNode={hasHumanInputNode}
|
||||
hasTriggerNode={hasTriggerNode}
|
||||
marketplaceActionDisabled={!currentPublishedAt}
|
||||
publishedAt={currentPublishedAt}
|
||||
publishingToMarketplace={publishingToMarketplace}
|
||||
showDeployAction={
|
||||
appDetail?.mode === AppModeEnum.WORKFLOW &&
|
||||
isCurrentWorkspaceEditor &&
|
||||
systemFeatures.enable_app_deploy
|
||||
missingStartNode={missingStartNode}
|
||||
published={published}
|
||||
publishedAt={publishedAt}
|
||||
showBatchRunConfig={
|
||||
hiddenLaunchVariables.length > 0 &&
|
||||
(appDetail?.mode === AppModeEnum.WORKFLOW ||
|
||||
appDetail?.mode === AppModeEnum.COMPLETION)
|
||||
}
|
||||
showMarketplaceAction={systemFeatures.enable_creators_platform}
|
||||
showRunConfig={hiddenLaunchVariables.length > 0}
|
||||
toolPublished={workflowToolPublished}
|
||||
toolPublished={toolPublished}
|
||||
workflowToolAvailable={workflowToolAvailableForUser}
|
||||
workflowToolIsLoading={workflowTool.isLoading}
|
||||
workflowToolMessage={workflowToolMessage}
|
||||
workflowToolOutdated={workflowTool.outdated}
|
||||
workflowToolMessage={workflowToolMessage}
|
||||
onConfigureWorkflowTool={openWorkflowToolDrawer}
|
||||
onPublishToMarketplace={handlePublishToMarketplace}
|
||||
/>
|
||||
{systemFeatures.enable_creators_platform && (
|
||||
<div className="border-t border-divider-subtle p-4">
|
||||
<SuggestedAction
|
||||
icon={<span className="i-ri-store-line size-4" />}
|
||||
disabled={!publishedAt || publishingToMarketplace}
|
||||
onClick={handlePublishToMarketplace}
|
||||
>
|
||||
{publishingToMarketplace
|
||||
? t(($) => $['common.publishingToMarketplace'], { ns: 'workflow' })
|
||||
: t(($) => $['common.publishToMarketplace'], { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
<EmbeddedModal
|
||||
siteInfo={appDetail?.site}
|
||||
isShow={embeddingModalOpen}
|
||||
onClose={() => setEmbeddingModalOpen(false)}
|
||||
appBaseUrl={appBaseURL}
|
||||
accessToken={accessToken}
|
||||
hiddenInputs={hiddenLaunchVariables}
|
||||
/>
|
||||
{showAppAccessControl && (
|
||||
<AccessControl
|
||||
app={appDetail!}
|
||||
@@ -510,14 +499,6 @@ export function AppPublisher({
|
||||
onSubmit={handleWorkflowLaunchConfirm}
|
||||
/>
|
||||
</Popover>
|
||||
{editVersionInfoOpen && (
|
||||
<VersionInfoModal
|
||||
isOpen={editVersionInfoOpen}
|
||||
versionInfo={publishedWorkflow ?? undefined}
|
||||
onClose={() => setEditVersionInfoOpen(false)}
|
||||
onPublish={handleUpdateVersionInfo}
|
||||
/>
|
||||
)}
|
||||
{workflowToolDrawerOpen && canManageTools && (
|
||||
<WorkflowToolDrawer
|
||||
isAdd={!workflowToolPublished}
|
||||
|
||||
@@ -19,13 +19,11 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import ModelIcon from '../../header/account-setting/model-provider-page/model-icon'
|
||||
|
||||
type PublishWithMultipleModelProps = {
|
||||
disabled?: boolean
|
||||
multipleModelConfigs: ModelAndParameter[]
|
||||
// textGenerationModelList?: Model[]
|
||||
onSelect: (v: ModelAndParameter) => void
|
||||
}
|
||||
const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
|
||||
disabled = false,
|
||||
multipleModelConfigs,
|
||||
// textGenerationModelList = [],
|
||||
onSelect,
|
||||
@@ -57,13 +55,13 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
|
||||
}
|
||||
})
|
||||
|
||||
const triggerDisabled = disabled || !validModelConfigs.length
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
disabled={triggerDisabled}
|
||||
render={<Button variant="primary" disabled={triggerDisabled} className="w-full" />}
|
||||
disabled={!validModelConfigs.length}
|
||||
render={
|
||||
<Button variant="primary" disabled={!validModelConfigs.length} className="mt-3 w-full" />
|
||||
}
|
||||
>
|
||||
<>
|
||||
{t(($) => $['operation.applyConfig'], { ns: 'appDebug' })}
|
||||
@@ -71,12 +69,12 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
|
||||
</>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[288px] p-1">
|
||||
<div className="flex h-5.5 items-center px-3 text-xs font-medium text-text-tertiary">
|
||||
<div className="flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary">
|
||||
{t(($) => $.publishAs, { ns: 'appDebug' })}
|
||||
</div>
|
||||
{validModelConfigs.map((item, index) => (
|
||||
<DropdownMenuItem key={item.id} className="gap-0 px-3" onClick={() => onSelect(item)}>
|
||||
<span className="min-w-4.5 italic">#{index + 1}</span>
|
||||
<span className="min-w-[18px] italic">#{index + 1}</span>
|
||||
<ModelIcon modelName={item.model} provider={item.providerItem} className="ml-2" />
|
||||
<div
|
||||
className="ml-1 truncate text-text-secondary"
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import type { AppPublisherProps } from './index'
|
||||
import type { PublishWorkflowParams, VersionHistory } from '@/types/workflow'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { formatForDisplay } from '@tanstack/react-hotkeys'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import { APP_PUBLISH_HOTKEY } from './hotkeys'
|
||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||
import SuggestedAction from './suggested-action'
|
||||
import { ACCESS_MODE_MAP } from './utils'
|
||||
import WorkflowToolAction from './workflow-tool-action'
|
||||
|
||||
type SummarySectionProps = Pick<
|
||||
AppPublisherProps,
|
||||
@@ -30,12 +29,9 @@ type SummarySectionProps = Pick<
|
||||
formatTimeFromNow: (value: number) => string
|
||||
handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise<void>
|
||||
handleRestore: () => Promise<void>
|
||||
hasUnpublishedChanges?: boolean
|
||||
isChatApp: boolean
|
||||
isWorkflowApp?: boolean
|
||||
onEditVersion?: () => void
|
||||
published: boolean
|
||||
upgradeHighlightStyle: CSSProperties
|
||||
versionInfo?: VersionHistory | null
|
||||
}
|
||||
|
||||
type AccessSectionProps = {
|
||||
@@ -48,7 +44,12 @@ type AccessSectionProps = {
|
||||
|
||||
type ActionsSectionProps = Pick<
|
||||
AppPublisherProps,
|
||||
'hasHumanInputNode' | 'hasTriggerNode' | 'publishedAt' | 'toolPublished' | 'workflowToolAvailable'
|
||||
| 'hasHumanInputNode'
|
||||
| 'hasTriggerNode'
|
||||
| 'missingStartNode'
|
||||
| 'toolPublished'
|
||||
| 'publishedAt'
|
||||
| 'workflowToolAvailable'
|
||||
> & {
|
||||
appDetail:
|
||||
| {
|
||||
@@ -65,17 +66,17 @@ type ActionsSectionProps = Pick<
|
||||
appURL: string
|
||||
disabledFunctionButton: boolean
|
||||
disabledFunctionTooltip?: string
|
||||
handleEmbed: () => void
|
||||
handleOpenInExplore: () => void
|
||||
handleOpenRunConfig?: (url: string) => void
|
||||
marketplaceActionDisabled?: boolean
|
||||
publishingToMarketplace?: boolean
|
||||
showDeployAction?: boolean
|
||||
showMarketplaceAction?: boolean
|
||||
handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise<void>
|
||||
published: boolean
|
||||
showBatchRunConfig?: boolean
|
||||
showRunConfig?: boolean
|
||||
workflowToolIsLoading: boolean
|
||||
workflowToolOutdated: boolean
|
||||
workflowToolMessage?: string
|
||||
workflowToolOutdated?: boolean
|
||||
onConfigureWorkflowTool: () => void
|
||||
onPublishToMarketplace?: () => void
|
||||
}
|
||||
|
||||
export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MAP }) => {
|
||||
@@ -97,227 +98,97 @@ export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MA
|
||||
)
|
||||
}
|
||||
|
||||
const PublisherTimelineMarker = ({ position }: { position: 'top' | 'bottom' }) => (
|
||||
<span
|
||||
className={cn(
|
||||
'relative flex w-4 shrink-0 items-start p-1',
|
||||
position === 'top' ? 'self-stretch' : 'h-4',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="relative z-1 size-2 rounded-full border-2 border-text-quaternary bg-components-panel-bg"
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'absolute left-1/2 w-0.5 -translate-x-1/2 bg-divider-subtle',
|
||||
position === 'top' ? 'top-3.5 -bottom-3.5' : '-top-3.5 h-4',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
|
||||
export const PublisherSummarySection = ({
|
||||
debugWithMultipleModel = false,
|
||||
draftUpdatedAt,
|
||||
formatTimeFromNow,
|
||||
handlePublish,
|
||||
handleRestore,
|
||||
hasUnpublishedChanges,
|
||||
isChatApp,
|
||||
isWorkflowApp = false,
|
||||
multipleModelConfigs = [],
|
||||
onEditVersion,
|
||||
publishDisabled = false,
|
||||
published,
|
||||
publishedAt,
|
||||
startNodeLimitExceeded = false,
|
||||
upgradeHighlightStyle,
|
||||
versionInfo,
|
||||
}: SummarySectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const hasPublishedVersion = Boolean(publishedAt)
|
||||
const resolvedHasUnpublishedChanges = hasUnpublishedChanges ?? !hasPublishedVersion
|
||||
const publishedTimestamp =
|
||||
publishedAt || (versionInfo?.created_at ? versionInfo.created_at * 1000 : undefined)
|
||||
const publisherName = versionInfo?.created_by.name
|
||||
const markedName = versionInfo?.marked_name
|
||||
const markedComment = versionInfo?.marked_comment
|
||||
const publishButtonDisabled =
|
||||
publishDisabled || (hasPublishedVersion && !resolvedHasUnpublishedChanges)
|
||||
const publishButtonLabel = !hasPublishedVersion
|
||||
? t(($) => $['common.publish'], { ns: 'workflow' })
|
||||
: resolvedHasUnpublishedChanges
|
||||
? t(($) => $['common.publishUpdate'], { ns: 'workflow' })
|
||||
: t(($) => $['common.published'], { ns: 'workflow' })
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
<div className="flex items-start gap-1 px-1 py-0.5">
|
||||
<PublisherTimelineMarker position="top" />
|
||||
{!hasPublishedVersion ? (
|
||||
<p className="min-w-0 flex-1 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['common.notPublishedYet'], { ns: 'workflow' })}
|
||||
</p>
|
||||
) : isWorkflowApp ? (
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<div className="flex min-h-4 min-w-0 items-center gap-1">
|
||||
<span className="truncate system-sm-semibold text-text-secondary">
|
||||
{markedName || versionInfo?.version}
|
||||
</span>
|
||||
<span aria-hidden className="system-xs-regular text-text-tertiary">
|
||||
·
|
||||
</span>
|
||||
{markedName ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex size-4 shrink-0 items-center justify-center rounded text-text-tertiary outline-hidden hover:text-text-accent focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
aria-label={t(($) => $['versionHistory.editVersionInfo'], { ns: 'workflow' })}
|
||||
disabled={!versionInfo || !onEditVersion}
|
||||
onClick={onEditVersion}
|
||||
>
|
||||
<span aria-hidden className="i-ri-edit-line size-3.5" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 items-center gap-1 rounded text-text-accent outline-hidden hover:text-text-accent-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-wait"
|
||||
disabled={!versionInfo || !onEditVersion}
|
||||
onClick={onEditVersion}
|
||||
>
|
||||
<span aria-hidden className="i-ri-edit-line size-3.5 shrink-0" />
|
||||
<span className="truncate system-xs-medium">
|
||||
{t(($) => $['versionHistory.nameIt'], { ns: 'workflow' })}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{markedComment && (
|
||||
<>
|
||||
<p className="line-clamp-3 system-xs-regular wrap-break-word text-text-tertiary">
|
||||
{markedComment}
|
||||
</p>
|
||||
<span aria-hidden className="my-1 h-px w-4 bg-divider-regular" />
|
||||
</>
|
||||
)}
|
||||
{publishedTimestamp && (
|
||||
<p className="system-xs-regular text-text-tertiary">
|
||||
{publisherName
|
||||
? t(($) => $['common.publishedBy'], {
|
||||
ns: 'workflow',
|
||||
time: formatTimeFromNow(publishedTimestamp),
|
||||
author: publisherName,
|
||||
})
|
||||
: `${t(($) => $['common.publishedAt'], { ns: 'workflow' })} ${formatTimeFromNow(publishedTimestamp)}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-w-0 flex-1 items-start justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<p className="truncate system-sm-semibold text-text-secondary">
|
||||
{t(($) => $['common.latestPublished'], { ns: 'workflow' })}
|
||||
</p>
|
||||
{publishedTimestamp && (
|
||||
<p className="truncate system-xs-regular text-text-tertiary">
|
||||
{publisherName
|
||||
? t(($) => $['common.publishedBy'], {
|
||||
ns: 'workflow',
|
||||
time: formatTimeFromNow(publishedTimestamp),
|
||||
author: publisherName,
|
||||
})
|
||||
: `${t(($) => $['common.publishedAt'], { ns: 'workflow' })} ${formatTimeFromNow(publishedTimestamp)}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isChatApp && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className="h-6 shrink-0 gap-1"
|
||||
onClick={handleRestore}
|
||||
disabled={!resolvedHasUnpublishedChanges}
|
||||
>
|
||||
<span aria-hidden className="i-ri-reset-left-line size-3.5" />
|
||||
{t(($) => $['common.restore'], { ns: 'workflow' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 pt-3">
|
||||
<div className="flex h-6 items-center system-xs-medium-uppercase text-text-tertiary">
|
||||
{publishedAt
|
||||
? t(($) => $['common.latestPublished'], { ns: 'workflow' })
|
||||
: t(($) => $['common.currentDraftUnpublished'], { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="flex w-full flex-col">
|
||||
{debugWithMultipleModel ? (
|
||||
<PublishWithMultipleModel
|
||||
disabled={publishDisabled}
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onSelect={(item) => handlePublish(item)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{publishedAt ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center system-sm-medium text-text-secondary">
|
||||
{t(($) => $['common.publishedAt'], { ns: 'workflow' })} {formatTimeFromNow(publishedAt)}
|
||||
</div>
|
||||
{isChatApp && (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => handlePublish()}
|
||||
disabled={publishButtonDisabled}
|
||||
variant="secondary-accent"
|
||||
size="small"
|
||||
onClick={handleRestore}
|
||||
disabled={published}
|
||||
>
|
||||
{publishButtonDisabled ? (
|
||||
publishButtonLabel
|
||||
) : (
|
||||
<span className="flex items-center gap-1">
|
||||
<span>{publishButtonLabel}</span>
|
||||
<KbdGroup aria-hidden>
|
||||
{APP_PUBLISH_HOTKEY.split('+').map((key) => (
|
||||
<Kbd key={key} color="white">
|
||||
{formatForDisplay(key)}
|
||||
</Kbd>
|
||||
))}
|
||||
</KbdGroup>
|
||||
</span>
|
||||
)}
|
||||
{t(($) => $['common.restore'], { ns: 'workflow' })}
|
||||
</Button>
|
||||
{startNodeLimitExceeded && (
|
||||
<div className="mt-3 flex flex-col items-stretch">
|
||||
<p
|
||||
className="text-sm/5 font-semibold text-transparent"
|
||||
style={upgradeHighlightStyle}
|
||||
>
|
||||
<span className="block">
|
||||
{t(($) => $['publishLimit.startNodeTitlePrefix'], { ns: 'workflow' })}
|
||||
</span>
|
||||
<span className="block">
|
||||
{t(($) => $['publishLimit.startNodeTitleSuffix'], { ns: 'workflow' })}
|
||||
</span>
|
||||
</p>
|
||||
<p className="mt-1 text-xs/4 text-text-secondary">
|
||||
{t(($) => $['publishLimit.startNodeDesc'], { ns: 'workflow' })}
|
||||
</p>
|
||||
<UpgradeBtn isShort className="mt-2.25 mb-3 h-8 w-23.25 self-start" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center system-sm-medium text-text-secondary">
|
||||
{t(($) => $['common.autoSaved'], { ns: 'workflow' })} ·
|
||||
{Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)}
|
||||
</div>
|
||||
)}
|
||||
{debugWithMultipleModel ? (
|
||||
<PublishWithMultipleModel
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onSelect={(item) => handlePublish(item)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="mt-3 w-full"
|
||||
onClick={() => handlePublish()}
|
||||
disabled={publishDisabled || published}
|
||||
>
|
||||
{published ? (
|
||||
t(($) => $['common.published'], { ns: 'workflow' })
|
||||
) : (
|
||||
<div className="flex gap-1">
|
||||
<span>{t(($) => $['common.publishUpdate'], { ns: 'workflow' })}</span>
|
||||
<KbdGroup>
|
||||
{APP_PUBLISH_HOTKEY.split('+').map((key) => (
|
||||
<Kbd key={key} color="white">
|
||||
{formatForDisplay(key)}
|
||||
</Kbd>
|
||||
))}
|
||||
</KbdGroup>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 py-0.5 pr-0.5 pl-1">
|
||||
<PublisherTimelineMarker position="bottom" />
|
||||
<p className="min-w-0 flex-1 truncate system-xs-regular text-text-tertiary">
|
||||
{resolvedHasUnpublishedChanges ? (
|
||||
<>
|
||||
{t(($) => $['common.unpublishedChanges'], { ns: 'workflow' })}
|
||||
{isWorkflowApp && Boolean(draftUpdatedAt) && (
|
||||
<>
|
||||
{' · '}
|
||||
{t(($) => $['common.savedAt'], {
|
||||
ns: 'workflow',
|
||||
time: formatTimeFromNow(draftUpdatedAt!),
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
t(($) => $['common.noChanges'], { ns: 'workflow' })
|
||||
</Button>
|
||||
{startNodeLimitExceeded && (
|
||||
<div className="mt-3 flex flex-col items-stretch">
|
||||
<p className="text-sm/5 font-semibold text-transparent" style={upgradeHighlightStyle}>
|
||||
<span className="block">
|
||||
{t(($) => $['publishLimit.startNodeTitlePrefix'], { ns: 'workflow' })}
|
||||
</span>
|
||||
<span className="block">
|
||||
{t(($) => $['publishLimit.startNodeTitleSuffix'], { ns: 'workflow' })}
|
||||
</span>
|
||||
</p>
|
||||
<p className="mt-1 text-xs/4 text-text-secondary">
|
||||
{t(($) => $['publishLimit.startNodeDesc'], { ns: 'workflow' })}
|
||||
</p>
|
||||
<UpgradeBtn isShort className="mt-[9px] mb-[12px] h-[32px] w-[93px] self-start" />
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -348,9 +219,8 @@ export const PublisherAccessSection = ({
|
||||
{t(($) => $['publishApp.title'], { ns: 'app' })}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-pointer items-center gap-x-0.5 rounded-lg border-0 bg-components-input-bg-normal py-1 pr-2 pl-2.5 text-left outline-hidden hover:bg-primary-50 hover:text-text-accent focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
<div
|
||||
className="flex h-8 cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pr-2 pl-2.5 hover:bg-primary-50 hover:text-text-accent"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex grow items-center gap-x-1.5 overflow-hidden pr-1">
|
||||
@@ -364,7 +234,7 @@ export const PublisherAccessSection = ({
|
||||
<div className="flex size-4 shrink-0 items-center justify-center">
|
||||
<span className="i-ri-arrow-right-s-line size-4 text-text-quaternary" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{!isAppAccessSet && (
|
||||
<p className="mt-1 system-xs-regular text-text-warning">
|
||||
{t(($) => $['publishApp.notSetDesc'], { ns: 'app' })}
|
||||
@@ -376,114 +246,143 @@ export const PublisherAccessSection = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ActionTooltip = ({
|
||||
disabled,
|
||||
tooltip,
|
||||
children,
|
||||
}: {
|
||||
disabled: boolean
|
||||
tooltip?: ReactNode
|
||||
children: ReactNode
|
||||
}) => {
|
||||
if (!disabled || !tooltip) return <>{children}</>
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div className="flex">{children}</div>} />
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export const PublisherActionsSection = ({
|
||||
appDetail,
|
||||
appURL,
|
||||
disabledFunctionButton,
|
||||
disabledFunctionTooltip,
|
||||
handleEmbed,
|
||||
handleOpenInExplore,
|
||||
handleOpenRunConfig,
|
||||
hasHumanInputNode = false,
|
||||
hasTriggerNode = false,
|
||||
marketplaceActionDisabled = false,
|
||||
missingStartNode = false,
|
||||
publishedAt,
|
||||
publishingToMarketplace = false,
|
||||
showDeployAction = false,
|
||||
showMarketplaceAction = false,
|
||||
showBatchRunConfig = false,
|
||||
showRunConfig = false,
|
||||
toolPublished = false,
|
||||
toolPublished,
|
||||
workflowToolAvailable = true,
|
||||
workflowToolIsLoading,
|
||||
workflowToolOutdated,
|
||||
workflowToolMessage,
|
||||
workflowToolOutdated = false,
|
||||
onConfigureWorkflowTool,
|
||||
onPublishToMarketplace,
|
||||
}: ActionsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const appId = appDetail?.id
|
||||
const hasPublishedVersion = Boolean(publishedAt)
|
||||
const showOpenWebApp = !hasTriggerNode
|
||||
const showDeploy = Boolean(showDeployAction && appId)
|
||||
const showWorkflowTool =
|
||||
appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && !hasTriggerNode
|
||||
const navigationDisabled = !hasPublishedVersion || !appId
|
||||
const workflowToolDisabled =
|
||||
!hasPublishedVersion || !workflowToolAvailable || (toolPublished && workflowToolIsLoading)
|
||||
if (hasTriggerNode) return null
|
||||
|
||||
const workflowToolDisabled = !publishedAt || !workflowToolAvailable
|
||||
|
||||
return (
|
||||
<div className="flex flex-col border-t-[0.5px] border-t-divider-regular p-3">
|
||||
{showOpenWebApp && (
|
||||
<div className="flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3">
|
||||
<ActionTooltip disabled={disabledFunctionButton} tooltip={disabledFunctionTooltip}>
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
disabled={disabledFunctionButton}
|
||||
link={appURL}
|
||||
icon={<span className="i-ri-play-circle-line size-4" />}
|
||||
actionButton={
|
||||
showRunConfig
|
||||
? {
|
||||
ariaLabel: t(($) => $['operation.config'], { ns: 'common' }),
|
||||
icon: <span className="i-ri-settings-2-line size-4" />,
|
||||
onClick: () => handleOpenRunConfig?.(appURL),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{t(($) => $['common.runApp'], { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</ActionTooltip>
|
||||
{appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION ? (
|
||||
<ActionTooltip disabled={disabledFunctionButton} tooltip={disabledFunctionTooltip}>
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
disabled={disabledFunctionButton}
|
||||
description={
|
||||
disabledFunctionButton && disabledFunctionTooltip
|
||||
? disabledFunctionTooltip
|
||||
: t(($) => $['common.openWebAppDescription'], { ns: 'workflow' })
|
||||
}
|
||||
external
|
||||
focusableWhenDisabled={Boolean(disabledFunctionTooltip)}
|
||||
link={appURL}
|
||||
icon={<span className="i-ri-planet-line size-4" />}
|
||||
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
||||
icon={<span className="i-ri-play-list-2-line size-4" />}
|
||||
actionButton={
|
||||
showRunConfig && handleOpenRunConfig
|
||||
showBatchRunConfig
|
||||
? {
|
||||
ariaLabel: t(($) => $['operation.config'], { ns: 'common' }),
|
||||
icon: <span className="i-ri-settings-2-line size-4" />,
|
||||
onClick: () => handleOpenRunConfig(appURL),
|
||||
onClick: () =>
|
||||
handleOpenRunConfig?.(
|
||||
`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{t(($) => $['common.openWebApp'], { ns: 'workflow' })}
|
||||
{t(($) => $['common.batchRunApp'], { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</ActionTooltip>
|
||||
) : (
|
||||
<SuggestedAction
|
||||
onClick={handleEmbed}
|
||||
disabled={!publishedAt}
|
||||
icon={<span className="i-custom-vender-line-development-code-browser size-4" />}
|
||||
>
|
||||
{t(($) => $['common.embedIntoSite'], { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
<SuggestedAction
|
||||
disabled={navigationDisabled}
|
||||
description={t(($) => $['common.accessPointDescription'], { ns: 'workflow' })}
|
||||
link={appId ? `/app/${appId}/access-point` : undefined}
|
||||
icon={<span className="i-custom-vender-agent-v2-access-point size-4" />}
|
||||
<ActionTooltip disabled={disabledFunctionButton} tooltip={disabledFunctionTooltip}>
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
if (publishedAt) handleOpenInExplore()
|
||||
}}
|
||||
disabled={disabledFunctionButton}
|
||||
icon={<span className="i-ri-planet-line size-4" />}
|
||||
>
|
||||
{t(($) => $['common.openInExplore'], { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
</ActionTooltip>
|
||||
<ActionTooltip
|
||||
disabled={!publishedAt || missingStartNode}
|
||||
tooltip={
|
||||
!publishedAt
|
||||
? t(($) => $.notPublishedYet, { ns: 'app' })
|
||||
: t(($) => $.noUserInputNode, { ns: 'app' })
|
||||
}
|
||||
>
|
||||
{t(($) => $['appMenus.accessPoint'], { ns: 'common' })}
|
||||
</SuggestedAction>
|
||||
{showDeploy && (
|
||||
<SuggestedAction
|
||||
disabled={navigationDisabled}
|
||||
description={t(($) => $['common.deployDescription'], { ns: 'workflow' })}
|
||||
link={`/app/${appId}/deploy`}
|
||||
icon={<span className="i-ri-instance-line size-4" />}
|
||||
className="flex-1"
|
||||
disabled={!publishedAt || missingStartNode}
|
||||
link="./develop"
|
||||
icon={<span className="i-ri-terminal-box-line size-4" />}
|
||||
>
|
||||
{t(($) => $['appMenus.deploy'], { ns: 'common' })}
|
||||
{t(($) => $['common.accessAPIReference'], { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
{showMarketplaceAction && (
|
||||
<SuggestedAction
|
||||
disabled={marketplaceActionDisabled || publishingToMarketplace || !onPublishToMarketplace}
|
||||
description={t(($) => $['common.publishToMarketplaceDescription'], {
|
||||
ns: 'workflow',
|
||||
})}
|
||||
icon={<span className="i-ri-store-2-line size-4" />}
|
||||
onClick={onPublishToMarketplace}
|
||||
>
|
||||
{publishingToMarketplace
|
||||
? t(($) => $['common.publishingToMarketplace'], { ns: 'workflow' })
|
||||
: t(($) => $['common.publishToMarketplace'], { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
{showWorkflowTool && (
|
||||
<>
|
||||
<div aria-hidden className="m-1 h-px bg-divider-subtle" />
|
||||
<WorkflowToolAction
|
||||
disabled={workflowToolDisabled}
|
||||
isLoading={workflowToolIsLoading}
|
||||
message={workflowToolMessage}
|
||||
outdated={workflowToolOutdated}
|
||||
published={toolPublished}
|
||||
onConfigure={onConfigureWorkflowTool}
|
||||
/>
|
||||
</>
|
||||
</ActionTooltip>
|
||||
{appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && (
|
||||
<WorkflowToolConfigureButton
|
||||
disabled={workflowToolDisabled}
|
||||
published={!!toolPublished}
|
||||
isLoading={workflowToolIsLoading}
|
||||
outdated={workflowToolOutdated}
|
||||
onConfigure={onConfigureWorkflowTool}
|
||||
disabledReason={workflowToolMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,166 +1,80 @@
|
||||
import type { PropsWithChildren, MouseEvent as ReactMouseEvent, ReactNode } from 'react'
|
||||
import type { HTMLProps, PropsWithChildren, MouseEvent as ReactMouseEvent } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useId } from 'react'
|
||||
import Link from '@/next/link'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
|
||||
type SuggestedActionButton = {
|
||||
ariaLabel: string
|
||||
icon: ReactNode
|
||||
icon: React.ReactNode
|
||||
onClick: (event: ReactMouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
type SuggestedActionProps = PropsWithChildren<{
|
||||
icon?: ReactNode
|
||||
link?: string
|
||||
external?: boolean
|
||||
disabled?: boolean
|
||||
focusableWhenDisabled?: boolean
|
||||
description?: ReactNode
|
||||
endIcon?: ReactNode
|
||||
actionButton?: SuggestedActionButton
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}>
|
||||
type SuggestedActionProps = PropsWithChildren<
|
||||
HTMLProps<HTMLAnchorElement> & {
|
||||
icon?: React.ReactNode
|
||||
link?: string
|
||||
disabled?: boolean
|
||||
actionButton?: SuggestedActionButton
|
||||
}
|
||||
>
|
||||
|
||||
const SuggestedAction = ({
|
||||
icon,
|
||||
link,
|
||||
external = false,
|
||||
disabled = false,
|
||||
focusableWhenDisabled = false,
|
||||
description,
|
||||
endIcon,
|
||||
actionButton,
|
||||
disabled,
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
actionButton,
|
||||
...props
|
||||
}: SuggestedActionProps) => {
|
||||
const id = useId()
|
||||
const labelId = `${id}-label`
|
||||
const descriptionId = `${id}-description`
|
||||
const interactiveClassName = cn(
|
||||
'group flex h-10 min-w-0 items-center gap-2 border-0 bg-transparent p-1 text-left outline-hidden transition-colors',
|
||||
actionButton ? 'flex-1 rounded-l-lg' : 'w-full rounded-lg',
|
||||
disabled
|
||||
? cn(
|
||||
'cursor-not-allowed',
|
||||
!actionButton && 'opacity-30',
|
||||
focusableWhenDisabled && 'focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
)
|
||||
: 'cursor-pointer hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
!actionButton && className,
|
||||
)
|
||||
const content = (
|
||||
<>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'flex size-8 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-components-panel-on-panel-item-bg text-text-secondary shadow-xs transition-colors',
|
||||
!disabled && 'group-hover:text-text-accent group-focus-visible:text-text-accent',
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span
|
||||
id={labelId}
|
||||
className={cn(
|
||||
'block truncate system-sm-medium text-text-secondary transition-colors',
|
||||
!disabled && 'group-hover:text-text-primary group-focus-visible:text-text-primary',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
{description && (
|
||||
<span
|
||||
id={descriptionId}
|
||||
className={cn(
|
||||
'hidden truncate system-xs-regular text-text-tertiary',
|
||||
!disabled && 'group-hover:block group-focus-visible:block',
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="flex min-h-4 min-w-4 shrink-0 items-center justify-center text-text-quaternary">
|
||||
{endIcon ?? <span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
const accessibilityProps = {
|
||||
'aria-labelledby': labelId,
|
||||
'aria-describedby': description ? descriptionId : undefined,
|
||||
const handleClick = (event: ReactMouseEvent<HTMLAnchorElement>) => {
|
||||
if (disabled) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
onClick?.(event)
|
||||
}
|
||||
|
||||
let mainAction: ReactNode
|
||||
const handleActionClick = (event: ReactMouseEvent<HTMLButtonElement>) => {
|
||||
if (disabled) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
mainAction = (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!focusableWhenDisabled}
|
||||
aria-disabled={focusableWhenDisabled || undefined}
|
||||
className={interactiveClassName}
|
||||
onClick={
|
||||
focusableWhenDisabled
|
||||
? (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onKeyDown={
|
||||
focusableWhenDisabled
|
||||
? (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') event.preventDefault()
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
{...accessibilityProps}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
} else if (!link) {
|
||||
mainAction = (
|
||||
<button
|
||||
type="button"
|
||||
className={interactiveClassName}
|
||||
onClick={onClick}
|
||||
{...accessibilityProps}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
} else if (external) {
|
||||
mainAction = (
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={interactiveClassName}
|
||||
onClick={onClick}
|
||||
{...accessibilityProps}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
mainAction = (
|
||||
<Link href={link} className={interactiveClassName} onClick={onClick} {...accessibilityProps}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
actionButton?.onClick(event)
|
||||
}
|
||||
|
||||
const mainAction = (
|
||||
<a
|
||||
href={disabled ? undefined : link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={cn(
|
||||
'flex min-w-0 items-center justify-start gap-2 px-2.5 py-2 text-text-secondary transition-colors',
|
||||
actionButton
|
||||
? 'flex-1 rounded-l-lg'
|
||||
: 'rounded-lg bg-background-section-burn not-first:mt-1',
|
||||
disabled
|
||||
? 'cursor-not-allowed opacity-30 shadow-xs'
|
||||
: 'cursor-pointer hover:bg-state-accent-hover hover:text-text-accent',
|
||||
)}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative size-4 shrink-0">{icon}</div>
|
||||
<div className="shrink grow basis-0 system-sm-medium">{children}</div>
|
||||
<RiArrowRightUpLine className="size-3.5 shrink-0" />
|
||||
</a>
|
||||
)
|
||||
|
||||
if (!actionButton) return mainAction
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full min-w-0 items-stretch rounded-lg',
|
||||
disabled && 'opacity-30',
|
||||
'flex items-stretch rounded-lg bg-background-section-burn not-first:mt-1',
|
||||
disabled ? 'opacity-30 shadow-xs' : '',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -170,16 +84,14 @@ const SuggestedAction = ({
|
||||
aria-label={actionButton.ariaLabel}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex w-9 shrink-0 items-center justify-center rounded-r-lg border-l-[0.5px] border-divider-subtle text-text-tertiary outline-hidden transition-colors',
|
||||
'flex w-9 shrink-0 items-center justify-center rounded-r-lg border-l-[0.5px] border-divider-subtle text-text-tertiary transition-colors',
|
||||
disabled
|
||||
? 'cursor-not-allowed'
|
||||
: 'cursor-pointer hover:bg-state-base-hover hover:text-text-accent focus-visible:bg-state-base-hover focus-visible:text-text-accent focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
: 'cursor-pointer hover:bg-state-accent-hover hover:text-text-accent',
|
||||
)}
|
||||
onClick={actionButton.onClick}
|
||||
onClick={handleActionClick}
|
||||
>
|
||||
<span aria-hidden className="flex size-4 items-center justify-center">
|
||||
{actionButton.icon}
|
||||
</span>
|
||||
{actionButton.icon}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
type WorkflowToolDisabledReasonProps = {
|
||||
message: string
|
||||
}
|
||||
|
||||
const WorkflowToolDisabledReason = ({ message }: WorkflowToolDisabledReasonProps) => {
|
||||
return <p className="mt-1 px-2.5 pb-2 system-xs-regular text-text-tertiary opacity-30">{message}</p>
|
||||
}
|
||||
|
||||
export default WorkflowToolDisabledReason
|
||||
@@ -1,133 +0,0 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { buildIntegrationPath } from '@/app/components/integrations/routes'
|
||||
import Link from '@/next/link'
|
||||
import SuggestedAction from '../suggested-action'
|
||||
import WorkflowToolDisabledReason from './disabled-reason'
|
||||
import WorkflowToolLoadingStatus from './loading-status'
|
||||
import WorkflowToolSetupStatus from './setup-status'
|
||||
import WorkflowToolStateLabel from './state-label'
|
||||
|
||||
type WorkflowToolActionProps = {
|
||||
disabled: boolean
|
||||
isLoading: boolean
|
||||
message?: string
|
||||
outdated: boolean
|
||||
published: boolean
|
||||
onConfigure: () => void
|
||||
}
|
||||
|
||||
const WorkflowToolAction = ({
|
||||
disabled,
|
||||
isLoading,
|
||||
message,
|
||||
outdated,
|
||||
published,
|
||||
onConfigure,
|
||||
}: WorkflowToolActionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const disabledReason = disabled ? message : undefined
|
||||
const workflowToolLabel = t(($) => $['common.workflowAsTool'], { ns: 'workflow' })
|
||||
|
||||
if (!published || isLoading)
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full rounded-lg'
|
||||
)}
|
||||
>
|
||||
<SuggestedAction
|
||||
className="flex-1"
|
||||
disabled={disabled}
|
||||
description={t(($) => $['common.workflowAsToolDescription'], { ns: 'workflow' })}
|
||||
endIcon={
|
||||
isLoading ? (
|
||||
<WorkflowToolLoadingStatus label={t(($) => $.loading, { ns: 'appApi' })} />
|
||||
) : (
|
||||
<WorkflowToolSetupStatus
|
||||
label={t(($) => $['common.configureRequired'], { ns: 'workflow' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
icon={<span className="i-ri-hammer-line size-4" />}
|
||||
onClick={onConfigure}
|
||||
>
|
||||
{workflowToolLabel}
|
||||
</SuggestedAction>
|
||||
{disabledReason && <WorkflowToolDisabledReason message={disabledReason} />}
|
||||
</div>
|
||||
)
|
||||
|
||||
const configureLabel = outdated
|
||||
? t(($) => $['common.workflowAsToolReconfigure'], { ns: 'workflow' })
|
||||
: t(($) => $['common.configure'], { ns: 'workflow' })
|
||||
const manageInToolsLabel = t(($) => $['common.manageInTools'], { ns: 'workflow' })
|
||||
const stateLabel = outdated
|
||||
? t(($) => $['common.workflowAsToolUpdateNeeded'], { ns: 'workflow' })
|
||||
: t(($) => $['common.workflowAsToolReady'], { ns: 'workflow' })
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full flex-col rounded-lg',
|
||||
disabled && 'opacity-30',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2 p-1">
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex size-8 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-components-panel-on-panel-item-bg text-text-secondary shadow-xs"
|
||||
>
|
||||
<span className="i-ri-hammer-line size-4" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-h-7 items-center gap-2 pt-1 pr-1 pb-1">
|
||||
<span className="min-w-0 flex-1 truncate system-sm-medium text-text-secondary">
|
||||
{workflowToolLabel}
|
||||
</span>
|
||||
<WorkflowToolStateLabel label={stateLabel} outdated={outdated} />
|
||||
</div>
|
||||
{outdated && (
|
||||
<p className="pr-4 pb-1 system-xs-regular text-text-warning">
|
||||
{t(($) => $['common.workflowAsToolTip'], { ns: 'workflow' })}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1 py-1">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
className="gap-1 px-1.5"
|
||||
onClick={onConfigure}
|
||||
>
|
||||
<span aria-hidden className="i-ri-equalizer-2-line size-3.5" />
|
||||
{configureLabel}
|
||||
</Button>
|
||||
{disabled ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="flex h-6 cursor-not-allowed items-center gap-1 rounded-md px-2 system-xs-medium text-components-button-tertiary-text-disabled"
|
||||
>
|
||||
{manageInToolsLabel}
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href={buildIntegrationPath('workflow-tool')}
|
||||
className="flex h-6 items-center gap-1 rounded-md px-2 system-xs-medium text-text-tertiary outline-hidden hover:bg-components-button-tertiary-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
>
|
||||
{manageInToolsLabel}
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{disabledReason && <WorkflowToolDisabledReason message={disabledReason} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowToolAction
|
||||
@@ -1,15 +0,0 @@
|
||||
type WorkflowToolLoadingStatusProps = {
|
||||
label: string
|
||||
}
|
||||
|
||||
const WorkflowToolLoadingStatus = ({ label }: WorkflowToolLoadingStatusProps) => {
|
||||
return (
|
||||
<span
|
||||
role="status"
|
||||
aria-label={label}
|
||||
className="i-ri-loader-2-line size-4 animate-spin motion-reduce:animate-none"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowToolLoadingStatus
|
||||
@@ -1,17 +0,0 @@
|
||||
type WorkflowToolSetupStatusProps = {
|
||||
label: string
|
||||
}
|
||||
|
||||
const WorkflowToolSetupStatus = ({ label }: WorkflowToolSetupStatusProps) => {
|
||||
return (
|
||||
<span
|
||||
role="status"
|
||||
aria-label={label}
|
||||
className="rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowToolSetupStatus
|
||||
@@ -1,25 +0,0 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
|
||||
type WorkflowToolStateLabelProps = {
|
||||
label: string
|
||||
outdated: boolean
|
||||
}
|
||||
|
||||
const WorkflowToolStateLabel = ({ label, outdated }: WorkflowToolStateLabelProps) => {
|
||||
return (
|
||||
<span
|
||||
role="status"
|
||||
aria-label={label}
|
||||
className={cn(
|
||||
'flex shrink-0 items-center gap-1 system-xs-semibold-uppercase',
|
||||
outdated ? 'text-util-colors-warning-warning-600' : 'text-util-colors-green-green-600',
|
||||
)}
|
||||
>
|
||||
<StatusDot size="small" status={outdated ? 'warning' : 'success'} />
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowToolStateLabel
|
||||
@@ -214,11 +214,6 @@ const createViewModel = (
|
||||
publishedConfig: {
|
||||
modelConfig: createContextValue().modelConfig,
|
||||
completionParams: {},
|
||||
promptMode: createContextValue().promptMode,
|
||||
chatPromptConfig: createContextValue().chatPromptConfig,
|
||||
completionPromptConfig: createContextValue().completionPromptConfig,
|
||||
datasetConfigs: createContextValue().datasetConfigs,
|
||||
externalDataToolsConfig: createContextValue().externalDataToolsConfig,
|
||||
},
|
||||
resetAppConfig: vi.fn(),
|
||||
} as ComponentProps<typeof AppPublisher>,
|
||||
@@ -253,7 +248,6 @@ const createViewModel = (
|
||||
onCompletionParamsChange: vi.fn(),
|
||||
onConfirmUseGPT4: vi.fn(),
|
||||
onEnableMultipleModelDebug: vi.fn(),
|
||||
onFeatureStoreChange: vi.fn(),
|
||||
onFeaturesChange: vi.fn(),
|
||||
onHideDebugPanel: vi.fn(),
|
||||
onModelChange: vi.fn(),
|
||||
|
||||
@@ -99,7 +99,6 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
|
||||
onCompletionParamsChange,
|
||||
onConfirmUseGPT4,
|
||||
onEnableMultipleModelDebug,
|
||||
onFeatureStoreChange,
|
||||
onFeaturesChange,
|
||||
onHideDebugPanel,
|
||||
onModelChange,
|
||||
@@ -129,7 +128,7 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={contextValue}>
|
||||
<FeaturesProvider features={featuresData} onFeaturesChange={onFeatureStoreChange}>
|
||||
<FeaturesProvider features={featuresData}>
|
||||
<MittProvider>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="relative flex h-[200px] grow pt-14">
|
||||
|
||||
@@ -1143,7 +1143,6 @@ describe('Debug', () => {
|
||||
enabled: true,
|
||||
}),
|
||||
}),
|
||||
{ silent: true },
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -369,7 +369,7 @@ const Debug: FC<IDebug> = ({
|
||||
enabled: supportedVision,
|
||||
}
|
||||
})
|
||||
setFeatures(newFeatures, { silent: true })
|
||||
setFeatures(newFeatures)
|
||||
}
|
||||
}, [debugWithMultipleModel, featuresStore, mode, multipleModelConfigs, textGenerationModelList])
|
||||
|
||||
|
||||
-3
@@ -24,7 +24,6 @@ describe('useAdvancedPromptConfig', () => {
|
||||
|
||||
it('should update the advanced chat prompt and mark user changes', () => {
|
||||
const handleUserChangedPrompt = vi.fn()
|
||||
const handlePublishConfigChange = vi.fn()
|
||||
const { result } = renderHook(() =>
|
||||
useAdvancedPromptConfig({
|
||||
appMode: AppModeEnum.CHAT,
|
||||
@@ -37,7 +36,6 @@ describe('useAdvancedPromptConfig', () => {
|
||||
completionParams: {},
|
||||
setCompletionParams: vi.fn(),
|
||||
setStop: vi.fn(),
|
||||
onPublishConfigChange: handlePublishConfigChange,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -53,7 +51,6 @@ describe('useAdvancedPromptConfig', () => {
|
||||
])
|
||||
expect(result.current.hasSetBlockStatus.query).toBe(true)
|
||||
expect(handleUserChangedPrompt).toHaveBeenCalledTimes(1)
|
||||
expect(handlePublishConfigChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should derive simple prompt block status from the pre-prompt', () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user