change validate_app_token

pull/21891/head
ytqh 1 year ago
parent b33f8fbecb
commit 2f19e4efdd

@ -1,10 +1,9 @@
from flask_restful import Resource, marshal_with # type: ignore
from controllers.common import fields from controllers.common import fields
from controllers.common import helpers as controller_helpers from controllers.common import helpers as controller_helpers
from controllers.service_api_with_auth import api from controllers.service_api_with_auth import api
from controllers.service_api_with_auth.app.error import AppUnavailableError from controllers.service_api_with_auth.app.error import AppUnavailableError
from controllers.service_api_with_auth.wraps import validate_app_token from controllers.service_api_with_auth.wraps import validate_app_token
from flask_restful import Resource, marshal_with # type: ignore
from models.model import App, AppMode from models.model import App, AppMode
from services.app_service import AppService from services.app_service import AppService
@ -15,7 +14,26 @@ class AppParameterApi(Resource):
@validate_app_token @validate_app_token
@marshal_with(fields.parameters_fields) @marshal_with(fields.parameters_fields)
def get(self, app_model: App): def get(self, app_model: App):
"""Retrieve app parameters.""" """Retrieve app parameters.
---
tags:
- app/parameters
summary: Get app parameters
description: Retrieve parameters for the current application
security:
- ApiKeyAuth: []
responses:
200:
description: Parameters retrieved successfully
schema:
type: object
400:
description: Invalid request
401:
description: Invalid or missing token
404:
description: App unavailable
"""
if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}: if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
workflow = app_model.workflow workflow = app_model.workflow
if workflow is None: if workflow is None:
@ -40,14 +58,53 @@ class AppParameterApi(Resource):
class AppMetaApi(Resource): class AppMetaApi(Resource):
@validate_app_token @validate_app_token
def get(self, app_model: App): def get(self, app_model: App):
"""Get app meta""" """Get app meta information.
---
tags:
- app/meta
summary: Get app meta
description: Retrieve meta information for the current application
security:
- ApiKeyAuth: []
responses:
200:
description: Meta information retrieved successfully
schema:
type: object
401:
description: Invalid or missing token
"""
return AppService().get_app_meta(app_model) return AppService().get_app_meta(app_model)
class AppInfoApi(Resource): class AppInfoApi(Resource):
@validate_app_token @validate_app_token
def get(self, app_model: App): def get(self, app_model: App):
"""Get app information""" """Get app information.
---
tags:
- app/info
summary: Get app info
description: Retrieve basic information about the current application
security:
- ApiKeyAuth: []
responses:
200:
description: App information retrieved successfully
schema:
type: object
properties:
name:
type: string
description:
type: string
tags:
type: array
items:
type: string
401:
description: Invalid or missing token
"""
tags = [tag.name for tag in app_model.tags] tags = [tag.name for tag in app_model.tags]
return {"name": app_model.name, "description": app_model.description, "tags": tags} return {"name": app_model.name, "description": app_model.description, "tags": tags}

