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
6 changes: 6 additions & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,14 @@ Workspace、System 资源管理及 System 共享资源的 `knowledge.ts` 提供
`hit_num` 和 `last_hit_time`。`putParagraph` 向同一路径提交标题和正文,解包后端三元素编辑响应的
首项为最新详情;不提交 `problem_list`,关联关系通过专属接口维护。
`postParagraphProblem` 向 `/<paragraphId>/problem` 提交 `{ content }`,创建或复用问题并关联分段;
`getParagraphProblems` GET 同一路径,返回当前分段的 `ParagraphProblem[]`,用于关联变更后刷新。
`putAssociateParagraphProblem` PUT 到 `/association`,通过查询参数传递 `paragraph_id`、`problem_id`,
不传请求体,用于关联已存在的问题。
`putUnassociateParagraphProblem` PUT 到 `/unassociation`,以查询参数传递 `paragraph_id`、
`problem_id`,不传请求体,仅解除当前分段的关联。
`workspace/knowledge/problem.ts` 的 `getProblemsPage(knowledgeId, page, query?)` 查询知识库问题分页,
路径为 `/workspace/<workspaceId>/knowledge/<knowledgeId>/problem/<currentPage>/<pageSize>`,
支持 `content` 搜索,返回 `ResponsePage<ParagraphProblem>`。分段添加问题沿用 v2 查询第一页 100 条候选。
`putMigrateParagraphs` 向分段前缀下的 `/migrate/knowledge/<targetKnowledgeId>/document/<targetDocumentId>`
提交 PUT `{ id_list }`,将所选分段迁移到目标文档;目标不能为当前文档。
`putGenerateParagraphQuestions` 向分段前缀下的 `/batch_generate_related` 提交 PUT
Expand Down
12 changes: 11 additions & 1 deletion ui/src/api/admin/workspace/knowledge/paragraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ const putParagraph = (knowledgeId: string, documentId: string, paragraphId: stri
const postParagraphProblem = (knowledgeId: string, documentId: string, paragraphId: string, content: string) =>
post<{ content: string }, ParagraphProblem>(`${getPrefix(knowledgeId, documentId)}/${paragraphId}/problem`, { content })

/** 获取当前分段关联的全部问题。 */
const getParagraphProblems = (knowledgeId: string, documentId: string, paragraphId: string) =>
get<ParagraphProblem[]>(`${getPrefix(knowledgeId, documentId)}/${paragraphId}/problem`)

/** 将知识库中的已有问题关联到当前分段。 */
const putAssociateParagraphProblem = (knowledgeId: string, documentId: string, paragraphId: string, problemId: string) =>
put<undefined, boolean>(`${getPrefix(knowledgeId, documentId)}/association`, undefined, { paragraph_id: paragraphId, problem_id: problemId })

/** 解除当前分段的问题关联,保留问题及其他分段的关联。 */
const putUnassociateParagraphProblem = (knowledgeId: string, documentId: string, paragraphId: string, problemId: string) =>
put<undefined, boolean>(`${getPrefix(knowledgeId, documentId)}/unassociation`, undefined, { paragraph_id: paragraphId, problem_id: problemId })
Expand All @@ -46,7 +54,7 @@ const putMigrateParagraphs = (knowledgeId: string, documentId: string, targetKno
id_list: paragraphIds,
})

/** 批量/单个为所选分段提交关联问题生成任务。 */
/** 批量/单个为生成问题。 */
const putGenerateParagraphQuestions = (knowledgeId: string, documentId: string, payload: ParagraphGeneratePayload) =>
put<ParagraphGeneratePayload, void>(`${getPrefix(knowledgeId, documentId)}/batch_generate_related`, payload)

