改进 文件上传和移除 UserFile
All checks were successful
Build / build (push) Successful in 8m42s

This commit is contained in:
ivamp 2024-09-18 12:57:57 +08:00
parent 92ea90ec81
commit e346e1a16a
4 changed files with 147 additions and 269 deletions

View File

@ -99,11 +99,12 @@ definitions:
file:
$ref: '#/definitions/entity.File'
file_id:
description: |-
FileId
虽然有了 UserFileId 但是 File Id 还是应该保留,因为这个是针对访客用户的
description: FileId
type: integer
hidden:
description: |-
UserFileId *schema.EntityId `json:"user_file_id"`
UserFile *UserFile `json:"user_file"`
type: boolean
id:
description: Id schema.EntityId `gorm:"primarykey" json:"id,string"`
@ -116,10 +117,6 @@ definitions:
type: integer
updated_at:
type: string
user_file:
$ref: '#/definitions/entity.UserFile'
user_file_id:
type: integer
type: object
entity.ChatMessageList:
properties:
@ -145,6 +142,9 @@ definitions:
file_id:
type: integer
hidden:
description: |-
UserFileId *schema.EntityId `json:"user_file_id"`
UserFile *UserFile `json:"user_file"`
type: boolean
id:
type: integer
@ -156,10 +156,6 @@ definitions:
type: integer
updated_at:
type: string
user_file:
$ref: '#/definitions/entity.UserFile'
user_file_id:
type: integer
type: object
entity.Document:
properties:
@ -185,6 +181,7 @@ definitions:
type: string
expired_at:
description: |-
Public bool `json:"public"` // 是否公开,访客上传的文件应始终公开,或归属于所有者
TODO: 移除 file 的到期时间,如果当 file 没有任何引用的时候再删除
因为有外键,所以直接删除是删不掉的,必须删除消息
type: string
@ -195,11 +192,6 @@ definitions:
type: integer
mime_type:
type: string
path:
type: string
public:
description: 是否公开,访客上传的文件应始终公开,或归属于所有者
type: boolean
size:
type: integer
updated_at:
@ -267,22 +259,6 @@ definitions:
user_id:
type: string
type: object
entity.UserFile:
properties:
created_at:
type: string
file:
$ref: '#/definitions/entity.File'
file_id:
type: integer
id:
description: Id schema.EntityId `gorm:"primarykey" json:"id,string"`
type: integer
updated_at:
type: string
user_id:
type: string
type: object
schema.AddPublicChatMessageRequest:
properties:
assistant_key:
@ -1738,38 +1714,16 @@ paths:
summary: 添加聊天记录
tags:
- chat_message
/api/v1/files/{id}/download:
/api/v1/files/download/{hash}:
get:
consumes:
- application/json
description: 根据 File ID 下载文件。如果文件是私有的,将无法下载
description: 根据文件 Hash 下载文件。仅支持图片下载,且图片具有有效期
parameters:
- in: path
name: id
- description: FileId uint64 `uri:"id" binding:"required"`
in: path
name: hash
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
type: file
summary: 下载公开文件
tags:
- file
/api/v1/files/user/{id}/download:
get:
consumes:
- application/json
description: 根据 File ID 下载文件。如果文件是私有的,将无法下载
parameters:
- in: path
name: id
required: true
type: integer
- in: query
name: id_token
type: string
produces:
- application/json
@ -1778,7 +1732,7 @@ paths:
description: OK
schema:
type: file
summary: 下载用户开文件
summary: 下载图片
tags:
- file
/api/v1/libraries:

View File

