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
3 changes: 3 additions & 0 deletions ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ Keep these behaviors:

## Frontend Implementation Notes

- Newly implemented frontend features must not add permission checks or permission-based visibility
or disabled states until the user explicitly requests them. Preserve existing permission logic
outside the requested scope; page-specific guidance is maintained in `src/views/VIEW_README.md`.
- Prefer existing project structure and naming.
- Do not modify shared components under `src/components/` without an explicit user instruction to
change those components. Follow the modification boundary in `src/components/COMPONENT_README.md`.
Expand Down
12 changes: 12 additions & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ Action、Drawer 或 Dialog。复用方直接使用 `typeof XxxApi` 约束完整

### 知识库维护

`exportKnowledgeExcel`、`exportKnowledgeZip`、`exportKnowledge` 分别通过 GET 请求知识库
`/<knowledgeId>/export`、`export_zip`、`export_knowledge`,复用 `getExportFile` 下载。
三种结果分别为文档 Excel、包含图片的文档 ZIP 和可导入创建的知识库 ZIP;优先使用服务端文件名。

`postKnowledgeImport(file, folderId)` 将文件与 `folder_id` 组装为 FormData,POST 到
`/workspace/<workspaceId>/knowledge/import_knowledge`,响应为 `{ knowledge_id, type }`。
后端校验知识库导出包并创建资源;导入成功后的用户权限和列表刷新由调用页面负责。

`workspace/knowledge/knowledge.ts` 与 `workspace/shared.ts` 的 `getAllKnowledge(query)`
分别查询工作空间及共享知识库的非分页列表,返回 `KnowledgeItem[]`,用于关联知识库选择等
需要全量选项的场景。原有 `getKnowledgePage` 继续用于分页列表。
Expand All @@ -121,6 +129,10 @@ Action、Drawer 或 Dialog。复用方直接使用 `typeof XxxApi` 约束完整
组装为 `{ id_list, folder_id }`,`putBatchDeleteKnowledge` 将 ID 数组组装为 `{ id_list }`,
分别使用 PUT 请求 `batch_move` 和 `batch_delete`。页面和 Action 负责类型判断及 loading。

`putReEmbeddingKnowledge` 使用 PUT 请求 `/<knowledgeId>/embedding` 重新向量化。
设置页更换向量模型时先确认、保存,再调用该接口;Web、飞书配置通过 `meta` 提交,保留未编辑的
已有配置,文件数量与大小限制仍作为知识库顶层字段提交。

知识库创建通过 `postKnowledge`、`postWebKnowledge` 分别提交到 `/base`、`/web`;
`postLarkKnowledge` 沿用飞书扩展接口 `/lark/save`,当前开源后端未包含该实现。
基础创建字段由 `KnowledgeCreatePayload` 统一维护,Web、飞书请求扩展对应类型;工作流创建
Expand Down
9 changes: 0 additions & 9 deletions ui/src/api/admin/core/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,6 @@ async function getResponseErrorMessage(error: unknown) {
return responseData?.message
}

async function getFetchErrorMessage(response: Response) {
const responseText = await response.text()
try {
const responseData = JSON.parse(responseText) as Partial<ApiResponse<unknown>>
return responseData.message || responseText
} catch {
return responseText
}
}

