Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 59 additions & 9 deletions ui/src/conversation-panel/components/message-input/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { ref, computed, reactive, watch, inject, type Ref, type InjectionKey } f
import type { LocationQuery } from 'vue-router'
import { pick } from 'lodash'
import { IMAGE_EXTENSIONS, DOCUMENT_EXTENSIONS, VIDEO_EXTENSIONS, AUDIO_EXTENSIONS } from '@/constants/file-type'
import { isImage, isDocument, isAudio, isVideo } from '@/utils/icon'
import { MsgError } from '@/utils/message'
import { isImage, isDocument, isAudio, isVideo, getFileExtension } from '@/utils/icon'
import { MsgError, MsgWarning } from '@/utils/message'
import type { Dict } from '@/api/types'
import { defaultFileUploadSetting } from '@/workflow-canvas/nodes/base-node/constant'
import type { BaseNodeForm, FileUploadSettingData } from '@/workflow-canvas/nodes/base-node/types'
import type { ChatMessage, Conversation, ConversationAppInfo, SendMessageOptions } from '../../core/types'
import { createUserFormStore } from './user-form/index'

Expand Down Expand Up @@ -77,10 +79,39 @@ export function createMessageInputStore(deps: MessageInputDeps) {
const question = ref('')
const fileList = ref<FileItem[]>([])
const sending = ref(false)
const maxFiles = 10
const maxSizeMB = 50

const acceptList = [...IMAGE_EXTENSIONS, ...DOCUMENT_EXTENSIONS, ...VIDEO_EXTENSIONS, ...AUDIO_EXTENSIONS].map((e) => `.${e}`).join(',')
// 文件上传配置:优先读取开始节点,缺失时回退到智能体字段,再补齐默认值。
const baseNodeData = computed(() => {
const baseNode = appInfo.value?.work_flow?.nodes?.find((node) => node.id === 'base-node')
return (baseNode?.properties?.node_data ?? {}) as Partial<Pick<BaseNodeForm, 'file_upload_enable' | 'file_upload_setting'>>
})
const uploadSetting = computed<FileUploadSettingData>(() => ({
...defaultFileUploadSetting,
...(baseNodeData.value.file_upload_setting ?? (appInfo.value?.file_upload_setting as Partial<FileUploadSettingData> | undefined)),
}))
const fileUploadEnabled = computed(() => Boolean(baseNodeData.value.file_upload_enable ?? appInfo.value?.file_upload_enable))
/** 本地上传:控制上传按钮、拖拽与粘贴入口。 */
const uploadEnabled = computed(() => fileUploadEnabled.value && uploadSetting.value.local_upload)
// TODO: URL 上传入口未实现,页面接入时使用 urlUploadEnabled 控制显示,并复用数量、大小、类型限制。
const urlUploadEnabled = computed(() => fileUploadEnabled.value && uploadSetting.value.url_upload)
const maxFiles = computed(() => uploadSetting.value.maxFiles)
const maxSizeMB = computed(() => uploadSetting.value.fileLimit)
/** 允许上传的大写扩展名:按勾选的文件类型拼接,其他文件使用自定义扩展名。 */
const allowedExtensions = computed(() => {
const { image, document, audio, video, other, otherExtensions } = uploadSetting.value
const extensions = [
...(image ? IMAGE_EXTENSIONS : []),
...(document ? DOCUMENT_EXTENSIONS : []),
...(audio ? AUDIO_EXTENSIONS : []),
...(video ? VIDEO_EXTENSIONS : []),
...(other ? (otherExtensions ?? []).map((extension) => extension.toUpperCase()) : []),
]
return [...new Set(extensions)]
})
const acceptList = computed(() => allowedExtensions.value.map((extension) => `.${extension}`).join(','))
const canUpload = computed(
() => uploadEnabled.value && !disabled.value && !streamLoading.value && !sending.value && fileList.value.length < maxFiles.value,
)

// 语音输入:智能体开启语音转文字且当前模式提供接口时可用
const transcribing = ref(false)
Expand All @@ -106,10 +137,19 @@ export function createMessageInputStore(deps: MessageInputDeps) {
const imageFiles = computed(() => fileList.value.filter((attachment) => isImage(attachment.name)))

// 附件上传与本地预览
const validateFile = (file: File) => fileList.value.length < maxFiles && file.size > 0 && file.size <= maxSizeMB * 1024 * 1024
/** 校验单个文件,返回不通过的提示;拖拽、粘贴不受 accept 限制,需校验扩展名。 */
const getFileError = (file: File) => {
if (fileList.value.length >= maxFiles.value) return `单次最多上传 ${maxFiles.value} 个文件`
if (!allowedExtensions.value.includes(getFileExtension(file.name))) return '不支持上传该类型的文件'
if (file.size <= 0) return '不能上传空文件'
if (file.size > maxSizeMB.value * 1024 * 1024) return `每个文件不能超过 ${maxSizeMB.value}MB`
}

const addFile = (file: File) => {
if (disabled.value || streamLoading.value || sending.value || !validateFile(file)) return
/** 添加并上传文件,校验不通过时返回提示。 */
const addFile = (file: File): string | undefined => {
if (!uploadEnabled.value || disabled.value || streamLoading.value || sending.value) return
const error = getFileError(file)
if (error) return error
const attachment: FileItem = reactive({
uid: Date.now() + Math.random(),
name: file.name,
Expand Down Expand Up @@ -159,7 +199,13 @@ export function createMessageInputStore(deps: MessageInputDeps) {

const addFiles = (files: FileList | File[] | null | undefined) => {
if (!files || disabled.value) return
Array.from(files).forEach(addFile)
// 批量添加时相同提示只显示一次
const errors = new Set<string>()
Array.from(files).forEach((file) => {
const error = addFile(file)
if (error) errors.add(error)
})
errors.forEach((error) => MsgWarning(error))
}

const removeFile = (index: number) => {
Expand Down Expand Up @@ -287,8 +333,12 @@ export function createMessageInputStore(deps: MessageInputDeps) {
userForm,
question,
fileList,
uploadEnabled,
urlUploadEnabled,
canUpload,
maxFiles,
maxSizeMB,
allowedExtensions,
acceptList,
streamLoading,
disabled,
Expand Down
14 changes: 8 additions & 6 deletions ui/src/conversation-panel/components/message-input/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import { useVoiceRecorder } from './voice-recorder/index'
const {
question,
fileList,
uploadEnabled,
canUpload,
maxFiles,
maxSizeMB,
allowedExtensions,
acceptList,
streamLoading,
disabled,
Expand All @@ -32,7 +35,7 @@ const {
// 所有附件入口共用禁用条件,上传和发送仍由输入 Store 处理。
const { fileInputRef, handleFileSelect, handlePaste, handleDragover, handleDrop, handleKeydown } = useInputEvents({
text: question,
canUpload: () => !disabled.value && !streamLoading.value && fileList.value.length < maxFiles,
canUpload: () => canUpload.value,
addFiles,
submit: () => {
if (canSend.value) send()
Expand Down Expand Up @@ -98,20 +101,19 @@ const { recording, seconds: recordSeconds, waveLevels, start: startRecording, st
<!-- 操作栏 -->
<div class="mt-3 flex items-center">
<UserForm class="mr-3" />
<input ref="fileInputRef" type="file" multiple :accept="acceptList" class="hidden" @change="handleFileSelect" />
<MkTooltip placement="top">
<input v-if="uploadEnabled" ref="fileInputRef" type="file" multiple :accept="acceptList" class="hidden" @change="handleFileSelect" />
<MkTooltip v-if="uploadEnabled" placement="top">
<template #content>
<div class="break-all whitespace-pre-wrap">
可拖拽到输入框内上传文件
<br />
上传文件:最多{{ maxFiles }}个,每个文件限制{{ maxSizeMB }}MB
<br />
<!-- // TODO: 类型格式处理 -->
文件类型:{{ acceptList }}
文件类型:{{ allowedExtensions.join('、') }}
</div>
</template>
<!-- 上传文件 -->
<el-button text :disabled="disabled || streamLoading || fileList.length >= maxFiles" @click="fileInputRef?.click()">
<el-button text :disabled="!canUpload" @click="fileInputRef?.click()">
<MkIcon name="icon_attachment_outlined" :size="18" class="text-N900!" />
</el-button>
</MkTooltip>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the diff looks syntactically valid and the refactor is reasonable. The main risk is ensuring the new canUpload computed has exactly the same semantics as the old inline condition, plus any upload-feature gating.

Potential issues

  1. canUpload must include uploadEnabled

    • The template now hides the upload input/button when uploadEnabled is false.
    • However, useInputEvents now relies only on canUpload.value.
    • If canUpload does not include uploadEnabled, then paste/drag-drop handlers may still add files even though the upload UI is hidden.
    • Safest defensive option:
      canUpload: () => uploadEnabled.value && canUpload.value
      unless canUpload already incorporates uploadEnabled.
  2. Guard allowedExtensions before calling .join()

    • allowedExtensions.join('、') will throw if allowedExtensions is undefined, null, or not an array.
    • Prefer a computed fallback:
      const allowedExtensionsText = computed(() =>
        allowedExtensions.value?.length
          ? allowedExtensions.value.join('、')
          : '无限制'
      )
      Then use {{ allowedExtensionsText }} in the template.
  3. Ensure canUpload preserves the old conditions

    • The old condition was:
      !disabled.value && !streamLoading.value && fileList.value.length < maxFiles
    • Confirm the new canUpload computed includes all of these. If it omits any condition, the upload button and paste/drop behavior may regress.
  4. v-if on the file input and fileInputRef

    • Using v-if="uploadEnabled" means fileInputRef is null while uploads are disabled.
    • That is fine if all click paths are also gated, but ensure no external code calls fileInputRef.value?.click() without checking uploadEnabled/canUpload.
  5. allowedExtensions vs acceptList consistency

    • The display now uses allowedExtensions, while the file input still uses acceptList.
    • Make sure these are kept in sync or derived from the same source. Otherwise the tooltip may describe different accepted types than the actual input.

Optimization / readability suggestions

  • Wrap both upload-related elements in one <template v-if="uploadEnabled"> instead of repeating v-if:

    <template v-if="uploadEnabled">
      <input ... />
      <MkTooltip ...>...</MkTooltip>
    </template>
  • Consider renaming or documenting the distinction between:

    • uploadEnabled — feature-level availability
    • canUpload — current-state eligibility
      This helps avoid future confusion.
  • If allowedExtensions is empty, decide whether to show “无限制” or fall back to acceptList; an empty string in the tooltip is not user-friendly.

No obvious syntax problems in the diff, but the canUpload/uploadEnabled relationship is the most important correctness point to verify.

Expand Down
Loading