|
|
@ -1,5 +1,5 @@ |
|
|
<template> |
|
|
<template> |
|
|
<t-card title="🎭 客服角色管理" :bordered="false"> |
|
|
|
|
|
|
|
|
<t-card title="🎭 客服角色管理" :bordered="false" class="role-page-card"> |
|
|
<p class="desc-text">为角色设定人设并授权可检索的知识库分类。对话页只能选择这里配置好的角色,检索范围由服务端按角色强制限定。</p> |
|
|
<p class="desc-text">为角色设定人设并授权可检索的知识库分类。对话页只能选择这里配置好的角色,检索范围由服务端按角色强制限定。</p> |
|
|
|
|
|
|
|
|
<div class="role-shell"> |
|
|
<div class="role-shell"> |
|
|
@ -77,20 +77,20 @@ |
|
|
|
|
|
|
|
|
<t-form label-align="top" class="detail-form"> |
|
|
<t-form label-align="top" class="detail-form"> |
|
|
<t-row :gutter="16"> |
|
|
<t-row :gutter="16"> |
|
|
<t-col :span="8"> |
|
|
|
|
|
|
|
|
<t-col :span="6" :xs="12"> |
|
|
<t-form-item label="角色名称 *"> |
|
|
<t-form-item label="角色名称 *"> |
|
|
<t-input v-model="form.name" :disabled="saving" placeholder="请输入角色名称" /> |
|
|
<t-input v-model="form.name" :disabled="saving" placeholder="请输入角色名称" /> |
|
|
</t-form-item> |
|
|
</t-form-item> |
|
|
</t-col> |
|
|
</t-col> |
|
|
<t-col :span="4"> |
|
|
|
|
|
<t-form-item label="状态"> |
|
|
|
|
|
<t-switch v-model="form.enabled" :disabled="saving" :label="['启用', '停用']" /> |
|
|
|
|
|
|
|
|
<t-col :span="6" :xs="12"> |
|
|
|
|
|
<t-form-item label="职责描述"> |
|
|
|
|
|
<t-input v-model="form.description" :disabled="saving" placeholder="职责描述(可选)" /> |
|
|
</t-form-item> |
|
|
</t-form-item> |
|
|
</t-col> |
|
|
</t-col> |
|
|
</t-row> |
|
|
</t-row> |
|
|
|
|
|
|
|
|
<t-form-item label="职责描述"> |
|
|
|
|
|
<t-input v-model="form.description" :disabled="saving" placeholder="职责描述(可选)" /> |
|
|
|
|
|
|
|
|
<t-form-item label="状态"> |
|
|
|
|
|
<t-switch v-model="form.enabled" :disabled="saving" :label="['启用', '停用']" /> |
|
|
</t-form-item> |
|
|
</t-form-item> |
|
|
|
|
|
|
|
|
<t-form-item label="人设 / 风格提示词"> |
|
|
<t-form-item label="人设 / 风格提示词"> |
|
|
@ -102,6 +102,8 @@ |
|
|
/> |
|
|
/> |
|
|
</t-form-item> |
|
|
</t-form-item> |
|
|
|
|
|
|
|
|
|
|
|
<div class="form-section-title">授权配置</div> |
|
|
|
|
|
|
|
|
<t-form-item label="可检索知识库授权"> |
|
|
<t-form-item label="可检索知识库授权"> |
|
|
<t-checkbox-group v-if="categoryStore.categories.length" v-model="form.categoryIds"> |
|
|
<t-checkbox-group v-if="categoryStore.categories.length" v-model="form.categoryIds"> |
|
|
<t-checkbox |
|
|
<t-checkbox |
|
|
@ -115,6 +117,59 @@ |
|
|
</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"> |
|
|
|
|
|
<template v-if="!allToolsSelected && mcpServers.length"> |
|
|
|
|
|
<div class="mcp-tools-toolbar"> |
|
|
|
|
|
<t-checkbox |
|
|
|
|
|
:checked="allToolsChecked" |
|
|
|
|
|
:indeterminate="allToolsIndeterminate" |
|
|
|
|
|
:disabled="saving" |
|
|
|
|
|
@change="toggleAllTools" |
|
|
|
|
|
> |
|
|
|
|
|
全选工具 |
|
|
|
|
|
</t-checkbox> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="mcp-tools-list"> |
|
|
|
|
|
<div v-for="s in mcpServers" :key="s.configId" class="mcp-server-group"> |
|
|
|
|
|
<div class="mcp-server-header"> |
|
|
|
|
|
<span class="mcp-server-name">{{ s.serverName }}</span> |
|
|
|
|
|
<t-checkbox |
|
|
|
|
|
:checked="isGroupAllChecked(s)" |
|
|
|
|
|
:indeterminate="isGroupIndeterminate(s)" |
|
|
|
|
|
:disabled="saving" |
|
|
|
|
|
@change="(val) => toggleGroupAll(s, val)" |
|
|
|
|
|
> |
|
|
|
|
|
全选 |
|
|
|
|
|
</t-checkbox> |
|
|
|
|
|
</div> |
|
|
|
|
|
<t-checkbox |
|
|
|
|
|
v-for="t in s.tools" |
|
|
|
|
|
:key="t.name" |
|
|
|
|
|
:checked="form.allowedMcpTools.includes(t.name)" |
|
|
|
|
|
:disabled="saving" |
|
|
|
|
|
@change="(val) => toggleTool(t.name, val)" |
|
|
|
|
|
> |
|
|
|
|
|
<span class="mcp-tool-label">{{ t.rawName }}</span> |
|
|
|
|
|
<span v-if="t.description" class="mcp-tool-desc"> — {{ t.description }}</span> |
|
|
|
|
|
</t-checkbox> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</template> |
|
|
|
|
|
<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 +185,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 +198,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 +210,7 @@ const form = reactive({ |
|
|
prompt: '', |
|
|
prompt: '', |
|
|
enabled: true, |
|
|
enabled: true, |
|
|
categoryIds: [] as string[], |
|
|
categoryIds: [] as string[], |
|
|
|
|
|
allowedMcpTools: [] as string[], |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
// ---- 脏检测快照(ref 保证响应式隔离) ---- |
|
|
// ---- 脏检测快照(ref 保证响应式隔离) ---- |
|
|
@ -164,6 +223,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 +233,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 +243,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 +251,93 @@ 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) |
|
|
|
|
|
// 勾选「全部授权」前暂存已选工具,取消时恢复 |
|
|
|
|
|
let lastToolsBeforeAll: string[] = [] |
|
|
|
|
|
|
|
|
|
|
|
// 「全部授权」复选框:以 ["*"] 表示全部,取消则恢复之前的勾选 |
|
|
|
|
|
const allToolsSelected = computed<boolean>({ |
|
|
|
|
|
get: () => form.allowedMcpTools.includes('*'), |
|
|
|
|
|
set: (val) => { |
|
|
|
|
|
if (val) { |
|
|
|
|
|
lastToolsBeforeAll = form.allowedMcpTools.filter(t => t !== '*') |
|
|
|
|
|
form.allowedMcpTools = ['*'] |
|
|
|
|
|
} else { |
|
|
|
|
|
form.allowedMcpTools = [...lastToolsBeforeAll] |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// ---- MCP 工具全选 ---- |
|
|
|
|
|
// 所有工具名(扁平、去重) |
|
|
|
|
|
const allToolNames = computed<string[]>(() => { |
|
|
|
|
|
const names = new Set<string>() |
|
|
|
|
|
for (const s of mcpServers.value) { |
|
|
|
|
|
for (const t of (s.tools || [])) { |
|
|
|
|
|
names.add(t.name) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return [...names] |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// 全局全选:所有工具是否都已勾选 |
|
|
|
|
|
const allToolsChecked = computed<boolean>(() => { |
|
|
|
|
|
const names = allToolNames.value |
|
|
|
|
|
return names.length > 0 && names.every(n => form.allowedMcpTools.includes(n)) |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// 全局半选:部分工具已勾选 |
|
|
|
|
|
const allToolsIndeterminate = computed<boolean>(() => { |
|
|
|
|
|
const names = allToolNames.value |
|
|
|
|
|
const selected = names.filter(n => form.allowedMcpTools.includes(n)).length |
|
|
|
|
|
return selected > 0 && selected < names.length |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
function toggleAllTools(val: boolean): void { |
|
|
|
|
|
if (val) { |
|
|
|
|
|
form.allowedMcpTools = [...new Set([...form.allowedMcpTools, ...allToolNames.value])] |
|
|
|
|
|
} else { |
|
|
|
|
|
const nameSet = new Set(allToolNames.value) |
|
|
|
|
|
form.allowedMcpTools = form.allowedMcpTools.filter(n => !nameSet.has(n)) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 单个 Server 分组是否全选 |
|
|
|
|
|
function isGroupAllChecked(group: any): boolean { |
|
|
|
|
|
const tools = group.tools || [] |
|
|
|
|
|
return tools.length > 0 && tools.every((t: any) => form.allowedMcpTools.includes(t.name)) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 单个 Server 分组是否半选 |
|
|
|
|
|
function isGroupIndeterminate(group: any): boolean { |
|
|
|
|
|
const tools = group.tools || [] |
|
|
|
|
|
const selected = tools.filter((t: any) => form.allowedMcpTools.includes(t.name)).length |
|
|
|
|
|
return selected > 0 && selected < tools.length |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function toggleGroupAll(group: any, val: boolean): void { |
|
|
|
|
|
const names = (group.tools || []).map((t: any) => t.name) |
|
|
|
|
|
if (val) { |
|
|
|
|
|
form.allowedMcpTools = [...new Set([...form.allowedMcpTools, ...names])] |
|
|
|
|
|
} else { |
|
|
|
|
|
const nameSet = new Set(names) |
|
|
|
|
|
form.allowedMcpTools = form.allowedMcpTools.filter(n => !nameSet.has(n)) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 单个工具勾选/取消 |
|
|
|
|
|
function toggleTool(name: string, val: boolean): void { |
|
|
|
|
|
if (val) { |
|
|
|
|
|
form.allowedMcpTools = [...new Set([...form.allowedMcpTools, name])] |
|
|
|
|
|
} else { |
|
|
|
|
|
form.allowedMcpTools = form.allowedMcpTools.filter(n => n !== name) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// ---- 初始化 ---- |
|
|
// ---- 初始化 ---- |
|
|
onMounted(() => { |
|
|
onMounted(() => { |
|
|
reload() |
|
|
reload() |
|
|
categoryStore.loadCategories() |
|
|
categoryStore.loadCategories() |
|
|
|
|
|
loadMcpTools() |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
// ---- 监听 selectedRole 变化:被他人删除时自动回退 ---- |
|
|
// ---- 监听 selectedRole 变化:被他人删除时自动回退 ---- |
|
|
@ -233,6 +377,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 +487,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') |
|
|
} |
|
|
} |
|
|
@ -381,10 +547,16 @@ async function handleDelete(): Promise<void> { |
|
|
</script> |
|
|
</script> |
|
|
|
|
|
|
|
|
<style scoped> |
|
|
<style scoped> |
|
|
|
|
|
/* ===== 页级留白 ===== */ |
|
|
|
|
|
.role-page-card { |
|
|
|
|
|
margin: 16px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/* ===== 整体分栏 ===== */ |
|
|
/* ===== 整体分栏 ===== */ |
|
|
.role-shell { |
|
|
.role-shell { |
|
|
display: flex; |
|
|
display: flex; |
|
|
gap: 16px; |
|
|
gap: 16px; |
|
|
|
|
|
height: calc(100vh - 220px); |
|
|
min-height: 520px; |
|
|
min-height: 520px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -458,8 +630,13 @@ async function handleDelete(): Promise<void> { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.role-item-name { |
|
|
.role-item-name { |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
min-width: 0; |
|
|
font-size: 13px; |
|
|
font-size: 13px; |
|
|
color: var(--td-text-color-primary); |
|
|
color: var(--td-text-color-primary); |
|
|
|
|
|
white-space: nowrap; |
|
|
|
|
|
overflow: hidden; |
|
|
|
|
|
text-overflow: ellipsis; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.role-item-desc { |
|
|
.role-item-desc { |
|
|
@ -479,13 +656,14 @@ async function handleDelete(): Promise<void> { |
|
|
border: 1px solid var(--td-border-level-1-color); |
|
|
border: 1px solid var(--td-border-level-1-color); |
|
|
border-radius: 10px; |
|
|
border-radius: 10px; |
|
|
padding: 20px 24px; |
|
|
padding: 20px 24px; |
|
|
overflow-y: auto; |
|
|
|
|
|
|
|
|
overflow: hidden; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.detail-header { |
|
|
.detail-header { |
|
|
display: flex; |
|
|
display: flex; |
|
|
align-items: flex-start; |
|
|
|
|
|
|
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
justify-content: space-between; |
|
|
|
|
|
flex-shrink: 0; |
|
|
margin-bottom: 20px; |
|
|
margin-bottom: 20px; |
|
|
padding-bottom: 14px; |
|
|
padding-bottom: 14px; |
|
|
border-bottom: 1px solid var(--td-border-level-1-color); |
|
|
border-bottom: 1px solid var(--td-border-level-1-color); |
|
|
@ -506,17 +684,82 @@ async function handleDelete(): Promise<void> { |
|
|
|
|
|
|
|
|
.detail-form { |
|
|
.detail-form { |
|
|
flex: 1; |
|
|
flex: 1; |
|
|
|
|
|
overflow-y: auto; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.detail-footer { |
|
|
.detail-footer { |
|
|
display: flex; |
|
|
display: flex; |
|
|
justify-content: flex-end; |
|
|
justify-content: flex-end; |
|
|
gap: 10px; |
|
|
gap: 10px; |
|
|
|
|
|
flex-shrink: 0; |
|
|
margin-top: 24px; |
|
|
margin-top: 24px; |
|
|
padding-top: 14px; |
|
|
padding-top: 14px; |
|
|
border-top: 1px solid var(--td-border-level-1-color); |
|
|
border-top: 1px solid var(--td-border-level-1-color); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* ===== 表单分区标题 ===== */ |
|
|
|
|
|
.form-section-title { |
|
|
|
|
|
margin: 8px 0 12px; |
|
|
|
|
|
padding-left: 8px; |
|
|
|
|
|
font-size: 13px; |
|
|
|
|
|
font-weight: 600; |
|
|
|
|
|
color: var(--td-text-color-primary); |
|
|
|
|
|
border-left: 3px solid var(--td-brand-color); |
|
|
|
|
|
line-height: 1.4; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* ===== MCP 工具授权 ===== */ |
|
|
|
|
|
.mcp-tools-box { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mcp-tools-toolbar { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
margin-top: 8px; |
|
|
|
|
|
padding-bottom: 8px; |
|
|
|
|
|
border-bottom: 1px dashed var(--td-border-level-1-color); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.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-header { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
justify-content: space-between; |
|
|
|
|
|
margin-bottom: 2px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.mcp-server-name { |
|
|
|
|
|
font-size: 12px; |
|
|
|
|
|
font-weight: 600; |
|
|
|
|
|
color: var(--td-text-color-secondary); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.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 { |
|
|
@ -528,12 +771,22 @@ async function handleDelete(): Promise<void> { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 768px) { |
|
|
@media (max-width: 768px) { |
|
|
|
|
|
.role-page-card { |
|
|
|
|
|
margin: 12px; |
|
|
|
|
|
} |
|
|
.role-shell { |
|
|
.role-shell { |
|
|
flex-direction: column; |
|
|
flex-direction: column; |
|
|
|
|
|
height: auto; |
|
|
} |
|
|
} |
|
|
.role-sidebar { |
|
|
.role-sidebar { |
|
|
width: auto; |
|
|
width: auto; |
|
|
max-height: 260px; |
|
|
max-height: 260px; |
|
|
} |
|
|
} |
|
|
|
|
|
.role-detail { |
|
|
|
|
|
overflow: visible; |
|
|
|
|
|
} |
|
|
|
|
|
.detail-form { |
|
|
|
|
|
overflow: visible; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</style> |