@ -1,9 +1,5 @@
import logging import logging
from flask import request
from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import InternalServerError
import services import services
from controllers.service_api_with_auth import api from controllers.service_api_with_auth import api
from controllers.service_api_with_auth.app.error import ( from controllers.service_api_with_auth.app.error import (
@ -17,9 +13,15 @@ from controllers.service_api_with_auth.app.error import (
ProviderQuotaExceededError, ProviderQuotaExceededError,
UnsupportedAudioTypeError, UnsupportedAudioTypeError,
) )
from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.service_api_with_auth.wraps import validate_app_token
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.errors.error import (
ModelCurrentlyNotSupportError,
ProviderTokenNotInitError,
QuotaExceededError,
)
from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.invoke import InvokeError
from flask import request
from flask_restful import Resource, reqparse # type: ignore
from models.model import App, AppMode, EndUser from models.model import App, AppMode, EndUser
from services.audio_service import AudioService from services.audio_service import AudioService
from services.errors.audio import ( from services.errors.audio import (
@ -28,11 +30,46 @@ from services.errors.audio import (
ProviderNotSupportSpeechToTextServiceError, ProviderNotSupportSpeechToTextServiceError,
UnsupportedAudioTypeServiceError, UnsupportedAudioTypeServiceError,
) )
from werkzeug.exceptions import InternalServerError
class AudioApi(Resource): class AudioApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.FORM)) @validate_app_token
def post(self, app_model: App, end_user: EndUser): def post(self, app_model: App, end_user: EndUser):
"""Transcribe audio to text.
---
tags:
- app/audio
summary: Transcribe audio
description: Convert audio file to text using speech-to-text
security:
- ApiKeyAuth: []
consumes:
- multipart/form-data
parameters:
- name: file
in: formData
required: true
type: file
description: The audio file to transcribe
responses:
200:
description: Audio transcribed successfully
schema:
type: object
properties:
text:
type: string
description: Transcribed text
400:
description: Invalid request, no audio uploaded, or unsupported audio type
401:
description: Invalid or missing token
413:
description: Audio file too large
500:
description: Provider error or internal server error
"""
file = request.files["file"] file = request.files["file"]
try: try:
@ -66,8 +103,51 @@ class AudioApi(Resource):
class TextApi(Resource): class TextApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) @validate_app_token
def post(self, app_model: App, end_user: EndUser): def post(self, app_model: App, end_user: EndUser):
"""Convert text to speech.
---
tags:
- app/audio
summary: Text to speech
description: Convert text to speech audio
security:
- ApiKeyAuth: []
parameters:
- name: body
in: body
required: true
schema:
type: object
required:
- text
properties:
text:
type: string
description: Text to convert to speech
voice:
type: string
description: Voice ID to use for speech synthesis
streaming:
type: boolean
default: false
description: Whether to stream the audio response
responses:
200:
description: Text converted to speech successfully
schema:
type: object
properties:
audio_url:
type: string
description: URL to the generated audio file
400:
description: Invalid request
401:
description: Invalid or missing token
500:
description: Provider error or internal server error
"""
try: try:
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("message_id", type=str, required=False, location="json") parser.add_argument("message_id", type=str, required=False, location="json")
@ -87,7 +167,11 @@ class TextApi(Resource):
voice = args.get("voice") or text_to_speech.get("voice") voice = args.get("voice") or text_to_speech.get("voice")
else: else:
try: try:
voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice") voice = args.get("voice") or (
app_model.app_model_config.text_to_speech_dict.get("voice")
if app_model.app_model_config
else None
)
except Exception: except Exception:
voice = None voice = None
response = AudioService.transcript_tts( response = AudioService.transcript_tts(

@ -1,9 +1,5 @@
import logging import logging
from libs.login import login_required
from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import InternalServerError, NotFound
import services import services
from controllers.service_api_with_auth import api from controllers.service_api_with_auth import api
from controllers.service_api_with_auth.app.error import ( from controllers.service_api_with_auth.app.error import (
@ -15,7 +11,11 @@ from controllers.service_api_with_auth.app.error import (
ProviderNotInitializeError, ProviderNotInitializeError,
ProviderQuotaExceededError, ProviderQuotaExceededError,
) )
from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.service_api_with_auth.wraps import (
FetchUserArg,
WhereisUserArg,
validate_app_token,
)
from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.base_app_queue_manager import AppQueueManager
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from core.errors.error import ( from core.errors.error import (
@ -24,15 +24,62 @@ from core.errors.error import (
QuotaExceededError, QuotaExceededError,
) )
from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.invoke import InvokeError
from flask_restful import Resource, reqparse # type: ignore
from libs import helper from libs import helper
from libs.helper import uuid_value from libs.helper import uuid_value
from libs.login import login_required
from models.model import App, AppMode, EndUser from models.model import App, AppMode, EndUser
from services.app_generate_service import AppGenerateService from services.app_generate_service import AppGenerateService
from werkzeug.exceptions import InternalServerError, NotFound
class CompletionApi(Resource): class CompletionApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) @validate_app_token
def post(self, app_model: App, end_user: EndUser): def post(self, app_model: App, end_user: EndUser):
"""Generate completion response.
---
tags:
- app/completion
summary: Generate completion
description: Generate a completion response for the provided inputs
security:
- ApiKeyAuth: []
parameters:
- name: body
in: body
required: true
schema:
type: object
required:
- inputs
properties:
inputs:
type: object
description: Input variables for the completion
query:
type: string
description: User query text
files:
type: array
description: List of files to process
response_mode:
type: string
enum: [blocking, streaming]
description: Response delivery mode
retriever_from:
type: string
default: dev
description: Source of the retriever
responses:
200:
description: Completion generated successfully
400:
description: Invalid request
401:
description: Invalid or missing token
404:
description: App unavailable or conversation not found
"""
if app_model.mode != "completion": if app_model.mode != "completion":
raise AppUnavailableError() raise AppUnavailableError()
@ -82,8 +129,36 @@ class CompletionApi(Resource):
class CompletionStopApi(Resource): class CompletionStopApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) @validate_app_token
def post(self, app_model: App, end_user: EndUser, task_id): def post(self, app_model: App, end_user: EndUser, task_id):
"""Stop a running completion task.
---
tags:
- app/completion
summary: Stop completion task
description: Stop a running completion generation task
security:
- ApiKeyAuth: []
parameters:
- name: task_id
in: path
required: true
type: string
description: ID of the task to stop
responses:
200:
description: Task stopped successfully
schema:
type: object
properties:
result:
type: string
example: success
401:
description: Invalid or missing token
404:
description: App unavailable
"""
if app_model.mode != "completion": if app_model.mode != "completion":
raise AppUnavailableError() raise AppUnavailableError()
@ -93,8 +168,61 @@ class CompletionStopApi(Resource):
class ChatApi(Resource): class ChatApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) @validate_app_token
def post(self, app_model: App, end_user: EndUser): def post(self, app_model: App, end_user: EndUser):
"""Generate chat response.
---
tags:
- app/chat
summary: Generate chat response
description: Generate a chat response for the provided inputs and query
security:
- ApiKeyAuth: []
parameters:
- name: body
in: body
required: true
schema:
type: object
required:
- inputs
- query
properties:
inputs:
type: object
description: Input variables for the chat
query:
type: string
description: User query text
files:
type: array
description: List of files to process
response_mode:
type: string
enum: [blocking, streaming]
description: Response delivery mode
conversation_id:
type: string
format: uuid
description: ID of an existing conversation to continue
retriever_from:
type: string
default: dev
description: Source of the retriever
auto_generate_name:
type: boolean
default: true
description: Whether to automatically generate a name for the conversation
responses:
200:
description: Chat response generated successfully
400:
description: Invalid request
401:
description: Invalid or missing token
404:
description: App unavailable or conversation not found
"""
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError() raise NotChatAppError()
@ -141,8 +269,36 @@ class ChatApi(Resource):
class ChatStopApi(Resource): class ChatStopApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) @validate_app_token
def post(self, app_model: App, end_user: EndUser, task_id): def post(self, app_model: App, end_user: EndUser, task_id):
"""Stop a running chat task.
---
tags:
- app/chat
summary: Stop chat task
description: Stop a running chat generation task
security:
- ApiKeyAuth: []
parameters:
- name: task_id
in: path
required: true
type: string
description: ID of the task to stop
responses:
200:
description: Task stopped successfully
schema:
type: object
properties:
result:
type: string
example: success
401:
description: Invalid or missing token
404:
description: App unavailable or not a chat app
"""
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError() raise NotChatAppError()

