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: 0 additions & 6 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,6 @@ Action、Drawer 或 Dialog。复用方直接使用 `typeof XxxApi` 约束完整
组装为 `{ id_list, folder_id }`,`putBatchDeleteKnowledge` 将 ID 数组组装为 `{ id_list }`,
分别使用 PUT 请求 `batch_move` 和 `batch_delete`。页面和 Action 负责类型判断及 loading。

### 知识库文档标签

`workspace/knowledge/knowledge.ts` 的 `getKnowledgeTags(knowledgeIds)` 查询所选知识库的全部文档
标签,返回按标签名称分组的 `KnowledgeTagGroup[]`。工作流页面将查询方法提供给文档标签检索
节点,节点负责本地搜索和最多 100 项的展示。

## 枚举与类型组织

API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文档统一维护。
Expand Down
4 changes: 4 additions & 0 deletions ui/src/assets/workflow/icon_document-extract.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions ui/src/assets/workflow/icon_reranker.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions ui/src/assets/workflow/icon_search-document.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions ui/src/views/tool/ToolView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ToolApi from '@/api/admin/workspace/tool/tool'
import SharedApi from '@/api/admin/workspace/shared'
import ToolStoreApi from '@/api/admin/tool-store.ts'
import type { Dict, FolderItem, OptionItem, ToolItem, ToolStoreResponse, ToolType } from '@/api/types'
import { RESOURCE_TYPE, TOOL_TYPE, TOOL_SCOPE } from '@/api/enums'
import { RESOURCE_TYPE, TOOL_TYPE } from '@/api/enums'
import { TOOL_TYPE_OPTIONS, FOLDER_ENTRIES, FOLDER_ENTRY_ID } from '@/constants'
import FolderTree from '@/components/business/folder-tree/index.vue'
import MoveToDialog from '@/components/business/folder-tree/MoveToDialog.vue'
Expand Down Expand Up @@ -69,7 +69,7 @@ const toolType = ref<ToolType | ''>('')

function loadToolsPage(pagination: { currentPage: number; pageSize: number }) {
const request = isShared.value ? SharedApi : ToolApi
const folderId = isShared.value ? {} : { folder_id: currentFolder.value.id || FOLDER_ENTRY_ID.ALL, scope: TOOL_SCOPE.WORKSPACE }
const folderId = isShared.value ? {} : { folder_id: currentFolder.value.id || FOLDER_ENTRY_ID.ALL }
return request.getToolPage(pagination, { ...toolQuery.value, tool_type: toolType.value, ...folderId })
}

Expand Down
10 changes: 7 additions & 3 deletions ui/src/workflow-canvas/WORKFLOW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,10 @@ AI 对话节点的提示词、历史记录、视觉理解和输出思考表单
标签条件使用 `MkFormList`,设置 `minRows` 为 `0`,保留允许删除到空列表的语义。文档标签检索通过
`NodeSearchScope` 复用知识库选择,保留知识库快照及缺少详情的关联 ID;标签条件使用
`MkFormList` 并设置 `minRows` 为 `0`,节点模型统一将新建及已有节点宽度设为 `455`。
`ApplicationWorkflowView` 和 `ToolWorkflowView` 提供 `getKnowledgeTags` 与 `getRerankerModels`
注入接口,分别用于文档标签选项与重排模型查询;新增模型后直接重新查询,不读取已有模型列表缓存。
`store/api/workspace/index.ts` 独立维护 `getAllTags(knowledgeIds)`,使用 `knowledge_ids[]` 查询
所选知识库的全部文档标签,返回 `KnowledgeTagGroup[]`。文档标签选项通过工作流 Store 的 `force.getAllTags(knowledgeIds)` 查询,刷新时跳过已有标签缓存。
`ApplicationWorkflowView` 和 `ToolWorkflowView` 提供重排模型查询注入接口;新增模型后直接重新查询,
不读取已有模型列表缓存。
标签请求只回写当前关联知识库的结果,过期或节点卸载后的响应不再修改节点。

固定字段写入统一使用直接赋值,例如 `model.properties.node_data = value`、
Expand All @@ -223,7 +225,9 @@ Vue `computed` 的 `set` 和原生 `Map.set()` 按各自 API 正常使用。
MkFormList 的排序、增删均以 `cloneDeep` 回写;MkTable 保留普通行对象引用,因此工作流节点
在 computed setter 中深拷贝新数组后写回 `model.properties.node_data`,并执行原有字段同步。
带 ID 的新行使用 `default-item` 工厂函数,每次添加生成独立 ID。条件分支等保留锚点 ID、
固定末尾分支的特殊规则不能直接替换成普通列表移动。
固定末尾分支的特殊规则不能直接替换成普通列表移动。判断器仅将 IF / ELSE IF 分支放入排序容器,
卡片悬停时显示排序手柄;ELSE 独立渲染并固定末尾。排序后按位置更新分支名称,保留分支 ID,
并同步刷新锚点顺序与连线。

