diff --git a/ui/AGENTS.md b/ui/AGENTS.md index 825fcaab8c6..59e91105d36 100644 --- a/ui/AGENTS.md +++ b/ui/AGENTS.md @@ -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 diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index 4df7d12914d..281dec65a59 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -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 和 @@ -240,3 +246,9 @@ API 对象和工作空间上下文,作为该抽屉的范围选择例外;用 `workspace/knowledge/workflow.ts` 维护保存和发布,保存提交 `work_flow`,响应使用 `KnowledgeWorkflowDetail`。前端详情与保存协议的 `default_model_setting` 复用 `DefaultModelSettingPayload`;服务端需支持该字段的持久化与回传(当前仓库知识库后端尚未实现)。 + +### 智能体复制 + +`ApplicationDetail` 复用 `ApplicationFormPayload` 中的配置字段,并保留详情接口的 `model` +和可空描述。复制通过 `getApplicationDetail` 获取完整配置,将 `model` 映射为 `model_id`, +再调用 `postApplication` 创建副本;不使用卡片列表摘要作为复制数据。 diff --git a/ui/src/api/types/application.ts b/ui/src/api/types/application.ts index c044e8efedd..6b0e32b8eea 100644 --- a/ui/src/api/types/application.ts +++ b/ui/src/api/types/application.ts @@ -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 { + /** 详情接口返回的主模型 ID。 */ + model?: string | null default_model_setting?: DefaultModelSettingPayload create_time?: string desc?: string | null diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index 0a0b0433ef5..76796bf331f 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -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` 调整触发区。 @@ -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` 事件返回新状态。 @@ -498,7 +502,7 @@ Workspace 的文件夹虚拟树业务组件。组件根据当前资源上下文 手动导入 `business/select-application-dialog/index.vue` 和 `business/select-tool-dialog/index.vue`。 两者沿用知识库选择弹窗的目录、名称搜索、三列卡片、悬停详情、跨目录选择和清空交互; `open()` 接收已选资源对象数组,`submit` 返回深拷贝后的资源对象数组,兼容只有 ID 的旧数据。 -每次打开先重置临时状态;取消不提交,过期请求不写回。 +每次打开先重置临时状态;取消不提交。 智能体通过 `getAllApplication` 全量查询已发布资源,不展示共享目录。工具通过工作空间或共享 `getAllTool` 全量查询,仅展示启用资源;`toolTypes` 默认包含自定义、工作流和内置工具, diff --git a/ui/src/components/business/select-application-dialog/index.vue b/ui/src/components/business/select-application-dialog/index.vue index 6f914bf6000..474d6d45213 100644 --- a/ui/src/components/business/select-application-dialog/index.vue +++ b/ui/src/components/business/select-application-dialog/index.vue @@ -22,7 +22,6 @@ const applicationOptions = ref([]) const selectedApplication = ref<(Partial & { id: string })[]>([]) const applicationLayoutRef = useTemplateRef<{ setScrollTop: (scrollTop: number) => void }>('applicationLayoutRef') const folderTreeRef = useTemplateRef>('folderTreeRef') -let dialogVersion = 0 const selectedApplicationIds = computed(() => selectedApplication.value.map(({ id }) => id)) @@ -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() @@ -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 }) } @@ -81,7 +78,6 @@ function submit() { visible.value = false } function resetData() { - dialogVersion++ loading.value = false searchKeyword.value = '' appliedSearchKeyword.value = '' diff --git a/ui/src/components/business/select-knowledge-dialog/index.vue b/ui/src/components/business/select-knowledge-dialog/index.vue index 9aa864a287e..bc3fbf322ac 100644 --- a/ui/src/components/business/select-knowledge-dialog/index.vue +++ b/ui/src/components/business/select-knowledge-dialog/index.vue @@ -22,7 +22,6 @@ const knowledgeOptions = ref([]) const selectedKnowledge = ref<(Partial & { id: string })[]>([]) const knowledgeLayoutRef = useTemplateRef<{ setScrollTop: (scrollTop: number) => void }>('knowledgeLayoutRef') const folderTreeRef = useTemplateRef>('folderTreeRef') -let dialogVersion = 0 // 与 v2 一致,以首个已选知识库的 Embedding 模型筛选可选资源。 const selectedKnowledgeIds = computed(() => selectedKnowledge.value.map(({ id }) => id)) @@ -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() @@ -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 }) } @@ -89,7 +86,6 @@ function submit() { visible.value = false } function resetData() { - dialogVersion++ loading.value = false searchKeyword.value = '' appliedSearchKeyword.value = '' diff --git a/ui/src/components/business/select-tool-dialog/index.vue b/ui/src/components/business/select-tool-dialog/index.vue index 0d5472e43f8..dd6aae67ee1 100644 --- a/ui/src/components/business/select-tool-dialog/index.vue +++ b/ui/src/components/business/select-tool-dialog/index.vue @@ -27,7 +27,6 @@ const toolOptions = ref([]) const selectedTool = ref<(Partial & { id: string })[]>([]) const toolLayoutRef = useTemplateRef<{ setScrollTop: (scrollTop: number) => void }>('toolLayoutRef') const folderTreeRef = useTemplateRef>('folderTreeRef') -let dialogVersion = 0 const selectedToolIds = computed(() => selectedTool.value.map(({ id }) => id)) @@ -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() @@ -52,7 +50,6 @@ 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), ) @@ -60,11 +57,11 @@ function refreshTool() { 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 }) } @@ -91,7 +88,6 @@ function submit() { visible.value = false } function resetData() { - dialogVersion++ loading.value = false searchKeyword.value = '' appliedSearchKeyword.value = '' diff --git a/ui/src/components/global/mk-collapse/index.vue b/ui/src/components/global/mk-collapse/index.vue index fd44a221c5d..fe5f92d7df0 100644 --- a/ui/src/components/global/mk-collapse/index.vue +++ b/ui/src/components/global/mk-collapse/index.vue @@ -1,39 +1,60 @@ diff --git a/ui/src/components/global/mk-source-card/index.vue b/ui/src/components/global/mk-source-card/index.vue index c3ddb5a1a1f..ba708fe95c6 100644 --- a/ui/src/components/global/mk-source-card/index.vue +++ b/ui/src/components/global/mk-source-card/index.vue @@ -1,6 +1,7 @@ + + diff --git a/ui/src/views/application/application-card/action-dropdown/copy-application-action/CopyApplicationDialog.vue b/ui/src/views/application/application-card/action-dropdown/copy-application-action/CopyApplicationDialog.vue new file mode 100644 index 00000000000..f6a599a3505 --- /dev/null +++ b/ui/src/views/application/application-card/action-dropdown/copy-application-action/CopyApplicationDialog.vue @@ -0,0 +1,126 @@ + + + diff --git a/ui/src/views/application/application-card/action-dropdown/index.ts b/ui/src/views/application/application-card/action-dropdown/index.ts index c3420a0da29..ba20ecddc49 100644 --- a/ui/src/views/application/application-card/action-dropdown/index.ts +++ b/ui/src/views/application/application-card/action-dropdown/index.ts @@ -4,3 +4,4 @@ export { default as MoveApplicationAction } from './MoveApplicationAction.vue' export { default as SettingApplicationAction } from './SettingApplicationAction.vue' export { default as AuthorizeApplicationAction } from './AuthorizeApplicationAction.vue' export { default as RelatedResourcesApplicationAction } from './RelatedResourcesApplicationAction.vue' +export { default as CopyApplicationAction } from './copy-application-action/CopyApplicationAction.vue' diff --git a/ui/src/views/knowledge-detail/WorkspaceKnowledgeDetailView.vue b/ui/src/views/knowledge-detail/WorkspaceKnowledgeDetailView.vue index a2fcbbe2931..661eea08f24 100644 --- a/ui/src/views/knowledge-detail/WorkspaceKnowledgeDetailView.vue +++ b/ui/src/views/knowledge-detail/WorkspaceKnowledgeDetailView.vue @@ -13,18 +13,16 @@ const router = useRouter() const knowledge = ref() const loading = ref(false) const knowledgeId = computed(() => String(route.params.knowledgeId ?? '')) -let detailRequestId = 0 function loadKnowledgeDetail() { - const requestId = ++detailRequestId knowledge.value = undefined loading.value = true return KnowledgeApi.getKnowledgeDetail(knowledgeId.value) .then((detail) => { - if (requestId === detailRequestId) knowledge.value = detail + knowledge.value = detail }) .finally(() => { - if (requestId === detailRequestId) loading.value = false + loading.value = false }) } diff --git a/ui/src/views/knowledge/KnowledgeView.vue b/ui/src/views/knowledge/KnowledgeView.vue index ecae11e627d..d2c8900a389 100644 --- a/ui/src/views/knowledge/KnowledgeView.vue +++ b/ui/src/views/knowledge/KnowledgeView.vue @@ -185,32 +185,28 @@ function handleBatchDelete() {

