feat: application upload file (#3487)

This commit is contained in:
shaohuzhang1 2025-07-04 19:52:45 +08:00 committed by GitHub
parent 0c874bd2d5
commit e7a30903ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 140 additions and 139 deletions

View File

@ -24,17 +24,7 @@ from knowledge.models import Paragraph, Knowledge
from knowledge.models import SearchMode from knowledge.models import SearchMode
from maxkb.conf import PROJECT_DIR from maxkb.conf import PROJECT_DIR
from models_provider.models import Model from models_provider.models import Model
from models_provider.tools import get_model from models_provider.tools import get_model, get_model_by_id
def get_model_by_id(_id, workspace_id):
model = QuerySet(Model).filter(id=_id, model_type="EMBEDDING")
get_authorized_model = DatabaseModelManage.get_model("get_authorized_model")
if get_authorized_model is not None:
model = get_authorized_model(model, workspace_id)
if model is None:
raise Exception(_("Model does not exist"))
return model
def get_embedding_id(knowledge_id_list): def get_embedding_id(knowledge_id_list):
@ -65,6 +55,8 @@ class BaseSearchDatasetStep(ISearchDatasetStep):
exec_problem_text = padding_problem_text if padding_problem_text is not None else problem_text exec_problem_text = padding_problem_text if padding_problem_text is not None else problem_text
model_id = get_embedding_id(knowledge_id_list) model_id = get_embedding_id(knowledge_id_list)
model = get_model_by_id(model_id, workspace_id) model = get_model_by_id(model_id, workspace_id)
if model.model_type != "EMBEDDING":
raise Exception(_("Model does not exist"))
self.context['model_name'] = model.name self.context['model_name'] = model.name
embedding_model = ModelManage.get_model(model_id, lambda _id: get_model(model)) embedding_model = ModelManage.get_model(model_id, lambda _id: get_model(model))
embedding_value = embedding_model.embed_query(exec_problem_text) embedding_value = embedding_model.embed_query(exec_problem_text)

View File

@ -71,7 +71,7 @@ class BaseDocumentExtractNode(IDocumentExtractNode):
for doc in document: for doc in document:
file = QuerySet(File).filter(id=doc['file_id']).first() file = QuerySet(File).filter(id=doc['file_id']).first()
buffer = io.BytesIO(file.get_bytes().tobytes()) buffer = io.BytesIO(file.get_bytes())
buffer.name = doc['name'] # this is the important line buffer.name = doc['name'] # this is the important line
for split_handle in (parse_table_handle_list + split_handles): for split_handle in (parse_table_handle_list + split_handles):

View File

@ -9,6 +9,7 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.views import APIView from rest_framework.views import APIView
@ -24,6 +25,8 @@ from common.auth.authentication import has_permissions
from common.constants.permission_constants import ChatAuth from common.constants.permission_constants import ChatAuth
from common.exception.app_exception import AppAuthenticationFailed from common.exception.app_exception import AppAuthenticationFailed
from common.result import result from common.result import result
from knowledge.models import FileSourceType
from oss.serializers.file import FileSerializer
from users.api import CaptchaAPI from users.api import CaptchaAPI
from users.serializers.login import CaptchaSerializer from users.serializers.login import CaptchaSerializer
@ -134,7 +137,7 @@ class CaptchaView(APIView):
summary=_("Get Chat captcha"), summary=_("Get Chat captcha"),
description=_("Get Chat captcha"), description=_("Get Chat captcha"),
operation_id=_("Get Chat captcha"), # type: ignore operation_id=_("Get Chat captcha"), # type: ignore
tags=[_("User Management")], # type: ignore tags=[_("Chat")], # type: ignore
responses=CaptchaAPI.get_response()) responses=CaptchaAPI.get_response())
def get(self, request: Request): def get(self, request: Request):
return result.success(CaptchaSerializer().generate()) return result.success(CaptchaSerializer().generate())
@ -150,7 +153,7 @@ class SpeechToText(APIView):
operation_id=_("speech to text"), # type: ignore operation_id=_("speech to text"), # type: ignore
request=SpeechToTextAPI.get_request(), request=SpeechToTextAPI.get_request(),
responses=SpeechToTextAPI.get_response(), responses=SpeechToTextAPI.get_response(),
tags=[_('Application')] # type: ignore tags=[_('Chat')] # type: ignore
) )
def post(self, request: Request): def post(self, request: Request):
return result.success( return result.success(
@ -169,10 +172,34 @@ class TextToSpeech(APIView):
operation_id=_("text to speech"), # type: ignore operation_id=_("text to speech"), # type: ignore
request=TextToSpeechAPI.get_request(), request=TextToSpeechAPI.get_request(),
responses=TextToSpeechAPI.get_response(), responses=TextToSpeechAPI.get_response(),
tags=[_('Application')] # type: ignore tags=[_('Chat')] # type: ignore
) )
def post(self, request: Request): def post(self, request: Request):
byte_data = TextToSpeechSerializers( byte_data = TextToSpeechSerializers(
data={'application_id': request.auth.application_id}).text_to_speech(request.data) data={'application_id': request.auth.application_id}).text_to_speech(request.data)
return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3', return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3',
'Content-Disposition': 'attachment; filename="abc.mp3"'}) 'Content-Disposition': 'attachment; filename="abc.mp3"'})
class UploadFile(APIView):
authentication_classes = [TokenAuth]
parser_classes = [MultiPartParser]
@extend_schema(
methods=['POST'],
description=_("Upload files"),
summary=_("Upload files"),
operation_id=_("Upload files"), # type: ignore
request=TextToSpeechAPI.get_request(),
responses=TextToSpeechAPI.get_response(),
tags=[_('Application')] # type: ignore
)
def post(self, request: Request, chat_id: str):
files = request.FILES.getlist('file')
file_ids = []
meta = {}
for file in files:
file_url = FileSerializer(
data={'file': file, 'meta': meta, 'source_id': chat_id, 'source_type': FileSourceType.CHAT, }).upload()
file_ids.append({'name': file.name, 'url': file_url, 'file_id': file_url.split('/')[-1]})
return result.success(file_ids)