Expand All @@ -60,6 +68,8 @@ export default {
getParagraphDetail,
putParagraph,
postParagraphProblem,
getParagraphProblems,
putAssociateParagraphProblem,
putUnassociateParagraphProblem,
putMigrateParagraphs,
putGenerateParagraphQuestions,
Expand Down
10 changes: 10 additions & 0 deletions ui/src/api/admin/workspace/knowledge/problem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { get } from '../../core/request'
import type { ParamsPage, ResponsePage } from '../../core/types'
import type { ParagraphProblem } from '@/api/types'
import { getWorkspaceId } from '@/utils/resource-context'

/** 分页搜索知识库问题,供分段选择已有问题进行关联。 */
const getProblemsPage = (knowledgeId: string, page: ParamsPage, query?: { content?: string }) =>
get<ResponsePage<ParagraphProblem>>(`/workspace/${getWorkspaceId()}/knowledge/${knowledgeId}/problem/${page.currentPage}/${page.pageSize}`, query)

export default { getProblemsPage }
2 changes: 1 addition & 1 deletion ui/src/styles/element-plus.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1262,7 +1262,7 @@
.el-textarea {
--el-input-placeholder-color: var(--mk-N500);
--el-input-text-color: var(--el-text-color-primary);
&:has(.el-input__inner[readonly]) {
&:has(.el-textarea__inner[readonly]) {
--el-input-focus-border-color: var(--el-input-border-color);
--el-input-hover-border-color: var(--el-input-border-color);
}
Expand Down
45 changes: 29 additions & 16 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ src/views/knowledge/
│ ├── GenerateQuestionsAction.vue # 知识库生成关联问题
│ ├── sync-knowledge-action/ # Web 知识库同步入口与方式选择弹窗
│ ├── KeywordIndexKnowledgeAction.vue # 分词索引入口
│ ├── mcp-config-action/ # MCP 配置入口与专属只读弹窗
│ ├── McpConfigKnowledgeAction.vue # MCP 配置查询、展示与复制
│ ├── ExportKnowledgeAction.vue # Excel、文档 ZIP 与知识库包导出
│ ├── MoveKnowledgeAction.vue
│ └── DeleteKnowledgeAction.vue
Expand Down Expand Up @@ -235,7 +235,8 @@ src/views/knowledge/
`workspaceId`。点击阻止事件冒泡,避免同时进入卡片详情;Workspace 共享知识库仍不展示操作菜单。
“设置”之后依次组合 `KeywordIndexKnowledgeAction` 和 `McpConfigKnowledgeAction`,接收完整
Knowledge API 并复用页面操作 loading。分词索引不增加确认弹窗,调用成功后只提示“操作成功”;
MCP 配置沿用工具的只读文本与悬浮复制交互,专属弹窗按需挂载、关闭后卸载。
`McpConfigKnowledgeAction` 内直接查询 MCP 配置字符串,通过 `MkDialog` 只读展示并提供悬浮复制,
配置为空时禁用复制,关闭动画结束后清理配置;弹窗外壳由 `MkDialog` 按需挂载和卸载。
两者接口暂为本地占位,MCP 返回空文本,分词索引模拟成功,尚不执行服务端操作。
`ExportKnowledgeAction` 接收完整 Knowledge API,暂不增加权限判断;悬停“导出”展开右侧
子菜单,分别导出文档 Excel、文档 ZIP 和可再次导入的知识库包。
Expand Down Expand Up @@ -318,9 +319,7 @@ src/views/tool/
│ ├── InitParamAction.vue
│ ├── MoveToolAction.vue
│ ├── ToolWorkflowAction.vue
│ └── mcp-config-action/
│ ├── McpConfigAction.vue
│ └── McpConfigDialog.vue # MCP 工具配置查看弹窗
│ └── McpConfigAction.vue # MCP 配置查询、展示与复制
└── tool-store/ # 工具商店列表、详情与添加流程
├── component/
│ └── ToolStoreCard.vue # 商店工具卡片及详情、添加入口
Expand Down Expand Up @@ -362,7 +361,9 @@ Workspace 的自定义工具和工作流工具菜单

工具列表页面通过 `ToolCard` 的 `actions` 和 `action-dropdown` 插槽组合
操作;编辑 Action 负责按 `TOOL_TYPE` 打开对应类型表单,启动参数 Action 获取工具详情后打开
`InitParamDialog`,MCP 配置 Action 获取 MCP 工具详情后打开 `McpConfigDialog`。工作流 Action 进入
`InitParamDialog`,`McpConfigAction` 获取 MCP 工具详情后,在 Action 内通过 `MkDialog` 只读展示
`code ?? ''` 并提供悬浮复制,配置为空时禁用复制,关闭动画结束后清理配置。
弹窗外壳由 `MkDialog` 按需挂载和卸载。工作流 Action 进入
独立的工具工作流画布;创建工作流工具后直接进入该画布,其他类型表单创建成功后通过 `refresh`
事件刷新列表。编辑成功后通过 `update` 事件返回接口响应的完整工具数据,由页面局部更新对应卡片;不要
把不同工具类型的字段重新合并到一个通用表单中。所有调用 `postTool` 创建工具的流程在接口成功后
Expand Down Expand Up @@ -894,18 +895,26 @@ Action 仍通过上层 `action-dropdown/index.ts` 导出,弹窗仅在目录内
内容必填且最多 100000 字符。正文使用 `MdEditor`,默认关闭预览,提供 Markdown 工具栏及字数统计;
通过必填的 `knowledgeId` 将图片上传到当前知识库,复用 `FileApi` 和 `FILE_SOURCE_TYPE.KNOWLEDGE`。
提交由 Dialog 编排。
- `ParagraphPreview` 只展示标题、Markdown 正文、召回次数和最近召回时间,通过 `edit` 事件通知 Dialog。
- `ParagraphQuestions` 管理问题草稿、校验、添加和移除,问题必填、去除首尾空格、最多 256 字符且不重复。
无 `paragraphId` 时维护本地草稿并随新增分段提交;已有分段通过专属接口即时添加或解除关联,
不删除问题本身。通过 `v-model:loading`、`v-model:adding` 与 Dialog 协调操作状态。

两个 Dialog 和三个组件均提供默认 `false` 的 `disabled`,表示业务只读:表单不可修改、问题操作隐藏,
详情不展示编辑入口,Dialog 方法同时阻止保存;编辑 Dialog 在只读时强制详情模式,动态切为只读也返回详情。
提交状态继续使用内容/按钮 loading,不作为表单 `disabled`;问题组件的 `blocked` 仅协调加载与保存期间的操作入口。
- `ParagraphPreview` 只展示标题、Markdown 正文、召回次数和最近召回时间;Dialog 管理使用 `mk-paragraph-card` 样式的详情卡片容器及绝对定位的编辑按钮。
- `ParagraphQuestions` 沿用 v2 的添加及移除流程:点击加号显示 `el-select`,使用远程搜索、
`allow-create` 和首项默认选择,选择已有问题或输入新问题后立即添加;失焦取消输入。
问题去除首尾空格、非空、最多 256 字符且不重复,已关联候选不可重复选择。
候选通过 `ProblemApi.getProblemsPage` 查询第一页 100 条;已有分段分别调用创建或关联接口,
添加或解除关联成功后重新查询当前分段问题。列表使用卡片,左侧显示问题文字,右侧提供移除按钮;移除只解除关联,
不删除问题本身。无 `paragraphId` 时维护 `{ content }` 本地草稿并随新增分段提交。
问题输入状态和请求 loading 均在组件内部维护,不向 Dialog 传递 `adding` 或 `loading`,
不阻止分段创建、正文保存、取消或关闭。新增分段只提交已加入列表的问题草稿;
问题请求失败保留输入与现有问题。

两个 Dialog、`ParagraphForm` 和 `ParagraphQuestions` 均不提供或传递 `disabled` Prop;
详情模式通过悬浮编辑入口进入表单,问题区始终提供添加与移除入口。
每个 Dialog 统一使用一个 `loading` 管理自身加载和提交、内容遮罩、按钮状态及重复操作保护;
问题请求使用子组件内部独立的 `loading`,只覆盖右侧问题区。提交状态不作为表单 `disabled`。
两个 Dialog 均接收完整 Paragraph API 和知识库/文档 ID,打开时固定目标,点击时挂载、关闭动画结束后卸载。
详情加载失败提供重试;请求失败保留草稿。正文保存不提交 `problem_list`,取消编辑不撤销即时保存的问题关联。
详情加载失败提供重试;请求失败保留草稿。正文保存不提交 `problem_list`,也不使用保存响应回填问题列表,
避免覆盖并行的问题操作;取消编辑不撤销即时保存的问题关联。
新增成功关闭弹窗并通知页面清空搜索、重新查询服务端排序;编辑成功通知页面刷新当前筛选结果。
分段卡片在非批量模式提供“详情”入口,批量生成问题继续使用 `components/ButtonGenerateQuestions.vue`。
分段卡片在非批量模式点击打开详情弹窗,批量模式点击切换勾选;批量生成问题继续使用 `components/ButtonGenerateQuestions.vue`。
Workspace 共享及 System 分段入口暂未接入,不回退 Workspace API。

System 资源管理与共享资源已注册共用知识库详情路由,
Expand Down Expand Up @@ -1199,6 +1208,10 @@ Layout 保留标题栏固定位置和外层间距,页面负责内部布局。
策略页的“生成预览”按当前策略解析文件,进入 `upload-document/DocumentImportPreview.vue`,
使用 `MkViewLayout` 的侧栏切换文档、正文展示分段,复用其主区滚动和固定标题;
支持本地编辑和删除,编辑须保存或取消后再导入。
分段编辑使用同目录的 `EditParagraphDialog.vue`,复用添加分段的 `ParagraphForm` 与
`ParagraphQuestions`,提供 Markdown 编辑、知识库图片上传和关联问题草稿;不传入已有分段 ID,
保存仅更新当前导入草稿,不创建分段或立即写入问题关联,取消不修改预览。
编辑弹窗按需挂载,关闭动画结束后卸载;关联问题随最终导入的 `problem_list` 提交。
预览仍属于第二步,返回策略页保留设置,重新生成时以新的解析结果替换预览。
预览导入直接批量创建编辑后的分段,保留来源文件及处理策略,不再次解析。
策略页“开始导入”将已上传的 `file_id_list` 与当前 `doc_strategy` 一次提交到 `document/import`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,45 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { cloneDeep } from 'lodash'
import { computed, nextTick, ref } from 'vue'
import type { DocumentImportParagraph, DocumentSplitResult } from '@/api/types'
import { getFileIconUrl } from '@/utils/icon'
import { MsgWarning } from '@/utils/message'
import EditParagraphDialog from './EditParagraphDialog.vue'

defineOptions({ name: 'DocumentImportPreview' })
defineProps<{ knowledgeId: string }>()
const documents = defineModel<DocumentSplitResult[]>({ required: true })
const selectedDocumentIndex = ref(0)
const selectedDocument = computed(() => documents.value[selectedDocumentIndex.value])
const editingParagraphIndex = ref<number>()
const paragraphDraft = ref<DocumentImportParagraph>({ title: '', content: '' })
const editingDocument = ref<DocumentSplitResult>()
const editDialogMounted = ref(false)
const editDialogRef = ref<InstanceType<typeof EditParagraphDialog>>()

/* 预览文档选择与分段草稿编辑 */
function handleSelectDocument(index: number) {
selectedDocumentIndex.value = index
editingParagraphIndex.value = undefined
}

function handleEditParagraph(index: number) {
async function handleEditParagraph(index: number) {
const paragraph = selectedDocument.value?.content[index]
if (!paragraph) return
paragraphDraft.value = cloneDeep(paragraph)
editingDocument.value = selectedDocument.value
editingParagraphIndex.value = index
editDialogMounted.value = true
await nextTick()
editDialogRef.value?.open(paragraph)
}

function handleSaveParagraph() {
if (!paragraphDraft.value.content.trim()) {
MsgWarning('分段内容不能为空')
return
}
function handleSaveParagraph(paragraph: DocumentImportParagraph) {
const index = editingParagraphIndex.value
if (index === undefined || !selectedDocument.value) return
selectedDocument.value.content[index] = cloneDeep(paragraphDraft.value)
if (index === undefined || !editingDocument.value) return
editingDocument.value.content[index] = paragraph
}

function handleEditDialogClosed() {
editDialogMounted.value = false
editingParagraphIndex.value = undefined
editingDocument.value = undefined
}

function validate() {
Expand All @@ -48,7 +54,6 @@ defineExpose({ validate })

function handleDeleteParagraph(index: number) {
selectedDocument.value?.content.splice(index, 1)
editingParagraphIndex.value = undefined
}
</script>

Expand Down Expand Up @@ -84,33 +89,17 @@ function handleDeleteParagraph(index: number) {
<div class="space-y-3">
<template v-for="(paragraph, index) in selectedDocument.content" :key="index">
<el-card shadow="never" class="group" body-class="relative">
<el-form v-if="editingParagraphIndex === index" label-position="top" @submit.prevent>
<el-form-item label="标题">
<el-input v-model="paragraphDraft.title" maxlength="256" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="内容" required>
<el-input v-model="paragraphDraft.content" type="textarea" :rows="6" placeholder="请输入分段内容" />
</el-form-item>
<div class="flex justify-end gap-2">
<!-- 取消分段编辑 -->
<el-button @click="editingParagraphIndex = undefined">取消</el-button>
<!-- 保存分段编辑 -->
<el-button type="primary" @click="handleSaveParagraph">保存</el-button>
</div>
</el-form>
<template v-else>
<div class="group-hover-visible absolute top-3 right-3 flex rounded-lg bg-white shadow-sm">
<!-- 编辑预览分段 -->
<el-button text title="编辑分段" @click="handleEditParagraph(index)"><MkIcon name="icon_edit_outlined" /></el-button>
<!-- 删除预览分段 -->
<el-button class="ml-0!" text title="删除分段" @click="handleDeleteParagraph(index)">
<MkIcon name="icon_delete-trash_outlined" />
</el-button>
</div>
<h4 v-if="paragraph.title" class="mb-3 pr-20">{{ index + 1 }}、{{ paragraph.title }}</h4>
<MdPreview :model-value="paragraph.content" />
<p class="mt-3 text-sm text-N600">{{ paragraph.content.length }} 字符</p>
</template>
<div class="group-hover-visible absolute top-3 right-3 flex rounded-lg bg-white shadow-sm">
<!-- 编辑预览分段 -->
<el-button text title="编辑分段" @click="handleEditParagraph(index)"><MkIcon name="icon_edit_outlined" /></el-button>
<!-- 删除预览分段 -->
<el-button class="ml-0!" text title="删除分段" @click="handleDeleteParagraph(index)">
<MkIcon name="icon_delete-trash_outlined" />
</el-button>
</div>
<h4 v-if="paragraph.title" class="mb-3 pr-20">{{ index + 1 }}、{{ paragraph.title }}</h4>
<MdPreview :model-value="paragraph.content" />
<p class="mt-3 text-sm text-N600">{{ paragraph.content.length }} 字符</p>
</el-card>
</template>
</div>
Expand All @@ -119,4 +108,11 @@ function handleDeleteParagraph(index: number) {
<MkEmpty v-else description="暂无预览文档" />
</template>
</MkViewLayout>
<EditParagraphDialog
v-if="editDialogMounted"
ref="editDialogRef"
:knowledge-id="knowledgeId"
@submit="handleSaveParagraph"
@closed="handleEditDialogClosed"
/>
</template>
Loading
Loading