Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8ab27c5ee | ||
|
|
fadf3acc94 | ||
|
|
2da773e8d8 | ||
|
|
e3e7ff26a5 | ||
|
|
1bd9cf8cc6 | ||
|
|
33ef76c5b8 | ||
|
|
56a026505e | ||
|
|
dcc0b95e11 | ||
|
|
f7c90d4873 | ||
|
|
28cc739a93 | ||
|
|
4350617694 | ||
|
|
431d6bb983 | ||
|
|
fe64c5d4a8 | ||
|
|
f9ed81c3f4 |
@@ -53,14 +53,20 @@ class AgentIdPath(BaseModel):
|
||||
class AgentAppCreatePayload(BaseModel):
|
||||
name: str = Field(..., min_length=1, description="Agent name")
|
||||
description: str | None = Field(default=None, description="Agent description (max 400 chars)", max_length=400)
|
||||
role: str = Field(default="", description="Agent role", max_length=255)
|
||||
icon_type: IconType | None = Field(default=None, description="Icon type")
|
||||
icon: str | None = Field(default=None, description="Icon")
|
||||
icon_background: str | None = Field(default=None, description="Icon background color")
|
||||
|
||||
|
||||
class AgentAppUpdatePayload(UpdateAppPayload):
|
||||
role: str | None = Field(default=None, description="Agent role", max_length=255)
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
AgentAppCreatePayload,
|
||||
AgentAppUpdatePayload,
|
||||
AgentInviteOptionsQuery,
|
||||
AgentIdPath,
|
||||
AppListQuery,
|
||||
@@ -89,20 +95,32 @@ def _serialize_agent_app_detail(app_model) -> dict:
|
||||
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]
|
||||
|
||||
payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json")
|
||||
agent_id = payload.pop("bound_agent_id", None)
|
||||
if not agent_id:
|
||||
agent = _agent_roster_service().get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=app_model.id)
|
||||
if not agent:
|
||||
raise AgentNotFoundError()
|
||||
payload["id"] = agent_id
|
||||
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
|
||||
payload["role"] = agent.role or ""
|
||||
return payload
|
||||
|
||||
|
||||
def _serialize_agent_app_pagination(app_pagination) -> dict:
|
||||
def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
|
||||
app_ids = [str(app.id) for app in app_pagination.items]
|
||||
agents_by_app_id = _agent_roster_service().load_app_backing_agents_by_app_id(
|
||||
tenant_id=tenant_id,
|
||||
app_ids=app_ids,
|
||||
)
|
||||
payload = AppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json")
|
||||
for item in payload["data"]:
|
||||
agent_id = item.pop("bound_agent_id", None)
|
||||
if agent_id:
|
||||
item["id"] = agent_id
|
||||
app_id = item["id"]
|
||||
item.pop("bound_agent_id", None)
|
||||
agent = agents_by_app_id.get(app_id)
|
||||
if agent:
|
||||
item["app_id"] = app_id
|
||||
item["id"] = agent.id
|
||||
item["role"] = agent.role or ""
|
||||
return payload
|
||||
|
||||
|
||||
@@ -137,7 +155,7 @@ class AgentAppListApi(Resource):
|
||||
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
|
||||
return empty.model_dump(mode="json")
|
||||
|
||||
return _serialize_agent_app_pagination(app_pagination)
|
||||
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__])
|
||||
@@ -156,6 +174,7 @@ class AgentAppListApi(Resource):
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
mode="agent",
|
||||
agent_role=args.role,
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
@@ -177,7 +196,7 @@ class AgentAppApi(Resource):
|
||||
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
|
||||
return _serialize_agent_app_detail(app_model)
|
||||
|
||||
@console_ns.expect(console_ns.models[UpdateAppPayload.__name__])
|
||||
@console_ns.expect(console_ns.models[AgentAppUpdatePayload.__name__])
|
||||
@console_ns.response(200, "Agent app updated successfully", console_ns.models[AppDetailWithSite.__name__])
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@console_ns.response(400, "Invalid request parameters")
|
||||
@@ -188,7 +207,7 @@ class AgentAppApi(Resource):
|
||||
@with_current_tenant_id
|
||||
def put(self, tenant_id: str, agent_id: UUID):
|
||||
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
|
||||
args = UpdateAppPayload.model_validate(console_ns.payload)
|
||||
args = AgentAppUpdatePayload.model_validate(console_ns.payload)
|
||||
args_dict: AppService.ArgsDict = {
|
||||
"name": args.name,
|
||||
"description": args.description or "",
|
||||
@@ -197,6 +216,7 @@ class AgentAppApi(Resource):
|
||||
"icon_background": args.icon_background or "",
|
||||
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
|
||||
"max_active_requests": args.max_active_requests or 0,
|
||||
"role": args.role,
|
||||
}
|
||||
updated = AppService().update_app(app_model, args_dict)
|
||||
return _serialize_agent_app_detail(updated)
|
||||
|
||||
@@ -400,6 +400,9 @@ class AppPartial(ResponseModel):
|
||||
has_draft_trigger: bool | None = None
|
||||
# For Agent App type: the roster Agent backing this app (None otherwise).
|
||||
bound_agent_id: str | None = None
|
||||
# For Agent App responses exposed through /agent.
|
||||
app_id: str | None = None
|
||||
role: str | None = None
|
||||
is_starred: bool = False
|
||||
|
||||
@computed_field(return_type=str | None) # type: ignore
|
||||
@@ -451,6 +454,9 @@ class AppDetailWithSite(AppDetail):
|
||||
site: Site | None = None
|
||||
# For Agent App type: the roster Agent backing this app (None otherwise).
|
||||
bound_agent_id: str | None = None
|
||||
# For Agent App responses exposed through /agent.
|
||||
app_id: str | None = None
|
||||
role: str | None = None
|
||||
|
||||
@computed_field(return_type=str | None) # type: ignore
|
||||
@property
|
||||
|
||||
@@ -246,6 +246,8 @@ class AgentAppGenerator(MessageBasedAppGenerator):
|
||||
"conversation_id": conversation.id,
|
||||
"message_id": message.id,
|
||||
"user_from": UserFrom.ACCOUNT if isinstance(user, Account) else UserFrom.END_USER,
|
||||
# Resume continues a paused agent run; skip input guards (see _generate_worker).
|
||||
"is_resume": True,
|
||||
},
|
||||
)
|
||||
worker_thread.start()
|
||||
@@ -270,6 +272,7 @@ class AgentAppGenerator(MessageBasedAppGenerator):
|
||||
conversation_id: str,
|
||||
message_id: str,
|
||||
user_from: UserFrom,
|
||||
is_resume: bool = False,
|
||||
) -> None:
|
||||
from libs.flask_utils import preserve_flask_contexts
|
||||
|
||||
@@ -279,20 +282,30 @@ class AgentAppGenerator(MessageBasedAppGenerator):
|
||||
message = self._get_message(message_id)
|
||||
app_config = application_generate_entity.app_config
|
||||
|
||||
# Apply app-level input guards (content moderation + annotation
|
||||
# reply) before reaching the Agent backend, mirroring the EasyUI
|
||||
# chat / agent-chat runners. These can short-circuit the turn.
|
||||
app_model = db.session.get(App, app_config.app_id)
|
||||
if app_model is None:
|
||||
raise AgentAppGeneratorError("App not found")
|
||||
handled, query = self._run_input_guards(
|
||||
application_generate_entity=application_generate_entity,
|
||||
app_model=app_model,
|
||||
message=message,
|
||||
queue_manager=queue_manager,
|
||||
)
|
||||
if handled:
|
||||
return
|
||||
if is_resume:
|
||||
# ENG-638: a resume continues a paused agent run; the human's
|
||||
# reply is threaded in by the runner as deferred_tool_results.
|
||||
# The query is the replayed paused-turn message, kept only to
|
||||
# match the suspended snapshot's layers — it is NOT new
|
||||
# end-user input, so input guards must NOT run. Moderation or an
|
||||
# annotation match on the replayed query would short-circuit the
|
||||
# turn and drop the human reply, stranding the ask_human session.
|
||||
query = application_generate_entity.query or ""
|
||||
else:
|
||||
# Apply app-level input guards (content moderation + annotation
|
||||
# reply) before reaching the Agent backend, mirroring the EasyUI
|
||||
# chat / agent-chat runners. These can short-circuit the turn.
|
||||
app_model = db.session.get(App, app_config.app_id)
|
||||
if app_model is None:
|
||||
raise AgentAppGeneratorError("App not found")
|
||||
handled, query = self._run_input_guards(
|
||||
application_generate_entity=application_generate_entity,
|
||||
app_model=app_model,
|
||||
message=message,
|
||||
queue_manager=queue_manager,
|
||||
)
|
||||
if handled:
|
||||
return
|
||||
|
||||
dify_context = DifyRunContext(
|
||||
tenant_id=app_config.tenant_id,
|
||||
|
||||
@@ -118,16 +118,18 @@ class WaterCrawlAPIClient(BaseAPIClient):
|
||||
response.raise_for_status()
|
||||
if response.status_code == 204:
|
||||
return None
|
||||
if response.headers.get("Content-Type") == "application/json":
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
media_type = content_type.split(";", 1)[0].strip().lower()
|
||||
if media_type == "application/json":
|
||||
return response.json() or {}
|
||||
|
||||
if response.headers.get("Content-Type") == "application/octet-stream":
|
||||
if media_type == "application/octet-stream":
|
||||
return response.content
|
||||
|
||||
if response.headers.get("Content-Type") == "text/event-stream":
|
||||
if media_type == "text/event-stream":
|
||||
return self.process_eventstream(response)
|
||||
|
||||
raise Exception(f"Unknown response type: {response.headers.get('Content-Type')}")
|
||||
raise Exception(f"Unknown response type: {content_type}")
|
||||
|
||||
def get_crawl_requests_list(self, page: int | None = None, page_size: int | None = None):
|
||||
query_params = {"page": page or 1, "page_size": page_size or 10}
|
||||
@@ -217,7 +219,7 @@ class WaterCrawlAPIClient(BaseAPIClient):
|
||||
return event_data["data"]
|
||||
|
||||
def download_result(self, result_object: dict[str, Any]):
|
||||
response = httpx.get(result_object["result"], timeout=None)
|
||||
response = httpx.get(result_object["result"], timeout=30)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
result_object["result"] = response.json()
|
||||
|
||||
@@ -382,7 +382,7 @@ Check if activation token is valid
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [UpdateAppPayload](#updateapppayload)<br> |
|
||||
| Yes | **application/json**: [AgentAppUpdatePayload](#agentappupdatepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
@@ -11286,6 +11286,7 @@ Default namespace
|
||||
| icon_background | string | Icon background color | No |
|
||||
| icon_type | [IconType](#icontype) | Icon type | No |
|
||||
| name | string | Agent name | Yes |
|
||||
| role | string | Agent role | No |
|
||||
|
||||
#### AgentAppFeaturesPayload
|
||||
|
||||
@@ -11304,6 +11305,19 @@ default (the config form sends the full desired feature state on save).
|
||||
| suggested_questions_after_answer | [AgentSuggestedQuestionsAfterAnswerFeatureConfig](#agentsuggestedquestionsafteranswerfeatureconfig) | Follow-up suggestions config, e.g. {'enabled': true} | No |
|
||||
| text_to_speech | [AgentTextToSpeechFeatureConfig](#agenttexttospeechfeatureconfig) | Text-to-speech config | No |
|
||||
|
||||
#### AgentAppUpdatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| description | string | App description (max 400 chars) | No |
|
||||
| icon | string | Icon | No |
|
||||
| icon_background | string | Icon background color | No |
|
||||
| icon_type | [IconType](#icontype) | Icon type | No |
|
||||
| max_active_requests | integer | Maximum active requests | No |
|
||||
| name | string | App name | Yes |
|
||||
| role | string | Agent role | No |
|
||||
| use_icon_as_answer_icon | boolean | Use icon as answer icon | No |
|
||||
|
||||
#### AgentCliToolAuthorizationStatus
|
||||
|
||||
Authorization state for Agent-scoped CLI tools.
|
||||
@@ -12502,6 +12516,7 @@ Enum class for api provider schema type.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| access_mode | string | | No |
|
||||
| api_base_url | string | | No |
|
||||
| app_id | string | | No |
|
||||
| bound_agent_id | string | | No |
|
||||
| created_at | integer | | No |
|
||||
| created_by | string | | No |
|
||||
@@ -12518,6 +12533,7 @@ 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 |
|
||||
@@ -12622,6 +12638,7 @@ AppMCPServer Status Enum
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| access_mode | string | | No |
|
||||
| app_id | string | | No |
|
||||
| author_name | string | | No |
|
||||
| bound_agent_id | string | | No |
|
||||
| create_user_name | string | | No |
|
||||
@@ -12639,6 +12656,7 @@ AppMCPServer Status Enum
|
||||
| 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 |
|
||||
|
||||
@@ -282,6 +282,7 @@ class AgentRosterService:
|
||||
app_id: str,
|
||||
name: str,
|
||||
description: str = "",
|
||||
role: str = "",
|
||||
icon_type: Any = None,
|
||||
icon: str | None = None,
|
||||
icon_background: str | None = None,
|
||||
@@ -298,7 +299,7 @@ class AgentRosterService:
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
description=description,
|
||||
role="",
|
||||
role=role,
|
||||
icon_type=icon_type,
|
||||
icon=icon,
|
||||
icon_background=icon_background,
|
||||
@@ -341,6 +342,21 @@ class AgentRosterService:
|
||||
self._session.flush()
|
||||
return agent
|
||||
|
||||
def load_app_backing_agents_by_app_id(self, *, tenant_id: str, app_ids: list[str]) -> dict[str, Agent]:
|
||||
"""Return active app-backed Agents keyed by Agent App id."""
|
||||
if not app_ids:
|
||||
return {}
|
||||
agents = self._session.scalars(
|
||||
select(Agent).where(
|
||||
Agent.tenant_id == tenant_id,
|
||||
Agent.app_id.in_(app_ids),
|
||||
Agent.scope == AgentScope.ROSTER,
|
||||
Agent.source == AgentSource.AGENT_APP,
|
||||
Agent.status == AgentStatus.ACTIVE,
|
||||
)
|
||||
).all()
|
||||
return {agent.app_id: agent for agent in agents if agent.app_id}
|
||||
|
||||
def get_app_backing_agent(self, *, tenant_id: str, app_id: str) -> Agent | None:
|
||||
"""Return the roster Agent that backs the given Agent App, if any."""
|
||||
return self._session.scalar(
|
||||
@@ -444,12 +460,36 @@ class AgentRosterService:
|
||||
agent.updated_by = account_id
|
||||
self._session.commit()
|
||||
|
||||
@staticmethod
|
||||
def _visible_version_operations(agent: Agent) -> set[AgentConfigRevisionOperation]:
|
||||
if agent.source == AgentSource.AGENT_APP:
|
||||
return {AgentConfigRevisionOperation.SAVE_NEW_VERSION}
|
||||
return {
|
||||
AgentConfigRevisionOperation.CREATE_VERSION,
|
||||
AgentConfigRevisionOperation.SAVE_NEW_VERSION,
|
||||
AgentConfigRevisionOperation.SAVE_NEW_AGENT,
|
||||
AgentConfigRevisionOperation.SAVE_TO_ROSTER,
|
||||
}
|
||||
|
||||
def list_agent_versions(self, *, tenant_id: str, agent_id: str) -> list[dict[str, Any]]:
|
||||
self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
|
||||
agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
|
||||
visible_version_ids = (
|
||||
select(AgentConfigRevision.current_snapshot_id)
|
||||
.where(
|
||||
AgentConfigRevision.tenant_id == tenant_id,
|
||||
AgentConfigRevision.agent_id == agent_id,
|
||||
AgentConfigRevision.operation.in_(self._visible_version_operations(agent)),
|
||||
)
|
||||
.subquery()
|
||||
)
|
||||
versions = list(
|
||||
self._session.scalars(
|
||||
select(AgentConfigSnapshot)
|
||||
.where(AgentConfigSnapshot.tenant_id == tenant_id, AgentConfigSnapshot.agent_id == agent_id)
|
||||
.where(
|
||||
AgentConfigSnapshot.tenant_id == tenant_id,
|
||||
AgentConfigSnapshot.agent_id == agent_id,
|
||||
AgentConfigSnapshot.id.in_(select(visible_version_ids.c.current_snapshot_id)),
|
||||
)
|
||||
.order_by(AgentConfigSnapshot.version.desc())
|
||||
).all()
|
||||
)
|
||||
@@ -460,7 +500,19 @@ class AgentRosterService:
|
||||
]
|
||||
|
||||
def get_agent_version_detail(self, *, tenant_id: str, agent_id: str, version_id: str) -> dict[str, Any]:
|
||||
self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
|
||||
agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
|
||||
visible_revision_id = self._session.scalar(
|
||||
select(AgentConfigRevision.id)
|
||||
.where(
|
||||
AgentConfigRevision.tenant_id == tenant_id,
|
||||
AgentConfigRevision.agent_id == agent_id,
|
||||
AgentConfigRevision.current_snapshot_id == version_id,
|
||||
AgentConfigRevision.operation.in_(self._visible_version_operations(agent)),
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
if not visible_revision_id:
|
||||
raise AgentVersionNotFoundError()
|
||||
version = self._get_version(tenant_id=tenant_id, agent_id=agent_id, version_id=version_id)
|
||||
revisions = list(
|
||||
self._session.scalars(
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal, TypedDict, cast, override
|
||||
from typing import Any, Literal, NotRequired, TypedDict, cast, override
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask_sqlalchemy.pagination import Pagination
|
||||
@@ -63,6 +63,7 @@ class CreateAppParams(BaseModel):
|
||||
name: str = Field(min_length=1)
|
||||
description: str | None = None
|
||||
mode: Literal["chat", "agent-chat", "agent", "advanced-chat", "workflow", "completion"]
|
||||
agent_role: str = Field(default="", max_length=255)
|
||||
icon_type: str | None = None
|
||||
icon: str | None = None
|
||||
icon_background: str | None = None
|
||||
@@ -90,6 +91,8 @@ class AppService:
|
||||
filters.append(App.mode == AppMode.AGENT_CHAT)
|
||||
elif params.mode == "agent":
|
||||
filters.append(App.mode == AppMode.AGENT)
|
||||
elif params.mode == "all":
|
||||
filters.append(App.mode != AppMode.AGENT)
|
||||
|
||||
if isinstance(params, AppListParams):
|
||||
if params.status:
|
||||
@@ -412,6 +415,7 @@ class AppService:
|
||||
app_id=app.id,
|
||||
name=params.name,
|
||||
description=params.description or "",
|
||||
role=params.agent_role,
|
||||
icon_type=icon_type,
|
||||
icon=params.icon,
|
||||
icon_background=params.icon_background,
|
||||
@@ -507,6 +511,7 @@ class AppService:
|
||||
icon_background: str
|
||||
use_icon_as_answer_icon: bool
|
||||
max_active_requests: int
|
||||
role: NotRequired[str | None]
|
||||
|
||||
@staticmethod
|
||||
def _get_backing_agent_for_update(app: App) -> Agent | None:
|
||||
@@ -538,6 +543,7 @@ class AppService:
|
||||
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:
|
||||
@@ -560,6 +566,8 @@ class AppService:
|
||||
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
|
||||
@@ -594,6 +602,7 @@ class AppService:
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -43,10 +43,16 @@ class JinaAuth(ApiKeyAuthBase):
|
||||
|
||||
def _handle_error(self, response):
|
||||
if response.status_code in {402, 409, 500}:
|
||||
error_message = response.json().get("error", "Unknown error occurred")
|
||||
try:
|
||||
error_message = response.json().get("error", "Unknown error occurred")
|
||||
except ValueError:
|
||||
error_message = response.text or "Unknown error occurred"
|
||||
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
|
||||
else:
|
||||
if response.text:
|
||||
error_message = json.loads(response.text).get("error", "Unknown error occurred")
|
||||
try:
|
||||
error_message = json.loads(response.text).get("error", "Unknown error occurred")
|
||||
except ValueError:
|
||||
error_message = response.text
|
||||
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
|
||||
raise Exception(f"Unexpected error occurred while trying to authorize. Status code: {response.status_code}")
|
||||
|
||||
@@ -43,10 +43,16 @@ class JinaAuth(ApiKeyAuthBase):
|
||||
|
||||
def _handle_error(self, response):
|
||||
if response.status_code in {402, 409, 500}:
|
||||
error_message = response.json().get("error", "Unknown error occurred")
|
||||
try:
|
||||
error_message = response.json().get("error", "Unknown error occurred")
|
||||
except ValueError:
|
||||
error_message = response.text or "Unknown error occurred"
|
||||
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
|
||||
else:
|
||||
if response.text:
|
||||
error_message = json.loads(response.text).get("error", "Unknown error occurred")
|
||||
try:
|
||||
error_message = json.loads(response.text).get("error", "Unknown error occurred")
|
||||
except ValueError:
|
||||
error_message = response.text
|
||||
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
|
||||
raise Exception(f"Unexpected error occurred while trying to authorize. Status code: {response.status_code}")
|
||||
|
||||
@@ -37,10 +37,16 @@ class WatercrawlAuth(ApiKeyAuthBase):
|
||||
|
||||
def _handle_error(self, response):
|
||||
if response.status_code in {402, 409, 500}:
|
||||
error_message = response.json().get("error", "Unknown error occurred")
|
||||
try:
|
||||
error_message = response.json().get("error", "Unknown error occurred")
|
||||
except ValueError:
|
||||
error_message = response.text or "Unknown error occurred"
|
||||
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
|
||||
else:
|
||||
if response.text:
|
||||
error_message = json.loads(response.text).get("error", "Unknown error occurred")
|
||||
try:
|
||||
error_message = json.loads(response.text).get("error", "Unknown error occurred")
|
||||
except ValueError:
|
||||
error_message = response.text
|
||||
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
|
||||
raise Exception(f"Unexpected error occurred while trying to authorize. Status code: {response.status_code}")
|
||||
|
||||
@@ -95,16 +95,30 @@ def check_and_handle_human_input_timeouts(limit: int = 100) -> None:
|
||||
timeout_status=HumanInputFormStatus.EXPIRED if is_global else HumanInputFormStatus.TIMEOUT,
|
||||
reason="global_timeout" if is_global else "node_timeout",
|
||||
)
|
||||
assert record.workflow_run_id is not None, "workflow_run_id should not be None for non-test form"
|
||||
if is_global:
|
||||
# Global timeout applies only to workflow-owned forms
|
||||
# (_is_global_timeout requires a workflow_run_id): end the run.
|
||||
assert record.workflow_run_id is not None, "global timeout requires a workflow_run_id"
|
||||
_handle_global_timeout(
|
||||
form_id=record.form_id,
|
||||
workflow_run_id=record.workflow_run_id,
|
||||
node_id=record.node_id,
|
||||
session_factory=session_factory,
|
||||
)
|
||||
else:
|
||||
elif record.workflow_run_id is not None:
|
||||
# Workflow Agent node / Human Input node form: resume the workflow.
|
||||
service.enqueue_resume(record.workflow_run_id)
|
||||
elif record.conversation_id is not None:
|
||||
# ENG-635: Agent v2 chat ask_human form is conversation-owned (no
|
||||
# workflow_run_id). Resume the chat turn so the timeout is threaded
|
||||
# back to the agent run as the ask_human deferred_tool_result
|
||||
# (status="timeout"), mirroring HumanInputService.submit_form_by_token.
|
||||
service.enqueue_agent_app_resume(conversation_id=record.conversation_id, form_id=record.form_id)
|
||||
else:
|
||||
logger.warning(
|
||||
"Timed-out form %s has neither workflow_run_id nor conversation_id; skipping resume",
|
||||
record.form_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to handle timeout for form_id=%s workflow_run_id=%s",
|
||||
|
||||
@@ -113,6 +113,7 @@ 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)
|
||||
@@ -195,6 +196,16 @@ def test_agent_app_list_and_create_use_agent_route(
|
||||
return _app_detail_obj(id="app-created", bound_agent_id="agent-created")
|
||||
|
||||
monkeypatch.setattr(roster_controller, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"load_app_backing_agents_by_app_id",
|
||||
lambda _self, **kwargs: {"app-list": SimpleNamespace(id="agent-list", role="List role")},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"get_app_backing_agent",
|
||||
lambda _self, **kwargs: SimpleNamespace(id="agent-created", role="Created role"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.FeatureService,
|
||||
"get_system_features",
|
||||
@@ -208,6 +219,8 @@ def test_agent_app_list_and_create_use_agent_route(
|
||||
assert listed["limit"] == 10
|
||||
assert listed["total"] == 1
|
||||
assert listed["data"][0]["id"] == "agent-list"
|
||||
assert listed["data"][0]["app_id"] == "app-list"
|
||||
assert listed["data"][0]["role"] == "List role"
|
||||
assert "bound_agent_id" not in listed["data"][0]
|
||||
list_call = cast(dict[str, object], captured["list"])
|
||||
list_params = cast(Any, list_call["params"])
|
||||
@@ -222,6 +235,8 @@ def test_agent_app_list_and_create_use_agent_route(
|
||||
|
||||
assert status == 201
|
||||
assert created["id"] == "agent-created"
|
||||
assert created["app_id"] == "app-created"
|
||||
assert created["role"] == "Created role"
|
||||
assert "bound_agent_id" not in created
|
||||
create_call = cast(dict[str, object], captured["create"])
|
||||
create_params = cast(Any, create_call["params"])
|
||||
@@ -240,6 +255,11 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
|
||||
"get_agent_app_model",
|
||||
lambda _self, **kwargs: app_model,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"get_app_backing_agent",
|
||||
lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="Resolved role"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.FeatureService,
|
||||
"get_system_features",
|
||||
@@ -262,6 +282,8 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
|
||||
|
||||
detail = unwrap(AgentAppApi.get)(AgentAppApi(), "tenant-1", agent_id)
|
||||
assert detail["id"] == agent_id
|
||||
assert detail["app_id"] == "app-1"
|
||||
assert detail["role"] == "Resolved role"
|
||||
assert "bound_agent_id" not in detail
|
||||
|
||||
with app.test_request_context(
|
||||
@@ -272,6 +294,8 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
|
||||
|
||||
assert updated["name"] == "Renamed"
|
||||
assert updated["id"] == agent_id
|
||||
assert updated["app_id"] == "app-1"
|
||||
assert updated["role"] == "Resolved role"
|
||||
assert "bound_agent_id" not in updated
|
||||
update_call = cast(dict[str, object], captured["update"])
|
||||
assert update_call["app"] is app_model
|
||||
|
||||
@@ -201,17 +201,18 @@ class TestGenerateWorker:
|
||||
mocker.patch(f"{MODULE}.AgentAppRunner", return_value=runner)
|
||||
return runner
|
||||
|
||||
def _call(self, generator, mocker: MockerFixture, queue_manager):
|
||||
def _call(self, generator, mocker: MockerFixture, queue_manager, *, is_resume=False, query="query"):
|
||||
generator._generate_worker(
|
||||
flask_app=mocker.MagicMock(),
|
||||
context=mocker.MagicMock(),
|
||||
application_generate_entity=mocker.MagicMock(
|
||||
agent_id="a", agent_config_snapshot_id="s", model_conf=mocker.MagicMock(model="m")
|
||||
agent_id="a", agent_config_snapshot_id="s", model_conf=mocker.MagicMock(model="m"), query=query
|
||||
),
|
||||
queue_manager=queue_manager,
|
||||
conversation_id="conv",
|
||||
message_id="msg",
|
||||
user_from=UserFrom.END_USER,
|
||||
is_resume=is_resume,
|
||||
)
|
||||
|
||||
def test_happy_path_runs_backend(self, generator, mocker: MockerFixture):
|
||||
@@ -227,6 +228,20 @@ class TestGenerateWorker:
|
||||
self._call(generator, mocker, queue_manager)
|
||||
runner.run.assert_not_called()
|
||||
|
||||
def test_resume_skips_input_guards_and_consumes_reply(self, generator, mocker: MockerFixture):
|
||||
# ENG-638 (review): on resume the replayed query is NOT new end-user input.
|
||||
# Input guards must be skipped, even if moderation/annotation would match,
|
||||
# so the run continues and the human reply (deferred_tool_results) is used.
|
||||
runner = self._wire(generator, mocker, handled=True) # guards WOULD short-circuit
|
||||
queue_manager = mocker.MagicMock()
|
||||
|
||||
self._call(generator, mocker, queue_manager, is_resume=True, query="the approved reply")
|
||||
|
||||
generator._run_input_guards.assert_not_called()
|
||||
runner.run.assert_called_once()
|
||||
# the replayed paused-turn query flows straight to the runner (snapshot match)
|
||||
assert runner.run.call_args.kwargs["query"] == "the approved reply"
|
||||
|
||||
def test_generate_task_stopped_is_swallowed(self, generator, mocker: MockerFixture):
|
||||
self._wire(generator, mocker, run_side_effect=GenerateTaskStoppedError())
|
||||
queue_manager = mocker.MagicMock()
|
||||
|
||||
@@ -168,6 +168,13 @@ class TestWaterCrawlAPIClient:
|
||||
assert client.process_response(_response(200, {"ok": True})) == {"ok": True}
|
||||
assert client.process_response(_response(200, None)) == {}
|
||||
|
||||
def test_process_response_accepts_json_content_type_parameters(self):
|
||||
client = WaterCrawlAPIClient(api_key="k")
|
||||
|
||||
response = _response(200, {"ok": True}, content_type="application/json; charset=utf-8")
|
||||
|
||||
assert client.process_response(response) == {"ok": True}
|
||||
|
||||
def test_process_response_octet_stream_returns_bytes(self):
|
||||
client = WaterCrawlAPIClient(api_key="k")
|
||||
assert (
|
||||
@@ -242,11 +249,18 @@ class TestWaterCrawlAPIClient:
|
||||
client = WaterCrawlAPIClient(api_key="k")
|
||||
|
||||
response = _response(200, {"markdown": "body"})
|
||||
monkeypatch.setattr(client_module.httpx, "get", lambda *args, **kwargs: response)
|
||||
captured = {}
|
||||
|
||||
def fake_get(*args, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return response
|
||||
|
||||
monkeypatch.setattr(client_module.httpx, "get", fake_get)
|
||||
|
||||
result = client.download_result({"result": "https://example.com/result.json"})
|
||||
|
||||
assert result["result"] == {"markdown": "body"}
|
||||
assert captured["timeout"] is not None
|
||||
response.close.assert_called_once()
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from services.agent.composer_validator import ComposerConfigValidator
|
||||
from services.agent.errors import InvalidComposerConfigError
|
||||
from services.agent.roster_service import AgentRosterService
|
||||
from services.agent.workflow_publish_service import WorkflowAgentPublishService
|
||||
from services.app_service import AppListParams, AppService
|
||||
from services.entities.agent_entities import AgentSoulConfig, ComposerSavePayload, ComposerSaveStrategy, ComposerVariant
|
||||
|
||||
|
||||
@@ -769,7 +770,7 @@ def test_roster_update_archive_versions_and_detail(monkeypatch):
|
||||
created_by="account-1",
|
||||
created_at=revision_created_at,
|
||||
)
|
||||
fake_session = FakeSession(scalars=[[listed_version], [revision]])
|
||||
fake_session = FakeSession(scalar=["visible-revision"], scalars=[[listed_version], [revision]])
|
||||
agent = Agent(
|
||||
id="agent-1",
|
||||
tenant_id="tenant-1",
|
||||
@@ -825,6 +826,7 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
|
||||
payload = roster_service.RosterAgentCreatePayload(
|
||||
name="Analyst",
|
||||
description="desc",
|
||||
role="Research assistant",
|
||||
icon_type="emoji",
|
||||
icon="A",
|
||||
icon_background="#fff",
|
||||
@@ -838,6 +840,7 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
|
||||
account_id="account-1",
|
||||
app_id="app-1",
|
||||
name="Backing Agent",
|
||||
role="Support agent",
|
||||
)
|
||||
found_agent = service._get_agent(tenant_id="tenant-1", agent_id="agent-1")
|
||||
with pytest.raises(roster_service.AgentNotFoundError):
|
||||
@@ -849,9 +852,11 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
|
||||
assert service._load_versions_by_id([]) == {}
|
||||
|
||||
assert created.name == "Analyst"
|
||||
assert created.role == "Research assistant"
|
||||
assert created.source == AgentSource.ROSTER
|
||||
assert created.active_config_snapshot_id is not None
|
||||
assert created.active_config_has_model is False
|
||||
assert backing_agent.role == "Support agent"
|
||||
assert backing_agent.active_config_snapshot_id is not None
|
||||
assert backing_agent.active_config_has_model is False
|
||||
assert found_agent.id == "agent-1"
|
||||
@@ -859,6 +864,30 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
|
||||
assert loaded_versions["version-1"].agent_id == "agent-1"
|
||||
|
||||
|
||||
def test_agent_app_visible_versions_exclude_draft_saves():
|
||||
agent_app = Agent(source=AgentSource.AGENT_APP)
|
||||
roster_agent = Agent(source=AgentSource.ROSTER)
|
||||
|
||||
agent_app_operations = AgentRosterService._visible_version_operations(agent_app)
|
||||
roster_operations = AgentRosterService._visible_version_operations(roster_agent)
|
||||
|
||||
assert agent_app_operations == {AgentConfigRevisionOperation.SAVE_NEW_VERSION}
|
||||
assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in agent_app_operations
|
||||
assert AgentConfigRevisionOperation.CREATE_VERSION in roster_operations
|
||||
assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in roster_operations
|
||||
|
||||
|
||||
def test_app_list_all_excludes_agent_apps_by_default():
|
||||
filters = AppService._build_app_list_filters(
|
||||
"account-1",
|
||||
"tenant-1",
|
||||
AppListParams(mode="all"),
|
||||
)
|
||||
sql = " ".join(str(filter_) for filter_ in filters)
|
||||
|
||||
assert "apps.mode != :mode_1" in sql
|
||||
|
||||
|
||||
def test_validator_dict_helpers_wrap_validation_errors():
|
||||
valid_soul = ComposerConfigValidator.validate_agent_soul_dict({"prompt": {"system_prompt": "x"}})
|
||||
valid_node_job = ComposerConfigValidator.validate_node_job_dict({"workflow_prompt": "x"})
|
||||
|
||||
@@ -68,6 +68,22 @@ class TestJinaAuth:
|
||||
auth.validate_credentials()
|
||||
assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required"
|
||||
|
||||
@patch("services.auth.jina.jina._http_client.post", autospec=True)
|
||||
def test_should_handle_http_error_with_non_json_text_response(self, mock_post):
|
||||
"""Test handling of known HTTP errors with non-JSON text response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 402
|
||||
mock_response.text = "Payment required"
|
||||
mock_response.json.side_effect = ValueError("Not JSON")
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}}
|
||||
auth = JinaAuth(credentials)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
auth.validate_credentials()
|
||||
assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required"
|
||||
|
||||
@patch("services.auth.jina.jina._http_client.post", autospec=True)
|
||||
def test_should_handle_http_409_error(self, mock_post):
|
||||
"""Test handling of 409 Conflict error"""
|
||||
@@ -114,6 +130,22 @@ class TestJinaAuth:
|
||||
auth.validate_credentials()
|
||||
assert str(exc_info.value) == "Failed to authorize. Status code: 403. Error: Forbidden"
|
||||
|
||||
@patch("services.auth.jina.jina._http_client.post", autospec=True)
|
||||
def test_should_handle_unexpected_error_with_non_json_text_response(self, mock_post):
|
||||
"""Test handling of unexpected errors with non-JSON text response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 403
|
||||
mock_response.text = "Forbidden"
|
||||
mock_response.json.side_effect = Exception("Not JSON")
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}}
|
||||
auth = JinaAuth(credentials)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
auth.validate_credentials()
|
||||
assert str(exc_info.value) == "Failed to authorize. Status code: 403. Error: Forbidden"
|
||||
|
||||
@patch("services.auth.jina.jina._http_client.post", autospec=True)
|
||||
def test_should_handle_unexpected_error_without_text(self, mock_post):
|
||||
"""Test handling of unexpected errors without text response"""
|
||||
|
||||
@@ -118,6 +118,17 @@ def test_handle_error_statuses_default_unknown_error(jina_module: ModuleType) ->
|
||||
auth._handle_error(response)
|
||||
|
||||
|
||||
def test_handle_error_statuses_fall_back_to_text_body(jina_module: ModuleType) -> None:
|
||||
auth = jina_module.JinaAuth(_credentials(api_key="k"))
|
||||
response = MagicMock()
|
||||
response.status_code = 402
|
||||
response.text = "Payment required"
|
||||
response.json.side_effect = ValueError("Not JSON")
|
||||
|
||||
with pytest.raises(Exception, match="Status code: 402.*Payment required"):
|
||||
auth._handle_error(response)
|
||||
|
||||
|
||||
def test_handle_error_with_text_json_body(jina_module: ModuleType) -> None:
|
||||
auth = jina_module.JinaAuth(_credentials(api_key="k"))
|
||||
response = MagicMock()
|
||||
@@ -128,6 +139,16 @@ def test_handle_error_with_text_json_body(jina_module: ModuleType) -> None:
|
||||
auth._handle_error(response)
|
||||
|
||||
|
||||
def test_handle_error_with_non_json_text_body(jina_module: ModuleType) -> None:
|
||||
auth = jina_module.JinaAuth(_credentials(api_key="k"))
|
||||
response = MagicMock()
|
||||
response.status_code = 403
|
||||
response.text = "Forbidden"
|
||||
|
||||
with pytest.raises(Exception, match="Status code: 403.*Forbidden"):
|
||||
auth._handle_error(response)
|
||||
|
||||
|
||||
def test_handle_error_with_text_json_body_missing_error(jina_module: ModuleType) -> None:
|
||||
auth = jina_module.JinaAuth(_credentials(api_key="k"))
|
||||
response = MagicMock()
|
||||
|
||||
@@ -99,12 +99,25 @@ class TestWatercrawlAuth:
|
||||
auth_instance.validate_credentials()
|
||||
assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}"
|
||||
|
||||
@patch("services.auth.watercrawl.watercrawl.httpx.get", autospec=True)
|
||||
def test_should_handle_http_error_with_non_json_text_response(self, mock_get, auth_instance):
|
||||
"""Test handling of known HTTP errors with non-JSON text response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 402
|
||||
mock_response.text = "Payment required"
|
||||
mock_response.json.side_effect = ValueError("Not JSON")
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
auth_instance.validate_credentials()
|
||||
assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "response_text", "has_json_error", "expected_error_contains"),
|
||||
[
|
||||
(403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"),
|
||||
(404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"),
|
||||
(401, "Not JSON", True, "Expecting value"), # JSON decode error
|
||||
(401, "Not JSON", True, "Failed to authorize. Status code: 401. Error: Not JSON"),
|
||||
],
|
||||
)
|
||||
@patch("services.auth.watercrawl.watercrawl.httpx.get", autospec=True)
|
||||
|
||||
@@ -63,6 +63,7 @@ class _FakeFormRepo:
|
||||
return SimpleNamespace(
|
||||
form_id=form_id,
|
||||
workflow_run_id=getattr(form, "workflow_run_id", None),
|
||||
conversation_id=getattr(form, "conversation_id", None),
|
||||
node_id=getattr(form, "node_id", None),
|
||||
)
|
||||
|
||||
@@ -70,11 +71,15 @@ class _FakeFormRepo:
|
||||
class _FakeService:
|
||||
def __init__(self, _session_factory, form_repository=None):
|
||||
self.enqueued: list[str] = []
|
||||
self.agent_app_resumed: list[tuple[str, str]] = []
|
||||
|
||||
def enqueue_resume(self, workflow_run_id: str | None) -> None:
|
||||
if workflow_run_id is not None:
|
||||
self.enqueued.append(workflow_run_id)
|
||||
|
||||
def enqueue_agent_app_resume(self, *, conversation_id: str, form_id: str) -> None:
|
||||
self.agent_app_resumed.append((conversation_id, form_id))
|
||||
|
||||
|
||||
def _build_form(
|
||||
*,
|
||||
@@ -84,6 +89,7 @@ def _build_form(
|
||||
expiration_time: datetime,
|
||||
workflow_run_id: str | None,
|
||||
node_id: str,
|
||||
conversation_id: str | None = None,
|
||||
) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
id=form_id,
|
||||
@@ -91,6 +97,7 @@ def _build_form(
|
||||
created_at=created_at,
|
||||
expiration_time=expiration_time,
|
||||
workflow_run_id=workflow_run_id,
|
||||
conversation_id=conversation_id,
|
||||
node_id=node_id,
|
||||
status=HumanInputFormStatus.WAITING,
|
||||
)
|
||||
@@ -208,3 +215,45 @@ def test_check_and_handle_human_input_timeouts_omits_global_filter_when_disabled
|
||||
assert stmt is not None
|
||||
stmt_text = str(stmt)
|
||||
assert "created_at <=" not in stmt_text
|
||||
|
||||
|
||||
def test_check_and_handle_human_input_timeouts_routes_conversation_owned_form_to_agent_app_resume(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
# ENG-635 (review): a conversation-owned Agent v2 chat ask_human form has no
|
||||
# workflow_run_id. On timeout it must enqueue the Agent App resume (so the
|
||||
# timeout is threaded back as the ask_human result), instead of asserting on
|
||||
# workflow_run_id — which previously raised and was swallowed by the except.
|
||||
now = datetime(2025, 1, 1, 12, 0, 0)
|
||||
monkeypatch.setattr(task_module, "naive_utc_now", lambda: now)
|
||||
monkeypatch.setattr(task_module.dify_config, "HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS", 3600)
|
||||
monkeypatch.setattr(task_module, "db", SimpleNamespace(engine=object()))
|
||||
|
||||
forms = [
|
||||
_build_form(
|
||||
form_id="form-chat",
|
||||
form_kind=HumanInputFormKind.RUNTIME,
|
||||
created_at=now - timedelta(minutes=5),
|
||||
expiration_time=now - timedelta(seconds=1),
|
||||
workflow_run_id=None,
|
||||
conversation_id="conv-1",
|
||||
node_id="agent",
|
||||
),
|
||||
]
|
||||
capture: dict[str, Any] = {}
|
||||
monkeypatch.setattr(task_module, "sessionmaker", lambda *args, **kwargs: _FakeSessionFactory(forms, capture))
|
||||
|
||||
repo = _FakeFormRepo(form_map={form.id: form for form in forms})
|
||||
service = _FakeService(None)
|
||||
monkeypatch.setattr(task_module, "HumanInputFormSubmissionRepository", lambda: repo)
|
||||
monkeypatch.setattr(task_module, "HumanInputService", lambda *_args, **_kwargs: service)
|
||||
monkeypatch.setattr(task_module, "_handle_global_timeout", lambda **_kwargs: None)
|
||||
|
||||
task_module.check_and_handle_human_input_timeouts(limit=100)
|
||||
|
||||
# Node timeout (conversation forms are never "global"), routed to Agent App resume.
|
||||
assert repo.calls == [
|
||||
{"form_id": "form-chat", "timeout_status": HumanInputFormStatus.TIMEOUT, "reason": "node_timeout"}
|
||||
]
|
||||
assert service.agent_app_resumed == [("conv-1", "form-chat")]
|
||||
assert service.enqueued == []
|
||||
|
||||
@@ -8,6 +8,7 @@ This module tests the mail sending functionality including:
|
||||
- Error handling and logging
|
||||
"""
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
|
||||
@@ -371,8 +372,7 @@ class TestMailTaskRetryLogic:
|
||||
|
||||
@patch("tasks.mail_register_task.get_email_i18n_service")
|
||||
@patch("tasks.mail_register_task.mail")
|
||||
@patch("tasks.mail_register_task.logger")
|
||||
def test_mail_task_logs_success(self, mock_logger, mock_mail, mock_email_service):
|
||||
def test_mail_task_logs_success(self, mock_mail, mock_email_service, caplog):
|
||||
"""Test that successful mail sends are logged properly."""
|
||||
# Arrange
|
||||
mock_mail.is_inited.return_value = True
|
||||
@@ -380,7 +380,8 @@ class TestMailTaskRetryLogic:
|
||||
mock_email_service.return_value = mock_service
|
||||
|
||||
# Act
|
||||
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
|
||||
with caplog.at_level(logging.INFO, logger="tasks.mail_register_task"):
|
||||
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
|
||||
|
||||
# Assert
|
||||
mock_service.send_email.assert_called_once_with(
|
||||
@@ -390,12 +391,14 @@ class TestMailTaskRetryLogic:
|
||||
template_context={"to": "test@example.com", "code": "123456"},
|
||||
)
|
||||
# Verify logging calls
|
||||
assert mock_logger.info.call_count == 2 # Start and success logs
|
||||
log_messages = [record.getMessage() for record in caplog.records]
|
||||
assert len(log_messages) == 2 # Start and success logs
|
||||
assert "Start email register mail to test@example.com" in log_messages[0]
|
||||
assert "Send email register mail to test@example.com succeeded" in log_messages[1]
|
||||
|
||||
@patch("tasks.mail_register_task.get_email_i18n_service")
|
||||
@patch("tasks.mail_register_task.mail")
|
||||
@patch("tasks.mail_register_task.logger")
|
||||
def test_mail_task_logs_failure(self, mock_logger, mock_mail, mock_email_service):
|
||||
def test_mail_task_logs_failure(self, mock_mail, mock_email_service, caplog):
|
||||
"""Test that failed mail sends are logged with exception details."""
|
||||
# Arrange
|
||||
mock_mail.is_inited.return_value = True
|
||||
@@ -404,10 +407,13 @@ class TestMailTaskRetryLogic:
|
||||
mock_email_service.return_value = mock_service
|
||||
|
||||
# Act
|
||||
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
|
||||
with caplog.at_level(logging.ERROR, logger="tasks.mail_register_task"):
|
||||
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
|
||||
|
||||
# Assert
|
||||
mock_logger.exception.assert_called_once_with("Send email register mail to %s failed", "test@example.com")
|
||||
assert "Send email register mail to test@example.com failed" in caplog.text
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].exc_info is not None
|
||||
|
||||
@patch("tasks.mail_reset_password_task.get_email_i18n_service")
|
||||
@patch("tasks.mail_reset_password_task.mail")
|
||||
@@ -574,8 +580,7 @@ class TestInnerEmailTask:
|
||||
@patch("tasks.mail_inner_task.get_email_i18n_service")
|
||||
@patch("tasks.mail_inner_task.mail")
|
||||
@patch("tasks.mail_inner_task._render_template_with_strategy")
|
||||
@patch("tasks.mail_inner_task.logger")
|
||||
def test_inner_email_task_logs_failure(self, mock_logger, mock_render, mock_mail, mock_email_service):
|
||||
def test_inner_email_task_logs_failure(self, mock_render, mock_mail, mock_email_service, caplog):
|
||||
"""Test inner email task logs failures properly."""
|
||||
# Arrange
|
||||
mock_mail.is_inited.return_value = True
|
||||
@@ -587,10 +592,13 @@ class TestInnerEmailTask:
|
||||
to_list = ["user@example.com"]
|
||||
|
||||
# Act
|
||||
send_inner_email_task(to=to_list, subject="Test", body="Body", substitutions={})
|
||||
with caplog.at_level(logging.ERROR, logger="tasks.mail_inner_task"):
|
||||
send_inner_email_task(to=to_list, subject="Test", body="Body", substitutions={})
|
||||
|
||||
# Assert
|
||||
mock_logger.exception.assert_called_once()
|
||||
assert "Send enterprise mail to ['user@example.com'] failed" in caplog.text
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].exc_info is not None
|
||||
|
||||
|
||||
class TestSendGridIntegration:
|
||||
@@ -890,9 +898,8 @@ class TestPerformanceAndTiming:
|
||||
|
||||
@patch("tasks.mail_register_task.get_email_i18n_service")
|
||||
@patch("tasks.mail_register_task.mail")
|
||||
@patch("tasks.mail_register_task.logger")
|
||||
@patch("tasks.mail_register_task.time")
|
||||
def test_mail_task_tracks_execution_time(self, mock_time, mock_logger, mock_mail, mock_email_service):
|
||||
def test_mail_task_tracks_execution_time(self, mock_time, mock_mail, mock_email_service, caplog):
|
||||
"""Test that mail tasks track and log execution time."""
|
||||
# Arrange
|
||||
mock_mail.is_inited.return_value = True
|
||||
@@ -903,13 +910,14 @@ class TestPerformanceAndTiming:
|
||||
mock_time.perf_counter.side_effect = [100.0, 100.5] # 0.5 second execution
|
||||
|
||||
# Act
|
||||
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
|
||||
with caplog.at_level(logging.INFO, logger="tasks.mail_register_task"):
|
||||
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
|
||||
|
||||
# Assert
|
||||
assert mock_time.perf_counter.call_count == 2
|
||||
# Verify latency is logged
|
||||
success_log_call = mock_logger.info.call_args_list[1]
|
||||
assert "latency" in str(success_log_call)
|
||||
assert len(caplog.records) == 2
|
||||
assert "latency: 0.5" in caplog.records[1].getMessage()
|
||||
|
||||
|
||||
class TestEdgeCasesAndErrorHandling:
|
||||
@@ -1457,8 +1465,7 @@ class TestLoggingAndMonitoring:
|
||||
|
||||
@patch("tasks.mail_register_task.get_email_i18n_service")
|
||||
@patch("tasks.mail_register_task.mail")
|
||||
@patch("tasks.mail_register_task.logger")
|
||||
def test_mail_task_logs_recipient_information(self, mock_logger, mock_mail, mock_email_service):
|
||||
def test_mail_task_logs_recipient_information(self, mock_mail, mock_email_service, caplog):
|
||||
"""
|
||||
Test that mail tasks log recipient information for audit trails.
|
||||
|
||||
@@ -1471,17 +1478,16 @@ class TestLoggingAndMonitoring:
|
||||
mock_email_service.return_value = mock_service
|
||||
|
||||
# Act
|
||||
send_email_register_mail_task(language="en-US", to="audit@example.com", code="123456")
|
||||
with caplog.at_level(logging.INFO, logger="tasks.mail_register_task"):
|
||||
send_email_register_mail_task(language="en-US", to="audit@example.com", code="123456")
|
||||
|
||||
# Assert
|
||||
# Check that recipient is logged in start message
|
||||
start_log_call = mock_logger.info.call_args_list[0]
|
||||
assert "audit@example.com" in str(start_log_call)
|
||||
assert "audit@example.com" in caplog.records[0].getMessage()
|
||||
|
||||
@patch("tasks.mail_inner_task.get_email_i18n_service")
|
||||
@patch("tasks.mail_inner_task.mail")
|
||||
@patch("tasks.mail_inner_task.logger")
|
||||
def test_inner_email_task_logs_subject_for_tracking(self, mock_logger, mock_mail, mock_email_service):
|
||||
def test_inner_email_task_logs_subject_for_tracking(self, mock_mail, mock_email_service, caplog):
|
||||
"""
|
||||
Test that inner email task logs subject for tracking purposes.
|
||||
|
||||
@@ -1494,11 +1500,11 @@ class TestLoggingAndMonitoring:
|
||||
mock_email_service.return_value = mock_service
|
||||
|
||||
# Act
|
||||
send_inner_email_task(
|
||||
to=["user@example.com"], subject="Important Notification", body="<p>Body</p>", substitutions={}
|
||||
)
|
||||
with caplog.at_level(logging.INFO, logger="tasks.mail_inner_task"):
|
||||
send_inner_email_task(
|
||||
to=["user@example.com"], subject="Important Notification", body="<p>Body</p>", substitutions={}
|
||||
)
|
||||
|
||||
# Assert
|
||||
# Check that subject is logged
|
||||
start_log_call = mock_logger.info.call_args_list[0]
|
||||
assert "Important Notification" in str(start_log_call)
|
||||
assert "Important Notification" in caplog.records[0].getMessage()
|
||||
|
||||
@@ -14,7 +14,7 @@ export type TokenStore = {
|
||||
|
||||
const DOC_VERSION = 1
|
||||
|
||||
type TokenDoc = {
|
||||
export type TokenDoc = {
|
||||
version?: number
|
||||
tokens?: Record<string, Record<string, string>>
|
||||
}
|
||||
|
||||
@@ -6,12 +6,7 @@ app:
|
||||
mode: advanced-chat
|
||||
name: echo-bot
|
||||
use_icon_as_answer_icon: false
|
||||
dependencies:
|
||||
- current_identifier: null
|
||||
type: marketplace
|
||||
value:
|
||||
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
|
||||
version: null
|
||||
dependencies: []
|
||||
kind: app
|
||||
version: 0.6.0
|
||||
workflow:
|
||||
@@ -68,18 +63,9 @@ workflow:
|
||||
edges:
|
||||
- data:
|
||||
sourceType: start
|
||||
targetType: llm
|
||||
id: 1779690795511-llm
|
||||
source: '1779690795511'
|
||||
sourceHandle: source
|
||||
target: llm
|
||||
targetHandle: target
|
||||
type: custom
|
||||
- data:
|
||||
sourceType: llm
|
||||
targetType: answer
|
||||
id: llm-answer
|
||||
source: llm
|
||||
id: 1779690795511-answer
|
||||
source: '1779690795511'
|
||||
sourceHandle: source
|
||||
target: answer
|
||||
targetHandle: target
|
||||
@@ -87,7 +73,7 @@ workflow:
|
||||
nodes:
|
||||
- data:
|
||||
selected: false
|
||||
title: 用户输入
|
||||
title: User Input
|
||||
type: start
|
||||
variables:
|
||||
- default: ''
|
||||
@@ -107,66 +93,24 @@ workflow:
|
||||
positionAbsolute:
|
||||
x: 79
|
||||
y: 282
|
||||
selected: true
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
context:
|
||||
enabled: false
|
||||
variable_selector: []
|
||||
memory:
|
||||
query_prompt_template: '{{#sys.query#}}
|
||||
|
||||
{{#sys.files#}}'
|
||||
role_prefix:
|
||||
assistant: ''
|
||||
user: ''
|
||||
window:
|
||||
enabled: false
|
||||
size: 10
|
||||
model:
|
||||
completion_params:
|
||||
temperature: 0.7
|
||||
mode: chat
|
||||
name: qwen3.6-plus
|
||||
provider: langgenius/tongyi/tongyi
|
||||
prompt_template:
|
||||
- id: 9b866a63-3619-4f5c-a46f-0aed04078587
|
||||
role: system
|
||||
text: 'User says: {{{#sys.query#}} Reply exactly: echo:{{#sys.query#}}'
|
||||
selected: false
|
||||
title: LLM
|
||||
type: llm
|
||||
vision:
|
||||
enabled: false
|
||||
height: 88
|
||||
id: llm
|
||||
position:
|
||||
x: 380
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 380
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
answer: '{{#llm.text#}}'
|
||||
answer: 'echo:{{#sys.query#}}'
|
||||
selected: false
|
||||
title: 直接回复
|
||||
title: Reply
|
||||
type: answer
|
||||
variables: []
|
||||
height: 103
|
||||
id: answer
|
||||
position:
|
||||
x: 680
|
||||
x: 380
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 680
|
||||
x: 380
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
|
||||
@@ -6,12 +6,7 @@ app:
|
||||
mode: workflow
|
||||
name: basic_auto_test
|
||||
use_icon_as_answer_icon: false
|
||||
dependencies:
|
||||
- current_identifier: null
|
||||
type: marketplace
|
||||
value:
|
||||
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
|
||||
version: null
|
||||
dependencies: []
|
||||
kind: app
|
||||
version: 0.6.0
|
||||
workflow:
|
||||
@@ -70,20 +65,9 @@ workflow:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: start
|
||||
targetType: llm
|
||||
id: 1779097154262-source-1779097204645-target
|
||||
source: '1779097154262'
|
||||
sourceHandle: source
|
||||
target: '1779097204645'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInLoop: false
|
||||
sourceType: llm
|
||||
targetType: end
|
||||
id: 1779097204645-source-1779171097399-target
|
||||
source: '1779097204645'
|
||||
id: 1779097154262-source-1779171097399-target
|
||||
source: '1779097154262'
|
||||
sourceHandle: source
|
||||
target: '1779171097399'
|
||||
targetHandle: target
|
||||
@@ -92,7 +76,7 @@ workflow:
|
||||
nodes:
|
||||
- data:
|
||||
selected: true
|
||||
title: 用户输入
|
||||
title: User Input
|
||||
type: start
|
||||
variables:
|
||||
- default: ''
|
||||
@@ -143,49 +127,15 @@ workflow:
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
context:
|
||||
enabled: false
|
||||
variable_selector: []
|
||||
model:
|
||||
completion_params:
|
||||
temperature: 0.7
|
||||
mode: chat
|
||||
name: qwen3.6-plus
|
||||
provider: langgenius/tongyi/tongyi
|
||||
prompt_template:
|
||||
- id: 1ddb3202-d84c-4faf-afe3-424eedc9049a
|
||||
role: system
|
||||
text: 'User says:{{#1779097154262.x#}}. Reply exactly: echo:{{#1779097154262.x#}}
|
||||
|
||||
'
|
||||
selected: false
|
||||
title: LLM
|
||||
type: llm
|
||||
vision:
|
||||
enabled: false
|
||||
height: 88
|
||||
id: '1779097204645'
|
||||
position:
|
||||
x: 382
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 382
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
outputs:
|
||||
- value_selector:
|
||||
- '1779097204645'
|
||||
- text
|
||||
- '1779097154262'
|
||||
- x
|
||||
value_type: string
|
||||
variable: x
|
||||
selected: false
|
||||
title: 输出
|
||||
title: Output
|
||||
type: end
|
||||
height: 88
|
||||
id: '1779171097399'
|
||||
|
||||
@@ -6,12 +6,7 @@ app:
|
||||
mode: advanced-chat
|
||||
name: DIFY_E2E_FILE_CHAT
|
||||
use_icon_as_answer_icon: false
|
||||
dependencies:
|
||||
- current_identifier: null
|
||||
type: marketplace
|
||||
value:
|
||||
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
|
||||
version: null
|
||||
dependencies: []
|
||||
kind: app
|
||||
version: 0.6.0
|
||||
workflow:
|
||||
@@ -68,18 +63,9 @@ workflow:
|
||||
edges:
|
||||
- data:
|
||||
sourceType: start
|
||||
targetType: llm
|
||||
id: 1780453002656-llm
|
||||
source: '1780453002656'
|
||||
sourceHandle: source
|
||||
target: llm
|
||||
targetHandle: target
|
||||
type: custom
|
||||
- data:
|
||||
sourceType: llm
|
||||
targetType: answer
|
||||
id: llm-answer
|
||||
source: llm
|
||||
id: 1780453002656-answer
|
||||
source: '1780453002656'
|
||||
sourceHandle: source
|
||||
target: answer
|
||||
targetHandle: target
|
||||
@@ -87,7 +73,7 @@ workflow:
|
||||
nodes:
|
||||
- data:
|
||||
selected: false
|
||||
title: 用户输入
|
||||
title: User Input
|
||||
type: start
|
||||
variables:
|
||||
- allowed_file_extensions: []
|
||||
@@ -119,60 +105,18 @@ workflow:
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
context:
|
||||
enabled: false
|
||||
variable_selector: []
|
||||
memory:
|
||||
query_prompt_template: '{{#sys.query#}}
|
||||
|
||||
{{#sys.files#}}'
|
||||
role_prefix:
|
||||
assistant: ''
|
||||
user: ''
|
||||
window:
|
||||
enabled: false
|
||||
size: 10
|
||||
model:
|
||||
completion_params:
|
||||
temperature: 0.7
|
||||
mode: chat
|
||||
name: qwen3.6-plus
|
||||
provider: langgenius/tongyi/tongyi
|
||||
prompt_template:
|
||||
- id: ebc516ad-be6b-4a78-af32-77f447305b34
|
||||
role: system
|
||||
text: 输出固定内容:""hello
|
||||
answer: ok
|
||||
selected: false
|
||||
title: LLM
|
||||
type: llm
|
||||
vision:
|
||||
enabled: false
|
||||
height: 88
|
||||
id: llm
|
||||
position:
|
||||
x: 380
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 380
|
||||
y: 282
|
||||
selected: true
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
answer: '{{#llm.text#}}'
|
||||
selected: false
|
||||
title: 直接回复
|
||||
title: Reply
|
||||
type: answer
|
||||
variables: []
|
||||
height: 103
|
||||
id: answer
|
||||
position:
|
||||
x: 680
|
||||
x: 380
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 680
|
||||
x: 380
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
|
||||
@@ -6,12 +6,7 @@ app:
|
||||
mode: workflow
|
||||
name: file_auto_test
|
||||
use_icon_as_answer_icon: false
|
||||
dependencies:
|
||||
- current_identifier: null
|
||||
type: marketplace
|
||||
value:
|
||||
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
|
||||
version: null
|
||||
dependencies: []
|
||||
kind: app
|
||||
version: 0.6.0
|
||||
workflow:
|
||||
@@ -70,21 +65,9 @@ workflow:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: start
|
||||
targetType: llm
|
||||
id: 1779693724732-source-1779693759949-target
|
||||
source: '1779693724732'
|
||||
sourceHandle: source
|
||||
target: '1779693759949'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: llm
|
||||
targetType: end
|
||||
id: 1779693759949-source-1779693765299-target
|
||||
source: '1779693759949'
|
||||
id: 1779693724732-source-1779693765299-target
|
||||
source: '1779693724732'
|
||||
sourceHandle: source
|
||||
target: '1779693765299'
|
||||
targetHandle: target
|
||||
@@ -93,7 +76,7 @@ workflow:
|
||||
nodes:
|
||||
- data:
|
||||
selected: true
|
||||
title: 用户输入
|
||||
title: User Input
|
||||
type: start
|
||||
variables:
|
||||
- allowed_file_extensions: []
|
||||
@@ -140,46 +123,9 @@ workflow:
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
context:
|
||||
enabled: false
|
||||
variable_selector: []
|
||||
model:
|
||||
completion_params:
|
||||
temperature: 0.7
|
||||
mode: chat
|
||||
name: qwen3.6-plus
|
||||
provider: langgenius/tongyi/tongyi
|
||||
prompt_template:
|
||||
- id: bb929f8f-5fa9-415b-91c3-c30228488dcf
|
||||
role: system
|
||||
text: 直接输出内容:hello
|
||||
outputs: []
|
||||
selected: false
|
||||
title: LLM
|
||||
type: llm
|
||||
vision:
|
||||
enabled: false
|
||||
height: 88
|
||||
id: '1779693759949'
|
||||
position:
|
||||
x: 382
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 382
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
outputs:
|
||||
- value_selector:
|
||||
- '1779693759949'
|
||||
- text
|
||||
value_type: string
|
||||
variable: x
|
||||
selected: false
|
||||
title: 输出
|
||||
title: Output
|
||||
type: end
|
||||
height: 88
|
||||
id: '1779693765299'
|
||||
|
||||
@@ -6,12 +6,7 @@ app:
|
||||
mode: workflow
|
||||
name: auto_test_workspace2
|
||||
use_icon_as_answer_icon: false
|
||||
dependencies:
|
||||
- current_identifier: null
|
||||
type: marketplace
|
||||
value:
|
||||
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
|
||||
version: null
|
||||
dependencies: []
|
||||
kind: app
|
||||
version: 0.6.0
|
||||
workflow:
|
||||
@@ -70,21 +65,9 @@ workflow:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: start
|
||||
targetType: llm
|
||||
id: 1780305524693-source-1780305526186-target
|
||||
source: '1780305524693'
|
||||
sourceHandle: source
|
||||
target: '1780305526186'
|
||||
targetHandle: target
|
||||
type: custom
|
||||
zIndex: 0
|
||||
- data:
|
||||
isInIteration: false
|
||||
isInLoop: false
|
||||
sourceType: llm
|
||||
targetType: end
|
||||
id: 1780305526186-source-1780305600095-target
|
||||
source: '1780305526186'
|
||||
id: 1780305524693-source-1780305600095-target
|
||||
source: '1780305524693'
|
||||
sourceHandle: source
|
||||
target: '1780305600095'
|
||||
targetHandle: target
|
||||
@@ -93,7 +76,7 @@ workflow:
|
||||
nodes:
|
||||
- data:
|
||||
selected: false
|
||||
title: 用户输入
|
||||
title: User Input
|
||||
type: start
|
||||
variables: []
|
||||
height: 73
|
||||
@@ -109,45 +92,9 @@ workflow:
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
context:
|
||||
enabled: false
|
||||
variable_selector: []
|
||||
model:
|
||||
completion_params:
|
||||
temperature: 0.7
|
||||
mode: chat
|
||||
name: qwen3.6-plus
|
||||
provider: langgenius/tongyi/tongyi
|
||||
prompt_template:
|
||||
- id: cd753cdd-d950-44bf-99ad-7cb19f42d5b6
|
||||
role: system
|
||||
text: 输出内容:hello
|
||||
outputs: []
|
||||
selected: false
|
||||
title: LLM
|
||||
type: llm
|
||||
vision:
|
||||
enabled: false
|
||||
height: 88
|
||||
id: '1780305526186'
|
||||
position:
|
||||
x: 382
|
||||
y: 282
|
||||
positionAbsolute:
|
||||
x: 382
|
||||
y: 282
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
width: 242
|
||||
- data:
|
||||
outputs:
|
||||
- value_selector:
|
||||
- '1780305526186'
|
||||
- text
|
||||
value_type: string
|
||||
variable: x
|
||||
selected: false
|
||||
title: 输出
|
||||
title: Output
|
||||
type: end
|
||||
height: 88
|
||||
id: '1780305600095'
|
||||
@@ -157,6 +104,7 @@ workflow:
|
||||
positionAbsolute:
|
||||
x: 684
|
||||
y: 282
|
||||
selected: false
|
||||
sourcePosition: right
|
||||
targetPosition: left
|
||||
type: custom
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
* withTempConfig) to prevent session state leaking between tests.
|
||||
*/
|
||||
|
||||
import type { TokenDoc } from '@/store/token-store'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { execSync, spawn } from 'node:child_process'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
/** Path to the dev entry point — no build required. */
|
||||
export const BIN = resolve(__dirname, '../../../bin/dev.js')
|
||||
@@ -208,13 +210,11 @@ function splitHost(host: string): { bare: string, scheme: string } {
|
||||
}
|
||||
|
||||
async function writeFileToken(configDir: string, host: string, email: string, bearer: string): Promise<void> {
|
||||
const dotParts = `tokens.${host}.${email}`.split('.')
|
||||
let yaml = ''
|
||||
for (let i = 0; i < dotParts.length - 1; i++) {
|
||||
yaml += `${' '.repeat(i) + dotParts[i]}:\n`
|
||||
const doc: TokenDoc = {
|
||||
version: 1,
|
||||
tokens: { [host]: { [email]: bearer } },
|
||||
}
|
||||
yaml += `${' '.repeat(dotParts.length - 1) + (dotParts[dotParts.length - 1] ?? '')}: "${bearer}"\n`
|
||||
await writeFile(join(configDir, 'tokens.yml'), yaml, { mode: 0o600 })
|
||||
await writeFile(join(configDir, 'tokens.yml'), yaml.dump(doc, { lineWidth: -1, noRefs: true }), { mode: 0o600 })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,11 +289,8 @@ export async function injectAuth(configDir: string, opts: AuthInjectionOptions):
|
||||
new Entry('difyctl', account).setPassword(JSON.stringify(opts.bearer))
|
||||
}
|
||||
else {
|
||||
// Fall back to tokens.yml.
|
||||
// YamlStore.doGet splits the key on '.' and traverses the nested object,
|
||||
// so "tokens.localhost.user@dify.ai" splits into 4 parts:
|
||||
// tokens -> localhost -> user@dify -> ai
|
||||
// The YAML must mirror that exact nesting.
|
||||
// Fall back to tokens.yml — FileTokenStore uses getTyped<TokenDoc>()
|
||||
// which expects flat tokens[host][email] with version: 1.
|
||||
await writeFileToken(configDir, bare, email, opts.bearer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,9 +349,11 @@ describe('E2E / agent skill — describe app -o json (auth required)', () => {
|
||||
assertPipeFriendlyJson(r)
|
||||
})
|
||||
|
||||
itWithAuth('[P0] nonexistent app → exit 1 + JSON error envelope', async () => {
|
||||
itWithAuth('[P0] invalid (non-UUID) app id → exit 2 + usage error envelope', async () => {
|
||||
// 'app-id-nonexistent-e2e-xyz' is not a valid UUID; describe app rejects it
|
||||
// client-side via isValidUuid() with usage_invalid_flag (exit 2).
|
||||
const r = await fx.r(['describe', 'app', 'app-id-nonexistent-e2e-xyz', '-o', 'json'])
|
||||
expect(r.exitCode).toBe(1)
|
||||
expect(r.exitCode).toBe(2)
|
||||
assertErrorEnvelope(r)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -131,11 +131,12 @@ describe('E2E / difyctl describe app', () => {
|
||||
|
||||
// ── Not found ─────────────────────────────────────────────────────────────
|
||||
|
||||
it('[P0] non-existent app returns exit code 1 with not-found error (3.83)', async () => {
|
||||
// Spec 3.83: describe non-existent app → stderr contains not-found, exit code 1.
|
||||
it('[P0] invalid (non-UUID) app id returns usage error (exit code 2)', async () => {
|
||||
// NONEXISTENT_ID is not a valid UUID, so the CLI rejects it client-side via
|
||||
// isValidUuid() before making any network request → usage_invalid_flag (exit 2).
|
||||
const result = await fx.r(['describe', 'app', NONEXISTENT_ID])
|
||||
expect(result.exitCode, 'non-existent app should exit with code 1').toBe(1)
|
||||
expect(result.stderr).toMatch(/not.?found|404|does not exist|server_5xx/i)
|
||||
expect(result.exitCode, 'invalid UUID should exit with code 2').toBe(2)
|
||||
expect(result.stderr).toMatch(/uuid|valid|usage_invalid_flag/i)
|
||||
})
|
||||
|
||||
it('[P1] non-existent app in JSON mode outputs JSON error envelope', async () => {
|
||||
|
||||
@@ -37,6 +37,7 @@ import { afterEach, beforeEach, describe, expect, inject, it } from 'vitest'
|
||||
import { ZERO } from '@/util/uuid.js'
|
||||
import {
|
||||
assertErrorEnvelope,
|
||||
assertExitCode,
|
||||
assertNoAnsi,
|
||||
assertNonZeroExit,
|
||||
} from '../../helpers/assert.js'
|
||||
@@ -141,7 +142,7 @@ describe('E2E / error message standards (spec 5.3)', () => {
|
||||
// Spec 5.70: submitting a value of the wrong type must fail.
|
||||
// The workflow app (workflowAppId) expects x as a string; passing a JSON
|
||||
// number causes the server to reject the request.
|
||||
// In v1.0 the server returns HTTP 500 for type validation failures.
|
||||
// After the @accepts/@returns contract unification, the server returns HTTP 422 for request schema failures.
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
@@ -156,6 +157,206 @@ describe('E2E / error message standards (spec 5.3)', () => {
|
||||
expect(result.stderr.trim().length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
// ── 5.70a/b/c P4 sanitization — 422 error body is clean (no leaks) ────────
|
||||
|
||||
it('[P0] 5.70a validation failure message is a plain string, not double-encoded JSON', async () => {
|
||||
// After the @accepts contract fix, the server aborts with
|
||||
// abort(422, message="Request validation failed", errors=[...])
|
||||
// The CLI wraps this into its envelope. The message field must be a plain
|
||||
// human-readable string — NOT a JSON-serialised string that itself contains
|
||||
// pydantic error details (which was the double-encoding bug in P4).
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
E.workflowAppId,
|
||||
'--inputs',
|
||||
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
const envelope = assertErrorEnvelope(result)
|
||||
// message must be a plain string, not a JSON string (no double encoding)
|
||||
expect(typeof envelope.error.message).toBe('string')
|
||||
expect(() => JSON.parse(envelope.error.message)).toThrow()
|
||||
})
|
||||
|
||||
it('[P1] 5.70b validation error response does not leak pydantic version URL', async () => {
|
||||
// Before the P4 fix, exc.json() included a "url" field pointing to
|
||||
// https://errors.pydantic.dev/<version>/... — exposing the server's pydantic
|
||||
// version. The sanitised response must not contain this URL.
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
E.workflowAppId,
|
||||
'--inputs',
|
||||
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
expect(result.stderr).not.toMatch(/errors\.pydantic\.dev|pydantic\.dev\//)
|
||||
})
|
||||
|
||||
it('[P1] 5.70c validation error response does not echo back user input', async () => {
|
||||
// Before the P4 fix, exc.json() included the user's original "input" value
|
||||
// inside the error details. The sanitised response must not repeat the
|
||||
// submitted value so that sensitive payloads are not reflected to callers.
|
||||
const sentValue = 'not-a-number-sentinel-12345'
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
E.workflowAppId,
|
||||
'--inputs',
|
||||
JSON.stringify({ x: 'hello', num: sentValue, enum_var: 'A', paragraph: 'ok' }),
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
expect(result.stderr).not.toContain(sentValue)
|
||||
})
|
||||
|
||||
// ── 5.70d-h ErrorBody contract — error.server structure and rendering priorities ──
|
||||
// PR #37285 introduces canonical ErrorBody on every /openapi/v1 non-2xx response.
|
||||
// CLI strict-parses via zErrorBody.safeParse; success → full struct at error.server.
|
||||
//
|
||||
// V2 rendering priorities (format.ts, verified against codebase):
|
||||
// header code : server?.code ?? cliCode — server wins, CLI fallback
|
||||
// hint : cliHint ?? server?.hint — CLI wins, server fallback (V2 correction)
|
||||
// details : server?.details[] — " - loc: msg (type)" per entry, no -v
|
||||
|
||||
it('[P0] 5.70d JSON envelope contains error.server with canonical code/status/message', async () => {
|
||||
// Trigger: describe app ZERO — server returns canonical 404 ErrorBody
|
||||
// { code:"not_found", status:404, message:"app not found" }.
|
||||
// zErrorBody.safeParse succeeds → error.server is populated on the current server.
|
||||
const result = await fx.r(['describe', 'app', ZERO, '-o', 'json'])
|
||||
assertNonZeroExit(result)
|
||||
const envelope = JSON.parse(result.stderr.trim()) as {
|
||||
error: { code: string, server?: { code: string, status: number, message: string } }
|
||||
}
|
||||
expect(envelope.error.server, 'error.server must be present when server returns canonical ErrorBody').toBeDefined()
|
||||
expect(typeof envelope.error.server?.code, 'error.server.code must be a string').toBe('string')
|
||||
expect(envelope.error.server?.code.length).toBeGreaterThan(0)
|
||||
expect(typeof envelope.error.server?.status, 'error.server.status must be a number').toBe('number')
|
||||
expect(typeof envelope.error.server?.message, 'error.server.message must be a string').toBe('string')
|
||||
expect(envelope.error.server?.message.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('[P1] 5.70e @accepts query validation returns canonical 422 with details array', async () => {
|
||||
// Trigger: direct fetch to GET /apps?page=not-integer — @accepts(query=AppListQuery)
|
||||
// validates page as int and emits canonical 422 ErrorBody with details[].
|
||||
// Direct fetch is used because the CLI validates --page as integer client-side
|
||||
// (would exit 2 before hitting the server); this pins the server-side contract.
|
||||
const res = await fetch(
|
||||
`${E.host.replace(/\/$/, '')}/openapi/v1/apps?workspace_id=${E.workspaceId}&page=not-an-integer`,
|
||||
{ headers: { Authorization: `Bearer ${E.token}` }, signal: AbortSignal.timeout(8_000) },
|
||||
)
|
||||
expect(res.status).toBe(422)
|
||||
const body = await res.json() as {
|
||||
code?: string
|
||||
status?: number
|
||||
details?: Array<{ type: string, loc: Array<string | number>, msg: string }>
|
||||
}
|
||||
expect(body.code).toBe('invalid_param')
|
||||
expect(body.status).toBe(422)
|
||||
expect(Array.isArray(body.details), 'details must be an array').toBe(true)
|
||||
expect(body.details!.length).toBeGreaterThan(0)
|
||||
const entry = body.details![0]!
|
||||
expect(typeof entry.type).toBe('string')
|
||||
expect(typeof entry.msg).toBe('string')
|
||||
expect(Array.isArray(entry.loc)).toBe(true)
|
||||
})
|
||||
|
||||
it('[P1] 5.70g rendering priority — header code: server code wins over CLI classification code', async () => {
|
||||
// renderHuman: headerCode = server?.code ?? e.code (server wins, V2 unchanged)
|
||||
// When canonical ErrorBody is parsed, the server semantic code replaces the CLI
|
||||
// classification code ("server_4xx_other") in the human-readable output header.
|
||||
// Trigger: describe app ZERO → canonical 404; header starts with "not_found:".
|
||||
const result = await fx.r(['describe', 'app', ZERO])
|
||||
assertNonZeroExit(result)
|
||||
expect(result.stderr.trimStart()).not.toMatch(/^server_4xx_other:/)
|
||||
expect(result.stderr.trimStart()).toMatch(/^not_found:/)
|
||||
})
|
||||
|
||||
it('[P1] 5.70g2 rendering priority — hint: CLI hint wins over server hint (V2 correction)', async () => {
|
||||
// renderHuman: hint = cliHint ?? server?.hint (CLI wins — V2 spec correction)
|
||||
// V1 incorrectly documented "server wins"; V2 aligns with codebase: CLI wins.
|
||||
// Test: 401 AuthExpired — classifyResponse sets c.hint = AUTH_LOGIN_HINT before
|
||||
// serverError is parsed; CLI hint takes precedence over any server-provided hint.
|
||||
// Verified on current server (no @accepts deployment required).
|
||||
const unauthTmp = await withTempConfig()
|
||||
try {
|
||||
const result = await run(['get', 'app', '-o', 'json'], { configDir: unauthTmp.configDir })
|
||||
assertExitCode(result, 4)
|
||||
const envelope = JSON.parse(result.stderr.trim()) as { error: { hint?: string } }
|
||||
expect(envelope.error.hint, 'CLI login hint must appear for auth error').toMatch(/auth login/i)
|
||||
}
|
||||
finally {
|
||||
await unauthTmp.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('[P1] 5.70h JSON envelope: error.code = CLI classification; error.server.code = server semantic code', async () => {
|
||||
// toEnvelope() sets error.code from HTTP status bucket (e.g. "server_4xx_other")
|
||||
// while the server's semantic code is separate in error.server.code.
|
||||
// Agents can branch on error.server.code without parsing human-readable text.
|
||||
// Trigger: describe app ZERO → canonical 404; error.code="server_4xx_other",
|
||||
// error.server.code="not_found" — always distinct when ErrorBody is present.
|
||||
const result = await fx.r(['describe', 'app', ZERO, '-o', 'json'])
|
||||
assertNonZeroExit(result)
|
||||
const envelope = JSON.parse(result.stderr.trim()) as {
|
||||
error: { code: string, server?: { code: string } }
|
||||
}
|
||||
expect(envelope.error.code).toBe('server_4xx_other')
|
||||
expect(envelope.error.server?.code).toBeDefined()
|
||||
expect(envelope.error.server?.code).not.toBe('server_4xx_other')
|
||||
})
|
||||
// ── 5.70i / 5.70j PR #37285 boundary contract ───────────────────────────
|
||||
|
||||
it('[P1] 5.70i unknown /openapi/v1 route returns canonical 404 ErrorBody without route suggestions', async () => {
|
||||
// PR #37285: ExternalApi._help_on_404 suppresses flask-restx route enumeration.
|
||||
// Previously, an unknown path under /openapi/v1/ returned flask-restx's default
|
||||
// 404 with a "Did you mean /openapi/v1/apps?" suggestion, leaking the route table.
|
||||
// After the fix it must return a canonical ErrorBody and contain no suggestions.
|
||||
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/this-path-does-not-exist-e2e`, {
|
||||
headers: { Authorization: `Bearer ${E.token}` },
|
||||
signal: AbortSignal.timeout(8_000),
|
||||
})
|
||||
expect(res.status).toBe(404)
|
||||
const body = await res.json() as Record<string, unknown>
|
||||
// canonical ErrorBody fields must be present
|
||||
expect(typeof body.code, '404 body must have a string code field').toBe('string')
|
||||
expect(body.status, '404 body must have status: 404').toBe(404)
|
||||
// no flask-restx route enumeration in the response
|
||||
const raw = JSON.stringify(body)
|
||||
expect(raw).not.toMatch(/did you mean/i)
|
||||
expect(raw).not.toMatch(/you might want/i)
|
||||
})
|
||||
|
||||
it('[P1] 5.70j device-flow 4xx uses RFC 8628 format, not ErrorBody — zErrorBody parse fails gracefully', async () => {
|
||||
// PR #37285 explicitly excludes RFC 8628 device-flow endpoints from the
|
||||
// ErrorBody contract. This test pins that contract:
|
||||
// - The device/token endpoint returns RFC 8628 {error: string} on failure,
|
||||
// not the canonical {code, status, message} shape.
|
||||
// - When the CLI's classifyResponse encounters this, zErrorBody.safeParse
|
||||
// returns failure → serverError = undefined → generic status-based message,
|
||||
// no error.server field, no crash.
|
||||
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/oauth/device/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device_code: 'fake-invalid-device-code-e2e-test', client_id: 'difyctl' }),
|
||||
signal: AbortSignal.timeout(8_000),
|
||||
})
|
||||
// device flow errors are 4xx (400 bad_request or 401 expired_token etc.)
|
||||
expect(res.status).toBeGreaterThanOrEqual(400)
|
||||
expect(res.status).toBeLessThan(500)
|
||||
const body = await res.json() as Record<string, unknown>
|
||||
// RFC 8628 shape: has 'error' string, must NOT have ErrorBody 'code'/'status' pair
|
||||
expect(typeof body.error, 'RFC 8628 body must have a string error field').toBe('string')
|
||||
expect(body).not.toHaveProperty('status')
|
||||
// zErrorBody.safeParse would fail → CLI sets serverError = undefined → generic message
|
||||
})
|
||||
|
||||
// ── 5.76 Failed command + -o yaml → stderr is still JSON envelope ────────
|
||||
|
||||
it('[P1] 5.76 failed command with -o yaml still outputs a JSON error envelope on stderr', async () => {
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* E2E: difyctl get/create/delete/set member — Member Management
|
||||
*
|
||||
* Data lifecycle:
|
||||
* beforeAll — generates two random emails, invites them as test fixtures
|
||||
* afterAll — removes both fixtures (best-effort)
|
||||
*
|
||||
* Email format: auto_test+<timestamp>@dify.ai
|
||||
* No extra env vars required.
|
||||
*
|
||||
* JSON response shape (MemberListResponse):
|
||||
* { page, limit, total, has_more, data: MemberResponse[] }
|
||||
*
|
||||
* MemberResponse fields:
|
||||
* { id, name, email, role, status, avatar?, current: bool }
|
||||
*/
|
||||
|
||||
import type { AuthFixture } from '../../helpers/cli.js'
|
||||
import { afterAll, beforeAll, describe, expect, inject, it } from 'vitest'
|
||||
import {
|
||||
assertErrorEnvelope,
|
||||
assertExitCode,
|
||||
assertJson,
|
||||
assertNoAnsi,
|
||||
assertNonZeroExit,
|
||||
} from '../../helpers/assert.js'
|
||||
import { run, withAuthFixture, withTempConfig } from '../../helpers/cli.js'
|
||||
import { resolveEnv } from '../../setup/env.js'
|
||||
|
||||
// @ts-expect-error — see test/e2e/helpers/vitest-context.ts for explanation
|
||||
const caps = inject('e2eCapabilities') as import('../../setup/env.js').E2ECapabilities
|
||||
const E = resolveEnv(caps)
|
||||
|
||||
// ── Fixture state ─────────────────────────────────────────────────────────────
|
||||
|
||||
let fx: AuthFixture
|
||||
|
||||
/** ID of the member used by get / set tests. */
|
||||
let testMemberId: string
|
||||
|
||||
/** ID of the member reserved for the delete-success test. */
|
||||
let deleteTargetId: string
|
||||
|
||||
const ts = Date.now()
|
||||
const memberEmail = `auto_test+${ts}@dify.ai`
|
||||
const deleteTargetEmail = `auto_test+${ts + 1}@dify.ai`
|
||||
|
||||
// ── Response type helpers ─────────────────────────────────────────────────────
|
||||
|
||||
type MemberItem = {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
role: string
|
||||
status: string
|
||||
current?: boolean
|
||||
}
|
||||
|
||||
type MemberListJson = {
|
||||
data: MemberItem[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
has_more: boolean
|
||||
}
|
||||
|
||||
// ── Setup / teardown ──────────────────────────────────────────────────────────
|
||||
|
||||
beforeAll(async () => {
|
||||
fx = await withAuthFixture(E)
|
||||
|
||||
// Invite the main test member; capture member_id from response
|
||||
const createMain = await fx.r([
|
||||
'create',
|
||||
'member',
|
||||
'--email',
|
||||
memberEmail,
|
||||
'--role',
|
||||
'normal',
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
if (createMain.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`beforeAll: failed to create test member (${memberEmail}): ${createMain.stderr}`,
|
||||
)
|
||||
}
|
||||
const mainData = JSON.parse(createMain.stdout.trim()) as { member_id?: string }
|
||||
testMemberId = mainData.member_id as string
|
||||
if (!testMemberId)
|
||||
throw new Error(`beforeAll: missing member_id in: ${createMain.stdout}`)
|
||||
|
||||
// Invite the delete-target member
|
||||
const createTarget = await fx.r([
|
||||
'create',
|
||||
'member',
|
||||
'--email',
|
||||
deleteTargetEmail,
|
||||
'--role',
|
||||
'normal',
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
if (createTarget.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`beforeAll: failed to create delete-target member (${deleteTargetEmail}): ${createTarget.stderr}`,
|
||||
)
|
||||
}
|
||||
const targetData = JSON.parse(createTarget.stdout.trim()) as { member_id?: string }
|
||||
deleteTargetId = targetData.member_id as string
|
||||
if (!deleteTargetId)
|
||||
throw new Error(`beforeAll: missing member_id in: ${createTarget.stdout}`)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (testMemberId) {
|
||||
await fx.r(['delete', 'member', testMemberId, '--yes']).catch(() => {})
|
||||
}
|
||||
if (deleteTargetId) {
|
||||
await fx.r(['delete', 'member', deleteTargetId, '--yes']).catch(() => {})
|
||||
}
|
||||
await fx.cleanup()
|
||||
})
|
||||
|
||||
// ── get member ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('E2E / difyctl get member', () => {
|
||||
it('[P0] member list contains the created test member', async () => {
|
||||
const result = await fx.r(['get', 'member', '-o', 'json'])
|
||||
assertExitCode(result, 0)
|
||||
const data = assertJson<MemberListJson>(result)
|
||||
const ids = (data.data ?? []).map(m => m.id)
|
||||
expect(ids, `testMemberId ${testMemberId} must appear in member list`).toContain(testMemberId)
|
||||
})
|
||||
|
||||
it('[P0] default table output contains required column headers', async () => {
|
||||
const result = await fx.r(['get', 'member'])
|
||||
assertExitCode(result, 0)
|
||||
expect(result.stdout).toMatch(/\bID\b/)
|
||||
expect(result.stdout).toMatch(/\bNAME\b/)
|
||||
expect(result.stdout).toMatch(/\bEMAIL\b/)
|
||||
expect(result.stdout).toMatch(/\bROLE\b/)
|
||||
expect(result.stdout).toMatch(/\bSTATUS\b/)
|
||||
})
|
||||
|
||||
it('[P0] authenticated account appears in member list', async () => {
|
||||
// The token owner (auto_test@dify.ai) must appear in the member list
|
||||
const result = await fx.r(['get', 'member', '-o', 'json'])
|
||||
assertExitCode(result, 0)
|
||||
const data = assertJson<MemberListJson>(result)
|
||||
const ownerRow = data.data.find(m => m.email === E.email)
|
||||
expect(ownerRow, `owner email ${E.email} must be in member list`).toBeDefined()
|
||||
expect(ownerRow?.role).toBe('owner')
|
||||
expect(ownerRow?.status).toBe('active')
|
||||
})
|
||||
|
||||
it('[P0] -o json returns valid JSON with data array', async () => {
|
||||
const result = await fx.r(['get', 'member', '-o', 'json'])
|
||||
assertExitCode(result, 0)
|
||||
const data = assertJson<MemberListJson>(result)
|
||||
expect(Array.isArray(data.data), 'data must be an array').toBe(true)
|
||||
expect(data.data.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('[P0] -o json each member has id, email, role, status fields', async () => {
|
||||
const result = await fx.r(['get', 'member', '-o', 'json'])
|
||||
assertExitCode(result, 0)
|
||||
const data = assertJson<MemberListJson>(result)
|
||||
const member = data.data[0]!
|
||||
expect(typeof member.id).toBe('string')
|
||||
expect(typeof member.email).toBe('string')
|
||||
expect(typeof member.role).toBe('string')
|
||||
expect(typeof member.status).toBe('string')
|
||||
})
|
||||
|
||||
it('[P0] output has no ANSI colour codes (non-TTY)', async () => {
|
||||
const result = await fx.r(['get', 'member'])
|
||||
assertExitCode(result, 0)
|
||||
assertNoAnsi(result.stdout, 'stdout')
|
||||
})
|
||||
|
||||
it('[P0] unauthenticated get member returns auth error (exit code 4)', async () => {
|
||||
const tmp = await withTempConfig()
|
||||
try {
|
||||
const result = await run(['get', 'member'], { configDir: tmp.configDir })
|
||||
assertExitCode(result, 4)
|
||||
expect(result.stderr).toMatch(/not.?logged.?in|auth.?login/i)
|
||||
}
|
||||
finally {
|
||||
await tmp.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('[P1] -o yaml returns valid YAML (non-empty, no JSON braces)', async () => {
|
||||
const result = await fx.r(['get', 'member', '-o', 'yaml'])
|
||||
assertExitCode(result, 0)
|
||||
expect(result.stdout.trim().length).toBeGreaterThan(0)
|
||||
expect(result.stdout.trimStart()).not.toMatch(/^\{/)
|
||||
})
|
||||
|
||||
it('[P1] -o json output is pipe-friendly (no ANSI, ends with newline)', async () => {
|
||||
const result = await fx.r(['get', 'member', '-o', 'json'])
|
||||
assertExitCode(result, 0)
|
||||
assertNoAnsi(result.stdout, 'stdout')
|
||||
expect(result.stdout.endsWith('\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('[P1] -w overrides the workspace', async () => {
|
||||
const result = await fx.r(['get', 'member', '-w', E.workspaceId, '-o', 'json'])
|
||||
assertExitCode(result, 0)
|
||||
const data = assertJson<MemberListJson>(result)
|
||||
expect(Array.isArray(data.data)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ── set member ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('E2E / difyctl set member', () => {
|
||||
it('[P0] owner/admin can promote normal → admin', async () => {
|
||||
const result = await fx.r(['set', 'member', testMemberId, '--role', 'admin', '-o', 'json'])
|
||||
assertExitCode(result, 0)
|
||||
const list = await fx.r(['get', 'member', '-o', 'json'])
|
||||
const data = assertJson<MemberListJson>(list)
|
||||
const updated = data.data.find(m => m.id === testMemberId)
|
||||
expect(updated?.role).toBe('admin')
|
||||
})
|
||||
|
||||
it('[P0] owner/admin can demote admin → normal', async () => {
|
||||
await fx.r(['set', 'member', testMemberId, '--role', 'admin'])
|
||||
const result = await fx.r(['set', 'member', testMemberId, '--role', 'normal', '-o', 'json'])
|
||||
assertExitCode(result, 0)
|
||||
const list = await fx.r(['get', 'member', '-o', 'json'])
|
||||
const data = assertJson<MemberListJson>(list)
|
||||
const updated = data.data.find(m => m.id === testMemberId)
|
||||
expect(updated?.role).toBe('normal')
|
||||
})
|
||||
|
||||
it('[P0] --role owner is rejected client-side (exit 2, no API call)', async () => {
|
||||
const result = await fx.r(['set', 'member', testMemberId, '--role', 'owner'])
|
||||
assertExitCode(result, 2)
|
||||
expect(result.stderr).toMatch(/invalid|role|owner/i)
|
||||
})
|
||||
|
||||
it('[P0] missing --role returns usage error', async () => {
|
||||
const result = await fx.r(['set', 'member', testMemberId])
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.stderr).toMatch(/role|required|missing/i)
|
||||
})
|
||||
|
||||
it('[P0] unauthenticated set member returns auth error (exit 4)', async () => {
|
||||
const tmp = await withTempConfig()
|
||||
try {
|
||||
const result = await run(['set', 'member', testMemberId, '--role', 'normal'], {
|
||||
configDir: tmp.configDir,
|
||||
})
|
||||
assertExitCode(result, 4)
|
||||
}
|
||||
finally {
|
||||
await tmp.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('[P1] missing member-id returns usage error', async () => {
|
||||
const result = await fx.r(['set', 'member', '--role', 'normal'])
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.stderr).toMatch(/missing|required|arg|member/i)
|
||||
})
|
||||
|
||||
it('[P1] non-existent member-id returns server error', async () => {
|
||||
const result = await fx.r([
|
||||
'set',
|
||||
'member',
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'--role',
|
||||
'normal',
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
expect(result.stderr.trim().length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
// ── create member — error paths ───────────────────────────────────────────────
|
||||
|
||||
describe('E2E / difyctl create member (error paths)', () => {
|
||||
it('[P0] --role with invalid value is rejected client-side (exit 2)', async () => {
|
||||
const result = await fx.r([
|
||||
'create',
|
||||
'member',
|
||||
'--email',
|
||||
`auto_test+unused${Date.now()}@dify.ai`,
|
||||
'--role',
|
||||
'superadmin',
|
||||
])
|
||||
assertExitCode(result, 2)
|
||||
expect(result.stderr).toMatch(/invalid|role/i)
|
||||
})
|
||||
|
||||
it('[P0] --role owner is rejected client-side (exit 2)', async () => {
|
||||
const result = await fx.r([
|
||||
'create',
|
||||
'member',
|
||||
'--email',
|
||||
`auto_test+unused${Date.now()}@dify.ai`,
|
||||
'--role',
|
||||
'owner',
|
||||
])
|
||||
assertExitCode(result, 2)
|
||||
expect(result.stderr).toMatch(/invalid|role|owner/i)
|
||||
})
|
||||
|
||||
it('[P0] missing --email returns usage error', async () => {
|
||||
const result = await fx.r(['create', 'member', '--role', 'normal'])
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.stderr).toMatch(/email|required|missing/i)
|
||||
})
|
||||
|
||||
it('[P0] missing --role returns usage error', async () => {
|
||||
const result = await fx.r(['create', 'member', '--email', memberEmail])
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.stderr).toMatch(/role|required|missing/i)
|
||||
})
|
||||
|
||||
it('[P0] unauthenticated create member returns auth error (exit 4)', async () => {
|
||||
const tmp = await withTempConfig()
|
||||
try {
|
||||
const result = await run(
|
||||
['create', 'member', '--email', `auto_test+unauth${Date.now()}@dify.ai`, '--role', 'normal'],
|
||||
{ configDir: tmp.configDir },
|
||||
)
|
||||
assertExitCode(result, 4)
|
||||
}
|
||||
finally {
|
||||
await tmp.cleanup()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ── delete member ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('E2E / difyctl delete member', () => {
|
||||
it('[P0] owner/admin can remove a member from the workspace', async () => {
|
||||
const result = await fx.r(['delete', 'member', deleteTargetId, '--yes'])
|
||||
assertExitCode(result, 0)
|
||||
const list = await fx.r(['get', 'member', '-o', 'json'])
|
||||
const data = assertJson<MemberListJson>(list)
|
||||
const ids = data.data.map(m => m.id)
|
||||
expect(ids).not.toContain(deleteTargetId)
|
||||
deleteTargetId = ''
|
||||
})
|
||||
|
||||
it('[P0] attempting to delete self returns server error', async () => {
|
||||
const list = await fx.r(['get', 'member', '-o', 'json'])
|
||||
const data = assertJson<MemberListJson>(list)
|
||||
const self = data.data.find(m => m.email === E.email)
|
||||
if (!self) {
|
||||
console.warn('[E2E] could not identify self in member list — skipping')
|
||||
return
|
||||
}
|
||||
const result = await fx.r(['delete', 'member', self.id, '--yes'])
|
||||
assertNonZeroExit(result)
|
||||
expect(result.stderr).toMatch(/self|yourself|cannot|not.*allow|400|forbidden/i)
|
||||
})
|
||||
|
||||
it('[P0] missing member-id argument returns usage error', async () => {
|
||||
const result = await fx.r(['delete', 'member'])
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.stderr).toMatch(/missing|required|arg|member/i)
|
||||
})
|
||||
|
||||
it('[P0] unauthenticated delete member returns auth error (exit 4)', async () => {
|
||||
const tmp = await withTempConfig()
|
||||
try {
|
||||
const result = await run(
|
||||
['delete', 'member', '00000000-0000-0000-0000-000000000000', '--yes'],
|
||||
{ configDir: tmp.configDir },
|
||||
)
|
||||
assertExitCode(result, 4)
|
||||
}
|
||||
finally {
|
||||
await tmp.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('[P1] non-existent member-id returns server error', async () => {
|
||||
const result = await fx.r([
|
||||
'delete',
|
||||
'member',
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'--yes',
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
expect(result.stderr.trim().length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('[P1] -o json outputs structured envelope on error', async () => {
|
||||
const result = await fx.r([
|
||||
'delete',
|
||||
'member',
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'--yes',
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
assertErrorEnvelope(result)
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@
|
||||
*
|
||||
* Staging app prerequisites (specified via DIFY_E2E_* env vars):
|
||||
* echo-chat — mode=chat, query variable, outputs "echo: {query}"
|
||||
* echo-workflow — mode=workflow, x variable (required), outputs "echo: {x}"
|
||||
* echo-workflow — mode=workflow, x variable (required), outputs x directly (no echo prefix)
|
||||
*/
|
||||
|
||||
import type { AuthFixture } from '../../helpers/cli.js'
|
||||
@@ -263,7 +263,7 @@ describe('E2E / difyctl run app', () => {
|
||||
JSON.stringify({ x: 'hello', num: 42, enum_var: 'A', paragraph: 'short text' }),
|
||||
])
|
||||
assertExitCode(happyResult, 0)
|
||||
assertStdoutContains(happyResult, 'echo:hello')
|
||||
assertStdoutContains(happyResult, 'hello') // workflow outputs x directly; echo: prefix removed (no sandbox on server)
|
||||
|
||||
// ── 4.1.17: number field receives a string value ─────────────────────
|
||||
const typedResult = await fx.r([
|
||||
@@ -289,6 +289,26 @@ describe('E2E / difyctl run app', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('[P1] validation failure returns http_status 422 in JSON error envelope', async () => {
|
||||
// After the @accepts/@returns server contract unification, input schema
|
||||
// validation failures consistently return HTTP 422 (not 400 or 500).
|
||||
// This verifies the CLI propagates the unified status code.
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
E.workflowAppId,
|
||||
'--inputs',
|
||||
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
const envelope = JSON.parse(result.stderr.trim()) as {
|
||||
error: { code: string, message: string, http_status?: number }
|
||||
}
|
||||
expect(envelope.error.http_status, 'validation failure must return http_status 422').toBe(422)
|
||||
})
|
||||
|
||||
// =========================================================================
|
||||
// Error scenarios
|
||||
// =========================================================================
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('E2E / difyctl run app --conversation', () => {
|
||||
'invalid-conv-id-xyz-not-exist',
|
||||
])
|
||||
assertExitCode(result, 1)
|
||||
expect(result.stderr).toMatch(/not.?found|conversation|404/i)
|
||||
expect(result.stderr).toMatch(/not.?found|conversation|404|422|validation/i)
|
||||
})
|
||||
|
||||
// ── Combined flags ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -277,7 +277,7 @@ describe('E2E / difyctl run app --stream (specialisation)', () => {
|
||||
'--stream',
|
||||
])
|
||||
expect(result.exitCode, 'wrong-type input should cause non-zero exit').not.toBe(0)
|
||||
expect(result.stderr).toMatch(/validation|invalid|type|400|server_5xx|must be/i)
|
||||
expect(result.stderr).toMatch(/validation|invalid|type|422|must be/i)
|
||||
})
|
||||
|
||||
// ── Non-existent app with positional query (4.2.16) ────────────────────
|
||||
|
||||
@@ -92,6 +92,8 @@ export default defineConfig({
|
||||
'test/e2e/suites/framework/**/*.e2e.ts',
|
||||
// discovery (get app / describe app)
|
||||
'test/e2e/suites/discovery/**/*.e2e.ts',
|
||||
// member management (get/create/delete/set member)
|
||||
'test/e2e/suites/member/**/*.e2e.ts',
|
||||
// dsl (export / import)
|
||||
'test/e2e/suites/dsl/**/*.e2e.ts',
|
||||
// run tests (require valid token)
|
||||
|
||||
@@ -18,11 +18,13 @@ export type AgentAppCreatePayload = {
|
||||
icon_background?: string | null
|
||||
icon_type?: IconType | null
|
||||
name: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
export type AppDetailWithSite = {
|
||||
access_mode?: string | null
|
||||
api_base_url?: string | null
|
||||
app_id?: string | null
|
||||
bound_agent_id?: string | null
|
||||
created_at?: number | null
|
||||
created_by?: string | null
|
||||
@@ -39,6 +41,7 @@ export type AppDetailWithSite = {
|
||||
mode: string
|
||||
model_config?: ModelConfig | null
|
||||
name: string
|
||||
role?: string | null
|
||||
site?: Site | null
|
||||
tags?: Array<Tag>
|
||||
tracing?: JsonValue | null
|
||||
@@ -56,13 +59,14 @@ export type AgentInviteOptionsResponse = {
|
||||
total: number
|
||||
}
|
||||
|
||||
export type UpdateAppPayload = {
|
||||
export type AgentAppUpdatePayload = {
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: IconType | null
|
||||
max_active_requests?: number | null
|
||||
name: string
|
||||
role?: string | null
|
||||
use_icon_as_answer_icon?: boolean | null
|
||||
}
|
||||
|
||||
@@ -255,6 +259,7 @@ export type AgentConfigSnapshotDetailResponse = {
|
||||
|
||||
export type AppPartial = {
|
||||
access_mode?: string | null
|
||||
app_id?: string | null
|
||||
author_name?: string | null
|
||||
bound_agent_id?: string | null
|
||||
create_user_name?: string | null
|
||||
@@ -272,6 +277,7 @@ export type AppPartial = {
|
||||
mode: string
|
||||
model_config?: ModelConfigPartial | null
|
||||
name: string
|
||||
role?: string | null
|
||||
tags?: Array<Tag>
|
||||
updated_at?: number | null
|
||||
updated_by?: string | null
|
||||
@@ -1219,6 +1225,7 @@ export type AppPaginationWritable = {
|
||||
export type AppDetailWithSiteWritable = {
|
||||
access_mode?: string | null
|
||||
api_base_url?: string | null
|
||||
app_id?: string | null
|
||||
bound_agent_id?: string | null
|
||||
created_at?: number | null
|
||||
created_by?: string | null
|
||||
@@ -1234,6 +1241,7 @@ export type AppDetailWithSiteWritable = {
|
||||
mode: string
|
||||
model_config?: ModelConfig | null
|
||||
name: string
|
||||
role?: string | null
|
||||
site?: SiteWritable | null
|
||||
tags?: Array<Tag>
|
||||
tracing?: JsonValue | null
|
||||
@@ -1245,6 +1253,7 @@ export type AppDetailWithSiteWritable = {
|
||||
|
||||
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
|
||||
@@ -1261,6 +1270,7 @@ export type AppPartialWritable = {
|
||||
mode: string
|
||||
model_config?: ModelConfigPartial | null
|
||||
name: string
|
||||
role?: string | null
|
||||
tags?: Array<Tag>
|
||||
updated_at?: number | null
|
||||
updated_by?: string | null
|
||||
@@ -1387,7 +1397,7 @@ export type GetAgentByAgentIdResponses = {
|
||||
export type GetAgentByAgentIdResponse = GetAgentByAgentIdResponses[keyof GetAgentByAgentIdResponses]
|
||||
|
||||
export type PutAgentByAgentIdData = {
|
||||
body: UpdateAppPayload
|
||||
body: AgentAppUpdatePayload
|
||||
path: {
|
||||
agent_id: string
|
||||
}
|
||||
|
||||
@@ -92,18 +92,20 @@ export const zAgentAppCreatePayload = z.object({
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: zIconType.nullish(),
|
||||
name: z.string().min(1),
|
||||
role: z.string().max(255).optional().default(''),
|
||||
})
|
||||
|
||||
/**
|
||||
* UpdateAppPayload
|
||||
* AgentAppUpdatePayload
|
||||
*/
|
||||
export const zUpdateAppPayload = z.object({
|
||||
export const zAgentAppUpdatePayload = z.object({
|
||||
description: z.string().max(400).nullish(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: zIconType.nullish(),
|
||||
max_active_requests: z.int().nullish(),
|
||||
name: z.string().min(1),
|
||||
role: z.string().max(255).nullish(),
|
||||
use_icon_as_answer_icon: z.boolean().nullish(),
|
||||
})
|
||||
|
||||
@@ -473,6 +475,7 @@ export const zModelConfigPartial = z.object({
|
||||
*/
|
||||
export const zAppPartial = 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(),
|
||||
@@ -490,6 +493,7 @@ export const zAppPartial = z.object({
|
||||
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(),
|
||||
@@ -531,6 +535,7 @@ export const zModelConfig = z.object({
|
||||
export const zAppDetailWithSite = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
api_base_url: z.string().nullish(),
|
||||
app_id: z.string().nullish(),
|
||||
bound_agent_id: z.string().nullish(),
|
||||
created_at: z.int().nullish(),
|
||||
created_by: z.string().nullish(),
|
||||
@@ -547,6 +552,7 @@ 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(),
|
||||
@@ -1729,6 +1735,7 @@ export const zMessageInfiniteScrollPaginationResponse = z.object({
|
||||
*/
|
||||
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(),
|
||||
@@ -1745,6 +1752,7 @@ export const zAppPartialWritable = z.object({
|
||||
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(),
|
||||
@@ -1788,6 +1796,7 @@ export const zSiteWritable = z.object({
|
||||
export const zAppDetailWithSiteWritable = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
api_base_url: z.string().nullish(),
|
||||
app_id: z.string().nullish(),
|
||||
bound_agent_id: z.string().nullish(),
|
||||
created_at: z.int().nullish(),
|
||||
created_by: z.string().nullish(),
|
||||
@@ -1803,6 +1812,7 @@ 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(),
|
||||
@@ -1880,7 +1890,7 @@ export const zGetAgentByAgentIdPath = z.object({
|
||||
*/
|
||||
export const zGetAgentByAgentIdResponse = zAppDetailWithSite
|
||||
|
||||
export const zPutAgentByAgentIdBody = zUpdateAppPayload
|
||||
export const zPutAgentByAgentIdBody = zAgentAppUpdatePayload
|
||||
|
||||
export const zPutAgentByAgentIdPath = z.object({
|
||||
agent_id: z.string(),
|
||||
|
||||
@@ -24,6 +24,7 @@ export type CreateAppPayload = {
|
||||
export type AppDetailWithSite = {
|
||||
access_mode?: string | null
|
||||
api_base_url?: string | null
|
||||
app_id?: string | null
|
||||
bound_agent_id?: string | null
|
||||
created_at?: number | null
|
||||
created_by?: string | null
|
||||
@@ -40,6 +41,7 @@ export type AppDetailWithSite = {
|
||||
mode: string
|
||||
model_config?: ModelConfig | null
|
||||
name: string
|
||||
role?: string | null
|
||||
site?: Site | null
|
||||
tags?: Array<Tag>
|
||||
tracing?: JsonValue | null
|
||||
@@ -1153,6 +1155,7 @@ export type ApiKeyItem = {
|
||||
|
||||
export type AppPartial = {
|
||||
access_mode?: string | null
|
||||
app_id?: string | null
|
||||
author_name?: string | null
|
||||
bound_agent_id?: string | null
|
||||
create_user_name?: string | null
|
||||
@@ -1170,6 +1173,7 @@ export type AppPartial = {
|
||||
mode: string
|
||||
model_config?: ModelConfigPartial | null
|
||||
name: string
|
||||
role?: string | null
|
||||
tags?: Array<Tag>
|
||||
updated_at?: number | null
|
||||
updated_by?: string | null
|
||||
@@ -2540,6 +2544,7 @@ export type AppPaginationWritable = {
|
||||
export type AppDetailWithSiteWritable = {
|
||||
access_mode?: string | null
|
||||
api_base_url?: string | null
|
||||
app_id?: string | null
|
||||
bound_agent_id?: string | null
|
||||
created_at?: number | null
|
||||
created_by?: string | null
|
||||
@@ -2555,6 +2560,7 @@ export type AppDetailWithSiteWritable = {
|
||||
mode: string
|
||||
model_config?: ModelConfig | null
|
||||
name: string
|
||||
role?: string | null
|
||||
site?: SiteWritable | null
|
||||
tags?: Array<Tag>
|
||||
tracing?: JsonValue | null
|
||||
@@ -2589,6 +2595,7 @@ export type WorkflowCommentDetailWritable = {
|
||||
|
||||
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
|
||||
@@ -2605,6 +2612,7 @@ export type AppPartialWritable = {
|
||||
mode: string
|
||||
model_config?: ModelConfigPartial | null
|
||||
name: string
|
||||
role?: string | null
|
||||
tags?: Array<Tag>
|
||||
updated_at?: number | null
|
||||
updated_by?: string | null
|
||||
|
||||
@@ -1946,6 +1946,7 @@ export const zModelConfigPartial = z.object({
|
||||
*/
|
||||
export const zAppPartial = 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(),
|
||||
@@ -1963,6 +1964,7 @@ export const zAppPartial = z.object({
|
||||
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(),
|
||||
@@ -2004,6 +2006,7 @@ export const zModelConfig = z.object({
|
||||
export const zAppDetailWithSite = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
api_base_url: z.string().nullish(),
|
||||
app_id: z.string().nullish(),
|
||||
bound_agent_id: z.string().nullish(),
|
||||
created_at: z.int().nullish(),
|
||||
created_by: z.string().nullish(),
|
||||
@@ -2020,6 +2023,7 @@ 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(),
|
||||
@@ -3433,6 +3437,7 @@ export const zGeneratedAppResponseWritable = zJsonValue
|
||||
*/
|
||||
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(),
|
||||
@@ -3449,6 +3454,7 @@ export const zAppPartialWritable = z.object({
|
||||
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(),
|
||||
@@ -3492,6 +3498,7 @@ export const zSiteWritable = z.object({
|
||||
export const zAppDetailWithSiteWritable = z.object({
|
||||
access_mode: z.string().nullish(),
|
||||
api_base_url: z.string().nullish(),
|
||||
app_id: z.string().nullish(),
|
||||
bound_agent_id: z.string().nullish(),
|
||||
created_at: z.int().nullish(),
|
||||
created_by: z.string().nullish(),
|
||||
@@ -3507,6 +3514,7 @@ 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(),
|
||||
|
||||
Generated
+172
-89
@@ -607,6 +607,8 @@ overrides:
|
||||
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
|
||||
picomatch@>=4.0.0 <4.0.4: 4.0.4
|
||||
postcss@<8.5.10: ^8.5.10
|
||||
postcss-selector-parser@>=6.0.0 <6.1.3: 6.1.4
|
||||
postcss-selector-parser@>=7.0.0 <7.1.3: 7.1.4
|
||||
rollup@>=4.0.0 <4.59.0: 4.61.1
|
||||
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
|
||||
side-channel: npm:@nolyfill/side-channel@^1.0.44
|
||||
@@ -617,6 +619,9 @@ overrides:
|
||||
ws@>=8.0.0 <8.20.1: ^8.21.0
|
||||
yaml@>=2.0.0 <2.8.3: 2.9.0
|
||||
yauzl@<3.2.1: 3.2.1
|
||||
'@babel/core@<=7.29.0': ^7.29.1
|
||||
js-yaml@<=4.1.1: ^4.1.2
|
||||
tar@<=7.5.15: ^7.5.16
|
||||
|
||||
importers:
|
||||
|
||||
@@ -1183,7 +1188,7 @@ importers:
|
||||
version: 11.1.8
|
||||
jotai:
|
||||
specifier: 'catalog:'
|
||||
version: 2.20.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.17)(react@19.2.7)
|
||||
version: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)
|
||||
js-audio-recorder:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.7
|
||||
@@ -1228,13 +1233,13 @@ importers:
|
||||
version: 1.0.0
|
||||
next:
|
||||
specifier: 'catalog:'
|
||||
version: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
version: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
next-themes:
|
||||
specifier: 'catalog:'
|
||||
version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
nuqs:
|
||||
specifier: 'catalog:'
|
||||
version: 2.8.9(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
|
||||
version: 2.8.9(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
|
||||
pinyin-pro:
|
||||
specifier: 'catalog:'
|
||||
version: 3.28.1
|
||||
@@ -1394,7 +1399,7 @@ importers:
|
||||
version: 10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))
|
||||
'@storybook/nextjs-vite':
|
||||
specifier: 'catalog:'
|
||||
version: 10.4.4(@babel/core@7.29.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)
|
||||
version: 10.4.4(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)
|
||||
'@storybook/react':
|
||||
specifier: 'catalog:'
|
||||
version: 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)
|
||||
@@ -1532,7 +1537,7 @@ importers:
|
||||
version: 3.19.3
|
||||
vinext:
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3)
|
||||
version: 0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3)
|
||||
vite:
|
||||
specifier: npm:@voidzero-dev/vite-plus-core@0.1.24
|
||||
version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)'
|
||||
@@ -1703,56 +1708,81 @@ packages:
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/compat-data@7.29.0':
|
||||
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
|
||||
'@babel/code-frame@7.29.7':
|
||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/core@7.29.0':
|
||||
resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
|
||||
'@babel/compat-data@7.29.7':
|
||||
resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/core@7.29.7':
|
||||
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/generator@7.29.1':
|
||||
resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-compilation-targets@7.28.6':
|
||||
resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
|
||||
'@babel/generator@7.29.7':
|
||||
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-compilation-targets@7.29.7':
|
||||
resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-globals@7.28.0':
|
||||
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-module-imports@7.28.6':
|
||||
resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
|
||||
'@babel/helper-globals@7.29.7':
|
||||
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-module-transforms@7.28.6':
|
||||
resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
|
||||
'@babel/helper-module-imports@7.29.7':
|
||||
resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-module-transforms@7.29.7':
|
||||
resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
'@babel/core': ^7.29.1
|
||||
|
||||
'@babel/helper-string-parser@7.27.1':
|
||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-string-parser@7.29.7':
|
||||
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5':
|
||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-option@7.27.1':
|
||||
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
||||
'@babel/helper-validator-identifier@7.29.7':
|
||||
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helpers@7.29.2':
|
||||
resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
|
||||
'@babel/helper-validator-option@7.29.7':
|
||||
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helpers@7.29.7':
|
||||
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/parser@7.29.2':
|
||||
resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@babel/parser@7.29.7':
|
||||
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/runtime@7.29.2':
|
||||
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1761,14 +1791,26 @@ packages:
|
||||
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.29.7':
|
||||
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/traverse@7.29.0':
|
||||
resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/traverse@7.29.7':
|
||||
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.29.7':
|
||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@base-ui/react@1.5.0':
|
||||
resolution: {integrity: sha512-z1gSAlced1yY+iM+mHDEtIkD8UI3Ebs52MuBPxvV6f5hRutk+xvCH/wuB7hDqDzK9JG5FoMz5nhrqtSs1wjt1A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -5715,6 +5757,7 @@ packages:
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
cssfontparser@1.2.1:
|
||||
resolution: {integrity: sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==}
|
||||
@@ -7159,7 +7202,7 @@ packages:
|
||||
resolution: {integrity: sha512-dnuKfU/GLi8B28RRMjQ3AfoN7kfzP8o41+AX2FmITZqEMY8PHnjABq+VkEooomLwYaGjda+pgy0yFSjaHX/ZPg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0'
|
||||
'@babel/core': ^7.29.1
|
||||
'@babel/template': '>=7.0.0'
|
||||
'@types/react': '>=17.0.0'
|
||||
react: '>=17.0.0'
|
||||
@@ -7191,9 +7234,6 @@ packages:
|
||||
js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
|
||||
js-yaml@4.2.0:
|
||||
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
|
||||
|
||||
@@ -8243,12 +8283,12 @@ packages:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
postcss-selector-parser@6.0.10:
|
||||
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
|
||||
postcss-selector-parser@6.1.4:
|
||||
resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-selector-parser@7.1.1:
|
||||
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
|
||||
postcss-selector-parser@7.1.4:
|
||||
resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
@@ -8984,8 +9024,8 @@ packages:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tar@7.5.11:
|
||||
resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
|
||||
tar@7.5.16:
|
||||
resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tiny-case@1.0.3:
|
||||
@@ -9955,19 +9995,25 @@ snapshots:
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/compat-data@7.29.0': {}
|
||||
|
||||
'@babel/core@7.29.0':
|
||||
'@babel/code-frame@7.29.7':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
'@babel/generator': 7.29.1
|
||||
'@babel/helper-compilation-targets': 7.28.6
|
||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/helpers': 7.29.2
|
||||
'@babel/parser': 7.29.2
|
||||
'@babel/template': 7.28.6
|
||||
'@babel/traverse': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/compat-data@7.29.7': {}
|
||||
|
||||
'@babel/core@7.29.7':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/generator': 7.29.7
|
||||
'@babel/helper-compilation-targets': 7.29.7
|
||||
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
|
||||
'@babel/helpers': 7.29.7
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
@@ -9985,47 +10031,65 @@ snapshots:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jsesc: 3.1.0
|
||||
|
||||
'@babel/helper-compilation-targets@7.28.6':
|
||||
'@babel/generator@7.29.7':
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.29.0
|
||||
'@babel/helper-validator-option': 7.27.1
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jsesc: 3.1.0
|
||||
|
||||
'@babel/helper-compilation-targets@7.29.7':
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.29.7
|
||||
'@babel/helper-validator-option': 7.29.7
|
||||
browserslist: 4.28.1
|
||||
lru-cache: 5.1.1
|
||||
semver: 6.3.1
|
||||
|
||||
'@babel/helper-globals@7.28.0': {}
|
||||
|
||||
'@babel/helper-module-imports@7.28.6':
|
||||
'@babel/helper-globals@7.29.7': {}
|
||||
|
||||
'@babel/helper-module-imports@7.29.7':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
|
||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-module-imports': 7.28.6
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
'@babel/traverse': 7.29.0
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/helper-module-imports': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
'@babel/traverse': 7.29.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
|
||||
'@babel/helper-string-parser@7.29.7': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5': {}
|
||||
|
||||
'@babel/helper-validator-option@7.27.1': {}
|
||||
'@babel/helper-validator-identifier@7.29.7': {}
|
||||
|
||||
'@babel/helpers@7.29.2':
|
||||
'@babel/helper-validator-option@7.29.7': {}
|
||||
|
||||
'@babel/helpers@7.29.7':
|
||||
dependencies:
|
||||
'@babel/template': 7.28.6
|
||||
'@babel/types': 7.29.0
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
|
||||
'@babel/parser@7.29.2':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/parser@7.29.7':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.7
|
||||
|
||||
'@babel/runtime@7.29.2': {}
|
||||
|
||||
'@babel/template@7.28.6':
|
||||
@@ -10034,6 +10098,12 @@ snapshots:
|
||||
'@babel/parser': 7.29.2
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/template@7.29.7':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
|
||||
'@babel/traverse@7.29.0':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
@@ -10046,11 +10116,28 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/traverse@7.29.7':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/generator': 7.29.7
|
||||
'@babel/helper-globals': 7.29.7
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@babel/types@7.29.7':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
||||
'@base-ui/react@1.5.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
@@ -10761,7 +10848,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
js-yaml: 4.1.1
|
||||
js-yaml: 4.2.0
|
||||
|
||||
'@hey-api/openapi-ts@0.98.2(magicast@0.5.2)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
@@ -10831,7 +10918,7 @@ snapshots:
|
||||
local-pkg: 1.1.2
|
||||
pathe: 2.0.3
|
||||
svgo: 3.3.3
|
||||
tar: 7.5.11
|
||||
tar: 7.5.16
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12348,18 +12435,18 @@ snapshots:
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
|
||||
'@storybook/nextjs-vite@10.4.4(@babel/core@7.29.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)':
|
||||
'@storybook/nextjs-vite@10.4.4(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@storybook/builder-vite': 10.4.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))
|
||||
'@storybook/react': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)
|
||||
'@storybook/react-vite': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)
|
||||
next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.7)
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7)
|
||||
vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)'
|
||||
vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)
|
||||
vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.17
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
||||
@@ -12541,7 +12628,7 @@ snapshots:
|
||||
|
||||
'@tailwindcss/typography@0.5.20(tailwindcss@4.3.1)':
|
||||
dependencies:
|
||||
postcss-selector-parser: 6.0.10
|
||||
postcss-selector-parser: 6.1.4
|
||||
tailwindcss: 4.3.1
|
||||
|
||||
'@tailwindcss/vite@4.3.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))':
|
||||
@@ -13099,13 +13186,13 @@ snapshots:
|
||||
dependencies:
|
||||
unpic: 4.2.2
|
||||
|
||||
'@unpic/react@1.0.2(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||
'@unpic/react@1.0.2(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||
dependencies:
|
||||
'@unpic/core': 1.0.3
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
optionalDependencies:
|
||||
next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
|
||||
'@upsetjs/venn.js@2.0.0':
|
||||
optionalDependencies:
|
||||
@@ -15206,7 +15293,7 @@ snapshots:
|
||||
eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2)
|
||||
natural-compare: 1.4.0
|
||||
nth-check: 2.1.1
|
||||
postcss-selector-parser: 7.1.1
|
||||
postcss-selector-parser: 7.1.4
|
||||
semver: 7.8.4
|
||||
vue-eslint-parser: 10.4.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
||||
xml-name-validator: 4.0.0
|
||||
@@ -15220,7 +15307,7 @@ snapshots:
|
||||
eslint: 10.5.0(jiti@2.7.0)
|
||||
natural-compare: 1.4.0
|
||||
nth-check: 2.1.1
|
||||
postcss-selector-parser: 7.1.1
|
||||
postcss-selector-parser: 7.1.4
|
||||
semver: 7.8.4
|
||||
vue-eslint-parser: 10.4.0(eslint@10.5.0(jiti@2.7.0))
|
||||
xml-name-validator: 4.0.0
|
||||
@@ -16095,10 +16182,10 @@ snapshots:
|
||||
|
||||
jiti@2.7.0: {}
|
||||
|
||||
jotai@2.20.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.17)(react@19.2.7):
|
||||
jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/template': 7.28.6
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/template': 7.29.7
|
||||
'@types/react': 19.2.17
|
||||
react: 19.2.7
|
||||
|
||||
@@ -16114,10 +16201,6 @@ snapshots:
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
js-yaml@4.2.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
@@ -16975,7 +17058,7 @@ snapshots:
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
|
||||
next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
'@next/env': 16.2.9
|
||||
'@swc/helpers': 0.5.15
|
||||
@@ -16984,7 +17067,7 @@ snapshots:
|
||||
postcss: 8.5.15
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.7)
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.2.9
|
||||
'@next/swc-darwin-x64': 16.2.9
|
||||
@@ -17030,12 +17113,12 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nuqs@2.8.9(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
|
||||
nuqs@2.8.9(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
react: 19.2.7
|
||||
optionalDependencies:
|
||||
next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
@@ -17487,12 +17570,12 @@ snapshots:
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-selector-parser@6.0.10:
|
||||
postcss-selector-parser@6.1.4:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
postcss-selector-parser@7.1.1:
|
||||
postcss-selector-parser@7.1.4:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
@@ -17586,7 +17669,7 @@ snapshots:
|
||||
|
||||
react-docgen@8.0.3:
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/traverse': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
'@types/babel__core': 7.20.5
|
||||
@@ -18387,12 +18470,12 @@ snapshots:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.7
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.7):
|
||||
styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.7
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.29.7
|
||||
|
||||
stylis@4.3.6: {}
|
||||
|
||||
@@ -18447,7 +18530,7 @@ snapshots:
|
||||
readable-stream: 3.6.2
|
||||
optional: true
|
||||
|
||||
tar@7.5.11:
|
||||
tar@7.5.16:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
@@ -18782,9 +18865,9 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vinext@0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3):
|
||||
vinext@0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@unpic/react': 1.0.2(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@unpic/react': 1.0.2(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@vercel/og': 0.8.6
|
||||
'@vitejs/plugin-react': 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))
|
||||
image-size: 2.0.2
|
||||
@@ -18837,13 +18920,13 @@ snapshots:
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3):
|
||||
vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@next/env': 16.0.0
|
||||
image-size: 2.0.2
|
||||
magic-string: 0.30.21
|
||||
module-alias: 2.3.4
|
||||
next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))
|
||||
ts-dedent: 2.2.0
|
||||
vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)'
|
||||
|
||||
@@ -27,19 +27,24 @@ packages:
|
||||
- packages/*
|
||||
- cli
|
||||
overrides:
|
||||
'@babel/core@<=7.29.0': ^7.29.1
|
||||
'@lexical/code': npm:lexical-code-no-prism@0.41.0
|
||||
canvas: ^3.2.3
|
||||
esbuild@<0.27.2: 0.27.2
|
||||
esbuild@>=0.17.0 <0.28.1: ^0.28.1
|
||||
esbuild@>=0.27.3 <0.28.1: ^0.28.1
|
||||
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
|
||||
js-yaml@<=4.1.1: ^4.1.2
|
||||
picomatch@>=4.0.0 <4.0.4: 4.0.4
|
||||
postcss-selector-parser@>=6.0.0 <6.1.3: 6.1.4
|
||||
postcss-selector-parser@>=7.0.0 <7.1.3: 7.1.4
|
||||
postcss@<8.5.10: ^8.5.10
|
||||
rollup@>=4.0.0 <4.59.0: 4.61.1
|
||||
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
|
||||
side-channel: npm:@nolyfill/side-channel@^1.0.44
|
||||
solid-js: 1.9.13
|
||||
string-width: ~8.2.1
|
||||
tar@<=7.5.15: ^7.5.16
|
||||
vite: npm:@voidzero-dev/vite-plus-core@0.1.24
|
||||
vitest: npm:@voidzero-dev/vite-plus-test@0.1.24
|
||||
ws@>=8.0.0 <8.20.1: ^8.21.0
|
||||
|
||||
Reference in New Issue
Block a user