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
1 change: 1 addition & 0 deletions ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Structural responsibilities:
- Keep LogicFlow initialization, node registration, and canvas behavior inside `workflow-canvas/`.
Every page rendering this canvas belongs in `views/workflow/`; those Views own the page header,
route and page-level actions around `WorkflowCanvas`.
- Request version guards are reserved for `MkInfiniteScroll`; add them elsewhere only when explicitly requested. See `src/api/API_README.md`.
- Put server communication in `api/`, isolate Admin and Chat request systems, and group business APIs
by domain and resource according to `src/api/API_README.md`.
- Put backend fixed-value enums in `api/enums/` and import them through `@/api/enums`; keep
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 @@ -46,6 +46,12 @@ src/api/
- 页面或 Store 负责 loading、成功提示、特殊业务错误以及请求成功后的状态变更。
- Chat 的 base URL、鉴权和错误处理独立实现,不复用 Admin 请求客户端。

## 请求版本控制

除 `components/global/mk-infinite-scroll/index.vue` 的滚动加载外,未经用户明确要求,
不添加 `requestVersion`、请求序号、递增 ID、代次标记等用于忽略旧响应的请求版本控制,
也不通过改名或封装工具引入同类逻辑。普通请求直接维护数据、loading 和错误处理。

## 业务接口组织

- 业务 API 先按 Admin 入口下的 `auth`、`workspace`、`system` 等一级业务域归类。Workspace 和
Expand Down Expand Up @@ -240,3 +246,9 @@ API 对象和工作空间上下文,作为该抽屉的范围选择例外;用
`workspace/knowledge/workflow.ts` 维护保存和发布,保存提交 `work_flow`,响应使用
`KnowledgeWorkflowDetail`。前端详情与保存协议的 `default_model_setting` 复用
`DefaultModelSettingPayload`;服务端需支持该字段的持久化与回传(当前仓库知识库后端尚未实现)。

### 智能体复制

`ApplicationDetail` 复用 `ApplicationFormPayload` 中的配置字段,并保留详情接口的 `model`
和可空描述。复制通过 `getApplicationDetail` 获取完整配置,将 `model` 映射为 `model_id`,
再调用 `postApplication` 创建副本;不使用卡片列表摘要作为复制数据。
4 changes: 3 additions & 1 deletion ui/src/api/types/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import type { DefaultModelSettingPayload } from './model'

export type ApplicationType = (typeof APPLICATION_TYPE)[keyof typeof APPLICATION_TYPE]

