Compare commits

..
Author SHA1 Message Date
fatelei 2bb01984ed fix: fix conflict 2026-07-28 19:03:11 +08:00
fatelei 77e41a37ee feat: agent can op file and fix version check 2026-07-28 18:36:06 +08:00
zxhlyh 79229b7ede fix(web): preserve IME composition in skill builder 2026-07-28 17:46:12 +08:00
zxhlyh 36ae648a63 fix(web): protect skill bindings in version view 2026-07-28 16:21:43 +08:00
zxhlyh c409d531da fix(web): paginate workspace skills list 2026-07-28 15:49:44 +08:00
autofix-ci[bot]andGitHub 511ca69e4e [autofix.ci] apply automated fixes 2026-07-28 07:17:11 +00:00
fatelei 4c7e60d7e7 fix: fix db version 2026-07-28 15:12:58 +08:00
fatelei a9e163403d fix: fix model not change 2026-07-28 14:52:16 +08:00
zxhlyhandfatelei bc136b89f9 fix(web): prevent duplicate skill autosaves 2026-07-28 14:52:16 +08:00
fatelei b1bd4f9a8b fix: fix model not update 2026-07-28 14:52:16 +08:00
fatelei 06d3927f05 fix: fix failed test 2026-07-28 14:52:16 +08:00
fatelei 5c3516cae8 fix: fix skill not in agent 2026-07-28 14:52:16 +08:00
fatelei 1c18d8ddbd fix: fix frontend issue and let draft check relax 2026-07-28 14:52:16 +08:00
fatelei 253dfe9351 fix: fix ci failed 2026-07-28 14:52:15 +08:00
fatelei 2d3999e984 fix: fix ci failed 2026-07-28 14:52:15 +08:00
fatelei a431cc726d fix: fix scroll and live mode can not enter 2026-07-28 14:52:15 +08:00
fatelei 08493d2429 fix: fix some bugs 2026-07-28 14:52:15 +08:00
fatelei 8dd0969006 chore: adjust skill entrance 2026-07-28 14:52:15 +08:00
fatelei 2d80d3c35c fix: fix delete skill ref 2026-07-28 14:52:14 +08:00
fatelei 4da764904c feat: support agent skill 2026-07-28 14:46:34 +08:00
fatelei 5c4f4fd1ef Merge branch 'skill' into deploy/dev 2026-07-28 14:44:57 +08:00
fatelei 54843971ac fix: fix model not change 2026-07-28 14:43:05 +08:00
zxhlyh 5ec5d3aeb9 fix(web): prevent duplicate skill autosaves 2026-07-28 14:30:17 +08:00
fatelei d661b53e49 fix: fix model not update 2026-07-28 14:10:49 +08:00
zxhlyh fd1e777f85 Merge branch 'codex/fix-skill-autosave-conflict' into deploy/dev 2026-07-28 14:04:25 +08:00
zxhlyh e5f77ce185 fix(web): prevent duplicate skill autosaves 2026-07-28 14:02:18 +08:00
fatelei 99929d1c16 fix: fix conflict 2026-07-27 08:12:58 +08:00
fatelei c699aa11db fix: fix failed test 2026-07-26 23:42:50 +08:00
fatelei 06cd0b56ac fix: fix skill not in agent 2026-07-26 21:00:38 +08:00
fatelei 241a9e1fec fix: fix frontend issue and let draft check relax 2026-07-26 21:00:38 +08:00
fatelei 3d79689fb5 fix: fix ci failed 2026-07-26 20:59:56 +08:00
fatelei 9f050f7957 fix: fix ci failed 2026-07-26 20:59:56 +08:00
fatelei cb691d47d3 fix: fix scroll and live mode can not enter 2026-07-26 20:57:29 +08:00
fatelei 04f5ee58d0 fix: fix some bugs 2026-07-26 20:57:29 +08:00
fatelei f5cff724f4 chore: adjust skill entrance 2026-07-26 20:57:29 +08:00
fatelei 1f4ccafdea fix: fix delete skill ref 2026-07-26 20:57:29 +08:00
fatelei 163049f6ca feat: support agent skill 2026-07-26 20:54:32 +08:00
fatelei 65e7507ca3 Merge branch 'skill' into deploy/dev 2026-07-24 13:38:15 +08:00
fatelei ba157f9604 fix: fix skill not in agent 2026-07-24 13:37:19 +08:00
fatelei 0dc913630e fix: fix conflict 2026-07-24 13:18:31 +08:00
fatelei 875cd30b1f fix: fix frontend issue and let draft check relax 2026-07-24 13:11:18 +08:00
fatelei aa4a32ae84 fix: fix ci failed 2026-07-24 11:13:44 +08:00
fatelei 8573e14777 fix: fix ci failed 2026-07-24 11:13:44 +08:00
fatelei 1855be234c fix: fix scroll and live mode can not enter 2026-07-24 11:13:44 +08:00
fatelei 9bb960ff12 fix: fix some bugs 2026-07-24 11:13:44 +08:00
fatelei 1c14c7d467 chore: adjust skill entrance 2026-07-24 11:13:44 +08:00
fatelei 61faec16ca fix: fix delete skill ref 2026-07-24 11:13:44 +08:00
fatelei 57c836e692 feat: support agent skill 2026-07-24 11:13:43 +08:00
samzong 626cc282b1 fix(web): echo OAuth state on authorize redirect (#39459)
Signed-off-by: samzong <samzong.lu@gmail.com>
2026-07-23 06:36:58 -04:00
fatelei 52624d54e3 fix: fix db migration 2026-07-23 16:28:27 +08:00
fatelei 5ce038ef92 fix: fix db migration 2026-07-23 16:25:33 +08:00
fatelei 30f4d4c0c6 fix: fix db migration 2026-07-23 16:04:09 +08:00
fatelei 510679a7d1 fix: fix db migration 2026-07-23 15:38:05 +08:00
fatelei 9237f2a14a fix: fix conflict 2026-07-23 15:23:09 +08:00
fatelei bd178c7b29 Merge branch 'skill' into deploy/dev 2026-07-23 14:38:04 +08:00
fatelei d80947aa72 fix: fix scroll and live mode can not enter 2026-07-23 14:36:44 +08:00
fatelei 1618c37d26 Merge branch 'skill' into deploy/dev 2026-07-23 14:17:52 +08:00
fatelei 701ab64462 fix: fix some bugs 2026-07-23 14:14:28 +08:00
fatelei b3298800e9 chore: adjust skill entrance 2026-07-22 16:30:38 +08:00
fatelei 0f1c6b3f78 chore: remove agent v2 feature 2026-07-22 16:26:00 +08:00
林玮 (Jade Lin) 9b4b246aad refactor(api): simplify batch cleanup refresh state
(cherry picked from commit 1ded32d949)
2026-07-22 14:49:11 +08:00
林玮 (Jade Lin) 1bd654a289 refactor(api): simplify vector cache refresh metric
(cherry picked from commit fa585cf134)
2026-07-22 14:49:11 +08:00
林玮 (Jade Lin) fc70329bdb refactor(api): derive cleanup tenant from dataset
(cherry picked from commit b124ec86fc)
2026-07-22 14:49:11 +08:00
林玮 (Jade Lin) cd8a82fbd4 fix(api): invalidate vector space cache after cleanup
(cherry picked from commit a07e5c6eb4)
2026-07-22 14:49:11 +08:00
fatelei 550cb7eff5 fix: fix db migration 2026-07-22 14:42:11 +08:00
fatelei 2e748c16e9 Merge branch 'skill' into deploy/dev 2026-07-22 14:17:08 +08:00
fatelei 577012b66d fix: fix delete skill ref 2026-07-22 14:11:55 +08:00
fatelei 251c324180 Merge branch 'skill' into deploy/dev 2026-07-22 12:59:22 +08:00
fatelei 865a618fd5 Revert "feat: support agent skill"
This reverts commit 991116990a.
2026-07-22 12:48:29 +08:00
fatelei 991116990a feat: support agent skill 2026-07-22 11:45:50 +08:00
fatelei eb5d1da0e8 feat: support agent skill 2026-07-22 10:25:25 +08:00
fatelei dce3b7a7fc feat: use celery task 2026-07-10 15:20:06 +08:00
fatelei be386aba3b chore: add switch 2026-07-10 15:19:56 +08:00
JzoNg 02e51e7d7c Merge branch 'main' into deploy/dev 2026-07-10 10:28:07 +08:00
Xiyuan ChenandGitHub 96b6d4f2c0 Merge branch 'fix/socketio-user-connect-app-context' into deploy/dev 2026-07-08 22:02:43 -07:00
GareArc 4c84c5957d fix: run user_connect authorization inside Flask app context
Socket.IO event handlers run in plain gevent greenlets without a Flask
application context, so the db.session() call added in #38227 raises
RuntimeError: Working outside of application context, and every
user_connect is rejected as unauthorized. Wrap the authorization call in
sio.app.app_context(), matching the existing connect handler.
2026-07-08 21:57:07 -07:00
CodingOnStar 34613ecdc5 fix(app): update app creation logic to use database session and ensure RBAC access initialization 2026-07-09 12:00:16 +08:00
CodingOnStar 6a14245401 Merge remote-tracking branch 'origin/main' into deploy/dev 2026-07-09 11:49:16 +08:00
CodingOnStar a758ca2aef fix(amplitude): update API key for amplitude initialization 2026-07-09 11:37:28 +08:00
CodingOnStar 9e60d4e213 Merge branch 'fix/report-slug' into deploy/dev 2026-07-09 11:29:04 +08:00
CodingOnStar ef29c8442c refactor(amplitude): comment out amplitude initialization logic for future implementation 2026-07-09 11:26:59 +08:00
CodingOnStar a1b45415ac fix(web): preserve attribution from auth redirect 2026-07-09 11:00:37 +08:00
fatelei 7fc46d75bd chore: add switch 2026-07-08 18:08:02 +08:00
fatelei 953a4ef0ca feat: create app sync scope and workspace user 2026-07-08 16:12:40 +08:00
237 changed files with 25214 additions and 6757 deletions
-43
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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:
+2
View File
@@ -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",
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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):
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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")
+834
View File
@@ -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",
]
+2
View File
@@ -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,
+1 -1
View File
@@ -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]):
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
@@ -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")
@@ -18,31 +18,109 @@ branch_labels = None
depends_on = None
def _is_pg(conn) -> bool:
return conn.dialect.name == "postgresql"
def _uuid_column(name: str, *, nullable: bool = False, primary_key: bool = False) -> sa.Column:
kwargs = {"nullable": nullable, "primary_key": primary_key}
if primary_key and _is_pg(op.get_bind()):
kwargs["server_default"] = sa.text("uuidv7()")
return sa.Column(name, models.types.StringUUID(), **kwargs)
def _has_table(table_name: str) -> bool:
return sa.inspect(op.get_bind()).has_table(table_name)
def _has_column(table_name: str, column_name: str) -> bool:
return any(
column["name"] == column_name for column in sa.inspect(op.get_bind()).get_columns(table_name)
)
def _has_unique_constraint(table_name: str, constraint_name: str) -> bool:
return any(
constraint["name"] == constraint_name
for constraint in sa.inspect(op.get_bind()).get_unique_constraints(table_name)
)
def upgrade():
if not _has_table("agent_debug_conversations"):
op.create_table(
"agent_debug_conversations",
_uuid_column("id", primary_key=True),
sa.Column("tenant_id", models.types.StringUUID(), nullable=False),
sa.Column("agent_id", models.types.StringUUID(), nullable=False),
sa.Column("app_id", models.types.StringUUID(), nullable=False),
sa.Column("account_id", models.types.StringUUID(), nullable=False),
sa.Column("conversation_id", models.types.StringUUID(), nullable=False),
sa.Column(
"draft_type",
sa.String(length=32),
nullable=False,
server_default=sa.text("'debug_build'"),
),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("agent_debug_conversation_pkey")),
sa.UniqueConstraint(
"tenant_id",
"agent_id",
"account_id",
"draft_type",
name=op.f("agent_debug_conversation_agent_account_draft_type_unique"),
),
)
op.create_index(
"agent_debug_conversation_conversation_idx",
"agent_debug_conversations",
["conversation_id"],
)
op.create_index(
"agent_debug_conversation_account_idx",
"agent_debug_conversations",
["tenant_id", "account_id"],
)
return
# Existing pointers have always represented Build chat because the Agent
# detail API exposes them as ``debug_conversation_id`` for that surface.
op.add_column(
if not _has_column("agent_debug_conversations", "draft_type"):
op.add_column(
"agent_debug_conversations",
sa.Column(
"draft_type",
sa.String(length=32),
nullable=False,
server_default=sa.text("'debug_build'"),
),
)
if _has_unique_constraint(
"agent_debug_conversations",
sa.Column(
"draft_type",
sa.String(length=32),
nullable=False,
server_default=sa.text("'debug_build'"),
),
)
op.drop_constraint(
"agent_debug_conversation_agent_account_unique",
):
op.drop_constraint(
"agent_debug_conversation_agent_account_unique",
"agent_debug_conversations",
type_="unique",
)
if not _has_unique_constraint(
"agent_debug_conversations",
type_="unique",
)
op.create_unique_constraint(
"agent_debug_conversation_agent_account_draft_type_unique",
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id", "draft_type"],
)
):
op.create_unique_constraint(
"agent_debug_conversation_agent_account_draft_type_unique",
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id", "draft_type"],
)
def downgrade():
if not _has_table("agent_debug_conversations"):
return
debug_conversations = sa.table(
"agent_debug_conversations",
sa.column("tenant_id", models.types.StringUUID()),
@@ -64,14 +142,23 @@ def downgrade():
),
)
)
op.drop_constraint(
if _has_unique_constraint(
"agent_debug_conversations",
"agent_debug_conversation_agent_account_draft_type_unique",
):
op.drop_constraint(
"agent_debug_conversation_agent_account_draft_type_unique",
"agent_debug_conversations",
type_="unique",
)
if not _has_unique_constraint(
"agent_debug_conversations",
type_="unique",
)
op.create_unique_constraint(
"agent_debug_conversation_agent_account_unique",
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id"],
)
op.drop_column("agent_debug_conversations", "draft_type")
):
op.create_unique_constraint(
"agent_debug_conversation_agent_account_unique",
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id"],
)
if _has_column("agent_debug_conversations", "draft_type"):
op.drop_column("agent_debug_conversations", "draft_type")
@@ -0,0 +1,21 @@
"""merge skill and agent debug conversation heads
Revision ID: e9f4a1b2c3d5
Revises: a4f8d2c9e1b0, d2825e7b9c10
Create Date: 2026-07-23 15:00:00.000000
"""
# revision identifiers, used by Alembic.
revision = "e9f4a1b2c3d5"
down_revision = ("a4f8d2c9e1b0", "d2825e7b9c10")
branch_labels = None
depends_on = None
def upgrade():
pass
def downgrade():
pass
+7
View File
@@ -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",
+1
View File
@@ -249,6 +249,7 @@ class TagType(StrEnum):
KNOWLEDGE = "knowledge"
APP = "app"
SNIPPET = "snippet"
SKILL = "skill"
class DatasetMetadataType(StrEnum):
+1 -1
View File
@@ -2667,7 +2667,7 @@ class Tag(TypeBase):
sa.Index("tag_name_idx", "name"),
)
TAG_TYPE_LIST = ["knowledge", "app", "snippet"]
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
+164
View File
@@ -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",
]
+728 -2
View File
@@ -9551,7 +9551,7 @@ Remove one or more tag bindings from a target.
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| keyword | query | Search keyword | No | string |
| type | query | Tag type filter | No | string, <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
View File
@@ -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/*
+3 -1
View File
@@ -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:
+46 -20
View File
@@ -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
+81 -12
View File
@@ -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 {
+2 -2
View File
@@ -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
+9
View File
@@ -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
-430
View File
@@ -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()
-961
View File
@@ -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
View File
@@ -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]]
-14
View File
@@ -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"
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+8
View File
@@ -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` |
+3 -3
View File
@@ -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:
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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 -7
View File
@@ -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 />
}
+5
View File
@@ -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) => {
+87 -106
View File
@@ -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"
+187 -288
View File
@@ -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])
@@ -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