View File

@ -95,8 +95,6 @@ def default_status_meta():
return {"state_time": {}} return {"state_time": {}}
class KnowledgeFolder(MPTTModel, AppModelMixin): class KnowledgeFolder(MPTTModel, AppModelMixin):
id = models.CharField(primary_key=True, max_length=64, editable=False, verbose_name="主键id") id = models.CharField(primary_key=True, max_length=64, editable=False, verbose_name="主键id")
name = models.CharField(max_length=64, verbose_name="文件夹名称") name = models.CharField(max_length=64, verbose_name="文件夹名称")
@ -133,9 +131,11 @@ class Knowledge(AppModelMixin):
class Meta: class Meta:
db_table = "knowledge" db_table = "knowledge"
def get_default_status(): def get_default_status():
return Status('').__str__() return Status('').__str__()
class Document(AppModelMixin): class Document(AppModelMixin):
""" """
文档表 文档表
@ -224,10 +224,12 @@ class FileSourceType(models.TextChoices):
TOOL = "TOOL" TOOL = "TOOL"
# 文档 # 文档
DOCUMENT = "DOCUMENT" DOCUMENT = "DOCUMENT"
# 对话
CHAT = "CHAT"
# 临时30分钟 数据30分钟后被清理 source_id 为TEMPORARY_30_MINUTE # 临时30分钟 数据30分钟后被清理 source_id 为TEMPORARY_30_MINUTE
TEMPORARY_30_MINUTE = "TEMPORARY_30_MINUTE" TEMPORARY_30_MINUTE = "TEMPORARY_30_MINUTE"
# 临时120分钟 数据120分钟后被清理 source_id为TEMPORARY_100_MINUTE # 临时120分钟 数据120分钟后被清理 source_id为TEMPORARY_100_MINUTE
TEMPORARY_120_MINUTE = "TEMPORARY_100_MINUTE" TEMPORARY_120_MINUTE = "TEMPORARY_120_MINUTE"
# 临时1天 数据1天后被清理 source_id为TEMPORARY_1_DAY # 临时1天 数据1天后被清理 source_id为TEMPORARY_1_DAY
TEMPORARY_1_DAY = "TEMPORARY_1_DAY" TEMPORARY_1_DAY = "TEMPORARY_1_DAY"

View File