async function downloadExportResponse(response: AxiosResponse<Blob>, fileName: string, mimeType = 'application/octet-stream') {
if (response.data.type.includes('application/json')) {
Expand Down
37 changes: 32 additions & 5 deletions ui/src/api/admin/workspace/knowledge/knowledge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { del, get, post, put } from '../../core/request'
import { del, get, getExportFile, post, put } from '../../core/request'
import type { ParamsPage, ResponsePage } from '../../core/types'
import type { Dict, KnowledgeDetail, KnowledgeItem, KnowledgeCreatePayload, WebKnowledgeCreatePayload, LarkKnowledgeCreatePayload } from '@/api/types'
import { getWorkspaceId } from '@/utils/resource-context'
Expand Down Expand Up @@ -52,6 +52,11 @@ const putLarkKnowledge = (knowledgeId: string, payload: Partial<KnowledgeItem>)
return put<Partial<KnowledgeItem>, KnowledgeItem>(`${getPrefix()}/lark/${knowledgeId}`, payload)
}

/** 对知识库中的文档重新向量化。 */
const putReEmbeddingKnowledge = (knowledgeId: string) => {
return put<undefined, boolean>(`${getPrefix()}/${knowledgeId}/embedding`)
}

/** 批量删除工作空间知识库。 */
const putBatchDeleteKnowledge = (knowledgeIds: string[]) => {
return put<{ id_list: string[] }, boolean>(`${getPrefix()}/batch_delete`, { id_list: knowledgeIds })
Expand All @@ -62,12 +67,33 @@ const putBatchMoveKnowledge = (knowledgeIds: string[], folderId: string) => {
return put<{ id_list: string[]; folder_id: string }, boolean>(`${getPrefix()}/batch_move`, { id_list: knowledgeIds, folder_id: folderId })
}

/** 导入知识库文件。 */
const importKnowledgeBundle = (payload: FormData) => {
return post<FormData, unknown>(`${getPrefix()}/import_knowledge`, payload)
/** 将知识库文档导出为 Excel。 */
const exportKnowledgeExcel = (knowledgeId: string, knowledgeName: string) => {
return getExportFile(`${knowledgeName}.xlsx`, `${getPrefix()}/${knowledgeId}/export`)
}

/** 将知识库文档及图片导出为 ZIP。 */
const exportKnowledgeZip = (knowledgeId: string, knowledgeName: string) => {
return getExportFile(`${knowledgeName}.zip`, `${getPrefix()}/${knowledgeId}/export_zip`)
}

/** 导出可用于导入创建的知识库压缩包。 */
const exportKnowledge = (knowledgeId: string, knowledgeName: string) => {
return getExportFile(`${knowledgeName}.zip`, `${getPrefix()}/${knowledgeId}/export_knowledge`)
}

/** 导入知识库文件并在指定文件夹创建知识库。 */
const postKnowledgeImport = (file: File, folderId: string) => {
const payload = new FormData()
payload.append('file', file)
payload.append('folder_id', folderId)
return post<FormData, { knowledge_id: string; type: KnowledgeItem['type'] }>(`${getPrefix()}/import_knowledge`, payload)
}

export default {
exportKnowledgeExcel,
exportKnowledgeZip,
exportKnowledge,
postKnowledge,
postWebKnowledge,
postLarkKnowledge,
Expand All @@ -77,7 +103,8 @@ export default {
getKnowledgePage,
putKnowledge,
putLarkKnowledge,
putReEmbeddingKnowledge,
putBatchDeleteKnowledge,
putBatchMoveKnowledge,
importKnowledgeBundle,
postKnowledgeImport,
}
2 changes: 1 addition & 1 deletion ui/src/api/enums/knowledge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** 后端知识库类型。 */
export const KNOWLEDGE_TYPE = { BASE: 0, WEB: 1, LARK: 2, YUQUE: 3, WORKFLOW: 4 } as const
export const KNOWLEDGE_TYPE = { BASE: 0, WEB: 1, LARK: 2, WORKFLOW: 4 } as const

/** 知识库分段检索模式。 */
export const KNOWLEDGE_SEARCH_MODE = { EMBEDDING: 'embedding', KEYWORDS: 'keywords', BLEND: 'blend' } as const
11 changes: 9 additions & 2 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,15 @@ Admin、Chat 入口调用 `configureMarkdownEditor()` 配置本地高亮、KaTeX

### MkCollapse

`trigger` 默认 `header`,点击标题区域切换;设置为 `indicator` 时,仅点击箭头按钮切换,
标题文字和空白区域不触发。箭头按钮支持键盘操作。

`v-model:expanded` 可控制展开状态;未绑定时沿用 `defaultExpanded` 的内部状态。

标题折叠区,`title` 或 `label` 插槽提供标题,默认插槽提供内容。
`defaultExpanded` 默认 `true`,仅设置初始状态;点击标题内部切换,当前不提供展开状态事件。
`defaultExpanded` 默认 `true`,仅设置初始状态;展开状态通过 `v-model:expanded` 同步。
`indicatorPosition` 默认 `before`,可设为 `after`;`triggerClass`、`triggerStyle` 调整触发区。
内容使用 `v-if`,收起会卸载内部组件。
内容使用 `v-show`,收起时保留内部组件及表单状态。

### MkEmpty

Expand Down Expand Up @@ -523,6 +526,10 @@ Skills 场景传入 `[TOOL_TYPE.SKILL]`,并通过 `title` 指定标题。两

### SelectModel

`teleported` 默认为 `false`,画布节点内保留此默认值,使下拉浮层跟随画布。
画布外的表单、Dialog 和 Drawer 显式传入 `teleported`(即 `true`),将下拉浮层挂载到 body,
避免被滚动容器裁剪。动态表单 Model 字段可通过透传属性配置该值。

按供应商分组展示模型,单选 `v-model` 为字符串,`multiple` 模式为字符串数组,变化时触发 `change`。`options` 为
`ModelItem[]`,`providerOptions` 为 `ModelProviderItem[]`,模型列表和供应商列表均由使用方查询。
需要接口加载的模型选项统一使用 `getModelListWithShared`,共享标签读取 `source: 'shared'`。
Expand Down
4 changes: 3 additions & 1 deletion ui/src/components/business/select-model/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const props = withDefaults(
canAdd?: boolean
modelParams?: Record<string, unknown>
disabled?: boolean
teleported?: boolean
multiple?: Multiple
}>(),
{
Expand All @@ -38,6 +39,7 @@ const props = withDefaults(
canAdd: false,
modelParams: () => ({}),
disabled: false,
teleported: false,
},
)

Expand Down Expand Up @@ -129,7 +131,7 @@ function openModelParams() {
filterable
:loading="loading"
:multiple="multiple"
:teleported="false"
:teleported="teleported"
>
<el-option-group v-for="group in modelOptionGroups" :key="group.provider" :label="group.name">
<el-option
Expand Down
40 changes: 22 additions & 18 deletions ui/src/components/global/mk-collapse/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ const props = withDefaults(
defaultExpanded?: boolean
indicatorPosition?: 'after' | 'before'
title?: string
trigger?: 'header' | 'indicator'
triggerClass?: HTMLAttributes['class']
triggerStyle?: HTMLAttributes['style']
}>(),
{ defaultExpanded: true, indicatorPosition: 'before' },
{ defaultExpanded: true, indicatorPosition: 'before', trigger: 'header' },
)

defineSlots<{ default?: () => unknown; label?: () => unknown }>()
Expand All @@ -30,26 +31,29 @@ const expanded = computed({

<template>
<section>
<div class="py-2" :class="triggerClass" :style="triggerStyle" @click="expanded = !expanded">
<div class="flex w-full cursor-pointer items-center gap-2 text-left">
<MkIcon
v-if="indicatorPosition === 'before'"
:icon="CaretBottom"
:size="14"
class="transition-transform text-N600!"
:class="{ '-rotate-90': !expanded }"
/>
<div class="py-2" :class="triggerClass" :style="triggerStyle" @click="trigger === 'header' && (expanded = !expanded)">
<div class="flex w-full items-center gap-2 text-left" :class="{ 'cursor-pointer': trigger === 'header' }">
<!-- 展开或收起内容 -->
<button
type="button"
class="flex-center shrink-0 cursor-pointer border-0 bg-transparent p-0"
:class="{ 'order-last': indicatorPosition === 'after' }"
:title="expanded ? '收起' : '展开'"
:aria-expanded="expanded"
@click.stop="expanded = !expanded"
>
<MkIcon
v-if="indicatorPosition === 'before'"
:icon="CaretBottom"
:size="14"
class="transition-transform text-N600!"
:class="{ '-rotate-90': !expanded }"
/>
<MkIcon v-else name="icon_down_outlined" :size="16" class="transition-transform text-N600!" :class="{ '-rotate-180': expanded }" />
</button>
<slot name="label">
<span>{{ title }}</span>
</slot>

<MkIcon
v-if="indicatorPosition === 'after'"
name="icon_down_outlined"
:size="16"
class="transition-transform text-N600!"
:class="{ '-rotate-180': expanded }"
/>
</div>
</div>
<el-collapse-transition>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ defineExpose({ getData, render })

<el-form-item label="可选模型" prop="provider_list" :rules="[{ required: true, message: '请选择模型', type: 'array' }]">
<SelectModel
teleported
multiple
v-model="selectedIds"
placeholder="请选择模型"
Expand All @@ -170,6 +171,6 @@ defineExpose({ getData, render })
:rules="formValue.required ? [{ required: true, message: '请选择模型' }] : []"
v-if="formValue.provider_list && formValue.provider_list.length > 0"
>
<SelectModel v-model="defaultModelId" placeholder="请选择模型" :options="selectedModelsOptions" :provider-options="providerOptions" />
<SelectModel v-model="defaultModelId" placeholder="请选择模型" :options="selectedModelsOptions" :provider-options="providerOptions" teleported />
</el-form-item>
</template>
2 changes: 1 addition & 1 deletion ui/src/constants/CONSTANT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ API 枚举值。例如角色类型和登录方式以 `src/api/enums` 为唯一
只维护它们的展示文案。

知识库类型的接口值保持为 `KNOWLEDGE_TYPE` 中的数字。`constants/knowledge.ts` 提供
`KNOWLEDGE_TYPE_MAP`,将数字转换为 `BASE`、`WEB`、`LARK`、`YUQUE`、`WORKFLOW` 字符串,
`KNOWLEDGE_TYPE_MAP`,将数字转换为 `BASE`、`WEB`、`LARK`、`WORKFLOW` 字符串,
供前端判断;`KNOWLEDGE_TYPE_LABELS` 按这些字符串提供展示文案。提交接口时仍使用数字枚举值。
字符串标识统一由 `KNOWLEDGE_TYPE_KEY` 维护,映射、文案和业务判断均引用该常量,避免重复写裸字符串。

Expand Down
2 changes: 0 additions & 2 deletions ui/src/constants/knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export const KNOWLEDGE_TYPE_MAP = {
[KNOWLEDGE_TYPE.BASE]: KNOWLEDGE_TYPE_KEY.BASE,
[KNOWLEDGE_TYPE.WEB]: KNOWLEDGE_TYPE_KEY.WEB,
[KNOWLEDGE_TYPE.LARK]: KNOWLEDGE_TYPE_KEY.LARK,
[KNOWLEDGE_TYPE.YUQUE]: KNOWLEDGE_TYPE_KEY.YUQUE,
[KNOWLEDGE_TYPE.WORKFLOW]: KNOWLEDGE_TYPE_KEY.WORKFLOW,
} as const satisfies Record<KnowledgeType, keyof typeof KNOWLEDGE_TYPE>

Expand All @@ -24,6 +23,5 @@ export const KNOWLEDGE_TYPE_LABELS = {
[KNOWLEDGE_TYPE_KEY.BASE]: '通用类型',
[KNOWLEDGE_TYPE_KEY.WEB]: 'web站点类型',
[KNOWLEDGE_TYPE_KEY.LARK]: '飞书类型',
[KNOWLEDGE_TYPE_KEY.YUQUE]: '语雀类型',
[KNOWLEDGE_TYPE_KEY.WORKFLOW]: '工作流类型',
} as const satisfies Record<(typeof KNOWLEDGE_TYPE_MAP)[KnowledgeType], string>
2 changes: 1 addition & 1 deletion ui/src/layout/ResourceDetailLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function navigateToDetailMenu(detailMenuItem: LayoutMenuItem) {
<MkViewLayout :loading="props.loading" title="">
<template #aside="{ Header }">
<component :is="Header">
<div class="flex min-w-0 items-center gap-1">
<div class="flex min-w-0 items-center gap-2">
<el-button class="-ml-1" text @click="navigateBack">
<MkIcon name="icon_arrow-left_outlined" :size="20" />
</el-button>
Expand Down
4 changes: 3 additions & 1 deletion ui/src/router/ROUTE_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Workspace 页面:
/admin/workspace/:workspaceId/knowledge/:knowledgeId/workflow
/admin/workspace/:workspaceId/knowledge/:knowledgeId
/admin/workspace/:workspaceId/knowledge/:knowledgeId/document
/admin/workspace/:workspaceId/knowledge/:knowledgeId/setting
/admin/workspace/:workspaceId/knowledge/:knowledgeId/workflow-entry
/admin/workspace/:workspaceId/knowledge/:knowledgeId/document/:documentId
```
Expand Down Expand Up @@ -315,6 +316,7 @@ System 共享资源页面:
- 每次新增、删除、移动或修改路由时,必须同步更新本文件。

工作空间知识库卡片进入 `workspace-knowledge-detail`,默认重定向到“文档”子路由。
详情容器复用 `ResourceDetailLayout`,二级目录包含“文档”和“工作流”;`workflow-entry`
详情容器复用 `ResourceDetailLayout`,二级目录包含“文档”、“工作流”和“设置”;`setting`
渲染 `knowledge-detail/setting/KnowledgeSettingView.vue`,使用容器提供的知识库详情。`workflow-entry`
仅作为目录跳转入口,重定向到独立的 `workflow-knowledge` 全屏画布。画布返回知识库详情。
文档详情继续保留原地址,并通过 `detailActiveMenu` 高亮“文档”。
11 changes: 9 additions & 2 deletions ui/src/router/admin/workspace/modules/knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@ export const knowledgeRoutes: RouteRecordRaw[] = [
path: 'document',
name: 'workspace-knowledge-document-list',
component: () => import('@/views/knowledge-detail/document/DocumentListView.vue'),
meta: { title: '文档', order: 10 },
meta: { title: '文档', icon: 'icon-draft_outlined', activeIcon: 'icon-draft_outlined', order: 10 },
},

{
path: 'workflow-entry',
name: 'workspace-knowledge-workflow',
redirect: (to) => ({ name: 'workflow-knowledge', params: { workspaceId: to.params.workspaceId, knowledgeId: to.params.knowledgeId } }),
meta: { title: '工作流', order: 20 },
meta: { title: '工作流', icon: 'icon_ticket-flow_outlined', activeIcon: 'icon_ticket-flow_outlined', order: 20 },
},
{
path: 'setting',
name: 'workspace-knowledge-setting',
component: () => import('@/views/knowledge-detail/setting/KnowledgeSettingView.vue'),
meta: { title: '设置', icon: 'icon_setting', activeIcon: 'icon_setting_filled', order: 30 },
},
// {
// path: 'document/:documentId',
Expand Down
Loading
Loading