mirror of
https://github.com/langgenius/dify.git
synced 2026-06-26 14:51:13 +08:00
refactor(api): remove remaining legacy field remnants
This commit is contained in:
parent
bb921bcc45
commit
c6e9cca47f
@ -27,6 +27,7 @@ from controllers.console.wraps import (
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from fields.file_fields import FileResponse, UploadConfig
|
||||
from libs.helper import dump_response
|
||||
from libs.login import login_required
|
||||
from models import Account
|
||||
from services.file_service import FileService
|
||||
@ -100,8 +101,7 @@ class FileApi(Resource):
|
||||
except services.errors.file.BlockedFileExtensionError as blocked_extension_error:
|
||||
raise BlockedFileExtensionError(blocked_extension_error.description)
|
||||
|
||||
response = FileResponse.model_validate(upload_file, from_attributes=True)
|
||||
return response.model_dump(mode="json"), 201
|
||||
return dump_response(FileResponse, upload_file), 201
|
||||
|
||||
|
||||
@console_ns.route("/files/<uuid:file_id>/preview")
|
||||
@ -114,7 +114,7 @@ class FilePreviewApi(Resource):
|
||||
def get(self, current_tenant_id: str, file_id: UUID):
|
||||
file_id_str = str(file_id)
|
||||
text = FileService(db.engine).get_file_preview(file_id_str, current_tenant_id)
|
||||
return {"content": text}
|
||||
return TextContentResponse(content=text).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/files/support-type")
|
||||
@ -124,4 +124,4 @@ class FileSupportTypeApi(Resource):
|
||||
@account_initialization_required
|
||||
@console_ns.response(200, "Success", console_ns.models[AllowedExtensionsResponse.__name__])
|
||||
def get(self):
|
||||
return {"allowed_extensions": list(DOCUMENT_EXTENSIONS)}
|
||||
return AllowedExtensionsResponse(allowed_extensions=list(DOCUMENT_EXTENSIONS)).model_dump(mode="json")
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from flask_restx import Resource
|
||||
from pydantic import RootModel
|
||||
from pydantic import Field, RootModel
|
||||
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from controllers.console.wraps import (
|
||||
@ -10,6 +11,7 @@ from controllers.console.wraps import (
|
||||
setup_required,
|
||||
)
|
||||
from core.schemas.schema_manager import SchemaManager
|
||||
from fields.base import ResponseModel
|
||||
from libs.login import login_required
|
||||
|
||||
from . import console_ns
|
||||
@ -17,11 +19,17 @@ from . import console_ns
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SchemaDefinitionsResponse(RootModel[Any]):
|
||||
root: Any
|
||||
class SchemaDefinitionItemResponse(ResponseModel):
|
||||
name: str
|
||||
label: str
|
||||
schema_: Mapping[str, Any] = Field(alias="schema")
|
||||
|
||||
|
||||
register_response_schema_models(console_ns, SchemaDefinitionsResponse)
|
||||
class SchemaDefinitionsResponse(RootModel[list[SchemaDefinitionItemResponse]]):
|
||||
pass
|
||||
|
||||
|
||||
register_response_schema_models(console_ns, SchemaDefinitionItemResponse, SchemaDefinitionsResponse)
|
||||
|
||||
|
||||
@console_ns.route("/spec/schema-definitions")
|
||||
|
||||
@ -2,11 +2,11 @@ from typing import Any, Union
|
||||
|
||||
from flask import Response
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from pydantic import BaseModel, Field, RootModel, ValidationError
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from controllers.common.schema import register_schema_model
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_model
|
||||
from controllers.mcp import mcp_ns
|
||||
from core.mcp import types as mcp_types
|
||||
from core.mcp.server.streamable_http import handle_mcp_request
|
||||
@ -33,7 +33,12 @@ class MCPRequestPayload(BaseModel):
|
||||
id: int | str | None = Field(default=None, description="Request ID for tracking responses")
|
||||
|
||||
|
||||
class MCPJSONRPCResponse(RootModel[mcp_types.JSONRPCResponse | mcp_types.JSONRPCError]):
|
||||
pass
|
||||
|
||||
|
||||
register_schema_model(mcp_ns, MCPRequestPayload)
|
||||
register_response_schema_models(mcp_ns, MCPJSONRPCResponse)
|
||||
|
||||
|
||||
@mcp_ns.route("/server/<string:server_code>/mcp")
|
||||
@ -42,13 +47,10 @@ class MCPAppApi(Resource):
|
||||
@mcp_ns.doc("handle_mcp_request")
|
||||
@mcp_ns.doc(description="Handle Model Context Protocol (MCP) requests for a specific server")
|
||||
@mcp_ns.doc(params={"server_code": "Unique identifier for the MCP server"})
|
||||
@mcp_ns.doc(
|
||||
responses={
|
||||
200: "MCP response successfully processed",
|
||||
400: "Invalid MCP request or parameters",
|
||||
404: "Server or app not found",
|
||||
}
|
||||
)
|
||||
@mcp_ns.response(200, "MCP JSON-RPC response", mcp_ns.models[MCPJSONRPCResponse.__name__])
|
||||
@mcp_ns.response(202, "MCP notification accepted")
|
||||
@mcp_ns.response(400, "Invalid MCP request or parameters")
|
||||
@mcp_ns.response(404, "Server or app not found")
|
||||
def post(self, server_code: str):
|
||||
"""Handle MCP requests for a specific server.
|
||||
|
||||
@ -64,6 +66,7 @@ class MCPAppApi(Resource):
|
||||
Raises:
|
||||
ValidationError: Invalid request format or parameters
|
||||
"""
|
||||
# response-contract:ignore MCP route returns Flask Response from JSON-RPC handler
|
||||
args = MCPRequestPayload.model_validate(mcp_ns.payload or {})
|
||||
request_id: Union[int, str] | None = args.id
|
||||
mcp_request = self._parse_mcp_request(args.model_dump(exclude_none=True))
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
from typing import override
|
||||
|
||||
from flask_restx import fields
|
||||
|
||||
from graphon.file import File
|
||||
|
||||
|
||||
class FilesContainedField(fields.Raw):
|
||||
@override
|
||||
def format(self, value):
|
||||
return self._format_file_object(value)
|
||||
|
||||
def _format_file_object(self, v):
|
||||
if isinstance(v, File):
|
||||
return v.model_dump()
|
||||
if isinstance(v, dict):
|
||||
return {k: self._format_file_object(vv) for k, vv in v.items()}
|
||||
if isinstance(v, list):
|
||||
return [self._format_file_object(vv) for vv in v]
|
||||
return v
|
||||
@ -19026,11 +19026,19 @@ Model class for provider quota configuration.
|
||||
| last_id | string | | No |
|
||||
| limit | integer, <br>**Default:** 20 | | No |
|
||||
|
||||
#### SchemaDefinitionItemResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| label | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| schema | object | | Yes |
|
||||
|
||||
#### SchemaDefinitionsResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| SchemaDefinitionsResponse | | | |
|
||||
| SchemaDefinitionsResponse | array | | |
|
||||
|
||||
#### SegmentAttachmentResponse
|
||||
|
||||
|
||||
@ -1423,6 +1423,7 @@ class SummaryIndexService:
|
||||
- generating: Number of summaries being generated
|
||||
- error: Number of summaries with errors
|
||||
- not_started: Number of segments without summary records
|
||||
- timeout: Number of summaries that timed out
|
||||
- summaries: List of summary records with status and content preview
|
||||
"""
|
||||
from services.dataset_service import SegmentService
|
||||
|
||||
@ -9,7 +9,17 @@ class TestSpecSchemaDefinitionsApi:
|
||||
api = spec_module.SpecSchemaDefinitionsApi()
|
||||
method = unwrap(api.get)
|
||||
|
||||
schema_definitions = [{"type": "string"}]
|
||||
schema_definitions = [
|
||||
{
|
||||
"name": "conversation-variable",
|
||||
"label": "Conversation variable",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
with patch.object(
|
||||
spec_module,
|
||||
@ -21,6 +31,12 @@ class TestSpecSchemaDefinitionsApi:
|
||||
|
||||
assert status == 200
|
||||
assert resp == schema_definitions
|
||||
assert spec_module.SchemaDefinitionsResponse.model_validate(resp).model_dump(mode="json") == schema_definitions
|
||||
|
||||
def test_get_documents_tight_response_model(self):
|
||||
response = spec_module.SpecSchemaDefinitionsApi.get.__apidoc__["responses"]["200"]
|
||||
|
||||
assert response[1].name == spec_module.SchemaDefinitionsResponse.__name__
|
||||
|
||||
def test_get_exception_returns_empty_list(self):
|
||||
api = spec_module.SpecSchemaDefinitionsApi()
|
||||
|
||||
@ -4,7 +4,15 @@ export type ClientOptions = {
|
||||
baseUrl: `${string}://${string}/console/api` | (string & {})
|
||||
}
|
||||
|
||||
export type SchemaDefinitionsResponse = unknown
|
||||
export type SchemaDefinitionsResponse = Array<SchemaDefinitionItemResponse>
|
||||
|
||||
export type SchemaDefinitionItemResponse = {
|
||||
label: string
|
||||
name: string
|
||||
schema: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type GetSpecSchemaDefinitionsData = {
|
||||
body?: never
|
||||
|
||||
@ -2,10 +2,19 @@
|
||||
|
||||
import * as z from 'zod'
|
||||
|
||||
/**
|
||||
* SchemaDefinitionItemResponse
|
||||
*/
|
||||
export const zSchemaDefinitionItemResponse = z.object({
|
||||
label: z.string(),
|
||||
name: z.string(),
|
||||
schema: z.record(z.string(), z.unknown()),
|
||||
})
|
||||
|
||||
/**
|
||||
* SchemaDefinitionsResponse
|
||||
*/
|
||||
export const zSchemaDefinitionsResponse = z.unknown()
|
||||
export const zSchemaDefinitionsResponse = z.array(zSchemaDefinitionItemResponse)
|
||||
|
||||
/**
|
||||
* Success
|
||||
|
||||
@ -10,13 +10,21 @@ type SwaggerSchema = JsonObject & {
|
||||
$ref?: string
|
||||
}
|
||||
|
||||
type OpenApiMediaType = JsonObject & {
|
||||
schema?: SwaggerSchema
|
||||
}
|
||||
|
||||
type OpenApiResponse = JsonObject & {
|
||||
content?: Record<string, OpenApiMediaType>
|
||||
}
|
||||
|
||||
type OpenApiComponents = JsonObject & {
|
||||
schemas?: Record<string, SwaggerSchema>
|
||||
}
|
||||
|
||||
type SwaggerOperation = JsonObject & {
|
||||
operationId?: string
|
||||
responses?: Record<string, unknown>
|
||||
responses?: Record<string, OpenApiResponse>
|
||||
}
|
||||
|
||||
type SwaggerDocument = JsonObject & {
|
||||
@ -52,6 +60,17 @@ const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const apiOpenApiDir = path.resolve(currentDir, 'openapi')
|
||||
|
||||
const operationMethods = new Set(['delete', 'get', 'patch', 'post', 'put'])
|
||||
const pydanticDecimalStringPattern = '^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$'
|
||||
const codegenSafeDecimalStringPattern = '^(?![-+.]*$)[+-]?0*\\d*\\.?\\d*$'
|
||||
|
||||
const opaqueJsonContent = (): Record<string, OpenApiMediaType> => ({
|
||||
'application/json': {
|
||||
schema: {
|
||||
additionalProperties: true,
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const apiSpecs: ApiSpec[] = [
|
||||
{ filename: 'console-openapi.json', name: 'console' },
|
||||
@ -182,6 +201,46 @@ const addOperationIds = (document: SwaggerDocument) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isOpaqueContractResponse = (response: OpenApiResponse) => {
|
||||
const content = response.content
|
||||
if (!isObject(content))
|
||||
return false
|
||||
|
||||
return Object.entries(content).some(([mediaType, media]) => {
|
||||
if (!isObject(media))
|
||||
return false
|
||||
|
||||
return (mediaType === 'application/json' || mediaType === 'text/event-stream') && !('schema' in media)
|
||||
})
|
||||
}
|
||||
|
||||
const hasOpaqueContractSuccessResponse = (operation: SwaggerOperation) => {
|
||||
return Object.entries(operation.responses ?? {}).some(([status, response]) => {
|
||||
return /^2\d\d$/.test(status) && isObject(response) && isOpaqueContractResponse(response)
|
||||
})
|
||||
}
|
||||
|
||||
const normalizeOpaqueContractResponses = (document: SwaggerDocument) => {
|
||||
// Some backend endpoints has no schema (e.g. external) and will trap heyapi here
|
||||
// So we forge an opaque schema here
|
||||
for (const pathItem of Object.values(document.paths ?? {})) {
|
||||
for (const [method, operation] of Object.entries(pathItem)) {
|
||||
if (!operationMethods.has(method) || !isObject(operation))
|
||||
continue
|
||||
|
||||
const swaggerOperation = operation as SwaggerOperation
|
||||
if (!hasOpaqueContractSuccessResponse(swaggerOperation))
|
||||
continue
|
||||
|
||||
Object.values(swaggerOperation.responses ?? {})
|
||||
.filter(response => isObject(response) && isOpaqueContractResponse(response))
|
||||
.forEach((response) => {
|
||||
response.content = opaqueJsonContent()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasSuccessResponse = (operation: SwaggerOperation) => {
|
||||
return Object.entries(operation.responses ?? {}).some(([status, response]) => {
|
||||
if (!/^2\d\d$/.test(status))
|
||||
@ -215,6 +274,7 @@ const filterContractOperations = (document: SwaggerDocument) => {
|
||||
}
|
||||
|
||||
const normalizeApiSwagger = (document: SwaggerDocument) => {
|
||||
normalizeOpaqueContractResponses(document)
|
||||
filterContractOperations(document)
|
||||
addOperationIds(document)
|
||||
|
||||
@ -380,10 +440,20 @@ const createApiConfig = (job: ApiJob): UserConfig => ({
|
||||
'name': 'zod',
|
||||
'~resolvers': {
|
||||
string: (ctx) => {
|
||||
if (ctx.schema.format !== 'binary')
|
||||
return undefined
|
||||
if (ctx.schema.format === 'binary')
|
||||
return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File')))
|
||||
|
||||
return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File')))
|
||||
if (ctx.schema.pattern === pydanticDecimalStringPattern) {
|
||||
// the pydantic generated regex will emit error like
|
||||
// regexp/no-useless-assertions, so patch the regex here
|
||||
return $(ctx.symbols.z)
|
||||
.attr('string')
|
||||
.call()
|
||||
.attr('regex')
|
||||
.call($.regexp(codegenSafeDecimalStringPattern))
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user