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
24 changes: 18 additions & 6 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ src/components/
├── mk-search-list/
│ └── index.vue # 搜索框与剩余空间滚动列表,手动导入
├── mk-tags-edit/
│ └── index.vue # 扩展名标签增删与重复检查,手动导入
│ └── index.vue # 标签增删与重复检查,手动导入
├── mk-logo/
│ ├── LogoFull.vue # 带产品名称的完整 Logo,手动导入
│ └── LogoIcon.vue # 不带产品名称的图形 Logo,手动导入
Expand Down Expand Up @@ -854,12 +854,13 @@ import MkFilterableDropdown from '@/components/mk-filterable-dropdown/index.vue'
### MkTagsEdit

手动导入 `@/components/mk-tags-edit/index.vue`,通过必填的 `string[]` 类型 `v-model`
编辑扩展名。可选 `reservedExtensions` 接收已有的大写扩展名数组,默认为空;组件检查保留值与
当前列表的重复项,并提示“该扩展名已存在”。输入失焦或按回车时去除首尾空格、一个前导点并转为
大写,空值不添加。组件内部维护输入框显隐与自动聚焦,根节点阻止点击冒泡;外部间距通过 `class` 设置。
编辑标签。可选 `reservedTags` 接收保留标签数组,默认为空;组件检查保留值与当前列表的
重复项,并提示“该标签已存在”。输入失焦或按回车时去除首尾空格,空值不添加;默认保留大小写
和前导点。业务需要格式化时通过 `normalizeTag(tag)` 传入转换函数,在重复检查前执行。
`addText` 默认为“添加标签”。组件内部维护输入框显隐与自动聚焦,根节点阻止点击冒泡;外部间距通过 `class` 设置。

```vue
<MkTagsEdit v-model="formData.otherExtensions" :reserved-extensions="reservedExtensions" class="mt-2" />
<MkTagsEdit v-model="formData.tags" :reserved-tags="reservedTags" class="mt-2" />
```

### MkCardCheckbox
Expand Down Expand Up @@ -1161,17 +1162,28 @@ const formValue = ref<Dict<DynamicFormValue>>({})
字段配置中的动态校验器和表格行表达式属于受信任的服务端协议,只允许加载可信配置;普通业务
输入不得作为脚本传入。

单选、多选和卡片单选配置器的自定义选项中,标签和选项值的必填校验跟随字段的“是否必填”;每行通过
单选、多选、MultiRow、RadioRow 和卡片单选配置器的自定义选项中,标签和选项值的必填校验跟随字段的“是否必填”;每行通过
`option_list.<index>.label`、`option_list.<index>.value` 参与配置表单校验。编辑中的不完整行
保留在表单内,仅两项都填写且非纯空白的选项进入默认值选择区和 `getData()` 返回的 `option_list`。
初始化、空配置回填和切回自定义赋值时保留一行空白选项。卡片单选仅在存在完整选项时显示默认值卡片区。
引用变量模式继续保留变量路径,不应用自定义选项过滤。
MultiRow 配置器的默认值复用 MultiRow 字段组件,通过 `el-checkbox-group` 和
`el-checkbox-button` 以圆角边框内的横向按钮展示多选项,复用原生禁用、键盘操作和表单校验,
选中项使用主题色文字和浅色背景;支持再次点击取消选择,不使用 Select。仅完整选项参与展示,
无完整选项时显示填写提示;配置器与运行时共用样式,字段输出仍为 `MultiRow`。
RadioRow 配置器沿用同样的选项编辑和完整性过滤,默认值复用 RadioRow 字段组件,
通过 `el-radio-group` 和 `el-radio-button` 展示横向单选,沿用全局按钮式单选样式及原生校验、
禁用和键盘交互;字段输出仍为 `RadioRow`。

动态表单的 Model 字段使用 `SelectModel`,将 `attrs.provider_list` 中的模型快照映射为
扁平 `ModelItem[]`,由选择器统一分组和展示供应商。旧快照未保存状态时保留可选行为,已保存的
状态按 `SelectModel` 的可用性规则处理。切换模型一次性回写 `model_id` 和深拷贝后的
`model_params_setting`;清空时回写空 ID 和空参数,避免修改原配置中的参数对象。

Knowledge 配置器复用 `SelectKnowledgeDialog` 选择可选知识库,沿用文件夹、共享资源查询及
相同 Embedding 模型约束。打开时传入已选快照,确认后回写可选知识库并清理已取消 ID 对应的
默认值;取消不修改配置。

Model 配置器的默认模型也使用 `SelectModel`,仅展示已选的可选模型,不开启参数设置入口。
选择时将模型 ID 转换为包含已配置参数的 `default_value` 对象,清空时重置为空对象。

Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/business/select-knowledge-dialog/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ defineExpose({ open })
</script>