## View 接入约定

Expand Down
6 changes: 6 additions & 0 deletions ui/src/workflow-canvas/icons/document-extract-node-icon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<el-avatar shape="square">
<img src="@/assets/workflow/icon_document-extract.svg" style="width: 64%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>
6 changes: 6 additions & 0 deletions ui/src/workflow-canvas/icons/reranker-node-icon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<el-avatar shape="square" class="bg-purple!">
<img src="@/assets/workflow/icon_reranker.svg" style="width: 64%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>
6 changes: 6 additions & 0 deletions ui/src/workflow-canvas/icons/search-document-node-icon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<el-avatar shape="square">
<img src="@/assets/workflow/icon_search-document.svg" style="width: 64%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>
128 changes: 77 additions & 51 deletions ui/src/workflow-canvas/nodes/condition-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import NodeContainer from '@/workflow-canvas/core/node-container/index.vue'
import { createAnchorGuard, handleNodeWheel } from '@/workflow-canvas/core/utils'
import { compareList } from '@/workflow-canvas/config/constants'
import { randomId } from '@/utils/common'
import { useSortable } from '@/utils/use-sortable'

defineOptions({ name: 'WorkflowConditionNode' })
const getModel = inject('getModel') as () => BaseNodeModel
Expand Down Expand Up @@ -52,6 +53,26 @@ const formData = computed(() => model.properties.node_data as { branch: BranchIt
const conditionNodeFormRef = useTemplateRef<FormInstance>('conditionNodeFormRef')
const nodeCascaderRefs = useTemplateRef<InstanceType<typeof NodeCascader>[]>('nodeCascaderRefs')

// 仅排序条件分支,ELSE 保留在独立容器和数据末尾。
const branchListRef = useTemplateRef<HTMLElement>('branchListRef')
const elseBranch = computed(() => formData.value.branch.at(-1))
const sortableBranches = computed({
get: () => formData.value.branch.slice(0, -1),
set: (branches: BranchItem[]) => {
const reorderedBranches = cloneDeep([...branches, ...formData.value.branch.slice(-1)])
reorderedBranches.slice(0, -1).forEach((branch, index) => {
branch.type = index === 0 ? 'IF' : `ELSE IF ${index}`
})
formData.value.branch = reorderedBranches
refreshBranchAnchors()
},
})
useSortable(branchListRef, sortableBranches, () => ({
disabled: sortableBranches.value.length < 2,
handle: '.condition-branch-handle',
draggable: '> .condition-branch',
}))

// 新分支插入 ELSE 之前,并保留稳定的锚点 ID。
function addBranch() {
const branches = cloneDeep(formData.value.branch)
Expand Down Expand Up @@ -134,11 +155,16 @@ onBeforeUnmount(() => {
<template>
<NodeContainer :node-model="model">
<el-form ref="conditionNodeFormRef" :model="formData" label-position="top" require-asterisk-position="right" @submit.prevent>
<template v-for="(branch, branchIndex) in formData.branch" :key="branch.id">
<div class="mb-2 flex items-center gap-1">
<div v-branch-resize="branch.id" class="mk-gray-card min-w-0 flex-1">
<div class="flex-between min-h-6">
<span>{{ branch.type }}</span>
<div ref="branchListRef">
<div v-for="(branch, branchIndex) in sortableBranches" :key="branch.id" class="condition-branch mb-2 flex items-center gap-1">
<div v-branch-resize="branch.id" class="mk-gray-card group/branch min-w-0 flex-1">
<div class="condition-branch-handle flex-between min-h-6">
<div class="flex items-center">
<el-button v-if="sortableBranches.length > 1" link class="hidden! group-hover/branch:inline-flex!" aria-label="拖拽排序分支">
<MkIcon name="icon_drag_outlined" />
</el-button>
<span>{{ branch.type }}</span>
</div>
<div v-if="branch.conditions.length > 1" class="flex items-center gap-2 text-N600">
<span>符合以下</span>
<el-select
Expand All @@ -156,63 +182,63 @@ onBeforeUnmount(() => {
</div>
</div>

<template v-if="branchIndex !== formData.branch.length - 1">
<div class="mt-2">
<MkFormList v-model="branch.conditions" :default-item="createCondition()" :first-row-has-label="false" add-text="添加条件">
<template #default="{ item: condition, index: conditionIndex }">
<el-form-item
class="small min-w-0 flex-2"
:prop="`branch.${branchIndex}.conditions.${conditionIndex}.field`"
:rules="{ type: 'array', required: true, message: '请选择变量', trigger: 'change' }"
<div class="mt-2">
<MkFormList v-model="branch.conditions" :default-item="createCondition()" :first-row-has-label="false" add-text="添加条件">
<template #default="{ item: condition, index: conditionIndex }">
<el-form-item
class="small min-w-0 flex-2"
:prop="`branch.${branchIndex}.conditions.${conditionIndex}.field`"
:rules="{ type: 'array', required: true, message: '请选择变量', trigger: 'change' }"
>
<NodeCascader ref="nodeCascaderRefs" v-model="condition.field" :node-model="model" placeholder="请选择变量" />
</el-form-item>
<el-form-item
class="small min-w-0 flex-1"
:prop="`branch.${branchIndex}.conditions.${conditionIndex}.compare`"
:rules="{ required: true, message: '请选择', trigger: 'change' }"
>
<el-select
v-model="condition.compare"
:teleported="false"
placeholder="请选择"
clearable
@change="changeComparison(condition)"
@visible-change="anchorGuard.setOverlayVisible(`${branch.id}:${conditionIndex}:compare`, $event)"
@wheel="handleNodeWheel"
>
<NodeCascader ref="nodeCascaderRefs" v-model="condition.field" :node-model="model" placeholder="请选择变量" />
</el-form-item>
<el-option v-for="comparison in compareList" :key="comparison.value" :label="comparison.label" :value="comparison.value" />
</el-select>
</el-form-item>
<div class="min-w-0 flex-1">
<el-form-item
class="small min-w-0 flex-1"
:prop="`branch.${branchIndex}.conditions.${conditionIndex}.compare`"
:rules="{ required: true, message: '请选择', trigger: 'change' }"
v-if="!valueLessComparisons.has(condition.compare)"
:prop="`branch.${branchIndex}.conditions.${conditionIndex}.value`"
:rules="{ required: true, message: '请输入比较值', trigger: 'blur' }"
class="small"
>
<el-select
v-model="condition.compare"
:teleported="false"
placeholder="请选择"
clearable
@change="changeComparison(condition)"
@visible-change="anchorGuard.setOverlayVisible(`${branch.id}:${conditionIndex}:compare`, $event)"
@wheel="handleNodeWheel"
>
<el-option v-for="comparison in compareList" :key="comparison.value" :label="comparison.label" :value="comparison.value" />
</el-select>
<el-input v-model="condition.value" placeholder="请输入比较值" />
</el-form-item>
<div class="min-w-0 flex-1">
<el-form-item
v-if="!valueLessComparisons.has(condition.compare)"
:prop="`branch.${branchIndex}.conditions.${conditionIndex}.value`"
:rules="{ required: true, message: '请输入比较值', trigger: 'blur' }"
class="small"
>
<el-input v-model="condition.value" placeholder="请输入比较值" />
</el-form-item>
</div>
</template>
</MkFormList>
</div>
</template>
</div>
</template>
</MkFormList>
</div>
</div>
<!-- 删除分支 -->
<div v-if="formData.branch.length > 2" class="h-6 w-6 shrink-0">
<el-button
v-if="branchIndex < formData.branch.length - 1"
text
class="h-6! w-6! p-0!"
aria-label="删除分支"
@click="deleteBranch(branchIndex)"
>
<el-button text class="h-6! w-6! p-0!" aria-label="删除分支" @click="deleteBranch(branchIndex)">
<MkIcon name="icon_delete-trash_outlined" />
</el-button>
</div>
</div>
</template>
</div>
<div v-if="elseBranch" class="mb-2 flex items-center gap-1">
<div :key="elseBranch.id" v-branch-resize="elseBranch.id" class="mk-gray-card min-w-0 flex-1">
<div class="flex-between min-h-6">
<span>{{ elseBranch.type }}</span>
</div>
</div>
<div v-if="formData.branch.length > 2" class="h-6 w-6 shrink-0" />
</div>

<el-button link type="primary" @click="addBranch">
<MkIcon name="icon_add_outlined" class="mr-1" />
Expand Down
Loading
Loading