export interface ApplicationDetail {
export interface ApplicationDetail extends Omit<ApplicationFormPayload, 'desc'> {
/** 详情接口返回的主模型 ID。 */
model?: string | null
default_model_setting?: DefaultModelSettingPayload
create_time?: string
desc?: string | null
Expand Down
6 changes: 5 additions & 1 deletion ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ Admin、Chat 入口调用 `configureMarkdownEditor()` 配置本地高亮、KaTeX

### MkCollapse

`v-model:expanded` 可控制展开状态;未绑定时沿用 `defaultExpanded` 的内部状态。
`destroyOnCollapse` 默认 `true`,设为 `false` 时折叠只隐藏内容,适用于需要保留参数表单及校验的场景。

标题折叠区,`title` 或 `label` 插槽提供标题,默认插槽提供内容。
`defaultExpanded` 默认 `true`,仅设置初始状态;点击标题内部切换,当前不提供展开状态事件。
`indicatorPosition` 默认 `before`,可设为 `after`;`triggerClass`、`triggerStyle` 调整触发区。
Expand Down Expand Up @@ -253,6 +256,7 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启
`icon`、`title`(透出 `{ title }`)、`subtitle`、`tag` 插槽可覆盖头部,默认插槽放详情。
`footer` 提供常驻内容及 `Action`、`ActionDropdown`:前者是悬浮/焦点操作容器,后者包裹 More 菜单,
内部直接放 `MkDropdownItem`,空菜单隐藏入口。仅需要开关或按钮时使用 `Action` 即可。
无有效 `footer` 内容时不渲染底栏,默认内容区不额外保留底部间距;有底栏时保留 16px 分隔。
`ActionDropdown` 固定 `persistent`,其管理的业务浮层应打开时挂载、`closed` 后卸载。

`selectable` 开启卡片选择,`selected` Prop 控制选中,`selected` 事件返回新状态。
Expand Down Expand Up @@ -498,7 +502,7 @@ Workspace 的文件夹虚拟树业务组件。组件根据当前资源上下文
手动导入 `business/select-application-dialog/index.vue` 和 `business/select-tool-dialog/index.vue`。
两者沿用知识库选择弹窗的目录、名称搜索、三列卡片、悬停详情、跨目录选择和清空交互;
`open()` 接收已选资源对象数组,`submit` 返回深拷贝后的资源对象数组,兼容只有 ID 的旧数据。
每次打开先重置临时状态;取消不提交,过期请求不写回
每次打开先重置临时状态;取消不提交。

智能体通过 `getAllApplication` 全量查询已发布资源,不展示共享目录。工具通过工作空间或共享
`getAllTool` 全量查询,仅展示启用资源;`toolTypes` 默认包含自定义、工作流和内置工具,
Expand Down
10 changes: 3 additions & 7 deletions ui/src/components/business/select-application-dialog/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const applicationOptions = ref<ApplicationDetail[]>([])
const selectedApplication = ref<(Partial<ApplicationDetail> & { id: string })[]>([])
const applicationLayoutRef = useTemplateRef<{ setScrollTop: (scrollTop: number) => void }>('applicationLayoutRef')
const folderTreeRef = useTemplateRef<InstanceType<typeof FolderTree>>('folderTreeRef')
let dialogVersion = 0

const selectedApplicationIds = computed(() => selectedApplication.value.map(({ id }) => id))

Expand All @@ -32,9 +31,8 @@ function toggleApplication(application: ApplicationDetail) {
: [...selectedApplication.value, cloneDeep(application)]
}

// 每次查询一次加载全部结果,忽略切换目录或关闭弹窗前发出的旧请求
// 每次查询一次加载全部结果。
function refreshApplication() {
const version = ++dialogVersion
loading.value = true
applicationOptions.value = []
appliedSearchKeyword.value = searchKeyword.value.trim()
Expand All @@ -44,17 +42,16 @@ function refreshApplication() {
...(appliedSearchKeyword.value ? { name: appliedSearchKeyword.value } : {}),
})
.then((application) => {
if (version !== dialogVersion) return
applicationOptions.value = application.filter((resource) => resource.is_publish && !props.excludedIds.includes(resource.id))
// 仅补全已选快照,不因查询结果缺少某个 ID 而删除关联。
const applicationById = new Map(application.map((application) => [application.id, application]))
selectedApplication.value = selectedApplication.value.map((application) => applicationById.get(application.id) ?? application)
return nextTick(() => {
if (version === dialogVersion) applicationLayoutRef.value?.setScrollTop(0)
applicationLayoutRef.value?.setScrollTop(0)
})
})
.finally(() => {
if (version === dialogVersion) loading.value = false
loading.value = false
})
}

Expand All @@ -81,7 +78,6 @@ function submit() {
visible.value = false
}
function resetData() {
dialogVersion++
loading.value = false
searchKeyword.value = ''
appliedSearchKeyword.value = ''
Expand Down
10 changes: 3 additions & 7 deletions ui/src/components/business/select-knowledge-dialog/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const knowledgeOptions = ref<KnowledgeItem[]>([])
const selectedKnowledge = ref<(Partial<KnowledgeItem> & { id: string })[]>([])
const knowledgeLayoutRef = useTemplateRef<{ setScrollTop: (scrollTop: number) => void }>('knowledgeLayoutRef')
const folderTreeRef = useTemplateRef<InstanceType<typeof FolderTree>>('folderTreeRef')
let dialogVersion = 0

// 与 v2 一致,以首个已选知识库的 Embedding 模型筛选可选资源。
const selectedKnowledgeIds = computed(() => selectedKnowledge.value.map(({ id }) => id))
Expand All @@ -39,9 +38,8 @@ function toggleKnowledge(knowledge: KnowledgeItem) {
: [...selectedKnowledge.value, cloneDeep(knowledge)]
}

// 每次查询一次加载全部结果,忽略切换目录或关闭弹窗前发出的旧请求
// 每次查询一次加载全部结果。
function refreshKnowledge() {
const version = ++dialogVersion
loading.value = true
knowledgeOptions.value = []
appliedSearchKeyword.value = searchKeyword.value.trim()
Expand All @@ -53,17 +51,16 @@ function refreshKnowledge() {
...(appliedSearchKeyword.value ? { name: appliedSearchKeyword.value } : {}),
})
.then((knowledge) => {
if (version !== dialogVersion) return
knowledgeOptions.value = knowledge
// 仅补全已选快照,不因查询结果缺少某个 ID 而删除关联。
const knowledgeById = new Map(knowledge.map((knowledge) => [knowledge.id, knowledge]))
selectedKnowledge.value = selectedKnowledge.value.map((knowledge) => knowledgeById.get(knowledge.id) ?? knowledge)
return nextTick(() => {
if (version === dialogVersion) knowledgeLayoutRef.value?.setScrollTop(0)
knowledgeLayoutRef.value?.setScrollTop(0)
})
})
.finally(() => {
if (version === dialogVersion) loading.value = false
loading.value = false
})
}

Expand All @@ -89,7 +86,6 @@ function submit() {
visible.value = false
}
function resetData() {
dialogVersion++
loading.value = false
searchKeyword.value = ''
appliedSearchKeyword.value = ''
Expand Down
10 changes: 3 additions & 7 deletions ui/src/components/business/select-tool-dialog/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const toolOptions = ref<ToolItem[]>([])
const selectedTool = ref<(Partial<ToolItem> & { id: string })[]>([])
const toolLayoutRef = useTemplateRef<{ setScrollTop: (scrollTop: number) => void }>('toolLayoutRef')
const folderTreeRef = useTemplateRef<InstanceType<typeof FolderTree>>('folderTreeRef')
let dialogVersion = 0

const selectedToolIds = computed(() => selectedTool.value.map(({ id }) => id))

Expand All @@ -37,9 +36,8 @@ function toggleTool(tool: ToolItem) {
: [...selectedTool.value, cloneDeep(tool)]
}

// 每次查询一次加载全部结果,忽略切换目录或关闭弹窗前发出的旧请求
// 每次查询一次加载全部结果。
function refreshTool() {
const version = ++dialogVersion
loading.value = true
toolOptions.value = []
appliedSearchKeyword.value = searchKeyword.value.trim()
Expand All @@ -52,19 +50,18 @@ function refreshTool() {
...(appliedSearchKeyword.value ? { name: appliedSearchKeyword.value } : {}),
})
.then((tool) => {
if (version !== dialogVersion) return
toolOptions.value = tool.filter(
(resource) => resource.is_active && props.toolTypes.includes(resource.tool_type) && !props.excludedIds.includes(resource.id),
)
// 仅补全已选快照,不因查询结果缺少某个 ID 而删除关联。
const toolById = new Map(tool.map((tool) => [tool.id, tool]))
selectedTool.value = selectedTool.value.map((tool) => toolById.get(tool.id) ?? tool)
return nextTick(() => {
if (version === dialogVersion) toolLayoutRef.value?.setScrollTop(0)
toolLayoutRef.value?.setScrollTop(0)
})
})
.finally(() => {
if (version === dialogVersion) loading.value = false
loading.value = false
})
}

Expand All @@ -91,7 +88,6 @@ function submit() {
visible.value = false
}
function resetData() {
dialogVersion++
loading.value = false
searchKeyword.value = ''
appliedSearchKeyword.value = ''
Expand Down
33 changes: 27 additions & 6 deletions ui/src/components/global/mk-collapse/index.vue
Original file line number Diff line number Diff line change
@@ -1,39 +1,60 @@
<script setup lang="ts">
import { CaretBottom } from '@element-plus/icons-vue'
import { ref, type HTMLAttributes } from 'vue'
import { computed, ref, type HTMLAttributes } from 'vue'

defineOptions({ name: 'MkCollapse' })

const props = withDefaults(
defineProps<{
defaultExpanded?: boolean
destroyOnCollapse?: boolean
indicatorPosition?: 'after' | 'before'
title?: string
triggerClass?: HTMLAttributes['class']
triggerStyle?: HTMLAttributes['style']
}>(),
{ defaultExpanded: true, indicatorPosition: 'before' },
{ defaultExpanded: true, destroyOnCollapse: true, indicatorPosition: 'before' },
)

defineSlots<{ default?: () => unknown; label?: () => unknown }>()

const expanded = ref(props.defaultExpanded)
const expandedModel = defineModel<boolean>('expanded')
const localExpanded = ref(props.defaultExpanded)
const expanded = computed({
get: () => expandedModel.value ?? localExpanded.value,
set: (value: boolean) => {
localExpanded.value = value
expandedModel.value = value
},
})
</script>

<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 }" />
<MkIcon
v-if="indicatorPosition === 'before'"
:icon="CaretBottom"
:size="14"
class="transition-transform text-N600!"
:class="{ '-rotate-90': !expanded }"
/>
<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 }" />
<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>
<div v-if="expanded">
<div v-if="!destroyOnCollapse || expanded" v-show="expanded">
<slot />
</div>
</el-collapse-transition>
Expand Down
26 changes: 19 additions & 7 deletions ui/src/components/global/mk-complex-search/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,17 @@ const emit = defineEmits<{ change: [value: Record<string, SearchValue | SearchVa

// 异步搜索
const remoteLoading = ref(false)
let remoteRequestId = 0
function handleRemoteSearch(query: string) {
const request = activeField.value?.remoteMethod?.(query)
if (!(request instanceof Promise)) return

const requestId = ++remoteRequestId
remoteLoading.value = true
request.then(
() => {
if (requestId === remoteRequestId) remoteLoading.value = false
remoteLoading.value = false
},
() => {
if (requestId === remoteRequestId) remoteLoading.value = false
remoteLoading.value = false
},
)
}
Expand All @@ -40,7 +38,7 @@ const activeField = computed(() => props.fields.find(({ value }) => value === se

function handleFieldChange() {
const shouldClearSearch = Array.isArray(searchValue.value) ? searchValue.value.length > 0 : searchValue.value !== ''
remoteRequestId += 1

remoteLoading.value = false
searchValue.value = activeField.value?.multiple ? [] : ''
if (shouldClearSearch) emit('change', undefined)
Expand Down Expand Up @@ -79,10 +77,24 @@ function handleChange() {
@change="handleChange"
:persistent="false"
>
<el-option v-for="(option, index) in activeField.options ?? []" :key="index" :disabled="option.disabled" :label="option.label" :value="option.value" />
<el-option
v-for="(option, index) in activeField.options ?? []"
:key="index"
:disabled="option.disabled"
:label="option.label"
:value="option.value"
/>
</el-select>

<el-input v-else :key="activeField?.value" v-model="searchValue" class="mk-complex-search__value w-50!" clearable placeholder="请输入" @change="handleChange" />
<el-input
v-else
:key="activeField?.value"
v-model="searchValue"
class="mk-complex-search__value w-50!"
clearable
placeholder="请输入"
@change="handleChange"
/>
</div>
</template>

Expand Down
8 changes: 5 additions & 3 deletions ui/src/components/global/mk-source-card/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { h, type FunctionalComponent } from 'vue'
import { computed, h, type FunctionalComponent } from 'vue'
import { dateFormat } from '@/utils/time'
import { hasRenderableSlotContent } from '@/utils/vnode'
import MkSourceCardAction from './mk-source-card-action.vue'
import MkSourceCardActionDropdown from './mk-source-card-action-dropdown.vue'

Expand Down Expand Up @@ -52,6 +53,7 @@ const slots = defineSlots<{
}>()

const SourceCardAction: FunctionalComponent = (_, { slots }) => (props.selectable ? null : h(MkSourceCardAction, null, slots))
const hasFooter = computed(() => hasRenderableSlotContent(slots.footer?.({ Action: SourceCardAction, ActionDropdown: MkSourceCardActionDropdown })))

function handleSelect() {
if (props.selectable) emit('selected', !props.selected)
Expand Down Expand Up @@ -101,11 +103,11 @@ function handleSelectedChange(selected: boolean | string | number) {

<slot name="tag" />
</header>
<div class="my-4 h-full text-N600" v-if="slots.default">
<div class="mt-4 h-full text-N600" :class="{ 'mb-4': hasFooter }" v-if="slots.default">
<slot />
</div>

<footer class="flex items-center gap-2">
<footer v-if="hasFooter" class="flex items-center gap-2">
<slot name="footer" :Action="SourceCardAction" :ActionDropdown="MkSourceCardActionDropdown" />
</footer>
</el-card>
Expand Down
3 changes: 3 additions & 0 deletions ui/src/constants/CONSTANT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ const knowledgeTypeLabel = KNOWLEDGE_TYPE_LABELS[knowledgeType]

`resource-authorization.ts` 的 `RESOURCE_PERMISSION_OPTIONS` 统一维护权限标签和说明;
资源授权页面与资源用户授权抽屉按版本和根目录约束筛选选项。

`trigger.ts` 的 `TRIGGER_SCHEDULE_OPTIONS` 统一提供触发器与长期记忆的周期级联选项,
包含周期文案、执行时间和间隔数值;使用方只读,配置回填与校验仍由各自组件维护。
Loading
Loading