{{ currentFolder.name }}

- - - - {{ batchSelectionMode ? '取消选择' : '批量选择' }} - - - - - 创建 - +
diff --git a/ui/src/views/system/identity/users/UserListView.vue b/ui/src/views/system/identity/users/UserListView.vue index 659035598bf..a1d634a7ca8 100644 --- a/ui/src/views/system/identity/users/UserListView.vue +++ b/ui/src/views/system/identity/users/UserListView.vue @@ -207,9 +207,11 @@ onMounted(() => loadSystemUsers()) :disabled="row.role === ROLE_TYPE.ADMIN || row.id === user.userInfo?.id" :before-change="() => handleChangeStatus(row)" size="small" + class="mr-3" /> + - +
diff --git a/ui/src/views/tool/ToolView.vue b/ui/src/views/tool/ToolView.vue index 3bbc4f2cf36..b645cd775ea 100644 --- a/ui/src/views/tool/ToolView.vue +++ b/ui/src/views/tool/ToolView.vue @@ -204,15 +204,18 @@ onMounted(() => {
@@ -272,8 +275,8 @@ onMounted(() => { @delete="handleDeleteTool" /> - + diff --git a/ui/src/views/tool/tool-card/ToolCard.vue b/ui/src/views/tool/tool-card/ToolCard.vue index 1ba5f84dd35..f2d1cc4ba36 100644 --- a/ui/src/views/tool/tool-card/ToolCard.vue +++ b/ui/src/views/tool/tool-card/ToolCard.vue @@ -6,7 +6,15 @@ import UpdateVersionButton from './UpdateVersionButton.vue' defineOptions({ name: 'ToolCard' }) -defineProps<{ api: typeof ToolApi; disabled?: boolean; selectable?: boolean; selected?: boolean; shared: boolean; storeTools: ToolStoreResponse['apps']; tool: ToolItem }>() +defineProps<{ + api: typeof ToolApi + disabled?: boolean + selectable?: boolean + selected?: boolean + shared: boolean + storeTools: ToolStoreResponse['apps'] + tool: ToolItem +}>() const loading = defineModel('loading', { default: false }) @@ -36,7 +44,13 @@ defineSlots<{ actions?: () => unknown; 'action-dropdown'?: () => unknown }>()

@@ -46,6 +60,7 @@ defineSlots<{ actions?: () => unknown; 'action-dropdown'?: () => unknown }>()