refactor(web): migrate snippet console contracts (#38258)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou 2026-07-02 00:31:39 +08:00 committed by GitHub
parent 37bcb60c26
commit 8540ca9242
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 674 additions and 795 deletions

View File

@ -1,11 +1,13 @@
import logging
import re
from datetime import datetime
from typing import Any
from urllib.parse import quote
from flask import Response, request
from flask_restx import Resource, marshal
from pydantic import RootModel
from pydantic import Field as PydanticField
from pydantic import field_validator
from sqlalchemy.orm import Session, sessionmaker
from werkzeug.datastructures import MultiDict
from werkzeug.exceptions import NotFound
@ -30,27 +32,34 @@ from controllers.console.wraps import (
with_current_tenant_id,
with_current_user,
)
from core.plugin.entities.plugin import PluginDependency
from extensions.ext_database import db
from fields.base import ResponseModel
from fields.snippet_fields import snippet_fields, snippet_list_fields, snippet_pagination_fields
from fields.snippet_fields import snippet_fields, snippet_list_fields
from libs.helper import to_timestamp
from libs.login import login_required
from models import Account
from models.snippet import SnippetType
from services.app_dsl_service import ImportStatus
from services.snippet_dsl_service import SnippetDslService
from services.snippet_dsl_service import ImportStatus, SnippetDslService
from services.snippet_service import SnippetService
logger = logging.getLogger(__name__)
_TAG_IDS_BRACKET_PATTERN = re.compile(r"^tag_ids\[(\d+)\]$")
_CREATOR_IDS_BRACKET_PATTERN = re.compile(r"^creator_ids\[(\d+)\]$")
_CREATORS_BRACKET_PATTERN = re.compile(r"^creators\[(\d+)\]$")
class SnippetImportResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class SnippetImportResponse(ResponseModel):
id: str
status: ImportStatus
snippet_id: str | None
current_dsl_version: str
imported_dsl_version: str
error: str
class SnippetDependencyCheckResponse(RootModel[dict[str, Any]]):
root: dict[str, Any]
class SnippetDependencyCheckResponse(ResponseModel):
leaked_dependencies: list[PluginDependency]
class SnippetUseCountResponse(ResponseModel):
@ -58,6 +67,77 @@ class SnippetUseCountResponse(ResponseModel):
use_count: int
class SnippetTagResponse(ResponseModel):
id: str
name: str
type: str
class SnippetAccountResponse(ResponseModel):
id: str
name: str
email: str
class SnippetListItemResponse(ResponseModel):
id: str
name: str
description: str | None
type: SnippetType
version: int
use_count: int
is_published: bool
icon_info: dict[str, Any] | None
tags: list[SnippetTagResponse]
created_by: str | None
author_name: str | None
created_at: int
updated_by: str | None
updated_at: int
@field_validator("created_at", "updated_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int:
timestamp = to_timestamp(value)
if timestamp is None:
raise ValueError("timestamp is required")
return timestamp
class SnippetResponse(ResponseModel):
id: str
name: str
description: str | None
type: SnippetType
version: int
use_count: int
is_published: bool
icon_info: dict[str, Any] | None
graph: dict[str, Any] = PydanticField(validation_alias="graph_dict")
input_fields: list[dict[str, Any]] = PydanticField(validation_alias="input_fields_list")
tags: list[SnippetTagResponse]
created_by: SnippetAccountResponse | None = PydanticField(validation_alias="created_by_account")
created_at: int
updated_by: SnippetAccountResponse | None = PydanticField(validation_alias="updated_by_account")
updated_at: int
@field_validator("created_at", "updated_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int:
timestamp = to_timestamp(value)
if timestamp is None:
raise ValueError("timestamp is required")
return timestamp
class SnippetPaginationResponse(ResponseModel):
data: list[SnippetListItemResponse]
page: int
limit: int
total: int
has_more: bool
def _snippet_service() -> SnippetService:
return SnippetService(sessionmaker(bind=db.engine, expire_on_commit=False))
@ -73,11 +153,19 @@ def _normalize_snippet_list_query_args(query_args: MultiDict[str, str]) -> dict[
indexed_tag_ids.extend((int(match.group(1)), value) for value in query_args.getlist(key))
continue
match = _CREATOR_IDS_BRACKET_PATTERN.fullmatch(key)
match = _CREATOR_IDS_BRACKET_PATTERN.fullmatch(key) or _CREATORS_BRACKET_PATTERN.fullmatch(key)
if match:
indexed_creator_ids.extend((int(match.group(1)), value) for value in query_args.getlist(key))
continue
if key in {"tag_ids", "creators", "creator_ids"}:
values = query_args.getlist(key)
if values:
normalized["creators" if key in {"creators", "creator_ids"} else key] = (
values if len(values) > 1 else values[0]
)
continue
value = query_args.get(key)
if value is not None:
normalized[key] = value
@ -105,19 +193,17 @@ register_response_schema_models(
SnippetImportResponse,
SnippetDependencyCheckResponse,
SnippetUseCountResponse,
SnippetListItemResponse,
SnippetResponse,
SnippetPaginationResponse,
)
# Create namespace models for marshaling
snippet_model = console_ns.model("Snippet", snippet_fields)
snippet_list_model = console_ns.model("SnippetList", snippet_list_fields)
snippet_pagination_model = console_ns.model("SnippetPagination", snippet_pagination_fields)
@console_ns.route("/workspaces/current/customized-snippets")
class CustomizedSnippetsApi(Resource):
@console_ns.doc("list_customized_snippets")
@console_ns.doc(params=query_params_from_model(SnippetListQuery))
@console_ns.response(200, "Snippets retrieved successfully", snippet_pagination_model)
@console_ns.response(200, "Snippets retrieved successfully", console_ns.models[SnippetPaginationResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@ -148,7 +234,7 @@ class CustomizedSnippetsApi(Resource):
@console_ns.doc("create_customized_snippet")
@console_ns.expect(console_ns.models.get(CreateSnippetPayload.__name__))
@console_ns.response(201, "Snippet created successfully", snippet_model)
@console_ns.response(201, "Snippet created successfully", console_ns.models[SnippetResponse.__name__])
@console_ns.response(400, "Invalid request")
@setup_required
@login_required
@ -191,7 +277,7 @@ class CustomizedSnippetsApi(Resource):
@console_ns.route("/workspaces/current/customized-snippets/<uuid:snippet_id>")
class CustomizedSnippetDetailApi(Resource):
@console_ns.doc("get_customized_snippet")
@console_ns.response(200, "Snippet retrieved successfully", snippet_model)
@console_ns.response(200, "Snippet retrieved successfully", console_ns.models[SnippetResponse.__name__])
@console_ns.response(404, "Snippet not found")
@setup_required
@login_required
@ -212,7 +298,7 @@ class CustomizedSnippetDetailApi(Resource):
@console_ns.doc("update_customized_snippet")
@console_ns.expect(console_ns.models.get(UpdateSnippetPayload.__name__))
@console_ns.response(200, "Snippet updated successfully", snippet_model)
@console_ns.response(200, "Snippet updated successfully", console_ns.models[SnippetResponse.__name__])
@console_ns.response(400, "Invalid request")
@console_ns.response(404, "Snippet not found")
@setup_required

View File

@ -9761,7 +9761,7 @@ Get list of available agent providers
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Snippets retrieved successfully | **application/json**: [SnippetPagination](#snippetpagination)<br> |
| 200 | Snippets retrieved successfully | **application/json**: [SnippetPaginationResponse](#snippetpaginationresponse)<br> |
### [POST] /workspaces/current/customized-snippets
**Create a new customized snippet**
@ -9776,7 +9776,7 @@ Get list of available agent providers
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Snippet created successfully | **application/json**: [Snippet](#snippet)<br> |
| 201 | Snippet created successfully | **application/json**: [SnippetResponse](#snippetresponse)<br> |
| 400 | Invalid request | |
### [POST] /workspaces/current/customized-snippets/imports
@ -9841,7 +9841,7 @@ Get list of available agent providers
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Snippet retrieved successfully | **application/json**: [Snippet](#snippet)<br> |
| 200 | Snippet retrieved successfully | **application/json**: [SnippetResponse](#snippetresponse)<br> |
| 404 | Snippet not found | |
### [PATCH] /workspaces/current/customized-snippets/{snippet_id}
@ -9863,7 +9863,7 @@ Get list of available agent providers
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Snippet updated successfully | **application/json**: [Snippet](#snippet)<br> |
| 200 | Snippet updated successfully | **application/json**: [SnippetResponse](#snippetresponse)<br> |
| 400 | Invalid request | |
| 404 | Snippet not found | |
@ -20667,31 +20667,19 @@ Validated metadata extracted from a Skill package.
| inferable | boolean | | Yes |
| reason | string | | No |
#### Snippet
#### SnippetAccountResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| created_at | long | | No |
| created_by | [_AnonymousInlineModel_b0fd3f86d9d5](#_anonymousinlinemodel_b0fd3f86d9d5) | | No |
| description | string | | No |
| graph | object | | No |
| icon_info | object | | No |
| id | string | | No |
| input_fields | object | | No |
| is_published | boolean | | No |
| name | string | | No |
| tags | [ [_AnonymousInlineModel_7b8b49ca164e](#_anonymousinlinemodel_7b8b49ca164e) ] | | No |
| type | string | | No |
| updated_at | long | | No |
| updated_by | [_AnonymousInlineModel_b0fd3f86d9d5](#_anonymousinlinemodel_b0fd3f86d9d5) | | No |
| use_count | integer | | No |
| version | integer | | No |
| email | string | | Yes |
| id | string | | Yes |
| name | string | | Yes |
#### SnippetDependencyCheckResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SnippetDependencyCheckResponse | object | | |
| leaked_dependencies | [ [PluginDependency](#plugindependency) ] | | Yes |
#### SnippetDraftConfigResponse
@ -20746,7 +20734,12 @@ Payload for importing snippet from DSL.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SnippetImportResponse | object | | |
| current_dsl_version | string | | Yes |
| error | string | | Yes |
| id | string | | Yes |
| imported_dsl_version | string | | Yes |
| snippet_id | string | | Yes |
| status | [ImportStatus](#importstatus) | | Yes |
#### SnippetIterationNodeRunPayload
@ -20756,24 +20749,24 @@ Payload for running an iteration node in snippet draft workflow.
| ---- | ---- | ----------- | -------- |
| inputs | object | | No |
#### SnippetList
#### SnippetListItemResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| author_name | string | | No |
| created_at | long | | No |
| created_by | string | | No |
| description | string | | No |
| icon_info | object | | No |
| id | string | | No |
| is_published | boolean | | No |
| name | string | | No |
| tags | [ [_AnonymousInlineModel_7b8b49ca164e](#_anonymousinlinemodel_7b8b49ca164e) ] | | No |
| type | string | | No |
| updated_at | long | | No |
| updated_by | string | | No |
| use_count | integer | | No |
| version | integer | | No |
| author_name | string | | Yes |
| created_at | integer | | Yes |
| created_by | string | | Yes |
| description | string | | Yes |
| icon_info | object | | Yes |
| id | string | | Yes |
| is_published | boolean | | Yes |
| name | string | | Yes |
| tags | [ [SnippetTagResponse](#snippettagresponse) ] | | Yes |
| type | [SnippetType](#snippettype) | | Yes |
| updated_at | integer | | Yes |
| updated_by | string | | Yes |
| use_count | integer | | Yes |
| version | integer | | Yes |
#### SnippetListQuery
@ -20796,15 +20789,51 @@ Payload for running a loop node in snippet draft workflow.
| ---- | ---- | ----------- | -------- |
| inputs | object | | No |
#### SnippetPagination
#### SnippetPaginationResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [_AnonymousInlineModel_744ff9cc03e6](#_anonymousinlinemodel_744ff9cc03e6) ] | | No |
| has_more | boolean | | No |
| limit | integer | | No |
| page | integer | | No |
| total | integer | | No |
| data | [ [SnippetListItemResponse](#snippetlistitemresponse) ] | | Yes |
| has_more | boolean | | Yes |
| limit | integer | | Yes |
| page | integer | | Yes |
| total | integer | | Yes |
#### SnippetResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| created_at | integer | | Yes |
| created_by | [SnippetAccountResponse](#snippetaccountresponse) | | Yes |
| description | string | | Yes |
| graph | object | | Yes |
| icon_info | object | | Yes |
| id | string | | Yes |
| input_fields | [ object ] | | Yes |
| is_published | boolean | | Yes |
| name | string | | Yes |
| tags | [ [SnippetTagResponse](#snippettagresponse) ] | | Yes |
| type | [SnippetType](#snippettype) | | Yes |
| updated_at | integer | | Yes |
| updated_by | [SnippetAccountResponse](#snippetaccountresponse) | | Yes |
| use_count | integer | | Yes |
| version | integer | | Yes |
#### SnippetTagResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| id | string | | Yes |
| name | string | | Yes |
| type | string | | Yes |
#### SnippetType
Snippet Type Enum
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SnippetType | string | Snippet Type Enum | |
#### SnippetUseCountResponse
@ -22862,41 +22891,6 @@ Workflow tool configuration
| data | [ [AccessPolicy](#accesspolicy) ] | | No |
| pagination | [Pagination](#pagination) | | No |
#### _AnonymousInlineModel_744ff9cc03e6
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| author_name | string | | No |
| created_at | long | | No |
| created_by | string | | No |
| description | string | | No |
| icon_info | object | | No |
| id | string | | No |
| is_published | boolean | | No |
| name | string | | No |
| tags | [ [_AnonymousInlineModel_7b8b49ca164e](#_anonymousinlinemodel_7b8b49ca164e) ] | | No |
| type | string | | No |
| updated_at | long | | No |
| updated_by | string | | No |
| use_count | integer | | No |
| version | integer | | No |
#### _AnonymousInlineModel_7b8b49ca164e
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| id | string | | No |
| name | string | | No |
| type | string | | No |
#### _AnonymousInlineModel_b0fd3f86d9d5
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| email | string | | No |
| id | string | | No |
| name | string | | No |
#### _AnonymousInlineModel_b1954337d565
| Name | Type | Description | Required |

View File

@ -1,3 +1,4 @@
from datetime import UTC, datetime
from inspect import unwrap
from types import SimpleNamespace
from unittest.mock import ANY, Mock
@ -8,7 +9,6 @@ from werkzeug.exceptions import NotFound
from controllers.console.workspace import snippets as snippets_module
from models.account import Account, TenantAccountRole
from models.snippet import CustomizedSnippet
from services.snippet_dsl_service import ImportStatus, SnippetImportInfo
@ -39,17 +39,30 @@ def _account(account_id: str = "account-1") -> Account:
return account
def _snippet(**overrides) -> CustomizedSnippet:
def _snippet(**overrides) -> SimpleNamespace:
data = {
"id": "snippet-1",
"tenant_id": "tenant-1",
"name": "Snippet",
"description": "Description",
"type": snippets_module.SnippetType.NODE,
"created_by": "account-1",
"version": 1,
"use_count": 0,
"is_published": False,
"icon_info": None,
"graph_dict": {},
"input_fields_list": [],
"tags": [],
"created_by": None,
"author_name": None,
"created_by_account": None,
"created_at": datetime.fromtimestamp(1_704_067_200, UTC),
"updated_by": None,
"updated_by_account": None,
"updated_at": datetime.fromtimestamp(1_704_153_600, UTC),
}
data.update(overrides)
return CustomizedSnippet(**data)
return SimpleNamespace(**data)
def test_normalize_snippet_list_query_args_sorts_indexed_values():
@ -70,24 +83,66 @@ def test_normalize_snippet_list_query_args_sorts_indexed_values():
}
def test_normalize_snippet_list_query_args_accepts_generated_creator_keys():
query_args = snippets_module.MultiDict(
[
("creators[1]", "account-b"),
("creators[0]", "account-a"),
]
)
assert snippets_module._normalize_snippet_list_query_args(query_args) == {
"creators": ["account-a", "account-b"],
}
def test_normalize_snippet_list_query_args_accepts_repeated_creator_keys():
query_args = snippets_module.MultiDict(
[
("creators", "account-a"),
("creators", "account-b"),
]
)
assert snippets_module._normalize_snippet_list_query_args(query_args) == {
"creators": ["account-a", "account-b"],
}
def test_list_snippets_returns_pagination(app: Flask, monkeypatch: pytest.MonkeyPatch):
snippets = [_snippet()]
tag_id = "11111111-1111-1111-1111-111111111111"
get_snippets = Mock(return_value=(snippets, 1, False))
monkeypatch.setattr(snippets_module.SnippetService, "get_snippets", get_snippets)
monkeypatch.setattr(snippets_module, "marshal", Mock(return_value=[{"id": "snippet-1"}]))
api = snippets_module.CustomizedSnippetsApi()
handler = unwrap(api.get)
with app.test_request_context(
f"/workspaces/current/customized-snippets?page=2&limit=10&tag_ids[0]={tag_id}&creator_ids[0]=account-2"
f"/workspaces/current/customized-snippets?page=2&limit=10&tag_ids[0]={tag_id}&creators[0]=account-2"
):
response, status_code = handler(api, "tenant-1")
assert status_code == 200
assert response == {
"data": [{"id": "snippet-1"}],
"data": [
{
"id": "snippet-1",
"name": "Snippet",
"description": "Description",
"type": snippets_module.SnippetType.NODE.value,
"version": 1,
"use_count": 0,
"is_published": False,
"icon_info": None,
"tags": [],
"created_by": None,
"author_name": None,
"created_at": 1_704_067_200,
"updated_by": None,
"updated_at": 1_704_153_600,
}
],
"page": 2,
"limit": 10,
"total": 1,
@ -124,7 +179,6 @@ def test_create_snippet_defaults_unknown_type_and_returns_created(app: Flask, mo
)
),
)
monkeypatch.setattr(snippets_module, "marshal", Mock(return_value={"id": "snippet-1"}))
api = snippets_module.CustomizedSnippetsApi()
handler = unwrap(api.post)
@ -137,7 +191,8 @@ def test_create_snippet_defaults_unknown_type_and_returns_created(app: Flask, mo
response, status_code = handler(api, "tenant-1", user)
assert status_code == 201
assert response == {"id": "snippet-1"}
assert response["id"] == "snippet-1"
assert response["type"] == snippets_module.SnippetType.NODE.value
assert create_snippet.call_args.kwargs["snippet_type"] == snippets_module.SnippetType.NODE
@ -184,7 +239,6 @@ def test_get_snippet_detail_raises_when_missing(app: Flask, monkeypatch: pytest.
def test_get_snippet_detail_returns_snippet(app: Flask, monkeypatch: pytest.MonkeyPatch):
snippet = _snippet()
monkeypatch.setattr(snippets_module.SnippetService, "get_snippet_by_id", Mock(return_value=snippet))
monkeypatch.setattr(snippets_module, "marshal", Mock(return_value={"id": "snippet-1"}))
api = snippets_module.CustomizedSnippetDetailApi()
handler = unwrap(api.get)
@ -193,7 +247,8 @@ def test_get_snippet_detail_returns_snippet(app: Flask, monkeypatch: pytest.Monk
response, status_code = handler(api, "tenant-1", snippet_id="snippet-1")
assert status_code == 200
assert response == {"id": "snippet-1"}
assert response["id"] == "snippet-1"
assert response["name"] == "Snippet"
def test_patch_snippet_returns_400_for_empty_payload(app: Flask, monkeypatch: pytest.MonkeyPatch):
@ -230,7 +285,6 @@ def test_patch_snippet_updates_and_commits(app: Flask, monkeypatch: pytest.Monke
monkeypatch.setattr(snippets_module.SnippetService, "update_snippet", update_snippet)
monkeypatch.setattr(snippets_module, "Session", SessionContext)
monkeypatch.setattr(snippets_module, "db", SimpleNamespace(engine=object()))
monkeypatch.setattr(snippets_module, "marshal", Mock(return_value={"id": "snippet-1", "name": "New"}))
api = snippets_module.CustomizedSnippetDetailApi()
handler = unwrap(api.patch)
@ -243,7 +297,8 @@ def test_patch_snippet_updates_and_commits(app: Flask, monkeypatch: pytest.Monke
response, status_code = handler(api, "tenant-1", user, snippet_id="snippet-1")
assert status_code == 200
assert response == {"id": "snippet-1", "name": "New"}
assert response["id"] == "snippet-1"
assert response["name"] == "New"
update_snippet.assert_called_once()
assert update_snippet.call_args.kwargs["data"] == {
"name": "New",
@ -428,9 +483,7 @@ def test_check_dependencies_raises_when_snippet_missing(app: Flask, monkeypatch:
def test_check_dependencies_returns_dependency_result(app: Flask, monkeypatch: pytest.MonkeyPatch):
snippet = _snippet()
check_dependencies = Mock(
return_value=SimpleNamespace(model_dump=Mock(return_value={"dependencies": [], "missing_dependencies": []}))
)
check_dependencies = Mock(return_value=SimpleNamespace(model_dump=Mock(return_value={"leaked_dependencies": []})))
session = SimpleNamespace()
class SessionContext(_SessionContext):
@ -453,7 +506,7 @@ def test_check_dependencies_returns_dependency_result(app: Flask, monkeypatch: p
response, status_code = handler(api, "tenant-1", snippet_id="snippet-1")
assert status_code == 200
assert response == {"dependencies": [], "missing_dependencies": []}
assert response == {"leaked_dependencies": []}
check_dependencies.assert_called_once_with(snippet=snippet)

View File

@ -7357,11 +7357,6 @@
"count": 4
}
},
"web/service/use-snippet-workflows.ts": {
"no-restricted-imports": {
"count": 1
}
},
"web/service/use-tools.ts": {
"no-restricted-imports": {
"count": 1

View File

@ -31,12 +31,12 @@ export type AgentProviderListResponse = Array<{
[key: string]: unknown
}>
export type SnippetPagination = {
data?: Array<AnonymousInlineModel744Ff9Cc03E6>
has_more?: boolean
limit?: number
page?: number
total?: number
export type SnippetPaginationResponse = {
data: Array<SnippetListItemResponse>
has_more: boolean
limit: number
page: number
total: number
}
export type CreateSnippetPayload = {
@ -50,28 +50,28 @@ export type CreateSnippetPayload = {
type?: 'group' | 'node'
}
export type Snippet = {
created_at?: number
created_by?: AnonymousInlineModelB0Fd3F86D9D5
description?: string
graph?: {
export type SnippetResponse = {
created_at: number
created_by: SnippetAccountResponse | null
description: string | null
graph: {
[key: string]: unknown
}
icon_info?: {
icon_info: {
[key: string]: unknown
}
id?: string
input_fields?: {
} | null
id: string
input_fields: Array<{
[key: string]: unknown
}
is_published?: boolean
name?: string
tags?: Array<AnonymousInlineModel7B8B49Ca164e>
type?: string
updated_at?: number
updated_by?: AnonymousInlineModelB0Fd3F86D9D5
use_count?: number
version?: number
}>
is_published: boolean
name: string
tags: Array<SnippetTagResponse>
type: SnippetType
updated_at: number
updated_by: SnippetAccountResponse | null
use_count: number
version: number
}
export type SnippetImportPayload = {
@ -84,7 +84,12 @@ export type SnippetImportPayload = {
}
export type SnippetImportResponse = {
[key: string]: unknown
current_dsl_version: string
error: string
id: string
imported_dsl_version: string
snippet_id: string | null
status: ImportStatus
}
export type UpdateSnippetPayload = {
@ -94,7 +99,7 @@ export type UpdateSnippetPayload = {
}
export type SnippetDependencyCheckResponse = {
[key: string]: unknown
leaked_dependencies: Array<PluginDependency>
}
export type TextFileResponse = string
@ -944,23 +949,23 @@ export type WorkspaceCustomConfigResponse = {
replace_webapp_logo?: string | null
}
export type AnonymousInlineModel744Ff9Cc03E6 = {
author_name?: string
created_at?: number
created_by?: string
description?: string
icon_info?: {
export type SnippetListItemResponse = {
author_name: string | null
created_at: number
created_by: string | null
description: string | null
icon_info: {
[key: string]: unknown
}
id?: string
is_published?: boolean
name?: string
tags?: Array<AnonymousInlineModel7B8B49Ca164e>
type?: string
updated_at?: number
updated_by?: string
use_count?: number
version?: number
} | null
id: string
is_published: boolean
name: string
tags: Array<SnippetTagResponse>
type: SnippetType
updated_at: number
updated_by: string | null
use_count: number
version: number
}
export type IconInfo = {
@ -981,16 +986,26 @@ export type InputFieldDefinition = {
type?: string | null
}
export type AnonymousInlineModelB0Fd3F86D9D5 = {
email?: string
id?: string
name?: string
export type SnippetAccountResponse = {
email: string
id: string
name: string
}
export type AnonymousInlineModel7B8B49Ca164e = {
id?: string
name?: string
type?: string
export type SnippetTagResponse = {
id: string
name: string
type: string
}
export type SnippetType = 'group' | 'node'
export type ImportStatus = 'completed' | 'completed-with-warnings' | 'failed' | 'pending'
export type PluginDependency = {
current_identifier?: string | null
type: Type
value: Github | Marketplace | Package
}
export type AccountWithRole = {
@ -1391,6 +1406,25 @@ export type TriggerProviderSubscriptionApiEntity = {
workflows_in_use: number
}
export type Type = 'github' | 'marketplace' | 'package'
export type Github = {
github_plugin_unique_identifier: string
package: string
repo: string
version: string
}
export type Marketplace = {
marketplace_plugin_unique_identifier: string
version?: string | null
}
export type Package = {
plugin_unique_identifier: string
version?: string | null
}
export type SimpleProviderEntityResponse = {
icon_small?: I18nObject | null
icon_small_dark?: I18nObject | null
@ -1879,7 +1913,7 @@ export type GetWorkspacesCurrentCustomizedSnippetsData = {
}
export type GetWorkspacesCurrentCustomizedSnippetsResponses = {
200: SnippetPagination
200: SnippetPaginationResponse
}
export type GetWorkspacesCurrentCustomizedSnippetsResponse
@ -1897,7 +1931,7 @@ export type PostWorkspacesCurrentCustomizedSnippetsErrors = {
}
export type PostWorkspacesCurrentCustomizedSnippetsResponses = {
201: Snippet
201: SnippetResponse
}
export type PostWorkspacesCurrentCustomizedSnippetsResponse
@ -1976,7 +2010,7 @@ export type GetWorkspacesCurrentCustomizedSnippetsBySnippetIdErrors = {
}
export type GetWorkspacesCurrentCustomizedSnippetsBySnippetIdResponses = {
200: Snippet
200: SnippetResponse
}
export type GetWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse
@ -1997,7 +2031,7 @@ export type PatchWorkspacesCurrentCustomizedSnippetsBySnippetIdErrors = {
}
export type PatchWorkspacesCurrentCustomizedSnippetsBySnippetIdResponses = {
200: Snippet
200: SnippetResponse
}
export type PatchWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse

View File

@ -26,16 +26,6 @@ export const zSnippetImportPayload = z.object({
yaml_url: z.string().nullish(),
})
/**
* SnippetImportResponse
*/
export const zSnippetImportResponse = z.record(z.string(), z.unknown())
/**
* SnippetDependencyCheckResponse
*/
export const zSnippetDependencyCheckResponse = z.record(z.string(), z.unknown())
/**
* TextFileResponse
*/
@ -801,91 +791,98 @@ export const zCreateSnippetPayload = z.object({
type: z.enum(['group', 'node']).optional().default('node'),
})
export const zAnonymousInlineModelB0Fd3F86D9D5 = z.object({
email: z.string().optional(),
id: z.string().optional(),
name: z.string().optional(),
/**
* SnippetAccountResponse
*/
export const zSnippetAccountResponse = z.object({
email: z.string(),
id: z.string(),
name: z.string(),
})
export const zAnonymousInlineModel7B8B49Ca164e = z.object({
id: z.string().optional(),
name: z.string().optional(),
type: z.string().optional(),
/**
* SnippetTagResponse
*/
export const zSnippetTagResponse = z.object({
id: z.string(),
name: z.string(),
type: z.string(),
})
export const zSnippet = z.object({
created_at: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
error: 'Invalid value: Expected int64 to be >= -9223372036854775808',
})
.max(BigInt('9223372036854775807'), {
error: 'Invalid value: Expected int64 to be <= 9223372036854775807',
})
.optional(),
created_by: zAnonymousInlineModelB0Fd3F86D9D5.optional(),
description: z.string().optional(),
graph: z.record(z.string(), z.unknown()).optional(),
icon_info: z.record(z.string(), z.unknown()).optional(),
id: z.string().optional(),
input_fields: z.record(z.string(), z.unknown()).optional(),
is_published: z.boolean().optional(),
name: z.string().optional(),
tags: z.array(zAnonymousInlineModel7B8B49Ca164e).optional(),
type: z.string().optional(),
updated_at: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
error: 'Invalid value: Expected int64 to be >= -9223372036854775808',
})
.max(BigInt('9223372036854775807'), {
error: 'Invalid value: Expected int64 to be <= 9223372036854775807',
})
.optional(),
updated_by: zAnonymousInlineModelB0Fd3F86D9D5.optional(),
use_count: z.int().optional(),
version: z.int().optional(),
/**
* SnippetType
*
* Snippet Type Enum
*/
export const zSnippetType = z.enum(['group', 'node'])
/**
* SnippetResponse
*/
export const zSnippetResponse = z.object({
created_at: z.int(),
created_by: zSnippetAccountResponse.nullable(),
description: z.string().nullable(),
graph: z.record(z.string(), z.unknown()),
icon_info: z.record(z.string(), z.unknown()).nullable(),
id: z.string(),
input_fields: z.array(z.record(z.string(), z.unknown())),
is_published: z.boolean(),
name: z.string(),
tags: z.array(zSnippetTagResponse),
type: zSnippetType,
updated_at: z.int(),
updated_by: zSnippetAccountResponse.nullable(),
use_count: z.int(),
version: z.int(),
})
export const zAnonymousInlineModel744Ff9Cc03E6 = z.object({
author_name: z.string().optional(),
created_at: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
error: 'Invalid value: Expected int64 to be >= -9223372036854775808',
})
.max(BigInt('9223372036854775807'), {
error: 'Invalid value: Expected int64 to be <= 9223372036854775807',
})
.optional(),
created_by: z.string().optional(),
description: z.string().optional(),
icon_info: z.record(z.string(), z.unknown()).optional(),
id: z.string().optional(),
is_published: z.boolean().optional(),
name: z.string().optional(),
tags: z.array(zAnonymousInlineModel7B8B49Ca164e).optional(),
type: z.string().optional(),
updated_at: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
error: 'Invalid value: Expected int64 to be >= -9223372036854775808',
})
.max(BigInt('9223372036854775807'), {
error: 'Invalid value: Expected int64 to be <= 9223372036854775807',
})
.optional(),
updated_by: z.string().optional(),
use_count: z.int().optional(),
version: z.int().optional(),
/**
* SnippetListItemResponse
*/
export const zSnippetListItemResponse = z.object({
author_name: z.string().nullable(),
created_at: z.int(),
created_by: z.string().nullable(),
description: z.string().nullable(),
icon_info: z.record(z.string(), z.unknown()).nullable(),
id: z.string(),
is_published: z.boolean(),
name: z.string(),
tags: z.array(zSnippetTagResponse),
type: zSnippetType,
updated_at: z.int(),
updated_by: z.string().nullable(),
use_count: z.int(),
version: z.int(),
})
export const zSnippetPagination = z.object({
data: z.array(zAnonymousInlineModel744Ff9Cc03E6).optional(),
has_more: z.boolean().optional(),
limit: z.int().optional(),
page: z.int().optional(),
total: z.int().optional(),
/**
* SnippetPaginationResponse
*/
export const zSnippetPaginationResponse = z.object({
data: z.array(zSnippetListItemResponse),
has_more: z.boolean(),
limit: z.int(),
page: z.int(),
total: z.int(),
})
/**
* ImportStatus
*/
export const zImportStatus = z.enum(['completed', 'completed-with-warnings', 'failed', 'pending'])
/**
* SnippetImportResponse
*/
export const zSnippetImportResponse = z.object({
current_dsl_version: z.string(),
error: z.string(),
id: z.string(),
imported_dsl_version: z.string(),
snippet_id: z.string().nullable(),
status: zImportStatus,
})
/**
@ -1493,6 +1490,53 @@ export const zTriggerProviderSubscriptionApiEntity = z.object({
*/
export const zTriggerSubscriptionListResponse = z.array(zTriggerProviderSubscriptionApiEntity)
/**
* Type
*/
export const zType = z.enum(['github', 'marketplace', 'package'])
/**
* Github
*/
export const zGithub = z.object({
github_plugin_unique_identifier: z.string(),
package: z.string(),
repo: z.string(),
version: z.string(),
})
/**
* Marketplace
*/
export const zMarketplace = z.object({
marketplace_plugin_unique_identifier: z.string(),
version: z.string().nullish(),
})
/**
* Package
*/
export const zPackage = z.object({
plugin_unique_identifier: z.string(),
version: z.string().nullish(),
})
/**
* PluginDependency
*/
export const zPluginDependency = z.object({
current_identifier: z.string().nullish(),
type: zType,
value: z.union([zGithub, zMarketplace, zPackage]),
})
/**
* SnippetDependencyCheckResponse
*/
export const zSnippetDependencyCheckResponse = z.object({
leaked_dependencies: z.array(zPluginDependency),
})
/**
* ConfigurateMethod
*
@ -2589,14 +2633,14 @@ export const zGetWorkspacesCurrentCustomizedSnippetsQuery = z.object({
/**
* Snippets retrieved successfully
*/
export const zGetWorkspacesCurrentCustomizedSnippetsResponse = zSnippetPagination
export const zGetWorkspacesCurrentCustomizedSnippetsResponse = zSnippetPaginationResponse
export const zPostWorkspacesCurrentCustomizedSnippetsBody = zCreateSnippetPayload
/**
* Snippet created successfully
*/
export const zPostWorkspacesCurrentCustomizedSnippetsResponse = zSnippet
export const zPostWorkspacesCurrentCustomizedSnippetsResponse = zSnippetResponse
export const zPostWorkspacesCurrentCustomizedSnippetsImportsBody = zSnippetImportPayload
@ -2631,7 +2675,7 @@ export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdPath = z.object({
/**
* Snippet retrieved successfully
*/
export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse = zSnippet
export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse = zSnippetResponse
export const zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdBody = zUpdateSnippetPayload
@ -2642,7 +2686,7 @@ export const zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdPath = z.object
/**
* Snippet updated successfully
*/
export const zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse = zSnippet
export const zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse = zSnippetResponse
export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdCheckDependenciesPath = z.object({
snippet_id: z.uuid(),

View File

@ -222,6 +222,8 @@ describe('SnippetInfoDropdown', () => {
expect(screen.getByText('snippet.editDialogTitle')).toBeInTheDocument()
expect(screen.getByText('common.operation.save')).toBeInTheDocument()
expect(screen.getByText(mockSnippet.name)).toBeInTheDocument()
if (!mockSnippet.description)
throw new Error('mockSnippet.description is required for this test')
expect(screen.getByText(mockSnippet.description)).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'submit-edit' }))

View File

@ -49,7 +49,7 @@ const SnippetInfoDropdown = ({ snippet }: SnippetInfoDropdownProps) => {
const initialValue = React.useMemo(() => ({
name: snippet.name,
description: snippet.description,
description: snippet.description ?? undefined,
}), [snippet.description, snippet.name])
const handleOpenEditDialog = React.useCallback(() => {
@ -80,7 +80,7 @@ const SnippetInfoDropdown = ({ snippet }: SnippetInfoDropdownProps) => {
params: { snippetId: snippet.id },
body: {
name,
description: description || undefined,
description,
},
}, {
onSuccess: () => {

View File

@ -113,6 +113,7 @@ const createSnippet = (overrides: Partial<SnippetListItem> = {}): SnippetListIte
updated_at: 1_704_153_600,
updated_by: 'updater-id',
...overrides,
version: overrides.version ?? 1,
})
describe('SnippetCard', () => {
@ -321,7 +322,7 @@ describe('SnippetCard', () => {
expect(mockUpdateMutate).toHaveBeenCalledWith(expect.objectContaining({
body: {
name: 'Updated Snippet',
description: undefined,
description: '',
},
}), expect.any(Object))
expect(mockToastError).toHaveBeenCalledWith('Update failed')

View File

@ -52,7 +52,13 @@ vi.mock('@/service/use-snippets', () => ({
vi.mock('@/service/client', () => ({
consoleClient: {
snippets: {
syncDraftWorkflow: mockSyncDraftWorkflow,
bySnippetId: {
workflows: {
draft: {
post: mockSyncDraftWorkflow,
},
},
},
},
},
}))
@ -104,7 +110,7 @@ describe('SnippetCreateButton', () => {
})
})
expect(mockSyncDraftWorkflow).toHaveBeenCalledWith({
params: { snippetId: 'snippet-123' },
params: { snippet_id: 'snippet-123' },
body: {
graph: {
nodes: [],

View File

@ -62,8 +62,8 @@ const SnippetCard = ({
return new Map((membersData?.accounts ?? []).map(member => [member.id, member.name]))
}, [membersData?.accounts])
const updatedByName = memberNameById.get(snippet.updated_by)
|| memberNameById.get(snippet.created_by)
const updatedByName = (snippet.updated_by ? memberNameById.get(snippet.updated_by) : undefined)
|| (snippet.created_by ? memberNameById.get(snippet.created_by) : undefined)
|| t('unknownUser')
const updatedAt = snippet.updated_at || snippet.created_at
@ -73,7 +73,7 @@ const SnippetCard = ({
})
const initialValue = useMemo(() => ({
name: snippet.name,
description: snippet.description,
description: snippet.description ?? undefined,
}), [snippet.description, snippet.name])
const handleOpenEditDialog = () => {
@ -119,7 +119,7 @@ const SnippetCard = ({
params: { snippetId: snippet.id },
body: {
name,
description: description || undefined,
description,
},
}, {
onSuccess: () => {
@ -148,7 +148,7 @@ const SnippetCard = ({
</div>
</div>
<div className="h-22.5 px-3.5 text-xs leading-normal text-text-tertiary">
<div className="line-clamp-2" title={snippet.description}>
<div className="line-clamp-2" title={snippet.description ?? undefined}>
{snippet.description}
</div>
</div>

View File

@ -144,6 +144,8 @@ describe('SnippetSidebarContent', () => {
expect(screen.queryByRole('link', { name: /snippet\.management/i })).not.toBeInTheDocument()
expect(screen.getByText(snippet.name)).toHaveAttribute('title', snippet.name)
expect(screen.getByText(snippet.name)).toHaveClass('truncate')
if (!snippet.description)
throw new Error('snippet.description is required for this test')
expect(screen.getByText(snippet.description)).toHaveAttribute('title', snippet.description)
expect(screen.getByText(snippet.description)).toHaveClass('truncate')
expect(screen.queryByRole('button', { name: /common\.operation\.add/i })).not.toBeInTheDocument()

View File

@ -33,9 +33,10 @@ export const useSnippetPublish = ({
params: { snippetId },
})
queryClient.setQueryData<SnippetContract | undefined>(
consoleQuery.snippets.detail.queryKey({
consoleQuery.workspaces.current.customizedSnippets.bySnippetId.get.key({
type: 'query',
input: {
params: { snippetId },
params: { snippet_id: snippetId },
},
}),
old => old ? { ...old, is_published: true } : old,

View File

@ -32,7 +32,13 @@ vi.mock('@/service/use-snippets', () => ({
vi.mock('@/service/client', () => ({
consoleClient: {
snippets: {
syncDraftWorkflow: mockSyncDraftWorkflow,
bySnippetId: {
workflows: {
draft: {
post: mockSyncDraftWorkflow,
},
},
},
},
},
}))
@ -123,7 +129,7 @@ describe('useCreateSnippet', () => {
},
})
expect(mockSyncDraftWorkflow).toHaveBeenCalledWith({
params: { snippetId: 'snippet-123' },
params: { snippet_id: 'snippet-123' },
body: {
graph,
input_fields: [

View File

@ -61,7 +61,13 @@ vi.mock('@/app/components/workflow/store', () => ({
vi.mock('@/service/client', () => ({
consoleClient: {
snippets: {
syncDraftWorkflow: (...args: unknown[]) => mockSyncDraftWorkflow(...args),
bySnippetId: {
workflows: {
draft: {
post: (...args: unknown[]) => mockSyncDraftWorkflow(...args),
},
},
},
},
},
}))
@ -123,7 +129,7 @@ describe('snippet/use-nodes-sync-draft', () => {
})
expect(mockSyncDraftWorkflow).toHaveBeenCalledWith({
params: { snippetId: 'snippet-1' },
params: { snippet_id: 'snippet-1' },
body: {
graph: {
nodes: [{ id: 'node-1', position: { x: 0, y: 0 }, data: { title: 'Start' } }],
@ -168,7 +174,7 @@ describe('snippet/use-nodes-sync-draft', () => {
})
expect(mockSyncDraftWorkflow).toHaveBeenCalledWith({
params: { snippetId: 'snippet-1' },
params: { snippet_id: 'snippet-1' },
body: {
graph: {
nodes: [{ id: 'node-1', position: { x: 0, y: 0 }, data: { title: 'Start' } }],
@ -190,7 +196,7 @@ describe('snippet/use-nodes-sync-draft', () => {
})
expect(mockSyncDraftWorkflow).toHaveBeenCalledWith({
params: { snippetId: 'snippet-1' },
params: { snippet_id: 'snippet-1' },
body: {
graph: {
nodes: [{ id: 'node-1', position: { x: 0, y: 0 }, data: { title: 'Start' } }],
@ -225,13 +231,13 @@ describe('snippet/use-nodes-sync-draft', () => {
})
expect(mockSyncDraftWorkflow).toHaveBeenNthCalledWith(1, {
params: { snippetId: 'snippet-1' },
params: { snippet_id: 'snippet-1' },
body: expect.objectContaining({
hash: 'draft-hash',
}),
})
expect(mockSyncDraftWorkflow).toHaveBeenNthCalledWith(2, {
params: { snippetId: 'snippet-1' },
params: { snippet_id: 'snippet-1' },
body: expect.objectContaining({
hash: 'hash-after-first-sync',
}),

View File

@ -42,15 +42,15 @@ export const useCreateSnippet = () => {
try {
const createPayload = {
name,
description: description || undefined,
description,
graph,
input_fields,
}
const snippet = await createSnippetMutation.mutateAsync({
body: createPayload,
})
await consoleClient.snippets.syncDraftWorkflow({
params: { snippetId: snippet.id },
await consoleClient.snippets.bySnippetId.workflows.draft.post({
params: { snippet_id: snippet.id },
body: {
graph: createPayload.graph,
input_fields: createPayload.input_fields,

View File

@ -113,8 +113,8 @@ export const useNodesSyncDraft = (snippetId: string) => {
} = workflowStore.getState()
try {
const response = await consoleClient.snippets.syncDraftWorkflow({
params: { snippetId },
const response = await consoleClient.snippets.bySnippetId.workflows.draft.post({
params: { snippet_id: snippetId },
body: {
...payload,
hash: syncWorkflowDraftHash || undefined,

View File

@ -31,6 +31,7 @@ const createSnippet = (overrides: Partial<PublishedSnippetListItem> = {}): Publi
updated_at: 2,
updated_by: 'user-1',
...overrides,
version: overrides.version ?? 1,
})
describe('SnippetDetailCard', () => {

View File

@ -15,6 +15,7 @@ const createSnippet = (overrides: Partial<PublishedSnippetListItem> = {}): Publi
updated_at: 2,
updated_by: 'user-1',
...overrides,
version: overrides.version ?? 1,
})
describe('SnippetListItem', () => {

View File

@ -283,9 +283,9 @@ export const useInsertSnippet = () => {
const handleInsertSnippet = useCallback(async (snippetId: string, insertPayload?: SnippetInsertPayload) => {
try {
const workflow = await queryClient.fetchQuery(consoleQuery.snippets.publishedWorkflow.queryOptions({
const workflow = await queryClient.fetchQuery(consoleQuery.snippets.bySnippetId.workflows.publish.get.queryOptions({
input: {
params: { snippetId },
params: { snippet_id: snippetId },
},
}))
const { nodes: snippetNodes, edges: snippetEdges } = getSnippetGraph(workflow.graph)

View File

@ -1,376 +1 @@
import type {
CreateSnippetPayload,
IncrementSnippetUseCountResponse,
PublishSnippetWorkflowResponse,
Snippet,
SnippetDraftConfig,
SnippetDraftNodeRunPayload,
SnippetDraftRunPayload,
SnippetDraftSyncPayload,
SnippetDraftSyncResponse,
SnippetImportPayload,
SnippetIterationNodeRunPayload,
SnippetListResponse,
SnippetLoopNodeRunPayload,
SnippetWorkflow,
UpdateSnippetPayload,
WorkflowNodeExecution,
WorkflowNodeExecutionListResponse,
WorkflowRunDetail,
WorkflowRunPagination,
} from '@/types/snippet'
import { type } from '@orpc/contract'
import { base } from '../base'
export const listCustomizedSnippetsContract = base
.route({
path: '/workspaces/current/customized-snippets',
method: 'GET',
})
.input(type<{
query: {
page: number
limit: number
keyword?: string
tag_ids?: string[]
creator_ids?: string[]
is_published?: boolean
}
}>())
.output(type<SnippetListResponse>())
export const createCustomizedSnippetContract = base
.route({
path: '/workspaces/current/customized-snippets',
method: 'POST',
})
.input(type<{
body: CreateSnippetPayload
}>())
.output(type<Snippet>())
export const getCustomizedSnippetContract = base
.route({
path: '/workspaces/current/customized-snippets/{snippetId}',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
}
}>())
.output(type<Snippet>())
export const updateCustomizedSnippetContract = base
.route({
path: '/workspaces/current/customized-snippets/{snippetId}',
method: 'PATCH',
})
.input(type<{
params: {
snippetId: string
}
body: UpdateSnippetPayload
}>())
.output(type<Snippet>())
export const deleteCustomizedSnippetContract = base
.route({
path: '/workspaces/current/customized-snippets/{snippetId}',
method: 'DELETE',
})
.input(type<{
params: {
snippetId: string
}
}>())
.output(type<unknown>())
export const exportCustomizedSnippetContract = base
.route({
path: '/workspaces/current/customized-snippets/{snippetId}/export',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
}
query: {
include_secret?: 'true' | 'false'
}
}>())
.output(type<string>())
export const importCustomizedSnippetContract = base
.route({
path: '/workspaces/current/customized-snippets/imports',
method: 'POST',
})
.input(type<{
body: SnippetImportPayload
}>())
.output(type<unknown>())
export const confirmSnippetImportContract = base
.route({
path: '/workspaces/current/customized-snippets/imports/{importId}/confirm',
method: 'POST',
})
.input(type<{
params: {
importId: string
}
}>())
.output(type<unknown>())
export const checkSnippetDependenciesContract = base
.route({
path: '/workspaces/current/customized-snippets/{snippetId}/check-dependencies',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
}
}>())
.output(type<unknown>())
export const incrementSnippetUseCountContract = base
.route({
path: '/workspaces/current/customized-snippets/{snippetId}/use-count/increment',
method: 'POST',
})
.input(type<{
params: {
snippetId: string
}
}>())
.output(type<IncrementSnippetUseCountResponse>())
export const getSnippetDraftWorkflowContract = base
.route({
path: '/snippets/{snippetId}/workflows/draft',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
}
}>())
.output(type<SnippetWorkflow>())
export const syncSnippetDraftWorkflowContract = base
.route({
path: '/snippets/{snippetId}/workflows/draft',
method: 'POST',
})
.input(type<{
params: {
snippetId: string
}
body: SnippetDraftSyncPayload
}>())
.output(type<SnippetDraftSyncResponse>())
export const getSnippetDraftConfigContract = base
.route({
path: '/snippets/{snippetId}/workflows/draft/config',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
}
}>())
.output(type<SnippetDraftConfig>())
export const getSnippetPublishedWorkflowContract = base
.route({
path: '/snippets/{snippetId}/workflows/publish',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
}
}>())
.output(type<SnippetWorkflow>())
export const publishSnippetWorkflowContract = base
.route({
path: '/snippets/{snippetId}/workflows/publish',
method: 'POST',
})
.input(type<{
params: {
snippetId: string
}
}>())
.output(type<PublishSnippetWorkflowResponse>())
export const getSnippetDefaultBlockConfigsContract = base
.route({
path: '/snippets/{snippetId}/workflows/default-workflow-block-configs',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
}
}>())
.output(type<unknown>())
export const listSnippetWorkflowRunsContract = base
.route({
path: '/snippets/{snippetId}/workflow-runs',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
}
query: {
last_id?: string
limit?: number
}
}>())
.output(type<WorkflowRunPagination>())
export const getSnippetWorkflowRunDetailContract = base
.route({
path: '/snippets/{snippetId}/workflow-runs/{runId}',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
runId: string
}
}>())
.output(type<WorkflowRunDetail>())
export const listSnippetWorkflowRunNodeExecutionsContract = base
.route({
path: '/snippets/{snippetId}/workflow-runs/{runId}/node-executions',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
runId: string
}
}>())
.output(type<WorkflowNodeExecutionListResponse>())
export const runSnippetDraftNodeContract = base
.route({
path: '/snippets/{snippetId}/workflows/draft/nodes/{nodeId}/run',
method: 'POST',
})
.input(type<{
params: {
snippetId: string
nodeId: string
}
body: SnippetDraftNodeRunPayload
}>())
.output(type<WorkflowNodeExecution>())
export const getSnippetDraftNodeLastRunContract = base
.route({
path: '/snippets/{snippetId}/workflows/draft/nodes/{nodeId}/last-run',
method: 'GET',
})
.input(type<{
params: {
snippetId: string
nodeId: string
}
}>())
.output(type<WorkflowNodeExecution>())
export const runSnippetDraftIterationNodeContract = base
.route({
path: '/snippets/{snippetId}/workflows/draft/iteration/nodes/{nodeId}/run',
method: 'POST',
})
.input(type<{
params: {
snippetId: string
nodeId: string
}
body: SnippetIterationNodeRunPayload
}>())
.output(type<unknown>())
export const runSnippetDraftLoopNodeContract = base
.route({
path: '/snippets/{snippetId}/workflows/draft/loop/nodes/{nodeId}/run',
method: 'POST',
})
.input(type<{
params: {
snippetId: string
nodeId: string
}
body: SnippetLoopNodeRunPayload
}>())
.output(type<unknown>())
export const runSnippetDraftWorkflowContract = base
.route({
path: '/snippets/{snippetId}/workflows/draft/run',
method: 'POST',
})
.input(type<{
params: {
snippetId: string
}
body: SnippetDraftRunPayload
}>())
.output(type<unknown>())
export const stopSnippetWorkflowTaskContract = base
.route({
path: '/snippets/{snippetId}/workflow-runs/tasks/{taskId}/stop',
method: 'POST',
})
.input(type<{
params: {
snippetId: string
taskId: string
}
}>())
.output(type<unknown>())
export const snippetsRouterContract = {
list: listCustomizedSnippetsContract,
create: createCustomizedSnippetContract,
detail: getCustomizedSnippetContract,
update: updateCustomizedSnippetContract,
delete: deleteCustomizedSnippetContract,
export: exportCustomizedSnippetContract,
import: importCustomizedSnippetContract,
confirmImport: confirmSnippetImportContract,
checkDependencies: checkSnippetDependenciesContract,
incrementUseCount: incrementSnippetUseCountContract,
draftWorkflow: getSnippetDraftWorkflowContract,
syncDraftWorkflow: syncSnippetDraftWorkflowContract,
draftConfig: getSnippetDraftConfigContract,
publishedWorkflow: getSnippetPublishedWorkflowContract,
publishWorkflow: publishSnippetWorkflowContract,
defaultBlockConfigs: getSnippetDefaultBlockConfigsContract,
workflowRuns: listSnippetWorkflowRunsContract,
workflowRunDetail: getSnippetWorkflowRunDetailContract,
workflowRunNodeExecutions: listSnippetWorkflowRunNodeExecutionsContract,
runDraftNode: runSnippetDraftNodeContract,
lastDraftNodeRun: getSnippetDraftNodeLastRunContract,
runDraftIterationNode: runSnippetDraftIterationNodeContract,
runDraftLoopNode: runSnippetDraftLoopNodeContract,
runDraftWorkflow: runSnippetDraftWorkflowContract,
stopWorkflowTask: stopSnippetWorkflowTaskContract,
}
export const snippetsConsoleRouterContract = {
snippets: snippetsRouterContract,
}
export const snippetsConsoleRouterContract = {}

View File

@ -34,6 +34,7 @@ import { resetPassword } from '@dify/contracts/api/console/reset-password/orpc.g
import { ruleCodeGenerate } from '@dify/contracts/api/console/rule-code-generate/orpc.gen'
import { ruleGenerate } from '@dify/contracts/api/console/rule-generate/orpc.gen'
import { ruleStructuredOutputGenerate } from '@dify/contracts/api/console/rule-structured-output-generate/orpc.gen'
import { snippets } from '@dify/contracts/api/console/snippets/orpc.gen'
import { spec } from '@dify/contracts/api/console/spec/orpc.gen'
import { systemFeatures } from '@dify/contracts/api/console/system-features/orpc.gen'
import { tagBindings } from '@dify/contracts/api/console/tag-bindings/orpc.gen'
@ -87,6 +88,7 @@ const communityContract = {
ruleCodeGenerate,
ruleGenerate,
ruleStructuredOutputGenerate,
snippets,
spec,
systemFeatures,
tagBindings,

View File

@ -8,7 +8,7 @@ export type SnippetSection = 'orchestrate'
export type SnippetListItem = {
id: string
name: string
description: string
description: string | null
updatedAt: string
usage: string
tags: Tag[]
@ -19,7 +19,7 @@ export type SnippetListItem = {
export type SnippetDetail = {
id: string
name: string
description: string
description: string | null
updatedAt: string
usage: string
tags: Tag[]

View File

@ -1,7 +1,6 @@
import type { SnippetWorkflow } from '@/types/snippet'
import type { PublishSnippetWorkflowResponse, SnippetWorkflow } from '@/types/snippet'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { consoleQuery } from '@/service/client'
import { get } from './base'
import { consoleClient, consoleQuery } from '@/service/client'
const isNotFoundError = (error: unknown) => {
return !!error && typeof error === 'object' && 'status' in error && error.status === 404
@ -9,7 +8,11 @@ const isNotFoundError = (error: unknown) => {
export const fetchSnippetDraftWorkflow = async (snippetId: string) => {
try {
return await get<SnippetWorkflow>(`/snippets/${snippetId}/workflows/draft`, {}, { silent: true })
return await consoleClient.snippets.bySnippetId.workflows.draft.get({
params: { snippet_id: snippetId },
}, {
context: { silent: true },
})
}
catch (error) {
if (isNotFoundError(error))
@ -19,30 +22,35 @@ export const fetchSnippetDraftWorkflow = async (snippetId: string) => {
}
}
const snippetWorkflowContract = consoleQuery.snippets.bySnippetId
const snippetWorkflowClient = consoleClient.snippets.bySnippetId
const invalidateSnippetWorkflowQueries = async (
queryClient: ReturnType<typeof useQueryClient>,
snippetId: string,
) => {
await Promise.all([
queryClient.invalidateQueries({
queryKey: consoleQuery.snippets.draftWorkflow.queryKey({
queryKey: snippetWorkflowContract.workflows.draft.get.key({
type: 'query',
input: {
params: { snippetId },
params: { snippet_id: snippetId },
},
}),
}),
queryClient.invalidateQueries({
queryKey: consoleQuery.snippets.publishedWorkflow.queryKey({
queryKey: snippetWorkflowContract.workflows.publish.get.key({
type: 'query',
input: {
params: { snippetId },
params: { snippet_id: snippetId },
},
}),
}),
queryClient.invalidateQueries({
queryKey: consoleQuery.snippets.workflowRuns.key(),
queryKey: snippetWorkflowContract.workflowRuns.get.key({ type: 'query' }),
}),
queryClient.invalidateQueries({
queryKey: consoleQuery.snippets.lastDraftNodeRun.key(),
queryKey: snippetWorkflowContract.workflows.draft.nodes.byNodeId.lastRun.get.key({ type: 'query' }),
}),
])
}
@ -50,9 +58,9 @@ export const useSnippetPublishedWorkflow = (
snippetId: string,
onSuccess?: (publishedWorkflow: SnippetWorkflow) => void,
) => {
const queryOptions = consoleQuery.snippets.publishedWorkflow.queryOptions({
const queryOptions = snippetWorkflowContract.workflows.publish.get.queryOptions({
input: {
params: { snippetId },
params: { snippet_id: snippetId },
},
enabled: !!snippetId,
})
@ -80,9 +88,9 @@ export const useSnippetDefaultBlockConfigs = (
snippetId: string,
onSuccess?: (nodesDefaultConfigs: unknown) => void,
) => {
const queryOptions = consoleQuery.snippets.defaultBlockConfigs.queryOptions({
const queryOptions = snippetWorkflowContract.workflows.defaultWorkflowBlockConfigs.get.queryOptions({
input: {
params: { snippetId },
params: { snippet_id: snippetId },
},
enabled: !!snippetId,
})
@ -100,11 +108,16 @@ export const useSnippetDefaultBlockConfigs = (
export const usePublishSnippetWorkflowMutation = (snippetId: string) => {
const queryClient = useQueryClient()
return useMutation({
...consoleQuery.snippets.publishWorkflow.mutationOptions({
onSuccess: async () => {
await invalidateSnippetWorkflowQueries(queryClient, snippetId)
return useMutation<PublishSnippetWorkflowResponse, Error, { params: { snippetId: string } }>({
mutationKey: snippetWorkflowContract.workflows.publish.post.mutationKey(),
mutationFn: ({ params }) => snippetWorkflowClient.workflows.publish.post({
params: {
snippet_id: params.snippetId,
},
body: {},
}),
onSuccess: async () => {
await invalidateSnippetWorkflowQueries(queryClient, snippetId)
},
})
}

View File

@ -6,10 +6,14 @@ import type {
SnippetListItem as SnippetListItemUIModel,
} from '@/models/snippet'
import type {
CreateSnippetPayload,
IncrementSnippetUseCountResponse,
Snippet as SnippetContract,
SnippetDSLImportResponse,
SnippetImportPayload,
SnippetListResponse,
SnippetWorkflow,
UpdateSnippetPayload,
} from '@/types/snippet'
import {
keepPreviousData,
@ -31,6 +35,17 @@ type SnippetListParams = {
}
type SnippetSummary = Pick<SnippetContract, 'id' | 'name' | 'description' | 'use_count' | 'tags' | 'updated_at' | 'is_published'>
type SnippetIdInput = {
params: {
snippetId: string
}
}
type CreateSnippetInput = {
body: CreateSnippetPayload
}
type UpdateSnippetInput = SnippetIdInput & {
body: UpdateSnippetPayload
}
const DEFAULT_SNIPPET_LIST_PARAMS = {
page: 1,
@ -124,8 +139,13 @@ const normalizeSnippetListParams = (params: SnippetListParams) => {
const snippetListRootKey = ['snippets', 'list'] as const
const snippetListKey = (params: SnippetListParams) => [...snippetListRootKey, params]
const customizedSnippetsContract = consoleQuery.workspaces.current.customizedSnippets
const customizedSnippetsClient = consoleClient.workspaces.current.customizedSnippets
const invalidateSnippetQueries = (queryClient: ReturnType<typeof useQueryClient>) => {
queryClient.invalidateQueries({
queryKey: customizedSnippetsContract.key(),
})
queryClient.invalidateQueries({
queryKey: consoleQuery.snippets.key(),
})
@ -134,15 +154,26 @@ const invalidateSnippetQueries = (queryClient: ReturnType<typeof useQueryClient>
})
}
const toGeneratedSnippetListQuery = (params: SnippetListParams) => {
return {
page: params.page,
limit: params.limit,
...(params.keyword ? { keyword: params.keyword } : {}),
...(params.tag_ids?.length ? { tag_ids: params.tag_ids } : {}),
...(params.creator_ids?.length ? { creators: params.creator_ids } : {}),
...(typeof params.is_published === 'boolean' ? { is_published: params.is_published } : {}),
}
}
export const useInfiniteSnippetList = (params: SnippetListParams = {}, options?: { enabled?: boolean }) => {
const normalizedParams = normalizeSnippetListParams(params)
return useInfiniteQuery<SnippetListResponse>({
queryKey: snippetListKey(normalizedParams),
queryFn: ({ pageParam = normalizedParams.page }) => {
return consoleClient.snippets.list({
return customizedSnippetsClient.get({
query: {
...normalizedParams,
...toGeneratedSnippetListQuery(normalizedParams),
page: pageParam as number,
},
})
@ -155,65 +186,82 @@ export const useInfiniteSnippetList = (params: SnippetListParams = {}, options?:
}
export const useSnippetApiDetail = (snippetId: string) => {
return useQuery(consoleQuery.snippets.detail.queryOptions({
input: {
params: { snippetId },
},
return useQuery({
...customizedSnippetsContract.bySnippetId.get.queryOptions({
input: {
params: { snippet_id: snippetId },
},
}),
enabled: !!snippetId,
}))
})
}
export const useCreateSnippetMutation = () => {
const queryClient = useQueryClient()
return useMutation(consoleQuery.snippets.create.mutationOptions({
return useMutation<SnippetContract, Error, CreateSnippetInput>({
mutationKey: customizedSnippetsContract.post.mutationKey(),
mutationFn: input => customizedSnippetsClient.post(input),
onSuccess: () => {
invalidateSnippetQueries(queryClient)
},
}))
})
}
export const useUpdateSnippetMutation = () => {
const queryClient = useQueryClient()
return useMutation({
...consoleQuery.snippets.update.mutationOptions({
onSuccess: () => {
invalidateSnippetQueries(queryClient)
return useMutation<SnippetContract, Error, UpdateSnippetInput>({
mutationKey: customizedSnippetsContract.bySnippetId.patch.mutationKey(),
mutationFn: ({ params, body }) => customizedSnippetsClient.bySnippetId.patch({
params: {
snippet_id: params.snippetId,
},
body,
}),
onSuccess: () => {
invalidateSnippetQueries(queryClient)
},
})
}
export const useDeleteSnippetMutation = () => {
const queryClient = useQueryClient()
return useMutation({
...consoleQuery.snippets.delete.mutationOptions({
onSuccess: () => {
invalidateSnippetQueries(queryClient)
return useMutation<unknown, Error, SnippetIdInput>({
mutationKey: customizedSnippetsContract.bySnippetId.delete.mutationKey(),
mutationFn: ({ params }) => customizedSnippetsClient.bySnippetId.delete({
params: {
snippet_id: params.snippetId,
},
}),
onSuccess: () => {
invalidateSnippetQueries(queryClient)
},
})
}
export const useIncrementSnippetUseCountMutation = () => {
const queryClient = useQueryClient()
return useMutation({
...consoleQuery.snippets.incrementUseCount.mutationOptions({
onSuccess: () => {
invalidateSnippetQueries(queryClient)
return useMutation<IncrementSnippetUseCountResponse, Error, SnippetIdInput>({
mutationKey: customizedSnippetsContract.bySnippetId.useCount.increment.post.mutationKey(),
mutationFn: ({ params }) => customizedSnippetsClient.bySnippetId.useCount.increment.post({
params: {
snippet_id: params.snippetId,
},
}),
onSuccess: () => {
invalidateSnippetQueries(queryClient)
},
})
}
export const useExportSnippetMutation = () => {
return useMutation<string, Error, { snippetId: string, include?: boolean }>({
mutationFn: ({ snippetId, include = false }) => {
return consoleClient.snippets.export({
params: { snippetId },
return customizedSnippetsClient.bySnippetId.export.get({
params: { snippet_id: snippetId },
query: { include_secret: include ? 'true' : 'false' },
})
},
@ -225,13 +273,15 @@ export const useImportSnippetDSLMutation = () => {
return useMutation<SnippetDSLImportResponse, Error, { mode: 'yaml-content' | 'yaml-url', yamlContent?: string, yamlUrl?: string }>({
mutationFn: ({ mode, yamlContent, yamlUrl }) => {
return consoleClient.snippets.import({
body: {
mode,
yaml_content: yamlContent,
yaml_url: yamlUrl,
},
}) as Promise<SnippetDSLImportResponse>
const body: SnippetImportPayload = {
mode,
yaml_content: yamlContent,
yaml_url: yamlUrl,
}
return customizedSnippetsClient.imports.post({
body,
})
},
onSuccess: () => {
invalidateSnippetQueries(queryClient)
@ -244,11 +294,11 @@ export const useConfirmSnippetImportMutation = () => {
return useMutation<SnippetDSLImportResponse, Error, { importId: string }>({
mutationFn: ({ importId }) => {
return consoleClient.snippets.confirmImport({
return customizedSnippetsClient.imports.byImportId.confirm.post({
params: {
importId,
import_id: importId,
},
}) as Promise<SnippetDSLImportResponse>
})
},
onSuccess: () => {
invalidateSnippetQueries(queryClient)

View File

@ -4,23 +4,37 @@ type SnippetType = 'node' | 'group'
type SnippetInputField = Record<string, unknown>
export type Snippet = {
type SnippetTag = Tag
type SnippetAccount = {
id: string
name: string
description: string
type: SnippetType
is_published: boolean
version: string
use_count: number
tags: Tag[]
input_fields: SnippetInputField[]
created_at: number
created_by: string
updated_at: number
updated_by: string
email: string
}
export type SnippetListItem = Omit<Snippet, 'version' | 'input_fields'>
export type SnippetListItem = {
id: string
name: string
description: string | null
type: SnippetType
is_published: boolean
version: number
use_count: number
icon_info?: Record<string, unknown> | null
tags: SnippetTag[]
created_at: number
created_by: string | null
author_name?: string | null
updated_at: number
updated_by: string | null
}
export type Snippet = Omit<SnippetListItem, 'created_by' | 'updated_by' | 'author_name'> & {
graph: Record<string, unknown>
input_fields: SnippetInputField[]
created_by: SnippetAccount | null
updated_by: SnippetAccount | null
}
export type SnippetListResponse = {
data: SnippetListItem[]
@ -44,7 +58,7 @@ export type UpdateSnippetPayload = {
}
export type SnippetImportPayload = {
mode?: string
mode: string
yaml_content?: string
yaml_url?: string
snippet_id?: string
@ -55,9 +69,9 @@ export type SnippetImportPayload = {
export type SnippetDSLImportResponse = {
id: string
status: string
snippet_id?: string
current_dsl_version?: string
imported_dsl_version?: string
snippet_id: string | null
current_dsl_version: string
imported_dsl_version: string
error: string
}
@ -77,9 +91,8 @@ export type SnippetWorkflow = {
}
export type SnippetDraftSyncPayload = {
graph?: Record<string, unknown>
graph: Record<string, unknown>
hash?: string
environment_variables?: Record<string, unknown>[]
conversation_variables?: Record<string, unknown>[]
input_fields?: SnippetInputField[]
}
@ -90,68 +103,12 @@ export type SnippetDraftSyncResponse = {
updated_at: number
}
export type SnippetDraftConfig = {
parallel_depth_limit: number
}
export type PublishSnippetWorkflowResponse = {
result: string
created_at: number
}
export type WorkflowRunDetail = {
id: string
version: string
status: 'running' | 'succeeded' | 'failed' | 'stopped' | 'partial-succeeded'
elapsed_time: number
total_tokens: number
total_steps: number
created_at: number
finished_at: number
exceptions_count: number
}
export type WorkflowRunPagination = {
limit: number
has_more: boolean
data: WorkflowRunDetail[]
}
export type WorkflowNodeExecution = {
id: string
index: number
node_id: string
node_type: string
title: string
inputs: Record<string, unknown>
process_data: Record<string, unknown>
outputs: Record<string, unknown>
status: string
error: string
elapsed_time: number
created_at: number
finished_at: number
}
export type WorkflowNodeExecutionListResponse = {
data: WorkflowNodeExecution[]
}
export type SnippetDraftNodeRunPayload = {
inputs?: Record<string, unknown>
query?: string
files?: Record<string, unknown>[]
}
export type SnippetDraftRunPayload = {
inputs?: Record<string, unknown>
inputs: Record<string, unknown>
files?: Record<string, unknown>[]
}
export type SnippetIterationNodeRunPayload = {
inputs?: Record<string, unknown>
}
export type SnippetLoopNodeRunPayload = {
inputs?: Record<string, unknown>
}