feat: add field_roles configuration and role filter to metadata schema page [AC-MRS-06,15]

This commit is contained in:
MerCry 2026-03-05 17:25:15 +08:00
parent 5c1f311656
commit eb7bc7722b
4 changed files with 210 additions and 5 deletions

View File

@ -5,12 +5,20 @@ import type {
MetadataFieldUpdateRequest, MetadataFieldUpdateRequest,
MetadataFieldListResponse, MetadataFieldListResponse,
MetadataPayload, MetadataPayload,
MetadataScope MetadataScope,
FieldRole
} from '@/types/metadata' } from '@/types/metadata'
export const metadataSchemaApi = { export const metadataSchemaApi = {
list: (status?: 'draft' | 'active' | 'deprecated') => list: (status?: 'draft' | 'active' | 'deprecated', fieldRole?: FieldRole) =>
request<MetadataFieldListResponse>({ method: 'GET', url: '/admin/metadata-schemas', params: status ? { status } : {} }), request<MetadataFieldListResponse>({
method: 'GET',
url: '/admin/metadata-schemas',
params: {
...(status ? { status } : {}),
...(fieldRole ? { field_role: fieldRole } : {})
}
}),
get: (id: string) => get: (id: string) =>
request<MetadataFieldDefinition>({ method: 'GET', url: `/admin/metadata-schemas/${id}` }), request<MetadataFieldDefinition>({ method: 'GET', url: `/admin/metadata-schemas/${id}` }),
@ -31,6 +39,13 @@ export const metadataSchemaApi = {
params: { scope, include_deprecated: includeDeprecated } params: { scope, include_deprecated: includeDeprecated }
}), }),
getByRole: (role: FieldRole, includeDeprecated = false) =>
request<MetadataFieldListResponse>({
method: 'GET',
url: '/admin/metadata-schemas/by-role',
params: { role, include_deprecated: includeDeprecated }
}),
validate: (metadata: MetadataPayload, scope?: MetadataScope) => validate: (metadata: MetadataPayload, scope?: MetadataScope) =>
request<{ valid: boolean; errors?: { field_key: string; message: string }[] }>({ request<{ valid: boolean; errors?: { field_key: string; message: string }[] }>({
method: 'POST', method: 'POST',

View File

@ -0,0 +1,114 @@
<template>
<div class="field-roles-selector">
<label class="selector-label">
字段角色
<el-tooltip content="为字段分配角色,工具将按角色精准消费字段" placement="top">
<el-icon class="help-icon"><QuestionFilled /></el-icon>
</el-tooltip>
</label>
<el-checkbox-group v-model="selectedRoles" class="roles-checkbox-group">
<el-checkbox
v-for="role in availableRoles"
:key="role.value"
:value="role.value"
class="role-checkbox"
>
<div class="role-item">
<span class="role-label">{{ role.label }}</span>
<el-tooltip :content="role.description" placement="top">
<el-icon class="role-help-icon"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</el-checkbox>
</el-checkbox-group>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { QuestionFilled } from '@element-plus/icons-vue'
import type { FieldRole } from '@/types/metadata'
const props = defineProps<{
modelValue: FieldRole[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: FieldRole[]): void
}>()
const availableRoles: { value: FieldRole; label: string; description: string }[] = [
{
value: 'resource_filter',
label: '资源过滤',
description: '用于 KB 文档检索时的元数据过滤kb_search_dynamic 工具将消费此类字段'
},
{
value: 'slot',
label: '运行时槽位',
description: '对话流程中的结构化槽位用于信息收集memory_recall 工具将消费此类字段'
},
{
value: 'prompt_var',
label: '提示词变量',
description: '注入到 LLM Prompt 中的变量template_engine 将消费此类字段'
},
{
value: 'routing_signal',
label: '路由信号',
description: '用于意图路由和风险判断的信号intent_hint/high_risk_check 工具将消费此类字段'
}
]
const selectedRoles = computed({
get: () => props.modelValue || [],
set: (val: FieldRole[]) => emit('update:modelValue', val)
})
</script>
<style scoped lang="scss">
.field-roles-selector {
width: 100%;
}
.selector-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: var(--el-text-color-regular);
margin-bottom: 8px;
}
.help-icon {
font-size: 14px;
color: var(--el-text-color-secondary);
cursor: help;
}
.roles-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.role-checkbox {
margin-right: 0 !important;
}
.role-item {
display: flex;
align-items: center;
gap: 4px;
}
.role-label {
font-size: 14px;
}
.role-help-icon {
font-size: 12px;
color: var(--el-text-color-secondary);
cursor: help;
}
</style>

View File

@ -1,6 +1,7 @@
export type MetadataFieldType = 'string' | 'number' | 'boolean' | 'enum' | 'array_enum' export type MetadataFieldType = 'string' | 'number' | 'boolean' | 'enum' | 'array_enum'
export type MetadataFieldStatus = 'draft' | 'active' | 'deprecated' export type MetadataFieldStatus = 'draft' | 'active' | 'deprecated'
export type MetadataScope = 'kb_document' | 'intent_rule' | 'script_flow' | 'prompt_template' export type MetadataScope = 'kb_document' | 'intent_rule' | 'script_flow' | 'prompt_template'
export type FieldRole = 'resource_filter' | 'slot' | 'prompt_var' | 'routing_signal'
export interface MetadataFieldDefinition { export interface MetadataFieldDefinition {
id: string id: string
@ -14,6 +15,7 @@ export interface MetadataFieldDefinition {
scope: MetadataScope[] scope: MetadataScope[]
is_filterable: boolean is_filterable: boolean
is_rank_feature: boolean is_rank_feature: boolean
field_roles: FieldRole[]
status: MetadataFieldStatus status: MetadataFieldStatus
created_at?: string created_at?: string
updated_at?: string updated_at?: string
@ -29,6 +31,7 @@ export interface MetadataFieldCreateRequest {
scope: MetadataScope[] scope: MetadataScope[]
is_filterable?: boolean is_filterable?: boolean
is_rank_feature?: boolean is_rank_feature?: boolean
field_roles?: FieldRole[]
status: MetadataFieldStatus status: MetadataFieldStatus
} }
@ -40,6 +43,7 @@ export interface MetadataFieldUpdateRequest {
scope?: MetadataScope[] scope?: MetadataScope[]
is_filterable?: boolean is_filterable?: boolean
is_rank_feature?: boolean is_rank_feature?: boolean
field_roles?: FieldRole[]
status?: MetadataFieldStatus status?: MetadataFieldStatus
} }
@ -91,3 +95,17 @@ export const STATUS_TAG_MAP: Record<MetadataFieldStatus, '' | 'success' | 'warni
active: 'success', active: 'success',
deprecated: 'danger' deprecated: 'danger'
} }
export const FIELD_ROLE_OPTIONS: { value: FieldRole; label: string; description: string }[] = [
{ value: 'resource_filter', label: '资源过滤', description: '用于 KB 文档检索时的元数据过滤' },
{ value: 'slot', label: '运行时槽位', description: '对话流程中的结构化槽位,用于信息收集' },
{ value: 'prompt_var', label: '提示词变量', description: '注入到 LLM Prompt 中的变量' },
{ value: 'routing_signal', label: '路由信号', description: '用于意图路由和风险判断的信号' }
]
export const FIELD_ROLE_TAG_MAP: Record<FieldRole, '' | 'success' | 'warning' | 'danger' | 'info'> = {
resource_filter: '',
slot: 'success',
prompt_var: 'warning',
routing_signal: 'danger'
}

View File

@ -15,6 +15,14 @@
:value="opt.value" :value="opt.value"
/> />
</el-select> </el-select>
<el-select v-model="filterRole" placeholder="按角色筛选" clearable style="width: 140px;">
<el-option
v-for="opt in FIELD_ROLE_OPTIONS"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-button type="primary" @click="handleCreate"> <el-button type="primary" @click="handleCreate">
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
新建字段 新建字段
@ -57,6 +65,22 @@
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="field_roles" label="字段角色" min-width="200">
<template #default="{ row }">
<div class="role-tags">
<el-tag
v-for="role in row.field_roles"
:key="role"
:type="FIELD_ROLE_TAG_MAP[role as FieldRole]"
size="small"
class="role-tag"
>
{{ getRoleLabel(role) }}
</el-tag>
<span v-if="!row.field_roles || row.field_roles.length === 0" class="no-role">-</span>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100"> <el-table-column prop="status" label="状态" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="STATUS_TAG_MAP[row.status as MetadataFieldStatus]" size="small"> <el-tag :type="STATUS_TAG_MAP[row.status as MetadataFieldStatus]" size="small">
@ -167,6 +191,9 @@
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>
</el-form-item> </el-form-item>
<el-form-item label="字段角色">
<FieldRolesSelector v-model="formData.field_roles" />
</el-form-item>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="是否必填"> <el-form-item label="是否必填">
@ -244,21 +271,26 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete, Switch, ArrowDown } from '@element-plus/icons-vue' import { Plus, Edit, Delete, Switch, ArrowDown } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { metadataSchemaApi } from '@/api/metadata-schema' import { metadataSchemaApi } from '@/api/metadata-schema'
import FieldRolesSelector from '@/components/metadata/FieldRolesSelector.vue'
import { import {
METADATA_STATUS_OPTIONS, METADATA_STATUS_OPTIONS,
METADATA_SCOPE_OPTIONS, METADATA_SCOPE_OPTIONS,
METADATA_TYPE_OPTIONS, METADATA_TYPE_OPTIONS,
STATUS_TAG_MAP, STATUS_TAG_MAP,
FIELD_ROLE_OPTIONS,
FIELD_ROLE_TAG_MAP,
type MetadataFieldDefinition, type MetadataFieldDefinition,
type MetadataFieldCreateRequest, type MetadataFieldCreateRequest,
type MetadataFieldUpdateRequest, type MetadataFieldUpdateRequest,
type MetadataFieldStatus, type MetadataFieldStatus,
type MetadataScope type MetadataScope,
type FieldRole
} from '@/types/metadata' } from '@/types/metadata'
const loading = ref(false) const loading = ref(false)
const fields = ref<MetadataFieldDefinition[]>([]) const fields = ref<MetadataFieldDefinition[]>([])
const filterStatus = ref<MetadataFieldStatus | ''>('') const filterStatus = ref<MetadataFieldStatus | ''>('')
const filterRole = ref<FieldRole | ''>('')
const dialogVisible = ref(false) const dialogVisible = ref(false)
const isEdit = ref(false) const isEdit = ref(false)
const submitting = ref(false) const submitting = ref(false)
@ -276,6 +308,7 @@ const formData = reactive({
scope: [] as MetadataScope[], scope: [] as MetadataScope[],
is_filterable: true, is_filterable: true,
is_rank_feature: false, is_rank_feature: false,
field_roles: [] as FieldRole[],
status: 'draft' as MetadataFieldStatus status: 'draft' as MetadataFieldStatus
}) })
@ -314,10 +347,14 @@ const getScopeLabel = (scope: MetadataScope) => {
return METADATA_SCOPE_OPTIONS.find(o => o.value === scope)?.label || scope return METADATA_SCOPE_OPTIONS.find(o => o.value === scope)?.label || scope
} }
const getRoleLabel = (role: FieldRole) => {
return FIELD_ROLE_OPTIONS.find(o => o.value === role)?.label || role
}
const fetchFields = async () => { const fetchFields = async () => {
loading.value = true loading.value = true
try { try {
const res = await metadataSchemaApi.list(filterStatus.value || undefined) const res = await metadataSchemaApi.list(filterStatus.value || undefined, filterRole.value || undefined)
fields.value = res.items || [] fields.value = res.items || []
} catch (error: any) { } catch (error: any) {
ElMessage.error(error.response?.data?.message || '获取元数据字段失败') ElMessage.error(error.response?.data?.message || '获取元数据字段失败')
@ -339,6 +376,7 @@ const handleCreate = () => {
scope: [], scope: [],
is_filterable: true, is_filterable: true,
is_rank_feature: false, is_rank_feature: false,
field_roles: [],
status: 'draft' status: 'draft'
}) })
dialogVisible.value = true dialogVisible.value = true
@ -357,6 +395,7 @@ const handleEdit = (field: MetadataFieldDefinition) => {
scope: [...field.scope], scope: [...field.scope],
is_filterable: field.is_filterable, is_filterable: field.is_filterable,
is_rank_feature: field.is_rank_feature, is_rank_feature: field.is_rank_feature,
field_roles: field.field_roles || [],
status: field.status status: field.status
}) })
dialogVisible.value = true dialogVisible.value = true
@ -433,6 +472,7 @@ const handleSubmit = async () => {
scope: formData.scope, scope: formData.scope,
is_filterable: formData.is_filterable, is_filterable: formData.is_filterable,
is_rank_feature: formData.is_rank_feature, is_rank_feature: formData.is_rank_feature,
field_roles: formData.field_roles,
status: formData.status status: formData.status
} }
@ -466,6 +506,10 @@ watch(filterStatus, () => {
fetchFields() fetchFields()
}) })
watch(filterRole, () => {
fetchFields()
})
onMounted(() => { onMounted(() => {
fetchFields() fetchFields()
}) })
@ -529,6 +573,20 @@ onMounted(() => {
margin: 0; margin: 0;
} }
.role-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.role-tag {
margin: 0;
}
.no-role {
color: var(--el-text-color-placeholder);
}
.field-hint { .field-hint {
margin-top: 4px; margin-top: 4px;
font-size: 12px; font-size: 12px;