@ -906,13 +906,13 @@ export interface EntityChatMessage {
*/
'file'?: EntityFile;
/**
* FileId UserFileId File Id 访
* FileId
* @type {number}
* @memberof EntityChatMessage
*/
'file_id'?: number;
/**
*
* UserFileId *schema.EntityId `json:\"user_file_id\"` UserFile *UserFile `json:\"user_file\"`
* @type {boolean}
* @memberof EntityChatMessage
*/
@ -947,18 +947,6 @@ export interface EntityChatMessage {
* @memberof EntityChatMessage
*/
'updated_at'?: string;
/**
*
* @type {EntityUserFile}
* @memberof EntityChatMessage
*/
'user_file'?: EntityUserFile;
/**
*
* @type {number}
* @memberof EntityChatMessage
*/
'user_file_id'?: number;
}
/**
*
@ -1015,7 +1003,7 @@ export interface EntityChatMessageList {
*/
'file_id'?: number;
/**
*
* UserFileId *schema.EntityId `json:\"user_file_id\"` UserFile *UserFile `json:\"user_file\"`
* @type {boolean}
* @memberof EntityChatMessageList
*/
@ -1050,18 +1038,6 @@ export interface EntityChatMessageList {
* @memberof EntityChatMessageList
*/
'updated_at'?: string;
/**
*
* @type {EntityUserFile}
* @memberof EntityChatMessageList
*/
'user_file'?: EntityUserFile;
/**
*
* @type {number}
* @memberof EntityChatMessageList
*/
'user_file_id'?: number;
}
/**
*
@ -1144,7 +1120,7 @@ export interface EntityFile {
*/
'created_at'?: string;
/**
* TODO: 移除 file file
* Public bool `json:\"public\"` // 是否公开,访客上传的文件应始终公开,或归属于所有者 TODO: 移除 file 的到期时间,如果当 file 没有任何引用的时候再删除 因为有外键,所以直接删除是删不掉的,必须删除消息
* @type {string}
* @memberof EntityFile
*/
@ -1167,18 +1143,6 @@ export interface EntityFile {
* @memberof EntityFile
*/
'mime_type'?: string;
/**
*
* @type {string}
* @memberof EntityFile
*/
'path'?: string;
/**
* 访
* @type {boolean}
* @memberof EntityFile
*/
'public'?: boolean;
/**
*
* @type {number}
@ -1357,49 +1321,6 @@ export interface EntityTool {
*/
'user_id'?: string;
}
/**
*
* @export
* @interface EntityUserFile
*/
export interface EntityUserFile {
/**
*
* @type {string}
* @memberof EntityUserFile
*/
'created_at'?: string;
/**
*
* @type {EntityFile}
* @memberof EntityUserFile
*/
'file'?: EntityFile;
/**
*
* @type {number}
* @memberof EntityUserFile
*/
'file_id'?: number;
/**
* Id schema.EntityId `gorm:\"primarykey\" json:\"id,string\"`
* @type {number}
* @memberof EntityUserFile
*/
'id'?: number;
/**
*
* @type {string}
* @memberof EntityUserFile
*/
'updated_at'?: string;
/**
*
* @type {string}
* @memberof EntityUserFile
*/
'user_id'?: string;
}
/**
*
* @export
@ -4663,17 +4584,17 @@ export class ChatPublicApi extends BaseAPI {
export const FileApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
* File ID
* @summary
* @param {number} id
* Hash
* @summary
* @param {string} hash FileId uint64 `uri:\"id\" binding:\"required\"`
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1FilesIdDownloadGet: async (id: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('apiV1FilesIdDownloadGet', 'id', id)
const localVarPath = `/api/v1/files/{id}/download`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
apiV1FilesDownloadHashGet: async (hash: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'hash' is not null or undefined
assertParamExists('apiV1FilesDownloadHashGet', 'hash', hash)
const localVarPath = `/api/v1/files/download/{hash}`
.replace(`{${"hash"}}`, encodeURIComponent(String(hash)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@ -4687,45 +4608,6 @@ export const FileApiAxiosParamCreator = function (configuration?: Configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* File ID
* @summary
* @param {number} id
* @param {string} [idToken]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1FilesUserIdDownloadGet: async (id: number, idToken?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('apiV1FilesUserIdDownloadGet', 'id', id)
const localVarPath = `/api/v1/files/user/{id}/download`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
if (idToken !== undefined) {
localVarQueryParameter['id_token'] = idToken;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -4746,30 +4628,16 @@ export const FileApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = FileApiAxiosParamCreator(configuration)
return {
/**
* File ID
* @summary
* @param {number} id
* Hash
* @summary
* @param {string} hash FileId uint64 &#x60;uri:\&quot;id\&quot; binding:\&quot;required\&quot;&#x60;
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiV1FilesIdDownloadGet(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.apiV1FilesIdDownloadGet(id, options);
async apiV1FilesDownloadHashGet(hash: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.apiV1FilesDownloadHashGet(hash, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['FileApi.apiV1FilesIdDownloadGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* File ID
* @summary
* @param {number} id
* @param {string} [idToken]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiV1FilesUserIdDownloadGet(id: number, idToken?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.apiV1FilesUserIdDownloadGet(id, idToken, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['FileApi.apiV1FilesUserIdDownloadGet']?.[localVarOperationServerIndex]?.url;
const localVarOperationServerBasePath = operationServerMap['FileApi.apiV1FilesDownloadHashGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
@ -4783,25 +4651,14 @@ export const FileApiFactory = function (configuration?: Configuration, basePath?
const localVarFp = FileApiFp(configuration)
return {
/**
* File ID
* @summary
* @param {number} id
* Hash
* @summary
* @param {string} hash FileId uint64 &#x60;uri:\&quot;id\&quot; binding:\&quot;required\&quot;&#x60;
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1FilesIdDownloadGet(id: number, options?: RawAxiosRequestConfig): AxiosPromise<File> {
return localVarFp.apiV1FilesIdDownloadGet(id, options).then((request) => request(axios, basePath));
},
/**
* File ID
* @summary
* @param {number} id
* @param {string} [idToken]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1FilesUserIdDownloadGet(id: number, idToken?: string, options?: RawAxiosRequestConfig): AxiosPromise<File> {
return localVarFp.apiV1FilesUserIdDownloadGet(id, idToken, options).then((request) => request(axios, basePath));
apiV1FilesDownloadHashGet(hash: string, options?: RawAxiosRequestConfig): AxiosPromise<File> {
return localVarFp.apiV1FilesDownloadHashGet(hash, options).then((request) => request(axios, basePath));
},
};
};
@ -4814,28 +4671,15 @@ export const FileApiFactory = function (configuration?: Configuration, basePath?
*/
export class FileApi extends BaseAPI {
/**
* File ID
* @summary
* @param {number} id
* Hash
* @summary
* @param {string} hash FileId uint64 &#x60;uri:\&quot;id\&quot; binding:\&quot;required\&quot;&#x60;
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof FileApi
*/
public apiV1FilesIdDownloadGet(id: number, options?: RawAxiosRequestConfig) {
return FileApiFp(this.configuration).apiV1FilesIdDownloadGet(id, options).then((request) => request(this.axios, this.basePath));
}
/**
* File ID
* @summary
* @param {number} id
* @param {string} [idToken]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof FileApi
*/
public apiV1FilesUserIdDownloadGet(id: number, idToken?: string, options?: RawAxiosRequestConfig) {
return FileApiFp(this.configuration).apiV1FilesUserIdDownloadGet(id, idToken, options).then((request) => request(this.axios, this.basePath));
public apiV1FilesDownloadHashGet(hash: string, options?: RawAxiosRequestConfig) {
return FileApiFp(this.configuration).apiV1FilesDownloadHashGet(hash, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -82,7 +82,12 @@
v-if="chatId !== null && chatData.assistant_id !== null"
>
<template #trigger>
<n-button tertiary circle size="large">
<n-button
tertiary
circle
size="large"
@click="showUploadModal = true"
>
<template #icon>
<n-icon><DocumentAttachOutline /></n-icon>
</template>
@ -148,6 +153,34 @@
</div>
</div>
</div>
<n-modal
v-model:show="showUploadModal"
:bordered="false"
class="w-0"
preset="card"
title="上传"
size="huge"
:style="{
width: '80%',
}"
>
<n-upload directory-dnd :custom-request="uploadFile" :max="5">
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<ArchiveOutline />
</n-icon>
</div>
<n-text style="font-size: 16px">
点击或者拖动文件到该区域来上传
</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">
请不要上传敏感数据比如你的银行卡号和密码信用卡号有效期和安全码
</n-p>
</n-upload-dragger>
</n-upload>
</n-modal>
</template>
<script setup lang="ts">
import { useUserStore } from "../../stores/user";
@ -157,6 +190,7 @@ import {
MicOutline,
DocumentAttachOutline,
TrashBinOutline,
ArchiveOutline,
} from "@vicons/ionicons5";
import {
EntityChatMessage,
@ -171,7 +205,7 @@ import router from "@/router";
import element from "@/config/element";
import { useIsMobile } from "@/utils/composables";
import { useAppStore } from "@/stores/app";
import { useDialog, useMessage } from "naive-ui";
import { UploadCustomRequestOptions, useDialog, useMessage } from "naive-ui";
import { useAssistantStore } from "@/stores/assistants";
// chatId
@ -211,6 +245,18 @@ const appStore = useAppStore();
const dialog = useDialog();
const assistantStore = useAssistantStore();
const message = useMessage();
const showUploadModal = ref(false);
const pasteUpload = (event: ClipboardEvent) => {
// const items = event.clipboardData && event.clipboardData.items;
// if (items && items.length) {
// for (let i = 0; i < items.length; i++) {
// if (items[i].type.indexOf("image") !== -1) {
// fileUpload.value = items[i].getAsFile();
// break;
// }
// }
// }
};
function onKeydown(e: KeyboardEvent) {
// Esc
@ -378,7 +424,7 @@ async function sendMessage(
}
let chatVariables = {
"now": new Date().toLocaleString(),
now: new Date().toLocaleString(),
};
//
@ -650,12 +696,16 @@ onMounted(() => {
if (chatId.value) {
getChat();
}
document.addEventListener("paste", pasteUpload);
});
onUnmounted(() => {
chatStore.currentChat = {
id: 0,
};
document.removeEventListener("paste", pasteUpload);
});
const assistantMenuOptions: any = ref([]);
@ -679,17 +729,28 @@ const showAssistantSelect = async () => {
}
};
const uploadFile = () => {
if (!fileUpload.value || !chatId.value) {
return;
}
const uploadFile = ({
file,
data,
headers,
withCredentials,
action,
onFinish,
onError,
onProgress,
}: UploadCustomRequestOptions) => {
// const formData = new FormData();
// formData.append(file.name, file.file as File);
// uploading.value = true;
uploading.value = true;
getApi()
.ChatMessage.apiV1ChatsIdFilesPost(
Number(chatId.value),
{
file: fileUpload.value,
file: file.file as File,
url: "",
},
{
@ -701,12 +762,14 @@ const uploadFile = () => {
.then((r) => {
//
if (r.status === 200 || r.status === 201) {
fileUpload.value = null;
getChatMessages();
onFinish();
}
})
.catch((err) => {
alert(err.response.data.message);
message.error("上传失败: " + err.response.data.message);
onError();
})
.finally(() => {
uploading.value = false;

View File

@ -8,18 +8,31 @@
<!-- 文件类型 -->
<n-flex justify="end">
<div class="flex items-center flex-nowrap">
<!-- 如果是 user file -->
<div class="flex items-end flex-col">
<div>
<n-divider class="!p-0 !m-0" title-placement="right">
{{ userStore.user.name }}
</n-divider>
</div>
<n-image
v-if="message.user_file"
v-if="message.file && message.file.mime_type?.startsWith('image/')"
width="100"
:src="fileBaseUrl + '/user/' + message.user_file.id + '/download'"
:src="fileBaseUrl + '/download/' + message.file.file_hash"
/>
<n-image
v-else-if="message.file"
width="100"
:src="fileBaseUrl + '/' + message.file.id + '/download'"
<n-text italic depth="3" v-else>
你上传了一个文件
</n-text>
</div>
<div class="relative h-full">
<n-avatar
round
size="large"
:src="userStore.user.avatar"
class="ml-3 min-w-10 absolute top-0"
/>
</div>
</div>
</n-flex>
</div>
<div v-else-if="message.role === 'user' && message.content">
@ -40,7 +53,7 @@
</div>
<div
v-if="mdInited"
class="markdown-body"
class="break-all break-words markdown-body"
v-html="mdIt.render(message.content)"
></div>
</div>
@ -80,7 +93,11 @@
<!-- message.content 变化时重新渲染 -->
<div>
<div
v-if="message.assistant_id && message.assistant !== null && message.assistant?.name !== ''"
v-if="
message.assistant_id &&
message.assistant !== null &&
message.assistant?.name !== ''
"
>
<n-divider class="!p-0 !m-0" title-placement="left">
{{ message.assistant?.name }}