feat(web): 合并知识库导航入口并调整视觉细节
This commit is contained in:
parent
a62077b693
commit
2a52b368f0
@ -31,6 +31,9 @@
|
||||
### 0.6.1
|
||||
|
||||
<!-- 0.6.1 的内容请放在这里 -->
|
||||
- 合并知识库导航入口:左侧导航仅保留“知识库”,文档知识库与图知识库在页面 header 中通过同一组轻量切换入口切换,保留原有列表与图谱内容区交互。
|
||||
- 抽象页面轻量切换 header:知识库与扩展管理页直接共用 `ViewSwitchHeader`,通过统一切换样式和 actions slot 收敛文档知识库、知识图谱、Tools、MCP、Subagents、Skills 等入口的信息层级;扩展管理各列表的刷新入口下沉到搜索框右侧,并统一搜索框与工具按钮的边框和圆角。
|
||||
- 调整任务中心交互:入口移动到 GitHub 按钮下方,并将右侧抽屉展示改为居中弹窗,减少对主页面布局的占用。
|
||||
- 调整 backend Python 工作区依赖边界:将 `backend/package/yuxi` 明确为承载核心运行依赖的业务包,根 `backend/pyproject.toml` 仅保留工作区入口与开发/测试配置,减少依赖职责混淆。
|
||||
- 将 `yuxi` 从 uv workspace 成员调整为 `backend/package` 下可独立构建的本地 Python 包,backend 通过 path dependency 以已安装包形式发现依赖,移除对 `PYTHONPATH=/app/package` 的运行时耦合。
|
||||
- 修复沙盒 `workspace` 隔离粒度:宿主机目录从共享 `saves/threads/shared/workspace` 收敛为用户级 `saves/threads/shared/<user_id>/workspace`,并同步传递 `user_id` 到 sandbox 路径解析、provisioner 挂载与 viewer/chat 测试,保证同用户跨线程共享、不同用户隔离。
|
||||
|
||||
@ -41,15 +41,11 @@
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding: 8px 12px 0;
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
.search-box :deep(.ant-input-affix-wrapper) {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
// border: none;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
background-color: var(--gray-0);
|
||||
box-shadow: none;
|
||||
@ -57,7 +53,27 @@
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.ant-input-affix-wrapper-focused {
|
||||
// border: none;
|
||||
border-color: var(--gray-200);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding: 8px 12px 0;
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
background-color: var(--gray-0);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.ant-input-affix-wrapper-focused {
|
||||
border-color: var(--gray-200);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@ -84,9 +100,9 @@
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
// :deep(.ant-input-outlined) {
|
||||
// border: none;
|
||||
// }
|
||||
:deep(.ant-input-outlined) {
|
||||
border-color: var(--gray-150);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-tool {
|
||||
@ -145,8 +161,7 @@
|
||||
|
||||
&.active {
|
||||
background-color: var(--gray-0);
|
||||
border-color: @border-color;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
border-color: var(--main-500);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
@ -212,7 +227,7 @@
|
||||
}
|
||||
|
||||
.list-section-title {
|
||||
padding: 10px 14px 6px;
|
||||
padding: 0px 14px;
|
||||
color: var(--gray-500);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
|
||||
@ -588,7 +588,7 @@ const configSwitchOptions = computed(() => {
|
||||
|
||||
if (userStore.isAdmin) {
|
||||
options.push({
|
||||
label: '新建配置',
|
||||
label: '+ 新建配置',
|
||||
value: CREATE_CONFIG_OPTION_VALUE
|
||||
})
|
||||
}
|
||||
|
||||
@ -2,12 +2,25 @@
|
||||
<div ref="panelRef" class="agent-panel" :class="{ resizing: isResizing }">
|
||||
<!-- 拖拽手柄 -->
|
||||
<div class="resize-handle" @pointerdown="startResize"></div>
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">
|
||||
<FolderCode :size="20" class="header-icon" />
|
||||
<span><strong>文件系统</strong></span>
|
||||
<div class="panel-header" :class="{ 'is-compact': isCompactHeader }">
|
||||
<div class="panel-header-main">
|
||||
<div class="panel-title">
|
||||
<span><strong>文件系统</strong></span>
|
||||
</div>
|
||||
<div class="window-actions">
|
||||
<button
|
||||
class="header-action-btn"
|
||||
:title="isExpanded ? '恢复高度' : '向上展开'"
|
||||
@click="emit('toggle-expand')"
|
||||
>
|
||||
<component :is="isExpanded ? ChevronsDownUp : ChevronsUpDown" :size="15" />
|
||||
</button>
|
||||
<button class="close-btn" @click="$emit('close')">
|
||||
<X :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="file-toolbar">
|
||||
<button
|
||||
class="header-action-btn"
|
||||
title="新建文件夹"
|
||||
@ -27,17 +40,6 @@
|
||||
<button class="header-action-btn" title="刷新" @click="emitRefresh">
|
||||
<RefreshCw :size="15" />
|
||||
</button>
|
||||
<span class="header-divider"></span>
|
||||
<button
|
||||
class="header-action-btn"
|
||||
:title="isExpanded ? '恢复高度' : '向上展开'"
|
||||
@click="emit('toggle-expand')"
|
||||
>
|
||||
<component :is="isExpanded ? ChevronsDownUp : ChevronsUpDown" :size="15" />
|
||||
</button>
|
||||
<button class="close-btn" @click="$emit('close')">
|
||||
<X :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
@ -166,7 +168,6 @@ import {
|
||||
ChevronsDownUp,
|
||||
ChevronsUpDown,
|
||||
Download,
|
||||
FolderCode,
|
||||
FolderPlus,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
@ -239,6 +240,7 @@ const expandedKeys = ref([])
|
||||
const deletingPaths = ref(new Set())
|
||||
|
||||
const useInlinePreview = computed(() => panelWidth.value >= INLINE_PREVIEW_MIN_WIDTH)
|
||||
const isCompactHeader = computed(() => panelWidth.value > 0 && panelWidth.value < 360)
|
||||
|
||||
const buildDisplayName = (fullPath) => {
|
||||
const normalized = String(fullPath || '').replace(/\/+$/, '')
|
||||
@ -866,10 +868,41 @@ watch(useInlinePreview, (isInline) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 4px 16px;
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
background: var(--gray-25);
|
||||
flex-shrink: 0;
|
||||
|
||||
&.is-compact {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
|
||||
.panel-header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-toolbar {
|
||||
order: 2;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
padding: 4px;
|
||||
border-right: none;
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
background: var(--gray-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-header-main {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
@ -902,26 +935,41 @@ watch(useInlinePreview, (isInline) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
order: 1;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--gray-900);
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
.file-toolbar,
|
||||
.window-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.header-divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: var(--gray-300);
|
||||
margin: 0 4px;
|
||||
.file-toolbar {
|
||||
order: 2;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid var(--gray-300);
|
||||
}
|
||||
|
||||
.window-actions {
|
||||
order: 3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hidden-file-input {
|
||||
|
||||
@ -43,7 +43,8 @@ defineProps({
|
||||
.header-container {
|
||||
background-color: var(--bg-sider);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 10px 24px;
|
||||
padding: 8px 16px;
|
||||
height: 50px;
|
||||
border-bottom: 1px solid var(--gray-150);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@ -52,6 +53,8 @@ defineProps({
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
@ -71,7 +74,7 @@ defineProps({
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--gray-2000);
|
||||
}
|
||||
|
||||
@ -7,15 +7,23 @@
|
||||
<!-- 左侧:MCP 列表 -->
|
||||
<div class="sidebar-list">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索 MCP..."
|
||||
allow-clear
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix><Search :size="14" class="text-muted" /></template>
|
||||
</a-input>
|
||||
<div class="sidebar-toolbar">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索 MCP..."
|
||||
allow-clear
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix><Search :size="14" class="text-muted" /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<a-tooltip title="刷新 MCP">
|
||||
<a-button class="sidebar-tool" :disabled="loading" @click="fetchServers">
|
||||
<RotateCw :size="14" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
|
||||
@ -6,15 +6,23 @@
|
||||
<div class="layout-wrapper" :class="{ 'content-loading': loading }">
|
||||
<!-- 左侧:技能列表 -->
|
||||
<div class="sidebar-list">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索技能..."
|
||||
allow-clear
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix><Search :size="14" class="text-muted" /></template>
|
||||
</a-input>
|
||||
<div class="sidebar-toolbar">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索技能..."
|
||||
allow-clear
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix><Search :size="14" class="text-muted" /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<a-tooltip title="刷新 Skills">
|
||||
<a-button class="sidebar-tool" :disabled="loading" @click="fetchSkills">
|
||||
<RotateCw :size="14" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="list-container">
|
||||
@ -1211,20 +1219,17 @@ defineExpose({
|
||||
.tree-container {
|
||||
width: 240px;
|
||||
border-right: 1px solid @border-color;
|
||||
background-color: @bg-secondary;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
|
||||
.tree-header {
|
||||
padding: 10px 16px;
|
||||
padding: 10px 12px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid @border-color;
|
||||
background-color: var(--gray-50);
|
||||
.label {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-500);
|
||||
text-transform: uppercase;
|
||||
@ -1251,7 +1256,8 @@ defineExpose({
|
||||
.tree-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1264,8 +1270,7 @@ defineExpose({
|
||||
min-height: 0;
|
||||
|
||||
.editor-header {
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid @border-color;
|
||||
padding: 8px 16px 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@ -1327,7 +1332,7 @@ defineExpose({
|
||||
height: 100%;
|
||||
border: none;
|
||||
resize: none;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
font-family: 'Fira Code', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
|
||||
@ -7,15 +7,23 @@
|
||||
<!-- 左侧:SubAgent 列表 -->
|
||||
<div class="sidebar-list">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索 SubAgent..."
|
||||
allow-clear
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix><Search :size="14" class="text-muted" /></template>
|
||||
</a-input>
|
||||
<div class="sidebar-toolbar">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索 SubAgent..."
|
||||
allow-clear
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix><Search :size="14" class="text-muted" /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<a-tooltip title="刷新 Subagents">
|
||||
<a-button class="sidebar-tool" :disabled="loading" @click="fetchSubAgents">
|
||||
<RotateCw :size="14" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- SubAgent 列表 -->
|
||||
@ -298,6 +306,7 @@ import {
|
||||
Bot,
|
||||
Pencil,
|
||||
Trash2,
|
||||
RotateCw,
|
||||
Info,
|
||||
MessageSquare,
|
||||
FileText,
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<a-drawer :open="isOpen" :width="620" title="任务中心" placement="right" @close="handleClose">
|
||||
<a-modal
|
||||
:open="isOpen"
|
||||
title="任务中心"
|
||||
:width="680"
|
||||
:footer="null"
|
||||
:destroy-on-close="false"
|
||||
class="task-center-modal"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<p>提醒:任务执行成功,只代表任务已执行完成,但是任务内部可能有问题已捕获,请注意观察日志。</p>
|
||||
<div class="task-center">
|
||||
<div class="task-toolbar">
|
||||
@ -37,8 +45,8 @@
|
||||
<div class="task-card-info">
|
||||
<div class="task-card-title">{{ task.name }}</div>
|
||||
<div class="task-card-meta">
|
||||
<span class="task-card-id">#{{ formatTaskId(task.id) }}</span>
|
||||
<span class="task-card-type">{{ taskTypeLabel(task.type) }}</span>
|
||||
<span class="task-card-id">#{{ formatTaskId(task.id) }}</span>
|
||||
<span v-if="getTaskDuration(task)" class="task-card-duration">{{
|
||||
getTaskDuration(task)
|
||||
}}</span>
|
||||
@ -107,7 +115,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -349,7 +357,9 @@ function canCancel(task) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
max-height: min(70vh, 720px);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-toolbar {
|
||||
@ -389,16 +399,20 @@ function canCancel(task) {
|
||||
}
|
||||
|
||||
.task-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: var(--gray-0);
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
padding: 12px 16px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -519,10 +533,7 @@ function canCancel(task) {
|
||||
}
|
||||
|
||||
.task-card-type {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--gray-100);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.task-card-duration {
|
||||
@ -586,7 +597,7 @@ function canCancel(task) {
|
||||
}
|
||||
|
||||
.task-card-actions :deep(.ant-btn) {
|
||||
height: 28px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
color: var(--gray-500);
|
||||
|
||||
@ -18,6 +18,12 @@
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<a-tooltip title="刷新工具">
|
||||
<a-button class="sidebar-tool" :disabled="loading" @click="fetchTools">
|
||||
<RotateCw :size="14" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip :title="`当前分类:${currentCategoryLabel}`">
|
||||
<a-dropdown trigger="click">
|
||||
<a-button
|
||||
@ -160,7 +166,16 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { Search, Wrench, Tag, Tags, FileText, List, SlidersHorizontal } from 'lucide-vue-next'
|
||||
import {
|
||||
Search,
|
||||
Wrench,
|
||||
Tag,
|
||||
Tags,
|
||||
FileText,
|
||||
List,
|
||||
SlidersHorizontal,
|
||||
RotateCw
|
||||
} from 'lucide-vue-next'
|
||||
import { toolApi } from '@/apis/tool_api'
|
||||
import { getToolIcon } from '@/components/ToolCallingResult/toolRegistry'
|
||||
|
||||
|
||||
109
web/src/components/ViewSwitchHeader.vue
Normal file
109
web/src/components/ViewSwitchHeader.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<HeaderComponent :title="title" :loading="loading">
|
||||
<template #behind-title>
|
||||
<nav class="view-switcher" :aria-label="ariaLabel">
|
||||
<slot name="options" :items="items" :active-key="activeKey" :select="selectItem">
|
||||
<template v-for="item in items" :key="item.key">
|
||||
<RouterLink
|
||||
v-if="item.path"
|
||||
:to="item.path"
|
||||
class="switcher-item"
|
||||
:class="{ active: activeKey === item.key }"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</RouterLink>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="switcher-item"
|
||||
:class="{ active: activeKey === item.key }"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</template>
|
||||
</slot>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<template #actions v-if="$slots.actions">
|
||||
<slot name="actions"></slot>
|
||||
</template>
|
||||
</HeaderComponent>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue'
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
activeKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: '视图切换'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:activeKey', 'change'])
|
||||
|
||||
const selectItem = (item) => {
|
||||
emit('update:activeKey', item.key)
|
||||
emit('change', item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.view-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding-left: 12px;
|
||||
margin-left: 2px;
|
||||
border-left: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.switcher-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--gray-700);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--main-color);
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--main-color);
|
||||
background-color: var(--main-40);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -2,7 +2,7 @@
|
||||
import { ref, reactive, onMounted, computed, provide } from 'vue'
|
||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||
import { GithubOutlined } from '@ant-design/icons-vue'
|
||||
import { Bot, Waypoints, LibraryBig, BarChart3, CircleCheck, Blocks } from 'lucide-vue-next'
|
||||
import { Bot, LibraryBig, BarChart3, ClipboardList, Blocks } from 'lucide-vue-next'
|
||||
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useDatabaseStore } from '@/stores/database'
|
||||
@ -113,20 +113,13 @@ const mainList = computed(() => {
|
||||
|
||||
if (userStore.isAdmin) {
|
||||
if (!isLiteMode) {
|
||||
items.push(
|
||||
{
|
||||
name: '图谱',
|
||||
path: '/graph',
|
||||
icon: Waypoints,
|
||||
activeIcon: Waypoints
|
||||
},
|
||||
{
|
||||
name: '知识库',
|
||||
path: '/database',
|
||||
icon: LibraryBig,
|
||||
activeIcon: LibraryBig
|
||||
}
|
||||
)
|
||||
items.push({
|
||||
name: '知识库',
|
||||
path: '/database',
|
||||
activePaths: ['/database', '/graph'],
|
||||
icon: LibraryBig,
|
||||
activeIcon: LibraryBig
|
||||
})
|
||||
}
|
||||
|
||||
if (userStore.isSuperAdmin) {
|
||||
@ -149,6 +142,11 @@ const mainList = computed(() => {
|
||||
return items
|
||||
})
|
||||
|
||||
const isNavItemActive = (item) => {
|
||||
const activePaths = item.activePaths || [item.path]
|
||||
return activePaths.some((path) => route.path === path || route.path.startsWith(`${path}/`))
|
||||
}
|
||||
|
||||
// Provide settings modal methods to child components
|
||||
provide('settingsModal', {
|
||||
openSettingsModal
|
||||
@ -171,35 +169,18 @@ provide('settingsModal', {
|
||||
:to="item.path"
|
||||
v-show="!item.hidden"
|
||||
class="nav-item"
|
||||
:class="{ active: isNavItemActive(item) }"
|
||||
active-class="active"
|
||||
>
|
||||
<a-tooltip placement="right">
|
||||
<template #title>{{ item.name }}</template>
|
||||
<component
|
||||
class="icon"
|
||||
:is="route.path.startsWith(item.path) ? item.activeIcon : item.icon"
|
||||
:is="isNavItemActive(item) ? item.activeIcon : item.icon"
|
||||
size="22"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</RouterLink>
|
||||
<div
|
||||
v-if="userStore.isAdmin"
|
||||
class="nav-item task-center"
|
||||
:class="{ active: isDrawerOpen }"
|
||||
@click="taskerStore.openDrawer()"
|
||||
>
|
||||
<a-tooltip placement="right">
|
||||
<template #title>任务中心</template>
|
||||
<a-badge
|
||||
:count="activeTaskCount"
|
||||
:overflow-count="99"
|
||||
class="task-center-badge"
|
||||
size="small"
|
||||
>
|
||||
<CircleCheck class="icon" size="22" />
|
||||
</a-badge>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<div class="github nav-item">
|
||||
@ -213,6 +194,24 @@ provide('settingsModal', {
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-if="userStore.isAdmin"
|
||||
class="nav-item task-center"
|
||||
:class="{ active: isDrawerOpen }"
|
||||
@click="taskerStore.openDrawer()"
|
||||
>
|
||||
<a-tooltip placement="right">
|
||||
<template #title>任务中心</template>
|
||||
<a-badge
|
||||
:count="activeTaskCount"
|
||||
:overflow-count="99"
|
||||
class="task-center-badge"
|
||||
size="small"
|
||||
>
|
||||
<ClipboardList class="icon" size="22" />
|
||||
</a-badge>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<!-- 用户信息组件 -->
|
||||
<div class="nav-item user-info">
|
||||
<UserInfoComponent />
|
||||
@ -392,6 +391,8 @@ div.header,
|
||||
display: none;
|
||||
}
|
||||
&.task-center {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.task-center-badge {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@ -539,6 +540,10 @@ div.header,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.task-center {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
<template>
|
||||
<div class="database-container layout-container">
|
||||
<HeaderComponent title="文档知识库" :loading="dbState.listLoading">
|
||||
<ViewSwitchHeader
|
||||
title="知识库"
|
||||
:active-key="knowledgeActiveView"
|
||||
:items="knowledgeViewItems"
|
||||
:loading="dbState.listLoading"
|
||||
aria-label="知识库视图切换"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="state.openNewDatabaseModel = true"> 新建知识库 </a-button>
|
||||
</template>
|
||||
</HeaderComponent>
|
||||
</ViewSwitchHeader>
|
||||
|
||||
<a-modal
|
||||
:open="state.openNewDatabaseModel"
|
||||
@ -240,7 +246,7 @@ import { useDatabaseStore } from '@/stores/database'
|
||||
import { LockOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { typeApi } from '@/apis/knowledge_api'
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue'
|
||||
import ViewSwitchHeader from '@/components/ViewSwitchHeader.vue'
|
||||
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
|
||||
import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue'
|
||||
import ShareConfigForm from '@/components/ShareConfigForm.vue'
|
||||
@ -257,6 +263,12 @@ const databaseStore = useDatabaseStore()
|
||||
// 使用 store 的状态
|
||||
const { databases, state: dbState } = storeToRefs(databaseStore)
|
||||
|
||||
const knowledgeActiveView = 'documents'
|
||||
const knowledgeViewItems = [
|
||||
{ key: 'documents', label: '文档知识库', path: '/database' },
|
||||
{ key: 'graph', label: '知识图谱', path: '/graph' }
|
||||
]
|
||||
|
||||
const state = reactive({
|
||||
openNewDatabaseModel: false
|
||||
})
|
||||
@ -668,7 +680,7 @@ onMounted(() => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.databases {
|
||||
padding: 20px;
|
||||
padding: 12px 16px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
@ -691,9 +703,9 @@ onMounted(() => {
|
||||
.dbcard,
|
||||
.database {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
height: 156px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
height: 140px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -721,9 +733,9 @@ onMounted(() => {
|
||||
margin-bottom: 14px;
|
||||
|
||||
.icon {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
font-size: 26px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 24px;
|
||||
margin-right: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -746,7 +758,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 17px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.4;
|
||||
|
||||
@ -1,96 +1,66 @@
|
||||
<template>
|
||||
<div class="extensions-view extension-page-root">
|
||||
<div class="extensions-header">
|
||||
<a-tabs v-model:activeKey="activeTab" class="extensions-tabs">
|
||||
<a-tab-pane key="tools" tab="工具" />
|
||||
<a-tab-pane key="mcp" tab="MCP 管理" />
|
||||
<a-tab-pane key="subagents" tab="Subagents 管理" />
|
||||
<a-tab-pane key="skills" tab="Skills 管理" />
|
||||
</a-tabs>
|
||||
<div class="header-actions">
|
||||
<!-- Skills Tab 的按钮 -->
|
||||
<template v-if="activeTab === 'skills'">
|
||||
<a-button
|
||||
@click="handleOpenRemoteInstall"
|
||||
:disabled="skillsLoading || skillsImporting"
|
||||
class="lucide-icon-btn"
|
||||
>
|
||||
<Computer :size="14" />
|
||||
<span>远程安装</span>
|
||||
</a-button>
|
||||
<a-upload
|
||||
accept=".zip,.md"
|
||||
:show-upload-list="false"
|
||||
:custom-request="handleImportUpload"
|
||||
:before-upload="beforeSkillUpload"
|
||||
:disabled="skillsLoading || skillsImporting"
|
||||
>
|
||||
<a-button type="primary" :loading="skillsImporting" class="lucide-icon-btn">
|
||||
<Upload :size="14" />
|
||||
<span>上传 Skill</span>
|
||||
<ViewSwitchHeader
|
||||
v-model:active-key="activeTab"
|
||||
title="扩展管理"
|
||||
:items="extensionTabs"
|
||||
aria-label="扩展管理视图切换"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="extension-header-actions">
|
||||
<!-- Skills Tab 的按钮 -->
|
||||
<template v-if="activeTab === 'skills'">
|
||||
<a-button
|
||||
@click="handleOpenRemoteInstall"
|
||||
:disabled="skillsLoading || skillsImporting"
|
||||
class="lucide-icon-btn"
|
||||
>
|
||||
<Computer :size="14" />
|
||||
<span>远程安装</span>
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<a-button @click="handleSkillsRefresh" :disabled="skillsLoading" class="lucide-icon-btn">
|
||||
<RotateCw :size="14" />
|
||||
<span>刷新</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- Tools Tab 的按钮 -->
|
||||
<template v-else-if="activeTab === 'tools'">
|
||||
<a-button @click="handleToolsRefresh" :disabled="toolsLoading" class="lucide-icon-btn">
|
||||
<RotateCw :size="14" />
|
||||
<span>刷新</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- MCP Tab 的按钮 -->
|
||||
<template v-else-if="activeTab === 'mcp'">
|
||||
<a-button type="primary" @click="handleMcpAdd" class="lucide-icon-btn">
|
||||
<Plus :size="14" />
|
||||
<span>添加 MCP</span>
|
||||
</a-button>
|
||||
<a-button @click="handleMcpRefresh" :disabled="mcpLoading" class="lucide-icon-btn">
|
||||
<RotateCw :size="14" />
|
||||
<span>刷新</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- Subagents Tab 的按钮 -->
|
||||
<template v-else-if="activeTab === 'subagents'">
|
||||
<a-button type="primary" @click="handleSubagentAdd" class="lucide-icon-btn">
|
||||
<Plus :size="14" />
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
@click="handleSubagentRefresh"
|
||||
:disabled="subagentsLoading"
|
||||
class="lucide-icon-btn"
|
||||
>
|
||||
<RotateCw :size="14" />
|
||||
<span>刷新</span>
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<a-upload
|
||||
accept=".zip,.md"
|
||||
:show-upload-list="false"
|
||||
:custom-request="handleImportUpload"
|
||||
:before-upload="beforeSkillUpload"
|
||||
:disabled="skillsLoading || skillsImporting"
|
||||
>
|
||||
<a-button type="primary" :loading="skillsImporting" class="lucide-icon-btn">
|
||||
<Upload :size="14" />
|
||||
<span>上传 Skill</span>
|
||||
</a-button>
|
||||
</a-upload>
|
||||
</template>
|
||||
<!-- MCP Tab 的按钮 -->
|
||||
<template v-else-if="activeTab === 'mcp'">
|
||||
<a-button type="primary" @click="handleMcpAdd" class="lucide-icon-btn">
|
||||
<Plus :size="14" />
|
||||
<span>添加 MCP</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- Subagents Tab 的按钮 -->
|
||||
<template v-else-if="activeTab === 'subagents'">
|
||||
<a-button type="primary" @click="handleSubagentAdd" class="lucide-icon-btn">
|
||||
<Plus :size="14" />
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</ViewSwitchHeader>
|
||||
|
||||
<div class="extensions-content">
|
||||
<div v-show="activeTab === 'tools'" class="tab-panel">
|
||||
<ToolsManagerComponent ref="toolsRef" @refresh="handleToolsRefresh" />
|
||||
<ToolsManagerComponent />
|
||||
</div>
|
||||
<div v-show="activeTab === 'skills'" class="tab-panel">
|
||||
<SkillsManagerComponent
|
||||
ref="skillsRef"
|
||||
@import="handleSkillsImport"
|
||||
@refresh="handleSkillsRefresh"
|
||||
/>
|
||||
<SkillsManagerComponent ref="skillsRef" @import="handleSkillsImport" />
|
||||
</div>
|
||||
<div v-show="activeTab === 'mcp'" class="tab-panel">
|
||||
<McpServersComponent ref="mcpRef" @add="handleMcpAdd" @refresh="handleMcpRefresh" />
|
||||
<McpServersComponent ref="mcpRef" @add="handleMcpAdd" />
|
||||
</div>
|
||||
<div v-show="activeTab === 'subagents'" class="tab-panel">
|
||||
<SubAgentsComponent
|
||||
ref="subagentsRef"
|
||||
@add="handleSubagentAdd"
|
||||
@refresh="handleSubagentRefresh"
|
||||
/>
|
||||
<SubAgentsComponent ref="subagentsRef" @add="handleSubagentAdd" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -100,16 +70,24 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { Upload, RotateCw, Plus, Computer } from 'lucide-vue-next'
|
||||
import { Upload, Plus, Computer } from 'lucide-vue-next'
|
||||
import SkillsManagerComponent from '@/components/SkillsManagerComponent.vue'
|
||||
import ToolsManagerComponent from '@/components/ToolsManagerComponent.vue'
|
||||
import McpServersComponent from '@/components/McpServersComponent.vue'
|
||||
import SubAgentsComponent from '@/components/SubAgentsComponent.vue'
|
||||
import ViewSwitchHeader from '@/components/ViewSwitchHeader.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const activeTab = ref('tools')
|
||||
const skillsRef = ref(null)
|
||||
|
||||
const extensionTabs = [
|
||||
{ key: 'tools', label: '工具' },
|
||||
{ key: 'mcp', label: 'MCP' },
|
||||
{ key: 'subagents', label: 'Subagents' },
|
||||
{ key: 'skills', label: 'Skills' }
|
||||
]
|
||||
|
||||
// 监听路由 query 参数变化
|
||||
watch(
|
||||
() => route.query,
|
||||
@ -120,16 +98,12 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
const toolsRef = ref(null)
|
||||
const mcpRef = ref(null)
|
||||
const subagentsRef = ref(null)
|
||||
|
||||
// Skills 相关状态(从子组件透传)
|
||||
const skillsLoading = ref(false)
|
||||
const skillsImporting = ref(false)
|
||||
const toolsLoading = ref(false)
|
||||
const mcpLoading = ref(false)
|
||||
const subagentsLoading = ref(false)
|
||||
|
||||
// 暴露给子组件的状态更新
|
||||
const updateSkillsState = (loading, importing) => {
|
||||
@ -137,18 +111,6 @@ const updateSkillsState = (loading, importing) => {
|
||||
skillsImporting.value = importing
|
||||
}
|
||||
|
||||
const updateToolsState = (loading) => {
|
||||
toolsLoading.value = loading
|
||||
}
|
||||
|
||||
const updateMcpState = (loading) => {
|
||||
mcpLoading.value = loading
|
||||
}
|
||||
|
||||
const updateSubagentsState = (loading) => {
|
||||
subagentsLoading.value = loading
|
||||
}
|
||||
|
||||
// Skills 事件处理
|
||||
const handleSkillsImport = () => {
|
||||
// 导入完成后自动刷新
|
||||
@ -170,16 +132,6 @@ const handleOpenRemoteInstall = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Tools 事件处理
|
||||
const handleToolsRefresh = () => {
|
||||
if (toolsRef.value?.fetchTools) {
|
||||
updateToolsState(true)
|
||||
toolsRef.value.fetchTools().finally(() => {
|
||||
updateToolsState(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MCP 事件处理
|
||||
const handleMcpAdd = () => {
|
||||
if (mcpRef.value?.showAddModal) {
|
||||
@ -187,15 +139,6 @@ const handleMcpAdd = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleMcpRefresh = () => {
|
||||
if (mcpRef.value?.fetchServers) {
|
||||
updateMcpState(true)
|
||||
mcpRef.value.fetchServers().finally(() => {
|
||||
updateMcpState(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Subagents 事件处理
|
||||
const handleSubagentAdd = () => {
|
||||
if (subagentsRef.value?.showAddModal) {
|
||||
@ -203,15 +146,6 @@ const handleSubagentAdd = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubagentRefresh = () => {
|
||||
if (subagentsRef.value?.fetchSubAgents) {
|
||||
updateSubagentsState(true)
|
||||
subagentsRef.value.fetchSubAgents().finally(() => {
|
||||
updateSubagentsState(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 上传前校验文件名:仅允许 .zip 或 SKILL.md
|
||||
const beforeSkillUpload = (file) => {
|
||||
const lower = file.name.toLowerCase()
|
||||
@ -242,57 +176,9 @@ const handleImportUpload = async ({ file, onSuccess, onError }) => {
|
||||
@import '@/assets/css/extensions.less';
|
||||
|
||||
.extensions-view {
|
||||
.extensions-header {
|
||||
.extension-header-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--gray-150);
|
||||
background-color: var(--gray-0);
|
||||
|
||||
.extensions-tabs {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&::before {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav::after) {
|
||||
content: none;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav-left-bar) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-items) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab) {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-ink-bar) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.extensions-content {
|
||||
|
||||
@ -7,7 +7,12 @@
|
||||
</a-empty>
|
||||
</div>
|
||||
<div class="graph-container layout-container" v-else>
|
||||
<HeaderComponent title="图数据库">
|
||||
<ViewSwitchHeader
|
||||
title="知识库"
|
||||
:active-key="knowledgeActiveView"
|
||||
:items="knowledgeViewItems"
|
||||
aria-label="知识库视图切换"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="db-selector">
|
||||
<div class="status-wrapper">
|
||||
@ -43,7 +48,7 @@
|
||||
<SyncOutlined v-if="!state.indexing" /> 为{{ unindexedCount }}个节点添加索引
|
||||
</a-button>
|
||||
</template>
|
||||
</HeaderComponent>
|
||||
</ViewSwitchHeader>
|
||||
|
||||
<div class="container-outter">
|
||||
<GraphCanvas
|
||||
@ -210,7 +215,7 @@ import {
|
||||
DatabaseOutlined,
|
||||
ExportOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue'
|
||||
import ViewSwitchHeader from '@/components/ViewSwitchHeader.vue'
|
||||
import { neo4jApi, unifiedApi } from '@/apis/graph_api'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import GraphCanvas from '@/components/GraphCanvas.vue'
|
||||
@ -220,6 +225,11 @@ import { useGraph } from '@/composables/useGraph'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const cur_embed_model = computed(() => configStore.config?.embed_model)
|
||||
const knowledgeActiveView = 'graph'
|
||||
const knowledgeViewItems = [
|
||||
{ key: 'documents', label: '文档知识库', path: '/database' },
|
||||
{ key: 'graph', label: '知识图谱', path: '/graph' }
|
||||
]
|
||||
const modelMatched = computed(
|
||||
() =>
|
||||
!graphInfo?.value?.embed_model_name ||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user