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