feat(agent-v2): sync nightly updates to main (#37599)
Co-authored-by: Jingyi-Dify <jingyi.qi@dify.ai> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: Bond Zhu <783504079@qq.com> Co-authored-by: Yansong Zhang <916125788@qq.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
@ -2,20 +2,28 @@ from uuid import UUID
|
||||
|
||||
from flask import abort, request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic import AliasChoices, BaseModel, Field, field_validator
|
||||
|
||||
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.agent.app_helpers import resolve_agent_app_model
|
||||
from controllers.console.app.app import (
|
||||
AppDetailWithSite,
|
||||
AppDetailWithSite as GenericAppDetailWithSite,
|
||||
)
|
||||
from controllers.console.app.app import (
|
||||
AppListQuery,
|
||||
AppPagination,
|
||||
AppPartial,
|
||||
CopyAppPayload,
|
||||
UpdateAppPayload,
|
||||
_normalize_app_list_query_args,
|
||||
)
|
||||
from controllers.console.app.app import (
|
||||
AppPagination as GenericAppPagination,
|
||||
)
|
||||
from controllers.console.app.app import (
|
||||
AppPartial as GenericAppPartial,
|
||||
)
|
||||
from controllers.console.app.app import (
|
||||
UpdateAppPayload as GenericUpdateAppPayload,
|
||||
)
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
@ -80,7 +88,10 @@ class AgentAppCreatePayload(BaseModel):
|
||||
return role
|
||||
|
||||
|
||||
class AgentAppUpdatePayload(UpdateAppPayload):
|
||||
# Keep agent-app roster DTOs agent-specific instead of reusing the shared
|
||||
# /apps response/request models. The roster surface needs Agent-only fields such
|
||||
# as `role`, while the generic console/apps contracts must stay unchanged.
|
||||
class AgentAppUpdatePayload(GenericUpdateAppPayload):
|
||||
role: str = Field(..., min_length=1, description="Agent role", max_length=255)
|
||||
|
||||
@field_validator("role")
|
||||
@ -100,19 +111,6 @@ class AgentAppPublishedReferenceResponse(BaseModel):
|
||||
app_icon_background: str | None = None
|
||||
|
||||
|
||||
class AgentAppPartial(AppPartial):
|
||||
published_reference_count: int = 0
|
||||
published_references: list[AgentAppPublishedReferenceResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AgentAppPagination(BaseModel):
|
||||
page: int
|
||||
limit: int
|
||||
total: int
|
||||
has_more: bool
|
||||
data: list[AgentAppPartial]
|
||||
|
||||
|
||||
class AgentLogsQuery(BaseModel):
|
||||
page: int = Field(default=1, ge=1, description="Page number")
|
||||
limit: int = Field(default=20, ge=1, le=100, description="Page size")
|
||||
@ -149,6 +147,26 @@ class AgentStatisticsQuery(BaseModel):
|
||||
return value
|
||||
|
||||
|
||||
class AgentAppPartial(GenericAppPartial):
|
||||
app_id: str | None = None
|
||||
role: str | None = None
|
||||
active_config_is_published: bool = False
|
||||
published_reference_count: int = 0
|
||||
published_references: list[AgentAppPublishedReferenceResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AgentAppDetailWithSite(GenericAppDetailWithSite):
|
||||
app_id: str | None = None
|
||||
role: str | None = None
|
||||
active_config_is_published: bool = False
|
||||
|
||||
|
||||
class AgentAppPagination(GenericAppPagination):
|
||||
data: list[AgentAppPartial] = Field( # type: ignore[assignment] # pyrefly: ignore[bad-override-mutable-attribute]
|
||||
validation_alias=AliasChoices("items", "data")
|
||||
)
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
AgentAppCreatePayload,
|
||||
@ -159,14 +177,14 @@ register_schema_models(
|
||||
AgentStatisticsQuery,
|
||||
AgentIdPath,
|
||||
AppListQuery,
|
||||
UpdateAppPayload,
|
||||
RosterListQuery,
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
AppDetailWithSite,
|
||||
AgentAppPagination,
|
||||
AgentAppPublishedReferenceResponse,
|
||||
AgentAppDetailWithSite,
|
||||
AgentAppPartial,
|
||||
AgentConfigSnapshotDetailResponse,
|
||||
AgentConfigSnapshotListResponse,
|
||||
AgentInviteOptionsResponse,
|
||||
@ -184,16 +202,25 @@ def _agent_roster_service() -> AgentRosterService:
|
||||
|
||||
|
||||
def _serialize_agent_app_detail(app_model) -> dict:
|
||||
"""Serialize an Agent App detail using roster-only DTOs.
|
||||
|
||||
`/agent` responses are roster-shaped rather than raw app-shaped: `id`
|
||||
becomes the backing roster Agent id, `app_id` carries the underlying App
|
||||
id, and `role` is injected from the backing roster Agent. Keeping that
|
||||
remap in this serializer lets generated console/agent contracts expose the
|
||||
roster persona fields without widening the shared /apps detail schema.
|
||||
"""
|
||||
|
||||
app_model = AppService().get_app(app_model)
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
|
||||
app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined]
|
||||
|
||||
roster_service = _agent_roster_service()
|
||||
agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=app_model.id)
|
||||
payload = AgentAppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json")
|
||||
agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=str(app_model.id))
|
||||
if not agent:
|
||||
raise AgentNotFoundError()
|
||||
payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json")
|
||||
payload.pop("bound_agent_id", None)
|
||||
payload["app_id"] = str(app_model.id)
|
||||
payload["id"] = agent.id
|
||||
@ -206,6 +233,14 @@ def _serialize_agent_app_detail(app_model) -> dict:
|
||||
|
||||
|
||||
def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
|
||||
"""Serialize Agent App lists with roster-shaped items.
|
||||
|
||||
Each item starts from the shared App list shape, then drops
|
||||
`bound_agent_id`, rewrites `id` to the backing roster Agent id, stores the
|
||||
original App id in `app_id`, and injects roster-only `role` when a backing
|
||||
Agent is present.
|
||||
"""
|
||||
|
||||
app_ids = [str(app.id) for app in app_pagination.items]
|
||||
roster_service = _agent_roster_service()
|
||||
agents_by_app_id = roster_service.load_app_backing_agents_by_app_id(
|
||||
@ -220,7 +255,7 @@ def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
|
||||
tenant_id=tenant_id,
|
||||
agent_ids=[agent.id for agent in agents_by_app_id.values()],
|
||||
)
|
||||
payload = AppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json")
|
||||
payload = AgentAppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json")
|
||||
for item in payload["data"]:
|
||||
app_id = item["id"]
|
||||
item.pop("bound_agent_id", None)
|
||||
@ -294,7 +329,7 @@ class AgentAppListApi(Resource):
|
||||
return _serialize_agent_app_pagination(app_pagination, tenant_id=current_tenant_id)
|
||||
|
||||
@console_ns.expect(console_ns.models[AgentAppCreatePayload.__name__])
|
||||
@console_ns.response(201, "Agent app created successfully", console_ns.models[AppDetailWithSite.__name__])
|
||||
@console_ns.response(201, "Agent app created successfully", console_ns.models[AgentAppDetailWithSite.__name__])
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@console_ns.response(400, "Invalid request parameters")
|
||||
@setup_required
|
||||
@ -322,7 +357,7 @@ class AgentAppListApi(Resource):
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>")
|
||||
class AgentAppApi(Resource):
|
||||
@console_ns.response(200, "Agent app detail", console_ns.models[AppDetailWithSite.__name__])
|
||||
@console_ns.response(200, "Agent app detail", console_ns.models[AgentAppDetailWithSite.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@ -333,7 +368,7 @@ class AgentAppApi(Resource):
|
||||
return _serialize_agent_app_detail(app_model)
|
||||
|
||||
@console_ns.expect(console_ns.models[AgentAppUpdatePayload.__name__])
|
||||
@console_ns.response(200, "Agent app updated successfully", console_ns.models[AppDetailWithSite.__name__])
|
||||
@console_ns.response(200, "Agent app updated successfully", console_ns.models[AgentAppDetailWithSite.__name__])
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@console_ns.response(400, "Invalid request parameters")
|
||||
@setup_required
|
||||
@ -373,7 +408,7 @@ class AgentAppApi(Resource):
|
||||
@console_ns.route("/agent/<uuid:agent_id>/copy")
|
||||
class AgentAppCopyApi(Resource):
|
||||
@console_ns.expect(console_ns.models[CopyAppPayload.__name__])
|
||||
@console_ns.response(201, "Agent app copied successfully", console_ns.models[AppDetailWithSite.__name__])
|
||||
@console_ns.response(201, "Agent app copied successfully", console_ns.models[AgentAppDetailWithSite.__name__])
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@console_ns.response(400, "Invalid request parameters")
|
||||
@setup_required
|
||||
|
||||
@ -30,7 +30,7 @@ from models import Account
|
||||
from models.agent_config_entities import AgentFileRefConfig, AgentSkillRefConfig
|
||||
from models.model import App, AppMode, UploadFile
|
||||
from services.agent.composer_service import AgentComposerService
|
||||
from services.agent.skill_package_service import SkillManifest, SkillPackageError, SkillPackageService
|
||||
from services.agent.skill_package_service import SkillManifest, SkillPackageError
|
||||
from services.agent.skill_standardize_service import SkillStandardizeService
|
||||
from services.agent.skill_tool_inference_service import (
|
||||
SkillToolInferenceError,
|
||||
@ -45,11 +45,18 @@ from services.agent_drive_service import (
|
||||
normalize_drive_key,
|
||||
)
|
||||
from services.agent_service import AgentService
|
||||
from services.file_service import FileService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_WORKFLOW_AGENT_DRIVE_APP_MODES = [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]
|
||||
_AGENT_SKILL_UPLOAD_PARAMS = {
|
||||
"file": {
|
||||
"in": "formData",
|
||||
"type": "file",
|
||||
"required": True,
|
||||
"description": "Skill package (.zip or .skill).",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AgentLogQuery(BaseModel):
|
||||
@ -125,11 +132,6 @@ class AgentSkillUploadResponse(ResponseModel):
|
||||
manifest: SkillManifest
|
||||
|
||||
|
||||
class AgentSkillStandardizeResponse(ResponseModel):
|
||||
skill: AgentSkillRefConfig
|
||||
manifest: SkillManifest
|
||||
|
||||
|
||||
class AgentDriveFileResponse(ResponseModel):
|
||||
name: str
|
||||
drive_key: str
|
||||
@ -156,7 +158,6 @@ register_response_schema_models(
|
||||
AgentDriveFileCommitResponse,
|
||||
AgentDriveFileResponse,
|
||||
AgentLogResponse,
|
||||
AgentSkillStandardizeResponse,
|
||||
AgentSkillUploadResponse,
|
||||
SkillToolInferenceResult,
|
||||
)
|
||||
@ -174,30 +175,9 @@ def _agent_not_bound() -> tuple[dict[str, str], int]:
|
||||
return {"code": "agent_not_bound", "message": "no agent is bound for this app/node"}, 400
|
||||
|
||||
|
||||
def _upload_skill_for_app(*, current_user: Account):
|
||||
if "file" not in request.files:
|
||||
return {"code": "no_file", "message": "no skill file uploaded"}, 400
|
||||
if len(request.files) > 1:
|
||||
return {"code": "too_many_files", "message": "only one skill file is allowed"}, 400
|
||||
def _upload_skill_for_app(*, current_user: Account, app_model: App):
|
||||
"""Upload one skill package and commit its normalized files into the agent drive."""
|
||||
|
||||
upload = request.files["file"]
|
||||
content = upload.stream.read()
|
||||
try:
|
||||
manifest = SkillPackageService().validate_and_extract(content=content, filename=upload.filename or "")
|
||||
except SkillPackageError as exc:
|
||||
return {"code": exc.code, "message": exc.message}, exc.status_code
|
||||
|
||||
upload_file = FileService(db.engine).upload_file(
|
||||
filename=upload.filename or "skill.zip",
|
||||
content=content,
|
||||
mimetype=upload.mimetype or "application/zip",
|
||||
user=current_user,
|
||||
)
|
||||
skill_ref = manifest.to_skill_ref(file_id=upload_file.id)
|
||||
return {"skill": skill_ref.model_dump(exclude_none=True), "manifest": manifest.model_dump()}, 201
|
||||
|
||||
|
||||
def _standardize_skill_for_app(*, current_user: Account, app_model: App):
|
||||
query = query_params_from_request(AgentDriveMutationQuery)
|
||||
agent_id = _resolve_agent_id(app_model, query.node_id)
|
||||
if not agent_id:
|
||||
@ -382,51 +362,9 @@ class AgentLogApi(Resource):
|
||||
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")
|
||||
class AgentSkillUploadByAgentApi(Resource):
|
||||
@console_ns.doc("upload_agent_skill_by_agent")
|
||||
@console_ns.doc(description="Upload + validate a Skill package for an Agent App")
|
||||
@console_ns.doc(params={"agent_id": "Agent ID"})
|
||||
@console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__])
|
||||
@console_ns.response(400, "Invalid skill package")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
|
||||
return _upload_skill_for_app(current_user=current_user)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/agent/skills/upload")
|
||||
class AgentSkillUploadApi(Resource):
|
||||
@console_ns.doc("upload_agent_skill")
|
||||
@console_ns.doc(description="Upload + validate a Skill package (.zip/.skill) and extract its manifest")
|
||||
@console_ns.doc(params={"app_id": "Application ID"})
|
||||
@console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__])
|
||||
@console_ns.response(400, "Invalid skill package")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
"""Validate an uploaded Skill package and persist the archive.
|
||||
|
||||
Returns a validated skill ref (to bind into the Agent soul config on save)
|
||||
plus its manifest. Standardizing into the agent drive is ENG-594.
|
||||
"""
|
||||
return _upload_skill_for_app(current_user=current_user)
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/skills/standardize")
|
||||
class AgentSkillStandardizeByAgentApi(Resource):
|
||||
@console_ns.doc("standardize_agent_skill_by_agent")
|
||||
@console_ns.doc(description="Validate + standardize a Skill into an Agent App drive")
|
||||
@console_ns.doc(params={"agent_id": "Agent ID"})
|
||||
@console_ns.response(
|
||||
201,
|
||||
"Skill standardized into drive",
|
||||
console_ns.models[AgentSkillStandardizeResponse.__name__],
|
||||
)
|
||||
@console_ns.doc(description="Upload + standardize a Skill into an Agent App drive")
|
||||
@console_ns.doc(consumes=["multipart/form-data"], params={"agent_id": "Agent ID", **_AGENT_SKILL_UPLOAD_PARAMS})
|
||||
@console_ns.response(201, "Skill uploaded into drive", console_ns.models[AgentSkillUploadResponse.__name__])
|
||||
@console_ns.response(400, "Invalid skill package or no bound agent")
|
||||
@setup_required
|
||||
@login_required
|
||||
@ -435,19 +373,22 @@ class AgentSkillStandardizeByAgentApi(Resource):
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
|
||||
return _standardize_skill_for_app(current_user=current_user, app_model=app_model)
|
||||
return _upload_skill_for_app(current_user=current_user, app_model=app_model)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/agent/skills/standardize")
|
||||
class AgentSkillStandardizeApi(Resource):
|
||||
@console_ns.doc("standardize_agent_skill")
|
||||
@console_ns.doc(description="Validate + standardize a Skill into the agent drive (ENG-594)")
|
||||
@console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(AgentDriveMutationQuery)})
|
||||
@console_ns.response(
|
||||
201,
|
||||
"Skill standardized into drive",
|
||||
console_ns.models[AgentSkillStandardizeResponse.__name__],
|
||||
@console_ns.route("/apps/<uuid:app_id>/agent/skills/upload")
|
||||
class AgentSkillUploadApi(Resource):
|
||||
@console_ns.doc("upload_agent_skill")
|
||||
@console_ns.doc(description="Upload + standardize a Skill into the agent drive")
|
||||
@console_ns.doc(
|
||||
consumes=["multipart/form-data"],
|
||||
params={
|
||||
"app_id": "Application ID",
|
||||
**query_params_from_model(AgentDriveMutationQuery),
|
||||
**_AGENT_SKILL_UPLOAD_PARAMS,
|
||||
},
|
||||
)
|
||||
@console_ns.response(201, "Skill uploaded into drive", console_ns.models[AgentSkillUploadResponse.__name__])
|
||||
@console_ns.response(400, "Invalid skill package or no bound agent")
|
||||
@setup_required
|
||||
@login_required
|
||||
@ -455,8 +396,8 @@ class AgentSkillStandardizeApi(Resource):
|
||||
@get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
"""Upload a Skill, validate it, and standardize it into the app agent's drive."""
|
||||
return _standardize_skill_for_app(current_user=current_user, app_model=app_model)
|
||||
"""Upload a Skill, validate it, and commit drive-backed skill files."""
|
||||
return _upload_skill_for_app(current_user=current_user, app_model=app_model)
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/files")
|
||||
|
||||
@ -402,8 +402,6 @@ class AppPartial(ResponseModel):
|
||||
bound_agent_id: str | None = None
|
||||
# For Agent App responses exposed through /agent.
|
||||
app_id: str | None = None
|
||||
role: str | None = None
|
||||
active_config_is_published: bool = False
|
||||
is_starred: bool = False
|
||||
|
||||
@computed_field(return_type=str | None) # type: ignore
|
||||
@ -457,8 +455,6 @@ class AppDetailWithSite(AppDetail):
|
||||
bound_agent_id: str | None = None
|
||||
# For Agent App responses exposed through /agent.
|
||||
app_id: str | None = None
|
||||
role: str | None = None
|
||||
active_config_is_published: bool = False
|
||||
|
||||
@computed_field(return_type=str | None) # type: ignore
|
||||
@property
|
||||
@ -541,10 +537,7 @@ register_schema_models(
|
||||
ModelConfig,
|
||||
Site,
|
||||
DeletedTool,
|
||||
AppPartial,
|
||||
AppDetail,
|
||||
AppDetailWithSite,
|
||||
AppPagination,
|
||||
AppExportResponse,
|
||||
Segmentation,
|
||||
PreProcessingRule,
|
||||
@ -564,6 +557,13 @@ register_schema_models(
|
||||
LoadBalancingPayload,
|
||||
)
|
||||
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
AppPartial,
|
||||
AppDetailWithSite,
|
||||
AppPagination,
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/apps")
|
||||
class AppListApi(Resource):
|
||||
|
||||
@ -323,7 +323,7 @@ Check if activation token is valid
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Agent app created successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)<br> |
|
||||
| 201 | Agent app created successfully | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)<br> |
|
||||
| 400 | Invalid request parameters | |
|
||||
| 403 | Insufficient permissions | |
|
||||
|
||||
@ -368,7 +368,7 @@ Check if activation token is valid
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Agent app detail | **application/json**: [AppDetailWithSite](#appdetailwithsite)<br> |
|
||||
| 200 | Agent app detail | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)<br> |
|
||||
|
||||
### [PUT] /agent/{agent_id}
|
||||
#### Parameters
|
||||
@ -387,7 +387,7 @@ Check if activation token is valid
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Agent app updated successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)<br> |
|
||||
| 200 | Agent app updated successfully | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)<br> |
|
||||
| 400 | Invalid request parameters | |
|
||||
| 403 | Insufficient permissions | |
|
||||
|
||||
@ -524,7 +524,7 @@ Stop a running Agent App chat message generation
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Agent app copied successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)<br> |
|
||||
| 201 | Agent app copied successfully | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)<br> |
|
||||
| 400 | Invalid request parameters | |
|
||||
| 403 | Insufficient permissions | |
|
||||
|
||||
@ -800,24 +800,8 @@ Upload one Agent App sandbox file as a Dify ToolFile mapping
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Uploaded | **application/json**: [SandboxUploadResponse](#sandboxuploadresponse)<br> |
|
||||
|
||||
### [POST] /agent/{agent_id}/skills/standardize
|
||||
Validate + standardize a Skill into an Agent App drive
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| agent_id | path | Agent ID | Yes | string (uuid) |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill standardized into drive | **application/json**: [AgentSkillStandardizeResponse](#agentskillstandardizeresponse)<br> |
|
||||
| 400 | Invalid skill package or no bound agent | |
|
||||
|
||||
### [POST] /agent/{agent_id}/skills/upload
|
||||
Upload + validate a Skill package for an Agent App
|
||||
Upload + standardize a Skill into an Agent App drive
|
||||
|
||||
#### Parameters
|
||||
|
||||
@ -825,12 +809,18 @@ Upload + validate a Skill package for an Agent App
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| agent_id | path | Agent ID | Yes | string (uuid) |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **multipart/form-data**: { **"file"**: binary }<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill validated | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)<br> |
|
||||
| 400 | Invalid skill package | |
|
||||
| 201 | Skill uploaded into drive | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)<br> |
|
||||
| 400 | Invalid skill package or no bound agent | |
|
||||
|
||||
### [DELETE] /agent/{agent_id}/skills/{slug}
|
||||
Delete a standardized skill from an Agent App drive
|
||||
@ -1517,10 +1507,10 @@ Get agent execution logs for an application
|
||||
| 200 | Agent logs retrieved successfully | **application/json**: [AgentLogResponse](#agentlogresponse)<br> |
|
||||
| 400 | Invalid request parameters | |
|
||||
|
||||
### [POST] /apps/{app_id}/agent/skills/standardize
|
||||
**Upload a Skill, validate it, and standardize it into the app agent's drive**
|
||||
### [POST] /apps/{app_id}/agent/skills/upload
|
||||
**Upload a Skill, validate it, and commit drive-backed skill files**
|
||||
|
||||
Validate + standardize a Skill into the agent drive (ENG-594)
|
||||
Upload + standardize a Skill into the agent drive
|
||||
|
||||
#### Parameters
|
||||
|
||||
@ -1529,33 +1519,19 @@ Validate + standardize a Skill into the agent drive (ENG-594)
|
||||
| app_id | path | Application ID | Yes | string (uuid) |
|
||||
| node_id | query | Workflow node ID (workflow composer variant) | No | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **multipart/form-data**: { **"file"**: binary }<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill standardized into drive | **application/json**: [AgentSkillStandardizeResponse](#agentskillstandardizeresponse)<br> |
|
||||
| 201 | Skill uploaded into drive | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)<br> |
|
||||
| 400 | Invalid skill package or no bound agent | |
|
||||
|
||||
### [POST] /apps/{app_id}/agent/skills/upload
|
||||
**Validate an uploaded Skill package and persist the archive**
|
||||
|
||||
Upload + validate a Skill package (.zip/.skill) and extract its manifest
|
||||
Returns a validated skill ref (to bind into the Agent soul config on save)
|
||||
plus its manifest. Standardizing into the agent drive is ENG-594.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | Application ID | Yes | string (uuid) |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Skill validated | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)<br> |
|
||||
| 400 | Invalid skill package | |
|
||||
|
||||
### [DELETE] /apps/{app_id}/agent/skills/{slug}
|
||||
Delete a standardized skill: soul ref first, then the <slug>/ drive prefix (ENG-625 D5)
|
||||
|
||||
@ -11378,6 +11354,39 @@ Default namespace
|
||||
| name | string | Agent name | Yes |
|
||||
| role | string | Agent role | Yes |
|
||||
|
||||
#### AgentAppDetailWithSite
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| access_mode | string | | No |
|
||||
| active_config_is_published | boolean | | No |
|
||||
| api_base_url | string | | No |
|
||||
| app_id | string | | No |
|
||||
| bound_agent_id | string | | No |
|
||||
| created_at | integer | | No |
|
||||
| created_by | string | | No |
|
||||
| deleted_tools | [ [DeletedTool](#deletedtool) ] | | No |
|
||||
| description | string | | No |
|
||||
| enable_api | boolean | | Yes |
|
||||
| enable_site | boolean | | Yes |
|
||||
| icon | string | | No |
|
||||
| icon_background | string | | No |
|
||||
| icon_type | string | | No |
|
||||
| icon_url | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| max_active_requests | integer | | No |
|
||||
| mode | string | | Yes |
|
||||
| model_config | [ModelConfig](#modelconfig) | | No |
|
||||
| name | string | | Yes |
|
||||
| role | string | | No |
|
||||
| site | [Site](#site) | | No |
|
||||
| tags | [ [Tag](#tag) ] | | No |
|
||||
| tracing | [JSONValue](#jsonvalue) | | No |
|
||||
| updated_at | integer | | No |
|
||||
| updated_by | string | | No |
|
||||
| use_icon_as_answer_icon | boolean | | No |
|
||||
| workflow | [WorkflowPartial](#workflowpartial) | | No |
|
||||
|
||||
#### AgentAppFeaturesPayload
|
||||
|
||||
Presentation features configurable on an Agent App.
|
||||
@ -12296,13 +12305,6 @@ Visibility and lifecycle scope of an Agent record.
|
||||
| skill_md_file_id | string | | No |
|
||||
| skill_md_key | string | | No |
|
||||
|
||||
#### AgentSkillStandardizeResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| manifest | [SkillManifest](#skillmanifest) | | Yes |
|
||||
| skill | [AgentSkillRefConfig](#agentskillrefconfig) | | Yes |
|
||||
|
||||
#### AgentSkillUploadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@ -12864,7 +12866,6 @@ Enum class for api provider schema type.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| access_mode | string | | No |
|
||||
| active_config_is_published | boolean | | No |
|
||||
| api_base_url | string | | No |
|
||||
| app_id | string | | No |
|
||||
| bound_agent_id | string | | No |
|
||||
@ -12883,7 +12884,6 @@ Enum class for api provider schema type.
|
||||
| mode | string | | Yes |
|
||||
| model_config | [ModelConfig](#modelconfig) | | No |
|
||||
| name | string | | Yes |
|
||||
| role | string | | No |
|
||||
| site | [Site](#site) | | No |
|
||||
| tags | [ [Tag](#tag) ] | | No |
|
||||
| tracing | [JSONValue](#jsonvalue) | | No |
|
||||
@ -12977,10 +12977,10 @@ AppMCPServer Status Enum
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| has_next | boolean | | Yes |
|
||||
| items | [ [AppPartial](#apppartial) ] | | Yes |
|
||||
| data | [ [AppPartial](#apppartial) ] | | Yes |
|
||||
| has_more | boolean | | Yes |
|
||||
| limit | integer | | Yes |
|
||||
| page | integer | | Yes |
|
||||
| per_page | integer | | Yes |
|
||||
| total | integer | | Yes |
|
||||
|
||||
#### AppPartial
|
||||
@ -12988,25 +12988,24 @@ AppMCPServer Status Enum
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| access_mode | string | | No |
|
||||
| active_config_is_published | boolean | | No |
|
||||
| app_id | string | | No |
|
||||
| app_model_config | [ModelConfigPartial](#modelconfigpartial) | | No |
|
||||
| author_name | string | | No |
|
||||
| bound_agent_id | string | | No |
|
||||
| create_user_name | string | | No |
|
||||
| created_at | integer | | No |
|
||||
| created_by | string | | No |
|
||||
| desc_or_prompt | string | | No |
|
||||
| description | string | | No |
|
||||
| has_draft_trigger | boolean | | No |
|
||||
| icon | string | | No |
|
||||
| icon_background | string | | No |
|
||||
| icon_type | string | | No |
|
||||
| icon_url | string | | Yes |
|
||||
| id | string | | Yes |
|
||||
| is_starred | boolean | | No |
|
||||
| max_active_requests | integer | | No |
|
||||
| mode_compatible_with_agent | string | | Yes |
|
||||
| mode | string | | Yes |
|
||||
| model_config | [ModelConfigPartial](#modelconfigpartial) | | No |
|
||||
| name | string | | Yes |
|
||||
| role | string | | No |
|
||||
| tags | [ [Tag](#tag) ] | | No |
|
||||
| updated_at | integer | | No |
|
||||
| updated_by | string | | No |
|
||||
|
||||
@ -10,7 +10,9 @@ to the agent drive (Agent Files §5.4 / §4):
|
||||
Both are stored as ``ToolFile`` records and bound via ``AgentDriveService.commit``
|
||||
with ``value_owned_by_drive=True`` (the drive owns their lifecycle). The returned
|
||||
skill ref records the stable drive paths + file ids (not just the raw upload id),
|
||||
so the Composer can reload the bound skill list.
|
||||
so the Composer can reload the bound skill list. The console ``/skills/upload``
|
||||
endpoints delegate to this service so "upload" now always means drive-backed skill
|
||||
normalization.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -34,7 +36,7 @@ def slugify_skill_name(name: str) -> str:
|
||||
|
||||
|
||||
class SkillStandardizeService:
|
||||
"""Validate + standardize a Skill package into a per-agent drive."""
|
||||
"""Validate + standardize a Skill package into a per-agent drive upload result."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@ -542,17 +542,21 @@ class AppService:
|
||||
*,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
role: str | None = None,
|
||||
icon_type: IconType | str | None = None,
|
||||
icon: str | None = None,
|
||||
icon_background: str | None = None,
|
||||
role: str | None = None,
|
||||
account_id: str | None = None,
|
||||
updated_at: datetime | None = None,
|
||||
) -> None:
|
||||
"""Keep the Roster identity aligned with its Agent App shell.
|
||||
|
||||
Agent Soul remains versioned through Composer. This helper only mirrors
|
||||
user-facing identity fields so Roster and Agent Console do not drift.
|
||||
user-facing identity fields, including the roster role/persona label,
|
||||
so Roster and Agent Console do not drift.
|
||||
|
||||
Role omission is intentional: ``role=None`` preserves the backing
|
||||
Agent's current role, while ``role=""`` explicitly clears it.
|
||||
"""
|
||||
agent = self._get_backing_agent_for_update(app)
|
||||
if agent is None:
|
||||
@ -562,14 +566,14 @@ class AppService:
|
||||
agent.name = name
|
||||
if description is not None:
|
||||
agent.description = description
|
||||
if role is not None:
|
||||
agent.role = role
|
||||
if icon_type is not None:
|
||||
agent.icon_type = self._to_agent_icon_type(icon_type)
|
||||
if icon is not None:
|
||||
agent.icon = icon
|
||||
if icon_background is not None:
|
||||
agent.icon_background = icon_background
|
||||
if role is not None:
|
||||
agent.role = role
|
||||
agent.updated_by = account_id
|
||||
if updated_at is not None:
|
||||
agent.updated_at = updated_at
|
||||
@ -601,10 +605,12 @@ class AppService:
|
||||
app,
|
||||
name=app.name,
|
||||
description=app.description,
|
||||
# Omitted role must stay omitted here: None means "preserve current
|
||||
# backing-agent role", while an empty string is an explicit clear.
|
||||
role=args.get("role"),
|
||||
icon_type=app.icon_type,
|
||||
icon=app.icon,
|
||||
icon_background=app.icon_background,
|
||||
role=args.get("role"),
|
||||
account_id=current_user.id,
|
||||
updated_at=app.updated_at,
|
||||
)
|
||||
|
||||
@ -60,6 +60,7 @@ class ComposerSavePayload(BaseModel):
|
||||
|
||||
class RosterAgentCreatePayload(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
mode: Literal["agent"] = "agent"
|
||||
description: str = ""
|
||||
role: str = Field(default="", max_length=255)
|
||||
icon_type: AgentIconType | None = None
|
||||
|
||||
@ -95,6 +95,7 @@ def _agent_app_composer_response() -> dict:
|
||||
def _app_detail_obj(**overrides):
|
||||
data = {
|
||||
"id": "app-1",
|
||||
"tenant_id": "tenant-1",
|
||||
"name": "Iris",
|
||||
"description": "Agent app",
|
||||
"mode_compatible_with_agent": "agent",
|
||||
@ -118,7 +119,6 @@ def _app_detail_obj(**overrides):
|
||||
"deleted_tools": [],
|
||||
"site": None,
|
||||
"bound_agent_id": "00000000-0000-0000-0000-000000000001",
|
||||
"tenant_id": "tenant-1",
|
||||
}
|
||||
data.update(overrides)
|
||||
return SimpleNamespace(**data)
|
||||
|
||||
@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
import inspect
|
||||
import io
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from flask import Flask
|
||||
|
||||
@ -18,8 +18,6 @@ from controllers.console.app.agent import (
|
||||
AgentDriveFilesByAgentApi,
|
||||
AgentSkillByAgentApi,
|
||||
AgentSkillInferToolsByAgentApi,
|
||||
AgentSkillStandardizeApi,
|
||||
AgentSkillStandardizeByAgentApi,
|
||||
AgentSkillUploadApi,
|
||||
AgentSkillUploadByAgentApi,
|
||||
)
|
||||
@ -45,47 +43,39 @@ _APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bou
|
||||
_WORKFLOW_APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW, bound_agent_id=None)
|
||||
|
||||
|
||||
def test_upload_validates_and_returns_skill_ref():
|
||||
def test_upload_standardizes_into_drive_and_returns_skill_ref():
|
||||
raw = _raw(AgentSkillUploadApi.post)
|
||||
manifest = MagicMock()
|
||||
manifest.to_skill_ref.return_value.model_dump.return_value = {"name": "S", "file_id": "uf-1"}
|
||||
manifest.model_dump.return_value = {"name": "S"}
|
||||
|
||||
with _file_ctx(files={"file": b"zip-bytes"}):
|
||||
with (
|
||||
patch(f"{_MOD}.SkillPackageService") as pkg,
|
||||
patch(f"{_MOD}.FileService") as fs,
|
||||
patch(f"{_MOD}.db"),
|
||||
patch(f"{_MOD}.SkillStandardizeService") as svc,
|
||||
):
|
||||
pkg.return_value.validate_and_extract.return_value = manifest
|
||||
fs.return_value.upload_file.return_value = SimpleNamespace(id="uf-1")
|
||||
svc.return_value.standardize.return_value = {
|
||||
"skill": {"path": "skill-a", "skill_md_key": "skill-a/SKILL.md"},
|
||||
"manifest": {"name": "Skill A"},
|
||||
}
|
||||
body, status = raw(AgentSkillUploadApi(), _USER, _APP)
|
||||
|
||||
assert status == 201
|
||||
assert body["skill"] == {"name": "S", "file_id": "uf-1"}
|
||||
manifest.to_skill_ref.assert_called_once_with(file_id="uf-1")
|
||||
assert body["skill"] == {"path": "skill-a", "skill_md_key": "skill-a/SKILL.md"}
|
||||
assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1"
|
||||
|
||||
|
||||
def test_upload_by_agent_resolves_app_and_returns_skill_ref():
|
||||
def test_upload_by_agent_resolves_app_and_standardizes_into_drive():
|
||||
raw = _raw(AgentSkillUploadByAgentApi.post)
|
||||
manifest = MagicMock()
|
||||
manifest.to_skill_ref.return_value.model_dump.return_value = {"name": "S", "file_id": "uf-1"}
|
||||
manifest.model_dump.return_value = {"name": "S"}
|
||||
|
||||
with _file_ctx(files={"file": b"zip-bytes"}):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.SkillPackageService") as pkg,
|
||||
patch(f"{_MOD}.FileService") as fs,
|
||||
patch(f"{_MOD}.db"),
|
||||
patch(f"{_MOD}.SkillStandardizeService") as svc,
|
||||
):
|
||||
pkg.return_value.validate_and_extract.return_value = manifest
|
||||
fs.return_value.upload_file.return_value = SimpleNamespace(id="uf-1")
|
||||
svc.return_value.standardize.return_value = {"skill": {"path": "skill-a"}, "manifest": {}}
|
||||
body, status = raw(AgentSkillUploadByAgentApi(), "tenant-1", _USER, "agent-1")
|
||||
|
||||
assert status == 201
|
||||
assert body["skill"] == {"name": "S", "file_id": "uf-1"}
|
||||
assert body["skill"] == {"path": "skill-a"}
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1"
|
||||
|
||||
|
||||
def test_upload_no_file_is_400():
|
||||
@ -99,8 +89,8 @@ def test_upload_no_file_is_400():
|
||||
def test_upload_maps_package_error():
|
||||
raw = _raw(AgentSkillUploadApi.post)
|
||||
with _file_ctx(files={"file": b"bad"}):
|
||||
with patch(f"{_MOD}.SkillPackageService") as pkg:
|
||||
pkg.return_value.validate_and_extract.side_effect = SkillPackageError(
|
||||
with patch(f"{_MOD}.SkillStandardizeService") as svc:
|
||||
svc.return_value.standardize.side_effect = SkillPackageError(
|
||||
"missing_skill_md", "no SKILL.md", status_code=400
|
||||
)
|
||||
body, status = raw(AgentSkillUploadApi(), _USER, _APP)
|
||||
@ -108,44 +98,17 @@ def test_upload_maps_package_error():
|
||||
assert body["code"] == "missing_skill_md"
|
||||
|
||||
|
||||
def test_standardize_returns_result():
|
||||
raw = _raw(AgentSkillStandardizeApi.post)
|
||||
with _file_ctx(files={"file": b"zip"}):
|
||||
with patch(f"{_MOD}.SkillStandardizeService") as svc:
|
||||
svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}}
|
||||
body, status = raw(AgentSkillStandardizeApi(), _USER, _APP)
|
||||
assert status == 201
|
||||
assert body["skill"] == {"path": "s"}
|
||||
assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1"
|
||||
|
||||
|
||||
def test_standardize_by_agent_resolves_app():
|
||||
raw = _raw(AgentSkillStandardizeByAgentApi.post)
|
||||
with _file_ctx(files={"file": b"zip"}):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.SkillStandardizeService") as svc,
|
||||
):
|
||||
svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}}
|
||||
body, status = raw(AgentSkillStandardizeByAgentApi(), "tenant-1", _USER, "agent-1")
|
||||
|
||||
assert status == 201
|
||||
assert body["skill"] == {"path": "s"}
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1"
|
||||
|
||||
|
||||
def test_standardize_no_bound_agent_is_400():
|
||||
raw = _raw(AgentSkillStandardizeApi.post)
|
||||
def test_upload_no_bound_agent_is_400():
|
||||
raw = _raw(AgentSkillUploadApi.post)
|
||||
app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bound_agent_id=None)
|
||||
with _file_ctx(files={"file": b"zip"}):
|
||||
body, status = raw(AgentSkillStandardizeApi(), _USER, app_without_agent)
|
||||
body, status = raw(AgentSkillUploadApi(), _USER, app_without_agent)
|
||||
assert status == 400
|
||||
assert body["code"] == "agent_not_bound"
|
||||
|
||||
|
||||
def test_standardize_resolves_workflow_node_agent():
|
||||
raw = _raw(AgentSkillStandardizeApi.post)
|
||||
def test_upload_resolves_workflow_node_agent():
|
||||
raw = _raw(AgentSkillUploadApi.post)
|
||||
with app.test_request_context(
|
||||
"/?node_id=agent-node-1", method="POST", data={"file": (io.BytesIO(b"zip"), "skill.zip")}
|
||||
):
|
||||
@ -155,19 +118,19 @@ def test_standardize_resolves_workflow_node_agent():
|
||||
):
|
||||
composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1"
|
||||
svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}}
|
||||
body, status = raw(AgentSkillStandardizeApi(), _USER, _WORKFLOW_APP)
|
||||
body, status = raw(AgentSkillUploadApi(), _USER, _WORKFLOW_APP)
|
||||
|
||||
assert status == 201
|
||||
assert body["skill"] == {"path": "s"}
|
||||
assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "wf-agent-1"
|
||||
|
||||
|
||||
def test_standardize_maps_drive_error():
|
||||
raw = _raw(AgentSkillStandardizeApi.post)
|
||||
def test_upload_maps_drive_error():
|
||||
raw = _raw(AgentSkillUploadApi.post)
|
||||
with _file_ctx(files={"file": b"zip"}):
|
||||
with patch(f"{_MOD}.SkillStandardizeService") as svc:
|
||||
svc.return_value.standardize.side_effect = AgentDriveError("source_not_found", "nope", status_code=404)
|
||||
body, status = raw(AgentSkillStandardizeApi(), _USER, _APP)
|
||||
body, status = raw(AgentSkillUploadApi(), _USER, _APP)
|
||||
assert status == 404
|
||||
assert body["code"] == "source_not_found"
|
||||
|
||||
|
||||
@ -351,6 +351,7 @@ def test_app_partial_serialization_uses_aliases(app_models):
|
||||
create_user_name="Creator",
|
||||
author_name="Author",
|
||||
has_draft_trigger=True,
|
||||
role="Should stay agent-only",
|
||||
)
|
||||
|
||||
serialized = AppPartial.model_validate(app_obj, from_attributes=True).model_dump(mode="json")
|
||||
@ -363,6 +364,7 @@ def test_app_partial_serialization_uses_aliases(app_models):
|
||||
assert serialized["model_config"]["model"] == {"provider": "openai", "name": "gpt-4o"}
|
||||
assert serialized["workflow"]["id"] == "wf-1"
|
||||
assert serialized["tags"][0]["name"] == "Utilities"
|
||||
assert "role" not in serialized
|
||||
|
||||
|
||||
def test_app_detail_with_site_includes_nested_serialization(app_models):
|
||||
@ -405,6 +407,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models):
|
||||
deleted_tools=[{"type": "api", "tool_name": "search", "provider_id": "prov"}],
|
||||
site=site,
|
||||
bound_agent_id="agent-1",
|
||||
role="Should stay agent-only",
|
||||
)
|
||||
|
||||
serialized = AppDetailWithSite.model_validate(app_obj, from_attributes=True).model_dump(mode="json")
|
||||
@ -415,6 +418,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models):
|
||||
assert serialized["site"]["icon_url"] == "signed:site-icon"
|
||||
assert serialized["site"]["created_at"] == int(timestamp.timestamp())
|
||||
assert serialized["bound_agent_id"] == "agent-1"
|
||||
assert "role" not in serialized
|
||||
|
||||
|
||||
def test_app_pagination_aliases_per_page_and_has_next(app_models):
|
||||
|
||||
@ -1247,6 +1247,7 @@ class TestAgentAppBackingAgent:
|
||||
app_id="app-1",
|
||||
name="Iris",
|
||||
description="clarifier",
|
||||
role="research assistant",
|
||||
)
|
||||
|
||||
# Agent is bound to the app and is a roster/agent_app entry.
|
||||
@ -1256,6 +1257,7 @@ class TestAgentAppBackingAgent:
|
||||
assert agent.status == AgentStatus.ACTIVE
|
||||
assert agent.agent_kind == AgentKind.DIFY_AGENT
|
||||
assert agent.name == "Iris"
|
||||
assert agent.role == "research assistant"
|
||||
# A v1 snapshot + revision are seeded and wired as the active version.
|
||||
snapshots = [a for a in session.added if isinstance(a, AgentConfigSnapshot)]
|
||||
assert len(snapshots) == 1
|
||||
|
||||
@ -168,6 +168,7 @@ class TestAgentAppType:
|
||||
mode=AppMode.AGENT,
|
||||
name="Old",
|
||||
description="old",
|
||||
role="draft",
|
||||
icon_type=IconType.EMOJI,
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
@ -178,6 +179,7 @@ class TestAgentAppType:
|
||||
backing_agent = SimpleNamespace(
|
||||
name="Old",
|
||||
description="old",
|
||||
role="draft",
|
||||
icon_type=AgentIconType.EMOJI,
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
@ -195,6 +197,7 @@ class TestAgentAppType:
|
||||
{
|
||||
"name": "Iris",
|
||||
"description": "agent app",
|
||||
"role": "research assistant",
|
||||
"icon_type": "image",
|
||||
"icon": "file-id",
|
||||
"icon_background": "#123456",
|
||||
@ -206,12 +209,114 @@ class TestAgentAppType:
|
||||
assert updated_app.name == "Iris"
|
||||
assert backing_agent.name == "Iris"
|
||||
assert backing_agent.description == "agent app"
|
||||
assert backing_agent.role == "research assistant"
|
||||
assert backing_agent.icon_type == AgentIconType.IMAGE
|
||||
assert backing_agent.icon == "file-id"
|
||||
assert backing_agent.icon_background == "#123456"
|
||||
assert backing_agent.updated_by == "account-2"
|
||||
assert backing_agent.updated_at == updated_app.updated_at
|
||||
|
||||
def test_update_agent_app_preserves_role_when_args_omit_it(self):
|
||||
from models.agent import AgentIconType
|
||||
from models.model import AppMode, IconType
|
||||
from services.app_service import AppService
|
||||
|
||||
app = SimpleNamespace(
|
||||
id="app-1",
|
||||
tenant_id="tenant-1",
|
||||
mode=AppMode.AGENT,
|
||||
name="Old",
|
||||
description="old",
|
||||
role="draft",
|
||||
icon_type=IconType.EMOJI,
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
use_icon_as_answer_icon=False,
|
||||
max_active_requests=None,
|
||||
created_by="account-1",
|
||||
)
|
||||
backing_agent = SimpleNamespace(
|
||||
name="Old",
|
||||
description="old",
|
||||
role="research assistant",
|
||||
icon_type=AgentIconType.EMOJI,
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
updated_by=None,
|
||||
updated_at=None,
|
||||
)
|
||||
|
||||
with (
|
||||
patch("services.app_service.db") as mock_db,
|
||||
patch("services.app_service.current_user", SimpleNamespace(id="account-2")),
|
||||
):
|
||||
mock_db.session.scalar.return_value = backing_agent
|
||||
AppService().update_app(
|
||||
app, # type: ignore[arg-type]
|
||||
{
|
||||
"name": "Iris",
|
||||
"description": "agent app",
|
||||
"icon_type": "image",
|
||||
"icon": "file-id",
|
||||
"icon_background": "#123456",
|
||||
"use_icon_as_answer_icon": False,
|
||||
"max_active_requests": 0,
|
||||
},
|
||||
)
|
||||
|
||||
assert backing_agent.role == "research assistant"
|
||||
|
||||
def test_update_agent_app_clears_role_when_args_set_empty_string(self):
|
||||
from models.agent import AgentIconType
|
||||
from models.model import AppMode, IconType
|
||||
from services.app_service import AppService
|
||||
|
||||
app = SimpleNamespace(
|
||||
id="app-1",
|
||||
tenant_id="tenant-1",
|
||||
mode=AppMode.AGENT,
|
||||
name="Old",
|
||||
description="old",
|
||||
role="draft",
|
||||
icon_type=IconType.EMOJI,
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
use_icon_as_answer_icon=False,
|
||||
max_active_requests=None,
|
||||
created_by="account-1",
|
||||
)
|
||||
backing_agent = SimpleNamespace(
|
||||
name="Old",
|
||||
description="old",
|
||||
role="research assistant",
|
||||
icon_type=AgentIconType.EMOJI,
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
updated_by=None,
|
||||
updated_at=None,
|
||||
)
|
||||
|
||||
with (
|
||||
patch("services.app_service.db") as mock_db,
|
||||
patch("services.app_service.current_user", SimpleNamespace(id="account-2")),
|
||||
):
|
||||
mock_db.session.scalar.return_value = backing_agent
|
||||
AppService().update_app(
|
||||
app, # type: ignore[arg-type]
|
||||
{
|
||||
"name": "Iris",
|
||||
"description": "agent app",
|
||||
"role": "",
|
||||
"icon_type": "image",
|
||||
"icon": "file-id",
|
||||
"icon_background": "#123456",
|
||||
"use_icon_as_answer_icon": False,
|
||||
"max_active_requests": 0,
|
||||
},
|
||||
)
|
||||
|
||||
assert backing_agent.role == ""
|
||||
|
||||
def test_delete_agent_app_archives_backing_agent(self):
|
||||
from models.agent import AgentStatus
|
||||
from models.model import AppMode
|
||||
|
||||
@ -150,6 +150,7 @@ NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false
|
||||
# Enable preview features still in development (currently the /create and
|
||||
# /refine slash commands in the "Go to Anything" command palette).
|
||||
NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false
|
||||
ENABLE_AGENT_V2=false
|
||||
EXPERIMENTAL_ENABLE_VINEXT=false
|
||||
|
||||
# Storage and default vector store
|
||||
|
||||
@ -25,6 +25,7 @@ ENABLE_WEBSITE_FIRECRAWL=true
|
||||
ENABLE_WEBSITE_WATERCRAWL=true
|
||||
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false
|
||||
NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false
|
||||
ENABLE_AGENT_V2=false
|
||||
NEXT_PUBLIC_COOKIE_DOMAIN=
|
||||
NEXT_PUBLIC_BATCH_CONCURRENCY=5
|
||||
CSP_WHITELIST=
|
||||
|
||||
@ -1089,14 +1089,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/app-icon/index.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/audio-btn/audio.ts": {
|
||||
"node/prefer-global/buffer": {
|
||||
"count": 1
|
||||
@ -1529,17 +1521,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 2
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/base/features/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
@ -2502,11 +2483,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/base/prompt-editor/utils.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/base/prompt-log-modal/index.stories.tsx": {
|
||||
"no-console": {
|
||||
"count": 1
|
||||
@ -5012,14 +4988,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/blocks.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/featured-tools.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
@ -5152,11 +5120,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/candidate-node-main.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/comment/comment-input.tsx": {
|
||||
"jsx-a11y/no-autofocus": {
|
||||
"count": 1
|
||||
@ -5300,11 +5263,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/hooks/use-nodes-interactions.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 8
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/hooks/use-serial-async-callback.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -5704,7 +5662,7 @@
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 6
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx": {
|
||||
@ -5885,11 +5843,6 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/components.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/data-source-empty/hooks.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -7060,19 +7013,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/run/agent-log/agent-log-trigger.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/run/agent-log/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/run/hooks.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -7750,11 +7690,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/client.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/common.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
@ -83,8 +83,7 @@ import {
|
||||
zPostAgentByAgentIdSandboxFilesUploadResponse,
|
||||
zPostAgentByAgentIdSkillsBySlugInferToolsPath,
|
||||
zPostAgentByAgentIdSkillsBySlugInferToolsResponse,
|
||||
zPostAgentByAgentIdSkillsStandardizePath,
|
||||
zPostAgentByAgentIdSkillsStandardizeResponse,
|
||||
zPostAgentByAgentIdSkillsUploadBody,
|
||||
zPostAgentByAgentIdSkillsUploadPath,
|
||||
zPostAgentByAgentIdSkillsUploadResponse,
|
||||
zPostAgentResponse,
|
||||
@ -599,31 +598,11 @@ export const sandbox = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate + standardize a Skill into an Agent App drive
|
||||
* Upload + standardize a Skill into an Agent App drive
|
||||
*/
|
||||
export const post8 = oc
|
||||
.route({
|
||||
description: 'Validate + standardize a Skill into an Agent App drive',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postAgentByAgentIdSkillsStandardize',
|
||||
path: '/agent/{agent_id}/skills/standardize',
|
||||
successStatus: 201,
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ params: zPostAgentByAgentIdSkillsStandardizePath }))
|
||||
.output(zPostAgentByAgentIdSkillsStandardizeResponse)
|
||||
|
||||
export const standardize = {
|
||||
post: post8,
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload + validate a Skill package for an Agent App
|
||||
*/
|
||||
export const post9 = oc
|
||||
.route({
|
||||
description: 'Upload + validate a Skill package for an Agent App',
|
||||
description: 'Upload + standardize a Skill into an Agent App drive',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postAgentByAgentIdSkillsUpload',
|
||||
@ -631,17 +610,22 @@ export const post9 = oc
|
||||
successStatus: 201,
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ params: zPostAgentByAgentIdSkillsUploadPath }))
|
||||
.input(
|
||||
z.object({
|
||||
body: zPostAgentByAgentIdSkillsUploadBody,
|
||||
params: zPostAgentByAgentIdSkillsUploadPath,
|
||||
}),
|
||||
)
|
||||
.output(zPostAgentByAgentIdSkillsUploadResponse)
|
||||
|
||||
export const upload2 = {
|
||||
post: post9,
|
||||
post: post8,
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer CLI tool + ENV suggestions from a standardized Agent App skill
|
||||
*/
|
||||
export const post10 = oc
|
||||
export const post9 = oc
|
||||
.route({
|
||||
description: 'Infer CLI tool + ENV suggestions from a standardized Agent App skill',
|
||||
inputStructure: 'detailed',
|
||||
@ -654,7 +638,7 @@ export const post10 = oc
|
||||
.output(zPostAgentByAgentIdSkillsBySlugInferToolsResponse)
|
||||
|
||||
export const inferTools = {
|
||||
post: post10,
|
||||
post: post9,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -678,7 +662,6 @@ export const bySlug = {
|
||||
}
|
||||
|
||||
export const skills = {
|
||||
standardize,
|
||||
upload: upload2,
|
||||
bySlug,
|
||||
}
|
||||
@ -804,7 +787,7 @@ export const get20 = oc
|
||||
.input(z.object({ query: zGetAgentQuery.optional() }))
|
||||
.output(zGetAgentResponse)
|
||||
|
||||
export const post11 = oc
|
||||
export const post10 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -818,7 +801,7 @@ export const post11 = oc
|
||||
|
||||
export const agent = {
|
||||
get: get20,
|
||||
post: post11,
|
||||
post: post10,
|
||||
inviteOptions,
|
||||
byAgentId,
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ export type AgentAppCreatePayload = {
|
||||
role: string
|
||||
}
|
||||
|
||||
export type AppDetailWithSite = {
|
||||
export type AgentAppDetailWithSite = {
|
||||
access_mode?: string | null
|
||||
active_config_is_published?: boolean
|
||||
api_base_url?: string | null
|
||||
@ -255,11 +255,6 @@ export type SandboxUploadResponse = {
|
||||
path: string
|
||||
}
|
||||
|
||||
export type AgentSkillStandardizeResponse = {
|
||||
manifest: SkillManifest
|
||||
skill: AgentSkillRefConfig
|
||||
}
|
||||
|
||||
export type AgentSkillUploadResponse = {
|
||||
manifest: SkillManifest
|
||||
skill: AgentSkillRefConfig
|
||||
@ -1431,7 +1426,7 @@ export type AgentAppPaginationWritable = {
|
||||
total: number
|
||||
}
|
||||
|
||||
export type AppDetailWithSiteWritable = {
|
||||
export type AgentAppDetailWithSiteWritable = {
|
||||
access_mode?: string | null
|
||||
active_config_is_published?: boolean
|
||||
api_base_url?: string | null
|
||||
@ -1550,7 +1545,7 @@ export type PostAgentErrors = {
|
||||
}
|
||||
|
||||
export type PostAgentResponses = {
|
||||
201: AppDetailWithSite
|
||||
201: AgentAppDetailWithSite
|
||||
}
|
||||
|
||||
export type PostAgentResponse = PostAgentResponses[keyof PostAgentResponses]
|
||||
@ -1604,7 +1599,7 @@ export type GetAgentByAgentIdData = {
|
||||
}
|
||||
|
||||
export type GetAgentByAgentIdResponses = {
|
||||
200: AppDetailWithSite
|
||||
200: AgentAppDetailWithSite
|
||||
}
|
||||
|
||||
export type GetAgentByAgentIdResponse = GetAgentByAgentIdResponses[keyof GetAgentByAgentIdResponses]
|
||||
@ -1624,7 +1619,7 @@ export type PutAgentByAgentIdErrors = {
|
||||
}
|
||||
|
||||
export type PutAgentByAgentIdResponses = {
|
||||
200: AppDetailWithSite
|
||||
200: AgentAppDetailWithSite
|
||||
}
|
||||
|
||||
export type PutAgentByAgentIdResponse = PutAgentByAgentIdResponses[keyof PutAgentByAgentIdResponses]
|
||||
@ -1770,7 +1765,7 @@ export type PostAgentByAgentIdCopyErrors = {
|
||||
}
|
||||
|
||||
export type PostAgentByAgentIdCopyResponses = {
|
||||
201: AppDetailWithSite
|
||||
201: AgentAppDetailWithSite
|
||||
}
|
||||
|
||||
export type PostAgentByAgentIdCopyResponse
|
||||
@ -2065,28 +2060,10 @@ export type PostAgentByAgentIdSandboxFilesUploadResponses = {
|
||||
export type PostAgentByAgentIdSandboxFilesUploadResponse
|
||||
= PostAgentByAgentIdSandboxFilesUploadResponses[keyof PostAgentByAgentIdSandboxFilesUploadResponses]
|
||||
|
||||
export type PostAgentByAgentIdSkillsStandardizeData = {
|
||||
body?: never
|
||||
path: {
|
||||
agent_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/agent/{agent_id}/skills/standardize'
|
||||
}
|
||||
|
||||
export type PostAgentByAgentIdSkillsStandardizeErrors = {
|
||||
400: unknown
|
||||
}
|
||||
|
||||
export type PostAgentByAgentIdSkillsStandardizeResponses = {
|
||||
201: AgentSkillStandardizeResponse
|
||||
}
|
||||
|
||||
export type PostAgentByAgentIdSkillsStandardizeResponse
|
||||
= PostAgentByAgentIdSkillsStandardizeResponses[keyof PostAgentByAgentIdSkillsStandardizeResponses]
|
||||
|
||||
export type PostAgentByAgentIdSkillsUploadData = {
|
||||
body?: never
|
||||
body: {
|
||||
file: Blob | File
|
||||
}
|
||||
path: {
|
||||
agent_id: string
|
||||
}
|
||||
|
||||
@ -549,14 +549,6 @@ export const zAgentSkillRefConfig = z.object({
|
||||
skill_md_key: z.string().max(512).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AgentSkillStandardizeResponse
|
||||
*/
|
||||
export const zAgentSkillStandardizeResponse = z.object({
|
||||
manifest: zSkillManifest,
|
||||
skill: zAgentSkillRefConfig,
|
||||
})
|
||||
|
||||
/**
|
||||
* AgentSkillUploadResponse
|
||||
*/
|
||||
@ -667,9 +659,9 @@ export const zModelConfig = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* AppDetailWithSite
|
||||
* AgentAppDetailWithSite
|
||||
*/
|
||||
export const zAppDetailWithSite = z.object({
|
||||
export const zAgentAppDetailWithSite = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
active_config_is_published: z.boolean().optional().default(false),
|
||||
api_base_url: z.string().nullish(),
|
||||
@ -2063,9 +2055,9 @@ export const zSiteWritable = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* AppDetailWithSite
|
||||
* AgentAppDetailWithSite
|
||||
*/
|
||||
export const zAppDetailWithSiteWritable = z.object({
|
||||
export const zAgentAppDetailWithSiteWritable = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
active_config_is_published: z.boolean().optional().default(false),
|
||||
api_base_url: z.string().nullish(),
|
||||
@ -2131,7 +2123,7 @@ export const zPostAgentBody = zAgentAppCreatePayload
|
||||
/**
|
||||
* Agent app created successfully
|
||||
*/
|
||||
export const zPostAgentResponse = zAppDetailWithSite
|
||||
export const zPostAgentResponse = zAgentAppDetailWithSite
|
||||
|
||||
export const zGetAgentInviteOptionsQuery = z.object({
|
||||
app_id: z.string().optional(),
|
||||
@ -2161,7 +2153,7 @@ export const zGetAgentByAgentIdPath = z.object({
|
||||
/**
|
||||
* Agent app detail
|
||||
*/
|
||||
export const zGetAgentByAgentIdResponse = zAppDetailWithSite
|
||||
export const zGetAgentByAgentIdResponse = zAgentAppDetailWithSite
|
||||
|
||||
export const zPutAgentByAgentIdBody = zAgentAppUpdatePayload
|
||||
|
||||
@ -2172,7 +2164,7 @@ export const zPutAgentByAgentIdPath = z.object({
|
||||
/**
|
||||
* Agent app updated successfully
|
||||
*/
|
||||
export const zPutAgentByAgentIdResponse = zAppDetailWithSite
|
||||
export const zPutAgentByAgentIdResponse = zAgentAppDetailWithSite
|
||||
|
||||
export const zGetAgentByAgentIdChatMessagesPath = z.object({
|
||||
agent_id: z.uuid(),
|
||||
@ -2259,7 +2251,7 @@ export const zPostAgentByAgentIdCopyPath = z.object({
|
||||
/**
|
||||
* Agent app copied successfully
|
||||
*/
|
||||
export const zPostAgentByAgentIdCopyResponse = zAppDetailWithSite
|
||||
export const zPostAgentByAgentIdCopyResponse = zAgentAppDetailWithSite
|
||||
|
||||
export const zGetAgentByAgentIdDriveFilesPath = z.object({
|
||||
agent_id: z.uuid(),
|
||||
@ -2452,21 +2444,16 @@ export const zPostAgentByAgentIdSandboxFilesUploadPath = z.object({
|
||||
*/
|
||||
export const zPostAgentByAgentIdSandboxFilesUploadResponse = zSandboxUploadResponse
|
||||
|
||||
export const zPostAgentByAgentIdSkillsStandardizePath = z.object({
|
||||
agent_id: z.uuid(),
|
||||
export const zPostAgentByAgentIdSkillsUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill standardized into drive
|
||||
*/
|
||||
export const zPostAgentByAgentIdSkillsStandardizeResponse = zAgentSkillStandardizeResponse
|
||||
|
||||
export const zPostAgentByAgentIdSkillsUploadPath = z.object({
|
||||
agent_id: z.uuid(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill validated
|
||||
* Skill uploaded into drive
|
||||
*/
|
||||
export const zPostAgentByAgentIdSkillsUploadResponse = zAgentSkillUploadResponse
|
||||
|
||||
|
||||
@ -271,10 +271,9 @@ import {
|
||||
zPostAppsByAppIdAgentSkillsBySlugInferToolsPath,
|
||||
zPostAppsByAppIdAgentSkillsBySlugInferToolsQuery,
|
||||
zPostAppsByAppIdAgentSkillsBySlugInferToolsResponse,
|
||||
zPostAppsByAppIdAgentSkillsStandardizePath,
|
||||
zPostAppsByAppIdAgentSkillsStandardizeQuery,
|
||||
zPostAppsByAppIdAgentSkillsStandardizeResponse,
|
||||
zPostAppsByAppIdAgentSkillsUploadBody,
|
||||
zPostAppsByAppIdAgentSkillsUploadPath,
|
||||
zPostAppsByAppIdAgentSkillsUploadQuery,
|
||||
zPostAppsByAppIdAgentSkillsUploadResponse,
|
||||
zPostAppsByAppIdAnnotationReplyByActionBody,
|
||||
zPostAppsByAppIdAnnotationReplyByActionPath,
|
||||
@ -939,57 +938,32 @@ export const logs = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a Skill, validate it, and standardize it into the app agent's drive
|
||||
* Upload a Skill, validate it, and commit drive-backed skill files
|
||||
*
|
||||
* Validate + standardize a Skill into the agent drive (ENG-594)
|
||||
* Upload + standardize a Skill into the agent drive
|
||||
*/
|
||||
export const post10 = oc
|
||||
.route({
|
||||
description: 'Validate + standardize a Skill into the agent drive (ENG-594)',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postAppsByAppIdAgentSkillsStandardize',
|
||||
path: '/apps/{app_id}/agent/skills/standardize',
|
||||
successStatus: 201,
|
||||
summary: 'Upload a Skill, validate it, and standardize it into the app agent\'s drive',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
params: zPostAppsByAppIdAgentSkillsStandardizePath,
|
||||
query: zPostAppsByAppIdAgentSkillsStandardizeQuery.optional(),
|
||||
}),
|
||||
)
|
||||
.output(zPostAppsByAppIdAgentSkillsStandardizeResponse)
|
||||
|
||||
export const standardize = {
|
||||
post: post10,
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an uploaded Skill package and persist the archive
|
||||
*
|
||||
* Upload + validate a Skill package (.zip/.skill) and extract its manifest
|
||||
* Returns a validated skill ref (to bind into the Agent soul config on save)
|
||||
* plus its manifest. Standardizing into the agent drive is ENG-594.
|
||||
*/
|
||||
export const post11 = oc
|
||||
.route({
|
||||
description:
|
||||
'Upload + validate a Skill package (.zip/.skill) and extract its manifest\nReturns a validated skill ref (to bind into the Agent soul config on save)\nplus its manifest. Standardizing into the agent drive is ENG-594.',
|
||||
description: 'Upload + standardize a Skill into the agent drive',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postAppsByAppIdAgentSkillsUpload',
|
||||
path: '/apps/{app_id}/agent/skills/upload',
|
||||
successStatus: 201,
|
||||
summary: 'Validate an uploaded Skill package and persist the archive',
|
||||
summary: 'Upload a Skill, validate it, and commit drive-backed skill files',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ params: zPostAppsByAppIdAgentSkillsUploadPath }))
|
||||
.input(
|
||||
z.object({
|
||||
body: zPostAppsByAppIdAgentSkillsUploadBody,
|
||||
params: zPostAppsByAppIdAgentSkillsUploadPath,
|
||||
query: zPostAppsByAppIdAgentSkillsUploadQuery.optional(),
|
||||
}),
|
||||
)
|
||||
.output(zPostAppsByAppIdAgentSkillsUploadResponse)
|
||||
|
||||
export const upload = {
|
||||
post: post11,
|
||||
post: post10,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -998,7 +972,7 @@ export const upload = {
|
||||
* Infer CLI tool + ENV suggestions from a standardized skill's SKILL.md (draft only, ENG-371)
|
||||
* Saving still goes through composer validation.
|
||||
*/
|
||||
export const post12 = oc
|
||||
export const post11 = oc
|
||||
.route({
|
||||
description:
|
||||
'Infer CLI tool + ENV suggestions from a standardized skill\'s SKILL.md (draft only, ENG-371)\nSaving still goes through composer validation.',
|
||||
@ -1018,7 +992,7 @@ export const post12 = oc
|
||||
.output(zPostAppsByAppIdAgentSkillsBySlugInferToolsResponse)
|
||||
|
||||
export const inferTools = {
|
||||
post: post12,
|
||||
post: post11,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1048,7 +1022,6 @@ export const bySlug = {
|
||||
}
|
||||
|
||||
export const skills = {
|
||||
standardize,
|
||||
upload,
|
||||
bySlug,
|
||||
}
|
||||
@ -1086,7 +1059,7 @@ export const status = {
|
||||
/**
|
||||
* Enable or disable annotation reply for an app
|
||||
*/
|
||||
export const post13 = oc
|
||||
export const post12 = oc
|
||||
.route({
|
||||
description: 'Enable or disable annotation reply for an app',
|
||||
inputStructure: 'detailed',
|
||||
@ -1104,7 +1077,7 @@ export const post13 = oc
|
||||
.output(zPostAppsByAppIdAnnotationReplyByActionResponse)
|
||||
|
||||
export const byAction = {
|
||||
post: post13,
|
||||
post: post12,
|
||||
status,
|
||||
}
|
||||
|
||||
@ -1134,7 +1107,7 @@ export const annotationSetting = {
|
||||
/**
|
||||
* Update annotation settings for an app
|
||||
*/
|
||||
export const post14 = oc
|
||||
export const post13 = oc
|
||||
.route({
|
||||
description: 'Update annotation settings for an app',
|
||||
inputStructure: 'detailed',
|
||||
@ -1152,7 +1125,7 @@ export const post14 = oc
|
||||
.output(zPostAppsByAppIdAnnotationSettingsByAnnotationSettingIdResponse)
|
||||
|
||||
export const byAnnotationSettingId = {
|
||||
post: post14,
|
||||
post: post13,
|
||||
}
|
||||
|
||||
export const annotationSettings = {
|
||||
@ -1162,7 +1135,7 @@ export const annotationSettings = {
|
||||
/**
|
||||
* Batch import annotations from CSV file with rate limiting and security checks
|
||||
*/
|
||||
export const post15 = oc
|
||||
export const post14 = oc
|
||||
.route({
|
||||
description: 'Batch import annotations from CSV file with rate limiting and security checks',
|
||||
inputStructure: 'detailed',
|
||||
@ -1175,7 +1148,7 @@ export const post15 = oc
|
||||
.output(zPostAppsByAppIdAnnotationsBatchImportResponse)
|
||||
|
||||
export const batchImport = {
|
||||
post: post15,
|
||||
post: post14,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1278,7 +1251,7 @@ export const delete3 = oc
|
||||
/**
|
||||
* Update or delete an annotation
|
||||
*/
|
||||
export const post16 = oc
|
||||
export const post15 = oc
|
||||
.route({
|
||||
description: 'Update or delete an annotation',
|
||||
inputStructure: 'detailed',
|
||||
@ -1297,7 +1270,7 @@ export const post16 = oc
|
||||
|
||||
export const byAnnotationId = {
|
||||
delete: delete3,
|
||||
post: post16,
|
||||
post: post15,
|
||||
hitHistories,
|
||||
}
|
||||
|
||||
@ -1336,7 +1309,7 @@ export const get15 = oc
|
||||
/**
|
||||
* Create a new annotation for an app
|
||||
*/
|
||||
export const post17 = oc
|
||||
export const post16 = oc
|
||||
.route({
|
||||
description: 'Create a new annotation for an app',
|
||||
inputStructure: 'detailed',
|
||||
@ -1354,7 +1327,7 @@ export const post17 = oc
|
||||
export const annotations = {
|
||||
delete: delete4,
|
||||
get: get15,
|
||||
post: post17,
|
||||
post: post16,
|
||||
batchImport,
|
||||
batchImportStatus,
|
||||
count: count2,
|
||||
@ -1365,7 +1338,7 @@ export const annotations = {
|
||||
/**
|
||||
* Enable or disable app API
|
||||
*/
|
||||
export const post18 = oc
|
||||
export const post17 = oc
|
||||
.route({
|
||||
description: 'Enable or disable app API',
|
||||
inputStructure: 'detailed',
|
||||
@ -1378,13 +1351,13 @@ export const post18 = oc
|
||||
.output(zPostAppsByAppIdApiEnableResponse)
|
||||
|
||||
export const apiEnable = {
|
||||
post: post18,
|
||||
post: post17,
|
||||
}
|
||||
|
||||
/**
|
||||
* Transcript audio to text for chat messages
|
||||
*/
|
||||
export const post19 = oc
|
||||
export const post18 = oc
|
||||
.route({
|
||||
description: 'Transcript audio to text for chat messages',
|
||||
inputStructure: 'detailed',
|
||||
@ -1397,7 +1370,7 @@ export const post19 = oc
|
||||
.output(zPostAppsByAppIdAudioToTextResponse)
|
||||
|
||||
export const audioToText = {
|
||||
post: post19,
|
||||
post: post18,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1487,7 +1460,7 @@ export const byMessageId = {
|
||||
/**
|
||||
* Stop a running chat message generation
|
||||
*/
|
||||
export const post20 = oc
|
||||
export const post19 = oc
|
||||
.route({
|
||||
description: 'Stop a running chat message generation',
|
||||
inputStructure: 'detailed',
|
||||
@ -1500,7 +1473,7 @@ export const post20 = oc
|
||||
.output(zPostAppsByAppIdChatMessagesByTaskIdStopResponse)
|
||||
|
||||
export const stop = {
|
||||
post: post20,
|
||||
post: post19,
|
||||
}
|
||||
|
||||
export const byTaskId = {
|
||||
@ -1594,7 +1567,7 @@ export const completionConversations = {
|
||||
/**
|
||||
* Stop a running completion message generation
|
||||
*/
|
||||
export const post21 = oc
|
||||
export const post20 = oc
|
||||
.route({
|
||||
description: 'Stop a running completion message generation',
|
||||
inputStructure: 'detailed',
|
||||
@ -1607,7 +1580,7 @@ export const post21 = oc
|
||||
.output(zPostAppsByAppIdCompletionMessagesByTaskIdStopResponse)
|
||||
|
||||
export const stop2 = {
|
||||
post: post21,
|
||||
post: post20,
|
||||
}
|
||||
|
||||
export const byTaskId2 = {
|
||||
@ -1617,7 +1590,7 @@ export const byTaskId2 = {
|
||||
/**
|
||||
* Generate completion message for debugging
|
||||
*/
|
||||
export const post22 = oc
|
||||
export const post21 = oc
|
||||
.route({
|
||||
description: 'Generate completion message for debugging',
|
||||
inputStructure: 'detailed',
|
||||
@ -1635,7 +1608,7 @@ export const post22 = oc
|
||||
.output(zPostAppsByAppIdCompletionMessagesResponse)
|
||||
|
||||
export const completionMessages = {
|
||||
post: post22,
|
||||
post: post21,
|
||||
byTaskId: byTaskId2,
|
||||
}
|
||||
|
||||
@ -1670,7 +1643,7 @@ export const conversationVariables = {
|
||||
* Convert expert mode of chatbot app to workflow mode
|
||||
* Convert Completion App to Workflow App
|
||||
*/
|
||||
export const post23 = oc
|
||||
export const post22 = oc
|
||||
.route({
|
||||
description:
|
||||
'Convert application to workflow mode\nConvert expert mode of chatbot app to workflow mode\nConvert Completion App to Workflow App',
|
||||
@ -1690,7 +1663,7 @@ export const post23 = oc
|
||||
.output(zPostAppsByAppIdConvertToWorkflowResponse)
|
||||
|
||||
export const convertToWorkflow = {
|
||||
post: post23,
|
||||
post: post22,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1698,7 +1671,7 @@ export const convertToWorkflow = {
|
||||
*
|
||||
* Create a copy of an existing application
|
||||
*/
|
||||
export const post24 = oc
|
||||
export const post23 = oc
|
||||
.route({
|
||||
description: 'Create a copy of an existing application',
|
||||
inputStructure: 'detailed',
|
||||
@ -1713,7 +1686,7 @@ export const post24 = oc
|
||||
.output(zPostAppsByAppIdCopyResponse)
|
||||
|
||||
export const copy = {
|
||||
post: post24,
|
||||
post: post23,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1767,7 +1740,7 @@ export const export3 = {
|
||||
/**
|
||||
* Create or update message feedback (like/dislike)
|
||||
*/
|
||||
export const post25 = oc
|
||||
export const post24 = oc
|
||||
.route({
|
||||
description: 'Create or update message feedback (like/dislike)',
|
||||
inputStructure: 'detailed',
|
||||
@ -1780,14 +1753,14 @@ export const post25 = oc
|
||||
.output(zPostAppsByAppIdFeedbacksResponse)
|
||||
|
||||
export const feedbacks = {
|
||||
post: post25,
|
||||
post: post24,
|
||||
export: export3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Update application icon
|
||||
*/
|
||||
export const post26 = oc
|
||||
export const post25 = oc
|
||||
.route({
|
||||
description: 'Update application icon',
|
||||
inputStructure: 'detailed',
|
||||
@ -1800,7 +1773,7 @@ export const post26 = oc
|
||||
.output(zPostAppsByAppIdIconResponse)
|
||||
|
||||
export const icon = {
|
||||
post: post26,
|
||||
post: post25,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1831,7 +1804,7 @@ export const messages = {
|
||||
*
|
||||
* Update application model configuration
|
||||
*/
|
||||
export const post27 = oc
|
||||
export const post26 = oc
|
||||
.route({
|
||||
description: 'Update application model configuration',
|
||||
inputStructure: 'detailed',
|
||||
@ -1847,13 +1820,13 @@ export const post27 = oc
|
||||
.output(zPostAppsByAppIdModelConfigResponse)
|
||||
|
||||
export const modelConfig = {
|
||||
post: post27,
|
||||
post: post26,
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app name is available
|
||||
*/
|
||||
export const post28 = oc
|
||||
export const post27 = oc
|
||||
.route({
|
||||
description: 'Check if app name is available',
|
||||
inputStructure: 'detailed',
|
||||
@ -1866,13 +1839,13 @@ export const post28 = oc
|
||||
.output(zPostAppsByAppIdNameResponse)
|
||||
|
||||
export const name = {
|
||||
post: post28,
|
||||
post: post27,
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish app to Creators Platform
|
||||
*/
|
||||
export const post29 = oc
|
||||
export const post28 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -1885,7 +1858,7 @@ export const post29 = oc
|
||||
.output(zPostAppsByAppIdPublishToCreatorsPlatformResponse)
|
||||
|
||||
export const publishToCreatorsPlatform = {
|
||||
post: post29,
|
||||
post: post28,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1906,7 +1879,7 @@ export const get26 = oc
|
||||
/**
|
||||
* Create MCP server configuration for an application
|
||||
*/
|
||||
export const post30 = oc
|
||||
export const post29 = oc
|
||||
.route({
|
||||
description: 'Create MCP server configuration for an application',
|
||||
inputStructure: 'detailed',
|
||||
@ -1936,14 +1909,14 @@ export const put = oc
|
||||
|
||||
export const server = {
|
||||
get: get26,
|
||||
post: post30,
|
||||
post: post29,
|
||||
put,
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset access token for application site
|
||||
*/
|
||||
export const post31 = oc
|
||||
export const post30 = oc
|
||||
.route({
|
||||
description: 'Reset access token for application site',
|
||||
inputStructure: 'detailed',
|
||||
@ -1956,13 +1929,13 @@ export const post31 = oc
|
||||
.output(zPostAppsByAppIdSiteAccessTokenResetResponse)
|
||||
|
||||
export const accessTokenReset = {
|
||||
post: post31,
|
||||
post: post30,
|
||||
}
|
||||
|
||||
/**
|
||||
* Update application site configuration
|
||||
*/
|
||||
export const post32 = oc
|
||||
export const post31 = oc
|
||||
.route({
|
||||
description: 'Update application site configuration',
|
||||
inputStructure: 'detailed',
|
||||
@ -1975,14 +1948,14 @@ export const post32 = oc
|
||||
.output(zPostAppsByAppIdSiteResponse)
|
||||
|
||||
export const site = {
|
||||
post: post32,
|
||||
post: post31,
|
||||
accessTokenReset,
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable app site
|
||||
*/
|
||||
export const post33 = oc
|
||||
export const post32 = oc
|
||||
.route({
|
||||
description: 'Enable or disable app site',
|
||||
inputStructure: 'detailed',
|
||||
@ -1995,7 +1968,7 @@ export const post33 = oc
|
||||
.output(zPostAppsByAppIdSiteEnableResponse)
|
||||
|
||||
export const siteEnable = {
|
||||
post: post33,
|
||||
post: post32,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2016,7 +1989,7 @@ export const delete7 = oc
|
||||
/**
|
||||
* Star an application for the current account
|
||||
*/
|
||||
export const post34 = oc
|
||||
export const post33 = oc
|
||||
.route({
|
||||
description: 'Star an application for the current account',
|
||||
inputStructure: 'detailed',
|
||||
@ -2030,7 +2003,7 @@ export const post34 = oc
|
||||
|
||||
export const star = {
|
||||
delete: delete7,
|
||||
post: post34,
|
||||
post: post33,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2263,7 +2236,7 @@ export const voices = {
|
||||
/**
|
||||
* Convert text to speech for chat messages
|
||||
*/
|
||||
export const post35 = oc
|
||||
export const post34 = oc
|
||||
.route({
|
||||
description: 'Convert text to speech for chat messages',
|
||||
inputStructure: 'detailed',
|
||||
@ -2278,7 +2251,7 @@ export const post35 = oc
|
||||
.output(zPostAppsByAppIdTextToAudioResponse)
|
||||
|
||||
export const textToAudio = {
|
||||
post: post35,
|
||||
post: post34,
|
||||
voices,
|
||||
}
|
||||
|
||||
@ -2303,7 +2276,7 @@ export const get36 = oc
|
||||
/**
|
||||
* Update app tracing configuration
|
||||
*/
|
||||
export const post36 = oc
|
||||
export const post35 = oc
|
||||
.route({
|
||||
description: 'Update app tracing configuration',
|
||||
inputStructure: 'detailed',
|
||||
@ -2317,7 +2290,7 @@ export const post36 = oc
|
||||
|
||||
export const trace = {
|
||||
get: get36,
|
||||
post: post36,
|
||||
post: post35,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2386,7 +2359,7 @@ export const patch = oc
|
||||
*
|
||||
* Create a new tracing configuration for an application
|
||||
*/
|
||||
export const post37 = oc
|
||||
export const post36 = oc
|
||||
.route({
|
||||
description: 'Create a new tracing configuration for an application',
|
||||
inputStructure: 'detailed',
|
||||
@ -2406,13 +2379,13 @@ export const traceConfig = {
|
||||
delete: delete8,
|
||||
get: get37,
|
||||
patch,
|
||||
post: post37,
|
||||
post: post36,
|
||||
}
|
||||
|
||||
/**
|
||||
* Update app trigger (enable/disable)
|
||||
*/
|
||||
export const post38 = oc
|
||||
export const post37 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -2430,7 +2403,7 @@ export const post38 = oc
|
||||
.output(zPostAppsByAppIdTriggerEnableResponse)
|
||||
|
||||
export const triggerEnable = {
|
||||
post: post38,
|
||||
post: post37,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2538,7 +2511,7 @@ export const count3 = {
|
||||
*
|
||||
* Stop running workflow task
|
||||
*/
|
||||
export const post39 = oc
|
||||
export const post38 = oc
|
||||
.route({
|
||||
description: 'Stop running workflow task',
|
||||
inputStructure: 'detailed',
|
||||
@ -2552,7 +2525,7 @@ export const post39 = oc
|
||||
.output(zPostAppsByAppIdWorkflowRunsTasksByTaskIdStopResponse)
|
||||
|
||||
export const stop3 = {
|
||||
post: post39,
|
||||
post: post38,
|
||||
}
|
||||
|
||||
export const byTaskId3 = {
|
||||
@ -2655,7 +2628,7 @@ export const read = {
|
||||
/**
|
||||
* Upload one workflow Agent sandbox file as a Dify ToolFile mapping
|
||||
*/
|
||||
export const post40 = oc
|
||||
export const post39 = oc
|
||||
.route({
|
||||
description: 'Upload one workflow Agent sandbox file as a Dify ToolFile mapping',
|
||||
inputStructure: 'detailed',
|
||||
@ -2673,7 +2646,7 @@ export const post40 = oc
|
||||
.output(zPostAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesUploadResponse)
|
||||
|
||||
export const upload2 = {
|
||||
post: post40,
|
||||
post: post39,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2824,7 +2797,7 @@ export const byReplyId = {
|
||||
*
|
||||
* Add a reply to a workflow comment
|
||||
*/
|
||||
export const post41 = oc
|
||||
export const post40 = oc
|
||||
.route({
|
||||
description: 'Add a reply to a workflow comment',
|
||||
inputStructure: 'detailed',
|
||||
@ -2844,7 +2817,7 @@ export const post41 = oc
|
||||
.output(zPostAppsByAppIdWorkflowCommentsByCommentIdRepliesResponse)
|
||||
|
||||
export const replies = {
|
||||
post: post41,
|
||||
post: post40,
|
||||
byReplyId,
|
||||
}
|
||||
|
||||
@ -2853,7 +2826,7 @@ export const replies = {
|
||||
*
|
||||
* Resolve a workflow comment
|
||||
*/
|
||||
export const post42 = oc
|
||||
export const post41 = oc
|
||||
.route({
|
||||
description: 'Resolve a workflow comment',
|
||||
inputStructure: 'detailed',
|
||||
@ -2867,7 +2840,7 @@ export const post42 = oc
|
||||
.output(zPostAppsByAppIdWorkflowCommentsByCommentIdResolveResponse)
|
||||
|
||||
export const resolve = {
|
||||
post: post42,
|
||||
post: post41,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2961,7 +2934,7 @@ export const get50 = oc
|
||||
*
|
||||
* Create a new workflow comment
|
||||
*/
|
||||
export const post43 = oc
|
||||
export const post42 = oc
|
||||
.route({
|
||||
description: 'Create a new workflow comment',
|
||||
inputStructure: 'detailed',
|
||||
@ -2982,7 +2955,7 @@ export const post43 = oc
|
||||
|
||||
export const comments = {
|
||||
get: get50,
|
||||
post: post43,
|
||||
post: post42,
|
||||
mentionUsers,
|
||||
byCommentId,
|
||||
}
|
||||
@ -3163,7 +3136,7 @@ export const get57 = oc
|
||||
/**
|
||||
* Update conversation variables for workflow draft
|
||||
*/
|
||||
export const post44 = oc
|
||||
export const post43 = oc
|
||||
.route({
|
||||
description: 'Update conversation variables for workflow draft',
|
||||
inputStructure: 'detailed',
|
||||
@ -3182,7 +3155,7 @@ export const post44 = oc
|
||||
|
||||
export const conversationVariables2 = {
|
||||
get: get57,
|
||||
post: post44,
|
||||
post: post43,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3206,7 +3179,7 @@ export const get58 = oc
|
||||
/**
|
||||
* Update environment variables for workflow draft
|
||||
*/
|
||||
export const post45 = oc
|
||||
export const post44 = oc
|
||||
.route({
|
||||
description: 'Update environment variables for workflow draft',
|
||||
inputStructure: 'detailed',
|
||||
@ -3225,13 +3198,13 @@ export const post45 = oc
|
||||
|
||||
export const environmentVariables = {
|
||||
get: get58,
|
||||
post: post45,
|
||||
post: post44,
|
||||
}
|
||||
|
||||
/**
|
||||
* Update draft workflow features
|
||||
*/
|
||||
export const post46 = oc
|
||||
export const post45 = oc
|
||||
.route({
|
||||
description: 'Update draft workflow features',
|
||||
inputStructure: 'detailed',
|
||||
@ -3249,7 +3222,7 @@ export const post46 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftFeaturesResponse)
|
||||
|
||||
export const features = {
|
||||
post: post46,
|
||||
post: post45,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3257,7 +3230,7 @@ export const features = {
|
||||
*
|
||||
* Test human input delivery for workflow
|
||||
*/
|
||||
export const post47 = oc
|
||||
export const post46 = oc
|
||||
.route({
|
||||
description: 'Test human input delivery for workflow',
|
||||
inputStructure: 'detailed',
|
||||
@ -3276,7 +3249,7 @@ export const post47 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdDeliveryTestResponse)
|
||||
|
||||
export const deliveryTest = {
|
||||
post: post47,
|
||||
post: post46,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3284,7 +3257,7 @@ export const deliveryTest = {
|
||||
*
|
||||
* Get human input form preview for workflow
|
||||
*/
|
||||
export const post48 = oc
|
||||
export const post47 = oc
|
||||
.route({
|
||||
description: 'Get human input form preview for workflow',
|
||||
inputStructure: 'detailed',
|
||||
@ -3303,7 +3276,7 @@ export const post48 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewResponse)
|
||||
|
||||
export const preview3 = {
|
||||
post: post48,
|
||||
post: post47,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3311,7 +3284,7 @@ export const preview3 = {
|
||||
*
|
||||
* Submit human input form preview for workflow
|
||||
*/
|
||||
export const post49 = oc
|
||||
export const post48 = oc
|
||||
.route({
|
||||
description: 'Submit human input form preview for workflow',
|
||||
inputStructure: 'detailed',
|
||||
@ -3330,7 +3303,7 @@ export const post49 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunResponse)
|
||||
|
||||
export const run5 = {
|
||||
post: post49,
|
||||
post: post48,
|
||||
}
|
||||
|
||||
export const form2 = {
|
||||
@ -3356,7 +3329,7 @@ export const humanInput2 = {
|
||||
*
|
||||
* Run draft workflow iteration node
|
||||
*/
|
||||
export const post50 = oc
|
||||
export const post49 = oc
|
||||
.route({
|
||||
description: 'Run draft workflow iteration node',
|
||||
inputStructure: 'detailed',
|
||||
@ -3375,7 +3348,7 @@ export const post50 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftIterationNodesByNodeIdRunResponse)
|
||||
|
||||
export const run6 = {
|
||||
post: post50,
|
||||
post: post49,
|
||||
}
|
||||
|
||||
export const byNodeId6 = {
|
||||
@ -3395,7 +3368,7 @@ export const iteration2 = {
|
||||
*
|
||||
* Run draft workflow loop node
|
||||
*/
|
||||
export const post51 = oc
|
||||
export const post50 = oc
|
||||
.route({
|
||||
description: 'Run draft workflow loop node',
|
||||
inputStructure: 'detailed',
|
||||
@ -3414,7 +3387,7 @@ export const post51 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunResponse)
|
||||
|
||||
export const run7 = {
|
||||
post: post51,
|
||||
post: post50,
|
||||
}
|
||||
|
||||
export const byNodeId7 = {
|
||||
@ -3446,7 +3419,7 @@ export const candidates = {
|
||||
get: get59,
|
||||
}
|
||||
|
||||
export const post52 = oc
|
||||
export const post51 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -3463,10 +3436,10 @@ export const post52 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactResponse)
|
||||
|
||||
export const impact = {
|
||||
post: post52,
|
||||
post: post51,
|
||||
}
|
||||
|
||||
export const post53 = oc
|
||||
export const post52 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -3483,10 +3456,10 @@ export const post53 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerSaveToRosterResponse)
|
||||
|
||||
export const saveToRoster = {
|
||||
post: post53,
|
||||
post: post52,
|
||||
}
|
||||
|
||||
export const post54 = oc
|
||||
export const post53 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -3503,7 +3476,7 @@ export const post54 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerValidateResponse)
|
||||
|
||||
export const validate = {
|
||||
post: post54,
|
||||
post: post53,
|
||||
}
|
||||
|
||||
export const get60 = oc
|
||||
@ -3566,7 +3539,7 @@ export const lastRun = {
|
||||
*
|
||||
* Run draft workflow node
|
||||
*/
|
||||
export const post55 = oc
|
||||
export const post54 = oc
|
||||
.route({
|
||||
description: 'Run draft workflow node',
|
||||
inputStructure: 'detailed',
|
||||
@ -3585,7 +3558,7 @@ export const post55 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdRunResponse)
|
||||
|
||||
export const run8 = {
|
||||
post: post55,
|
||||
post: post54,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3593,7 +3566,7 @@ export const run8 = {
|
||||
*
|
||||
* Poll for trigger events and execute single node when event arrives
|
||||
*/
|
||||
export const post56 = oc
|
||||
export const post55 = oc
|
||||
.route({
|
||||
description: 'Poll for trigger events and execute single node when event arrives',
|
||||
inputStructure: 'detailed',
|
||||
@ -3607,7 +3580,7 @@ export const post56 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdTriggerRunResponse)
|
||||
|
||||
export const run9 = {
|
||||
post: post56,
|
||||
post: post55,
|
||||
}
|
||||
|
||||
export const trigger = {
|
||||
@ -3667,7 +3640,7 @@ export const nodes7 = {
|
||||
*
|
||||
* Run draft workflow
|
||||
*/
|
||||
export const post57 = oc
|
||||
export const post56 = oc
|
||||
.route({
|
||||
description: 'Run draft workflow',
|
||||
inputStructure: 'detailed',
|
||||
@ -3686,7 +3659,7 @@ export const post57 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftRunResponse)
|
||||
|
||||
export const run10 = {
|
||||
post: post57,
|
||||
post: post56,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3808,7 +3781,7 @@ export const systemVariables = {
|
||||
*
|
||||
* Poll for trigger events and execute full workflow when event arrives
|
||||
*/
|
||||
export const post58 = oc
|
||||
export const post57 = oc
|
||||
.route({
|
||||
description: 'Poll for trigger events and execute full workflow when event arrives',
|
||||
inputStructure: 'detailed',
|
||||
@ -3827,7 +3800,7 @@ export const post58 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftTriggerRunResponse)
|
||||
|
||||
export const run11 = {
|
||||
post: post58,
|
||||
post: post57,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3835,7 +3808,7 @@ export const run11 = {
|
||||
*
|
||||
* Full workflow debug when the start node is a trigger
|
||||
*/
|
||||
export const post59 = oc
|
||||
export const post58 = oc
|
||||
.route({
|
||||
description: 'Full workflow debug when the start node is a trigger',
|
||||
inputStructure: 'detailed',
|
||||
@ -3854,7 +3827,7 @@ export const post59 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsDraftTriggerRunAllResponse)
|
||||
|
||||
export const runAll = {
|
||||
post: post59,
|
||||
post: post58,
|
||||
}
|
||||
|
||||
export const trigger2 = {
|
||||
@ -4007,7 +3980,7 @@ export const get70 = oc
|
||||
*
|
||||
* Sync draft workflow configuration
|
||||
*/
|
||||
export const post60 = oc
|
||||
export const post59 = oc
|
||||
.route({
|
||||
description: 'Sync draft workflow configuration',
|
||||
inputStructure: 'detailed',
|
||||
@ -4027,7 +4000,7 @@ export const post60 = oc
|
||||
|
||||
export const draft2 = {
|
||||
get: get70,
|
||||
post: post60,
|
||||
post: post59,
|
||||
conversationVariables: conversationVariables2,
|
||||
environmentVariables,
|
||||
features,
|
||||
@ -4063,7 +4036,7 @@ export const get71 = oc
|
||||
/**
|
||||
* Publish workflow
|
||||
*/
|
||||
export const post61 = oc
|
||||
export const post60 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
@ -4082,7 +4055,7 @@ export const post61 = oc
|
||||
|
||||
export const publish = {
|
||||
get: get71,
|
||||
post: post61,
|
||||
post: post60,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4219,7 +4192,7 @@ export const triggers2 = {
|
||||
/**
|
||||
* Restore a published workflow version into the draft workflow
|
||||
*/
|
||||
export const post62 = oc
|
||||
export const post61 = oc
|
||||
.route({
|
||||
description: 'Restore a published workflow version into the draft workflow',
|
||||
inputStructure: 'detailed',
|
||||
@ -4232,7 +4205,7 @@ export const post62 = oc
|
||||
.output(zPostAppsByAppIdWorkflowsByWorkflowIdRestoreResponse)
|
||||
|
||||
export const restore = {
|
||||
post: post62,
|
||||
post: post61,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4457,7 +4430,7 @@ export const get79 = oc
|
||||
*
|
||||
* Create a new API key for an app
|
||||
*/
|
||||
export const post63 = oc
|
||||
export const post62 = oc
|
||||
.route({
|
||||
description: 'Create a new API key for an app',
|
||||
inputStructure: 'detailed',
|
||||
@ -4473,7 +4446,7 @@ export const post63 = oc
|
||||
|
||||
export const apiKeys = {
|
||||
get: get79,
|
||||
post: post63,
|
||||
post: post62,
|
||||
byApiKeyId,
|
||||
}
|
||||
|
||||
@ -4531,7 +4504,7 @@ export const get81 = oc
|
||||
*
|
||||
* Create a new application
|
||||
*/
|
||||
export const post64 = oc
|
||||
export const post63 = oc
|
||||
.route({
|
||||
description: 'Create a new application',
|
||||
inputStructure: 'detailed',
|
||||
@ -4547,7 +4520,7 @@ export const post64 = oc
|
||||
|
||||
export const apps = {
|
||||
get: get81,
|
||||
post: post64,
|
||||
post: post63,
|
||||
imports,
|
||||
starred,
|
||||
workflows,
|
||||
|
||||
@ -5,10 +5,10 @@ export type ClientOptions = {
|
||||
}
|
||||
|
||||
export type AppPagination = {
|
||||
has_next: boolean
|
||||
items: Array<AppPartial>
|
||||
data: Array<AppPartial>
|
||||
has_more: boolean
|
||||
limit: number
|
||||
page: number
|
||||
per_page: number
|
||||
total: number
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@ export type CreateAppPayload = {
|
||||
|
||||
export type AppDetailWithSite = {
|
||||
access_mode?: string | null
|
||||
active_config_is_published?: boolean
|
||||
api_base_url?: string | null
|
||||
app_id?: string | null
|
||||
bound_agent_id?: string | null
|
||||
@ -42,7 +41,6 @@ export type AppDetailWithSite = {
|
||||
mode: string
|
||||
model_config?: ModelConfig | null
|
||||
name: string
|
||||
role?: string | null
|
||||
site?: Site | null
|
||||
tags?: Array<Tag>
|
||||
tracing?: JsonValue | null
|
||||
@ -213,11 +211,6 @@ export type AgentLogResponse = {
|
||||
meta: AgentLogMetaResponse
|
||||
}
|
||||
|
||||
export type AgentSkillStandardizeResponse = {
|
||||
manifest: SkillManifest
|
||||
skill: AgentSkillRefConfig
|
||||
}
|
||||
|
||||
export type AgentSkillUploadResponse = {
|
||||
manifest: SkillManifest
|
||||
skill: AgentSkillRefConfig
|
||||
@ -1156,25 +1149,24 @@ export type ApiKeyItem = {
|
||||
|
||||
export type AppPartial = {
|
||||
access_mode?: string | null
|
||||
active_config_is_published?: boolean
|
||||
app_id?: string | null
|
||||
app_model_config?: ModelConfigPartial | null
|
||||
author_name?: string | null
|
||||
bound_agent_id?: string | null
|
||||
create_user_name?: string | null
|
||||
created_at?: number | null
|
||||
created_by?: string | null
|
||||
desc_or_prompt?: string | null
|
||||
description?: string | null
|
||||
has_draft_trigger?: boolean | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
readonly icon_url: string | null
|
||||
id: string
|
||||
is_starred?: boolean
|
||||
max_active_requests?: number | null
|
||||
mode_compatible_with_agent: string
|
||||
mode: string
|
||||
model_config?: ModelConfigPartial | null
|
||||
name: string
|
||||
role?: string | null
|
||||
tags?: Array<Tag>
|
||||
updated_at?: number | null
|
||||
updated_by?: string | null
|
||||
@ -2575,9 +2567,16 @@ export type AgentModerationIoConfig = {
|
||||
|
||||
export type ValueSourceType = 'constant' | 'variable'
|
||||
|
||||
export type AppPaginationWritable = {
|
||||
data: Array<AppPartialWritable>
|
||||
has_more: boolean
|
||||
limit: number
|
||||
page: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type AppDetailWithSiteWritable = {
|
||||
access_mode?: string | null
|
||||
active_config_is_published?: boolean
|
||||
api_base_url?: string | null
|
||||
app_id?: string | null
|
||||
bound_agent_id?: string | null
|
||||
@ -2595,7 +2594,6 @@ export type AppDetailWithSiteWritable = {
|
||||
mode: string
|
||||
model_config?: ModelConfig | null
|
||||
name: string
|
||||
role?: string | null
|
||||
site?: SiteWritable | null
|
||||
tags?: Array<Tag>
|
||||
tracing?: JsonValue | null
|
||||
@ -2628,6 +2626,32 @@ export type WorkflowCommentDetailWritable = {
|
||||
updated_at?: number | null
|
||||
}
|
||||
|
||||
export type AppPartialWritable = {
|
||||
access_mode?: string | null
|
||||
app_id?: string | null
|
||||
author_name?: string | null
|
||||
bound_agent_id?: string | null
|
||||
create_user_name?: string | null
|
||||
created_at?: number | null
|
||||
created_by?: string | null
|
||||
description?: string | null
|
||||
has_draft_trigger?: boolean | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
id: string
|
||||
is_starred?: boolean
|
||||
max_active_requests?: number | null
|
||||
mode: string
|
||||
model_config?: ModelConfigPartial | null
|
||||
name: string
|
||||
tags?: Array<Tag>
|
||||
updated_at?: number | null
|
||||
updated_by?: string | null
|
||||
use_icon_as_answer_icon?: boolean | null
|
||||
workflow?: WorkflowPartial | null
|
||||
}
|
||||
|
||||
export type SiteWritable = {
|
||||
chat_color_theme?: string | null
|
||||
chat_color_theme_inverted: boolean
|
||||
@ -3144,34 +3168,16 @@ export type GetAppsByAppIdAgentLogsResponses = {
|
||||
export type GetAppsByAppIdAgentLogsResponse
|
||||
= GetAppsByAppIdAgentLogsResponses[keyof GetAppsByAppIdAgentLogsResponses]
|
||||
|
||||
export type PostAppsByAppIdAgentSkillsStandardizeData = {
|
||||
body?: never
|
||||
export type PostAppsByAppIdAgentSkillsUploadData = {
|
||||
body: {
|
||||
file: Blob | File
|
||||
}
|
||||
path: {
|
||||
app_id: string
|
||||
}
|
||||
query?: {
|
||||
node_id?: string
|
||||
}
|
||||
url: '/apps/{app_id}/agent/skills/standardize'
|
||||
}
|
||||
|
||||
export type PostAppsByAppIdAgentSkillsStandardizeErrors = {
|
||||
400: unknown
|
||||
}
|
||||
|
||||
export type PostAppsByAppIdAgentSkillsStandardizeResponses = {
|
||||
201: AgentSkillStandardizeResponse
|
||||
}
|
||||
|
||||
export type PostAppsByAppIdAgentSkillsStandardizeResponse
|
||||
= PostAppsByAppIdAgentSkillsStandardizeResponses[keyof PostAppsByAppIdAgentSkillsStandardizeResponses]
|
||||
|
||||
export type PostAppsByAppIdAgentSkillsUploadData = {
|
||||
body?: never
|
||||
path: {
|
||||
app_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/apps/{app_id}/agent/skills/upload'
|
||||
}
|
||||
|
||||
|
||||
@ -990,14 +990,6 @@ export const zAgentSkillRefConfig = z.object({
|
||||
skill_md_key: z.string().max(512).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AgentSkillStandardizeResponse
|
||||
*/
|
||||
export const zAgentSkillStandardizeResponse = z.object({
|
||||
manifest: zSkillManifest,
|
||||
skill: zAgentSkillRefConfig,
|
||||
})
|
||||
|
||||
/**
|
||||
* AgentSkillUploadResponse
|
||||
*/
|
||||
@ -1946,25 +1938,24 @@ export const zModelConfigPartial = z.object({
|
||||
*/
|
||||
export const zAppPartial = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
active_config_is_published: z.boolean().optional().default(false),
|
||||
app_id: z.string().nullish(),
|
||||
app_model_config: zModelConfigPartial.nullish(),
|
||||
author_name: z.string().nullish(),
|
||||
bound_agent_id: z.string().nullish(),
|
||||
create_user_name: z.string().nullish(),
|
||||
created_at: z.int().nullish(),
|
||||
created_by: z.string().nullish(),
|
||||
desc_or_prompt: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
has_draft_trigger: z.boolean().nullish(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
icon_url: z.string().nullable(),
|
||||
id: z.string(),
|
||||
is_starred: z.boolean().optional().default(false),
|
||||
max_active_requests: z.int().nullish(),
|
||||
mode_compatible_with_agent: z.string(),
|
||||
mode: z.string(),
|
||||
model_config: zModelConfigPartial.nullish(),
|
||||
name: z.string(),
|
||||
role: z.string().nullish(),
|
||||
tags: z.array(zTag).optional(),
|
||||
updated_at: z.int().nullish(),
|
||||
updated_by: z.string().nullish(),
|
||||
@ -1976,10 +1967,10 @@ export const zAppPartial = z.object({
|
||||
* AppPagination
|
||||
*/
|
||||
export const zAppPagination = z.object({
|
||||
has_next: z.boolean(),
|
||||
items: z.array(zAppPartial),
|
||||
data: z.array(zAppPartial),
|
||||
has_more: z.boolean(),
|
||||
limit: z.int(),
|
||||
page: z.int(),
|
||||
per_page: z.int(),
|
||||
total: z.int(),
|
||||
})
|
||||
|
||||
@ -2005,7 +1996,6 @@ export const zModelConfig = z.object({
|
||||
*/
|
||||
export const zAppDetailWithSite = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
active_config_is_published: z.boolean().optional().default(false),
|
||||
api_base_url: z.string().nullish(),
|
||||
app_id: z.string().nullish(),
|
||||
bound_agent_id: z.string().nullish(),
|
||||
@ -2024,7 +2014,6 @@ export const zAppDetailWithSite = z.object({
|
||||
mode: z.string(),
|
||||
model_config: zModelConfig.nullish(),
|
||||
name: z.string(),
|
||||
role: z.string().nullish(),
|
||||
site: zSite.nullish(),
|
||||
tags: z.array(zTag).optional(),
|
||||
tracing: zJsonValue.nullish(),
|
||||
@ -3472,6 +3461,46 @@ export const zMessageInfiniteScrollPaginationResponse = z.object({
|
||||
*/
|
||||
export const zGeneratedAppResponseWritable = zJsonValue
|
||||
|
||||
/**
|
||||
* AppPartial
|
||||
*/
|
||||
export const zAppPartialWritable = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
app_id: z.string().nullish(),
|
||||
author_name: z.string().nullish(),
|
||||
bound_agent_id: z.string().nullish(),
|
||||
create_user_name: z.string().nullish(),
|
||||
created_at: z.int().nullish(),
|
||||
created_by: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
has_draft_trigger: z.boolean().nullish(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
id: z.string(),
|
||||
is_starred: z.boolean().optional().default(false),
|
||||
max_active_requests: z.int().nullish(),
|
||||
mode: z.string(),
|
||||
model_config: zModelConfigPartial.nullish(),
|
||||
name: z.string(),
|
||||
tags: z.array(zTag).optional(),
|
||||
updated_at: z.int().nullish(),
|
||||
updated_by: z.string().nullish(),
|
||||
use_icon_as_answer_icon: z.boolean().nullish(),
|
||||
workflow: zWorkflowPartial.nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AppPagination
|
||||
*/
|
||||
export const zAppPaginationWritable = z.object({
|
||||
data: z.array(zAppPartialWritable),
|
||||
has_more: z.boolean(),
|
||||
limit: z.int(),
|
||||
page: z.int(),
|
||||
total: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Site
|
||||
*/
|
||||
@ -3496,7 +3525,6 @@ export const zSiteWritable = z.object({
|
||||
*/
|
||||
export const zAppDetailWithSiteWritable = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
active_config_is_published: z.boolean().optional().default(false),
|
||||
api_base_url: z.string().nullish(),
|
||||
app_id: z.string().nullish(),
|
||||
bound_agent_id: z.string().nullish(),
|
||||
@ -3514,7 +3542,6 @@ export const zAppDetailWithSiteWritable = z.object({
|
||||
mode: z.string(),
|
||||
model_config: zModelConfig.nullish(),
|
||||
name: z.string(),
|
||||
role: z.string().nullish(),
|
||||
site: zSiteWritable.nullish(),
|
||||
tags: z.array(zTag).optional(),
|
||||
tracing: zJsonValue.nullish(),
|
||||
@ -3917,25 +3944,20 @@ export const zGetAppsByAppIdAgentLogsQuery = z.object({
|
||||
*/
|
||||
export const zGetAppsByAppIdAgentLogsResponse = zAgentLogResponse
|
||||
|
||||
export const zPostAppsByAppIdAgentSkillsStandardizePath = z.object({
|
||||
app_id: z.uuid(),
|
||||
export const zPostAppsByAppIdAgentSkillsUploadBody = z.object({
|
||||
file: z.custom<Blob | File>(),
|
||||
})
|
||||
|
||||
export const zPostAppsByAppIdAgentSkillsStandardizeQuery = z.object({
|
||||
node_id: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill standardized into drive
|
||||
*/
|
||||
export const zPostAppsByAppIdAgentSkillsStandardizeResponse = zAgentSkillStandardizeResponse
|
||||
|
||||
export const zPostAppsByAppIdAgentSkillsUploadPath = z.object({
|
||||
app_id: z.uuid(),
|
||||
})
|
||||
|
||||
export const zPostAppsByAppIdAgentSkillsUploadQuery = z.object({
|
||||
node_id: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Skill validated
|
||||
* Skill uploaded into drive
|
||||
*/
|
||||
export const zPostAppsByAppIdAgentSkillsUploadResponse = zAgentSkillUploadResponse
|
||||
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z" fill="#155AEF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,8 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z" fill="currentColor" />
|
||||
<path d="M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z" fill="currentColor" />
|
||||
<path d="M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z" fill="currentColor" />
|
||||
<path d="M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z" fill="currentColor" />
|
||||
<path d="M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z" fill="currentColor" />
|
||||
<path d="M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z" fill="currentColor"/>
|
||||
<path d="M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 693 B |
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z" fill="#676F83" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 675 B |
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none">
|
||||
<path d="M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z" fill="#676F83"/>
|
||||
<path d="M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z" fill="#676F83"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z" fill="#676F83"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@ -0,0 +1,6 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.9362 1.5C12.4177 1.50006 12.8411 1.81975 12.9727 2.28296L15.4651 11.061C15.5117 11.2251 15.6125 11.3687 15.7515 11.4675L16.1235 11.7319C16.3544 11.8963 16.4728 12.1769 16.4297 12.4571L15.9206 15.7669C15.8394 16.2948 15.2475 16.5708 14.7905 16.2942L12.8006 15.0893C12.6837 15.0186 12.5618 14.9546 12.4307 14.9165C12.2411 14.8615 12.0443 14.833 11.8462 14.833C10.6885 14.833 9.75 13.8945 9.75 12.7368V9.14722C9.75 8.23747 10.4875 7.5 11.3972 7.5C11.5688 7.5 11.7164 7.62113 11.7503 7.78928L12.3824 10.9483L12.4043 11.0215C12.4721 11.182 12.6457 11.2781 12.8233 11.2427C13.0009 11.2072 13.1242 11.0515 13.125 10.8772L13.1177 10.8017L12.4856 7.6428C12.3818 7.12384 11.9265 6.75002 11.3972 6.75C11.08 6.75 10.7771 6.81165 10.5 6.92359V2.93628C10.5 2.14312 11.1431 1.5 11.9362 1.5Z" fill="currentColor"/>
|
||||
<path d="M2.28761 11.1211C2.263 11.2855 2.25026 11.4538 2.25026 11.625C2.25026 13.3862 3.59948 14.8313 5.32057 14.9854L3.0757 16.3674C2.65801 16.6245 2.10961 16.418 1.96534 15.9492L0.926773 12.5742C0.823558 12.2388 0.967018 11.876 1.27174 11.7019L2.28761 11.1211Z" fill="currentColor"/>
|
||||
<path d="M6.42041 1.5C7.01664 1.5 7.49997 1.98337 7.49997 2.57959V8.81835C6.96373 8.4594 6.31878 8.25 5.62501 8.25C4.91854 8.25 4.26271 8.46705 3.7207 8.83815L5.01124 2.6455C5.15039 1.97822 5.73876 1.50007 6.42041 1.5Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.625 9C7.07475 9 8.25 10.1753 8.25 11.625C8.25 13.0747 7.07475 14.25 5.625 14.25C4.17525 14.25 3 13.0747 3 11.625C3 10.1753 4.17525 9 5.625 9ZM5.625 10.875C5.21078 10.875 4.875 11.2108 4.875 11.625C4.875 12.0392 5.21078 12.375 5.625 12.375C6.03921 12.375 6.375 12.0392 6.375 11.625C6.375 11.2108 6.03921 10.875 5.625 10.875Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,9 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.875 11.8125C7.875 13.1587 6.7837 14.25 5.4375 14.25C4.0913 14.25 3 13.1587 3 11.8125C3 10.4663 4.0913 9.375 5.4375 9.375C6.7837 9.375 7.875 10.4663 7.875 11.8125Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.25 15.75L5.625 14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.5 11.25L3 10.875" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.41699 12.5625C5.83121 12.5625 6.16699 12.2267 6.16699 11.8125C6.16699 11.3983 5.83121 11.0625 5.41699 11.0625C5.00278 11.0625 4.66699 11.3983 4.66699 11.8125C4.66699 12.2267 5.00278 12.5625 5.41699 12.5625Z" fill="currentColor"/>
|
||||
<path d="M13.125 11.25L12.4956 8.10292C12.4255 7.75237 12.1177 7.5 11.7601 7.5H11.625C10.7966 7.5 10.125 8.17155 10.125 9V12.3939C10.125 13.419 10.956 14.25 11.9811 14.25C12.2408 14.25 12.4976 14.3045 12.7349 14.41L15.75 15.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.875 7.5V3.75C10.875 2.92157 11.5466 2.25 12.375 2.25H12.5394C12.8836 2.25 13.1836 2.48422 13.267 2.81811L15.3332 11.0833L16.5 11.625" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.125 9.75V3C7.125 2.58579 6.78921 2.25 6.375 2.25H5.52089C5.14964 2.25 4.83426 2.5216 4.77919 2.88875L3.75 9.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -1,8 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="agent">
|
||||
<g id="Vector">
|
||||
<path d="M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z" fill="white"/>
|
||||
<path d="M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z" fill="white"/>
|
||||
</g>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="agent" transform="translate(2.5 1)">
|
||||
<path d="M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z" fill="currentColor"/>
|
||||
<path d="M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.0 KiB |
@ -1,7 +1,10 @@
|
||||
{
|
||||
"prefix": "custom-public",
|
||||
"lastModified": 1781246368,
|
||||
"lastModified": 1781515983,
|
||||
"icons": {
|
||||
"agent-building-blocks": {
|
||||
"body": "<path fill=\"#155AEF\" fill-rule=\"evenodd\" d=\"M8.303 1.546c.178-.045.364-.051.544-.017c.23.043.432.167.573.246l3.757 2.113c.12.067.29.156.433.289l.06.06c.12.131.21.288.267.457c.07.215.063.445.063.6V9.56c0 .146.007.36-.056.563q-.055.181-.162.338l-.075.1c-.137.163-.32.274-.442.353l-5.013 3.259c-.135.088-.33.224-.556.282a1.3 1.3 0 0 1-.543.017c-.23-.043-.433-.166-.573-.245l-3.757-2.114c-.136-.077-.34-.182-.493-.35a1.3 1.3 0 0 1-.267-.456C1.993 11.09 2 10.86 2 10.704V6.441c0-.146-.007-.36.055-.563l.043-.118a1.3 1.3 0 0 1 .195-.32l.053-.059c.128-.131.282-.225.389-.294L7.86 1.755c.122-.078.273-.165.443-.209m-4.97 9.158l.001.164l.033.02l.11.062l3.264 1.836v-1.137L3.333 9.732zm4.741.917v1.076l4.464-2.901l.098-.065l.029-.02v-.034l.001-.118v-.923zm-4.74-3.419L6.74 10.12V8.982L3.333 7.066zm4.74.752v1.076l4.592-2.985V5.969zm.51-6.08l-4.631 3.01l3.429 1.93l4.664-3.032l-3.28-1.846l-.15-.082z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"avatar-user": {
|
||||
"body": "<g fill=\"none\"><g clip-path=\"url(#svgID0)\"><rect width=\"512\" height=\"512\" fill=\"#B2DDFF\" rx=\"256\"/><circle cx=\"256\" cy=\"196\" r=\"84\" fill=\"#fff\" opacity=\".68\"/><ellipse cx=\"256\" cy=\"583.5\" fill=\"#fff\" opacity=\".68\" rx=\"266\" ry=\"274.5\"/></g><defs><clipPath id=\"svgID0\"><rect width=\"512\" height=\"512\" fill=\"#fff\" rx=\"256\"/></clipPath></defs></g>",
|
||||
"width": 512,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"prefix": "custom-public",
|
||||
"name": "Dify Custom Public",
|
||||
"total": 144,
|
||||
"total": 145,
|
||||
"version": "0.0.0-private",
|
||||
"author": {
|
||||
"name": "LangGenius, Inc.",
|
||||
@ -13,12 +13,12 @@
|
||||
"url": "https://github.com/langgenius/dify/blob/main/LICENSE"
|
||||
},
|
||||
"samples": [
|
||||
"agent-building-blocks",
|
||||
"avatar-user",
|
||||
"billing-ar-cube-1",
|
||||
"billing-asterisk",
|
||||
"billing-aws-marketplace-dark",
|
||||
"billing-aws-marketplace-light",
|
||||
"billing-azure"
|
||||
"billing-aws-marketplace-light"
|
||||
],
|
||||
"palette": false
|
||||
}
|
||||
|
||||
@ -1,7 +1,29 @@
|
||||
{
|
||||
"prefix": "custom-vender",
|
||||
"lastModified": 1781246368,
|
||||
"lastModified": 1781515983,
|
||||
"icons": {
|
||||
"agent-v2-access-point": {
|
||||
"body": "<g fill=\"none\"><path d=\"M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z\" fill=\"currentColor\"/><path d=\"M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z\" fill=\"currentColor\"/><path d=\"M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z\" fill=\"currentColor\"/><path d=\"M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z\" fill=\"currentColor\"/><path d=\"M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z\" fill=\"currentColor\"/><path d=\"M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 15,
|
||||
"height": 15
|
||||
},
|
||||
"agent-v2-end-user-auth": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>"
|
||||
},
|
||||
"agent-v2-plan": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"agent-v2-prompt-insert": {
|
||||
"body": "<g fill=\"none\"><path d=\"M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 14,
|
||||
"height": 14
|
||||
},
|
||||
"agent-v2-robot-3": {
|
||||
"body": "<g fill=\"none\"><path d=\"M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z\" fill=\"currentColor\"/><path d=\"M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 17
|
||||
},
|
||||
"features-citations": {
|
||||
"body": "<g fill=\"none\"><path d=\"M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM7 11.9702V14.958H11.0356V11.2339H8.8125C8.78418 10.8185 8.85498 10.4173 9.0249 10.0303C9.35531 9.29395 10.002 8.77474 10.9648 8.47266V7C9.67155 7.25488 8.68506 7.79297 8.00537 8.61426C7.33512 9.43555 7 10.5542 7 11.9702ZM15.0391 10.0586C15.3695 9.29395 16.0114 8.7653 16.9648 8.47266V7C15.7093 7.25488 14.7323 7.78825 14.0337 8.6001C13.3446 9.41195 13 10.5353 13 11.9702V14.958H17.0356V11.2339H14.8125C14.7747 10.8563 14.8503 10.4645 15.0391 10.0586Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 24,
|
||||
@ -811,6 +833,16 @@
|
||||
"width": 24,
|
||||
"height": 24
|
||||
},
|
||||
"main-nav-roster": {
|
||||
"body": "<g fill=\"none\"><path d=\"M7.875 11.8125C7.875 13.1587 6.7837 14.25 5.4375 14.25C4.0913 14.25 3 13.1587 3 11.8125C3 10.4663 4.0913 9.375 5.4375 9.375C6.7837 9.375 7.875 10.4663 7.875 11.8125Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2.25 15.75L5.625 14.25\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M1.5 11.25L3 10.875\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M5.41699 12.5625C5.83121 12.5625 6.16699 12.2267 6.16699 11.8125C6.16699 11.3983 5.83121 11.0625 5.41699 11.0625C5.00278 11.0625 4.66699 11.3983 4.66699 11.8125C4.66699 12.2267 5.00278 12.5625 5.41699 12.5625Z\" fill=\"currentColor\"/><path d=\"M13.125 11.25L12.4956 8.10292C12.4255 7.75237 12.1177 7.5 11.7601 7.5H11.625C10.7966 7.5 10.125 8.17155 10.125 9V12.3939C10.125 13.419 10.956 14.25 11.9811 14.25C12.2408 14.25 12.4976 14.3045 12.7349 14.41L15.75 15.75\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M10.875 7.5V3.75C10.875 2.92157 11.5466 2.25 12.375 2.25H12.5394C12.8836 2.25 13.1836 2.48422 13.267 2.81811L15.3332 11.0833L16.5 11.625\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M7.125 9.75V3C7.125 2.58579 6.78921 2.25 6.375 2.25H5.52089C5.14964 2.25 4.83426 2.5216 4.77919 2.88875L3.75 9.75\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-roster-active": {
|
||||
"body": "<g fill=\"none\"><path d=\"M11.9362 1.5C12.4177 1.50006 12.8411 1.81975 12.9727 2.28296L15.4651 11.061C15.5117 11.2251 15.6125 11.3687 15.7515 11.4675L16.1235 11.7319C16.3544 11.8963 16.4728 12.1769 16.4297 12.4571L15.9206 15.7669C15.8394 16.2948 15.2475 16.5708 14.7905 16.2942L12.8006 15.0893C12.6837 15.0186 12.5618 14.9546 12.4307 14.9165C12.2411 14.8615 12.0443 14.833 11.8462 14.833C10.6885 14.833 9.75 13.8945 9.75 12.7368V9.14722C9.75 8.23747 10.4875 7.5 11.3972 7.5C11.5688 7.5 11.7164 7.62113 11.7503 7.78928L12.3824 10.9483L12.4043 11.0215C12.4721 11.182 12.6457 11.2781 12.8233 11.2427C13.0009 11.2072 13.1242 11.0515 13.125 10.8772L13.1177 10.8017L12.4856 7.6428C12.3818 7.12384 11.9265 6.75002 11.3972 6.75C11.08 6.75 10.7771 6.81165 10.5 6.92359V2.93628C10.5 2.14312 11.1431 1.5 11.9362 1.5Z\" fill=\"currentColor\"/><path d=\"M2.28761 11.1211C2.263 11.2855 2.25026 11.4538 2.25026 11.625C2.25026 13.3862 3.59948 14.8313 5.32057 14.9854L3.0757 16.3674C2.65801 16.6245 2.10961 16.418 1.96534 15.9492L0.926773 12.5742C0.823558 12.2388 0.967018 11.876 1.27174 11.7019L2.28761 11.1211Z\" fill=\"currentColor\"/><path d=\"M6.42041 1.5C7.01664 1.5 7.49997 1.98337 7.49997 2.57959V8.81835C6.96373 8.4594 6.31878 8.25 5.62501 8.25C4.91854 8.25 4.26271 8.46705 3.7207 8.83815L5.01124 2.6455C5.15039 1.97822 5.73876 1.50007 6.42041 1.5Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M5.625 9C7.07475 9 8.25 10.1753 8.25 11.625C8.25 13.0747 7.07475 14.25 5.625 14.25C4.17525 14.25 3 13.0747 3 11.625C3 10.1753 4.17525 9 5.625 9ZM5.625 10.875C5.21078 10.875 4.875 11.2108 4.875 11.625C4.875 12.0392 5.21078 12.375 5.625 12.375C6.03921 12.375 6.375 12.0392 6.375 11.625C6.375 11.2108 6.03921 10.875 5.625 10.875Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-studio": {
|
||||
"body": "<g fill=\"none\"><path d=\"M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z\" fill=\"currentColor\"/><path d=\"M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
|
||||
"width": 20,
|
||||
@ -1295,9 +1327,9 @@
|
||||
"height": 24
|
||||
},
|
||||
"workflow-agent": {
|
||||
"body": "<g fill=\"none\"><g id=\"agent\"><g id=\"Vector\"><path d=\"M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z\" fill=\"currentColor\"/><path d=\"M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z\" fill=\"currentColor\"/></g></g></g>",
|
||||
"width": 16,
|
||||
"height": 16
|
||||
"body": "<g fill=\"none\"><g id=\"agent\" transform=\"translate(2.5 1)\"><path d=\"M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z\" fill=\"currentColor\"/><path d=\"M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z\" fill=\"currentColor\"/></g></g>",
|
||||
"width": 24,
|
||||
"height": 24
|
||||
},
|
||||
"workflow-answer": {
|
||||
"body": "<g fill=\"none\"><g id=\"icons/answer\"><path id=\"Vector (Stroke)\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M3.50114 1.67701L10.5011 1.677C11.5079 1.677 12.3241 2.49311 12.3241 3.49992V9.35414C12.3241 10.3609 11.5079 11.177 10.5012 11.1771H8.9954L7.41734 12.4845C7.17339 12.6866 6.81987 12.6856 6.57708 12.4821L5.02026 11.1771H3.50114C2.49436 11.1771 1.67822 10.3608 1.67822 9.35414V3.49993C1.67822 2.49316 2.49437 1.67701 3.50114 1.67701ZM10.5011 2.9895L3.50114 2.98951C3.21924 2.98951 2.99072 3.21803 2.99072 3.49993V9.35414C2.99072 9.63601 3.21926 9.86455 3.50114 9.86455H5.04675C5.33794 9.86455 5.61984 9.96705 5.84302 10.1541L7.00112 11.1249L8.17831 10.1496C8.40069 9.96537 8.68041 9.86455 8.96916 9.86455H10.5011C10.5011 9.86455 10.5011 9.86455 10.5011 9.86455C10.783 9.8645 11.0116 9.63592 11.0116 9.35414V3.49992C11.0116 3.21806 10.7831 2.9895 10.5011 2.9895ZM9.06809 4.93171C9.32437 5.18799 9.32437 5.60351 9.06809 5.85979L7.02642 7.90146C6.77014 8.15774 6.35464 8.15774 6.09835 7.90146L5.22333 7.02646C4.96704 6.77019 4.96704 6.35467 5.22332 6.09839C5.4796 5.8421 5.89511 5.8421 6.15139 6.09837L6.56238 6.50935L8.14001 4.93171C8.3963 4.67543 8.81181 4.67543 9.06809 4.93171Z\" fill=\"currentColor\"/></g></g>",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"prefix": "custom-vender",
|
||||
"name": "Dify Custom Vender",
|
||||
"total": 319,
|
||||
"total": 326,
|
||||
"version": "0.0.0-private",
|
||||
"author": {
|
||||
"name": "LangGenius, Inc.",
|
||||
@ -13,12 +13,12 @@
|
||||
"url": "https://github.com/langgenius/dify/blob/main/LICENSE"
|
||||
},
|
||||
"samples": [
|
||||
"features-citations",
|
||||
"features-content-moderation",
|
||||
"features-document",
|
||||
"features-folder-upload",
|
||||
"features-love-message",
|
||||
"features-message-fast"
|
||||
"agent-v2-access-point",
|
||||
"agent-v2-end-user-auth",
|
||||
"agent-v2-plan",
|
||||
"agent-v2-prompt-insert",
|
||||
"agent-v2-robot-3",
|
||||
"features-citations"
|
||||
],
|
||||
"palette": false
|
||||
}
|
||||
|
||||
@ -90,6 +90,9 @@ NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false
|
||||
# /refine slash commands in the "Go to Anything" command palette)
|
||||
NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false
|
||||
|
||||
# Enable Agent v2 frontend entry points.
|
||||
NEXT_PUBLIC_ENABLE_AGENT_V2=false
|
||||
|
||||
# The maximum number of tree node depth for workflow
|
||||
NEXT_PUBLIC_MAX_TREE_DEPTH=50
|
||||
|
||||
|
||||
@ -38,6 +38,15 @@
|
||||
- Do not access `localStorage`, `window.localStorage`, or `globalThis.localStorage` directly in app code; use the storage hook boundary and preserve existing raw/custom storage formats.
|
||||
- Do not add ad hoc global event listeners for shared state. Prefer atoms, existing stores, or a shared subscription hook so listeners are centralized and deduplicated.
|
||||
|
||||
## Agent V2 Frontend
|
||||
|
||||
- Keep Agent V2 separate from legacy workflow Agent. Use `web/features/agent-v2`, `web/app/components/workflow/nodes/agent-v2`, the `agent_node_kind: 'dify_agent'` and `version: '2'` payload discriminator, and `BlockEnum.AgentV2` where the graph type is already migrated. Do not bridge Agent V2 to legacy `agent_strategy_*` behavior or data shapes.
|
||||
- Use generated contracts and `consoleQuery` / `consoleClient` from `@/service/client` for Agent V2 backend calls. Do not add handwritten REST helpers, handwritten API types, mock-backed app state, or direct edits to generated contract files.
|
||||
- Treat TanStack Query as the server source of truth. Scope editable drafts with an instance-level `AgentComposerProvider`, hydrate Jotai `originalDraft`, `publishedDraft`, and `draft` from contract data, and compute dirty or unpublished state from those draft snapshots.
|
||||
- Keep transitional defaults and mock data at the owning surface, such as the configure page or workflow node, not in shared composer defaults.
|
||||
- Use `@langgenius/dify-ui/*` primitives and primitive data/CSS selectors first. Add call-site Tailwind only for real design deltas, avoid arbitrary values when token utilities exist, and keep focus rings visible without making inert layout regions focusable.
|
||||
- Keep Agent V2 copy in the `agentV2` i18n namespace, currently backed by `agent-v-2.json` in the maintained locale set.
|
||||
|
||||
## Automated Test Generation
|
||||
|
||||
- Use `./docs/test.md` as the canonical instruction set for generating frontend automated tests.
|
||||
|
||||
42
web/__tests__/env.spec.ts
Normal file
@ -0,0 +1,42 @@
|
||||
describe('env runtime transport', () => {
|
||||
const originalAgentV2Env = process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
vi.doUnmock('../utils/client')
|
||||
document.body.removeAttribute('data-enable-agent-v2')
|
||||
document.body.removeAttribute('data-enable-agent-v-2')
|
||||
delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (originalAgentV2Env === undefined)
|
||||
delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||
else
|
||||
process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = originalAgentV2Env
|
||||
})
|
||||
|
||||
it('should read NEXT_PUBLIC_ENABLE_AGENT_V2 from the browser runtime dataset key', async () => {
|
||||
document.body.setAttribute('data-enable-agent-v2', 'true')
|
||||
|
||||
const { env } = await import('../env')
|
||||
|
||||
expect(env.NEXT_PUBLIC_ENABLE_AGENT_V2).toBe(true)
|
||||
})
|
||||
|
||||
it('should emit the Agent v2 runtime dataset attribute from getDatasetMap on the server', async () => {
|
||||
process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = 'true'
|
||||
|
||||
vi.doMock('../utils/client', () => ({
|
||||
isClient: false,
|
||||
isServer: true,
|
||||
}))
|
||||
|
||||
const { getDatasetMap } = await import('../env')
|
||||
const datasetMap = getDatasetMap()
|
||||
|
||||
expect(datasetMap['data-enable-agent-v2']).toBe(true)
|
||||
expect(datasetMap['data-enable-agent-v-2']).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@ -7,7 +7,7 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { redirect, usePathname } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
|
||||
const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/deployments', '/snippets', '/explore', '/tools', '/integrations'] as const
|
||||
const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/deployments', '/snippets', '/roster', '/explore', '/tools', '/integrations'] as const
|
||||
|
||||
function isPathUnderRoute(pathname: string, route: string) {
|
||||
return pathname === route || pathname.startsWith(`${route}/`)
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
const mocks = vi.hoisted(() => ({
|
||||
agentV2Enabled: true,
|
||||
notFound: vi.fn(() => {
|
||||
throw new Error('NEXT_NOT_FOUND')
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/env', () => ({
|
||||
env: {
|
||||
get NEXT_PUBLIC_ENABLE_AGENT_V2() {
|
||||
return mocks.agentV2Enabled
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
notFound: () => mocks.notFound(),
|
||||
}))
|
||||
|
||||
describe('guardAgentV2Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.agentV2Enabled = true
|
||||
})
|
||||
|
||||
it('should allow roster routes when Agent v2 is enabled', async () => {
|
||||
const { guardAgentV2Route } = await import('../feature-guard')
|
||||
|
||||
expect(() => guardAgentV2Route()).not.toThrow()
|
||||
expect(mocks.notFound).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should throw notFound when Agent v2 is disabled', async () => {
|
||||
mocks.agentV2Enabled = false
|
||||
|
||||
const { guardAgentV2Route } = await import('../feature-guard')
|
||||
|
||||
expect(() => guardAgentV2Route()).toThrow('NEXT_NOT_FOUND')
|
||||
expect(mocks.notFound).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
43
web/app/(commonLayout)/roster/__tests__/layout.spec.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
guardAgentV2Route: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../feature-guard', () => ({
|
||||
guardAgentV2Route: () => mocks.guardAgentV2Route(),
|
||||
}))
|
||||
|
||||
describe('RosterLayout', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render children when Agent v2 is enabled', async () => {
|
||||
const { default: RosterLayout } = await import('../layout')
|
||||
|
||||
render(
|
||||
<RosterLayout>
|
||||
<div>Roster content</div>
|
||||
</RosterLayout>,
|
||||
)
|
||||
|
||||
expect(mocks.guardAgentV2Route).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByText('Roster content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should block rendering when the roster guard throws notFound', async () => {
|
||||
mocks.guardAgentV2Route.mockImplementation(() => {
|
||||
throw new Error('NEXT_NOT_FOUND')
|
||||
})
|
||||
|
||||
const { default: RosterLayout } = await import('../layout')
|
||||
|
||||
expect(() => render(
|
||||
<RosterLayout>
|
||||
<div>Roster content</div>
|
||||
</RosterLayout>,
|
||||
)).toThrow('NEXT_NOT_FOUND')
|
||||
expect(mocks.guardAgentV2Route).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,13 @@
|
||||
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return <AgentDetailPage agentId={agentId} section="access" />
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return <AgentDetailPage agentId={agentId} section="configure" />
|
||||
}
|
||||
20
web/app/(commonLayout)/roster/agent/[agentId]/layout.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { AgentDetailLayout } from '@/features/agent-v2/agent-detail/layout'
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: LayoutProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return (
|
||||
<AgentDetailLayout agentId={agentId}>
|
||||
{children}
|
||||
</AgentDetailLayout>
|
||||
)
|
||||
}
|
||||
13
web/app/(commonLayout)/roster/agent/[agentId]/logs/page.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return <AgentDetailPage agentId={agentId} section="logs" />
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return <AgentDetailPage agentId={agentId} section="monitoring" />
|
||||
}
|
||||
13
web/app/(commonLayout)/roster/agent/[agentId]/page.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { redirect } from '@/next/navigation'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
redirect(`/roster/agent/${agentId}/configure`)
|
||||
}
|
||||
7
web/app/(commonLayout)/roster/feature-guard.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { env } from '@/env'
|
||||
import { notFound } from '@/next/navigation'
|
||||
|
||||
export const guardAgentV2Route = () => {
|
||||
if (!env.NEXT_PUBLIC_ENABLE_AGENT_V2)
|
||||
notFound()
|
||||
}
|
||||
12
web/app/(commonLayout)/roster/layout.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { guardAgentV2Route } from './feature-guard'
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
guardAgentV2Route()
|
||||
|
||||
return children
|
||||
}
|
||||
5
web/app/(commonLayout)/roster/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import RosterPage from '@/features/agent-v2/roster/page'
|
||||
|
||||
export default function Page() {
|
||||
return <RosterPage />
|
||||
}
|
||||
15
web/app/(shareLayout)/agent/[token]/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history'
|
||||
import AuthenticatedLayout from '../../components/authenticated-layout'
|
||||
|
||||
function Agent() {
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<ChatWithHistoryWrap />
|
||||
</AuthenticatedLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Agent)
|
||||
@ -186,6 +186,20 @@ describe('SettingBuiltInTool', () => {
|
||||
expect(screen.getByTestId('mock-form')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render a masked drawer with balanced vertical offsets', async () => {
|
||||
const { baseElement } = renderComponent()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('mock-form')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(baseElement.querySelector('.bg-background-overlay')).toBeInTheDocument()
|
||||
const drawerPopup = baseElement.querySelector('[role="dialog"]')
|
||||
expect(drawerPopup).toHaveClass(
|
||||
'data-[swipe-direction=right]:top-6',
|
||||
'data-[swipe-direction=right]:bottom-6',
|
||||
)
|
||||
})
|
||||
|
||||
it('should call onSave with updated values when save button clicked', async () => {
|
||||
const { onSave } = renderComponent()
|
||||
await waitFor(() => expect(screen.getByTestId('mock-form')).toBeInTheDocument())
|
||||
|
||||
@ -181,9 +181,9 @@ const SettingBuiltInTool: FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerBackdrop className="bg-background-overlay" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-6 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-6 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
{isLoading && <Loading type="app" />}
|
||||
{!isLoading && (
|
||||
|
||||
@ -19,6 +19,7 @@ import { useInfiniteDatasets } from '@/service/knowledge/use-dataset'
|
||||
|
||||
type ISelectDataSetProps = {
|
||||
isShow: boolean
|
||||
modal?: boolean
|
||||
onClose: () => void
|
||||
selectedIds: string[]
|
||||
onSelect: (dataSet: DataSet[]) => void
|
||||
@ -26,6 +27,7 @@ type ISelectDataSetProps = {
|
||||
|
||||
const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
isShow,
|
||||
modal,
|
||||
onClose,
|
||||
selectedIds,
|
||||
onSelect,
|
||||
@ -90,8 +92,8 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
}, [handleClose])
|
||||
|
||||
return (
|
||||
<Dialog open={isShow} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="w-100 overflow-hidden">
|
||||
<Dialog modal={modal} open={isShow} onOpenChange={handleOpenChange}>
|
||||
<DialogContent backdropProps={{ forceRender: true }} className="w-100 overflow-hidden">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t('feature.dataSet.selectTitle', { ns: 'appDebug' })}
|
||||
</DialogTitle>
|
||||
|
||||
@ -30,6 +30,7 @@ import { RetrievalChangeTip, RetrievalSection } from './retrieval-section'
|
||||
|
||||
type SettingsModalProps = {
|
||||
currentDataset: DataSet
|
||||
height?: string
|
||||
onCancel: () => void
|
||||
onSave: (newDataset: DataSet) => void
|
||||
}
|
||||
@ -44,6 +45,7 @@ const labelClass = `
|
||||
|
||||
const SettingsModal: FC<SettingsModalProps> = ({
|
||||
currentDataset,
|
||||
height = 'calc(100vh - 72px)',
|
||||
onCancel,
|
||||
onSave,
|
||||
}) => {
|
||||
@ -186,9 +188,9 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
|
||||
className="flex min-h-0 w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
|
||||
style={{
|
||||
height: 'calc(100vh - 72px)',
|
||||
height,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
|
||||
@ -8,10 +8,18 @@ import { useHover } from 'ahooks'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { init } from 'emoji-mart'
|
||||
import * as React from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { useRef, useSyncExternalStore } from 'react'
|
||||
|
||||
init({ data })
|
||||
|
||||
const subscribeHydrationState = () => () => {}
|
||||
|
||||
const useIsHydrated = () => useSyncExternalStore(
|
||||
subscribeHydrationState,
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
|
||||
type AppIconProps = {
|
||||
size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl'
|
||||
rounded?: boolean
|
||||
@ -105,9 +113,20 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
}) => {
|
||||
const isValidImageIcon = iconType === 'image' && imageUrl
|
||||
const emojiIcon = (icon && icon !== '') ? icon : '🤖'
|
||||
const Icon = <em-emoji key={emojiIcon} id={emojiIcon} />
|
||||
const isHydrated = useIsHydrated()
|
||||
const Icon = isHydrated ? <em-emoji key={emojiIcon} id={emojiIcon} /> : emojiIcon
|
||||
const wrapperRef = useRef<HTMLSpanElement>(null)
|
||||
const isHovering = useHover(wrapperRef)
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||
if (!onClick)
|
||||
return
|
||||
|
||||
if (event.key !== 'Enter' && event.key !== ' ')
|
||||
return
|
||||
|
||||
event.preventDefault()
|
||||
onClick()
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
@ -115,6 +134,9 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
className={cn(appIconVariants({ size, rounded }), className)}
|
||||
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
|
||||
onClick={onClick}
|
||||
onKeyDown={onClick ? handleKeyDown : undefined}
|
||||
role={onClick ? 'button' : undefined}
|
||||
tabIndex={onClick ? 0 : undefined}
|
||||
>
|
||||
{
|
||||
isValidImageIcon
|
||||
|
||||
@ -57,6 +57,7 @@ const renderPanel = (props: Partial<{
|
||||
onClose: () => void
|
||||
inWorkflow: boolean
|
||||
showFileUpload: boolean
|
||||
showAnnotationReply: boolean
|
||||
}> = {}) => {
|
||||
return render(
|
||||
<FeaturesProvider features={defaultFeatures}>
|
||||
@ -68,6 +69,7 @@ const renderPanel = (props: Partial<{
|
||||
onClose={props.onClose ?? vi.fn()}
|
||||
inWorkflow={props.inWorkflow}
|
||||
showFileUpload={props.showFileUpload}
|
||||
showAnnotationReply={props.showAnnotationReply}
|
||||
/>
|
||||
</FeaturesProvider>,
|
||||
)
|
||||
@ -191,5 +193,11 @@ describe('NewFeaturePanel', () => {
|
||||
|
||||
expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render AnnotationReply when showAnnotationReply is false', () => {
|
||||
renderPanel({ isChatMode: true, inWorkflow: false, showAnnotationReply: false })
|
||||
|
||||
expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
@ -26,9 +27,14 @@ type Props = Readonly<{
|
||||
onClose: () => void
|
||||
inWorkflow?: boolean
|
||||
showFileUpload?: boolean
|
||||
showModeration?: boolean
|
||||
showAnnotationReply?: boolean
|
||||
promptVariables?: PromptVariable[]
|
||||
workflowVariables?: InputVar[]
|
||||
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
|
||||
title?: ReactNode
|
||||
description?: ReactNode
|
||||
drawerClassName?: string
|
||||
}>
|
||||
|
||||
const NewFeaturePanel = ({
|
||||
@ -39,9 +45,14 @@ const NewFeaturePanel = ({
|
||||
onClose,
|
||||
inWorkflow = true,
|
||||
showFileUpload = true,
|
||||
showModeration = true,
|
||||
showAnnotationReply = true,
|
||||
promptVariables,
|
||||
workflowVariables,
|
||||
onAutoAddPromptVariable,
|
||||
title,
|
||||
description,
|
||||
drawerClassName,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
|
||||
@ -52,13 +63,14 @@ const NewFeaturePanel = ({
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
inWorkflow={inWorkflow}
|
||||
className={drawerClassName}
|
||||
>
|
||||
<div className="flex h-full grow flex-col">
|
||||
{/* header */}
|
||||
<div className="flex shrink-0 justify-between p-4 pb-3">
|
||||
<div>
|
||||
<div className="system-xl-semibold text-text-primary">{t('common.features', { ns: 'workflow' })}</div>
|
||||
<div className="body-xs-regular text-text-tertiary">{t('common.featuresDescription', { ns: 'workflow' })}</div>
|
||||
<div className="system-xl-semibold text-text-primary">{title ?? t('common.features', { ns: 'workflow' })}</div>
|
||||
<div className="body-xs-regular text-text-tertiary">{description ?? t('common.featuresDescription', { ns: 'workflow' })}</div>
|
||||
</div>
|
||||
<DrawerCloseButton
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
@ -93,8 +105,8 @@ const NewFeaturePanel = ({
|
||||
{isChatMode && (
|
||||
<Citation disabled={disabled} onChange={onChange} />
|
||||
)}
|
||||
{(isChatMode || !inWorkflow) && <Moderation disabled={disabled} onChange={onChange} />}
|
||||
{!inWorkflow && isChatMode && (
|
||||
{showModeration && (isChatMode || !inWorkflow) && <Moderation disabled={disabled} onChange={onChange} />}
|
||||
{showAnnotationReply && !inWorkflow && isChatMode && (
|
||||
<AnnotationReply disabled={disabled} onChange={onChange} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -302,6 +302,37 @@ describe('ModerationSettingModal', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('should save the latest preset response when content textarea changes', async () => {
|
||||
const data: ModerationConfig = {
|
||||
...defaultData,
|
||||
config: {
|
||||
keywords: 'bad',
|
||||
inputs_config: { enabled: true, preset_response: 'blocked' },
|
||||
outputs_config: { enabled: false, preset_response: '' },
|
||||
},
|
||||
}
|
||||
await renderModal(
|
||||
<ModerationSettingModal
|
||||
data={data}
|
||||
onCancel={vi.fn()}
|
||||
onSave={onSave}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox', { name: /feature\.moderation\.modal\.content\.preset/ }), {
|
||||
target: { value: 'updated blocked response' },
|
||||
})
|
||||
fireEvent.click(screen.getByText(/operation\.save/))
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
inputs_config: expect.objectContaining({
|
||||
preset_response: 'updated blocked response',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('should show api selector when api type is selected', async () => {
|
||||
await renderModal(
|
||||
<ModerationSettingModal
|
||||
@ -702,7 +733,10 @@ describe('ModerationSettingModal', () => {
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalled()
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' })
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'provider',
|
||||
onCancelCallback: expect.any(Function),
|
||||
})
|
||||
})
|
||||
|
||||
it('should not save when OpenAI type is selected but not configured', async () => {
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from 'react'
|
||||
import type { ModerationContentConfig } from '@/models/debug'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ModerationContentProps = {
|
||||
@ -19,57 +20,71 @@ const ModerationContent: FC<ModerationContentProps> = ({
|
||||
onConfigChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [presetResponse, setPresetResponse] = useState(config.preset_response || '')
|
||||
|
||||
const handleConfigChange = (field: string, value: boolean | string) => {
|
||||
if (field === 'preset_response' && typeof value === 'string')
|
||||
value = value.slice(0, 100)
|
||||
onConfigChange({ ...config, [field]: value })
|
||||
|
||||
onConfigChange({
|
||||
...config,
|
||||
preset_response: field === 'preset_response' ? value as string : presetResponse,
|
||||
[field]: value,
|
||||
})
|
||||
}
|
||||
|
||||
const handlePresetResponseChange = (value: string) => {
|
||||
const nextValue = value.slice(0, 100)
|
||||
setPresetResponse(nextValue)
|
||||
handleConfigChange('preset_response', nextValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="rounded-lg border border-components-panel-border bg-components-panel-bg">
|
||||
<div className="flex h-10 items-center justify-between rounded-lg px-3">
|
||||
<div className="shrink-0 text-sm font-medium text-text-primary">{title}</div>
|
||||
<div className="flex grow items-center justify-end">
|
||||
{
|
||||
info && (
|
||||
<div className="mr-2 truncate text-xs text-text-tertiary" title={info}>{info}</div>
|
||||
)
|
||||
}
|
||||
<Switch
|
||||
size="lg"
|
||||
checked={config.enabled}
|
||||
onCheckedChange={v => handleConfigChange('enabled', v)}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-xs">
|
||||
<div className="flex min-h-10 items-center gap-2 px-3 py-2">
|
||||
<div className="min-w-0 flex-1 system-sm-medium text-text-secondary">{title}</div>
|
||||
<div className="flex min-w-0 shrink-0 items-center justify-end">
|
||||
{
|
||||
info && (
|
||||
<div className="mr-2 truncate system-xs-regular text-text-tertiary" title={info}>{info}</div>
|
||||
)
|
||||
}
|
||||
<Switch
|
||||
checked={config.enabled}
|
||||
onCheckedChange={v => handleConfigChange('enabled', v)}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
config.enabled && showPreset && (
|
||||
<div className="rounded-lg bg-components-panel-bg px-3 pt-1 pb-3">
|
||||
<div className="flex h-8 items-center justify-between text-[13px] font-medium text-text-secondary">
|
||||
</div>
|
||||
{
|
||||
config.enabled && showPreset && (
|
||||
<div className="px-3 pt-0.5 pb-3">
|
||||
<div className="flex h-8 items-center justify-between gap-2">
|
||||
<span className="system-2xs-medium-uppercase text-text-secondary">
|
||||
{t('feature.moderation.modal.content.preset', { ns: 'appDebug' })}
|
||||
<span className="text-xs font-normal text-text-tertiary">{t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })}</span>
|
||||
</div>
|
||||
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
|
||||
<div className="relative h-20">
|
||||
<Textarea
|
||||
aria-label={t('feature.moderation.modal.content.preset', { ns: 'appDebug' }) as string}
|
||||
value={config.preset_response || ''}
|
||||
className="size-full resize-none pb-8"
|
||||
placeholder={t('feature.moderation.modal.content.placeholder', { ns: 'appDebug' }) || ''}
|
||||
onValueChange={value => handleConfigChange('preset_response', value)}
|
||||
/>
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
|
||||
<span>{(config.preset_response || '').length}</span>
|
||||
/
|
||||
<span className="text-text-tertiary">100</span>
|
||||
</div>
|
||||
</span>
|
||||
<span className="flex shrink-0 items-center gap-0.5 rounded bg-background-section px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
<span className="i-ri-markdown-line size-3" aria-hidden />
|
||||
{t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })}
|
||||
</span>
|
||||
</div>
|
||||
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
|
||||
<div className="relative h-20">
|
||||
<Textarea
|
||||
aria-label={t('feature.moderation.modal.content.preset', { ns: 'appDebug' }) as string}
|
||||
value={presetResponse}
|
||||
className="size-full resize-none pb-8"
|
||||
placeholder={t('feature.moderation.modal.content.placeholder', { ns: 'appDebug' }) || ''}
|
||||
onValueChange={handlePresetResponseChange}
|
||||
/>
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 system-2xs-medium-uppercase text-text-quaternary">
|
||||
<span>{presetResponse.length}</span>
|
||||
/
|
||||
<span className="text-text-tertiary">100</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { CodeBasedExtensionItem } from '@/models/common'
|
||||
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
@ -6,7 +6,7 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { ApiBasedExtensionSelector } from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
@ -27,6 +27,27 @@ type Provider = {
|
||||
form_schema?: CodeBasedExtensionItem['form_schema']
|
||||
}
|
||||
|
||||
function ProviderIcon({ type }: { type: string }) {
|
||||
if (type === 'openai_moderation')
|
||||
return <span className="i-ri-openai-fill size-4 text-text-secondary" aria-hidden />
|
||||
|
||||
if (type === 'keywords')
|
||||
return <span className="i-ri-search-line size-4 text-util-colors-green-green-600" aria-hidden />
|
||||
|
||||
return <span className="i-ri-image-line size-4 text-util-colors-violet-violet-600" aria-hidden />
|
||||
}
|
||||
|
||||
function LabeledDivider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<span className="shrink-0 system-xs-medium-uppercase text-text-tertiary">
|
||||
{children}
|
||||
</span>
|
||||
<Divider bgStyle="gradient" className="my-0 h-px flex-1" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ModerationSettingModalProps = {
|
||||
data: ModerationConfig
|
||||
onCancel: () => void
|
||||
@ -41,12 +62,27 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const locale = useLocale()
|
||||
const { data: modelProviders, isPending: isLoading } = useModelProviders()
|
||||
const { data: modelProviders, isPending: isLoading, refetch: refetchModelProviders } = useModelProviders()
|
||||
const localeDataRef = useRef<ModerationConfig>(data)
|
||||
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const updateLocaleData = useCallback((
|
||||
update: ModerationConfig | ((current: ModerationConfig) => ModerationConfig),
|
||||
options: { render?: boolean } = {},
|
||||
) => {
|
||||
const nextLocaleData = typeof update === 'function'
|
||||
? update(localeDataRef.current)
|
||||
: update
|
||||
|
||||
localeDataRef.current = nextLocaleData
|
||||
|
||||
if (options.render !== false)
|
||||
setLocaleData(nextLocaleData)
|
||||
}, [])
|
||||
const handleOpenSettingsModal = () => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
onCancelCallback: refetchModelProviders,
|
||||
})
|
||||
}
|
||||
const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation')
|
||||
@ -85,20 +121,20 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
const currentProvider = providers.find(provider => provider.key === localeData.type)
|
||||
|
||||
const handleDataTypeChange = (type: string) => {
|
||||
let config: undefined | Record<string, any>
|
||||
let config: undefined | Record<string, string>
|
||||
const currProvider = providers.find(provider => provider.key === type)
|
||||
|
||||
if (systemTypes.findIndex(t => t === type) < 0 && currProvider?.form_schema) {
|
||||
config = currProvider?.form_schema.reduce((prev, next) => {
|
||||
prev[next.variable] = next.default
|
||||
return prev
|
||||
}, {} as Record<string, any>)
|
||||
}, {} as Record<string, string>)
|
||||
}
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
type,
|
||||
config,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDataKeywordsChange = (value: string) => {
|
||||
@ -111,43 +147,46 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
return prev
|
||||
}, [])
|
||||
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
config: {
|
||||
...localeData.config,
|
||||
...current.config,
|
||||
keywords: arr.slice(0, 100).join('\n'),
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDataContentChange = (contentType: string, contentConfig: ModerationContentConfig) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
const previousContentConfig = localeDataRef.current.config?.[contentType] as ModerationContentConfig | undefined
|
||||
const shouldRender = previousContentConfig?.enabled !== contentConfig.enabled
|
||||
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
config: {
|
||||
...localeData.config,
|
||||
...current.config,
|
||||
[contentType]: contentConfig,
|
||||
},
|
||||
})
|
||||
}), { render: shouldRender })
|
||||
}
|
||||
|
||||
const handleDataApiBasedChange = (apiBasedExtensionId: string) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
config: {
|
||||
...localeData.config,
|
||||
...current.config,
|
||||
api_based_extension_id: apiBasedExtensionId,
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDataExtraChange = (extraValue: Record<string, string>) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
config: {
|
||||
...localeData.config,
|
||||
...current.config,
|
||||
...extraValue,
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const formatData = (originData: ModerationConfig) => {
|
||||
@ -179,115 +218,116 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const currentLocaleData = localeDataRef.current
|
||||
const providerForSave = providers.find(provider => provider.key === currentLocaleData.type)
|
||||
|
||||
/* v8 ignore next -- UI-invariant guard: same condition is used in Save button disabled logic, so when true handleSave has no user-triggerable invocation path. @preserve */
|
||||
if (localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
|
||||
if (currentLocaleData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
|
||||
return
|
||||
|
||||
if (!localeData.config?.inputs_config?.enabled && !localeData.config?.outputs_config?.enabled) {
|
||||
if (!currentLocaleData.config?.inputs_config?.enabled && !currentLocaleData.config?.outputs_config?.enabled) {
|
||||
toast.error(t('feature.moderation.modal.content.condition', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'keywords' && !localeData.config.keywords) {
|
||||
if (currentLocaleData.type === 'keywords' && !currentLocaleData.config.keywords) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'keywords' : '关键词' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
|
||||
if (currentLocaleData.type === 'api' && !currentLocaleData.config.api_based_extension_id) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
|
||||
for (let i = 0; i < currentProvider.form_schema.length; i++) {
|
||||
if (!localeData.config?.[currentProvider.form_schema[i]!.variable] && currentProvider.form_schema[i]!.required) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i]!.label['en-US'] : currentProvider.form_schema[i]!.label['zh-Hans'] }))
|
||||
if (systemTypes.findIndex(t => t === currentLocaleData.type) < 0 && providerForSave?.form_schema) {
|
||||
for (let i = 0; i < providerForSave.form_schema.length; i++) {
|
||||
if (!currentLocaleData.config?.[providerForSave.form_schema[i]!.variable] && providerForSave.form_schema[i]!.required) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? providerForSave.form_schema[i]!.label['en-US'] : providerForSave.form_schema[i]!.label['zh-Hans'] }))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (localeData.config.inputs_config?.enabled && !localeData.config.inputs_config.preset_response && localeData.type !== 'api') {
|
||||
if (currentLocaleData.config.inputs_config?.enabled && !currentLocaleData.config.inputs_config.preset_response && currentLocaleData.type !== 'api') {
|
||||
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.config.outputs_config?.enabled && !localeData.config.outputs_config.preset_response && localeData.type !== 'api') {
|
||||
if (currentLocaleData.config.outputs_config?.enabled && !currentLocaleData.config.outputs_config.preset_response && currentLocaleData.type !== 'api') {
|
||||
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
onSave(formatData(localeData))
|
||||
onSave(formatData(currentLocaleData))
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open>
|
||||
<DialogContent className="mt-14! w-[600px]! max-w-none! border-none p-6! text-left align-middle">
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t('feature.moderation.modal.title', { ns: 'appDebug' })}</div>
|
||||
<DialogContent className="mt-14! w-[600px]! max-w-none! overflow-hidden border-[0.5px]! border-components-panel-border! p-0! text-left align-middle">
|
||||
<div className="flex items-start gap-2 px-6 pt-6 pr-14 pb-3">
|
||||
<div className="title-2xl-semi-bold text-text-primary">
|
||||
{t('feature.moderation.modal.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
className="absolute top-5 right-5 flex size-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<span className="i-ri-close-line size-4 text-text-tertiary" aria-hidden="true" />
|
||||
<span className="i-ri-close-line size-[18px]" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="text-sm/9 font-medium text-text-primary">
|
||||
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
{
|
||||
providers.map(provider => (
|
||||
<div
|
||||
<div className="flex flex-col gap-4 px-6 py-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="system-sm-medium text-text-secondary">
|
||||
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{providers.map(provider => (
|
||||
<button
|
||||
type="button"
|
||||
key={provider.key}
|
||||
className={cn(
|
||||
'flex h-8 cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 system-sm-regular text-text-secondary',
|
||||
localeData.type !== provider.key && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg system-sm-medium shadow-xs',
|
||||
'flex min-h-[68px] flex-col items-start justify-center gap-1.5 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg px-3 py-2 text-left text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
localeData.type !== provider.key && 'hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
|
||||
localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled',
|
||||
)}
|
||||
onClick={() => handleDataTypeChange(provider.key)}
|
||||
>
|
||||
<div className={cn(
|
||||
'mr-2 size-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs',
|
||||
localeData.type === provider.key && 'border-[5px] border-components-radio-border-checked',
|
||||
)}
|
||||
>
|
||||
<div className="flex size-8 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-background-default-dodge">
|
||||
<ProviderIcon type={provider.key} />
|
||||
</div>
|
||||
{provider.name}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
|
||||
<span className="w-full truncate system-xs-regular">
|
||||
{provider.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
|
||||
<div className="mt-2 flex items-center rounded-lg border border-[#FEF0C7] bg-[#FFFAEB] px-3 py-2">
|
||||
<span className="mr-1 i-custom-vender-line-general-info-circle h-4 w-4 text-[#F79009]" />
|
||||
<div className="flex items-center text-xs font-medium text-gray-700">
|
||||
{t('feature.moderation.modal.openaiNotConfig.before', { ns: 'appDebug' })}
|
||||
<span
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-primary-600"
|
||||
onClick={handleOpenSettingsModal}
|
||||
>
|
||||
|
||||
|
||||
{t('settings.provider', { ns: 'common' })}
|
||||
|
||||
</span>
|
||||
|
||||
</button>
|
||||
{t('feature.moderation.modal.openaiNotConfig.after', { ns: 'appDebug' })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
localeData.type === 'keywords' && (
|
||||
<div className="py-2">
|
||||
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
|
||||
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
|
||||
)}
|
||||
</div>
|
||||
{localeData.type === 'keywords' && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="system-sm-medium text-text-secondary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
|
||||
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
|
||||
<div className="relative h-[88px]">
|
||||
<Textarea
|
||||
@ -297,7 +337,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
className="size-full resize-none pb-8"
|
||||
placeholder={t('feature.moderation.modal.keywords.placeholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 system-2xs-medium-uppercase text-text-quaternary">
|
||||
<span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>
|
||||
/
|
||||
<span className="text-text-tertiary">
|
||||
@ -307,18 +347,16 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
localeData.type === 'api' && (
|
||||
<div className="py-2">
|
||||
<div className="flex h-9 items-center justify-between">
|
||||
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
|
||||
)}
|
||||
{localeData.type === 'api' && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex h-6 items-center justify-between">
|
||||
<div className="system-sm-medium text-text-secondary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
|
||||
<a
|
||||
href={docLink('/use-dify/workspace/api-extension/api-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center text-xs text-text-tertiary hover:text-primary-600"
|
||||
className="group flex items-center system-xs-regular text-text-tertiary hover:text-primary-600"
|
||||
>
|
||||
<span className="mr-1 i-custom-vender-line-education-book-open-01 size-3 text-text-tertiary group-hover:text-primary-600" />
|
||||
{t('apiBasedExtension.link', { ns: 'common' })}
|
||||
@ -329,46 +367,51 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
onChange={handleDataApiBasedChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
&& currentProvider?.form_schema
|
||||
&& (
|
||||
<FormGeneration
|
||||
forms={currentProvider?.form_schema}
|
||||
value={localeData.config}
|
||||
onChange={handleDataExtraChange}
|
||||
)}
|
||||
{systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
&& currentProvider?.form_schema
|
||||
&& (
|
||||
<FormGeneration
|
||||
forms={currentProvider?.form_schema}
|
||||
value={localeData.config}
|
||||
onChange={handleDataExtraChange}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<LabeledDivider>{t('feature.moderation.title', { ns: 'appDebug' })}</LabeledDivider>
|
||||
<ModerationContent
|
||||
key={`inputs-${localeData.type}-${localeData.config?.inputs_config?.preset_response ?? ''}`}
|
||||
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('inputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Divider bgStyle="gradient" className="my-3 h-px" />
|
||||
<ModerationContent
|
||||
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('inputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
<ModerationContent
|
||||
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('outputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
<div className="mt-1 mb-8 text-xs font-medium text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<ModerationContent
|
||||
key={`outputs-${localeData.type}-${localeData.config?.outputs_config?.preset_response ?? ''}`}
|
||||
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('outputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
<div className="py-0.5 system-xs-regular text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[76px] items-center justify-end gap-2 px-6 pt-5 pb-6">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
className="mr-2"
|
||||
size="medium"
|
||||
className="min-w-[72px]"
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
onClick={handleSave}
|
||||
disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}
|
||||
className="min-w-[72px]"
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
@ -15,35 +15,27 @@
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "agent"
|
||||
"id": "agent",
|
||||
"transform": "translate(2.5 1)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector"
|
||||
"d": "M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -9,6 +9,13 @@ import {
|
||||
UPDATE_HISTORY_EVENT_EMITTER,
|
||||
} from '../constants'
|
||||
import PromptEditor from '../index'
|
||||
import { CustomTextNode } from '../plugins/custom-text/node'
|
||||
|
||||
type MockNodeReplacementConfig = {
|
||||
replace?: unknown
|
||||
with?: (arg: { __text: string }) => void
|
||||
withKlass?: unknown
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const commandHandlers = new Map<unknown, (payload: unknown) => boolean>()
|
||||
@ -18,6 +25,7 @@ const mocks = vi.hoisted(() => {
|
||||
return {
|
||||
emit: vi.fn(),
|
||||
rootLines: ['first line', 'second line'],
|
||||
nodeReplacementConfig: undefined as MockNodeReplacementConfig | undefined,
|
||||
commandHandlers,
|
||||
subscriptions,
|
||||
rootElement,
|
||||
@ -86,7 +94,7 @@ vi.mock('@lexical/react/LexicalComposer', () => ({
|
||||
LexicalComposer: ({ initialConfig, children }: {
|
||||
initialConfig: {
|
||||
onError?: (error: Error) => void
|
||||
nodes?: Array<{ replace?: unknown, with: (arg: { __text: string }) => void }>
|
||||
nodes?: unknown[]
|
||||
}
|
||||
children: ReactNode
|
||||
}) => {
|
||||
@ -99,9 +107,11 @@ vi.mock('@lexical/react/LexicalComposer', () => ({
|
||||
}
|
||||
}
|
||||
if (initialConfig?.nodes) {
|
||||
const textNodeConf = initialConfig.nodes.find((n: { replace?: unknown, with: (arg: { __text: string }) => void }) => n?.replace)
|
||||
if (textNodeConf)
|
||||
textNodeConf.with({ __text: 'test' })
|
||||
const textNodeConf = initialConfig.nodes.find((node): node is MockNodeReplacementConfig => {
|
||||
return typeof node === 'object' && node !== null && 'replace' in node
|
||||
})
|
||||
mocks.nodeReplacementConfig = textNodeConf
|
||||
textNodeConf?.with?.({ __text: 'test' })
|
||||
}
|
||||
return <div data-testid="lexical-composer">{children}</div>
|
||||
},
|
||||
@ -173,10 +183,17 @@ describe('PromptEditor', () => {
|
||||
mocks.commandHandlers.clear()
|
||||
mocks.subscriptions.length = 0
|
||||
mocks.rootLines = ['first line', 'second line']
|
||||
mocks.nodeReplacementConfig = undefined
|
||||
})
|
||||
|
||||
// Rendering shell and text output from lexical state.
|
||||
describe('Rendering', () => {
|
||||
it('should register CustomTextNode as the TextNode replacement class', () => {
|
||||
render(<PromptEditor />)
|
||||
|
||||
expect(mocks.nodeReplacementConfig?.withKlass).toBe(CustomTextNode)
|
||||
})
|
||||
|
||||
it('should render placeholder and call onChange with joined lexical text', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import {
|
||||
$getRoot,
|
||||
BLUR_COMMAND,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
@ -17,6 +18,7 @@ import { useEffect } from 'react'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
import { AgentOutputBlockNode } from '../plugins/agent-output-block/node'
|
||||
import { ContextBlockNode } from '../plugins/context-block'
|
||||
import { CurrentBlockNode } from '../plugins/current-block'
|
||||
import { CustomTextNode } from '../plugins/custom-text/node'
|
||||
@ -26,6 +28,7 @@ import { HITLInputNode } from '../plugins/hitl-input-block'
|
||||
import { LastRunBlockNode } from '../plugins/last-run-block'
|
||||
import { QueryBlockNode } from '../plugins/query-block'
|
||||
import { RequestURLBlockNode } from '../plugins/request-url-block'
|
||||
import { RosterReferenceBlockNode } from '../plugins/roster-reference-block/node'
|
||||
import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '../plugins/update-block'
|
||||
import { VariableValueBlockNode } from '../plugins/variable-value-block/node'
|
||||
import { WorkflowVariableBlockNode } from '../plugins/workflow-variable-block'
|
||||
@ -108,10 +111,12 @@ const PromptEditorContentHarness = ({
|
||||
RequestURLBlockNode,
|
||||
WorkflowVariableBlockNode,
|
||||
VariableValueBlockNode,
|
||||
RosterReferenceBlockNode,
|
||||
HITLInputNode,
|
||||
CurrentBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
LastRunBlockNode,
|
||||
AgentOutputBlockNode,
|
||||
],
|
||||
editorState: textToEditorState(initialText),
|
||||
onError: (error: Error) => {
|
||||
@ -201,6 +206,117 @@ describe('PromptEditorContent', () => {
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render adjacent agent output tokens as inline output blocks', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
const initialText = '[§output:ccc:ccc§][§output:output_3ggg:output_3ggg§][§output:ggg:ggg§][§output:output_3fdfdf:output_3fdfdf§]'
|
||||
|
||||
const { container } = render(
|
||||
<PromptEditorContentHarness
|
||||
captures={captures}
|
||||
initialText={initialText}
|
||||
shortcutPopups={[]}
|
||||
floatingAnchorElem={document.createElement('div')}
|
||||
onEditorChange={vi.fn()}
|
||||
agentOutputBlock={{
|
||||
show: true,
|
||||
outputs: [
|
||||
{ name: 'ccc', type: 'string' },
|
||||
{ name: 'output_3ggg', type: 'string' },
|
||||
{ name: 'ggg', type: 'string' },
|
||||
{ name: 'output_3fdfdf', type: 'string' },
|
||||
],
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('ccc')).toBeInTheDocument()
|
||||
expect(screen.getByText('output_3ggg')).toBeInTheDocument()
|
||||
expect(screen.getByText('ggg')).toBeInTheDocument()
|
||||
expect(screen.getByText('output_3fdfdf')).toBeInTheDocument()
|
||||
})
|
||||
expect(container).not.toHaveTextContent('[§output:ccc:ccc§]')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(captures.editor).not.toBeNull()
|
||||
})
|
||||
captures.editor!.getEditorState().read(() => {
|
||||
expect($getRoot().getTextContent()).toBe(initialText)
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer file type when rendering a file-name output token', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
|
||||
render(
|
||||
<PromptEditorContentHarness
|
||||
captures={captures}
|
||||
initialText="[§output:qna_report.pdf:qna_report.pdf§]"
|
||||
shortcutPopups={[]}
|
||||
floatingAnchorElem={document.createElement('div')}
|
||||
onEditorChange={vi.fn()}
|
||||
agentOutputBlock={{
|
||||
show: true,
|
||||
outputs: [
|
||||
{ name: 'qna_report.pdf', type: 'string' },
|
||||
],
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('qna_report.pdf')).toBeInTheDocument()
|
||||
expect(screen.getByText('file')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update rendered output block type when declared output config changes', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
const outputBlock = {
|
||||
show: true,
|
||||
outputs: [
|
||||
{ name: 'summary', type: 'string' as const },
|
||||
],
|
||||
}
|
||||
|
||||
const { rerender } = render(
|
||||
<PromptEditorContentHarness
|
||||
captures={captures}
|
||||
initialText="[§output:summary:summary§]"
|
||||
shortcutPopups={[]}
|
||||
floatingAnchorElem={document.createElement('div')}
|
||||
onEditorChange={vi.fn()}
|
||||
agentOutputBlock={outputBlock}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('summary')).toBeInTheDocument()
|
||||
expect(screen.getByText('string')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
rerender(
|
||||
<PromptEditorContentHarness
|
||||
captures={captures}
|
||||
initialText="[§output:summary:summary§]"
|
||||
shortcutPopups={[]}
|
||||
floatingAnchorElem={document.createElement('div')}
|
||||
onEditorChange={vi.fn()}
|
||||
agentOutputBlock={{
|
||||
show: true,
|
||||
outputs: [
|
||||
{ name: 'summary', type: 'file' },
|
||||
],
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('file')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.queryByText('string')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render optional blocks and open shortcut popups with the real editor runtime', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
const onEditorChange = vi.fn()
|
||||
@ -291,5 +407,29 @@ describe('PromptEditorContent', () => {
|
||||
expect(screen.queryByTestId('draggable-target-line')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('common.promptEditor.placeholder')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render roster references as inline token pills when enabled', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
|
||||
const { container } = render(
|
||||
<PromptEditorContentHarness
|
||||
captures={captures}
|
||||
shortcutPopups={[]}
|
||||
initialText="Use [§file:file-1:qna_report.pdf§]"
|
||||
floatingAnchorElem={null}
|
||||
onEditorChange={vi.fn()}
|
||||
rosterReferenceBlock={{ show: true }}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(captures.editor).not.toBeNull()
|
||||
})
|
||||
|
||||
const token = container.querySelector('[data-roster-reference-kind="file"]') as HTMLElement
|
||||
expect(token).toBeInTheDocument()
|
||||
expect(token).toHaveTextContent('qna_report.pdf')
|
||||
expect(token.querySelector('.i-ri-file-pdf-2-fill')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
import type { FC } from 'react'
|
||||
import type { Hotkey, ShortcutPopupDisplayMode, ShortcutPopupInsertHandler } from './plugins/shortcuts-popup-plugin'
|
||||
import type {
|
||||
AgentOutputBlockType,
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
@ -16,6 +17,7 @@ import type {
|
||||
LastRunBlockType,
|
||||
QueryBlockType,
|
||||
RequestURLBlockType,
|
||||
RosterReferenceBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
} from './types'
|
||||
@ -34,6 +36,7 @@ import {
|
||||
UPDATE_DATASETS_EVENT_EMITTER,
|
||||
UPDATE_HISTORY_EVENT_EMITTER,
|
||||
} from './constants'
|
||||
import { AgentOutputBlockNode } from './plugins/agent-output-block/node'
|
||||
import {
|
||||
ContextBlockNode,
|
||||
} from './plugins/context-block'
|
||||
@ -44,10 +47,10 @@ import { CustomTextNode } from './plugins/custom-text/node'
|
||||
import {
|
||||
ErrorMessageBlockNode,
|
||||
} from './plugins/error-message-block'
|
||||
|
||||
import {
|
||||
HistoryBlockNode,
|
||||
} from './plugins/history-block'
|
||||
|
||||
import {
|
||||
HITLInputNode,
|
||||
} from './plugins/hitl-input-block'
|
||||
@ -60,6 +63,7 @@ import {
|
||||
import {
|
||||
RequestURLBlockNode,
|
||||
} from './plugins/request-url-block'
|
||||
import { RosterReferenceBlockNode } from './plugins/roster-reference-block/node'
|
||||
import { VariableValueBlockNode } from './plugins/variable-value-block/node'
|
||||
import {
|
||||
WorkflowVariableBlockNode,
|
||||
@ -108,6 +112,7 @@ const EditableSyncPlugin: FC<{ editable: boolean }> = ({ editable }) => {
|
||||
|
||||
export type PromptEditorProps = {
|
||||
instanceId?: string
|
||||
children?: React.ReactNode
|
||||
compact?: boolean
|
||||
wrapperClassName?: string
|
||||
className?: string
|
||||
@ -124,13 +129,17 @@ export type PromptEditorProps = {
|
||||
requestURLBlock?: RequestURLBlockType
|
||||
historyBlock?: HistoryBlockType
|
||||
variableBlock?: VariableBlockType
|
||||
rosterReferenceBlock?: RosterReferenceBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
agentOutputBlock?: AgentOutputBlockType
|
||||
hitlInputBlock?: HITLInputBlockType
|
||||
currentBlock?: CurrentBlockType
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
isSupportFileVar?: boolean
|
||||
disableSlashPicker?: boolean
|
||||
disableBracePicker?: boolean
|
||||
shortcutPopups?: Array<{
|
||||
hotkey: Hotkey
|
||||
displayMode?: ShortcutPopupDisplayMode
|
||||
@ -140,6 +149,7 @@ export type PromptEditorProps = {
|
||||
|
||||
const PromptEditor: FC<PromptEditorProps> = ({
|
||||
instanceId,
|
||||
children,
|
||||
compact,
|
||||
wrapperClassName,
|
||||
className,
|
||||
@ -156,13 +166,17 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
requestURLBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
rosterReferenceBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
agentOutputBlock,
|
||||
hitlInputBlock,
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
isSupportFileVar,
|
||||
disableSlashPicker = false,
|
||||
disableBracePicker = false,
|
||||
shortcutPopups = [],
|
||||
}) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@ -177,6 +191,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
{
|
||||
replace: TextNode,
|
||||
with: (node: TextNode) => new CustomTextNode(node.__text),
|
||||
withKlass: CustomTextNode,
|
||||
},
|
||||
ContextBlockNode,
|
||||
HistoryBlockNode,
|
||||
@ -184,10 +199,12 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
RequestURLBlockNode,
|
||||
WorkflowVariableBlockNode,
|
||||
VariableValueBlockNode,
|
||||
RosterReferenceBlockNode,
|
||||
HITLInputNode,
|
||||
CurrentBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
LastRunBlockNode, // LastRunBlockNode is used for error message block replacement
|
||||
AgentOutputBlockNode,
|
||||
],
|
||||
editorState: textToEditorState(value || ''),
|
||||
onError: (error: Error) => {
|
||||
@ -242,13 +259,17 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
requestURLBlock={requestURLBlock}
|
||||
historyBlock={historyBlock}
|
||||
variableBlock={variableBlock}
|
||||
rosterReferenceBlock={rosterReferenceBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
agentOutputBlock={agentOutputBlock}
|
||||
hitlInputBlock={hitlInputBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
disableSlashPicker={disableSlashPicker}
|
||||
disableBracePicker={disableBracePicker}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
instanceId={instanceId}
|
||||
@ -257,6 +278,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
/>
|
||||
<ValueSyncPlugin value={value} />
|
||||
<EditableSyncPlugin editable={editable} />
|
||||
{children}
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
)
|
||||
|
||||
@ -0,0 +1,369 @@
|
||||
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
import AgentOutputBlockComponent from '../component'
|
||||
import { $createAgentOutputBlockNode } from '../node'
|
||||
|
||||
const { mockEditorFocus, mockEditorUpdate, mockGetRootText, mockNodeReplace, mockSelectNext } = vi.hoisted(() => ({
|
||||
mockEditorFocus: vi.fn(),
|
||||
mockEditorUpdate: vi.fn((callback: () => void) => callback()),
|
||||
mockGetRootText: {
|
||||
value: '[§output:summary:summary§]',
|
||||
},
|
||||
mockNodeReplace: vi.fn(),
|
||||
mockSelectNext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@lexical/react/LexicalComposerContext')
|
||||
vi.mock('@langgenius/dify-ui/select', () => ({
|
||||
Select: ({
|
||||
children,
|
||||
onValueChange,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onValueChange: (value: string) => void
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
<button type="button" onClick={() => onValueChange('file')}>Select file</button>
|
||||
</div>
|
||||
),
|
||||
SelectContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectItemIndicator: () => <span />,
|
||||
SelectItemText: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||
SelectLabel: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||
SelectTrigger: ({
|
||||
children,
|
||||
onClick,
|
||||
onMouseDown,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button type="button" onClick={onClick} onMouseDown={onMouseDown} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
vi.mock('lexical', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('lexical')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
$getNodeByKey: vi.fn(),
|
||||
$getRoot: vi.fn(() => ({
|
||||
getChildren: () => [{
|
||||
getTextContent: () => mockGetRootText.value,
|
||||
}],
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../node', () => ({
|
||||
$createAgentOutputBlockNode: vi.fn((name: string, outputType: string, isEditing: boolean) => ({
|
||||
isEditing,
|
||||
name,
|
||||
outputType,
|
||||
})),
|
||||
$isAgentOutputBlockNode: () => true,
|
||||
}))
|
||||
|
||||
const outputs: DeclaredOutputConfig[] = [
|
||||
{
|
||||
name: 'output',
|
||||
type: 'string',
|
||||
required: false,
|
||||
},
|
||||
]
|
||||
|
||||
describe('AgentOutputBlockComponent', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetRootText.value = '[§output:summary:summary§]'
|
||||
vi.mocked(useLexicalComposerContext).mockReturnValue([
|
||||
{
|
||||
focus: mockEditorFocus,
|
||||
update: mockEditorUpdate,
|
||||
},
|
||||
{},
|
||||
] as unknown as ReturnType<typeof useLexicalComposerContext>)
|
||||
vi.mocked($getNodeByKey).mockReturnValue({
|
||||
replace: mockNodeReplace.mockReturnValue({
|
||||
selectNext: mockSelectNext,
|
||||
}),
|
||||
} as never)
|
||||
})
|
||||
|
||||
it('does not replace the Lexical node while typing an output name and commits on blur', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<AgentOutputBlockComponent
|
||||
nodeKey="output-node"
|
||||
name="output"
|
||||
outputType="string"
|
||||
isEditing
|
||||
outputs={outputs}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' })
|
||||
|
||||
await user.clear(input)
|
||||
await user.type(input, 'summary')
|
||||
|
||||
expect(input).toHaveValue('summary')
|
||||
expect(mockNodeReplace).not.toHaveBeenCalled()
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
|
||||
await user.tab()
|
||||
|
||||
expect($createAgentOutputBlockNode).toHaveBeenCalledWith(
|
||||
'summary',
|
||||
'string',
|
||||
false,
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'summary',
|
||||
type: 'string',
|
||||
required: false,
|
||||
}),
|
||||
]),
|
||||
onChange,
|
||||
)
|
||||
expect(mockNodeReplace).toHaveBeenCalledTimes(1)
|
||||
expect(mockSelectNext).not.toHaveBeenCalled()
|
||||
expect(mockEditorFocus).not.toHaveBeenCalled()
|
||||
expect(onChange).toHaveBeenCalledWith(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'summary',
|
||||
type: 'string',
|
||||
required: false,
|
||||
}),
|
||||
]), '[§output:summary:summary§]')
|
||||
})
|
||||
|
||||
it('moves the editor selection after the output block when committing with Enter', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<AgentOutputBlockComponent
|
||||
nodeKey="output-node"
|
||||
name="output"
|
||||
outputType="string"
|
||||
isEditing
|
||||
outputs={outputs}
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' })
|
||||
|
||||
await user.clear(input)
|
||||
await user.type(input, 'summary')
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(mockNodeReplace).toHaveBeenCalledTimes(1)
|
||||
expect(mockSelectNext).toHaveBeenCalledTimes(1)
|
||||
await waitFor(() => {
|
||||
expect(mockEditorFocus).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('commits the input DOM value on Enter even before React state rerenders', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<AgentOutputBlockComponent
|
||||
nodeKey="output-node"
|
||||
name="output"
|
||||
outputType="string"
|
||||
isEditing
|
||||
outputs={outputs}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' })
|
||||
|
||||
fireEvent.change(input, { target: { value: 'summary' } })
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
|
||||
expect($createAgentOutputBlockNode).toHaveBeenCalledWith(
|
||||
'summary',
|
||||
'string',
|
||||
false,
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'summary',
|
||||
}),
|
||||
]),
|
||||
onChange,
|
||||
)
|
||||
})
|
||||
|
||||
it('automatically commits file-name outputs as file type', () => {
|
||||
const onChange = vi.fn()
|
||||
mockGetRootText.value = '[§output:qna_report.pdf:qna_report.pdf§]'
|
||||
|
||||
render(
|
||||
<AgentOutputBlockComponent
|
||||
nodeKey="output-node"
|
||||
name="output"
|
||||
outputType="string"
|
||||
isEditing
|
||||
outputs={outputs}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' })
|
||||
|
||||
fireEvent.change(input, { target: { value: 'qna_report.pdf' } })
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
|
||||
expect($createAgentOutputBlockNode).toHaveBeenCalledWith(
|
||||
'qna_report.pdf',
|
||||
'file',
|
||||
false,
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'qna_report.pdf',
|
||||
type: 'file',
|
||||
file: {
|
||||
extensions: [],
|
||||
mime_types: [],
|
||||
},
|
||||
}),
|
||||
]),
|
||||
onChange,
|
||||
)
|
||||
expect(onChange).toHaveBeenCalledWith(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'qna_report.pdf',
|
||||
type: 'file',
|
||||
}),
|
||||
]), '[§output:qna_report.pdf:qna_report.pdf§]')
|
||||
})
|
||||
|
||||
it('keeps dotted output names as the selected type when the extension is not whitelisted', () => {
|
||||
const onChange = vi.fn()
|
||||
mockGetRootText.value = '[§output:report.customext:report.customext§]'
|
||||
|
||||
render(
|
||||
<AgentOutputBlockComponent
|
||||
nodeKey="output-node"
|
||||
name="output"
|
||||
outputType="string"
|
||||
isEditing
|
||||
outputs={outputs}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' })
|
||||
|
||||
fireEvent.change(input, { target: { value: 'report.customext' } })
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
|
||||
expect($createAgentOutputBlockNode).toHaveBeenCalledWith(
|
||||
'report.customext',
|
||||
'string',
|
||||
false,
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'report.customext',
|
||||
type: 'string',
|
||||
}),
|
||||
]),
|
||||
onChange,
|
||||
)
|
||||
})
|
||||
|
||||
it('does not commit the name blur before selecting an output type', () => {
|
||||
const onChange = vi.fn()
|
||||
mockGetRootText.value = '[§output:summary:summary§]'
|
||||
|
||||
render(
|
||||
<AgentOutputBlockComponent
|
||||
nodeKey="output-node"
|
||||
name="output"
|
||||
outputType="string"
|
||||
isEditing
|
||||
outputs={outputs}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' })
|
||||
const typeTrigger = screen.getByRole('button', { name: 'workflow.nodes.agent.outputVars.typeLabel' })
|
||||
|
||||
fireEvent.change(input, { target: { value: 'summary' } })
|
||||
fireEvent.mouseDown(typeTrigger)
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Select file' }))
|
||||
|
||||
expect($createAgentOutputBlockNode).toHaveBeenCalledWith(
|
||||
'summary',
|
||||
'file',
|
||||
false,
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'summary',
|
||||
type: 'file',
|
||||
}),
|
||||
]),
|
||||
onChange,
|
||||
)
|
||||
})
|
||||
|
||||
it('does not save stale output data when the Lexical node has already been replaced', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
vi.mocked($getNodeByKey).mockReturnValue(null)
|
||||
|
||||
render(
|
||||
<AgentOutputBlockComponent
|
||||
nodeKey="output-node"
|
||||
name="output"
|
||||
outputType="string"
|
||||
isEditing
|
||||
outputs={outputs}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' })
|
||||
|
||||
await user.clear(input)
|
||||
await user.type(input, 'summary')
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(mockNodeReplace).not.toHaveBeenCalled()
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(mockEditorFocus).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders the committed output block as non-editable adaptive text', () => {
|
||||
render(
|
||||
<AgentOutputBlockComponent
|
||||
nodeKey="output-node"
|
||||
name="qna_report_pdf"
|
||||
outputType="file"
|
||||
isEditing={false}
|
||||
outputs={outputs}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'workflow.nodes.agent.outputVars.typeLabel' })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('qna_report_pdf')).toBeInTheDocument()
|
||||
expect(screen.getByText('file')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,106 @@
|
||||
import type {
|
||||
Klass,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
} from 'lexical'
|
||||
import { createEditor } from 'lexical'
|
||||
import {
|
||||
$createAgentOutputBlockNode,
|
||||
$isAgentOutputBlockNode,
|
||||
AgentOutputBlockNode,
|
||||
} from '../node'
|
||||
import {
|
||||
extractAgentOutputNames,
|
||||
getAgentOutputToken,
|
||||
inferAgentOutputType,
|
||||
parseAgentOutputToken,
|
||||
replaceAgentOutputName,
|
||||
} from '../utils'
|
||||
|
||||
describe('AgentOutputBlockNode', () => {
|
||||
let editor: LexicalEditor
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
editor = createEditor({
|
||||
nodes: [AgentOutputBlockNode as unknown as Klass<LexicalNode>],
|
||||
})
|
||||
})
|
||||
|
||||
const runInEditor = (callback: () => void) => {
|
||||
editor.update(callback, { discrete: true })
|
||||
}
|
||||
|
||||
it('should persist output tokens with bracket wrappers', () => {
|
||||
runInEditor(() => {
|
||||
const node = $createAgentOutputBlockNode('summary', 'string')
|
||||
|
||||
expect(node.getTextContent()).toBe('[§output:summary:summary§]')
|
||||
expect(getAgentOutputToken('summary')).toBe('[§output:summary:summary§]')
|
||||
})
|
||||
})
|
||||
|
||||
it('should persist and parse file-name output tokens', () => {
|
||||
runInEditor(() => {
|
||||
const node = $createAgentOutputBlockNode('qna_report.pdf', 'file')
|
||||
|
||||
expect(node.getTextContent()).toBe('[§output:qna_report.pdf:qna_report.pdf§]')
|
||||
expect(getAgentOutputToken('qna_report.pdf')).toBe('[§output:qna_report.pdf:qna_report.pdf§]')
|
||||
})
|
||||
|
||||
expect(parseAgentOutputToken('Use [§output:qna_report.pdf:qna_report.pdf§]')).toEqual({
|
||||
name: 'qna_report.pdf',
|
||||
start: 4,
|
||||
end: 44,
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer file type only for whitelisted file extensions', () => {
|
||||
expect(inferAgentOutputType('qna_report.pdf', 'string')).toBe('file')
|
||||
expect(inferAgentOutputType('QNA_REPORT.PDF', 'string')).toBe('file')
|
||||
expect(inferAgentOutputType('report.customext', 'string')).toBe('string')
|
||||
expect(inferAgentOutputType('report.customext', 'object')).toBe('object')
|
||||
})
|
||||
|
||||
it('should parse bracketed output tokens and legacy bare tokens', () => {
|
||||
expect(parseAgentOutputToken('before [§output:summary:summary§] after')).toEqual({
|
||||
name: 'summary',
|
||||
start: 7,
|
||||
end: 33,
|
||||
})
|
||||
expect(parseAgentOutputToken('before §output:summary:summary§ after')).toEqual({
|
||||
name: 'summary',
|
||||
start: 7,
|
||||
end: 31,
|
||||
})
|
||||
expect(parseAgentOutputToken('[§skill:summary:summary§]')).toBeNull()
|
||||
})
|
||||
|
||||
it('should extract output names from bracketed and legacy tokens', () => {
|
||||
expect([...extractAgentOutputNames('A [§output:summary:summary§] B §output:qna_report.pdf:qna_report.pdf§')]).toEqual([
|
||||
'summary',
|
||||
'qna_report.pdf',
|
||||
])
|
||||
})
|
||||
|
||||
it('should replace only matching output token names', () => {
|
||||
expect(replaceAgentOutputName(
|
||||
'Use [§output:summary:summary§] and §output:other:other§',
|
||||
'summary',
|
||||
'final_summary',
|
||||
)).toBe('Use [§output:final_summary:final_summary§] and §output:other:other§')
|
||||
})
|
||||
|
||||
it('should create node with helper and support type guard checks', () => {
|
||||
runInEditor(() => {
|
||||
const node = $createAgentOutputBlockNode('result', 'object')
|
||||
|
||||
expect(node).toBeInstanceOf(AgentOutputBlockNode)
|
||||
expect(node.getTextContent()).toBe('[§output:result:result§]')
|
||||
expect($isAgentOutputBlockNode(node)).toBe(true)
|
||||
expect($isAgentOutputBlockNode(null)).toBe(false)
|
||||
expect($isAgentOutputBlockNode(undefined)).toBe(false)
|
||||
expect($isAgentOutputBlockNode({} as LexicalNode)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,3 @@
|
||||
import { createCommand } from 'lexical'
|
||||
|
||||
export const INSERT_AGENT_OUTPUT_BLOCK_COMMAND = createCommand('INSERT_AGENT_OUTPUT_BLOCK_COMMAND')
|
||||
@ -0,0 +1,211 @@
|
||||
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { AgentOutputTypeOptionValue } from './utils'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $getNodeByKey, $getRoot } from 'lexical'
|
||||
import { memo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { $createAgentOutputBlockNode, $isAgentOutputBlockNode } from './node'
|
||||
import {
|
||||
AGENT_OUTPUT_NAME_PATTERN,
|
||||
AGENT_OUTPUT_TYPE_OPTIONS,
|
||||
createAgentOutputConfig,
|
||||
getAgentOutputTypeOption,
|
||||
inferAgentOutputType,
|
||||
} from './utils'
|
||||
|
||||
type AgentOutputBlockComponentProps = {
|
||||
nodeKey: string
|
||||
name: string
|
||||
outputType: AgentOutputTypeOptionValue
|
||||
isEditing: boolean
|
||||
outputs: DeclaredOutputConfig[]
|
||||
onChange?: (outputs: DeclaredOutputConfig[], prompt?: string) => void
|
||||
}
|
||||
|
||||
function upsertOutput(
|
||||
outputs: DeclaredOutputConfig[],
|
||||
oldName: string,
|
||||
nextName: string,
|
||||
outputType: AgentOutputTypeOptionValue,
|
||||
) {
|
||||
const trimmedName = nextName.trim()
|
||||
if (!AGENT_OUTPUT_NAME_PATTERN.test(trimmedName))
|
||||
return null
|
||||
|
||||
const nextOutputType = inferAgentOutputType(trimmedName, outputType)
|
||||
const existingIndex = outputs.findIndex(output => output.name === oldName)
|
||||
const duplicateIndex = outputs.findIndex(output => output.name === trimmedName && output.name !== oldName)
|
||||
if (duplicateIndex >= 0)
|
||||
return null
|
||||
|
||||
const nextOutput = createAgentOutputConfig(trimmedName, nextOutputType)
|
||||
if (existingIndex >= 0)
|
||||
return outputs.map((output, index) => index === existingIndex ? nextOutput : output)
|
||||
|
||||
return [...outputs, nextOutput]
|
||||
}
|
||||
|
||||
const AgentOutputBlockComponent = ({
|
||||
nodeKey,
|
||||
name,
|
||||
outputType,
|
||||
isEditing,
|
||||
outputs,
|
||||
onChange,
|
||||
}: AgentOutputBlockComponentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const selected = getAgentOutputTypeOption(outputType)
|
||||
const [draftName, setDraftName] = useState(name)
|
||||
const [lastNodeName, setLastNodeName] = useState(name)
|
||||
const skipNextBlurCommitRef = useRef(false)
|
||||
const latestDraftNameRef = useRef(name)
|
||||
|
||||
if (name !== lastNodeName) {
|
||||
setLastNodeName(name)
|
||||
setDraftName(name)
|
||||
latestDraftNameRef.current = name
|
||||
}
|
||||
|
||||
const commitOutput = (nextName: string, nextType: AgentOutputTypeOptionValue, selectAfterCommit = false) => {
|
||||
const trimmedName = nextName.trim()
|
||||
const nextOutputs = upsertOutput(outputs, name, trimmedName, nextType)
|
||||
if (!nextOutputs) {
|
||||
setDraftName(name)
|
||||
return false
|
||||
}
|
||||
|
||||
let didCommit = false
|
||||
let nextPrompt: string | undefined
|
||||
editor.update(() => {
|
||||
const node = $getNodeByKey(nodeKey)
|
||||
if (!$isAgentOutputBlockNode(node))
|
||||
return
|
||||
|
||||
const nextOutputType = inferAgentOutputType(trimmedName, nextType)
|
||||
const nextNode = node.replace($createAgentOutputBlockNode(trimmedName, nextOutputType, false, nextOutputs, onChange))
|
||||
if (selectAfterCommit)
|
||||
nextNode.selectNext()
|
||||
nextPrompt = $getRoot().getChildren().map(node => node.getTextContent()).join('\n')
|
||||
didCommit = true
|
||||
})
|
||||
|
||||
if (!didCommit)
|
||||
return false
|
||||
|
||||
onChange?.(nextOutputs, nextPrompt)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<span
|
||||
contentEditable={false}
|
||||
className="inline-flex min-w-[18px] items-center gap-1 rounded-[5px] border border-util-colors-violet-violet-100 bg-util-colors-violet-violet-50 px-1 py-0.5 align-middle shadow-xs"
|
||||
>
|
||||
<span aria-hidden="true" className="i-custom-vender-workflow-variable-x size-3.5 shrink-0 text-util-colors-violet-violet-700" />
|
||||
<span className="system-xs-medium whitespace-nowrap text-util-colors-violet-violet-700">
|
||||
{name}
|
||||
</span>
|
||||
<span className="rounded-[3px] px-0.5 system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary">
|
||||
{selected.label}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
contentEditable={false}
|
||||
className="inline-flex items-center gap-[3px] rounded-[5px] border border-util-colors-violet-violet-700 bg-util-colors-violet-violet-50 p-px align-middle shadow-xs"
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-0.5 pl-0.5">
|
||||
<span aria-hidden="true" className="i-custom-vender-workflow-variable-x size-3.5 shrink-0 text-util-colors-violet-violet-700" />
|
||||
<input
|
||||
aria-label={t('nodes.agent.outputVars.nameLabel', { ns: 'workflow' })}
|
||||
value={draftName}
|
||||
className="h-4 max-w-28 min-w-5 border-0 bg-transparent p-0 text-center system-xs-regular text-util-colors-violet-violet-700 outline-hidden placeholder:text-util-colors-violet-violet-700/50 focus:w-24"
|
||||
placeholder={t('nodes.agent.outputVars.namePlaceholder', { ns: 'workflow' })}
|
||||
onMouseDown={event => event.stopPropagation()}
|
||||
onClick={event => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation()
|
||||
event.nativeEvent.stopImmediatePropagation?.()
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
skipNextBlurCommitRef.current = true
|
||||
const didCommit = commitOutput(event.currentTarget.value, outputType, true)
|
||||
if (!didCommit) {
|
||||
skipNextBlurCommitRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
editor.focus()
|
||||
})
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
setDraftName(name)
|
||||
event.currentTarget.blur()
|
||||
}
|
||||
}}
|
||||
onChange={(event) => {
|
||||
latestDraftNameRef.current = event.currentTarget.value
|
||||
setDraftName(event.currentTarget.value)
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (skipNextBlurCommitRef.current) {
|
||||
skipNextBlurCommitRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
commitOutput(event.currentTarget.value, outputType)
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<Select<AgentOutputTypeOptionValue>
|
||||
value={outputType}
|
||||
onValueChange={(nextType) => {
|
||||
skipNextBlurCommitRef.current = false
|
||||
if (nextType)
|
||||
commitOutput(latestDraftNameRef.current, nextType)
|
||||
}}
|
||||
>
|
||||
<SelectLabel className="sr-only">
|
||||
{t('nodes.agent.outputVars.typeLabel', { ns: 'workflow' })}
|
||||
</SelectLabel>
|
||||
<SelectTrigger
|
||||
aria-label={t('nodes.agent.outputVars.typeLabel', { ns: 'workflow' })}
|
||||
className="h-4 min-w-4 rounded bg-util-colors-violet-violet-200 py-0 pr-0.5 pl-1 system-2xs-semibold-uppercase text-util-colors-violet-violet-700 hover:bg-util-colors-violet-violet-200"
|
||||
onMouseDown={() => {
|
||||
skipNextBlurCommitRef.current = true
|
||||
}}
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
{selected.label}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-40">
|
||||
{AGENT_OUTPUT_TYPE_OPTIONS.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.label}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AgentOutputBlockComponent)
|
||||
@ -0,0 +1,138 @@
|
||||
import type { ElementNode, TextNode } from 'lexical'
|
||||
import type { AgentOutputBlockType } from '../../types'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import {
|
||||
$applyNodeReplacement,
|
||||
$getRoot,
|
||||
$insertNodes,
|
||||
$isElementNode,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
} from 'lexical'
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
import { INSERT_AGENT_OUTPUT_BLOCK_COMMAND } from './commands'
|
||||
import { $createAgentOutputBlockNode, AgentOutputBlockNode } from './node'
|
||||
import {
|
||||
createAgentOutputConfig,
|
||||
getAgentOutputTypeOptionValue,
|
||||
getUniqueAgentOutputName,
|
||||
inferAgentOutputType,
|
||||
parseAgentOutputToken,
|
||||
} from './utils'
|
||||
|
||||
function getAgentOutputBlockNodeType(name: string, outputs: NonNullable<AgentOutputBlockType['outputs']>) {
|
||||
const output = outputs.find(item => item.name === name)
|
||||
|
||||
return inferAgentOutputType(name, output ? getAgentOutputTypeOptionValue(output) : 'string')
|
||||
}
|
||||
|
||||
const AgentOutputBlock = memo(({
|
||||
outputs = [],
|
||||
onChange,
|
||||
}: AgentOutputBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([AgentOutputBlockNode]))
|
||||
throw new Error('AgentOutputBlockPlugin: AgentOutputBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_AGENT_OUTPUT_BLOCK_COMMAND,
|
||||
() => {
|
||||
const name = getUniqueAgentOutputName(outputs)
|
||||
const outputType = 'string'
|
||||
const nextOutputs = [...outputs, createAgentOutputConfig(name, outputType)]
|
||||
const agentOutputBlockNode = $createAgentOutputBlockNode(name, outputType, true, nextOutputs, onChange)
|
||||
|
||||
$insertNodes([agentOutputBlockNode])
|
||||
const nextPrompt = $getRoot().getChildren().map(node => node.getTextContent()).join('\n')
|
||||
onChange?.(nextOutputs, nextPrompt)
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onChange, outputs])
|
||||
|
||||
return null
|
||||
})
|
||||
AgentOutputBlock.displayName = 'AgentOutputBlock'
|
||||
|
||||
const AgentOutputBlockReplacementBlock = memo(({
|
||||
outputs = [],
|
||||
onChange,
|
||||
}: AgentOutputBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([AgentOutputBlockNode]))
|
||||
throw new Error('AgentOutputBlockNodePlugin: AgentOutputBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createAgentOutputBlockNode = useCallback((textNode: TextNode): AgentOutputBlockNode => {
|
||||
const match = parseAgentOutputToken(textNode.getTextContent())
|
||||
const name = match?.name || ''
|
||||
const outputType = getAgentOutputBlockNodeType(name, outputs)
|
||||
|
||||
return $applyNodeReplacement($createAgentOutputBlockNode(name, outputType, false, outputs, onChange))
|
||||
}, [onChange, outputs])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const match = parseAgentOutputToken(text)
|
||||
|
||||
if (!match)
|
||||
return null
|
||||
|
||||
return {
|
||||
end: match.end,
|
||||
start: match.start,
|
||||
}
|
||||
}, [])
|
||||
|
||||
const transformListener = useCallback((textNode: CustomTextNode) => {
|
||||
return decoratorTransform(textNode, getMatch, createAgentOutputBlockNode, {
|
||||
allowAdjacentMatches: true,
|
||||
})
|
||||
}, [createAgentOutputBlockNode, getMatch])
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, transformListener),
|
||||
)
|
||||
}, [editor, transformListener])
|
||||
|
||||
useEffect(() => {
|
||||
editor.update(() => {
|
||||
const visitNode = (node: ElementNode) => {
|
||||
node.getChildren().forEach((child) => {
|
||||
if (child instanceof AgentOutputBlockNode) {
|
||||
const name = child.getName()
|
||||
const outputType = getAgentOutputBlockNodeType(name, outputs)
|
||||
if (
|
||||
child.getOutputType() !== outputType
|
||||
|| child.getOutputs() !== outputs
|
||||
|| child.getOnChange() !== onChange
|
||||
) {
|
||||
child.replace($createAgentOutputBlockNode(name, outputType, child.isEditing(), outputs, onChange))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ($isElementNode(child))
|
||||
visitNode(child)
|
||||
})
|
||||
}
|
||||
|
||||
visitNode($getRoot())
|
||||
})
|
||||
}, [editor, onChange, outputs])
|
||||
|
||||
return null
|
||||
})
|
||||
AgentOutputBlockReplacementBlock.displayName = 'AgentOutputBlockReplacementBlock'
|
||||
|
||||
export { AgentOutputBlock, AgentOutputBlockReplacementBlock }
|
||||
@ -0,0 +1,131 @@
|
||||
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import type { AgentOutputTypeOptionValue } from './utils'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import AgentOutputBlockComponent from './component'
|
||||
import { getAgentOutputToken } from './utils'
|
||||
|
||||
type SerializedNode = SerializedLexicalNode & {
|
||||
name: string
|
||||
outputType: AgentOutputTypeOptionValue
|
||||
}
|
||||
|
||||
export class AgentOutputBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
__name: string
|
||||
__outputType: AgentOutputTypeOptionValue
|
||||
__isEditing: boolean
|
||||
__outputs: DeclaredOutputConfig[]
|
||||
__onChange?: (outputs: DeclaredOutputConfig[], prompt?: string) => void
|
||||
|
||||
static override getType(): string {
|
||||
return 'agent-output-block'
|
||||
}
|
||||
|
||||
static override clone(node: AgentOutputBlockNode): AgentOutputBlockNode {
|
||||
return new AgentOutputBlockNode(
|
||||
node.__name,
|
||||
node.__outputType,
|
||||
node.__isEditing,
|
||||
node.__outputs,
|
||||
node.__onChange,
|
||||
node.__key,
|
||||
)
|
||||
}
|
||||
|
||||
override isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
outputType: AgentOutputTypeOptionValue,
|
||||
isEditing = false,
|
||||
outputs: DeclaredOutputConfig[] = [],
|
||||
onChange?: (outputs: DeclaredOutputConfig[], prompt?: string) => void,
|
||||
key?: NodeKey,
|
||||
) {
|
||||
super(key)
|
||||
|
||||
this.__name = name
|
||||
this.__outputType = outputType
|
||||
this.__isEditing = isEditing
|
||||
this.__outputs = outputs
|
||||
this.__onChange = onChange
|
||||
}
|
||||
|
||||
override createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
override updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
override decorate(): React.JSX.Element {
|
||||
return (
|
||||
<AgentOutputBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
name={this.getName()}
|
||||
outputType={this.getOutputType()}
|
||||
isEditing={this.isEditing()}
|
||||
outputs={this.getOutputs()}
|
||||
onChange={this.getOnChange()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
static override importJSON(serializedNode: SerializedNode): AgentOutputBlockNode {
|
||||
return $createAgentOutputBlockNode(serializedNode.name, serializedNode.outputType)
|
||||
}
|
||||
|
||||
override exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'agent-output-block',
|
||||
version: 1,
|
||||
name: this.getName(),
|
||||
outputType: this.getOutputType(),
|
||||
}
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getLatest().__name
|
||||
}
|
||||
|
||||
getOutputType(): AgentOutputTypeOptionValue {
|
||||
return this.getLatest().__outputType
|
||||
}
|
||||
|
||||
isEditing(): boolean {
|
||||
return this.getLatest().__isEditing
|
||||
}
|
||||
|
||||
getOutputs(): DeclaredOutputConfig[] {
|
||||
return this.getLatest().__outputs
|
||||
}
|
||||
|
||||
getOnChange(): ((outputs: DeclaredOutputConfig[], prompt?: string) => void) | undefined {
|
||||
return this.getLatest().__onChange
|
||||
}
|
||||
|
||||
override getTextContent(): string {
|
||||
return getAgentOutputToken(this.getName())
|
||||
}
|
||||
}
|
||||
|
||||
export function $createAgentOutputBlockNode(
|
||||
name: string,
|
||||
outputType: AgentOutputTypeOptionValue = 'string',
|
||||
isEditing = false,
|
||||
outputs: DeclaredOutputConfig[] = [],
|
||||
onChange?: (outputs: DeclaredOutputConfig[], prompt?: string) => void,
|
||||
): AgentOutputBlockNode {
|
||||
return new AgentOutputBlockNode(name, outputType, isEditing, outputs, onChange)
|
||||
}
|
||||
|
||||
export function $isAgentOutputBlockNode(
|
||||
node: AgentOutputBlockNode | LexicalNode | null | undefined,
|
||||
): node is AgentOutputBlockNode {
|
||||
return node instanceof AgentOutputBlockNode
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
import type { DeclaredOutputConfig, DeclaredOutputType } from '@dify/contracts/api/console/apps/types.gen'
|
||||
|
||||
export type AgentOutputTypeOptionValue
|
||||
= DeclaredOutputType
|
||||
| 'array[boolean]'
|
||||
| 'array[file]'
|
||||
| 'array[number]'
|
||||
| 'array[object]'
|
||||
| 'array[string]'
|
||||
|
||||
export type AgentOutputTypeOption = {
|
||||
label: string
|
||||
type: DeclaredOutputType
|
||||
value: AgentOutputTypeOptionValue
|
||||
arrayItemType?: DeclaredOutputType
|
||||
}
|
||||
|
||||
const AGENT_OUTPUT_TOKEN_REGEX = /\[§output:([^:§\]]+):([^:§\]]+)§\]/
|
||||
const LEGACY_AGENT_OUTPUT_TOKEN_REGEX = /§output:([^:§\]]+):([^:§\]]+)§/
|
||||
export const AGENT_OUTPUT_NAME_PATTERN = /^[a-z_][\w.-]*$/i
|
||||
const AGENT_OUTPUT_FILE_NAME_PATTERN = /^[^.][^/\\:*?"<>|\n\r]*\.([a-z0-9]{1,16})$/i
|
||||
const AGENT_OUTPUT_FILE_EXTENSION_WHITELIST = new Set([
|
||||
'amr',
|
||||
'csv',
|
||||
'doc',
|
||||
'docx',
|
||||
'eml',
|
||||
'epub',
|
||||
'gif',
|
||||
'html',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'm4a',
|
||||
'markdown',
|
||||
'md',
|
||||
'mdx',
|
||||
'mov',
|
||||
'mp3',
|
||||
'mp4',
|
||||
'mpeg',
|
||||
'mpga',
|
||||
'msg',
|
||||
'pdf',
|
||||
'png',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'svg',
|
||||
'txt',
|
||||
'wav',
|
||||
'webm',
|
||||
'webp',
|
||||
'xls',
|
||||
'xlsx',
|
||||
'xml',
|
||||
])
|
||||
|
||||
export function getAgentOutputToken(name: string) {
|
||||
return `[§output:${name}:${name}§]`
|
||||
}
|
||||
|
||||
export function parseAgentOutputToken(text: string) {
|
||||
const match = AGENT_OUTPUT_TOKEN_REGEX.exec(text) ?? LEGACY_AGENT_OUTPUT_TOKEN_REGEX.exec(text)
|
||||
if (!match)
|
||||
return null
|
||||
|
||||
return {
|
||||
name: match[1]!,
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
}
|
||||
}
|
||||
|
||||
export function extractAgentOutputNames(text: string) {
|
||||
const names = new Set<string>()
|
||||
const bracketedRegex = new RegExp(AGENT_OUTPUT_TOKEN_REGEX.source, 'g')
|
||||
const legacyRegex = new RegExp(LEGACY_AGENT_OUTPUT_TOKEN_REGEX.source, 'g')
|
||||
|
||||
for (const match of text.matchAll(bracketedRegex))
|
||||
names.add(match[1]!)
|
||||
|
||||
for (const match of text.matchAll(legacyRegex))
|
||||
names.add(match[1]!)
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
export function replaceAgentOutputName(text: string, oldName: string, nextName: string) {
|
||||
const replaceTokenName = (match: string, name: string, mirrorName: string) => {
|
||||
if (name !== oldName)
|
||||
return match
|
||||
|
||||
return match.replace(`${name}:${mirrorName}`, `${nextName}:${nextName}`)
|
||||
}
|
||||
|
||||
return text
|
||||
.replace(new RegExp(AGENT_OUTPUT_TOKEN_REGEX.source, 'g'), replaceTokenName)
|
||||
.replace(new RegExp(LEGACY_AGENT_OUTPUT_TOKEN_REGEX.source, 'g'), replaceTokenName)
|
||||
}
|
||||
|
||||
export const AGENT_OUTPUT_TYPE_OPTIONS: AgentOutputTypeOption[] = [
|
||||
{ value: 'string', label: 'string', type: 'string' },
|
||||
{ value: 'number', label: 'number', type: 'number' },
|
||||
{ value: 'boolean', label: 'boolean', type: 'boolean' },
|
||||
{ value: 'object', label: 'object', type: 'object' },
|
||||
{ value: 'file', label: 'file', type: 'file' },
|
||||
{ value: 'array[string]', label: 'array[string]', type: 'array', arrayItemType: 'string' },
|
||||
{ value: 'array[number]', label: 'array[number]', type: 'array', arrayItemType: 'number' },
|
||||
{ value: 'array[boolean]', label: 'array[boolean]', type: 'array', arrayItemType: 'boolean' },
|
||||
{ value: 'array[object]', label: 'array[object]', type: 'array', arrayItemType: 'object' },
|
||||
{ value: 'array[file]', label: 'array[file]', type: 'array', arrayItemType: 'file' },
|
||||
]
|
||||
|
||||
export function getAgentOutputTypeOption(value: AgentOutputTypeOptionValue) {
|
||||
return AGENT_OUTPUT_TYPE_OPTIONS.find(option => option.value === value) || AGENT_OUTPUT_TYPE_OPTIONS[0]!
|
||||
}
|
||||
|
||||
function isSupportedAgentOutputFileName(name: string) {
|
||||
const match = AGENT_OUTPUT_FILE_NAME_PATTERN.exec(name.trim())
|
||||
if (!match)
|
||||
return false
|
||||
|
||||
return AGENT_OUTPUT_FILE_EXTENSION_WHITELIST.has(match[1]!.toLowerCase())
|
||||
}
|
||||
|
||||
export function inferAgentOutputType(name: string, fallbackType: AgentOutputTypeOptionValue): AgentOutputTypeOptionValue {
|
||||
if (isSupportedAgentOutputFileName(name))
|
||||
return 'file'
|
||||
|
||||
return fallbackType
|
||||
}
|
||||
|
||||
export function getAgentOutputTypeOptionValue(output: DeclaredOutputConfig): AgentOutputTypeOptionValue {
|
||||
if (output.type !== 'array')
|
||||
return output.type
|
||||
|
||||
return `array[${output.array_item?.type || 'object'}]` as AgentOutputTypeOptionValue
|
||||
}
|
||||
|
||||
export function createAgentOutputConfig(name: string, type: AgentOutputTypeOptionValue): DeclaredOutputConfig {
|
||||
const option = getAgentOutputTypeOption(type)
|
||||
const output: DeclaredOutputConfig = {
|
||||
name: name.trim(),
|
||||
type: option.type,
|
||||
required: false,
|
||||
}
|
||||
|
||||
if (option.type === 'array') {
|
||||
output.array_item = {
|
||||
type: option.arrayItemType || 'object',
|
||||
}
|
||||
}
|
||||
|
||||
if (option.type === 'file') {
|
||||
output.file = {
|
||||
extensions: [],
|
||||
mime_types: [],
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export function getUniqueAgentOutputName(outputs: DeclaredOutputConfig[]) {
|
||||
const outputNames = new Set(outputs.map(output => output.name))
|
||||
const baseName = 'output'
|
||||
if (!outputNames.has(baseName))
|
||||
return baseName
|
||||
|
||||
let index = 1
|
||||
while (outputNames.has(`${baseName}_${index}`))
|
||||
index += 1
|
||||
|
||||
return `${baseName}_${index}`
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import type { EventEmitter } from 'ahooks/lib/useEventEmitter'
|
||||
import type { LexicalEditor } from 'lexical'
|
||||
import type {
|
||||
AgentOutputBlockType,
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
@ -33,6 +34,7 @@ import { VAR_REFERENCE_CHILD_POPUP_CLASS_NAME } from '@/app/components/workflow/
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
import { INSERT_AGENT_OUTPUT_BLOCK_COMMAND } from '../../agent-output-block/commands'
|
||||
import { INSERT_CONTEXT_BLOCK_COMMAND } from '../../context-block'
|
||||
import { INSERT_CURRENT_BLOCK_COMMAND } from '../../current-block'
|
||||
import { INSERT_ERROR_MESSAGE_BLOCK_COMMAND } from '../../error-message-block'
|
||||
@ -148,6 +150,7 @@ const MinimalEditor: React.FC<{
|
||||
queryBlock?: QueryBlockType
|
||||
variableBlock?: VariableBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
agentOutputBlock?: AgentOutputBlockType
|
||||
currentBlock?: CurrentBlockType
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
@ -158,6 +161,7 @@ const MinimalEditor: React.FC<{
|
||||
queryBlock,
|
||||
variableBlock,
|
||||
workflowVariableBlock,
|
||||
agentOutputBlock,
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
@ -187,6 +191,7 @@ const MinimalEditor: React.FC<{
|
||||
queryBlock={queryBlock}
|
||||
variableBlock={variableBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
agentOutputBlock={agentOutputBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
@ -600,6 +605,48 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('clears slash trigger state after creating an agent output from the footer action', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
|
||||
render((
|
||||
<MinimalEditor
|
||||
triggerString="/"
|
||||
workflowVariableBlock={makeWorkflowVariableBlock({}, [
|
||||
makeWorkflowVarNode('node-1', 'Node 1', [
|
||||
makeWorkflowNodeVar('output', VarType.string),
|
||||
]),
|
||||
])}
|
||||
agentOutputBlock={{
|
||||
show: true,
|
||||
outputs: [],
|
||||
onChange: vi.fn(),
|
||||
}}
|
||||
captures={captures}
|
||||
/>
|
||||
))
|
||||
|
||||
const editor = await waitForEditor(captures)
|
||||
const dispatchSpy = vi.spyOn(editor, 'dispatchCommand')
|
||||
|
||||
await setEditorText(editor, '/', true)
|
||||
await flushNextTick()
|
||||
|
||||
const newOutputAction = await screen.findByText('workflow.nodes.agent.outputVars.newOutput')
|
||||
fireEvent.click(newOutputAction)
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(INSERT_AGENT_OUTPUT_BLOCK_COMMAND, undefined)
|
||||
await waitFor(() => {
|
||||
expect(readEditorText(editor)).not.toContain('/')
|
||||
expect(screen.queryByText('workflow.nodes.agent.outputVars.newOutput')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
const editable = screen.getByTestId(CONTENT_EDITABLE_TEST_ID)
|
||||
fireEvent.focus(editable)
|
||||
|
||||
await flushNextTick()
|
||||
expect(screen.queryByText('workflow.nodes.agent.outputVars.newOutput')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('defaults to the first workflow variable and removes the full slash query when selecting by keyboard', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import type { LexicalEditor, TextNode } from 'lexical'
|
||||
import type {
|
||||
AgentOutputBlockType,
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
@ -39,11 +40,13 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import VarReferenceVars, { VAR_REFERENCE_CHILD_POPUP_CLASS_NAME } from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useBasicTypeaheadTriggerMatch } from '../../hooks'
|
||||
import { $splitNodeContainingQuery } from '../../utils'
|
||||
import { INSERT_AGENT_OUTPUT_BLOCK_COMMAND } from '../agent-output-block/commands'
|
||||
import { INSERT_CURRENT_BLOCK_COMMAND } from '../current-block'
|
||||
import { INSERT_ERROR_MESSAGE_BLOCK_COMMAND } from '../error-message-block'
|
||||
import { INSERT_LAST_RUN_BLOCK_COMMAND } from '../last-run-block'
|
||||
@ -60,6 +63,7 @@ type ComponentPickerProps = {
|
||||
variableBlock?: VariableBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
agentOutputBlock?: AgentOutputBlockType
|
||||
currentBlock?: CurrentBlockType
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
@ -74,11 +78,13 @@ const ComponentPicker = ({
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
agentOutputBlock,
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
isSupportFileVar,
|
||||
}: ComponentPickerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const { refs, floatingStyles, isPositioned } = useFloating({
|
||||
placement: 'bottom-start',
|
||||
@ -105,6 +111,7 @@ const ComponentPicker = ({
|
||||
const [queryString, setQueryString] = useState<string | null>(null)
|
||||
const [blurHidden, setBlurHidden] = useState(false)
|
||||
const blurTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const showAgentOutputAction = triggerString === '/' && agentOutputBlock?.show
|
||||
|
||||
const clearBlurTimer = useCallback(() => {
|
||||
if (blurTimerRef.current) {
|
||||
@ -210,6 +217,12 @@ const ComponentPicker = ({
|
||||
}
|
||||
}, [editor, currentBlock?.generatorType, checkForTriggerMatch, triggerString])
|
||||
|
||||
const resetTypeaheadState = useCallback(() => {
|
||||
triggerMatchRef.current = null
|
||||
setQueryString(null)
|
||||
setBlurHidden(true)
|
||||
}, [])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' })
|
||||
editor.dispatchCommand(KEY_ESCAPE_COMMAND, escapeEvent)
|
||||
@ -223,7 +236,7 @@ const ComponentPicker = ({
|
||||
|
||||
if (blurHidden)
|
||||
return null
|
||||
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
|
||||
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show || showAgentOutputAction)))
|
||||
return null
|
||||
|
||||
setTimeout(() => {
|
||||
@ -299,6 +312,29 @@ const ComponentPicker = ({
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{showAgentOutputAction && (
|
||||
<div className="mt-1 border-t border-divider-subtle p-1">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-6 w-full items-center gap-1 rounded-md py-1 pr-1 pl-3 text-left system-sm-regular text-text-secondary hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
onMouseDown={event => event.preventDefault()}
|
||||
onClick={() => {
|
||||
editor.update(() => {
|
||||
const currentTriggerMatch = triggerMatchRef.current ?? checkForTriggerMatch(triggerString, editor)
|
||||
const needRemove = currentTriggerMatch ? $splitNodeContainingQuery(currentTriggerMatch) : null
|
||||
if (needRemove)
|
||||
needRemove.remove()
|
||||
})
|
||||
editor.dispatchCommand(INSERT_AGENT_OUTPUT_BLOCK_COMMAND, undefined)
|
||||
resetTypeaheadState()
|
||||
}}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-add-line size-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">{t('nodes.agent.outputVars.newOutput', { ns: 'workflow' })}</span>
|
||||
<span aria-hidden="true" className="i-ri-question-line size-3.5 shrink-0 text-text-quaternary" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
@ -306,7 +342,7 @@ const ComponentPicker = ({
|
||||
}
|
||||
</>
|
||||
)
|
||||
}, [blurHidden, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, triggerString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])
|
||||
}, [blurHidden, allFlattenOptions.length, workflowVariableBlock?.show, showAgentOutputAction, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, triggerString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField, editor, checkForTriggerMatch, t, resetTypeaheadState])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
import type {
|
||||
Klass,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
} from 'lexical'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { createEditor } from 'lexical'
|
||||
import RosterReferenceBlockComponent from '../component'
|
||||
import {
|
||||
$createRosterReferenceBlockNode,
|
||||
$isRosterReferenceBlockNode,
|
||||
RosterReferenceBlockNode,
|
||||
} from '../node'
|
||||
import {
|
||||
getRosterReferenceFileIconType,
|
||||
parseRosterReferenceToken,
|
||||
} from '../utils'
|
||||
|
||||
describe('RosterReferenceBlockNode', () => {
|
||||
let editor: LexicalEditor
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
editor = createEditor({
|
||||
nodes: [RosterReferenceBlockNode as unknown as Klass<LexicalNode>],
|
||||
})
|
||||
})
|
||||
|
||||
const runInEditor = (callback: () => void) => {
|
||||
editor.update(callback, { discrete: true })
|
||||
}
|
||||
|
||||
it('should parse roster reference tokens and infer icon classes', () => {
|
||||
expect(parseRosterReferenceToken('[§skill:2c3176de8a01:tender-analyzer§]')).toEqual({
|
||||
kind: 'skill',
|
||||
id: '2c3176de8a01',
|
||||
label: 'tender-analyzer',
|
||||
})
|
||||
expect(parseRosterReferenceToken('[§file:1f0ad3e2:qna_report:final.pdf§]')).toEqual({
|
||||
kind: 'file',
|
||||
id: '1f0ad3e2',
|
||||
label: 'qna_report:final.pdf',
|
||||
})
|
||||
expect(parseRosterReferenceToken('[§unknown:1:item§]')).toBeNull()
|
||||
expect(getRosterReferenceFileIconType('qna_report.pdf')).toBe('pdf')
|
||||
})
|
||||
|
||||
it('should render a non-editable token pill component', () => {
|
||||
const { container } = render(
|
||||
<RosterReferenceBlockComponent text="[§tool-all:tavily/tavily:tavily§]" />,
|
||||
)
|
||||
|
||||
const token = screen.getByTitle('tavily')
|
||||
expect(token).toHaveAttribute('contenteditable', 'false')
|
||||
expect(token).toHaveAttribute('data-roster-reference-kind', 'tool-all')
|
||||
expect(token).toHaveAttribute('data-roster-reference-id', 'tavily/tavily')
|
||||
expect(token).toHaveClass('border-state-accent-hover-alt')
|
||||
expect(token).toHaveClass('bg-state-accent-hover')
|
||||
expect(token).toHaveTextContent('tavily')
|
||||
expect(container.querySelector('.i-custom-public-other-default-tool-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render knowledge icon with the configured retrieval row style', () => {
|
||||
const { container } = render(
|
||||
<RosterReferenceBlockComponent text="[§knowledge:manual-1:产品手册§]" />,
|
||||
)
|
||||
|
||||
const iconShell = container.querySelector('.bg-util-colors-green-green-500')
|
||||
expect(iconShell).toBeInTheDocument()
|
||||
expect(iconShell).toHaveClass('text-text-primary-on-surface')
|
||||
expect(iconShell?.querySelector('.i-ri-book-open-line')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose DecoratorNode behavior and preserve raw text content', () => {
|
||||
runInEditor(() => {
|
||||
const node = new RosterReferenceBlockNode('[§tool-all:tavily/tavily:tavily§]', 'node-key')
|
||||
const cloned = RosterReferenceBlockNode.clone(node)
|
||||
const dom = node.createDOM()
|
||||
|
||||
expect(RosterReferenceBlockNode.getType()).toBe('roster-reference-block')
|
||||
expect(cloned).toBeInstanceOf(RosterReferenceBlockNode)
|
||||
expect(cloned.getKey()).toBe('node-key')
|
||||
expect(node.isInline()).toBe(true)
|
||||
expect(dom).toHaveClass('inline-flex')
|
||||
expect(dom).toHaveClass('align-middle')
|
||||
expect(node.getTextContent()).toBe('[§tool-all:tavily/tavily:tavily§]')
|
||||
})
|
||||
})
|
||||
|
||||
it('should import and export serialized node text', () => {
|
||||
runInEditor(() => {
|
||||
const imported = RosterReferenceBlockNode.importJSON({
|
||||
text: '[§knowledge:manual-1:产品手册§]',
|
||||
type: 'roster-reference-block',
|
||||
version: 1,
|
||||
})
|
||||
const exported = imported.exportJSON()
|
||||
|
||||
expect(exported).toEqual({
|
||||
text: '[§knowledge:manual-1:产品手册§]',
|
||||
type: 'roster-reference-block',
|
||||
version: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should create node with helper and support type guard checks', () => {
|
||||
runInEditor(() => {
|
||||
const node = $createRosterReferenceBlockNode('[§skill:playwright:Playwright§]')
|
||||
|
||||
expect(node).toBeInstanceOf(RosterReferenceBlockNode)
|
||||
expect(node.getTextContent()).toBe('[§skill:playwright:Playwright§]')
|
||||
expect($isRosterReferenceBlockNode(node)).toBe(true)
|
||||
expect($isRosterReferenceBlockNode(null)).toBe(false)
|
||||
expect($isRosterReferenceBlockNode(undefined)).toBe(false)
|
||||
expect($isRosterReferenceBlockNode({} as LexicalNode)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,55 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { FileTreeIcon } from '@langgenius/dify-ui/file-tree'
|
||||
import { use } from 'react'
|
||||
import { RosterReferenceBlockContext } from './context'
|
||||
import {
|
||||
getRosterReferenceFileIconType,
|
||||
getRosterReferenceIconClassName,
|
||||
parseRosterReferenceToken,
|
||||
} from './utils'
|
||||
|
||||
type RosterReferenceBlockComponentProps = {
|
||||
text: string
|
||||
}
|
||||
|
||||
const RosterReferenceBlockComponent = ({
|
||||
text,
|
||||
}: RosterReferenceBlockComponentProps) => {
|
||||
const rosterReferenceBlock = use(RosterReferenceBlockContext)
|
||||
const token = parseRosterReferenceToken(text)
|
||||
|
||||
if (!token)
|
||||
return null
|
||||
|
||||
const isKnowledge = token.kind === 'knowledge'
|
||||
const customIcon = rosterReferenceBlock?.renderIcon?.(token)
|
||||
const defaultIcon = token.kind === 'file'
|
||||
? <FileTreeIcon type={getRosterReferenceFileIconType(token.label)} className="size-4" />
|
||||
: <span className={cn(isKnowledge ? 'size-3.5' : 'size-3.5 shrink-0', getRosterReferenceIconClassName(token))} />
|
||||
|
||||
return (
|
||||
<span
|
||||
contentEditable={false}
|
||||
data-roster-reference-kind={token.kind}
|
||||
data-roster-reference-id={token.id}
|
||||
title={token.label}
|
||||
className="inline-flex min-w-[18px] items-center gap-0.5 overflow-hidden rounded-[5px] border border-state-accent-hover-alt bg-state-accent-hover py-px pr-1 pl-px align-middle shadow-xs shadow-shadow-shadow-3"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'inline-flex size-4 shrink-0 items-center justify-center rounded-[5px] border-[0.5px] border-divider-subtle bg-background-default-dodge',
|
||||
token.kind === 'cli_tool' && 'border-divider-regular bg-text-tertiary',
|
||||
isKnowledge && 'border-divider-subtle bg-util-colors-green-green-500 p-[3px] text-text-primary-on-surface shadow-xs shadow-shadow-shadow-3',
|
||||
)}
|
||||
>
|
||||
{customIcon || defaultIcon}
|
||||
</span>
|
||||
<span className="max-w-48 truncate system-xs-medium text-text-accent">
|
||||
{token.label}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default RosterReferenceBlockComponent
|
||||
@ -0,0 +1,4 @@
|
||||
import type { RosterReferenceBlockType } from '../../types'
|
||||
import { createContext } from 'react'
|
||||
|
||||
export const RosterReferenceBlockContext = createContext<RosterReferenceBlockType | undefined>(undefined)
|
||||
@ -0,0 +1,63 @@
|
||||
import type { EntityMatch } from '@lexical/text'
|
||||
import type { LexicalEditor, TextNode } from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
import { RosterReferenceBlockNode } from './node'
|
||||
import { ROSTER_REFERENCE_REGEX } from './utils'
|
||||
|
||||
type RosterReferenceNodeRegistry = {
|
||||
_nodes: Map<string, { klass: typeof RosterReferenceBlockNode }>
|
||||
}
|
||||
|
||||
function createRegisteredRosterReferenceBlockNode(editor: LexicalEditor, textNode: TextNode): RosterReferenceBlockNode {
|
||||
const RegisteredRosterReferenceBlockNode = (editor as unknown as RosterReferenceNodeRegistry)
|
||||
._nodes
|
||||
.get(RosterReferenceBlockNode.getType())
|
||||
?.klass ?? RosterReferenceBlockNode
|
||||
|
||||
return $applyNodeReplacement(new RegisteredRosterReferenceBlockNode(textNode.getTextContent()))
|
||||
}
|
||||
|
||||
const RosterReferenceBlock = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([RosterReferenceBlockNode]))
|
||||
throw new Error('RosterReferenceBlockPlugin: RosterReferenceBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createRosterReferenceBlockNode = useCallback((textNode: CustomTextNode): RosterReferenceBlockNode => (
|
||||
createRegisteredRosterReferenceBlockNode(editor, textNode)
|
||||
), [editor])
|
||||
|
||||
const getRosterReferenceMatch = useCallback((text: string): EntityMatch | null => {
|
||||
const matchArr = ROSTER_REFERENCE_REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + matchArr[0].length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getRosterReferenceMatch, createRosterReferenceBlockNode)),
|
||||
)
|
||||
}, [createRosterReferenceBlockNode, editor, getRosterReferenceMatch])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default RosterReferenceBlock
|
||||
@ -0,0 +1,76 @@
|
||||
import type {
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical'
|
||||
import type { JSX } from 'react'
|
||||
import {
|
||||
$applyNodeReplacement,
|
||||
DecoratorNode,
|
||||
} from 'lexical'
|
||||
import RosterReferenceBlockComponent from './component'
|
||||
|
||||
type SerializedRosterReferenceBlockNode = SerializedLexicalNode & {
|
||||
text: string
|
||||
}
|
||||
|
||||
export class RosterReferenceBlockNode extends DecoratorNode<JSX.Element> {
|
||||
__text: string
|
||||
|
||||
static override getType(): string {
|
||||
return 'roster-reference-block'
|
||||
}
|
||||
|
||||
static override clone(node: RosterReferenceBlockNode): RosterReferenceBlockNode {
|
||||
return new RosterReferenceBlockNode(node.__text, node.__key)
|
||||
}
|
||||
|
||||
constructor(text: string, key?: NodeKey) {
|
||||
super(key)
|
||||
this.__text = text
|
||||
}
|
||||
|
||||
override isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override createDOM(): HTMLElement {
|
||||
const span = document.createElement('span')
|
||||
span.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return span
|
||||
}
|
||||
|
||||
override updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
override decorate(): JSX.Element {
|
||||
return <RosterReferenceBlockComponent text={this.getTextContent()} />
|
||||
}
|
||||
|
||||
static override importJSON(serializedNode: SerializedRosterReferenceBlockNode): RosterReferenceBlockNode {
|
||||
return $createRosterReferenceBlockNode(serializedNode.text)
|
||||
}
|
||||
|
||||
override exportJSON(): SerializedRosterReferenceBlockNode {
|
||||
return {
|
||||
text: this.getTextContent(),
|
||||
type: 'roster-reference-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
override getTextContent(): string {
|
||||
return this.getLatest().__text
|
||||
}
|
||||
}
|
||||
|
||||
export function $createRosterReferenceBlockNode(text = ''): RosterReferenceBlockNode {
|
||||
return $applyNodeReplacement(new RosterReferenceBlockNode(text))
|
||||
}
|
||||
|
||||
export function $isRosterReferenceBlockNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
): node is RosterReferenceBlockNode {
|
||||
return node instanceof RosterReferenceBlockNode
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
import type { FileTreeIconType } from '@langgenius/dify-ui/file-tree'
|
||||
|
||||
type RosterReferenceKind = 'skill' | 'file' | 'tool-all' | 'tool' | 'cli_tool' | 'knowledge'
|
||||
|
||||
export type RosterReferenceToken = {
|
||||
kind: RosterReferenceKind
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export const ROSTER_REFERENCE_REGEX = /\[§(?:skill|file|tool-all|tool|cli_tool|knowledge):[^\]§\n\r]+§\]/
|
||||
|
||||
const KNOWN_KINDS = new Set<RosterReferenceKind>([
|
||||
'skill',
|
||||
'file',
|
||||
'tool-all',
|
||||
'tool',
|
||||
'cli_tool',
|
||||
'knowledge',
|
||||
])
|
||||
|
||||
export function parseRosterReferenceToken(text: string): RosterReferenceToken | null {
|
||||
if (!text.startsWith('[§') || !text.endsWith('§]'))
|
||||
return null
|
||||
|
||||
const body = text.slice(2, -2)
|
||||
const firstColonIndex = body.indexOf(':')
|
||||
if (firstColonIndex === -1)
|
||||
return null
|
||||
|
||||
const kind = body.slice(0, firstColonIndex) as RosterReferenceKind
|
||||
if (!KNOWN_KINDS.has(kind))
|
||||
return null
|
||||
|
||||
const rest = body.slice(firstColonIndex + 1)
|
||||
const secondColonIndex = rest.indexOf(':')
|
||||
const id = secondColonIndex === -1 ? rest : rest.slice(0, secondColonIndex)
|
||||
const label = secondColonIndex === -1 ? id : rest.slice(secondColonIndex + 1)
|
||||
|
||||
if (!id || !label)
|
||||
return null
|
||||
|
||||
return {
|
||||
kind,
|
||||
id,
|
||||
label,
|
||||
}
|
||||
}
|
||||
|
||||
const codeFileExtensions = new Set([
|
||||
'css',
|
||||
'go',
|
||||
'html',
|
||||
'htm',
|
||||
'js',
|
||||
'jsx',
|
||||
'py',
|
||||
'rb',
|
||||
'rs',
|
||||
'scss',
|
||||
'sh',
|
||||
'ts',
|
||||
'tsx',
|
||||
'vue',
|
||||
'yaml',
|
||||
'yml',
|
||||
])
|
||||
const imageFileExtensions = new Set(['apng', 'avif', 'bmp', 'gif', 'ico', 'jpeg', 'jpg', 'png', 'svg', 'webp'])
|
||||
const tableFileExtensions = new Set(['csv', 'xls', 'xlsx'])
|
||||
const archiveFileExtensions = new Set(['7z', 'gz', 'rar', 'tar', 'zip'])
|
||||
|
||||
export function getRosterReferenceFileIconType(label: string): FileTreeIconType {
|
||||
const extension = label.includes('.') ? label.split('.').pop()?.toLowerCase() : undefined
|
||||
|
||||
if (!extension)
|
||||
return 'folder'
|
||||
if (imageFileExtensions.has(extension))
|
||||
return 'image'
|
||||
if (extension === 'pdf')
|
||||
return 'pdf'
|
||||
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
|
||||
return 'markdown'
|
||||
if (extension === 'json')
|
||||
return 'json'
|
||||
if (tableFileExtensions.has(extension))
|
||||
return 'table'
|
||||
if (archiveFileExtensions.has(extension))
|
||||
return 'archive'
|
||||
if (codeFileExtensions.has(extension))
|
||||
return 'code'
|
||||
if (extension === 'txt')
|
||||
return 'text'
|
||||
|
||||
return 'file'
|
||||
}
|
||||
|
||||
export function getRosterReferenceIconClassName(token: RosterReferenceToken) {
|
||||
switch (token.kind) {
|
||||
case 'skill':
|
||||
return 'i-custom-public-agent-building-blocks text-text-tertiary'
|
||||
case 'file':
|
||||
return ''
|
||||
case 'tool-all':
|
||||
case 'tool':
|
||||
return 'i-custom-public-other-default-tool-icon text-[#ef5b39]'
|
||||
case 'cli_tool':
|
||||
return 'i-ri-terminal-box-line text-text-primary-on-surface'
|
||||
case 'knowledge':
|
||||
return 'i-ri-book-open-line'
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import type { EditorState } from 'lexical'
|
||||
import type { FC } from 'react'
|
||||
import type { Hotkey, ShortcutPopupDisplayMode, ShortcutPopupInsertHandler } from './plugins/shortcuts-popup-plugin'
|
||||
import type {
|
||||
AgentOutputBlockType,
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
@ -11,6 +12,7 @@ import type {
|
||||
LastRunBlockType,
|
||||
QueryBlockType,
|
||||
RequestURLBlockType,
|
||||
RosterReferenceBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
} from './types'
|
||||
@ -21,6 +23,10 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
|
||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
AgentOutputBlock,
|
||||
AgentOutputBlockReplacementBlock,
|
||||
} from './plugins/agent-output-block'
|
||||
import ComponentPickerBlock from './plugins/component-picker-block'
|
||||
import {
|
||||
ContextBlock,
|
||||
@ -57,6 +63,8 @@ import {
|
||||
RequestURLBlock,
|
||||
RequestURLBlockReplacementBlock,
|
||||
} from './plugins/request-url-block'
|
||||
import RosterReferenceBlock from './plugins/roster-reference-block'
|
||||
import { RosterReferenceBlockContext } from './plugins/roster-reference-block/context'
|
||||
import ShortcutsPopupPlugin from './plugins/shortcuts-popup-plugin'
|
||||
import UpdateBlock from './plugins/update-block'
|
||||
import VariableBlock from './plugins/variable-block'
|
||||
@ -84,13 +92,17 @@ type PromptEditorContentProps = {
|
||||
requestURLBlock?: RequestURLBlockType
|
||||
historyBlock?: HistoryBlockType
|
||||
variableBlock?: VariableBlockType
|
||||
rosterReferenceBlock?: RosterReferenceBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
agentOutputBlock?: AgentOutputBlockType
|
||||
hitlInputBlock?: HITLInputBlockType
|
||||
currentBlock?: CurrentBlockType
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
isSupportFileVar?: boolean
|
||||
disableSlashPicker?: boolean
|
||||
disableBracePicker?: boolean
|
||||
onBlur?: () => void
|
||||
onFocus?: () => void
|
||||
instanceId?: string
|
||||
@ -110,13 +122,17 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
requestURLBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
rosterReferenceBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
agentOutputBlock,
|
||||
hitlInputBlock,
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
isSupportFileVar,
|
||||
disableSlashPicker,
|
||||
disableBracePicker,
|
||||
onBlur,
|
||||
onFocus,
|
||||
instanceId,
|
||||
@ -124,7 +140,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
onEditorChange,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<RosterReferenceBlockContext value={rosterReferenceBlock}>
|
||||
<RichTextPlugin
|
||||
contentEditable={(
|
||||
<ContentEditable
|
||||
@ -150,34 +166,40 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
{(closePortal, onInsert) => <Popup onClose={closePortal} onInsert={onInsert} />}
|
||||
</ShortcutsPopupPlugin>
|
||||
))}
|
||||
<ComponentPickerBlock
|
||||
triggerString="/"
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
<ComponentPickerBlock
|
||||
triggerString="{"
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
{!disableSlashPicker && (
|
||||
<ComponentPickerBlock
|
||||
triggerString="/"
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
agentOutputBlock={agentOutputBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
)}
|
||||
{!disableBracePicker && (
|
||||
<ComponentPickerBlock
|
||||
triggerString="{"
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
agentOutputBlock={agentOutputBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
)}
|
||||
{contextBlock?.show && (
|
||||
<>
|
||||
<ContextBlock {...contextBlock} />
|
||||
@ -202,12 +224,21 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
<VariableValueBlock />
|
||||
</>
|
||||
)}
|
||||
{rosterReferenceBlock?.show && (
|
||||
<RosterReferenceBlock />
|
||||
)}
|
||||
{workflowVariableBlock?.show && (
|
||||
<>
|
||||
<WorkflowVariableBlock {...workflowVariableBlock} />
|
||||
<WorkflowVariableBlockReplacementBlock {...workflowVariableBlock} />
|
||||
</>
|
||||
)}
|
||||
{agentOutputBlock?.show && (
|
||||
<>
|
||||
<AgentOutputBlock {...agentOutputBlock} />
|
||||
<AgentOutputBlockReplacementBlock {...agentOutputBlock} />
|
||||
</>
|
||||
)}
|
||||
{hitlInputBlock?.show && (
|
||||
<>
|
||||
<HITLInputBlock {...hitlInputBlock} />
|
||||
@ -248,7 +279,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
{floatingAnchorElem && (
|
||||
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
|
||||
)}
|
||||
</>
|
||||
</RosterReferenceBlockContext>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { GeneratorType } from '../../app/configuration/config/automatic/types'
|
||||
import type { FormInputItem } from '../../workflow/nodes/human-input/types'
|
||||
import type { Type } from '../../workflow/nodes/llm/types'
|
||||
import type { Dataset } from './plugins/context-block'
|
||||
import type { RoleName } from './plugins/history-block'
|
||||
import type { RosterReferenceToken } from './plugins/roster-reference-block/utils'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
@ -59,6 +61,11 @@ export type VariableBlockType = {
|
||||
variables?: Option[]
|
||||
}
|
||||
|
||||
export type RosterReferenceBlockType = {
|
||||
show?: boolean
|
||||
renderIcon?: (token: RosterReferenceToken) => React.ReactNode
|
||||
}
|
||||
|
||||
export type ExternalToolBlockType = {
|
||||
show?: boolean
|
||||
externalTools?: ExternalToolOption[]
|
||||
@ -81,6 +88,12 @@ export type WorkflowVariableBlockType = {
|
||||
onManageInputField?: () => void
|
||||
}
|
||||
|
||||
export type AgentOutputBlockType = {
|
||||
show?: boolean
|
||||
outputs?: DeclaredOutputConfig[]
|
||||
onChange?: (outputs: DeclaredOutputConfig[], prompt?: string) => void
|
||||
}
|
||||
|
||||
export type WorkflowNodesMap = Record<string, Pick<Node['data'], 'title' | 'type' | 'height' | 'width' | 'position'> & { modelProvider?: string }>
|
||||
|
||||
export type HITLInputBlockType = {
|
||||
|
||||
@ -140,9 +140,9 @@ export function registerLexicalTextEntity<T extends TextNode>(
|
||||
let nodeToReplace
|
||||
|
||||
if (match.start === 0)
|
||||
[nodeToReplace, currentNode] = (currentNode.splitText(match.end)) as [any, TextNode]
|
||||
[nodeToReplace, currentNode] = currentNode.splitText(match.end) as [TextNode, TextNode]
|
||||
else
|
||||
[, nodeToReplace, currentNode] = (currentNode.splitText(match.start, match.end)) as [unknown, any, TextNode]
|
||||
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end) as [TextNode, TextNode, TextNode]
|
||||
|
||||
const replacementNode = createNode(nodeToReplace!)
|
||||
replacementNode.setFormat(nodeToReplace!.getFormat())
|
||||
@ -194,6 +194,9 @@ export const decoratorTransform = (
|
||||
node: CustomTextNode,
|
||||
getMatch: (text: string) => null | EntityMatch,
|
||||
createNode: (textNode: TextNode) => LexicalNode,
|
||||
options?: {
|
||||
allowAdjacentMatches?: boolean
|
||||
},
|
||||
) => {
|
||||
if (!node.isSimpleText())
|
||||
return
|
||||
@ -227,7 +230,7 @@ export const decoratorTransform = (
|
||||
else {
|
||||
const nextMatch = getMatch(nextText)
|
||||
|
||||
if (nextMatch !== null && nextMatch.start === 0)
|
||||
if (!options?.allowAdjacentMatches && nextMatch !== null && nextMatch.start === 0)
|
||||
return
|
||||
}
|
||||
|
||||
@ -240,9 +243,9 @@ export const decoratorTransform = (
|
||||
let nodeToReplace
|
||||
|
||||
if (match.start === 0)
|
||||
[nodeToReplace, currentNode] = (currentNode.splitText(match.end)) as [any, CustomTextNode]
|
||||
[nodeToReplace, currentNode] = currentNode.splitText(match.end) as [CustomTextNode, CustomTextNode]
|
||||
else
|
||||
[, nodeToReplace, currentNode] = (currentNode.splitText(match.start, match.end)) as [unknown, any, CustomTextNode]
|
||||
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end) as [CustomTextNode, CustomTextNode, CustomTextNode]
|
||||
|
||||
const replacementNode = createNode(nodeToReplace!)
|
||||
nodeToReplace!.replace(replacementNode)
|
||||
|
||||
@ -40,4 +40,25 @@ describe('useIntegrationsSetting', () => {
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'mcp' })
|
||||
})
|
||||
|
||||
it('should preserve the agent source for agent-scoped settings', () => {
|
||||
const { result } = renderHook(() => useIntegrationsSetting())
|
||||
|
||||
act(() => {
|
||||
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: 'agent' })
|
||||
})
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider', source: 'agent' })
|
||||
})
|
||||
|
||||
it('should preserve the cancel callback for migrated integrations settings', () => {
|
||||
const onCancelCallback = vi.fn()
|
||||
const { result } = renderHook(() => useIntegrationsSetting())
|
||||
|
||||
act(() => {
|
||||
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, onCancelCallback })
|
||||
})
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider', onCancelCallback })
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,6 +4,7 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type DialogProps = {
|
||||
backdropClassName?: string
|
||||
className?: string
|
||||
children: ReactNode
|
||||
show: boolean
|
||||
@ -11,6 +12,7 @@ type DialogProps = {
|
||||
}
|
||||
|
||||
const MenuDialog = ({
|
||||
backdropClassName,
|
||||
className,
|
||||
children,
|
||||
show,
|
||||
@ -27,7 +29,7 @@ const MenuDialog = ({
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
backdropClassName="z-40 bg-transparent"
|
||||
backdropClassName={cn('z-40 bg-transparent', backdropClassName)}
|
||||
className={cn(
|
||||
'top-0 left-0 z-40 h-full max-h-none w-full max-w-none translate-x-0 translate-y-0 scale-100 overflow-hidden rounded-none border-none bg-background-sidenav-bg p-0 shadow-none backdrop-blur-md transition-opacity data-ending-style:scale-100 data-starting-style:scale-100',
|
||||
className,
|
||||
|
||||
@ -247,6 +247,7 @@ export type DefaultModelResponse = {
|
||||
export type DefaultModel = {
|
||||
provider: string
|
||||
model: string
|
||||
plugin_id?: string
|
||||
}
|
||||
|
||||
export type CustomConfigurationModelFixedFields = {
|
||||
|
||||
@ -108,7 +108,11 @@ describe('ModelSelector', () => {
|
||||
fireEvent.click(screen.getByRole('combobox'))
|
||||
fireEvent.click(screen.getByText('select'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith({ provider: 'openai', model: 'gpt-4' })
|
||||
expect(onSelect).toHaveBeenCalledWith({
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
plugin_id: 'langgenius/openai',
|
||||
})
|
||||
})
|
||||
|
||||
it('should close popup when popup requests hide', () => {
|
||||
|
||||
@ -11,6 +11,15 @@ import ModelSelectorTrigger from './model-selector-trigger'
|
||||
import Popup from './popup'
|
||||
import { getModelSelectorValueLabel, isSameModelSelectorValue } from './types'
|
||||
|
||||
const getModelProviderPluginId = (provider: string) => {
|
||||
const [organization, pluginName] = provider.split('/').filter(Boolean)
|
||||
|
||||
if (organization && pluginName)
|
||||
return `${organization}/${pluginName}`
|
||||
|
||||
return provider ? `langgenius/${provider}` : ''
|
||||
}
|
||||
|
||||
type ModelSelectorProps = {
|
||||
defaultModel?: DefaultModel
|
||||
modelList: Model[]
|
||||
@ -24,6 +33,7 @@ type ModelSelectorProps = {
|
||||
showDeprecatedWarnIcon?: boolean
|
||||
hideProviderSettingsFooter?: boolean
|
||||
onConfigureEmptyState?: () => void
|
||||
providerSettingsSource?: 'agent'
|
||||
showModelMeta?: boolean
|
||||
}
|
||||
function ModelSelector({
|
||||
@ -39,6 +49,7 @@ function ModelSelector({
|
||||
showDeprecatedWarnIcon = true,
|
||||
hideProviderSettingsFooter,
|
||||
onConfigureEmptyState,
|
||||
providerSettingsSource,
|
||||
showModelMeta,
|
||||
}: ModelSelectorProps) {
|
||||
const { t } = useTranslation()
|
||||
@ -74,8 +85,13 @@ function ModelSelector({
|
||||
setOpen(false)
|
||||
setInputValue('')
|
||||
|
||||
if (onSelect)
|
||||
onSelect({ provider, model: model.model })
|
||||
if (onSelect) {
|
||||
onSelect({
|
||||
provider,
|
||||
model: model.model,
|
||||
plugin_id: getModelProviderPluginId(provider),
|
||||
})
|
||||
}
|
||||
}, [onSelect])
|
||||
|
||||
const handleValueChange = useCallback((value: ModelSelectorValue | null) => {
|
||||
@ -150,6 +166,7 @@ function ModelSelector({
|
||||
modelList={modelList}
|
||||
scopeFeatures={scopeFeatures}
|
||||
hideProviderSettingsFooter={hideProviderSettingsFooter}
|
||||
providerSettingsSource={providerSettingsSource}
|
||||
onConfigureEmptyState={onConfigureEmptyState ? handleConfigureEmptyState : undefined}
|
||||
onInputValueChange={setInputValue}
|
||||
onHide={handleHide}
|
||||
|
||||
@ -38,6 +38,7 @@ export type PopupProps = {
|
||||
modelList: Model[]
|
||||
scopeFeatures?: ModelFeatureEnum[]
|
||||
hideProviderSettingsFooter?: boolean
|
||||
providerSettingsSource?: 'agent'
|
||||
onConfigureEmptyState?: () => void
|
||||
onInputValueChange: (value: string) => void
|
||||
onHide: () => void
|
||||
@ -48,6 +49,7 @@ function Popup({
|
||||
modelList,
|
||||
scopeFeatures = [],
|
||||
hideProviderSettingsFooter,
|
||||
providerSettingsSource,
|
||||
onConfigureEmptyState,
|
||||
onInputValueChange,
|
||||
onHide,
|
||||
@ -173,8 +175,8 @@ function Popup({
|
||||
|
||||
const handleOpenSettings = useCallback(() => {
|
||||
onHide()
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
}, [onHide, openIntegrationsSetting])
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: providerSettingsSource })
|
||||
}, [onHide, openIntegrationsSetting, providerSettingsSource])
|
||||
const handleClosePreviewCard = useCallback(() => {
|
||||
previewCardHandle.close()
|
||||
}, [previewCardHandle])
|
||||
|
||||
@ -7,8 +7,8 @@ import { useModalContext } from '@/context/modal-context'
|
||||
import { integrationSectionByMovedAccountSettingTab } from './destinations'
|
||||
|
||||
type IntegrationsSettingState
|
||||
= | { payload: MovedAccountSettingTab }
|
||||
| { section: IntegrationSection }
|
||||
= | { payload: MovedAccountSettingTab, source?: 'agent', onCancelCallback?: () => void }
|
||||
| { section: IntegrationSection, source?: 'agent', onCancelCallback?: () => void }
|
||||
|
||||
export const useIntegrationsSetting = () => {
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
@ -19,7 +19,12 @@ export const useIntegrationsSetting = () => {
|
||||
? state.section
|
||||
: integrationSectionByMovedAccountSettingTab[state.payload]
|
||||
|
||||
if (section)
|
||||
setShowAccountSettingModal({ payload: section })
|
||||
if (section) {
|
||||
setShowAccountSettingModal({
|
||||
payload: section,
|
||||
...(state.source ? { source: state.source } : {}),
|
||||
...(state.onCancelCallback ? { onCancelCallback: state.onCancelCallback } : {}),
|
||||
})
|
||||
}
|
||||
}, [setShowAccountSettingModal])
|
||||
}
|
||||
|
||||
@ -24,15 +24,20 @@ import MainNav from '../index'
|
||||
|
||||
const activeEdgeClassName = 'before:pointer-events-none'
|
||||
|
||||
const { mockSwitchWorkspace, mockToastSuccess, hotkeyRegistrations } = vi.hoisted(() => ({
|
||||
const { mockIsAgentV2Enabled, mockSwitchWorkspace, mockToastSuccess, hotkeyRegistrations } = vi.hoisted(() => ({
|
||||
mockSwitchWorkspace: vi.fn(),
|
||||
mockToastSuccess: vi.fn(),
|
||||
mockIsAgentV2Enabled: vi.fn(() => true),
|
||||
hotkeyRegistrations: new Map<string, {
|
||||
handler: (event: { preventDefault: () => void }) => void
|
||||
options?: { ignoreInputs?: boolean }
|
||||
}>(),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/agent-v2/feature-flag', () => ({
|
||||
isAgentV2Enabled: () => mockIsAgentV2Enabled(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
@ -159,6 +164,15 @@ vi.mock('@/app/components/app-sidebar/dataset-detail-top', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/agent-v2/agent-detail/navigation', () => ({
|
||||
AgentDetailSection: ({ expand }: { expand: boolean }) => <div data-testid="agent-detail-section" data-expand={expand} />,
|
||||
AgentDetailTop: ({ expand, onToggle }: { expand: boolean, onToggle: () => void }) => (
|
||||
<div data-testid="agent-detail-top" data-expand={expand}>
|
||||
<button type="button" data-testid="agent-detail-toggle" onClick={onToggle}>Toggle</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/deployments/detail/deployment-sidebar', () => ({
|
||||
DeploymentDetailSection: ({ expand }: { expand: boolean }) => <div data-testid="deployment-detail-section" data-expand={expand} />,
|
||||
DeploymentDetailTop: ({ expand, onToggle }: { expand: boolean, onToggle: () => void }) => (
|
||||
@ -285,6 +299,7 @@ describe('MainNav', () => {
|
||||
{ id: 'workspace-1', name: 'Solar Studio', plan: Plan.team, status: 'normal', created_at: 0, current: true },
|
||||
{ id: 'workspace-2', name: 'Evan Workspace', plan: Plan.sandbox, status: 'normal', created_at: 0, current: false },
|
||||
]
|
||||
mockIsAgentV2Enabled.mockReturnValue(true)
|
||||
|
||||
;(usePathname as Mock).mockImplementation(() => mockPathname)
|
||||
;(useRouter as Mock).mockReturnValue({
|
||||
@ -331,11 +346,20 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(Plan.team)
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
|
||||
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
|
||||
expect(screen.getByRole('link', { name: /common.menus.roster/ })).toHaveAttribute('href', '/roster')
|
||||
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute('href', '/integrations/model-provider')
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/marketplace')
|
||||
})
|
||||
|
||||
it('hides the roster entry when Agent v2 is disabled', () => {
|
||||
mockIsAgentV2Enabled.mockReturnValue(false)
|
||||
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders deployments in primary navigation when app deploy is enabled', () => {
|
||||
renderMainNav({ branding: { enabled: false }, enable_app_deploy: true })
|
||||
|
||||
@ -460,6 +484,7 @@ describe('MainNav', () => {
|
||||
|
||||
expect(screen.queryByRole('link', { name: /common.mainNav.home/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.apps/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
|
||||
expect(screen.queryByRole('link', { name: /common.mainNav.integrations/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/marketplace')
|
||||
@ -483,6 +508,7 @@ describe('MainNav', () => {
|
||||
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.menus.roster/ })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.datasets/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.deployments/ })).not.toBeInTheDocument()
|
||||
@ -652,6 +678,33 @@ describe('MainNav', () => {
|
||||
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
})
|
||||
|
||||
it('replaces global navigation with agent detail navigation on roster detail routes', () => {
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.getByTestId('agent-detail-top')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('agent-detail-section')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-[248px]')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('bg-background-body')
|
||||
expect(screen.queryByRole('button', { name: 'common.mainNav.workspace.openMenu' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps roster detail navigation hidden when Agent v2 is disabled', () => {
|
||||
mockIsAgentV2Enabled.mockReturnValue(false)
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.queryByTestId('agent-detail-top')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('agent-detail-section')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('replaces global navigation with deployment detail navigation on deployment routes', () => {
|
||||
mockPathname = '/deployments/app-instance-1/releases'
|
||||
|
||||
@ -669,6 +722,19 @@ describe('MainNav', () => {
|
||||
expect(screen.queryByRole('link', { name: /common.menus.deployments/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('collapses agent detail navigation from the top-right toggle', () => {
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'false')
|
||||
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'false')
|
||||
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
|
||||
})
|
||||
|
||||
it('collapses deployment detail navigation from the top-right toggle', () => {
|
||||
mockPathname = '/deployments/app-instance-1/releases'
|
||||
|
||||
@ -705,6 +771,21 @@ describe('MainNav', () => {
|
||||
expect.objectContaining({ ignoreInputs: false }),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows agent detail navigation as a floating preview when hovering the collapsed top toggle', () => {
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
|
||||
fireEvent.mouseEnter(screen.getByTestId('agent-detail-top').parentElement!)
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
|
||||
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
|
||||
expect(screen.getAllByTestId('agent-detail-top')).toHaveLength(1)
|
||||
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'/datasets/create',
|
||||
'/datasets/create-from-pipeline',
|
||||
@ -731,6 +812,16 @@ describe('MainNav', () => {
|
||||
expect(marketplaceLink).toHaveClass(activeEdgeClassName)
|
||||
})
|
||||
|
||||
it('marks roster active on roster routes', () => {
|
||||
mockPathname = '/roster'
|
||||
|
||||
renderMainNav()
|
||||
|
||||
const rosterLink = screen.getByRole('link', { name: /common.menus.roster/ })
|
||||
expect(rosterLink).toHaveClass(activeEdgeClassName)
|
||||
expect(rosterLink).toHaveAttribute('aria-current', 'page')
|
||||
})
|
||||
|
||||
it('applies the Figma glass active state to the Home route', () => {
|
||||
mockPathname = '/'
|
||||
|
||||
|
||||
@ -17,6 +17,8 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||
import EnvNav from '@/app/components/header/env-nav'
|
||||
import { buildIntegrationPath } from '@/app/components/integrations/routes'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation'
|
||||
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
|
||||
import { DeploymentDetailSection, DeploymentDetailTop } from '@/features/deployments/detail/deployment-sidebar'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import Link from '@/next/link'
|
||||
@ -62,6 +64,12 @@ const isDatasetDetailPathname = (pathname: string) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const isAgentDetailPathname = (pathname: string) => {
|
||||
const [section, type, agentId] = pathname.split('/').filter(Boolean)
|
||||
|
||||
return section === 'roster' && type === 'agent' && !!agentId
|
||||
}
|
||||
|
||||
const isDeploymentDetailPathname = (pathname: string) => {
|
||||
const [section, appInstanceId] = pathname.split('/').filter(Boolean)
|
||||
|
||||
@ -81,13 +89,15 @@ const MainNav = ({
|
||||
const pathname = usePathname()
|
||||
const { langGeniusVersionInfo, isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const agentV2Enabled = isAgentV2Enabled()
|
||||
const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT'
|
||||
const canUseAppDeploy = isCurrentWorkspaceEditor && systemFeatures.enable_app_deploy
|
||||
const showAppDetailNavigation = !isCurrentWorkspaceDatasetOperator && pathname.startsWith('/app/')
|
||||
const showDatasetDetailNavigation = isDatasetDetailPathname(pathname)
|
||||
const showAgentDetailNavigation = agentV2Enabled && !isCurrentWorkspaceDatasetOperator && isAgentDetailPathname(pathname)
|
||||
const showDeploymentDetailNavigation = canUseAppDeploy && !isCurrentWorkspaceDatasetOperator && isDeploymentDetailPathname(pathname)
|
||||
const showSnippetDetailBottomNavigation = isSnippetDetailPathname(pathname)
|
||||
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showDeploymentDetailNavigation
|
||||
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showAgentDetailNavigation || showDeploymentDetailNavigation
|
||||
const { hasAppDetail, appSidebarExpand, setAppDetail, setAppSidebarExpand } = useAppStore(useShallow(state => ({
|
||||
hasAppDetail: !!state.appDetail,
|
||||
appSidebarExpand: state.appSidebarExpand,
|
||||
@ -192,6 +202,15 @@ const MainNav = ({
|
||||
icon: 'i-custom-vender-main-nav-studio',
|
||||
activeIcon: 'i-custom-vender-main-nav-studio-active',
|
||||
},
|
||||
...(agentV2Enabled
|
||||
? [{
|
||||
href: '/roster',
|
||||
label: t('menus.roster', { ns: 'common' }),
|
||||
active: (path: string) => path.startsWith('/roster'),
|
||||
icon: 'i-custom-vender-main-nav-roster',
|
||||
activeIcon: 'i-custom-vender-main-nav-roster-active',
|
||||
}]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
...((isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator)
|
||||
@ -232,25 +251,29 @@ const MainNav = ({
|
||||
activeIcon: 'i-ri-rocket-fill',
|
||||
}]
|
||||
: []),
|
||||
], [canUseAppDeploy, isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor, t])
|
||||
], [agentV2Enabled, canUseAppDeploy, isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor, t])
|
||||
|
||||
const renderLogo = () => (
|
||||
<Link
|
||||
href="/"
|
||||
className="flex h-8 shrink-0 items-center overflow-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
aria-label={systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'}
|
||||
>
|
||||
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
|
||||
? (
|
||||
<img
|
||||
src={systemFeatures.branding.workspace_logo}
|
||||
className="block h-5.5 w-auto object-contain"
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
: <DifyLogo alt="" />}
|
||||
</Link>
|
||||
)
|
||||
const renderLogo = () => {
|
||||
const appTitle = systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/"
|
||||
className="flex h-8 shrink-0 items-center overflow-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
aria-label={appTitle}
|
||||
>
|
||||
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
|
||||
? (
|
||||
<img
|
||||
src={systemFeatures.branding.workspace_logo}
|
||||
className="block h-5.5 w-auto object-contain"
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
: <DifyLogo alt="" />}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
@ -298,12 +321,19 @@ const MainNav = ({
|
||||
onToggle={handleToggleDetailNavigation}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<DeploymentDetailTop
|
||||
expand={detailNavigationVisibleExpanded}
|
||||
onToggle={handleToggleDetailNavigation}
|
||||
/>
|
||||
)
|
||||
: showAgentDetailNavigation
|
||||
? (
|
||||
<AgentDetailTop
|
||||
expand={detailNavigationVisibleExpanded}
|
||||
onToggle={handleToggleDetailNavigation}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<DeploymentDetailTop
|
||||
expand={detailNavigationVisibleExpanded}
|
||||
onToggle={handleToggleDetailNavigation}
|
||||
/>
|
||||
)
|
||||
: showSnippetDetailBottomNavigation
|
||||
? null
|
||||
: (
|
||||
@ -322,7 +352,9 @@ const MainNav = ({
|
||||
? <AppDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: showDatasetDetailNavigation
|
||||
? <DatasetDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: <DeploymentDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: showAgentDetailNavigation
|
||||
? <AgentDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: <DeploymentDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: showSnippetDetailBottomNavigation
|
||||
? null
|
||||
: (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, PluginPayload } from '../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../types'
|
||||
|
||||
@ -118,6 +118,24 @@ describe('AuthorizedInNode Component', () => {
|
||||
expect(screen.getByText('plugin.auth.workspaceDefault'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose the workspace default credential id when requested', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const onDefaultCredentialChange = vi.fn()
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode
|
||||
pluginPayload={pluginPayload}
|
||||
onAuthorizationItemClick={vi.fn()}
|
||||
onDefaultCredentialChange={onDefaultCredentialChange}
|
||||
/>,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onDefaultCredentialChange).toHaveBeenCalledWith('test-credential-id')
|
||||
})
|
||||
})
|
||||
|
||||
it('should render credential name when credentialId matches', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const credential = createCredential({ id: 'selected-id', name: 'My Credential' })
|
||||
|
||||
@ -10,6 +10,7 @@ import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -22,11 +23,13 @@ type AuthorizedInNodeProps = {
|
||||
pluginPayload: PluginPayload
|
||||
onAuthorizationItemClick: (id: string) => void
|
||||
credentialId?: string
|
||||
onDefaultCredentialChange?: (id?: string) => void
|
||||
}
|
||||
const AuthorizedInNode = ({
|
||||
pluginPayload,
|
||||
onAuthorizationItemClick,
|
||||
credentialId,
|
||||
onDefaultCredentialChange,
|
||||
}: AuthorizedInNodeProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@ -38,6 +41,12 @@ const AuthorizedInNode = ({
|
||||
invalidPluginCredentialInfo,
|
||||
notAllowCustomCredential,
|
||||
} = usePluginAuth(pluginPayload, true, credentialId ? [credentialId] : undefined)
|
||||
const defaultCredentialId = credentials.find(c => c.is_default)?.id
|
||||
|
||||
useEffect(() => {
|
||||
onDefaultCredentialChange?.(defaultCredentialId)
|
||||
}, [defaultCredentialId, onDefaultCredentialChange])
|
||||
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
let label = ''
|
||||
let removed = false
|
||||
@ -108,9 +117,15 @@ const AuthorizedInNode = ({
|
||||
},
|
||||
]
|
||||
const handleAuthorizationItemClick = useCallback((id: string) => {
|
||||
onAuthorizationItemClick(id)
|
||||
onAuthorizationItemClick(
|
||||
id === '__workspace_default__' && onDefaultCredentialChange
|
||||
? defaultCredentialId || id
|
||||
: id,
|
||||
)
|
||||
setIsOpen(false)
|
||||
}, [
|
||||
defaultCredentialId,
|
||||
onDefaultCredentialChange,
|
||||
onAuthorizationItemClick,
|
||||
setIsOpen,
|
||||
])
|
||||
|
||||
@ -3,7 +3,6 @@ import type { CredentialFormSchemaBase } from '../header/account-setting/model-p
|
||||
import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types'
|
||||
import type { TypeWithI18N } from '@/app/components/base/form/types'
|
||||
import type { Collection, ToolCredential } from '@/app/components/tools/types'
|
||||
import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
|
||||
export enum PluginCategoryEnum {
|
||||
@ -567,6 +566,12 @@ export type StrategyDetail = {
|
||||
features: AgentFeature[]
|
||||
}
|
||||
|
||||
const AgentFeature = {
|
||||
HISTORY_MESSAGES: 'history-messages',
|
||||
} as const
|
||||
|
||||
type AgentFeature = typeof AgentFeature[keyof typeof AgentFeature]
|
||||
|
||||
type Identity = {
|
||||
author: string
|
||||
name: string
|
||||
|
||||
@ -3,10 +3,16 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useAvailableNodesMetaData } from '../use-available-nodes-meta-data'
|
||||
|
||||
const mockIsAgentV2Enabled = vi.hoisted(() => vi.fn(() => true))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path?: string) => `https://docs.dify.ai${path || ''}`,
|
||||
}))
|
||||
|
||||
vi.mock('@/features/agent-v2/feature-flag', () => ({
|
||||
isAgentV2Enabled: () => mockIsAgentV2Enabled(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/constants/node', () => ({
|
||||
WORKFLOW_COMMON_NODES: [
|
||||
{
|
||||
@ -21,6 +27,14 @@ vi.mock('@/app/components/workflow/constants/node', () => ({
|
||||
metaData: { type: BlockEnum.HttpRequest },
|
||||
defaultValue: { title: 'HTTP Request' },
|
||||
},
|
||||
{
|
||||
metaData: { type: BlockEnum.Agent },
|
||||
defaultValue: { title: 'Agent' },
|
||||
},
|
||||
{
|
||||
metaData: { type: BlockEnum.AgentV2 },
|
||||
defaultValue: { title: 'Agent' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
@ -46,6 +60,11 @@ vi.mock('@/app/components/workflow/nodes/knowledge-base/default', () => ({
|
||||
}))
|
||||
|
||||
describe('useAvailableNodesMetaData', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsAgentV2Enabled.mockReturnValue(true)
|
||||
})
|
||||
|
||||
it('should return nodes and nodesMap', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
|
||||
@ -98,7 +117,7 @@ describe('useAvailableNodesMetaData', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
|
||||
result.current.nodes.forEach((node) => {
|
||||
expect(node.defaultValue.type).toBe(node.metaData.type)
|
||||
expect(node.defaultValue.type).toBe(node.metaData.type === BlockEnum.AgentV2 ? BlockEnum.Agent : node.metaData.type)
|
||||
expect(node.defaultValue.title).toBe(node.metaData.title)
|
||||
})
|
||||
})
|
||||
@ -127,4 +146,26 @@ describe('useAvailableNodesMetaData', () => {
|
||||
expect(nodeTypes).toContain(BlockEnum.HttpRequest)
|
||||
expect(nodeTypes).not.toContain(BlockEnum.HumanInput)
|
||||
})
|
||||
|
||||
it('should expose Agent v2 instead of legacy Agent when Agent v2 is enabled', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
const nodeTypes = result.current.nodes.map(n => n.metaData.type)
|
||||
|
||||
expect(nodeTypes).toContain(BlockEnum.AgentV2)
|
||||
expect(nodeTypes).not.toContain(BlockEnum.Agent)
|
||||
expect(result.current.nodesMap[BlockEnum.AgentV2]).toBeDefined()
|
||||
expect(result.current.nodesMap[BlockEnum.Agent]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should expose legacy Agent instead of Agent v2 when Agent v2 is disabled', () => {
|
||||
mockIsAgentV2Enabled.mockReturnValue(false)
|
||||
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
const nodeTypes = result.current.nodes.map(n => n.metaData.type)
|
||||
|
||||
expect(nodeTypes).toContain(BlockEnum.Agent)
|
||||
expect(nodeTypes).not.toContain(BlockEnum.AgentV2)
|
||||
expect(result.current.nodesMap[BlockEnum.Agent]).toBeDefined()
|
||||
expect(result.current.nodesMap[BlockEnum.AgentV2]).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
|
||||
@ -7,14 +8,21 @@ import dataSourceDefault from '@/app/components/workflow/nodes/data-source/defau
|
||||
import knowledgeBaseDefault from '@/app/components/workflow/nodes/knowledge-base/default'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
|
||||
|
||||
export const useAvailableNodesMetaData = () => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const agentV2Enabled = isAgentV2Enabled()
|
||||
|
||||
const mergedNodesMetaData = useMemo(() => [
|
||||
// RAG pipeline doesn't support human-input node temporarily
|
||||
...WORKFLOW_COMMON_NODES.filter(node => node.metaData.type !== BlockEnum.HumanInput),
|
||||
...WORKFLOW_COMMON_NODES.filter(node =>
|
||||
node.metaData.type !== BlockEnum.HumanInput
|
||||
&& (
|
||||
agentV2Enabled
|
||||
? node.metaData.type !== BlockEnum.Agent
|
||||
: node.metaData.type !== BlockEnum.AgentV2
|
||||
)),
|
||||
{
|
||||
...dataSourceDefault,
|
||||
defaultValue: {
|
||||
@ -24,7 +32,7 @@ export const useAvailableNodesMetaData = () => {
|
||||
},
|
||||
knowledgeBaseDefault,
|
||||
dataSourceEmptyDefault,
|
||||
], [])
|
||||
], [agentV2Enabled])
|
||||
|
||||
const helpLinkUri = useMemo(() => docLink(
|
||||
'/use-dify/knowledge/knowledge-pipeline/knowledge-pipeline-orchestration',
|
||||
@ -33,7 +41,7 @@ export const useAvailableNodesMetaData = () => {
|
||||
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
|
||||
const { metaData } = node
|
||||
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
|
||||
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
|
||||
const description = t(`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>, { ns: 'workflow' })
|
||||
return {
|
||||
...node,
|
||||
metaData: {
|
||||
@ -44,7 +52,7 @@ export const useAvailableNodesMetaData = () => {
|
||||
},
|
||||
defaultValue: {
|
||||
...node.defaultValue,
|
||||
type: metaData.type,
|
||||
type: metaData.type === BlockEnum.AgentV2 ? BlockEnum.Agent : metaData.type,
|
||||
title,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import type { IntegrationSection } from '@/app/components/integrations/routes'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
|
||||
@ -10,23 +11,35 @@ import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
type IntegrationsSettingModalProps = {
|
||||
section: IntegrationSection
|
||||
source?: 'agent'
|
||||
onCancel: () => void
|
||||
onSectionChange: (section: IntegrationSection) => void
|
||||
}
|
||||
|
||||
export default function IntegrationsSettingModal({
|
||||
section,
|
||||
source,
|
||||
onCancel,
|
||||
onSectionChange,
|
||||
}: IntegrationsSettingModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const isAgentSource = source === 'agent'
|
||||
const handleSwitchToMarketplace = useCallback((path: string) => {
|
||||
window.open(getMarketplaceUrl(path), '_blank', 'noopener,noreferrer')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MenuDialog show onClose={onCancel}>
|
||||
<div className="mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6">
|
||||
<MenuDialog
|
||||
show
|
||||
backdropClassName={isAgentSource ? 'bg-background-overlay' : undefined}
|
||||
className={isAgentSource ? 'bg-transparent backdrop-blur-none' : undefined}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<div className={cn(
|
||||
'mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6',
|
||||
isAgentSource && 'w-full p-6',
|
||||
)}
|
||||
>
|
||||
<div className="relative flex min-h-0 w-full shrink-0 overflow-hidden rounded-2xl border border-divider-subtle bg-components-panel-bg shadow-2xl">
|
||||
<div className="fixed top-6 right-6 z-9999 flex flex-col items-center">
|
||||
<Button
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type {
|
||||
PluginDefaultValue,
|
||||
BlockDefaultValue,
|
||||
TriggerDefaultValue,
|
||||
} from '@/app/components/workflow/block-selector/types'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
@ -98,7 +98,7 @@ const WorkflowChildren = () => {
|
||||
handleOnboardingClose()
|
||||
}, [handleOnboardingClose])
|
||||
|
||||
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
|
||||
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => {
|
||||
const nodeDefault = availableNodesMetaData.nodesMap?.[nodeType]
|
||||
if (!nodeDefault?.defaultValue)
|
||||
return
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { AppPublisherProps } from '@/app/components/app/app-publisher'
|
||||
import type { App } from '@/types/app'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import FeaturesTrigger from '../features-trigger'
|
||||
|
||||
const mockUseIsChatMode = vi.fn()
|
||||
@ -167,7 +169,15 @@ const createProviderContext = ({
|
||||
})
|
||||
|
||||
const renderWithToast = (ui: ReactElement) => {
|
||||
return render(ui)
|
||||
const queryClient = new QueryClient()
|
||||
return {
|
||||
queryClient,
|
||||
...render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ui}
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
describe('FeaturesTrigger', () => {
|
||||
@ -442,6 +452,72 @@ describe('FeaturesTrigger', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should invalidate roster list after publishing a workflow with a roster Agent v2 node', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
mockUseNodes.mockReturnValue([
|
||||
{ id: 'start', data: { type: BlockEnum.Start } },
|
||||
{
|
||||
id: 'agent-v2',
|
||||
data: {
|
||||
type: BlockEnum.AgentV2,
|
||||
version: '2',
|
||||
agent_node_kind: 'dify_agent',
|
||||
agent_binding: {
|
||||
binding_type: 'roster_agent',
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
const { queryClient } = renderWithToast(<FeaturesTrigger />)
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: 'publisher-publish' }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.agent.get.key(),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep roster list cache stable after publishing a workflow without roster Agent v2 nodes', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
mockUseNodes.mockReturnValue([
|
||||
{ id: 'start', data: { type: BlockEnum.Start } },
|
||||
{
|
||||
id: 'inline-agent-v2',
|
||||
data: {
|
||||
type: BlockEnum.AgentV2,
|
||||
version: '2',
|
||||
agent_node_kind: 'dify_agent',
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
agent_id: 'agent-1',
|
||||
current_snapshot_id: 'snapshot-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
const { queryClient } = renderWithToast(<FeaturesTrigger />)
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: 'publisher-publish' }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockPublishWorkflow).toHaveBeenCalled()
|
||||
})
|
||||
expect(invalidateQueries).not.toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.agent.get.key(),
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass publish params to workflow publish mutation', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
|
||||
@ -9,6 +9,7 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiApps2AddLine } from '@remixicon/react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
@ -28,6 +29,7 @@ import {
|
||||
useNodesSyncDraft,
|
||||
// useWorkflowRunValidation,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { hasValidRosterAgentBinding, isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/types'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
@ -41,6 +43,7 @@ import {
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useInvalidateAppTriggers } from '@/service/use-tools'
|
||||
import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
|
||||
@ -49,6 +52,7 @@ const FeaturesTrigger = () => {
|
||||
const { theme } = useTheme()
|
||||
const isChatMode = useIsChatMode()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const queryClient = useQueryClient()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appID = appDetail?.id
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
@ -166,6 +170,11 @@ const FeaturesTrigger = () => {
|
||||
updatePublishedWorkflow(appID!)
|
||||
updateAppDetail()
|
||||
invalidateAppTriggers(appID!)
|
||||
if (nodes.some(node => isAgentV2NodeData(node.data) && hasValidRosterAgentBinding(node.data))) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.get.key(),
|
||||
})
|
||||
}
|
||||
workflowStore.getState().setPublishedAt(res.created_at)
|
||||
workflowStore.getState().setLastPublishedHasUserInput(hasUserInputNode)
|
||||
resetWorkflowVersionHistory()
|
||||
@ -174,7 +183,7 @@ const FeaturesTrigger = () => {
|
||||
else {
|
||||
throw new Error('Checklist failed')
|
||||
}
|
||||
}, [needWarningNodes, handleCheckBeforePublish, publishWorkflow, appID, t, updatePublishedWorkflow, updateAppDetail, workflowStore, resetWorkflowVersionHistory, invalidateAppTriggers, hasUserInputNode])
|
||||
}, [needWarningNodes, handleCheckBeforePublish, publishWorkflow, appID, t, updatePublishedWorkflow, updateAppDetail, invalidateAppTriggers, nodes, queryClient, workflowStore, hasUserInputNode, resetWorkflowVersionHistory])
|
||||
|
||||
const onPublisherToggle = useCallback((state: boolean) => {
|
||||
if (state)
|
||||
|
||||