|
|
@ -115,6 +115,40 @@ |
|
|
</t-checkbox-group> |
|
|
</t-checkbox-group> |
|
|
<span v-else class="desc-text">暂无知识库分类,请先在知识库管理中创建分类</span> |
|
|
<span v-else class="desc-text">暂无知识库分类,请先在知识库管理中创建分类</span> |
|
|
</t-form-item> |
|
|
</t-form-item> |
|
|
|
|
|
|
|
|
|
|
|
<t-form-item label="MCP 工具授权"> |
|
|
|
|
|
<div class="mcp-tools-box"> |
|
|
|
|
|
<t-checkbox |
|
|
|
|
|
v-model="allToolsSelected" |
|
|
|
|
|
:disabled="saving" |
|
|
|
|
|
> |
|
|
|
|
|
全部授权(允许调用所有已接入的 MCP 工具) |
|
|
|
|
|
</t-checkbox> |
|
|
|
|
|
<t-loading size="small" :loading="mcpLoading"> |
|
|
|
|
|
<t-checkbox-group |
|
|
|
|
|
v-if="!allToolsSelected && mcpServers.length" |
|
|
|
|
|
v-model="form.allowedMcpTools" |
|
|
|
|
|
class="mcp-tools-list" |
|
|
|
|
|
> |
|
|
|
|
|
<div v-for="s in mcpServers" :key="s.configId" class="mcp-server-group"> |
|
|
|
|
|
<div class="mcp-server-name">{{ s.serverName }}</div> |
|
|
|
|
|
<t-checkbox |
|
|
|
|
|
v-for="t in s.tools" |
|
|
|
|
|
:key="t.name" |
|
|
|
|
|
:value="t.name" |
|
|
|
|
|
:disabled="saving" |
|
|
|
|
|
> |
|
|
|
|
|
<span class="mcp-tool-label">{{ t.rawName }}</span> |
|
|
|
|
|
<span v-if="t.description" class="mcp-tool-desc"> — {{ t.description }}</span> |
|
|
|
|
|
</t-checkbox> |
|
|
|
|
|
</div> |
|
|
|
|
|
</t-checkbox-group> |
|
|
|
|
|
<span v-else-if="!allToolsSelected && !mcpServers.length && !mcpLoading" class="desc-text"> |
|
|
|
|
|
暂无已接入的 MCP 工具,请先在「MCP 服务管理」中配置并启用 Server |
|
|
|
|
|
</span> |
|
|
|
|
|
</t-loading> |
|
|
|
|
|
</div> |
|
|
|
|
|
</t-form-item> |
|
|
</t-form> |
|
|
</t-form> |
|
|
|
|
|
|
|
|
<div class="detail-footer"> |
|
|
<div class="detail-footer"> |
|
|
@ -130,7 +164,8 @@ |
|
|
<script setup lang="ts"> |
|
|
<script setup lang="ts"> |
|
|
import { ref, reactive, computed, onMounted, watch } from 'vue' |
|
|
import { ref, reactive, computed, onMounted, watch } from 'vue' |
|
|
import { useCategoryStore } from '@/stores/category' |
|
|
import { useCategoryStore } from '@/stores/category' |
|
|
import { getAllRoles, createRole, updateRole, deleteRole, updateRoleCategories } from '@/api/role' |
|
|
|
|
|
|
|
|
import { getAllRoles, createRole, updateRole, deleteRole, updateRoleCategories, updateRoleMcpTools } from '@/api/role' |
|
|
|
|
|
import { listAvailableMcpTools } from '@/api/mcp-server' |
|
|
import { toast } from '@/utils/toast' |
|
|
import { toast } from '@/utils/toast' |
|
|
import { useConfirm } from '@/composables/useConfirm' |
|
|
import { useConfirm } from '@/composables/useConfirm' |
|
|
|
|
|
|
|
|
@ -142,6 +177,8 @@ const roles = ref<any[]>([]) |
|
|
const loading = ref(false) |
|
|
const loading = ref(false) |
|
|
const saving = ref(false) |
|
|
const saving = ref(false) |
|
|
const deleting = ref(false) |
|
|
const deleting = ref(false) |
|
|
|
|
|
const mcpServers = ref<any[]>([]) |
|
|
|
|
|
const mcpLoading = ref(false) |
|
|
|
|
|
|
|
|
const mode = ref<'view' | 'create' | 'edit'>('view') |
|
|
const mode = ref<'view' | 'create' | 'edit'>('view') |
|
|
const selectedId = ref<string | null>(null) |
|
|
const selectedId = ref<string | null>(null) |
|
|
@ -152,6 +189,7 @@ const form = reactive({ |
|
|
prompt: '', |
|
|
prompt: '', |
|
|
enabled: true, |
|
|
enabled: true, |
|
|
categoryIds: [] as string[], |
|
|
categoryIds: [] as string[], |
|
|
|
|
|
allowedMcpTools: [] as string[], |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
// ---- 脏检测快照(ref 保证响应式隔离) ---- |
|
|
// ---- 脏检测快照(ref 保证响应式隔离) ---- |
|
|
@ -164,6 +202,7 @@ function takeSnapshot(): string { |
|
|
prompt: form.prompt, |
|
|
prompt: form.prompt, |
|
|
enabled: form.enabled, |
|
|
enabled: form.enabled, |
|
|
categoryIds: [...form.categoryIds].sort(), |
|
|
categoryIds: [...form.categoryIds].sort(), |
|
|
|
|
|
allowedMcpTools: [...form.allowedMcpTools].sort(), |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -173,6 +212,7 @@ function resetForm(): void { |
|
|
form.prompt = '' |
|
|
form.prompt = '' |
|
|
form.enabled = true |
|
|
form.enabled = true |
|
|
form.categoryIds = [] |
|
|
form.categoryIds = [] |
|
|
|
|
|
form.allowedMcpTools = [] |
|
|
snapshot.value = takeSnapshot() |
|
|
snapshot.value = takeSnapshot() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -182,6 +222,7 @@ function setFormFromRole(role: any): void { |
|
|
form.prompt = role.prompt || '' |
|
|
form.prompt = role.prompt || '' |
|
|
form.enabled = role.enabled !== false |
|
|
form.enabled = role.enabled !== false |
|
|
form.categoryIds = [...(role.categoryIds || [])] |
|
|
form.categoryIds = [...(role.categoryIds || [])] |
|
|
|
|
|
form.allowedMcpTools = [...(role.allowedMcpTools || [])] |
|
|
snapshot.value = takeSnapshot() |
|
|
snapshot.value = takeSnapshot() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -189,11 +230,17 @@ const isDirty = computed(() => takeSnapshot() !== snapshot.value) |
|
|
|
|
|
|
|
|
// ---- 计算属性 ---- |
|
|
// ---- 计算属性 ---- |
|
|
const selectedRole = computed(() => roles.value.find(r => r.id === selectedId.value) ?? null) |
|
|
const selectedRole = computed(() => roles.value.find(r => r.id === selectedId.value) ?? null) |
|
|
|
|
|
// 「全部授权」复选框:以 ["*"] 表示全部,取消则清空 |
|
|
|
|
|
const allToolsSelected = computed<boolean>({ |
|
|
|
|
|
get: () => form.allowedMcpTools.includes('*'), |
|
|
|
|
|
set: (val) => { form.allowedMcpTools = val ? ['*'] : [] }, |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
// ---- 初始化 ---- |
|
|
// ---- 初始化 ---- |
|
|
onMounted(() => { |
|
|
onMounted(() => { |
|
|
reload() |
|
|
reload() |
|
|
categoryStore.loadCategories() |
|
|
categoryStore.loadCategories() |
|
|
|
|
|
loadMcpTools() |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
// ---- 监听 selectedRole 变化:被他人删除时自动回退 ---- |
|
|
// ---- 监听 selectedRole 变化:被他人删除时自动回退 ---- |
|
|
@ -233,6 +280,23 @@ async function reload(): Promise<void> { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ---- 加载可用 MCP 工具 ---- |
|
|
|
|
|
async function loadMcpTools(): Promise<void> { |
|
|
|
|
|
mcpLoading.value = true |
|
|
|
|
|
try { |
|
|
|
|
|
const r = await listAvailableMcpTools() |
|
|
|
|
|
if (r.success) { |
|
|
|
|
|
mcpServers.value = r.data || [] |
|
|
|
|
|
} else { |
|
|
|
|
|
toast('加载 MCP 工具列表失败:' + (r.message || '未知错误'), 'error') |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e: any) { |
|
|
|
|
|
toast('加载 MCP 工具列表失败:' + e.message, 'error') |
|
|
|
|
|
} finally { |
|
|
|
|
|
mcpLoading.value = false |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// ---- 刷新(带脏检测守卫) ---- |
|
|
// ---- 刷新(带脏检测守卫) ---- |
|
|
async function handleRefresh(): Promise<void> { |
|
|
async function handleRefresh(): Promise<void> { |
|
|
if (!(await guardDirty('刷新'))) return |
|
|
if (!(await guardDirty('刷新'))) return |
|
|
@ -326,6 +390,11 @@ async function handleSubmit(): Promise<void> { |
|
|
if (!catRes.success) { |
|
|
if (!catRes.success) { |
|
|
toast(catRes.message || '保存成功,但知识库授权失败', 'error') |
|
|
toast(catRes.message || '保存成功,但知识库授权失败', 'error') |
|
|
} |
|
|
} |
|
|
|
|
|
// 提交 MCP 工具授权 |
|
|
|
|
|
const mcpRes = await updateRoleMcpTools(roleId, [...form.allowedMcpTools]) |
|
|
|
|
|
if (!mcpRes.success) { |
|
|
|
|
|
toast(mcpRes.message || '保存成功,但 MCP 工具授权失败', 'error') |
|
|
|
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
toast('保存成功,但无法获取角色 ID,知识库授权未提交', 'warning') |
|
|
toast('保存成功,但无法获取角色 ID,知识库授权未提交', 'warning') |
|
|
} |
|
|
} |
|
|
@ -517,6 +586,44 @@ async function handleDelete(): Promise<void> { |
|
|
border-top: 1px solid var(--td-border-level-1-color); |
|
|
border-top: 1px solid var(--td-border-level-1-color); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* ===== MCP 工具授权 ===== */ |
|
|
|
|
|
.mcp-tools-box { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mcp-tools-list { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
gap: 8px; |
|
|
|
|
|
margin-top: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mcp-server-group { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
gap: 4px; |
|
|
|
|
|
padding: 8px 10px; |
|
|
|
|
|
background: var(--td-bg-color-page); |
|
|
|
|
|
border-radius: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mcp-server-name { |
|
|
|
|
|
font-size: 12px; |
|
|
|
|
|
font-weight: 600; |
|
|
|
|
|
color: var(--td-text-color-secondary); |
|
|
|
|
|
margin-bottom: 2px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mcp-tool-label { |
|
|
|
|
|
font-weight: 500; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mcp-tool-desc { |
|
|
|
|
|
font-size: 12px; |
|
|
|
|
|
color: var(--td-text-color-placeholder); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/* ===== 响应式 ===== */ |
|
|
/* ===== 响应式 ===== */ |
|
|
@media (max-width: 1024px) { |
|
|
@media (max-width: 1024px) { |
|
|
.role-sidebar { |
|
|
.role-sidebar { |
|
|
|