@ -1,12 +1,11 @@
from flask_restful import Resource, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range # type: ignore
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
import services import services
from controllers.service_api_with_auth import api from controllers.service_api_with_auth import api
from controllers.service_api_with_auth.app.error import NotChatAppError from controllers.service_api_with_auth.app.error import NotChatAppError
from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.service_api_with_auth.wraps import (
FetchUserArg,
WhereisUserArg,
validate_app_token,
)
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from extensions.ext_database import db from extensions.ext_database import db
from fields.conversation_fields import ( from fields.conversation_fields import (
@ -14,15 +13,63 @@ from fields.conversation_fields import (
conversation_infinite_scroll_pagination_fields, conversation_infinite_scroll_pagination_fields,
simple_conversation_fields, simple_conversation_fields,
) )
from flask_restful import Resource, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range # type: ignore
from libs.helper import uuid_value from libs.helper import uuid_value
from models.model import App, AppMode, EndUser from models.model import App, AppMode, EndUser
from services.conversation_service import ConversationService from services.conversation_service import ConversationService
from sqlalchemy.orm import Session # type: ignore
from werkzeug.exceptions import NotFound
class ConversationApi(Resource): class ConversationApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) @validate_app_token
@marshal_with(conversation_infinite_scroll_pagination_fields) @marshal_with(conversation_infinite_scroll_pagination_fields)
def get(self, app_model: App, end_user: EndUser): def get(self, app_model: App, end_user: EndUser):
"""Get conversations list.
---
tags:
- app/conversation
summary: List conversations
description: Get a paginated list of conversations for the current user
security:
- ApiKeyAuth: []
parameters:
- name: last_id
in: query
type: string
format: uuid
description: ID of the last conversation for pagination
- name: limit
in: query
type: integer
minimum: 1
maximum: 100
default: 20
description: Number of conversations to return
- name: sort_by
in: query
type: string
enum: [created_at, -created_at, updated_at, -updated_at]
default: -updated_at
description: Field to sort by, prefix with - for descending order
responses:
200:
description: Conversations retrieved successfully
schema:
type: object
properties:
data:
type: array
items:
type: object
has_more:
type: boolean
401:
description: Invalid or missing token
404:
description: Not a chat app
"""
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError() raise NotChatAppError()
@ -56,9 +103,38 @@ class ConversationApi(Resource):
class ConversationDetailApi(Resource): class ConversationDetailApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) @validate_app_token
@marshal_with(conversation_delete_fields) @marshal_with(conversation_delete_fields)
def delete(self, app_model: App, end_user: EndUser, c_id): def delete(self, app_model: App, end_user: EndUser, c_id):
"""Delete a conversation.
---
tags:
- app/conversation
summary: Delete conversation
description: Delete a specific conversation
security:
- ApiKeyAuth: []
parameters:
- name: c_id
in: path
required: true
type: string
format: uuid
description: ID of the conversation to delete
responses:
200:
description: Conversation deleted successfully
schema:
type: object
properties:
result:
type: string
example: success
401:
description: Invalid or missing token
404:
description: Conversation not found or not a chat app
"""
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError() raise NotChatAppError()
@ -73,9 +149,57 @@ class ConversationDetailApi(Resource):
class ConversationRenameApi(Resource): class ConversationRenameApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) @validate_app_token
@marshal_with(simple_conversation_fields) @marshal_with(simple_conversation_fields)
def post(self, app_model: App, end_user: EndUser, c_id): def post(self, app_model: App, end_user: EndUser, c_id):
"""Rename a conversation.
---
tags:
- app/conversation
summary: Rename conversation
description: Change the name of a specific conversation
security:
- ApiKeyAuth: []
parameters:
- name: c_id
in: path
required: true
type: string
format: uuid
description: ID of the conversation to rename
- name: body
in: body
required: true
schema:
type: object
required:
- name
properties:
name:
type: string
description: New name for the conversation
auto_generate:
type: boolean
default: false
description: Whether to auto-generate the name
responses:
200:
description: Conversation renamed successfully
schema:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
400:
description: Invalid request
401:
description: Invalid or missing token
404:
description: Conversation not found or not a chat app
"""
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError() raise NotChatAppError()

