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
2 changes: 2 additions & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文
- 同一业务边界内的重复类型提取到最近共同目录的 `common.ts`;`common.ts` 不得成为无关类型
的集合。
- API 与 View 或 Component 使用同一业务类型时只保留一份声明,不得在两层分别定义。
- 智能体详情及保存参数中的 `default_model_setting` 共用模型类别 `DefaultModelType` 和单项
配置 `ModelConfig`(`types/model.ts`),工作流页面及设置抽屉从 `@/api/types` 引用。
- 名称相同但业务含义或字段约束不同的类型不要强行合并,应使用明确的领域名称区分。
- 使用 `interface` 描述对象结构,使用 `type` 描述联合类型、交叉类型、工具类型结果或别名。
- 类型名称必须体现业务含义,避免使用 `Data`、`Item`、`Info` 等脱离领域后含义不清的名称。
Expand Down
3 changes: 3 additions & 0 deletions ui/src/api/types/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import type LogicFlow from '@logicflow/core'
import { APPLICATION_TYPE } from '@/api/enums'
import type { DefaultModelType, ModelConfig } from './model'

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

export interface ApplicationDetail {
default_model_setting?: Partial<Record<DefaultModelType, ModelConfig>>
create_time?: string
desc?: string | null
folder?: string
Expand All @@ -27,6 +29,7 @@ export interface ApplicationDetail {
}

export interface ApplicationFormPayload {
default_model_setting?: ApplicationDetail['default_model_setting']
name?: string
desc?: string
model_id?: string
Expand Down
9 changes: 9 additions & 0 deletions ui/src/api/types/model.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
/** Workspace 模型与工具页面共用的业务类型。 */

import { MODEL_STATUS } from '@/api/enums'
import type { Dict } from './common'

export type ModelStatus = (typeof MODEL_STATUS)[keyof typeof MODEL_STATUS]

/** 智能体默认模型配置支持的模型类别。 */
export type DefaultModelType = 'LLM' | 'TTS' | 'STT' | 'IMAGE' | 'TTI' | 'TTV' | 'ITV' | 'RERANKER'

export interface ModelConfig {
model_id?: string
model_params_setting?: Dict<unknown>
}

export interface ModelProviderItem {
icon: string
name: string
Expand Down
15 changes: 13 additions & 2 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1053,11 +1053,22 @@ import FolderTree from '@/components/business/folder-tree/index.vue'
按供应商分组展示模型,通过 `v-model` 控制模型 ID,并在选择变化时触发 `change`。`options` 为
`ModelItem[]`,`providerOptions` 为 `ModelProviderItem[]`,模型列表和供应商列表均由使用方查询。

`showModelParams` 默认为 `false`,设为 `true` 时显示参数按钮,设为 `false` 时隐藏。
`canEditParams` 默认为 `false`,设为 `true` 时显示参数按钮,设为 `false` 时隐藏。
参数按钮位于选择框右侧内部,与选择框共用外边框,并通过短竖线与下拉箭头分隔。
未选择模型或传入 `disabled` 时按钮禁用;点击打开组件内的 `ModelParamsDialog`。
通过 `v-model:model-params` 绑定参数:切换模型时加载默认值,清空模型时清空参数,弹窗确认后
回写配置,取消不修改已保存参数。参数表单沿用上层提供的 `getModelParamsForm(modelId)` 注入方法;
隐藏参数入口时,组件只处理模型选择,不加载或修改参数。
使用方开启参数入口后应绑定对应参数字段,并移除独立的参数按钮、弹窗和默认参数请求。
通过 Props 与事件回写设置的子组件,应分别发送模型 ID 和参数的局部更新,由父级合并,避免
同次交互连续更新时使用旧 Props 覆盖刚选中的模型。

`canAdd` 默认为 `false`,开启后在下拉列表底部显示“添加模型”,复用 `ModelCreateButton` 的
供应商选择和模型创建流程。组件通过 `isWorkspaceResource()`、`isSystemSharedResource()` 读取
`resourceScope`:Workspace 传入 `ModelApi`,System 共享资源传入 `SystemSharedModelApi`;
其他范围暂不展示创建入口。创建成功后触发 `refresh`,使用方重新加载原业务范围的模型选项,
不自动替换当前选中模型。`footer` 插槽仍可覆盖默认创建入口。


```vue
<script setup lang="ts">
Expand All @@ -1067,7 +1078,7 @@ import ModelSelect from '@/components/business/model-select/index.vue'
<ModelSelect
v-model="selectedModelId"
v-model:model-params="modelParams"
show-model-params
can-edit-params
:options="modelOptions"
:provider-options="providerOptions"
placeholder="请选择模型"
Expand Down
123 changes: 81 additions & 42 deletions ui/src/components/business/model-select/index.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, useTemplateRef } from 'vue'
import { Check, Operation } from '@element-plus/icons-vue'
import { computed, ref, useTemplateRef } from 'vue'
import type { SelectInstance } from 'element-plus'
import { MODEL_STATUS } from '@/api/enums'
import type { ModelProviderItem, ModelItem } from '@/api/types'
import { groupBy } from 'lodash'
import ModelParamsDialog from './ModelParamsDialog.vue'
import ModelCreateButton from '@/views/model/create-model/ModelCreateButton.vue'
import ModelApi from '@/api/admin/workspace/model/model'
import SystemSharedModelApi from '@/api/admin/system/shared-resources/model'
import { isWorkspaceResource, isSystemSharedResource } from '@/utils/resource-context'
defineOptions({ name: 'ModelSelect', inheritAttrs: false })

interface ModelOptionGroup {
Expand All @@ -19,28 +23,50 @@ const props = withDefaults(
modelValue: string
options: ModelItem[]
providerOptions: ModelProviderItem[]
showModelParams?: boolean
canEditParams?: boolean
canAdd?: boolean
modelParams?: Record<string, unknown>
disabled?: boolean
}>(),
{
modelValue: '',
options: () => [],
providerOptions: () => [],
showModelParams: false,
canEditParams: false,
canAdd: false,
modelParams: () => ({}),
disabled: false,
},
)
const _options = computed(() => {
return groupBy(props.options, 'provider')
})

const emit = defineEmits<{
change: [modelId: string]
refresh: []
'update:modelValue': [modelId: string]
'update:modelParams': [settings: Record<string, unknown>]
}>()

const modelOptionGroups = computed<ModelOptionGroup[]>(() => {
const providerMap = new Map(props.providerOptions.map((provider) => [provider.provider, provider]))

return Object.entries(_options.value ?? {}).map(([provider, models]) => {
const providerOption = providerMap.get(provider)
return {
icon: providerOption?.icon ?? '',
models: [...models].sort((left, right) => Number(right.status === MODEL_STATUS.SUCCESS) - Number(left.status === MODEL_STATUS.SUCCESS)),
name: providerOption?.name ?? provider,
provider,
}
})
})

const selectedProviderIcon = computed(
() => modelOptionGroups.value.find(({ models }) => models.some(({ id }) => id === selectedModelId.value))?.icon ?? '',
)
const _options = computed(() => {
return groupBy(props.options, 'provider')
})

const loading = ref(false)

const selectedModelId = computed({
Expand All @@ -52,17 +78,30 @@ const selectedModelId = computed({
},
})

// 创建模型:根据当前资源范围传入完整 API,创建后由调用方刷新选项。
const selectRef = useTemplateRef<SelectInstance>('selectRef')
const allModelProvider: ModelProviderItem = { icon: '', name: '全部模型', provider: 'all' }
const createModelApi = computed(() => {
if (isWorkspaceResource()) return ModelApi
if (isSystemSharedResource()) return SystemSharedModelApi
return undefined
})

function handleOpenCreateModel(open: () => void) {
if (props.disabled || !props.canAdd || !createModelApi.value) return
selectRef.value?.blur()
open()
}

// 模型参数:切换模型加载默认值,确认弹窗后回写配置。
const modelParamsDialogRef = useTemplateRef<InstanceType<typeof ModelParamsDialog>>('modelParamsDialogRef')
let modelParamsRequestId = 0

function resetModelParams(modelId: string) {
const requestId = ++modelParamsRequestId
if (!props.showModelParams) return
if (!props.canEditParams) return
emit('update:modelParams', {})
if (!modelId) return
modelParamsDialogRef.value?.resetDefault(modelId).then((settings) => {
if (requestId !== modelParamsRequestId || props.modelValue !== modelId || !props.showModelParams) return
if (props.modelValue !== modelId || !props.canEditParams) return
emit('update:modelParams', settings)
})
}
Expand All @@ -71,36 +110,16 @@ function openModelParams() {
if (!props.modelValue || props.disabled) return
modelParamsDialogRef.value?.open(props.modelValue, props.modelParams)
}

onBeforeUnmount(() => {
modelParamsRequestId += 1
})

const modelOptionGroups = computed<ModelOptionGroup[]>(() => {
const providerMap = new Map(props.providerOptions.map((provider) => [provider.provider, provider]))

return Object.entries(_options.value ?? {}).map(([provider, models]) => {
const providerOption = providerMap.get(provider)
return {
icon: providerOption?.icon ?? '',
models: [...models].sort((left, right) => Number(right.status === MODEL_STATUS.SUCCESS) - Number(left.status === MODEL_STATUS.SUCCESS)),
name: providerOption?.name ?? provider,
provider,
}
})
})

const selectedProviderIcon = computed(
() => modelOptionGroups.value.find(({ models }) => models.some(({ id }) => id === selectedModelId.value))?.icon ?? '',
)
</script>

<template>
<div class="flex w-full items-center gap-2">
<div class="relative w-full" :class="{ 'model-select--with-params': canEditParams }">
<el-select
ref="selectRef"
v-model="selectedModelId"
placeholder="请选择模型"
v-bind="$attrs"
class="min-w-0 flex-1"
class="w-full"
:disabled="disabled"
clearable
filterable
Expand All @@ -120,7 +139,6 @@ const selectedProviderIcon = computed(
<span class="min-w-0 flex-1 truncate" :title="model.name">{{ model.name }}</span>
<el-tag v-if="model.source === 'shared'" size="small" type="info">共享</el-tag>
<span v-if="model.status !== MODEL_STATUS.SUCCESS" class="text-danger">不可用</span>
<Check v-if="model.id === selectedModelId" class="h-4 w-4 shrink-0 text-primary" />
</div>
</el-option>
</el-option-group>
Expand All @@ -132,13 +150,34 @@ const selectedProviderIcon = computed(
</div>
</template>

<template #empty>
<MkEmpty description="暂无可用模型" />
<template v-if="canAdd && createModelApi" #footer>
<slot name="footer">
<ModelCreateButton :api="createModelApi" :current-provider="allModelProvider" :providers="providerOptions" @refresh="emit('refresh')">
<template #default="{ open }">
<el-button type="primary" link :disabled="disabled" @click.stop="handleOpenCreateModel(open)">
<MkIcon name="icon_add_outlined" />
<span>添加模型</span>
</el-button>
</template>
</ModelCreateButton>
</slot>
</template>
</el-select>
<el-button v-if="showModelParams" :disabled="disabled || !modelValue" title="模型参数设置" aria-label="模型参数设置" @click="openModelParams">
<MkIcon :icon="Operation" />
</el-button>
<div v-if="canEditParams" class="absolute inset-y-px right-3 flex items-center gap-3">
<el-divider direction="vertical" />
<el-button link :disabled="disabled || !modelValue" title="模型参数设置" @click.stop="openModelParams">
<MkIcon name="icon_preferences_outlined" />
</el-button>
</div>
</div>
<ModelParamsDialog v-if="showModelParams" ref="modelParamsDialogRef" @submit="emit('update:modelParams', $event)" />
<ModelParamsDialog v-if="canEditParams" ref="modelParamsDialogRef" @submit="emit('update:modelParams', $event)" />
</template>

<style scoped>
/* 参数入口与选择器共用外边框,为原生下拉箭头和清空按钮预留空间。 */
.model-select--with-params {
:deep(.el-select__wrapper) {
padding-right: calc(var(--spacing) * 14);
}
}
</style>
14 changes: 7 additions & 7 deletions ui/src/constants/model.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export const MODEL_TYPE_LABELS: Record<string, string> = {
LLM: 'AI 模型',
EMBEDDING: '向量模型',
IMAGE: '图像模型',
ITV: '图生视频',
LLM: '大语言模型',
IMAGE: '视觉模型',
ITV: '图生视频模型',
STT: '语音识别模型',
TTI: '图片生成模型',
TTS: '语音合成模型',
TTV: '文生视频模型',
RERANKER: '重排模型',
STT: '语音识别',
TTI: '文生图',
TTS: '语音合成',
TTV: '文生视频',
}
1 change: 1 addition & 0 deletions ui/src/router/ROUTE_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Login、Workflow 路由没有挂载 `AppLayout`,因此不会出现在左侧导
- `login`:登录及账户相关独立页面。
- `workflow`:集中维护各业务的全屏画布路由,当前包含智能体和工具工作流,后续知识库等画布
继续在 `admin/workflow/index.ts` 中注册。
- Workspace 的全屏 Workflow 路由显式设置 `resourceScope: 'workspace'`,使模型创建等资源操作使用 Workspace API。
- Workflow 路由保留当前 Workspace 和资源标识;智能体工作流地址为
`/workspace/:workspaceId/application/:applicationId/workflow`,工具工作流地址为
`/workspace/:workspaceId/tool/:toolId/workflow`。两个页面均可从资源操作入口进入,按住 Ctrl 或
Expand Down
4 changes: 2 additions & 2 deletions ui/src/router/admin/workflow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ export const workflowRoutes: RouteRecordRaw[] = [
path: '/workspace/:workspaceId/application/:applicationId/workflow',
name: 'workflow-application',
component: () => import('@/views/workflow/ApplicationWorkflowView.vue'),
meta: { title: '智能体工作流' },
meta: { title: '智能体工作流', resourceScope: 'workspace' },
},
{
path: '/workspace/:workspaceId/tool/:toolId/workflow',
name: 'workflow-tool',
component: () => import('@/views/workflow/ToolWorkflowView.vue'),
meta: { title: '工具工作流' },
meta: { title: '工具工作流', resourceScope: 'workspace' },
},
]
9 changes: 9 additions & 0 deletions ui/src/styles/element-plus.scss
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@

/* drawer */
.el-drawer {
box-shadow: -6px 0px 24px 0px rgb(var(--mk-N900-rgb) / 8%);

&__header {
position: relative;
border-bottom: 1px solid var(--el-border-color);
Expand Down Expand Up @@ -779,6 +781,13 @@
}
}

.el-select-group {
&__title {
color: var(--mk-N500);
font-size: var(--mk-font-size-base);
}
}

/* steps */
.el-steps {
align-items: center;
Expand Down
Loading
Loading