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
11 changes: 11 additions & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,17 @@ System 不读取 workspaceId;飞书在知识库前缀追加 lark/<knowledgeId>
System/飞书扩展及文件 ID 导入、策略协议需对应部署支持,不能因前端接口存在认定已联调。

文档分页使用 ParamsPage/ResponsePage<DocumentItem>,名称/创建者/任务/启用/命中/标签筛选支持组合。
图片与文档共用 Document API 的 `getDocumentPage` 列表接口及响应结构,图片列表额外传入查询参数
`resource_type: 'image'`,不单独封装图片列表接口。
Workspace 与 System 资源管理的 `knowledge/image.ts` 提供图片专属接口,通过上下文 `resourceApi.image` 读取。
`postUploadImagePreview` 通过 POST `image/preview` 按策略生成图片预览,
FormData 重复提交 `file` 并附带 JSON 字符串 `doc_strategy`,返回 ImageDocumentPreview[];
图片预览结果沿用 DocumentSplitResult 的分段结构,额外包含 preview_id;页面直接展示 content,不进行结构转换。
`putBatchCreateImageDocuments` 通过 PUT `image/batch_create` 提交 `{ preview_ids }`,返回 DocumentItem[]。
未生成预览时,`preview_ids` 直接使用上一步上传成功的图片文件 ID;已预览时使用生成结果的预览 ID。
图片支持 JPG、JPEG、PNG、WEBP、BMP;共享范围尚未装配图片上传和导入接口,不回退到 Workspace。
System 资源管理接口前缀为 `/system/resource/knowledge/<knowledgeId>/document/image`;
生成预览 POST `preview`,批量添加 PUT `batch_create`。
is_active 为 false 时保留,“全部”省略;tags 传叶子 ID 数组或 NO_TAG,按 tags[] 序列化。
状态使用 DOCUMENT_TASK_STATE 与 DOCUMENT_TASK_TYPE,不用通用 STATE_TYPES;非任务状态省略 task_type。
status_meta 可空,aggs 为 `{ status, count }[]`,state_time 按任务和状态索引;null 聚合状态按忽略处理。
Expand Down
21 changes: 21 additions & 0 deletions ui/src/api/admin/system/resource-management/knowledge/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { post, put } from '@/api/admin/core/request'
import type { DocumentItem, DocumentStrategy, ImageDocumentPreview } from '@/api/types'

const getPrefix = (knowledgeId: string) => `/system/resource/knowledge/${knowledgeId}/document/image`

/** 按图片处理策略上传图片并生成预览。 */
const postUploadImagePreview = (knowledgeId: string, files: File[], strategy: DocumentStrategy) => {
const data = new FormData()
files.forEach((file) => data.append('file', file))
data.append('doc_strategy', JSON.stringify(strategy))
return post<FormData, ImageDocumentPreview[]>(`${getPrefix(knowledgeId)}/preview`, data)
}

/** 将已上传的图片预览导入知识库。 */
const putBatchCreateImageDocuments = (knowledgeId: string, previewIds: string[]) =>
put<{ preview_ids: string[] }, DocumentItem[]>(`${getPrefix(knowledgeId)}/batch_create`, { preview_ids: previewIds })