@ -1,6 +1,3 @@
from flask import request
from flask_restful import Resource, marshal_with # type: ignore
import services import services
from controllers.common.errors import FilenameNotExistsError from controllers.common.errors import FilenameNotExistsError
from controllers.service_api_with_auth import api from controllers.service_api_with_auth import api
@ -10,16 +7,66 @@ from controllers.service_api_with_auth.app.error import (
TooManyFilesError, TooManyFilesError,
UnsupportedFileTypeError, UnsupportedFileTypeError,
) )
from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.service_api_with_auth.wraps import (
FetchUserArg,
WhereisUserArg,
validate_app_token,
)
from fields.file_fields import file_fields from fields.file_fields import file_fields
from flask import request
from flask_restful import Resource, marshal_with # type: ignore
from models.model import App, EndUser from models.model import App, EndUser
from services.file_service import FileService from services.file_service import FileService
class FileApi(Resource): class FileApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.FORM)) @validate_app_token
@marshal_with(file_fields) @marshal_with(file_fields)
def post(self, app_model: App, end_user: EndUser): def post(self, app_model: App, end_user: EndUser):
"""Upload a file.
---
tags:
- app/file
summary: Upload file
description: Upload a file to be used with the application
security:
- ApiKeyAuth: []
consumes:
- multipart/form-data
parameters:
- name: file
in: formData
required: true
type: file
description: The file to upload
responses:
201:
description: File uploaded successfully
schema:
type: object
properties:
id:
type: string
name:
type: string
size:
type: integer
extension:
type: string
mime_type:
type: string
url:
type: string
created_at:
type: string
format: date-time
400:
description: Invalid request, no file uploaded, unsupported file type, or too many files
401:
description: Invalid or missing token
413:
description: File too large
"""
file = request.files["file"] file = request.files["file"]
# check file # check file