@ -42,6 +42,7 @@ urlpatterns = [
path(admin_api_prefix, include("system_manage.urls")), path(admin_api_prefix, include("system_manage.urls")),
path(admin_api_prefix, include("application.urls")), path(admin_api_prefix, include("application.urls")),
path(admin_api_prefix, include("oss.urls")), path(admin_api_prefix, include("oss.urls")),
path(chat_api_prefix, include("oss.urls")),
path(chat_api_prefix, include("chat.urls")), path(chat_api_prefix, include("chat.urls")),
path(f'{admin_ui_prefix[1:]}/', include('oss.retrieval_urls')), path(f'{admin_ui_prefix[1:]}/', include('oss.retrieval_urls')),
path(f'{chat_ui_prefix[1:]}/', include('oss.retrieval_urls')), path(f'{chat_ui_prefix[1:]}/', include('oss.retrieval_urls')),

View File

@ -296,6 +296,30 @@ const getMcpTools: (application_id: String, loading?: Ref<boolean>) => Promise<R
return get(`${prefix.value}/${application_id}/mcp_tools`, undefined, loading) return get(`${prefix.value}/${application_id}/mcp_tools`, undefined, loading)
} }
/**
*
* @param file:file
*/
const uploadFile: (
file: any,
sourceId: string,
resourceType:
| 'KNOWLEDGE'
| 'APPLICATION'
| 'TOOL'
| 'DOCUMENT'
| 'CHAT'
| 'TEMPORARY_30_MINUTE'
| 'TEMPORARY_120_MINUTE'
| 'TEMPORARY_1_DAY',
) => Promise<Result<any>> = (file, sourceId, resourceType) => {
const fd = new FormData()
fd.append('file', file)
fd.append('source_id', sourceId)
fd.append('source_type', resourceType)
return post(`/oss/file`, fd)
}
export default { export default {
getAllApplication, getAllApplication,
getApplication, getApplication,
@ -321,4 +345,5 @@ export default {
postTextToSpeech, postTextToSpeech,
speechToText, speechToText,
getMcpTools, getMcpTools,
uploadFile,
} }

View File

@ -270,7 +270,7 @@ const speechToText: (data: any, loading?: Ref<boolean>) => Promise<Result<any>>
* @param loading * @param loading
* @returns * @returns
*/ */
const deleteChat: (chat_id:string, loading?: Ref<boolean>) => Promise<Result<any>> = ( const deleteChat: (chat_id: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
chat_id, chat_id,
loading, loading,
) => { ) => {
@ -283,12 +283,39 @@ const deleteChat: (chat_id:string, loading?: Ref<boolean>) => Promise<Result<any
* @param loading * @param loading
* @returns * @returns
*/ */
const modifyChat: (chat_id:string, data:any, loading?: Ref<boolean> ) => Promise<Result<any>> = ( const modifyChat: (chat_id: string, data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
chat_id,data,loading chat_id,
data,
loading,
) => { ) => {
return put(`historical_conversation/${chat_id}`, data, undefined, loading) return put(`historical_conversation/${chat_id}`, data, undefined, loading)
} }
/**
*
* @param file
* @param sourceId id
* @param resourceType
* @returns
*/
const uploadFile: (
file: any,
sourceId: string,
resourceType:
| 'KNOWLEDGE'
| 'APPLICATION'
| 'TOOL'
| 'DOCUMENT'
| 'CHAT'
| 'TEMPORARY_30_MINUTE'
| 'TEMPORARY_120_MINUTE'
| 'TEMPORARY_1_DAY',
) => Promise<Result<any>> = (file, sourceId, sourceType) => {
const fd = new FormData()
fd.append('file', file)
fd.append('source_id', sourceId)
fd.append('source_type', sourceType)
return post(`/oss/file`, fd)
}
export default { export default {
open, open,
chat, chat,
@ -316,5 +343,6 @@ export default {
textToSpeech, textToSpeech,
speechToText, speechToText,
deleteChat, deleteChat,
modifyChat modifyChat,
uploadFile,
} }

View File

@ -42,7 +42,7 @@
</div> </div>
</div> </div>
<div <div
@click="deleteFile(index, 'document')" @click="deleteFile(item)"
class="delete-icon color-secondary" class="delete-icon color-secondary"
v-if="showDelete === item.url" v-if="showDelete === item.url"
> >
@ -80,7 +80,7 @@
</div> </div>
</div> </div>
<div <div
@click="deleteFile(index, 'other')" @click="deleteFile(item)"
class="delete-icon color-secondary" class="delete-icon color-secondary"
v-if="showDelete === item.url" v-if="showDelete === item.url"
> >
@ -115,7 +115,7 @@
</div> </div>
</div> </div>
<div <div
@click="deleteFile(index, 'audio')" @click="deleteFile(item)"
class="delete-icon color-secondary" class="delete-icon color-secondary"
v-if="showDelete === item.url" v-if="showDelete === item.url"
> >
@ -136,7 +136,7 @@
@mouseleave.stop="mouseleave()" @mouseleave.stop="mouseleave()"
> >
<div <div
@click="deleteFile(index, 'image')" @click="deleteFile(item)"
class="delete-icon color-secondary" class="delete-icon color-secondary"
v-if="showDelete === item.url" v-if="showDelete === item.url"
> >
@ -297,7 +297,6 @@ import { ref, computed, onMounted, nextTick, watch, type Ref } from 'vue'
import Recorder from 'recorder-core' import Recorder from 'recorder-core'
import TouchChat from './TouchChat.vue' import TouchChat from './TouchChat.vue'
import applicationApi from '@/api/application/application' import applicationApi from '@/api/application/application'
import UserForm from '@/components/ai-chat/component/user-form/index.vue'
import { MsgAlert } from '@/utils/message' import { MsgAlert } from '@/utils/message'
import { type chatType } from '@/api/type/application' import { type chatType } from '@/api/type/application'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@ -354,7 +353,6 @@ const localLoading = computed({
}, },
}) })
const upload = ref() const upload = ref()
const imageExtensions = ['JPG', 'JPEG', 'PNG', 'GIF', 'BMP'] const imageExtensions = ['JPG', 'JPEG', 'PNG', 'GIF', 'BMP']
@ -421,92 +419,24 @@ const uploadFile = async (file: any, fileList: any) => {
fileList.splice(0, fileList.length) fileList.splice(0, fileList.length)
return return
} }
fileAllList.value = fileList
const formData = new FormData()
formData.append('file', file.raw, file.name)
//
const extension = file.name.split('.').pop().toUpperCase() //
if (imageExtensions.includes(extension)) {
uploadImageList.value.push(file)
} else if (documentExtensions.includes(extension)) {
uploadDocumentList.value.push(file)
} else if (videoExtensions.includes(extension)) {
uploadVideoList.value.push(file)
} else if (audioExtensions.includes(extension)) {
uploadAudioList.value.push(file)
} else if (otherExtensions.includes(extension)) {
uploadOtherList.value.push(file)
}
if (!chatId_context.value) { if (!chatId_context.value) {
const res = await props.openChatId() const res = await props.openChatId()
chatId_context.value = res chatId_context.value = res
} }
const api =
if (props.type === 'debug-ai-chat') { props.type === 'debug-ai-chat'
formData.append('debug', 'true') ? applicationApi.uploadFile(file.raw, 'TEMPORARY_120_MINUTE', 'TEMPORARY_120_MINUTE')
} else { : chatAPI.uploadFile(file.raw, chatId_context.value, 'CHAT')
formData.append('debug', 'false') api.then((ok) => {
file.url = ok.data
const split_path = ok.data.split('/')
file.file_id = split_path[split_path.length - 1]
})
if (!inputValue.value && uploadImageList.value.length > 0) {
inputValue.value = t('chat.uploadFile.imageMessage')
} }
applicationApi
.uploadFile(
props.applicationDetails.id as string,
chatId_context.value as string,
formData,
localLoading,
)
.then((response: any) => {
fileList.splice(0, fileList.length)
uploadImageList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') === file.name.replaceAll(' ', ''),
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
uploadDocumentList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') == file.name.replaceAll(' ', ''),
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
uploadAudioList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') === file.name.replaceAll(' ', ''),
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
uploadVideoList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') === file.name.replaceAll(' ', ''),
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
uploadOtherList.value.forEach((file: any) => {
const f = response.data.filter(
(f: any) => f.name.replaceAll(' ', '') === file.name.replaceAll(' ', ''),
)
if (f.length > 0) {
file.url = f[0].url
file.file_id = f[0].file_id
}
})
if (!inputValue.value && uploadImageList.value.length > 0) {
inputValue.value = t('chat.uploadFile.imageMessage')
}
})
} }
// //
const handlePaste = (event: ClipboardEvent) => { const handlePaste = (event: ClipboardEvent) => {
@ -564,11 +494,19 @@ const recorderTime = ref(0)
const recorderStatus = ref<'START' | 'TRANSCRIBING' | 'STOP'>('STOP') const recorderStatus = ref<'START' | 'TRANSCRIBING' | 'STOP'>('STOP')
const inputValue = ref<string>('') const inputValue = ref<string>('')
const uploadImageList = ref<Array<any>>([])
const uploadDocumentList = ref<Array<any>>([]) const fileAllList = ref<Array<any>>([])
const uploadVideoList = ref<Array<any>>([])
const uploadAudioList = ref<Array<any>>([]) const fileFilter = (fileList: Array<any>, extensionList: Array<string>) => {
const uploadOtherList = ref<Array<any>>([]) return fileList.filter((f) => {
return extensionList.includes(f.name.split('.').pop().toUpperCase())
})
}
const uploadImageList = computed(() => fileFilter(fileAllList.value, imageExtensions))
const uploadDocumentList = computed(() => fileFilter(fileAllList.value, documentExtensions))
const uploadVideoList = computed(() => fileFilter(fileAllList.value, videoExtensions))
const uploadAudioList = computed(() => fileFilter(fileAllList.value, audioExtensions))
const uploadOtherList = computed(() => fileFilter(fileAllList.value, otherExtensions))
const showDelete = ref('') const showDelete = ref('')
@ -794,11 +732,11 @@ function autoSendMessage() {
other_list: uploadOtherList.value, other_list: uploadOtherList.value,
}) })
inputValue.value = '' inputValue.value = ''
uploadImageList.value = [] fileAllList.value = []
uploadDocumentList.value = [] if (upload.value) {
uploadAudioList.value = [] upload.value.clearFiles()
uploadVideoList.value = [] }
uploadOtherList.value = []
if (quickInputRef.value) { if (quickInputRef.value) {
quickInputRef.value.textarea.style.height = '45px' quickInputRef.value.textarea.style.height = '45px'
} }
@ -845,18 +783,8 @@ const insertNewlineAtCursor = (event?: any) => {
}) })
} }
function deleteFile(index: number, val: string) { function deleteFile(item: any) {
if (val === 'image') { fileAllList.value = fileAllList.value.filter((i) => i != item)
uploadImageList.value.splice(index, 1)
} else if (val === 'document') {
uploadDocumentList.value.splice(index, 1)
} else if (val === 'video') {
uploadVideoList.value.splice(index, 1)
} else if (val === 'audio') {
uploadAudioList.value.splice(index, 1)
} else if (val === 'other') {
uploadOtherList.value.splice(index, 1)
}
} }
function mouseenter(row: any) { function mouseenter(row: any) {

View File

@ -218,7 +218,6 @@ const open = (folder: string, type?: string) => {
const submitHandle = async (formEl: FormInstance | undefined) => { const submitHandle = async (formEl: FormInstance | undefined) => {
if (!formEl) return if (!formEl) return
console.log(applicationForm.value.type)
await formEl.validate((valid) => { await formEl.validate((valid) => {
if (valid) { if (valid) {
if (isWorkFlow(applicationForm.value.type) && appTemplate.value === 'blank') { if (isWorkFlow(applicationForm.value.type) && appTemplate.value === 'blank') {
@ -226,7 +225,6 @@ const submitHandle = async (formEl: FormInstance | undefined) => {
workflowDefault.value.nodes[0].properties.node_data.name = applicationForm.value.name workflowDefault.value.nodes[0].properties.node_data.name = applicationForm.value.name
applicationForm.value['work_flow'] = workflowDefault.value applicationForm.value['work_flow'] = workflowDefault.value
} }
console.log(applicationForm.value.type)
applicationApi applicationApi
.postApplication({ ...applicationForm.value, folder_id: currentFolder.value }, loading) .postApplication({ ...applicationForm.value, folder_id: currentFolder.value }, loading)
.then((res) => { .then((res) => {