<template>
<MkDialog v-model="visible" align-center class="mk-aside-content-dialog" title="关联知识库" width="1200" @closed="resetData">
<MkDialog v-model="visible" align-center class="mk-aside-content-dialog" width="1200" @closed="resetData">
<template #header="{ titleId }">
<div class="flex-between pr-8">
<div class="flex items-center gap-2">
<h4 :id="titleId">关联知识库</h4>
<h4 :id="titleId">知识库</h4>
<span class="text-N600!">所选知识库必须使用相同的 Embedding 模型</span>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,178 +1,123 @@
<script setup lang="ts">
import type { DynamicFormValue } from '../../type'
import { computed, reactive, ref } from 'vue'
import { CaretBottom } from '@element-plus/icons-vue'
import { computed, nextTick, useTemplateRef } from 'vue'
import type { FormItemInstance } from 'element-plus'
import type { KnowledgeItem } from '@/api/types'
import SelectKnowledgeDialog from '@/components/business/select-knowledge-dialog/index.vue'
import Knowledge from '../../items/knowledge/Knowledge.vue'
import MkCardCheckbox from '@/components/mk-card-checkbox/index.vue'
import type { FormField } from '../../type'
import KnowledgeApi from '@/api/admin/workspace/knowledge/knowledge'
import type { KnowledgeItem } from '@/api/types'
import { FOLDER_ENTRY_ID } from '@/constants'

const props = defineProps<{ modelValue: DynamicFormValue }>()
type KnowledgeOption = Pick<KnowledgeItem, 'id'> & Partial<Pick<KnowledgeItem, 'name' | 'type' | 'embedding_model_id'>>
interface KnowledgeConstructorValue {
field?: string
required?: boolean
knowledge_list?: KnowledgeOption[]
default_value?: string[]
}

const emit = defineEmits(['update:modelValue'])
const props = defineProps<{ modelValue: KnowledgeConstructorValue }>()
const emit = defineEmits<{ 'update:modelValue': [value: KnowledgeConstructorValue] }>()

const collapseData = reactive({ optional_knowledge: true })
// 配置回填与序列化:保留知识库快照及默认 ID 的字段协议。
const formValue = computed({
set: (item: DynamicFormValue) => {
emit('update:modelValue', item)
},
get: () => {
return props.modelValue || { knowledge_list: [], default_value: [] }
},
get: () => props.modelValue || { knowledge_list: [], default_value: [] },
set: (value: KnowledgeConstructorValue) => emit('update:modelValue', value),
})

const formField = computed<FormField>(() => {
return { attrs: { knowledge_list: formValue.value.knowledge_list } } as DynamicFormValue
})

