Merge branch 'main' of https://github.com/maxkb-dev/maxkb
This commit is contained in:
commit
1a8544a03f
@ -19,7 +19,7 @@ class ParagraphPipelineModel:
|
|||||||
|
|
||||||
def __init__(self, _id: str, document_id: str, dataset_id: str, content: str, title: str, status: str,
|
def __init__(self, _id: str, document_id: str, dataset_id: str, content: str, title: str, status: str,
|
||||||
is_active: bool, comprehensive_score: float, similarity: float, dataset_name: str, document_name: str,
|
is_active: bool, comprehensive_score: float, similarity: float, dataset_name: str, document_name: str,
|
||||||
hit_handling_method: str, directly_return_similarity: float):
|
hit_handling_method: str, directly_return_similarity: float, meta: dict = None):
|
||||||
self.id = _id
|
self.id = _id
|
||||||
self.document_id = document_id
|
self.document_id = document_id
|
||||||
self.dataset_id = dataset_id
|
self.dataset_id = dataset_id
|
||||||
@ -33,6 +33,7 @@ class ParagraphPipelineModel:
|
|||||||
self.document_name = document_name
|
self.document_name = document_name
|
||||||
self.hit_handling_method = hit_handling_method
|
self.hit_handling_method = hit_handling_method
|
||||||
self.directly_return_similarity = directly_return_similarity
|
self.directly_return_similarity = directly_return_similarity
|
||||||
|
self.meta = meta
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
@ -46,7 +47,8 @@ class ParagraphPipelineModel:
|
|||||||
'comprehensive_score': self.comprehensive_score,
|
'comprehensive_score': self.comprehensive_score,
|
||||||
'similarity': self.similarity,
|
'similarity': self.similarity,
|
||||||
'dataset_name': self.dataset_name,
|
'dataset_name': self.dataset_name,
|
||||||
'document_name': self.document_name
|
'document_name': self.document_name,
|
||||||
|
'meta': self.meta,
|
||||||
}
|
}
|
||||||
|
|
||||||
class builder:
|
class builder:
|
||||||
@ -58,6 +60,7 @@ class ParagraphPipelineModel:
|
|||||||
self.dataset_name = None
|
self.dataset_name = None
|
||||||
self.hit_handling_method = None
|
self.hit_handling_method = None
|
||||||
self.directly_return_similarity = 0.9
|
self.directly_return_similarity = 0.9
|
||||||
|
self.meta = {}
|
||||||
|
|
||||||
def add_paragraph(self, paragraph):
|
def add_paragraph(self, paragraph):
|
||||||
if isinstance(paragraph, Paragraph):
|
if isinstance(paragraph, Paragraph):
|
||||||
@ -97,6 +100,10 @@ class ParagraphPipelineModel:
|
|||||||
self.similarity = similarity
|
self.similarity = similarity
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def add_meta(self, meta: dict):
|
||||||
|
self.meta = meta
|
||||||
|
return self
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
return ParagraphPipelineModel(str(self.paragraph.get('id')), str(self.paragraph.get('document_id')),
|
return ParagraphPipelineModel(str(self.paragraph.get('id')), str(self.paragraph.get('document_id')),
|
||||||
str(self.paragraph.get('dataset_id')),
|
str(self.paragraph.get('dataset_id')),
|
||||||
@ -104,7 +111,8 @@ class ParagraphPipelineModel:
|
|||||||
self.paragraph.get('status'),
|
self.paragraph.get('status'),
|
||||||
self.paragraph.get('is_active'),
|
self.paragraph.get('is_active'),
|
||||||
self.comprehensive_score, self.similarity, self.dataset_name,
|
self.comprehensive_score, self.similarity, self.dataset_name,
|
||||||
self.document_name, self.hit_handling_method, self.directly_return_similarity)
|
self.document_name, self.hit_handling_method, self.directly_return_similarity,
|
||||||
|
self.meta)
|
||||||
|
|
||||||
|
|
||||||
class IBaseChatPipelineStep:
|
class IBaseChatPipelineStep:
|
||||||
|
|||||||
@ -79,6 +79,7 @@ class BaseSearchDatasetStep(ISearchDatasetStep):
|
|||||||
.add_document_name(paragraph.get('document_name'))
|
.add_document_name(paragraph.get('document_name'))
|
||||||
.add_hit_handling_method(paragraph.get('hit_handling_method'))
|
.add_hit_handling_method(paragraph.get('hit_handling_method'))
|
||||||
.add_directly_return_similarity(paragraph.get('directly_return_similarity'))
|
.add_directly_return_similarity(paragraph.get('directly_return_similarity'))
|
||||||
|
.add_meta(paragraph.get('meta'))
|
||||||
.build())
|
.build())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -40,7 +40,7 @@ class BaseDocumentExtractNode(IDocumentExtractNode):
|
|||||||
"index": index,
|
"index": index,
|
||||||
'run_time': self.context.get('run_time'),
|
'run_time': self.context.get('run_time'),
|
||||||
'type': self.node.type,
|
'type': self.node.type,
|
||||||
'content': self.context.get('content'),
|
# 'content': self.context.get('content'), # 不保存content内容,因为content内容可能会很大
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
'err_message': self.err_message,
|
'err_message': self.err_message,
|
||||||
'document_list': self.context.get('document_list')
|
'document_list': self.context.get('document_list')
|
||||||
|
|||||||
@ -67,11 +67,13 @@ class BaseImageUnderstandNode(IImageUnderstandNode):
|
|||||||
image,
|
image,
|
||||||
**kwargs) -> NodeResult:
|
**kwargs) -> NodeResult:
|
||||||
image_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'))
|
image_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'))
|
||||||
history_message = self.get_history_message(history_chat_record, dialogue_number)
|
# 执行详情中的历史消息不需要图片内容
|
||||||
|
history_message =self.get_history_message_for_details(history_chat_record, dialogue_number)
|
||||||
self.context['history_message'] = history_message
|
self.context['history_message'] = history_message
|
||||||
question = self.generate_prompt_question(prompt)
|
question = self.generate_prompt_question(prompt)
|
||||||
self.context['question'] = question.content
|
self.context['question'] = question.content
|
||||||
message_list = self.generate_message_list(image_model, system, prompt, history_message, image)
|
# 生成消息列表, 真实的history_message
|
||||||
|
message_list = self.generate_message_list(image_model, system, prompt, self.get_history_message(history_chat_record, dialogue_number), image)
|
||||||
self.context['message_list'] = message_list
|
self.context['message_list'] = message_list
|
||||||
self.context['image_list'] = image
|
self.context['image_list'] = image
|
||||||
self.context['dialogue_type'] = dialogue_type
|
self.context['dialogue_type'] = dialogue_type
|
||||||
@ -86,6 +88,15 @@ class BaseImageUnderstandNode(IImageUnderstandNode):
|
|||||||
'history_message': history_message, 'question': question.content}, {},
|
'history_message': history_message, 'question': question.content}, {},
|
||||||
_write_context=write_context)
|
_write_context=write_context)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_history_message_for_details(history_chat_record, dialogue_number):
|
||||||
|
start_index = len(history_chat_record) - dialogue_number
|
||||||
|
history_message = reduce(lambda x, y: [*x, *y], [
|
||||||
|
[history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
|
||||||
|
for index in
|
||||||
|
range(start_index if start_index > 0 else 0, len(history_chat_record))], [])
|
||||||
|
return history_message
|
||||||
|
|
||||||
def get_history_message(self, history_chat_record, dialogue_number):
|
def get_history_message(self, history_chat_record, dialogue_number):
|
||||||
start_index = len(history_chat_record) - dialogue_number
|
start_index = len(history_chat_record) - dialogue_number
|
||||||
history_message = reduce(lambda x, y: [*x, *y], [
|
history_message = reduce(lambda x, y: [*x, *y], [
|
||||||
|
|||||||
@ -2,6 +2,7 @@ SELECT
|
|||||||
paragraph.*,
|
paragraph.*,
|
||||||
dataset."name" AS "dataset_name",
|
dataset."name" AS "dataset_name",
|
||||||
"document"."name" AS "document_name",
|
"document"."name" AS "document_name",
|
||||||
|
"document"."meta" AS "meta",
|
||||||
"document"."hit_handling_method" AS "hit_handling_method",
|
"document"."hit_handling_method" AS "hit_handling_method",
|
||||||
"document"."directly_return_similarity" as "directly_return_similarity"
|
"document"."directly_return_similarity" as "directly_return_similarity"
|
||||||
FROM
|
FROM
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
<div class="flex align-center">
|
<div class="flex align-center">
|
||||||
<span
|
<span
|
||||||
class="mr-16 color-secondary"
|
class="mr-16 color-secondary"
|
||||||
v-if="item.type === WorkflowType.Question || item.type === WorkflowType.AiChat"
|
v-if="item.type === WorkflowType.Question || item.type === WorkflowType.AiChat || item.type === WorkflowType.ImageUnderstandNode"
|
||||||
>{{ item?.message_tokens + item?.answer_tokens }} tokens</span
|
>{{ item?.message_tokens + item?.answer_tokens }} tokens</span
|
||||||
>
|
>
|
||||||
<span class="mr-16 color-secondary">{{ item?.run_time?.toFixed(2) || 0.0 }} s</span>
|
<span class="mr-16 color-secondary">{{ item?.run_time?.toFixed(2) || 0.0 }} s</span>
|
||||||
|
|||||||
@ -9,15 +9,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-8" v-if="!isWorkFlow(props.type)">
|
<div class="mt-8" v-if="!isWorkFlow(props.type)">
|
||||||
<el-space wrap>
|
<el-space wrap>
|
||||||
<el-button
|
<div v-for="(paragraph, index) in uniqueParagraphList" :key="index">
|
||||||
v-for="(dataset, index) in data.dataset_list"
|
<el-icon class="mr-4" :size="25">
|
||||||
:key="index"
|
<img :src="getIconPath(paragraph.document_name)" style="width: 90%" alt="" />
|
||||||
size="small"
|
</el-icon>
|
||||||
class="source_dataset-button"
|
<span
|
||||||
@click="openParagraph(data, dataset.id)"
|
v-if="!paragraph.source_url"
|
||||||
|
class="ellipsis"
|
||||||
|
:title="paragraph?.document_name?.trim()"
|
||||||
>
|
>
|
||||||
<span class="ellipsis" :title="dataset.name"> {{ dataset.name }}</span>
|
{{ paragraph?.document_name }}
|
||||||
</el-button>
|
</span>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
@click="openLink(paragraph.source_url)"
|
||||||
|
class="ellipsis"
|
||||||
|
:title="paragraph?.document_name?.trim()"
|
||||||
|
>
|
||||||
|
<span :title="paragraph?.document_name?.trim()">
|
||||||
|
{{ paragraph?.document_name }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</el-space>
|
</el-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -42,7 +55,7 @@
|
|||||||
<ExecutionDetailDialog ref="ExecutionDetailDialogRef" />
|
<ExecutionDetailDialog ref="ExecutionDetailDialogRef" />
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import ParagraphSourceDialog from './ParagraphSourceDialog.vue'
|
import ParagraphSourceDialog from './ParagraphSourceDialog.vue'
|
||||||
import ExecutionDetailDialog from './ExecutionDetailDialog.vue'
|
import ExecutionDetailDialog from './ExecutionDetailDialog.vue'
|
||||||
import { isWorkFlow } from '@/utils/application'
|
import { isWorkFlow } from '@/utils/application'
|
||||||
@ -57,6 +70,15 @@ const props = defineProps({
|
|||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const iconMap: { [key: string]: string } = {
|
||||||
|
doc: '../../assets/doc-icon.svg',
|
||||||
|
docx: '../../assets/docx-icon.svg',
|
||||||
|
pdf: '../../assets/pdf-icon.svg',
|
||||||
|
md: '../../assets/md-icon.svg',
|
||||||
|
txt: '../../assets/txt-icon.svg',
|
||||||
|
xls: '../../assets/xls-icon.svg',
|
||||||
|
xlsx: '../../assets/xlsx-icon.svg'
|
||||||
|
}
|
||||||
|
|
||||||
const ParagraphSourceDialogRef = ref()
|
const ParagraphSourceDialogRef = ref()
|
||||||
const ExecutionDetailDialogRef = ref()
|
const ExecutionDetailDialogRef = ref()
|
||||||
@ -66,6 +88,40 @@ function openParagraph(row: any, id?: string) {
|
|||||||
function openExecutionDetail(row: any) {
|
function openExecutionDetail(row: any) {
|
||||||
ExecutionDetailDialogRef.value.open(row)
|
ExecutionDetailDialogRef.value.open(row)
|
||||||
}
|
}
|
||||||
|
const uniqueParagraphList = computed(() => {
|
||||||
|
const seen = new Set()
|
||||||
|
return (
|
||||||
|
props.data.paragraph_list?.filter((paragraph: any) => {
|
||||||
|
const key = paragraph.document_name.trim()
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
seen.add(key)
|
||||||
|
// 判断如果 meta 属性不是 {} 需要json解析 转对象
|
||||||
|
if (paragraph.meta && typeof paragraph.meta === 'string') {
|
||||||
|
paragraph.meta = JSON.parse(paragraph.meta)
|
||||||
|
paragraph.source_url = paragraph.meta.source_url
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}) || []
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function getIconPath(documentName: string) {
|
||||||
|
const extension = documentName.split('.').pop()?.toLowerCase()
|
||||||
|
if (!documentName || !extension) return new URL(`${iconMap['doc']}`, import.meta.url).href
|
||||||
|
if (iconMap && extension && iconMap[extension]) {
|
||||||
|
return new URL(`${iconMap[extension]}`, import.meta.url).href
|
||||||
|
}
|
||||||
|
return new URL(`${iconMap['doc']}`, import.meta.url).href
|
||||||
|
}
|
||||||
|
function openLink(url: string) {
|
||||||
|
// 如果url不是以/结尾,加上/
|
||||||
|
if (url && !url.endsWith('/')) {
|
||||||
|
url += '/'
|
||||||
|
}
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.source_dataset-button {
|
.source_dataset-button {
|
||||||
|
|||||||
@ -18,10 +18,48 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="footer-content flex-between">
|
<div class="footer-content flex-between">
|
||||||
<el-text class="flex align-center" style="width: 70%">
|
<el-text class="flex align-center" style="width: 70%">
|
||||||
<el-icon class="mr-4">
|
<el-icon class="mr-4" :size="25">
|
||||||
<Document />
|
<img
|
||||||
|
src="@/assets/doc-icon.svg"
|
||||||
|
style="width: 90%"
|
||||||
|
alt=""
|
||||||
|
v-if="data?.document_name?.includes('doc')"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="@/assets/docx-icon.svg"
|
||||||
|
style="width: 90%"
|
||||||
|
alt=""
|
||||||
|
v-else-if="data?.document_name?.includes('docx')"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="@/assets/pdf-icon.svg"
|
||||||
|
style="width: 90%"
|
||||||
|
alt=""
|
||||||
|
v-else-if="data?.document_name?.includes('pdf')"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="@/assets/md-icon.svg"
|
||||||
|
style="width: 90%"
|
||||||
|
alt=""
|
||||||
|
v-else-if="data?.document_name?.includes('md')"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="@/assets/xls-icon.svg"
|
||||||
|
style="width: 90%"
|
||||||
|
alt=""
|
||||||
|
v-else-if="data?.document_name?.includes('xls')"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="@/assets/txt-icon.svg"
|
||||||
|
style="width: 90%"
|
||||||
|
alt=""
|
||||||
|
v-else-if="data?.document_name?.includes('txt')"
|
||||||
|
/>
|
||||||
|
<img src="@/assets/doc-icon.svg" style="width: 90%" alt="" v-else />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span class="ellipsis" :title="data?.document_name?.trim()"> {{ data?.document_name.trim() }}</span>
|
<span class="ellipsis" :title="data?.document_name?.trim()">
|
||||||
|
{{ data?.document_name.trim() }}</span
|
||||||
|
>
|
||||||
</el-text>
|
</el-text>
|
||||||
<div class="flex align-center" style="line-height: 32px">
|
<div class="flex align-center" style="line-height: 32px">
|
||||||
<AppAvatar class="mr-8 avatar-blue" shape="square" :size="18">
|
<AppAvatar class="mr-8 avatar-blue" shape="square" :size="18">
|
||||||
@ -45,6 +83,23 @@ const props = defineProps({
|
|||||||
default: 0
|
default: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const iconMap: { [key: string]: string } = {
|
||||||
|
doc: '../../assets/doc-icon.svg',
|
||||||
|
docx: '../../assets/docx-icon.svg',
|
||||||
|
pdf: '../../assets/pdf-icon.svg',
|
||||||
|
md: '../../assets/md-icon.svg',
|
||||||
|
txt: '../../assets/txt-icon.svg',
|
||||||
|
xls: '../../assets/xls-icon.svg',
|
||||||
|
xlsx: '../../assets/xlsx-icon.svg'
|
||||||
|
}
|
||||||
|
function getIconPath(documentName: string) {
|
||||||
|
const extension = documentName.split('.').pop()?.toLowerCase()
|
||||||
|
if (!documentName || !extension) return new URL(`${iconMap['doc']}`, import.meta.url).href
|
||||||
|
if (iconMap && extension && iconMap[extension]) {
|
||||||
|
return new URL(`${iconMap[extension]}`, import.meta.url).href
|
||||||
|
}
|
||||||
|
return new URL(`${iconMap['doc']}`, import.meta.url).href
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.paragraph-source-card-height {
|
.paragraph-source-card-height {
|
||||||
|
|||||||
@ -117,6 +117,8 @@ const currentIndex = ref(null)
|
|||||||
const inputTypeList = ref([
|
const inputTypeList = ref([
|
||||||
{ label: '文本框', value: 'TextInputConstructor' },
|
{ label: '文本框', value: 'TextInputConstructor' },
|
||||||
{ label: '单选框', value: 'SingleSelectConstructor' },
|
{ label: '单选框', value: 'SingleSelectConstructor' },
|
||||||
|
{ label: '多选框', value: 'MultiSelectConstructor' },
|
||||||
|
{ label: '选项卡', value: 'RadioCardConstructor' },
|
||||||
{ label: '日期', value: 'DatePickerConstructor' }
|
{ label: '日期', value: 'DatePickerConstructor' }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,8 @@
|
|||||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'Slider'">滑块</el-tag>
|
<el-tag type="info" class="info-tag" v-if="row.input_type === 'Slider'">滑块</el-tag>
|
||||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SwitchInput'">开关</el-tag>
|
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SwitchInput'">开关</el-tag>
|
||||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SingleSelect'">单选框</el-tag>
|
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SingleSelect'">单选框</el-tag>
|
||||||
|
<el-tag type="info" class="info-tag" v-if="row.input_type === 'MultiSelect'">多选框</el-tag>
|
||||||
|
<el-tag type="info" class="info-tag" v-if="row.input_type === 'RadioCard'">选项卡</el-tag>
|
||||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'DatePicker'">日期</el-tag>
|
<el-tag type="info" class="info-tag" v-if="row.input_type === 'DatePicker'">日期</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|||||||
@ -395,8 +395,8 @@ const switchFileUpload = () => {
|
|||||||
|
|
||||||
if (form_data.value.file_upload_enable) {
|
if (form_data.value.file_upload_enable) {
|
||||||
form_data.value.file_upload_setting = form_data.value.file_upload_setting || default_upload_setting
|
form_data.value.file_upload_setting = form_data.value.file_upload_setting || default_upload_setting
|
||||||
props.nodeModel.graphModel.eventCenter.emit('refreshFileUploadConfig')
|
|
||||||
}
|
}
|
||||||
|
props.nodeModel.graphModel.eventCenter.emit('refreshFileUploadConfig')
|
||||||
}
|
}
|
||||||
const openFileUploadSettingDialog = () => {
|
const openFileUploadSettingDialog = () => {
|
||||||
FileUploadSettingDialogRef.value?.open(form_data.value.file_upload_setting)
|
FileUploadSettingDialogRef.value?.open(form_data.value.file_upload_setting)
|
||||||
|
|||||||
@ -65,12 +65,16 @@ const refreshFileUploadConfig = () => {
|
|||||||
let fields = cloneDeep(props.nodeModel.properties.config.fields)
|
let fields = cloneDeep(props.nodeModel.properties.config.fields)
|
||||||
const form_data = props.nodeModel.graphModel.nodes
|
const form_data = props.nodeModel.graphModel.nodes
|
||||||
.filter((v: any) => v.id === 'base-node')
|
.filter((v: any) => v.id === 'base-node')
|
||||||
|
.filter((v: any) => v.properties.node_data.file_upload_enable)
|
||||||
.map((v: any) => cloneDeep(v.properties.node_data.file_upload_setting))
|
.map((v: any) => cloneDeep(v.properties.node_data.file_upload_setting))
|
||||||
.filter((v: any) => v)
|
.filter((v: any) => v)
|
||||||
|
|
||||||
|
fields = fields.filter((item: any) => item.value !== 'image' && item.value !== 'document')
|
||||||
|
|
||||||
if (form_data.length === 0) {
|
if (form_data.length === 0) {
|
||||||
|
set(props.nodeModel.properties.config, 'fields', fields)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fields = fields.filter((item: any) => item.value !== 'image' && item.value !== 'document')
|
|
||||||
let fileUploadFields = []
|
let fileUploadFields = []
|
||||||
if (form_data[0].document) {
|
if (form_data[0].document) {
|
||||||
fileUploadFields.push({ label: '文档', value: 'document' })
|
fileUploadFields.push({ label: '文档', value: 'document' })
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user