export default {
postUploadImagePreview,
putBatchCreateImageDocuments,
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ const postTermbases = (knowledgeId: string, contents: string[]) => post<string[]
const getTermbasePage = (knowledgeId: string, page: ParamsPage, query?: { content?: string }) =>
get<ResponsePage<TermbaseItem>>(`${getPrefix(knowledgeId)}/${page.currentPage}/${page.pageSize}`, query)

/** 编辑单个知识库自定义分词条目。 */
const putTermbase = (knowledgeId: string, termbaseId: string, payload: TermbaseUpdatePayload) =>
put<TermbaseUpdatePayload, void>(`${getPrefix(knowledgeId)}/${termbaseId}`, payload)

/** 删除单个知识库自定义分词条目。 */
const deleteTermbase = (knowledgeId: string, termbaseId: string) => del<undefined, boolean>(`${getPrefix(knowledgeId)}/${termbaseId}`)

Expand All @@ -29,7 +25,6 @@ const postExportTermbases = (knowledgeId: string, termbaseIds: string[]) =>
export default {
postTermbases,
getTermbasePage,
putTermbase,
deleteTermbase,
putBatchDeleteTermbases,
postExportTermbases,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ const postTermbases = (knowledgeId: string, contents: string[]) => post<string[]
const getTermbasePage = (knowledgeId: string, page: ParamsPage, query?: { content?: string }) =>
get<ResponsePage<TermbaseItem>>(`${getPrefix(knowledgeId)}/${page.currentPage}/${page.pageSize}`, query)

/** 编辑单个知识库自定义分词条目。 */
const putTermbase = (knowledgeId: string, termbaseId: string, payload: TermbaseUpdatePayload) =>
put<TermbaseUpdatePayload, void>(`${getPrefix(knowledgeId)}/${termbaseId}`, payload)

/** 删除单个知识库自定义分词条目。 */
const deleteTermbase = (knowledgeId: string, termbaseId: string) => del<undefined, boolean>(`${getPrefix(knowledgeId)}/${termbaseId}`)

Expand All @@ -29,7 +25,6 @@ const postExportTermbases = (knowledgeId: string, termbaseIds: string[]) =>
export default {
postTermbases,
getTermbasePage,
putTermbase,
deleteTermbase,
putBatchDeleteTermbases,
postExportTermbases,
Expand Down
22 changes: 22 additions & 0 deletions ui/src/api/admin/workspace/knowledge/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { post, put } from '@/api/admin/core/request'
import type { DocumentItem, DocumentStrategy, ImageDocumentPreview } from '@/api/types'
import { getWorkspaceId } from '@/utils/resource-context'

const getPrefix = (knowledgeId: string) => `/workspace/${getWorkspaceId()}/knowledge/${knowledgeId}/document/image`

/** 按图片处理策略上传图片并生成预览。 */
const postUploadImagePreview = (knowledgeId: string, files: File[], strategy: DocumentStrategy) => {
const data = new FormData()
files.forEach((file) => data.append('file', file))
data.append('doc_strategy', JSON.stringify(strategy))
return post<FormData, ImageDocumentPreview[]>(`${getPrefix(knowledgeId)}/preview`, data)
}

/** 将已上传的图片预览导入知识库。 */
const putBatchCreateImageDocuments = (knowledgeId: string, previewIds: string[]) =>
put<{ preview_ids: string[] }, DocumentItem[]>(`${getPrefix(knowledgeId)}/batch_create`, { preview_ids: previewIds })

export default {
postUploadImagePreview,
putBatchCreateImageDocuments,
}
5 changes: 0 additions & 5 deletions ui/src/api/admin/workspace/knowledge/termbase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ const postTermbases = (knowledgeId: string, contents: string[]) => post<string[]
const getTermbasePage = (knowledgeId: string, page: ParamsPage, query?: { content?: string }) =>
get<ResponsePage<TermbaseItem>>(`${getPrefix(knowledgeId)}/${page.currentPage}/${page.pageSize}`, query)

/** 编辑单个知识库自定义分词条目。 */
const putTermbase = (knowledgeId: string, termbaseId: string, payload: TermbaseUpdatePayload) =>
put<TermbaseUpdatePayload, void>(`${getPrefix(knowledgeId)}/${termbaseId}`, payload)

/** 删除单个知识库自定义分词条目。 */
const deleteTermbase = (knowledgeId: string, termbaseId: string) => del<undefined, boolean>(`${getPrefix(knowledgeId)}/${termbaseId}`)

Expand All @@ -30,7 +26,6 @@ const postExportTermbases = (knowledgeId: string, termbaseIds: string[]) =>
export default {
postTermbases,
getTermbasePage,
putTermbase,
deleteTermbase,
putBatchDeleteTermbases,
postExportTermbases,
Expand Down
3 changes: 3 additions & 0 deletions ui/src/api/enums/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ export const DOCUMENT_TASK_STATE = { PENDING: '0', STARTED: '1', SUCCESS: '2', F

/** 文档状态字符串中各任务的位置。 */
export const DOCUMENT_TASK_TYPE = { EMBEDDING: 1, GENERATE_PROBLEM: 2, SYNC: 3, TOKENIZE: 4 } as const

/** 独立图片视觉增强的处理状态。 */
export const IMAGE_PROCESS_STATUS = { PENDING: 'pending', SUCCESS: 'success', FAILURE: 'failure', SKIPPED: 'skipped' } as const
14 changes: 13 additions & 1 deletion ui/src/api/types/document.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DOCUMENT_HIT_HANDLING, DOCUMENT_TASK_STATE, DOCUMENT_TASK_TYPE } from '@/api/enums'
import type { DOCUMENT_HIT_HANDLING, DOCUMENT_TASK_STATE, DOCUMENT_TASK_TYPE, IMAGE_PROCESS_STATUS } from '@/api/enums'
import type { DocumentStrategy, KnowledgeGeneratePayload, KnowledgeType } from './knowledge'
export type DocumentHitHandling = (typeof DOCUMENT_HIT_HANDLING)[keyof typeof DOCUMENT_HIT_HANDLING]
export type DocumentTaskState = (typeof DOCUMENT_TASK_STATE)[keyof typeof DOCUMENT_TASK_STATE]
Expand Down Expand Up @@ -102,3 +102,15 @@ export interface LarkDocumentList {
export interface LarkDocumentImportPayload extends Pick<LarkDocumentNode, 'name' | 'token' | 'type'> {
doc_strategy: DocumentStrategy
}

/** 独立图片上传后的处理预览。 */
export interface ImageDocumentPreview extends DocumentSplitResult {
preview_id: string
name: string
url: string
caption: string
ocr_text: string
description: string
process_status: (typeof IMAGE_PROCESS_STATUS)[keyof typeof IMAGE_PROCESS_STATUS]
process_error: string
}
15 changes: 13 additions & 2 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Element Plus 已在 Admin、Chat 入口注册。使用前先检查原生 API,
- `close()` 只发起关闭,表单清理放在 `closed`,避免关闭动画中内容跳变;依赖本次参数的默认值、
编辑快照、动态字段及每次打开必须重新查询的数据仍在 `open()` 初始化。
父级负责校验或请求时,子组件提交数据后由父级成功调用 `close()`,失败保持打开。
- 仅登录表单支持回车提交;其他业务表单使用 `@submit.prevent`,保存、添加由按钮触发。
- 仅登录表单及 `MkQuickCreate` 行内快捷创建支持回车提交;其他业务表单使用 `@submit.prevent`,保存、添加由按钮触发。
自定义 `submit` 事件不等于原生表单提交。

## 全局 UI 组件
Expand Down Expand Up @@ -281,6 +281,7 @@ Element Plus `append`,仅在传入该插槽时将直属表体滚动容器设
由页面管理请求、`loading`、成功提示及列表刷新;成功后通过组件 ref 调用 `close()` 清空并收起,失败保留输入。
`text` 默认为“快速创建”,`placeholder` 默认为“请输入名称”,`maxlength` 默认为 256。
组件不使用 Form 或表单校验;空名称、纯空白及提交期间禁用创建按钮,提交前 trim。
输入框按 Enter 触发同一创建流程,输入法选词时不提交。
Esc 或取消清理草稿;`loading` 默认为 false,为 true 时禁止创建和取消。组件不等待 Promise,不自动收起。

`paginationConfig` 包含 `currentPage`、`pageSize`、`total` 和可选 `pageSizes`;不传则隐藏分页器,
Expand Down Expand Up @@ -541,8 +542,15 @@ Logo 数据统一从 Theme Store 获取。
兼容 UploadUserFile,支持 file_id、errMsg、canRetry;类型从组件 `types.ts` 导入。

- `multiple` 默认 false;单文件展示卡片和更换入口,多文件保留拖拽区、文件夹选择与列表。
- 多文件 `mode` 默认 `file`,使用文件图标和上传进度;`mode="image"` 按选择顺序展示 104×104 的
`el-image` 方块,不使用 Card,不展示汇总、文件名或状态文案。等待时中央显示空进度条,上传时显示实际进度,
成功或失败后隐藏进度条。右上角提供删除,未上传成功时
在原图上叠加灰色遮罩,成功后移除;网络失败和大小超限使用红框,仅网络失败在中央提供重试。
失败且有错误信息时通过 MkTooltip 展示文件名及失败原因,其他情况禁用提示。内部仍维护上传状态与 uploadingCount,本地缩略图 URL 在文件移除
和组件卸载时释放。单文件不使用 mode。
- `accept`、`limit` 控制格式/数量,多文件 `sizeLimit` 默认 100MB;格式、空文件、重复选择移除,
大小超限保留不可重试 fail 卡片,网络失败可重试。超量发出 exceed,DS_Store 格式不支持时静默移除。
大小超限保留不可重试 fail 文件,图片模式保留原图和红框;网络失败可重试。
超量发出 exceed,DS_Store 格式不支持时静默移除。
- `dragText`、`selectText`、`tipText`、`replaceText` 覆盖文案;download 作用域插槽提供下载操作。
- 多文件通过 `inject('upload')` 获取 `DragUploadHandler`,返回 `{ request: Promise<string>, abort }`。
change 后等待调用方回写和校验,再上传 ready 文件;响应成功才标记 success 并提取 file_id,100% 仅表示传输完成。
Expand Down Expand Up @@ -773,6 +781,9 @@ Skills 场景传入 `[TOOL_TYPE.SKILL]`,并通过 `title` 指定标题。两

### SelectModel

`loading` 默认为 `false`,透传给 `el-select` 展示列表加载状态;模型选项加载使用 `loading`,不因此禁用选择器。
`disabled` 继续表示只读或业务禁用。

`teleported` 默认为 `false`,画布节点内保留此默认值,使下拉浮层跟随画布。
画布外的表单、Dialog 和 Drawer 显式传入 `teleported`(即 `true`),将下拉浮层挂载到 body,
避免被滚动容器裁剪。动态表单 Model 字段可通过透传属性配置该值。
Expand Down
5 changes: 3 additions & 2 deletions ui/src/components/business/select-model/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts" generic="Multiple extends boolean = false">
import { computed, ref, useTemplateRef } from 'vue'
import { computed, useTemplateRef } from 'vue'
import type { SelectInstance } from 'element-plus'
import { MODEL_STATUS } from '@/api/enums'
import type { ModelProviderItem, ModelItem } from '@/api/types'
Expand Down Expand Up @@ -29,6 +29,7 @@ const props = withDefaults(
canEditParams?: boolean
canAdd?: boolean
modelParams?: Record<string, unknown>
loading?: boolean
disabled?: boolean
teleported?: boolean
multiple?: Multiple
Expand All @@ -39,6 +40,7 @@ const props = withDefaults(
canEditParams: false,
canAdd: false,
modelParams: () => ({}),
loading: false,
disabled: false,
teleported: false,
},
Expand Down Expand Up @@ -69,7 +71,6 @@ const _options = computed(() => {
return groupBy(props.options, 'provider')
})

const loading = ref(false)
const canEditModelParams = computed(() => props.canEditParams && !props.multiple)

const selectedModelValue = computed<SelectModelValue>({
Expand Down
7 changes: 7 additions & 0 deletions ui/src/components/global/mk-table/MkQuickCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ function handleCreate() {
emit('create', createName.value.trim())
}

function handleKeydown(event: KeyboardEvent) {
if (event.key !== 'Enter' || event.isComposing || event.keyCode === 229) return
event.preventDefault()
handleCreate()
}

defineExpose({ close: resetDraft })
</script>

Expand All @@ -54,6 +60,7 @@ defineExpose({ close: resetDraft })
class="mr-3 w-80!"
:placeholder="props.placeholder"
:maxlength="props.maxlength"
@keydown="handleKeydown"
show-word-limit
clearable
/>
Expand Down
53 changes: 49 additions & 4 deletions ui/src/components/mk-drag-upload/MultipleFileUpload.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, inject, nextTick, onBeforeUnmount, useTemplateRef } from 'vue'
import { computed, inject, nextTick, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'
import type { UploadFile, UploadFiles, UploadInstance, UploadRawFile, UploadUserFile } from 'element-plus'
import type { DragUploadFile, DragUploadHandler } from './types'
import type { DragUploadFile, DragUploadHandler, DragUploadMode } from './types'
import FileApi from '@/api/admin/file'
import uploadImage from '@/assets/mk_icon_upload.svg'
import { getFileIconUrl } from '@/utils/icon'
Expand All @@ -12,6 +12,7 @@ defineOptions({ name: 'MultipleFileUpload' })

const props = withDefaults(
defineProps<{
mode?: DragUploadMode
accept?: string
disabled?: boolean
limit?: number
Expand All @@ -21,6 +22,7 @@ const props = withDefaults(
tipText?: string
}>(),
{
mode: 'file',
accept: '',
disabled: false,
sizeLimit: 100,
Expand All @@ -45,6 +47,21 @@ const folderInputRef = useTemplateRef<HTMLInputElement>('folderInputRef')

const uploadRequests = new Map<number, () => void>()

/* 图片缩略图只保留当前文件的本地 URL,移除或卸载时释放。 */
const imagePreviewUrls = ref(new Map<File, string>())
watch(
() => (props.mode === 'image' ? fileList.value.flatMap((file) => (file.raw ? [file.raw] : [])) : []),
(files) => {
const nextUrls = new Map<File, string>()
for (const file of files) nextUrls.set(file, imagePreviewUrls.value.get(file) ?? URL.createObjectURL(file))
for (const [file, url] of imagePreviewUrls.value) {
if (!nextUrls.has(file)) URL.revokeObjectURL(url)
}
imagePreviewUrls.value = nextUrls
},
{ immediate: true },
)

async function handleFileChange(file: UploadFile, files: UploadFiles) {
emit('change', file, files)
// 等待调用方完成校验与列表回写,仅上传保留的有效文件。
Expand Down Expand Up @@ -146,6 +163,7 @@ function cancelUpload(file: DragUploadFile) {

onBeforeUnmount(() => {
for (const file of fileList.value) cancelUpload(file)
for (const url of imagePreviewUrls.value.values()) URL.revokeObjectURL(url)
})

function handleExceed(files: File[], selectedFiles: UploadUserFile[]) {
Expand Down Expand Up @@ -270,7 +288,7 @@ defineExpose({ clearFiles, handleRemove })
<p>已完成 {{ uploadSummary.completed }}/{{ fileList.length }}</p>

<div class="flex-align-center gap-4">
<MkStatusLabel v-if="uploadSummary.uploading" type="loading" label="上传中" />
<MkStatusLabel v-if="mode === 'file' && uploadSummary.uploading" type="loading" label="上传中" />
<template v-else-if="uploadSummary.error">
<MkStatusLabel type="failure" :label="`失败 ${uploadSummary.error} 个`" />
<!-- 重试所有可重试的失败文件 -->
Expand All @@ -282,7 +300,34 @@ defineExpose({ clearFiles, handleRemove })
<MkStatusLabel v-else-if="uploadSummary.success === fileList.length" type="success" label="全部成功" />
</div>
</div>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div v-if="mode === 'image'" class="mt-4 flex flex-wrap gap-3">
<template v-for="file in fileList" :key="file.uid ?? file.name">
<MkTooltip :content="`${file.name}:${file.errMsg}`" :disabled="file.status !== 'fail' || !file.errMsg" placement="top">
<div class="group relative shrink-0 overflow-hidden rounded-md">
<el-image
:src="(file.raw ? imagePreviewUrls.get(file.raw) : undefined) ?? file.url"
fit="cover"
class="h-26! w-26!"
:class="{ 'border-danger!': file.status === 'fail' }"
></el-image>

<div v-if="file.status !== 'success'" class="pointer-events-none absolute inset-0 bg-N900/30" />
<div v-if="file.status === 'ready' || file.status === 'uploading'" class="absolute-center w-16">
<el-progress :percentage="file.status === 'uploading' ? (file.percentage ?? 0) : 0" :stroke-width="4" :show-text="false" />
</div>
<!-- 重试网络失败的图片 -->
<div v-if="file.status === 'fail' && file.canRetry" class="absolute-center cursor-pointer" @click="handleRetry(file)">
<MkIcon name="icon_refresh_outlined" class="text-white!" />
</div>
<!-- 删除已选图片 -->
<div class="group-hover-visible mk-remove-button" @click="handleRemove(file)">
<MkIcon name="icon_close_bold_outlined" class="text-white!" :size="6" />
</div>
</div>
</MkTooltip>
</template>
</div>
<div v-else class="grid grid-cols-1 gap-3 md:grid-cols-2">
<template v-for="file in sortedFiles" :key="file.uid ?? file.name">
<el-card class="small relative" shadow="never" :class="{ 'border-danger!': file.status === 'fail' }">
<div class="flex-align-center gap-3">
Expand Down
4 changes: 3 additions & 1 deletion ui/src/components/mk-drag-upload/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { computed, useTemplateRef } from 'vue'
import type { UploadFile, UploadFiles, UploadUserFile } from 'element-plus'
import SingleFileUpload from './SingleFileUpload.vue'
import MultipleFileUpload from './MultipleFileUpload.vue'
import type { DragUploadFile } from './types'
import type { DragUploadFile, DragUploadMode } from './types'

defineOptions({ name: 'MkDragUpload' })

defineProps<{
accept?: string
disabled?: boolean
multiple?: boolean
mode?: DragUploadMode
limit?: number
sizeLimit?: number
dragText?: string
Expand Down Expand Up @@ -65,6 +66,7 @@ defineExpose({ clearFiles, handleRemove, uploadingCount })
<template>
<component
:is="multiple ? MultipleFileUpload : SingleFileUpload"
v-bind="multiple ? { mode } : {}"
ref="uploadRef"
v-model="fileList"
:accept="accept"
Expand Down
Loading
Loading