const getData = () => {
const knowledgeItemList = (formValue.value.knowledge_list || []).map((k: DynamicFormValue) => {
return { id: k.id, name: k.name, type: k.type, embedding_model_id: k.embedding_model_id }
})

return { input_type: 'Knowledge', default_value: formValue.value.default_value || [], attrs: { knowledge_list: knowledgeItemList } }
const availableKnowledge = computed(() => formValue.value.knowledge_list || [])
const formField = computed<FormField>(() => ({
field: formValue.value.field || '',
input_type: 'Knowledge',
attrs: { knowledge_list: availableKnowledge.value },
}))

function getData(): Partial<FormField> {
return {
input_type: 'Knowledge',
default_value: formValue.value.default_value || [],
attrs: {
knowledge_list: availableKnowledge.value.map(({ id, name, type, embedding_model_id }) => ({ id, name, type, embedding_model_id })),
},
}
}

const render = (formData: DynamicFormValue) => {
function render(formData: FormField) {
formValue.value.default_value = formData.default_value || []
formValue.value.knowledge_list = formData.attrs?.knowledge_list || []
}

defineExpose({ getData, render })

// ── 添加知识库 ─────────────────────────────────────────
const knowledgeDialogVisible = ref(false)
const knowledgeLoading = ref(false)
const knowledgeSearch = ref('')
const knowledgeList = ref<KnowledgeItem[]>([])
const selectedKnowledgeIds = ref<Array<string>>([])

function changeKnowledgeSelection(knowledgeId: string, checked: boolean) {
selectedKnowledgeIds.value = checked ? [...selectedKnowledgeIds.value, knowledgeId] : selectedKnowledgeIds.value.filter((id) => id !== knowledgeId)
// 自定义选择列表不会自动触发表单校验,用户修改后等待字段更新再校验。
const knowledgeFormItemRef = useTemplateRef<FormItemInstance>('knowledgeFormItemRef')
function validateKnowledgeSelection() {
void nextTick(() => {
void knowledgeFormItemRef.value?.validate('change').catch(() => {
// 校验失败由表单项展示,避免产生未处理的 Promise 拒绝。
})
})
}

const filteredKnowledgeList = computed(() => {
const keyword = knowledgeSearch.value.trim().toLocaleLowerCase()
if (!keyword) return knowledgeList.value
return knowledgeList.value.filter((item) => item.name.toLocaleLowerCase().includes(keyword))
})
// 添加知识库:由公共弹窗管理查询和临时选择,确认后同步可选项及默认值。
const knowledgeDialogRef = useTemplateRef<InstanceType<typeof SelectKnowledgeDialog>>('knowledgeDialogRef')

const openAddKnowledgeDialog = async () => {
knowledgeDialogVisible.value = true
selectedKnowledgeIds.value = (formValue.value.knowledge_list || []).map((k: DynamicFormValue) => k.id)
knowledgeLoading.value = true
try {
const res = await KnowledgeApi.getKnowledgePage({ currentPage: 1, pageSize: 100 }, { folder_id: FOLDER_ENTRY_ID.ALL })
knowledgeList.value = res.records || []
} finally {
knowledgeLoading.value = false
}
function openAddKnowledgeDialog() {
knowledgeDialogRef.value?.open(availableKnowledge.value)
}

const handleKnowledgeSelect = () => {
const selectIds = new Set(selectedKnowledgeIds.value)
const currentList = (formValue.value.knowledge_list || []).filter((k: DynamicFormValue) => selectIds.has(k.id))
knowledgeList.value.forEach((item) => {
if (selectIds.has(item.id) && !currentList.some((k: DynamicFormValue) => k.id === item.id)) {
currentList.push({ id: item.id, name: item.name, type: item.type, embedding_model_id: item.embedding_model_id })
}
})
formValue.value.knowledge_list = currentList
if (formValue.value.default_value) {
formValue.value.default_value = formValue.value.default_value.filter((id: string) => selectIds.has(id))
}
knowledgeDialogVisible.value = false
function handleKnowledgeSelect(knowledge: (Partial<KnowledgeItem> & { id: string })[]) {
const selectedIds = new Set(knowledge.map(({ id }) => id))
formValue.value.knowledge_list = knowledge.map(({ id, name, type, embedding_model_id }) => ({ id, name, type, embedding_model_id }))
formValue.value.default_value = (formValue.value.default_value || []).filter((id) => selectedIds.has(id))
validateKnowledgeSelection()
}

function removeKnowledge(id: string) {
formValue.value.knowledge_list = formValue.value.knowledge_list.filter((k: DynamicFormValue) => k.id !== id)
if (formValue.value.default_value) {
formValue.value.default_value = formValue.value.default_value.filter((k_id: string) => k_id !== id)
}
// 移除可选知识库时同步清理默认值,保留其他关联。
function removeKnowledge(knowledgeId: string) {
formValue.value.knowledge_list = availableKnowledge.value.filter(({ id }) => id !== knowledgeId)
formValue.value.default_value = (formValue.value.default_value || []).filter((id) => id !== knowledgeId)
validateKnowledgeSelection()
}
</script>

<template>
<el-form-item prop="knowledge_list" :rules="[{ message: '请选择可选知识库', type: 'array', min: 1 }]">
<template #label>
<div class="flex-between mb-2 cursor" @click="collapseData.optional_knowledge = !collapseData.optional_knowledge">
<div class="flex align-center">
<MkIcon
:icon="CaretBottom"
:size="14"
class="mr-1 text-N600! transition-transform"
:class="{ '-rotate-90': !collapseData.optional_knowledge }"
/>
<span class="lighter"
>可选知识库
<span class="text-danger">*</span>
</span>
<span class="ml-1" v-if="formValue.knowledge_list?.length">({{ formValue.knowledge_list.length }})</span>
</div>
<el-button type="primary" link @click.stop="openAddKnowledgeDialog">
<MkIcon name="icon_add_outlined" class="mr-1" />
添加
</el-button>
</div>
</template>
<div class="w-full" v-if="collapseData.optional_knowledge">
<div v-if="formValue.knowledge_list?.length > 0">
<template v-for="(item, index) in formValue.knowledge_list" :key="index">
<div class="flex-between border border-r-6 white-bg mb-8" style="padding: 3px 12px">
<div class="flex align-center" style="width: 80%">
<KnowledgeIcon :type="item.type" class="mr-8" :size="20" style="--el-avatar-border-radius: 6px" />
<el-form-item ref="knowledgeFormItemRef" prop="knowledge_list" :rules="[{ required: true, message: '请选择可选知识库', type: 'array', min: 1 }]">
<MkCollapse class="w-full" trigger-class="pt-0 pb-2">
<template #label>
<div class="flex-between w-full">
<div class="flex items-center">
<span class="mk-required">可选知识库</span>
<span v-if="availableKnowledge.length">({{ availableKnowledge.length }})</span>
</div>

<span class="ellipsis cursor" :title="item.name"> {{ item.name }}</span>
<el-button type="primary" text @click.stop="openAddKnowledgeDialog">
<MkIcon name="icon_add_outlined" />
</el-button>
</div>
</template>
<div v-if="availableKnowledge.length" class="space-y-2">
<template v-for="knowledge in availableKnowledge" :key="knowledge.id">
<el-card class="small" shadow="never">
<div class="flex-between">
<span class="flex min-w-0 items-center gap-2">
<KnowledgeIcon :type="knowledge.type" :size="20" class="shrink-0" />
<span class="min-w-0 flex-1 truncate" :title="knowledge.name">{{ knowledge.name }}</span>
</span>
<el-button text @click="removeKnowledge(knowledge.id)"><MkIcon name="icon_close_outlined" /></el-button>
</div>
<el-button text @click="removeKnowledge(item.id)">
<el-icon><Close /></el-icon>
</el-button>
</div>
</el-card>
</template>
</div>
<el-text type="info" v-else> 请选择可选知识库 </el-text>
</div>
<el-text v-else type="info">请选择可选知识库</el-text>
</MkCollapse>
</el-form-item>
<el-form-item
v-if="availableKnowledge.length"
label="默认知识库"
prop="default_value"
:required="formValue.required"
:rules="formValue.required ? [{ message: '请选择知识库', type: 'array', min: 1 }] : []"
v-if="formValue.knowledge_list && formValue.knowledge_list.length > 0"
>
<div class="w-full" v-if="formValue.knowledge_list?.length > 0">
<Knowledge v-model="formValue.default_value" :form-field="formField" />
</div>
<Knowledge v-model="formValue.default_value" :form-field="formField" />
</el-form-item>

<MkDialog v-model="knowledgeDialogVisible" title="添加知识库" width="600px" append-to-body>
<el-input v-model="knowledgeSearch" placeholder="按名称搜索" clearable class="mb-3">
<template #prefix>
<MkIcon name="icon_magnify_outlined" />
</template>
</el-input>
<div v-loading="knowledgeLoading" class="max-h-[360px] overflow-auto">
<div class="space-y-2">
<MkCardCheckbox
v-for="knowledge in filteredKnowledgeList"
:key="knowledge.id"
:model-value="selectedKnowledgeIds.includes(knowledge.id)"
:label="knowledge.name"
@update:model-value="changeKnowledgeSelection(knowledge.id, $event)"
>
<div class="flex min-w-0 items-center gap-2">
<KnowledgeIcon :type="knowledge.type" :size="20" class="shrink-0" style="--el-avatar-border-radius: 6px" />
<span class="min-w-0 flex-1 truncate" :title="knowledge.name">{{ knowledge.name }}</span>
</div>
</MkCardCheckbox>
</div>
<el-empty v-if="!knowledgeLoading && filteredKnowledgeList.length === 0" description="暂无知识库" :image-size="60" />
</div>
<template #footer>
<el-button @click="knowledgeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleKnowledgeSelect">确定</el-button>
</template>
</MkDialog>
<SelectKnowledgeDialog ref="knowledgeDialogRef" @submit="handleKnowledgeSelect" />
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,13 @@ defineExpose({ getData, render })
</script>

<template>
<el-form-item label="模型类型" required prop="model_type" :rules="[{ required: true, message: '请选择模型类型' }]">
<el-form-item label="模型类型" prop="model_type" :rules="[{ required: true, message: '请选择模型类型' }]">
<el-select v-model="formValue.model_type" placeholder="请选择模型类型" @change="handleModelTypeChange">
<el-option v-for="(label, value) in MODEL_TYPE_LABELS" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>

<el-form-item label="可选模型" required prop="provider_list" :rules="[{ required: true, message: '请选择模型', type: 'array' }]">
<!-- // TODO -->
<el-form-item label="可选模型" prop="provider_list" :rules="[{ required: true, message: '请选择模型', type: 'array' }]">
<SelectModel
multiple
v-model="selectedIds"
Expand All @@ -170,7 +169,6 @@ defineExpose({ getData, render })
:rules="formValue.required ? [{ required: true, message: '请选择模型' }] : []"
v-if="formValue.provider_list && formValue.provider_list.length > 0"
>
<!-- // TODO -->
<SelectModel v-model="defaultModelId" placeholder="请选择模型" :options="selectedModelsOptions" :provider-options="providerOptions" />
</el-form-item>
</template>
Loading
Loading