@ -1,20 +1,23 @@
import logging import logging
from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range # type: ignore
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
import services import services
from controllers.service_api_with_auth import api from controllers.service_api_with_auth import api
from controllers.service_api_with_auth.app.error import NotChatAppError from controllers.service_api_with_auth.app.error import NotChatAppError
from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.service_api_with_auth.wraps import (
FetchUserArg,
WhereisUserArg,
validate_app_token,
)
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from fields.conversation_fields import message_file_fields from fields.conversation_fields import message_file_fields
from fields.raws import FilesContainedField from fields.raws import FilesContainedField
from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range # type: ignore
from libs.helper import TimestampField, uuid_value from libs.helper import TimestampField, uuid_value
from models.model import App, AppMode, EndUser from models.model import App, AppMode, EndUser
from services.errors.message import SuggestedQuestionsAfterAnswerDisabledError from services.errors.message import SuggestedQuestionsAfterAnswerDisabledError
from services.message_service import MessageService from services.message_service import MessageService
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
class MessageListApi(Resource): class MessageListApi(Resource):
@ -74,9 +77,57 @@ class MessageListApi(Resource):
"data": fields.List(fields.Nested(message_fields)), "data": fields.List(fields.Nested(message_fields)),
} }
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) @validate_app_token
@marshal_with(message_infinite_scroll_pagination_fields) @marshal_with(message_infinite_scroll_pagination_fields)
def get(self, app_model: App, end_user: EndUser): def get(self, app_model: App, end_user: EndUser):
"""Get messages list.
---
tags:
- app/message
summary: List messages
description: Get a paginated list of messages for a conversation
security:
- ApiKeyAuth: []
parameters:
- name: conversation_id
in: query
required: true
type: string
format: uuid
description: ID of the conversation to get messages for
- name: first_id
in: query
type: string
format: uuid
description: ID of the first message for pagination
- name: limit
in: query
type: integer
minimum: 1
maximum: 100
default: 20
description: Number of messages to return
responses:
200:
description: Messages retrieved successfully
schema:
type: object
properties:
limit:
type: integer
has_more:
type: boolean
data:
type: array
items:
type: object
400:
description: Invalid request
401:
description: Invalid or missing token
404:
description: Conversation not found or not a chat app
"""
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError() raise NotChatAppError()
@ -98,8 +149,54 @@ class MessageListApi(Resource):
class MessageFeedbackApi(Resource): class MessageFeedbackApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) @validate_app_token
def post(self, app_model: App, end_user: EndUser, message_id): def post(self, app_model: App, end_user: EndUser, message_id):
"""Submit feedback for a message.
---
tags:
- app/message
summary: Submit message feedback
description: Submit user feedback for a specific message
security:
- ApiKeyAuth: []
parameters:
- name: message_id
in: path
required: true
type: string
format: uuid
description: ID of the message to provide feedback for
- name: body
in: body
required: true
schema:
type: object
required:
- rating
properties:
rating:
type: string
enum: [like, dislike, null]
description: User's rating of the message
content:
type: string
description: Additional feedback content
responses:
200:
description: Feedback submitted successfully
schema:
type: object
properties:
result:
type: string
example: success
400:
description: Invalid request
401:
description: Invalid or missing token
404:
description: Message not found or not a chat app
"""
message_id = str(message_id) message_id = str(message_id)
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@ -122,8 +219,43 @@ class MessageFeedbackApi(Resource):
class MessageSuggestedApi(Resource): class MessageSuggestedApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY, required=True)) @validate_app_token
def get(self, app_model: App, end_user: EndUser, message_id): def get(self, app_model: App, end_user: EndUser, message_id):
"""Get suggested questions for a message.
---
tags:
- app/message
summary: Get suggested questions
description: Get suggested follow-up questions for a specific message
security:
- ApiKeyAuth: []
parameters:
- name: message_id
in: path
required: true
type: string
format: uuid
description: ID of the message to get suggestions for
responses:
200:
description: Suggested questions retrieved successfully
schema:
type: object
properties:
result:
type: string
example: success
data:
type: array
items:
type: string
400:
description: Invalid request or suggestions disabled
401:
description: Invalid or missing token
404:
description: Message not found or not a chat app
"""
message_id = str(message_id) message_id = str(message_id)
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:

@ -1,9 +1,5 @@
import logging import logging
from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range # type: ignore
from werkzeug.exceptions import InternalServerError
from controllers.service_api_with_auth import api from controllers.service_api_with_auth import api
from controllers.service_api_with_auth.app.error import ( from controllers.service_api_with_auth.app.error import (
CompletionRequestError, CompletionRequestError,
@ -12,7 +8,11 @@ from controllers.service_api_with_auth.app.error import (
ProviderNotInitializeError, ProviderNotInitializeError,
ProviderQuotaExceededError, ProviderQuotaExceededError,
) )
from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.service_api_with_auth.wraps import (
FetchUserArg,
WhereisUserArg,
validate_app_token,
)
from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.base_app_queue_manager import AppQueueManager
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from core.errors.error import ( from core.errors.error import (
@ -23,11 +23,14 @@ from core.errors.error import (
from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.invoke import InvokeError
from extensions.ext_database import db from extensions.ext_database import db
from fields.workflow_app_log_fields import workflow_app_log_pagination_fields from fields.workflow_app_log_fields import workflow_app_log_pagination_fields
from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range # type: ignore
from libs import helper from libs import helper
from models.model import App, AppMode, EndUser from models.model import App, AppMode, EndUser
from models.workflow import WorkflowRun from models.workflow import WorkflowRun
from services.app_generate_service import AppGenerateService from services.app_generate_service import AppGenerateService
from services.workflow_app_service import WorkflowAppService from services.workflow_app_service import WorkflowAppService
from werkzeug.exceptions import InternalServerError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -50,8 +53,55 @@ class WorkflowRunDetailApi(Resource):
@validate_app_token @validate_app_token
@marshal_with(workflow_run_fields) @marshal_with(workflow_run_fields)
def get(self, app_model: App, workflow_id: str): def get(self, app_model: App, workflow_id: str):
""" """Get workflow run details.
Get a workflow task running detail ---
tags:
- app/workflow
summary: Get workflow run details
description: Retrieve details of a specific workflow run
security:
- ApiKeyAuth: []
parameters:
- name: workflow_id
in: path
required: true
type: string
description: ID of the workflow run to retrieve
responses:
200:
description: Workflow run details retrieved successfully
schema:
type: object
properties:
id:
type: string
workflow_id:
type: string
status:
type: string
inputs:
type: object
outputs:
type: object
error:
type: string
total_steps:
type: integer
total_tokens:
type: integer
created_at:
type: string
format: date-time
finished_at:
type: string
format: date-time
elapsed_time:
type: number
format: float
401:
description: Invalid or missing token
404:
description: Workflow run not found or not a workflow app
""" """
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode != AppMode.WORKFLOW: if app_mode != AppMode.WORKFLOW:
@ -62,10 +112,41 @@ class WorkflowRunDetailApi(Resource):
class WorkflowRunApi(Resource): class WorkflowRunApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) @validate_app_token
def post(self, app_model: App, end_user: EndUser): def post(self, app_model: App, end_user: EndUser):
""" """Run a workflow.
Run workflow ---
tags:
- app/workflow
summary: Run workflow
description: Execute a workflow with the provided inputs
security:
- ApiKeyAuth: []
parameters:
- name: body
in: body
required: true
schema:
type: object
required:
- inputs
properties:
inputs:
type: object
description: Input variables for the workflow
response_mode:
type: string
enum: [blocking, streaming]
description: Response delivery mode
responses:
200:
description: Workflow executed successfully
400:
description: Invalid request
401:
description: Invalid or missing token
404:
description: Not a workflow app
""" """
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode != AppMode.WORKFLOW: if app_mode != AppMode.WORKFLOW:
@ -101,10 +182,35 @@ class WorkflowRunApi(Resource):
class WorkflowTaskStopApi(Resource): class WorkflowTaskStopApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) @validate_app_token
def post(self, app_model: App, end_user: EndUser, task_id: str): def post(self, app_model: App, end_user: EndUser, task_id: str):
""" """Stop a running workflow task.
Stop workflow task ---
tags:
- app/workflow
summary: Stop workflow task
description: Stop a running workflow task
security:
- ApiKeyAuth: []
parameters:
- name: task_id
in: path
required: true
type: string
description: ID of the task to stop
responses:
200:
description: Task stopped successfully
schema:
type: object
properties:
result:
type: string
example: success
401:
description: Invalid or missing token
404:
description: Not a workflow app
""" """
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
if app_mode != AppMode.WORKFLOW: if app_mode != AppMode.WORKFLOW:
@ -119,8 +225,50 @@ class WorkflowAppLogApi(Resource):
@validate_app_token @validate_app_token
@marshal_with(workflow_app_log_pagination_fields) @marshal_with(workflow_app_log_pagination_fields)
def get(self, app_model: App): def get(self, app_model: App):
""" """Get workflow app logs.
Get workflow app logs ---
tags:
- app/workflow
summary: Get workflow logs
description: Retrieve logs for workflow app executions
security:
- ApiKeyAuth: []
parameters:
- name: page
in: query
type: integer
minimum: 1
default: 1
description: Page number for pagination
- name: limit
in: query
type: integer
minimum: 1
maximum: 100
default: 20
description: Number of logs per page
responses:
200:
description: Workflow logs retrieved successfully
schema:
type: object
properties:
data:
type: array
items:
type: object
has_more:
type: boolean
limit:
type: integer
total:
type: integer
page:
type: integer
401:
description: Invalid or missing token
404:
description: Not a workflow app
""" """
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("keyword", type=str, location="args") parser.add_argument("keyword", type=str, location="args")

@ -9,6 +9,7 @@ from flask import current_app, request
from flask_login import user_logged_in # type: ignore from flask_login import user_logged_in # type: ignore
from flask_restful import Resource # type: ignore from flask_restful import Resource # type: ignore
from libs.login import _get_user from libs.login import _get_user
from libs.passport import PassportService
from models.account import Account, Tenant, TenantAccountJoin, TenantStatus from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
from models.model import ApiToken, App, EndUser from models.model import ApiToken, App, EndUser
from pydantic import BaseModel # type: ignore from pydantic import BaseModel # type: ignore
@ -33,16 +34,34 @@ class FetchUserArg(BaseModel):
required: bool = False required: bool = False
# TODO: add auth jwt token check def validate_app_token(view: Optional[Callable] = None):
def validate_app_token(
view: Optional[Callable] = None, *, fetch_user_arg: Optional[FetchUserArg] = None
):
def decorator(view_func): def decorator(view_func):
@wraps(view_func) @wraps(view_func)
def decorated_view(*args, **kwargs): def decorated_view(*args, **kwargs):
api_token = validate_and_get_api_token("app") # Extract user info from Bearer token
auth_header = request.headers.get("Authorization")
if auth_header is None or " " not in auth_header:
raise Unauthorized("Authorization header must be provided and start with 'Bearer'")
auth_scheme, auth_token = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower()
if auth_scheme != "bearer":
raise Unauthorized("Authorization scheme must be 'Bearer'")
# Decode the JWT token to extract user info
try:
decoded = PassportService().verify(auth_token)
user_id = decoded.get("user_id")
if not user_id:
raise Unauthorized("Invalid token: missing user_id")
except Exception as e:
raise Unauthorized(f"Failed to extract user_id from token: {str(e)}")
# Get app model using hardcoded ID
app_id = "b278ba96-fa8e-48a8-b3e9-debe34468be0" # TODO: ytqh Replace with your actual hardcoded app ID
app_model = db.session.query(App).filter(App.id == app_id).first()
app_model = db.session.query(App).filter(App.id == api_token.app_id).first()
if not app_model: if not app_model:
raise Forbidden("The app no longer exists.") raise Forbidden("The app no longer exists.")
@ -52,11 +71,7 @@ def validate_app_token(
if not app_model.enable_api: if not app_model.enable_api:
raise Forbidden("The app's API service has been disabled.") raise Forbidden("The app's API service has been disabled.")
tenant = ( tenant = db.session.query(Tenant).filter(Tenant.id == app_model.tenant_id).first()
db.session.query(Tenant)
.filter(Tenant.id == app_model.tenant_id)
.first()
)
if tenant is None: if tenant is None:
raise ValueError("Tenant does not exist.") raise ValueError("Tenant does not exist.")
if tenant.status == TenantStatus.ARCHIVE: if tenant.status == TenantStatus.ARCHIVE:
@ -64,26 +79,7 @@ def validate_app_token(
kwargs["app_model"] = app_model kwargs["app_model"] = app_model
if fetch_user_arg: kwargs["end_user"] = create_or_update_end_user_for_user_id(app_model, user_id)
if fetch_user_arg.fetch_from == WhereisUserArg.QUERY:
user_id = request.args.get("user")
elif fetch_user_arg.fetch_from == WhereisUserArg.JSON:
user_id = request.get_json().get("user")
elif fetch_user_arg.fetch_from == WhereisUserArg.FORM:
user_id = request.form.get("user")
else:
# use default-user
user_id = None
if not user_id and fetch_user_arg.required:
raise ValueError("Arg user must be provided.")
if user_id:
user_id = str(user_id)
kwargs["end_user"] = create_or_update_end_user_for_user_id(
app_model, user_id
)
return view_func(*args, **kwargs) return view_func(*args, **kwargs)
@ -108,27 +104,13 @@ def cloud_edition_billing_resource_check(resource: str, api_token_type: str):
documents_upload_quota = features.documents_upload_quota documents_upload_quota = features.documents_upload_quota
if resource == "members" and 0 < members.limit <= members.size: if resource == "members" and 0 < members.limit <= members.size:
raise Forbidden( raise Forbidden("The number of members has reached the limit of your subscription.")
"The number of members has reached the limit of your subscription."
)
elif resource == "apps" and 0 < apps.limit <= apps.size: elif resource == "apps" and 0 < apps.limit <= apps.size:
raise Forbidden( raise Forbidden("The number of apps has reached the limit of your subscription.")
"The number of apps has reached the limit of your subscription." elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size:
) raise Forbidden("The capacity of the vector space has reached the limit of your subscription.")
elif ( elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
resource == "vector_space" raise Forbidden("The number of documents has reached the limit of your subscription.")
and 0 < vector_space.limit <= vector_space.size
):
raise Forbidden(
"The capacity of the vector space has reached the limit of your subscription."
)
elif (
resource == "documents"
and 0 < documents_upload_quota.limit <= documents_upload_quota.size
):
raise Forbidden(
"The number of documents has reached the limit of your subscription."
)
else: else:
return view(*args, **kwargs) return view(*args, **kwargs)
@ -204,9 +186,7 @@ def validate_and_get_api_token(scope: str | None = None):
""" """
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
if auth_header is None or " " not in auth_header: if auth_header is None or " " not in auth_header:
raise Unauthorized( raise Unauthorized("Authorization header must be provided and start with 'Bearer'")
"Authorization header must be provided and start with 'Bearer'"
)
auth_scheme, auth_token = auth_header.split(None, 1) auth_scheme, auth_token = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower() auth_scheme = auth_scheme.lower()
@ -221,10 +201,7 @@ def validate_and_get_api_token(scope: str | None = None):
update(ApiToken) update(ApiToken)
.where( .where(
ApiToken.token == auth_token, ApiToken.token == auth_token,
( (ApiToken.last_used_at.is_(None) | (ApiToken.last_used_at < cutoff_time)),
ApiToken.last_used_at.is_(None)
| (ApiToken.last_used_at < cutoff_time)
),
ApiToken.type == scope, ApiToken.type == scope,
) )
.values(last_used_at=current_time) .values(last_used_at=current_time)
@ -234,9 +211,7 @@ def validate_and_get_api_token(scope: str | None = None):
api_token = result.scalar_one_or_none() api_token = result.scalar_one_or_none()
if not api_token: if not api_token:
stmt = select(ApiToken).where( stmt = select(ApiToken).where(ApiToken.token == auth_token, ApiToken.type == scope)
ApiToken.token == auth_token, ApiToken.type == scope
)
api_token = session.scalar(stmt) api_token = session.scalar(stmt)
if not api_token: if not api_token:
raise Unauthorized("Access token is invalid") raise Unauthorized("Access token is invalid")
@ -246,9 +221,7 @@ def validate_and_get_api_token(scope: str | None = None):
return api_token return api_token
def create_or_update_end_user_for_user_id( def create_or_update_end_user_for_user_id(app_model: App, user_id: Optional[str] = None) -> EndUser:
app_model: App, user_id: Optional[str] = None
) -> EndUser:
""" """
Create or update session terminal based on user ID. Create or update session terminal based on user ID.
""" """

Loading…
Cancel
Save