mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
feat: enhance storage engine configuration and UI updates
- Updated `.air.toml` to include additional CGO flags for improved build settings. - Expanded `StorageEngineConfig` interface to support "tos" (火山引擎 TOS) as a new storage provider. - Modified related components and views to accommodate the new storage engine, including updates to `ListSpaceSidebar`, `AgentList`, `KnowledgeBaseList`, and `OrganizationList`. - Improved UI elements for batch management in the menu and sidebar components. - Added internationalization support for new memory features and storage engine descriptions across multiple languages. This update enhances the flexibility of storage options and improves user experience with better UI interactions.
This commit is contained in:
@@ -5,7 +5,7 @@ tmp_dir = "tmp"
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ./cmd/server"
|
||||
cmd = "CGO_CFLAGS='-Wno-deprecated-declarations -Wno-gnu-folding-constant' CGO_LDFLAGS='-Wl,-no_warn_duplicate_libraries' go build -ldflags=\"-X 'google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn'\" -o ./tmp/main ./cmd/server"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "frontend", "migrations", "node_modules", "docs"]
|
||||
exclude_file = []
|
||||
|
||||
@@ -173,7 +173,7 @@ export function reconnectDocReader(addr: string): Promise<ParserEnginesResponse
|
||||
// ---- 存储引擎配置(租户级,供文档/图片存储与 docreader 使用) ----
|
||||
|
||||
export interface StorageEngineConfig {
|
||||
default_provider: string // "local" | "minio" | "cos"
|
||||
default_provider: string // "local" | "minio" | "cos" | "tos"
|
||||
local?: { path_prefix: string }
|
||||
minio?: { mode: string; endpoint: string; access_key_id: string; secret_access_key: string; bucket_name: string; use_ssl: boolean; path_prefix: string }
|
||||
cos?: {
|
||||
@@ -184,6 +184,14 @@ export interface StorageEngineConfig {
|
||||
app_id: string
|
||||
path_prefix: string
|
||||
}
|
||||
tos?: {
|
||||
endpoint: string
|
||||
region: string
|
||||
access_key: string
|
||||
secret_key: string
|
||||
bucket_name: string
|
||||
path_prefix: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface StorageEngineStatusItem {
|
||||
@@ -210,9 +218,10 @@ export function getStorageEngineStatus(): Promise<{ data: GetStorageEngineStatus
|
||||
}
|
||||
|
||||
export interface StorageCheckRequest {
|
||||
provider: string // "minio" | "cos"
|
||||
provider: string // "minio" | "cos" | "tos"
|
||||
minio?: StorageEngineConfig['minio']
|
||||
cos?: StorageEngineConfig['cos']
|
||||
tos?: StorageEngineConfig['tos']
|
||||
}
|
||||
|
||||
export interface StorageCheckResponse {
|
||||
|
||||
@@ -1,13 +1,61 @@
|
||||
<template>
|
||||
<aside class="list-space-sidebar">
|
||||
<div class="sidebar-header-row">
|
||||
<span class="sidebar-title">{{ $t('listSpaceSidebar.title') }}</span>
|
||||
<div v-if="$slots.actions" class="sidebar-actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
<aside class="list-space-sidebar" :class="{ collapsed }">
|
||||
<!-- 折叠/展开按钮:浮在右侧边缘垂直居中 -->
|
||||
<div class="sidebar-toggle" @click="toggleCollapse">
|
||||
<t-icon :name="collapsed ? 'chevron-right' : 'chevron-left'" size="14px" />
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<!-- 全部:仅组织模式显示;知识库/智能体列表不展示「全部」 -->
|
||||
|
||||
<!-- ========== 折叠态:44px 图标工具条 ========== -->
|
||||
<template v-if="collapsed">
|
||||
<div class="icon-strip">
|
||||
<!-- 组织模式:全部 -->
|
||||
<t-tooltip v-if="mode !== 'resource'" :content="tooltipText($t('listSpaceSidebar.all'), countAll)" placement="right">
|
||||
<div class="icon-item" :class="{ active: selected === 'all' }" @click="select('all')">
|
||||
<t-icon name="layers" size="16px" />
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
<!-- 资源模式:我的 + 共享给我 + 空间 -->
|
||||
<template v-if="mode === 'resource'">
|
||||
<t-tooltip :content="tooltipText($t('listSpaceSidebar.mine'), countMine)" placement="right">
|
||||
<div class="icon-item" :class="{ active: selected === 'mine' }" @click="select('mine')">
|
||||
<t-icon name="user" size="16px" />
|
||||
</div>
|
||||
</t-tooltip>
|
||||
<t-tooltip v-if="countShared !== undefined && countShared > 0" :content="tooltipText($t('listSpaceSidebar.sharedToMe'), countShared)" placement="right">
|
||||
<div class="icon-item" :class="{ active: selected === 'shared' }" @click="select('shared')">
|
||||
<t-icon name="share" size="16px" />
|
||||
</div>
|
||||
</t-tooltip>
|
||||
<template v-if="organizationsWithCount.length">
|
||||
<div class="icon-strip-divider" />
|
||||
<t-tooltip v-for="org in organizationsWithCount" :key="org.id" :content="tooltipText(org.name, getOrgCount(org.id))" placement="right">
|
||||
<div class="icon-item" :class="{ active: selected === org.id }" @click="select(org.id)">
|
||||
<SpaceAvatar :name="org.name" :avatar="org.avatar" size="small" />
|
||||
</div>
|
||||
</t-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 组织模式:我创建的 + 我加入的 -->
|
||||
<template v-else>
|
||||
<t-tooltip :content="tooltipText($t('organization.createdByMe'), countCreated)" placement="right">
|
||||
<div class="icon-item" :class="{ active: selected === 'created' }" @click="select('created')">
|
||||
<t-icon name="usergroup-add" size="16px" />
|
||||
</div>
|
||||
</t-tooltip>
|
||||
<t-tooltip :content="tooltipText($t('organization.joinedByMe'), countJoined)" placement="right">
|
||||
<div class="icon-item" :class="{ active: selected === 'joined' }" @click="select('joined')">
|
||||
<t-icon name="usergroup" size="16px" />
|
||||
</div>
|
||||
</t-tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ========== 展开态:200px 完整侧边栏 ========== -->
|
||||
<template v-else>
|
||||
<nav class="sidebar-nav">
|
||||
<div
|
||||
v-if="mode !== 'resource'"
|
||||
class="sidebar-item"
|
||||
@@ -20,7 +68,6 @@
|
||||
</div>
|
||||
<span v-if="countAll !== undefined" class="item-count">{{ countAll }}</span>
|
||||
</div>
|
||||
<!-- 资源列表模式:我的 + 共享给我 + 空间列表 -->
|
||||
<template v-if="mode === 'resource'">
|
||||
<div
|
||||
class="sidebar-item"
|
||||
@@ -64,7 +111,6 @@
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 共享空间列表模式:我创建的 + 我加入的 -->
|
||||
<template v-else>
|
||||
<div
|
||||
class="sidebar-item"
|
||||
@@ -89,7 +135,8 @@
|
||||
<span v-if="countJoined !== undefined" class="item-count">{{ countJoined }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
</nav>
|
||||
</template>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -101,25 +148,30 @@ import { useOrganizationStore } from '@/stores/organization'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** resource = 知识库/智能体(全部+我的+共享给我+空间列表);organization = 共享空间(全部+我创建的+我加入的) */
|
||||
mode?: 'resource' | 'organization'
|
||||
modelValue: string
|
||||
/** 全部数量(可选) */
|
||||
collapsedKey?: string
|
||||
countAll?: number
|
||||
/** 我的数量(resource 模式) */
|
||||
countMine?: number
|
||||
/** 共享给我的数量(resource 模式) */
|
||||
countShared?: number
|
||||
/** 各空间下的数量(resource 模式),key 为 organization_id */
|
||||
countByOrg?: Record<string, number>
|
||||
/** 我创建的数量(organization 模式) */
|
||||
countCreated?: number
|
||||
/** 我加入的数量(organization 模式) */
|
||||
countJoined?: number
|
||||
}>(),
|
||||
{ mode: 'resource', countAll: undefined, countMine: undefined, countShared: undefined, countByOrg: () => ({}), countCreated: undefined, countJoined: undefined }
|
||||
{ mode: 'resource', collapsedKey: 'sidebar-collapsed-list', countAll: undefined, countMine: undefined, countShared: undefined, countByOrg: () => ({}), countCreated: undefined, countJoined: undefined }
|
||||
)
|
||||
|
||||
const collapsed = ref(localStorage.getItem(props.collapsedKey) === 'true')
|
||||
|
||||
function toggleCollapse() {
|
||||
collapsed.value = !collapsed.value
|
||||
localStorage.setItem(props.collapsedKey, String(collapsed.value))
|
||||
}
|
||||
|
||||
function tooltipText(name: string, count?: number): string {
|
||||
return count !== undefined ? `${name} (${count})` : name
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
@@ -132,7 +184,6 @@ const selected = computed({
|
||||
|
||||
const organizations = computed(() => orgStore.organizations || [])
|
||||
|
||||
/** 资源模式下只展示数量大于 0 的空间 */
|
||||
const organizationsWithCount = computed(() => {
|
||||
if (props.mode !== 'resource') return organizations.value
|
||||
return organizations.value.filter((org) => (props.countByOrg?.[org.id] ?? 0) > 0)
|
||||
@@ -153,88 +204,118 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 筛选区:白底卡片感(与右侧内容区风格对调),细分界保持统一
|
||||
.list-space-sidebar {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e7ebf0;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
|
||||
padding: 16px;
|
||||
padding: 24px 16px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
transition: width 0.2s ease, padding 0.2s ease;
|
||||
|
||||
&.collapsed {
|
||||
width: 44px;
|
||||
padding: 24px 0 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-header-row {
|
||||
/* 浮动折叠/展开按钮 */
|
||||
.sidebar-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -10px;
|
||||
transform: translateY(-50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e9f2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
min-height: 28px;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
color: #86909c;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0;
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
.list-space-sidebar:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f2f4f7;
|
||||
color: #1d2129;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 折叠态图标工具条 ========== */
|
||||
.icon-strip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #5c6470;
|
||||
transition: all 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: #f2f4f7;
|
||||
color: #1d2129;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
&.active {
|
||||
background: #e6f7ec;
|
||||
color: #07c05f;
|
||||
|
||||
:deep(.t-button) {
|
||||
padding: 0;
|
||||
min-width: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
gap: 0;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
background: #f2f3f5 !important;
|
||||
border: 1px solid #e5e9f2 !important;
|
||||
color: #4e5969;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
&:hover {
|
||||
background: #e5e9f2 !important;
|
||||
border-color: #c9cdd4 !important;
|
||||
color: #1d2129;
|
||||
background: #d4f4e3;
|
||||
}
|
||||
}
|
||||
:deep(.t-button .t-button__icon),
|
||||
:deep(.t-button .btn-icon-wrapper) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:deep(.t-button .t-icon),
|
||||
:deep(.t-button .btn-icon-wrapper) {
|
||||
color: #07c05f;
|
||||
}
|
||||
:deep(.t-button:hover .t-icon),
|
||||
:deep(.t-button:hover .btn-icon-wrapper) {
|
||||
color: #07c05f;
|
||||
}
|
||||
:deep(.t-button .t-icon + .t-button__text:not(:empty)) {
|
||||
margin-left: 0;
|
||||
}
|
||||
:deep(.sidebar-org-icon) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
:deep(.space-avatar) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-strip-divider {
|
||||
width: 20px;
|
||||
height: 1px;
|
||||
background: #e7ebf0;
|
||||
margin: 4px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ========== 展开态 ========== */
|
||||
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -20,21 +20,28 @@
|
||||
</div>
|
||||
<span class="menu_title" :title="item.title">{{ item.title }}</span>
|
||||
<span v-if="item.path === 'organizations' && orgStore.totalPendingJoinRequestCount > 0" class="menu-pending-badge" :title="t('organization.settings.pendingJoinRequestsBadge')">{{ orgStore.totalPendingJoinRequestCount }}</span>
|
||||
<t-icon v-if="item.path === 'creatChat'" name="add" class="menu-create-hint" />
|
||||
<span v-if="item.path === 'creatChat' && batchMode" class="batch-cancel-hint" @click.stop="exitBatchMode">{{ t('batchManage.cancel') }}</span>
|
||||
<t-icon v-else-if="item.path === 'creatChat'" name="add" class="menu-create-hint" />
|
||||
</div>
|
||||
</div>
|
||||
<div ref="submenuscrollContainer" @scroll="handleScroll" class="submenu" v-if="item.children">
|
||||
<template v-for="(group, groupIndex) in groupedSessions" :key="groupIndex">
|
||||
<div class="timeline_header">{{ group.label }}</div>
|
||||
<div class="submenu_item_p" v-for="(subitem, subindex) in group.items" :key="subitem.id">
|
||||
<div :class="['submenu_item', currentSecondpath == subitem.path ? 'submenu_item_active' : '']"
|
||||
<div :class="['submenu_item', !batchMode && currentSecondpath == subitem.path ? 'submenu_item_active' : '', batchMode && batchSelectedIds.includes(subitem.id) ? 'submenu_item_selected' : '', batchMode ? 'submenu_item_batch' : '']"
|
||||
@mouseenter="mouseenteBotDownr(subitem.id)" @mouseleave="mouseleaveBotDown"
|
||||
@click="gotopage(subitem.path)">
|
||||
@click="batchMode ? toggleBatchSelect(subitem.id) : gotopage(subitem.path)">
|
||||
<t-checkbox v-if="batchMode"
|
||||
class="batch-checkbox"
|
||||
:checked="batchSelectedIds.includes(subitem.id)"
|
||||
@click.stop
|
||||
@change="toggleBatchSelect(subitem.id)"
|
||||
/>
|
||||
<span class="submenu_title"
|
||||
:style="currentSecondpath == subitem.path ? 'margin-left:18px;max-width:160px;' : 'margin-left:18px;max-width:185px;'">
|
||||
:style="batchMode ? 'margin-left:4px;max-width:170px;' : (currentSecondpath == subitem.path ? 'margin-left:18px;max-width:160px;' : 'margin-left:18px;max-width:185px;')">
|
||||
{{ subitem.title }}
|
||||
</span>
|
||||
<t-dropdown
|
||||
<t-dropdown v-if="!batchMode"
|
||||
:options="[{ content: t('upload.deleteRecord'), value: 'delete' }, { content: t('menu.batchManage'), value: 'batchManage' }]"
|
||||
@click="handleSessionMenuClick($event, subitem.originalIndex, subitem)"
|
||||
placement="bottom-right"
|
||||
@@ -47,6 +54,27 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="batchMode && item.path === 'creatChat'" class="batch-inline-footer">
|
||||
<div class="batch-footer-left">
|
||||
<t-checkbox
|
||||
:checked="isAllBatchSelected"
|
||||
:indeterminate="isBatchIndeterminate"
|
||||
@change="toggleBatchSelectAll"
|
||||
>
|
||||
{{ t('batchManage.selectAll') }}
|
||||
</t-checkbox>
|
||||
</div>
|
||||
<t-button
|
||||
size="small"
|
||||
theme="danger"
|
||||
variant="base"
|
||||
:disabled="batchSelectedIds.length === 0"
|
||||
:loading="batchDeleting"
|
||||
@click="handleInlineBatchDelete"
|
||||
>
|
||||
{{ t('batchManage.delete') }}{{ batchSelectedIds.length > 0 ? `(${batchSelectedIds.length})` : '' }}
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,17 +97,16 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, watch, computed, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getSessionsList, delSession } from "@/api/chat/index";
|
||||
import { getSessionsList, delSession, batchDelSessions } from "@/api/chat/index";
|
||||
import { getKnowledgeBaseById } from '@/api/knowledge-base';
|
||||
import { logout as logoutApi } from '@/api/auth';
|
||||
import { useMenuStore } from '@/stores/menu';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useOrganizationStore } from '@/stores/organization';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
import { MessagePlugin, DialogPlugin } from "tdesign-vue-next";
|
||||
import UserMenu from '@/components/UserMenu.vue';
|
||||
import TenantSelector from '@/components/TenantSelector.vue';
|
||||
import BatchManageDialog from '@/components/BatchManageDialog.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { getSystemInfo } from '@/api/system';
|
||||
|
||||
@@ -105,7 +132,9 @@ let activeSubmenu = ref<string>('');
|
||||
const isLiteEdition = ref(false);
|
||||
|
||||
// 批量管理状态
|
||||
const batchManageVisible = ref(false);
|
||||
const batchMode = ref(false)
|
||||
const batchSelectedIds = ref<string[]>([])
|
||||
const batchDeleting = ref(false)
|
||||
|
||||
// 是否可以访问所有租户
|
||||
const canAccessAllTenants = computed(() => authStore.canAccessAllTenants);
|
||||
@@ -266,11 +295,77 @@ const mouseleaveBotDown = () => {
|
||||
activeSubmenu.value = '';
|
||||
}
|
||||
|
||||
const enterBatchMode = () => {
|
||||
batchMode.value = true
|
||||
batchSelectedIds.value = []
|
||||
}
|
||||
|
||||
const exitBatchMode = () => {
|
||||
batchMode.value = false
|
||||
batchSelectedIds.value = []
|
||||
}
|
||||
|
||||
const toggleBatchSelect = (id: string) => {
|
||||
const idx = batchSelectedIds.value.indexOf(id)
|
||||
if (idx > -1) {
|
||||
batchSelectedIds.value.splice(idx, 1)
|
||||
} else {
|
||||
batchSelectedIds.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleBatchSelectAll = (checked: boolean) => {
|
||||
batchSelectedIds.value = checked ? [...allSessionIds.value] : []
|
||||
}
|
||||
|
||||
const handleInlineBatchDelete = () => {
|
||||
if (batchSelectedIds.value.length === 0) return
|
||||
const confirmDialog = DialogPlugin.confirm({
|
||||
header: t('batchManage.deleteConfirmTitle'),
|
||||
body: t('batchManage.deleteConfirmBody', { count: batchSelectedIds.value.length }),
|
||||
confirmBtn: { content: t('batchManage.delete'), theme: 'danger' as const },
|
||||
cancelBtn: t('batchManage.cancel'),
|
||||
theme: 'warning',
|
||||
onConfirm: async () => {
|
||||
batchDeleting.value = true
|
||||
try {
|
||||
const ids = [...batchSelectedIds.value]
|
||||
const res: any = await batchDelSessions(ids)
|
||||
if (res && res.success === true) {
|
||||
const chatMenuItem = (menuArr.value as any[]).find((m: any) => m.path === 'creatChat');
|
||||
if (chatMenuItem && chatMenuItem.children) {
|
||||
for (const id of ids) {
|
||||
const idx = chatMenuItem.children.findIndex((s: any) => s.id === id);
|
||||
if (idx !== -1) chatMenuItem.children.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
total.value = Math.max(0, total.value - ids.length);
|
||||
const currentChatId = route.params.chatid as string;
|
||||
if (currentChatId && ids.includes(currentChatId)) {
|
||||
router.push('/platform/creatChat');
|
||||
}
|
||||
batchSelectedIds.value = []
|
||||
MessagePlugin.success(t('batchManage.deleteSuccess'))
|
||||
if (!chatMenuItem?.children?.length) {
|
||||
exitBatchMode()
|
||||
}
|
||||
} else {
|
||||
MessagePlugin.error(t('batchManage.deleteFailed'))
|
||||
}
|
||||
} catch {
|
||||
MessagePlugin.error(t('batchManage.deleteFailed'))
|
||||
}
|
||||
batchDeleting.value = false
|
||||
confirmDialog.destroy()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleSessionMenuClick = (data: { value: string }, index: number, item: any) => {
|
||||
if (data?.value === 'delete') {
|
||||
delCard(index, item);
|
||||
} else if (data?.value === 'batchManage') {
|
||||
batchManageVisible.value = true;
|
||||
enterBatchMode()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -304,22 +399,6 @@ const delCard = (index: number, item: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleBatchDeleted = (ids: string[]) => {
|
||||
const chatMenuItem = (menuArr.value as any[]).find((m: any) => m.path === 'creatChat');
|
||||
if (chatMenuItem && chatMenuItem.children) {
|
||||
const children = chatMenuItem.children;
|
||||
for (const id of ids) {
|
||||
const idx = children.findIndex((s: any) => s.id === id);
|
||||
if (idx !== -1) children.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
total.value = Math.max(0, total.value - ids.length);
|
||||
// 如果当前会话被删除,跳转到创建页
|
||||
const currentChatId = route.params.chatid as string;
|
||||
if (currentChatId && ids.includes(currentChatId)) {
|
||||
router.push('/platform/creatChat');
|
||||
}
|
||||
}
|
||||
|
||||
const debounce = (fn: (...args: any[]) => void, delay: number) => {
|
||||
let timer: ReturnType<typeof setTimeout>
|
||||
@@ -869,6 +948,53 @@ const mouseleaveMenu = (path: string) => {
|
||||
max-width: 160px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.submenu_item_batch {
|
||||
padding-left: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.submenu_item_selected {
|
||||
background: #07c05f0d !important;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.batch-checkbox {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.batch-cancel-hint {
|
||||
margin-left: auto;
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
color: #00000066;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s ease;
|
||||
font-weight: 400;
|
||||
|
||||
&:hover {
|
||||
color: #000000b3;
|
||||
}
|
||||
}
|
||||
|
||||
.batch-inline-footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 14px;
|
||||
border-top: 1px solid #e7ebf0;
|
||||
background: #fff;
|
||||
|
||||
.batch-footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #00000099;
|
||||
}
|
||||
}
|
||||
|
||||
/* 知识库下拉菜单样式 */
|
||||
|
||||
@@ -506,6 +506,8 @@ export default {
|
||||
webSearchConfig: 'Web Search',
|
||||
enableMemory: 'Enable Memory',
|
||||
enableMemoryDesc: 'When enabled, the system will record your conversation history and automatically recall relevant content in future conversations to provide more personalized answers.',
|
||||
memoryRequiresNeo4j: 'Memory feature requires Neo4j graph database. Please configure and enable Neo4j (set NEO4J_ENABLE=true) before enabling this feature.',
|
||||
memoryHowToEnable: 'View Neo4j Configuration Guide',
|
||||
storageEngine: 'Storage Engine',
|
||||
mcpService: 'MCP Service',
|
||||
systemSettings: 'System Settings',
|
||||
|
||||
@@ -354,9 +354,10 @@ export default {
|
||||
conversationConfig: "대화 설정",
|
||||
conversationStrategy: "대화 전략",
|
||||
webSearchConfig: "웹 검색",
|
||||
enableMemory: "메모리 기능 활성화",
|
||||
enableMemoryDesc:
|
||||
"일단 켜면 시스템은 대화 기록을 기록하고 이후 대화에서 관련 내용을 자동으로 불러와 보다 개인화된 답변을 제공합니다.",
|
||||
enableMemory: "기억 기능 활성화",
|
||||
enableMemoryDesc: "활성화하면 시스템이 대화 기록을 저장하고 향후 대화에서 관련 내용을 자동으로 회상하여 더 개인화된 답변을 제공합니다.",
|
||||
memoryRequiresNeo4j: "기억 기능은 Neo4j 그래프 데이터베이스가 필요합니다. 이 기능을 활성화하기 전에 Neo4j를 구성하고 활성화해 주세요 (NEO4J_ENABLE=true 설정).",
|
||||
memoryHowToEnable: "Neo4j 구성 가이드 보기",
|
||||
mcpService: "MCP 서비스",
|
||||
systemSettings: "시스템 설정",
|
||||
tenantInfo: "테넌트 정보",
|
||||
|
||||
@@ -243,6 +243,10 @@ export default {
|
||||
modelManagement: 'Управление моделями',
|
||||
agentConfig: 'Настройки агента',
|
||||
webSearchConfig: 'Сетевой поиск',
|
||||
enableMemory: 'Включить память',
|
||||
enableMemoryDesc: 'При включении система будет записывать историю ваших разговоров и автоматически вспоминать соответствующий контент в будущих беседах для более персонализированных ответов.',
|
||||
memoryRequiresNeo4j: 'Функция памяти требует графовую базу данных Neo4j. Пожалуйста, настройте и включите Neo4j (установите NEO4J_ENABLE=true) перед активацией этой функции.',
|
||||
memoryHowToEnable: 'Руководство по настройке Neo4j',
|
||||
mcpService: 'Сервис MCP',
|
||||
conversationConfig: 'Настройки диалога',
|
||||
conversationStrategy: 'Стратегия диалога',
|
||||
|
||||
@@ -353,6 +353,8 @@ export default {
|
||||
webSearchConfig: "网络搜索",
|
||||
enableMemory: "开启记忆功能",
|
||||
enableMemoryDesc: "开启后,系统将记录您的对话历史,并在后续对话中自动回忆相关内容,提供更个性化的回答。",
|
||||
memoryRequiresNeo4j: "记忆功能依赖 Neo4j 图数据库,请先配置并启用 Neo4j(设置环境变量 NEO4J_ENABLE=true)后再开启此功能。",
|
||||
memoryHowToEnable: "查看 Neo4j 配置指南",
|
||||
storageEngine: "存储引擎",
|
||||
mcpService: "MCP服务",
|
||||
systemSettings: "系统设置",
|
||||
|
||||
@@ -1,44 +1,39 @@
|
||||
<template>
|
||||
<div class="agent-list-container">
|
||||
<!-- 头部:仅标题与副标题 -->
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<h2>{{ $t('agent.title') }}</h2>
|
||||
<p class="header-subtitle">{{ $t('agent.subtitle') }}</p>
|
||||
<ListSpaceSidebar
|
||||
v-model="spaceSelection"
|
||||
:count-all="allAgentsCount"
|
||||
:count-mine="agents.length"
|
||||
:count-by-org="effectiveSharedCountByOrg"
|
||||
/>
|
||||
<div class="agent-list-content">
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<div class="title-row">
|
||||
<h2>{{ $t('agent.title') }}</h2>
|
||||
<t-tooltip :content="$t('agent.createAgent')" placement="bottom">
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
size="small"
|
||||
class="header-action-btn"
|
||||
@click="handleCreateAgent"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="btn-icon-wrapper">
|
||||
<svg class="sparkles-icon" width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 3L10.8 6.2C10.9 6.7 11.3 7.1 11.8 7.2L15 8L11.8 8.8C11.3 8.9 10.9 9.3 10.8 9.8L10 13L9.2 9.8C9.1 9.3 8.7 8.9 8.2 8.8L5 8L8.2 7.2C8.7 7.1 9.1 6.7 9.2 6.2L10 3Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.5 4L15.8 5.2C15.85 5.45 16.05 5.65 16.3 5.7L17.5 6L16.3 6.3C16.05 6.35 15.85 6.55 15.8 6.8L15.5 8L15.2 6.8C15.15 6.55 14.95 6.35 14.7 6.3L13.5 6L14.7 5.7C14.95 5.65 15.15 5.45 15.2 5.2L15.5 4Z" fill="currentColor" stroke="currentColor" stroke-width="0.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.5 13L4.8 14.2C4.85 14.45 5.05 14.65 5.3 14.7L6.5 15L5.3 15.3C5.05 15.35 4.85 15.55 4.8 15.8L4.5 17L4.2 15.8C4.15 15.55 3.95 15.35 3.7 15.3L2.5 15L3.7 14.7C3.95 14.65 4.15 14.45 4.2 14.2L4.5 13Z" fill="currentColor" stroke="currentColor" stroke-width="0.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
<p class="header-subtitle">{{ $t('agent.subtitle') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧菜单 + 主内容 -->
|
||||
<div class="agent-list-body">
|
||||
<ListSpaceSidebar
|
||||
v-model="spaceSelection"
|
||||
:count-all="allAgentsCount"
|
||||
:count-mine="agents.length"
|
||||
:count-by-org="effectiveSharedCountByOrg"
|
||||
>
|
||||
<template #actions>
|
||||
<t-tooltip :content="$t('agent.createAgent')" placement="top">
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
class="sidebar-action-btn"
|
||||
size="small"
|
||||
:aria-label="$t('agent.createAgent')"
|
||||
@click="handleCreateAgent"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="btn-icon-wrapper">
|
||||
<svg class="sparkles-icon" width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 3L10.8 6.2C10.9 6.7 11.3 7.1 11.8 7.2L15 8L11.8 8.8C11.3 8.9 10.9 9.3 10.8 9.8L10 13L9.2 9.8C9.1 9.3 8.7 8.9 8.2 8.8L5 8L8.2 7.2C8.7 7.1 9.1 6.7 9.2 6.2L10 3Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.5 4L15.8 5.2C15.85 5.45 16.05 5.65 16.3 5.7L17.5 6L16.3 6.3C16.05 6.35 15.85 6.55 15.8 6.8L15.5 8L15.2 6.8C15.15 6.55 14.95 6.35 14.7 6.3L13.5 6L14.7 5.7C14.95 5.65 15.15 5.45 15.2 5.2L15.5 4Z" fill="currentColor" stroke="currentColor" stroke-width="0.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.5 13L4.8 14.2C4.85 14.45 5.05 14.65 5.3 14.7L6.5 15L5.3 15.3C5.05 15.35 4.85 15.55 4.8 15.8L4.5 17L4.2 15.8C4.15 15.55 3.95 15.35 3.7 15.3L2.5 15L3.7 14.7C3.95 14.65 4.15 14.45 4.2 14.2L4.5 13Z" fill="currentColor" stroke="currentColor" stroke-width="0.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</template>
|
||||
</ListSpaceSidebar>
|
||||
<div class="agent-list-main">
|
||||
<!-- 全部:我的 + 共享 -->
|
||||
<div v-if="spaceSelection === 'all' && filteredAgents.length > 0" class="agent-card-wrap">
|
||||
@@ -930,24 +925,20 @@ defineExpose({
|
||||
|
||||
<style scoped lang="less">
|
||||
.agent-list-container {
|
||||
padding: 24px 32px;
|
||||
margin: 0 16px 0 4px;
|
||||
margin: 0 16px 0 0;
|
||||
height: calc(100vh);
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.agent-list-body {
|
||||
display: flex;
|
||||
.agent-list-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #fafbfc;
|
||||
border: 1px solid #e7ebf0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 24px 32px 0 32px;
|
||||
}
|
||||
|
||||
.agent-list-main {
|
||||
@@ -955,8 +946,7 @@ defineExpose({
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px;
|
||||
background: #fafbfc;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.agent-list-main-loading {
|
||||
@@ -982,6 +972,7 @@ defineExpose({
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.header-title {
|
||||
@@ -990,6 +981,12 @@ defineExpose({
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #000000e6;
|
||||
@@ -1070,6 +1067,33 @@ defineExpose({
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
padding: 0 !important;
|
||||
min-width: 28px !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
background: #f2f3f5 !important;
|
||||
border: 1px solid #e5e9f2 !important;
|
||||
border-radius: 6px !important;
|
||||
color: #4e5969;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #e5e9f2 !important;
|
||||
border-color: #c9cdd4 !important;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
:deep(.t-icon),
|
||||
:deep(.btn-icon-wrapper) {
|
||||
color: #07c05f;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,37 +1,32 @@
|
||||
<template>
|
||||
<div class="kb-list-container">
|
||||
<!-- 头部:仅标题与副标题 -->
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<h2>{{ $t('knowledgeBase.title') }}</h2>
|
||||
<p class="header-subtitle">{{ $t('knowledgeList.subtitle') }}</p>
|
||||
<ListSpaceSidebar
|
||||
v-model="spaceSelection"
|
||||
:count-all="allKnowledgeBases"
|
||||
:count-mine="kbs.length"
|
||||
:count-shared="sharedKbs.length"
|
||||
:count-by-org="effectiveSharedCountByOrg"
|
||||
/>
|
||||
<div class="kb-list-content">
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<div class="title-row">
|
||||
<h2>{{ $t('knowledgeBase.title') }}</h2>
|
||||
<t-tooltip :content="$t('knowledgeList.create')" placement="bottom">
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
size="small"
|
||||
class="header-action-btn"
|
||||
@click="handleCreateKnowledgeBase"
|
||||
>
|
||||
<template #icon><t-icon name="folder-add" size="16px" /></template>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
<p class="header-subtitle">{{ $t('knowledgeList.subtitle') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧菜单 + 主内容 -->
|
||||
<div class="kb-list-body">
|
||||
<ListSpaceSidebar
|
||||
v-model="spaceSelection"
|
||||
:count-all="allKnowledgeBases"
|
||||
:count-mine="kbs.length"
|
||||
:count-shared="sharedKbs.length"
|
||||
:count-by-org="effectiveSharedCountByOrg"
|
||||
>
|
||||
<template #actions>
|
||||
<t-tooltip :content="$t('knowledgeList.create')" placement="top">
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
class="sidebar-action-btn"
|
||||
size="small"
|
||||
:aria-label="$t('knowledgeList.create')"
|
||||
@click="handleCreateKnowledgeBase"
|
||||
>
|
||||
<template #icon><t-icon name="folder-add" size="16px" /></template>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</template>
|
||||
</ListSpaceSidebar>
|
||||
<div class="kb-list-main">
|
||||
<!-- 未初始化知识库提示 -->
|
||||
<div v-if="hasUninitializedKbs" class="warning-banner">
|
||||
@@ -1151,16 +1146,22 @@ const handleUploadFinishedEvent = (event: Event) => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.kb-list-container {
|
||||
padding: 24px 32px;
|
||||
margin: 0 16px 0 4px;
|
||||
margin: 0 16px 0 0;
|
||||
height: calc(100vh);
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.kb-list-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 24px 32px 0 32px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1173,6 +1174,12 @@ const handleUploadFinishedEvent = (event: Event) => {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #000000e6;
|
||||
@@ -1194,23 +1201,12 @@ const handleUploadFinishedEvent = (event: Event) => {
|
||||
}
|
||||
}
|
||||
|
||||
.kb-list-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #fafbfc;
|
||||
border: 1px solid #e7ebf0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.kb-list-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px;
|
||||
background: #fafbfc;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.kb-list-main-loading {
|
||||
@@ -1242,6 +1238,33 @@ const handleUploadFinishedEvent = (event: Event) => {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
padding: 0 !important;
|
||||
min-width: 28px !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
background: #f2f3f5 !important;
|
||||
border: 1px solid #e5e9f2 !important;
|
||||
border-radius: 6px !important;
|
||||
color: #4e5969;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #e5e9f2 !important;
|
||||
border-color: #c9cdd4 !important;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
:deep(.t-icon),
|
||||
:deep(.btn-icon-wrapper) {
|
||||
color: #07c05f;
|
||||
}
|
||||
}
|
||||
|
||||
// Tab 切换样式(已由左侧菜单替代,保留以备兼容)
|
||||
.kb-tabs {
|
||||
display: flex;
|
||||
|
||||
@@ -95,6 +95,13 @@ const engineOptions = computed(() => {
|
||||
available: statusMap.cos,
|
||||
disabled: statusMap.cos === false,
|
||||
},
|
||||
{
|
||||
value: 'tos',
|
||||
label: '火山引擎 TOS',
|
||||
desc: '火山引擎对象存储,适合公有云部署',
|
||||
available: statusMap.tos,
|
||||
disabled: statusMap.tos === false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -126,7 +133,7 @@ async function load() {
|
||||
engineStatus.value = engines
|
||||
defaultProvider.value = configRes?.data?.default_provider || 'local'
|
||||
const d = configRes?.data
|
||||
hasAnyConfig.value = !!(d?.local?.path_prefix || d?.minio?.bucket_name || d?.cos?.bucket_name)
|
||||
hasAnyConfig.value = !!(d?.local?.path_prefix || d?.minio?.bucket_name || d?.cos?.bucket_name || d?.tos?.bucket_name)
|
||||
if (!localProvider.value || localProvider.value === '') {
|
||||
localProvider.value = defaultProvider.value
|
||||
emit('update:storageProvider', localProvider.value)
|
||||
|
||||
@@ -1,49 +1,45 @@
|
||||
<template>
|
||||
<div class="org-list-container">
|
||||
<!-- 头部:仅标题与副标题 -->
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<h2>{{ $t('organization.title') }}</h2>
|
||||
<p class="header-subtitle">{{ $t('organization.subtitle') }}</p>
|
||||
<ListSpaceSidebar
|
||||
mode="organization"
|
||||
v-model="spaceSelection"
|
||||
:count-all="organizations.length"
|
||||
:count-created="createdCount"
|
||||
:count-joined="joinedCount"
|
||||
/>
|
||||
<div class="org-list-content">
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<div class="title-row">
|
||||
<h2>{{ $t('organization.title') }}</h2>
|
||||
<div class="header-actions">
|
||||
<t-tooltip :content="$t('organization.joinOrg')" placement="bottom">
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
size="small"
|
||||
class="header-action-btn"
|
||||
@click="handleJoinOrganization"
|
||||
>
|
||||
<template #icon><t-icon name="enter" size="16px" /></template>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
<t-tooltip :content="$t('organization.createOrg')" placement="bottom">
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
size="small"
|
||||
class="header-action-btn"
|
||||
@click="handleCreateOrganization"
|
||||
>
|
||||
<template #icon><img src="@/assets/img/organization-green.svg" class="org-create-icon" alt="" aria-hidden="true" /></template>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<p class="header-subtitle">{{ $t('organization.subtitle') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧筛选 + 主内容 -->
|
||||
<div class="org-list-body">
|
||||
<ListSpaceSidebar
|
||||
mode="organization"
|
||||
v-model="spaceSelection"
|
||||
:count-all="organizations.length"
|
||||
:count-created="createdCount"
|
||||
:count-joined="joinedCount"
|
||||
>
|
||||
<template #actions>
|
||||
<t-tooltip :content="$t('organization.joinOrg')" placement="top">
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
class="sidebar-action-btn"
|
||||
size="small"
|
||||
:aria-label="$t('organization.joinOrg')"
|
||||
@click="handleJoinOrganization"
|
||||
>
|
||||
<template #icon><t-icon name="enter" size="16px" /></template>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
<t-tooltip :content="$t('organization.createOrg')" placement="top">
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
class="sidebar-action-btn"
|
||||
size="small"
|
||||
:aria-label="$t('organization.createOrg')"
|
||||
@click="handleCreateOrganization"
|
||||
>
|
||||
<template #icon><img src="@/assets/img/organization-green.svg" class="org-create-icon sidebar-org-icon" alt="" aria-hidden="true" /></template>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</template>
|
||||
</ListSpaceSidebar>
|
||||
<div class="org-list-main">
|
||||
<!-- 卡片网格 -->
|
||||
<div v-if="filteredOrganizations.length > 0" class="org-card-wrap">
|
||||
@@ -1223,24 +1219,20 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.org-list-container {
|
||||
padding: 24px 32px;
|
||||
margin: 0 16px 0 4px;
|
||||
margin: 0 16px 0 0;
|
||||
height: calc(100vh);
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.org-list-body {
|
||||
display: flex;
|
||||
.org-list-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #fafbfc;
|
||||
border: 1px solid #e7ebf0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 24px 32px 0 32px;
|
||||
}
|
||||
|
||||
.org-list-main {
|
||||
@@ -1248,13 +1240,13 @@ onUnmounted(() => {
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px;
|
||||
background: #fafbfc;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -1264,6 +1256,12 @@ onUnmounted(() => {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #000000e6;
|
||||
@@ -1274,6 +1272,13 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.org-join-btn {
|
||||
border-color: rgba(7, 192, 95, 0.5);
|
||||
color: #07c05f;
|
||||
@@ -1324,6 +1329,39 @@ onUnmounted(() => {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
padding: 0 !important;
|
||||
min-width: 28px !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
background: #f2f3f5 !important;
|
||||
border: 1px solid #e5e9f2 !important;
|
||||
border-radius: 6px !important;
|
||||
color: #4e5969;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #e5e9f2 !important;
|
||||
border-color: #c9cdd4 !important;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
:deep(.t-icon),
|
||||
:deep(.btn-icon-wrapper),
|
||||
:deep(.org-create-icon) {
|
||||
color: #07c05f;
|
||||
}
|
||||
|
||||
:deep(.org-create-icon) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// Tab 切换样式(下划线式,与整体协作感一致)
|
||||
.org-tabs {
|
||||
display: flex;
|
||||
|
||||
@@ -34,9 +34,25 @@
|
||||
<p class="desc">{{ $t('settings.enableMemoryDesc') }}</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<t-switch v-model="isMemoryEnabled" @change="handleMemoryChange" />
|
||||
<t-switch
|
||||
v-model="isMemoryEnabled"
|
||||
:disabled="!isNeo4jAvailable"
|
||||
@change="handleMemoryChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<t-alert
|
||||
v-if="!isNeo4jAvailable"
|
||||
theme="warning"
|
||||
style="margin-top: -8px; margin-bottom: 16px;"
|
||||
>
|
||||
<template #message>
|
||||
<div>{{ $t('settings.memoryRequiresNeo4j') }}</div>
|
||||
<t-link theme="primary" href="https://github.com/Tencent/WeKnora/blob/main/docs/KnowledgeGraph.md" target="_blank">
|
||||
{{ $t('settings.memoryHowToEnable') }}
|
||||
</t-link>
|
||||
</template>
|
||||
</t-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -46,6 +62,7 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { getSystemInfo } from '@/api/system'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const settingsStore = useSettingsStore()
|
||||
@@ -54,6 +71,13 @@ const settingsStore = useSettingsStore()
|
||||
const localLanguage = ref('zh-CN')
|
||||
const localTheme = ref('light')
|
||||
|
||||
// 系统信息
|
||||
const systemInfo = ref<any>(null)
|
||||
|
||||
const isNeo4jAvailable = computed(() => {
|
||||
return systemInfo.value?.graph_database_engine && systemInfo.value.graph_database_engine !== '未启用'
|
||||
})
|
||||
|
||||
// 记忆功能状态
|
||||
const isMemoryEnabled = computed({
|
||||
get: () => settingsStore.isMemoryEnabled,
|
||||
@@ -61,7 +85,7 @@ const isMemoryEnabled = computed({
|
||||
})
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 从 localStorage 加载语言设置
|
||||
const savedLocale = localStorage.getItem('locale')
|
||||
if (savedLocale) {
|
||||
@@ -70,6 +94,17 @@ onMounted(() => {
|
||||
} else {
|
||||
localLanguage.value = locale.value
|
||||
}
|
||||
|
||||
// 加载系统信息以检查 Neo4j 可用性
|
||||
try {
|
||||
const response = await getSystemInfo()
|
||||
systemInfo.value = response.data
|
||||
if (!isNeo4jAvailable.value && settingsStore.isMemoryEnabled) {
|
||||
settingsStore.toggleMemory(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load system info:', error)
|
||||
}
|
||||
})
|
||||
|
||||
// 处理语言变化
|
||||
@@ -81,6 +116,11 @@ const handleLanguageChange = () => {
|
||||
|
||||
// 处理记忆功能变化
|
||||
const handleMemoryChange = (val: boolean) => {
|
||||
if (val && !isNeo4jAvailable.value) {
|
||||
MessagePlugin.warning(t('settings.memoryRequiresNeo4j'))
|
||||
settingsStore.toggleMemory(false)
|
||||
return
|
||||
}
|
||||
settingsStore.toggleMemory(val)
|
||||
MessagePlugin.success(t('common.success'))
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
<span class="block-name">腾讯云 COS</span>
|
||||
<t-tag theme="success" variant="light" size="small">可配置</t-tag>
|
||||
</div>
|
||||
<p class="block-desc">腾讯云对象存储服务,适合公有云部署,支持 CDN 加速。</p>
|
||||
<p class="block-desc">腾讯云对象存储服务,适合公有云部署,支持 CDN 加速。<a class="engine-link" href="https://console.cloud.tencent.com/cos" target="_blank" rel="noopener">控制台</a> · <a class="engine-link" href="https://cloud.tencent.com/document/product/436" target="_blank" rel="noopener">文档</a></p>
|
||||
<div class="block-config">
|
||||
<div class="config-item">
|
||||
<label>Secret ID</label>
|
||||
@@ -263,6 +263,78 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOS -->
|
||||
<div class="engine-block">
|
||||
<div class="block-header">
|
||||
<span class="block-name">火山引擎 TOS</span>
|
||||
<t-tag theme="success" variant="light" size="small">可配置</t-tag>
|
||||
</div>
|
||||
<p class="block-desc">火山引擎对象存储服务(TOS),适合公有云部署。<a class="engine-link" href="https://console.volcengine.com/tos" target="_blank" rel="noopener">控制台</a> · <a class="engine-link" href="https://www.volcengine.com/docs/6349" target="_blank" rel="noopener">文档</a></p>
|
||||
<div class="block-config">
|
||||
<div class="config-item">
|
||||
<label>Endpoint</label>
|
||||
<t-input
|
||||
v-model="config.tos.endpoint"
|
||||
size="small"
|
||||
placeholder="如 https://tos-cn-beijing.volces.com"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Region</label>
|
||||
<t-input
|
||||
v-model="config.tos.region"
|
||||
size="small"
|
||||
placeholder="如 cn-beijing"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Access Key</label>
|
||||
<t-input
|
||||
v-model="config.tos.access_key"
|
||||
size="small"
|
||||
placeholder="火山引擎 Access Key"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Secret Key</label>
|
||||
<t-input
|
||||
v-model="config.tos.secret_key"
|
||||
size="small"
|
||||
type="password"
|
||||
placeholder="火山引擎 Secret Key"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Bucket 名称</label>
|
||||
<t-input
|
||||
v-model="config.tos.bucket_name"
|
||||
size="small"
|
||||
placeholder="存储桶名称"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>路径前缀(可选)</label>
|
||||
<t-input
|
||||
v-model="config.tos.path_prefix"
|
||||
size="small"
|
||||
placeholder="如 weknora"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-bar">
|
||||
<t-button size="small" variant="outline" :loading="checkingTos" @click="onCheckTos">测试连接</t-button>
|
||||
<span v-if="tosCheckResult" :class="['check-msg', tosCheckResult.ok ? 'success' : 'error']">
|
||||
{{ tosCheckResult.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -277,6 +349,7 @@
|
||||
<t-option value="local" label="Local(本地)" />
|
||||
<t-option value="minio" label="MinIO" />
|
||||
<t-option value="cos" label="腾讯云 COS" />
|
||||
<t-option value="tos" label="火山引擎 TOS" />
|
||||
</t-select>
|
||||
<span class="hint">新建知识库时默认选用的存储引擎</span>
|
||||
</div>
|
||||
@@ -315,6 +388,14 @@ const defaultConfig = (): StorageEngineConfig => ({
|
||||
app_id: '',
|
||||
path_prefix: '',
|
||||
},
|
||||
tos: {
|
||||
endpoint: '',
|
||||
region: '',
|
||||
access_key: '',
|
||||
secret_key: '',
|
||||
bucket_name: '',
|
||||
path_prefix: '',
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(true)
|
||||
@@ -336,6 +417,8 @@ const checkingMinio = ref(false)
|
||||
const minioCheckResult = ref<{ ok: boolean; message: string } | null>(null)
|
||||
const checkingCos = ref(false)
|
||||
const cosCheckResult = ref<{ ok: boolean; message: string } | null>(null)
|
||||
const checkingTos = ref(false)
|
||||
const tosCheckResult = ref<{ ok: boolean; message: string } | null>(null)
|
||||
|
||||
const minioAvailable = computed(() => {
|
||||
if (config.value.minio?.mode === 'remote') {
|
||||
@@ -373,6 +456,16 @@ async function loadConfig() {
|
||||
path_prefix: d.cos.path_prefix || '',
|
||||
}
|
||||
: defaultConfig().cos!,
|
||||
tos: d.tos
|
||||
? {
|
||||
endpoint: d.tos.endpoint || '',
|
||||
region: d.tos.region || '',
|
||||
access_key: d.tos.access_key || '',
|
||||
secret_key: d.tos.secret_key || '',
|
||||
bucket_name: d.tos.bucket_name || '',
|
||||
path_prefix: d.tos.path_prefix || '',
|
||||
}
|
||||
: defaultConfig().tos!,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -448,6 +541,14 @@ function buildPayload(): StorageEngineConfig {
|
||||
app_id: (config.value.cos?.app_id || '').trim(),
|
||||
path_prefix: (config.value.cos?.path_prefix || '').trim(),
|
||||
},
|
||||
tos: {
|
||||
endpoint: (config.value.tos?.endpoint || '').trim(),
|
||||
region: (config.value.tos?.region || '').trim(),
|
||||
access_key: (config.value.tos?.access_key || '').trim(),
|
||||
secret_key: (config.value.tos?.secret_key || '').trim(),
|
||||
bucket_name: (config.value.tos?.bucket_name || '').trim(),
|
||||
path_prefix: (config.value.tos?.path_prefix || '').trim(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +596,20 @@ async function onCheckCos() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onCheckTos() {
|
||||
checkingTos.value = true
|
||||
tosCheckResult.value = null
|
||||
try {
|
||||
const payload = buildPayload()
|
||||
const res = await checkStorageEngine({ provider: 'tos', tos: payload.tos })
|
||||
tosCheckResult.value = res?.data ?? { ok: false, message: '未知错误' }
|
||||
} catch (e: unknown) {
|
||||
tosCheckResult.value = { ok: false, message: e instanceof Error ? e.message : '请求失败' }
|
||||
} finally {
|
||||
checkingTos.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadAll)
|
||||
</script>
|
||||
|
||||
@@ -590,6 +705,16 @@ onMounted(loadAll)
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.engine-link {
|
||||
color: var(--td-brand-color, #0052d9);
|
||||
text-decoration: none;
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.block-config {
|
||||
margin-top: 6px;
|
||||
padding-top: 14px;
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"github.com/Tencent/WeKnora/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/volcengine/ve-tos-golang-sdk/v2/tos"
|
||||
"github.com/volcengine/ve-tos-golang-sdk/v2/tos/enum"
|
||||
@@ -71,6 +72,34 @@ func NewTosFileServiceWithTempBucket(endpoint, region, accessKey, secretKey, buc
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckConnectivity verifies TOS is reachable by performing a HeadBucket request.
|
||||
func (s *tosFileService) CheckConnectivity(ctx context.Context) error {
|
||||
checkCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
_, err := s.client.HeadBucket(checkCtx, &tos.HeadBucketInput{
|
||||
Bucket: s.bucketName,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// CheckTosConnectivity tests TOS connectivity using the provided credentials.
|
||||
func CheckTosConnectivity(ctx context.Context, endpoint, region, accessKey, secretKey, bucketName string) error {
|
||||
client, err := tos.NewClientV2(
|
||||
endpoint,
|
||||
tos.WithRegion(region),
|
||||
tos.WithCredentials(tos.NewStaticCredentials(accessKey, secretKey)),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize TOS client: %w", err)
|
||||
}
|
||||
checkCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
_, err = client.HeadBucket(checkCtx, &tos.HeadBucketInput{
|
||||
Bucket: bucketName,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func ensureTOSBucket(client *tos.ClientV2, bucketName string) error {
|
||||
_, err := client.HeadBucket(context.Background(), &tos.HeadBucketInput{
|
||||
Bucket: bucketName,
|
||||
@@ -152,7 +181,11 @@ func (s *tosFileService) SaveFile(ctx context.Context, file *multipart.FileHeade
|
||||
}
|
||||
|
||||
func (s *tosFileService) SaveBytes(ctx context.Context, data []byte, tenantID uint64, fileName string, temp bool) (string, error) {
|
||||
ext := filepath.Ext(fileName)
|
||||
safeName, err := utils.SafeFileName(fileName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid file name: %w", err)
|
||||
}
|
||||
ext := filepath.Ext(safeName)
|
||||
reader := bytes.NewReader(data)
|
||||
|
||||
targetBucket := s.bucketName
|
||||
@@ -172,7 +205,7 @@ func (s *tosFileService) SaveBytes(ctx context.Context, data []byte, tenantID ui
|
||||
)
|
||||
}
|
||||
|
||||
_, err := s.client.PutObjectV2(ctx, &tos.PutObjectV2Input{
|
||||
_, err = s.client.PutObjectV2(ctx, &tos.PutObjectV2Input{
|
||||
PutObjectBasicInput: tos.PutObjectBasicInput{
|
||||
Bucket: targetBucket,
|
||||
Key: objectName,
|
||||
@@ -192,6 +225,9 @@ func (s *tosFileService) GetFile(ctx context.Context, filePath string) (io.ReadC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := utils.SafeObjectKey(objectName); err != nil {
|
||||
return nil, fmt.Errorf("invalid file path: %w", err)
|
||||
}
|
||||
|
||||
output, err := s.client.GetObjectV2(ctx, &tos.GetObjectV2Input{
|
||||
Bucket: bucketName,
|
||||
@@ -208,6 +244,9 @@ func (s *tosFileService) DeleteFile(ctx context.Context, filePath string) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := utils.SafeObjectKey(objectName); err != nil {
|
||||
return fmt.Errorf("invalid file path: %w", err)
|
||||
}
|
||||
|
||||
_, err = s.client.DeleteObjectV2(ctx, &tos.DeleteObjectV2Input{
|
||||
Bucket: bucketName,
|
||||
@@ -224,6 +263,9 @@ func (s *tosFileService) GetFileURL(ctx context.Context, filePath string) (strin
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := utils.SafeObjectKey(objectName); err != nil {
|
||||
return "", fmt.Errorf("invalid file path: %w", err)
|
||||
}
|
||||
|
||||
output, err := s.client.PreSignedURL(&tos.PreSignedURLInput{
|
||||
HTTPMethod: enum.HttpMethodGet,
|
||||
|
||||
@@ -473,7 +473,7 @@ func (s *sessionService) KnowledgeQA(
|
||||
knowledgeIDs = nil
|
||||
logger.Infof(ctx, "RetrieveKBOnlyWhenMentioned is enabled and no @ mention found, KB retrieval disabled for this request")
|
||||
} else {
|
||||
knowledgeBaseIDs = s.resolveKnowledgeBasesFromAgent(ctx, customAgent)
|
||||
knowledgeBaseIDs = s.resolveKnowledgeBasesFromAgent(ctx, customAgent, session.TenantID)
|
||||
}
|
||||
|
||||
// Determine chat model ID: prioritize request's summaryModelID, then Remote models
|
||||
@@ -861,7 +861,11 @@ func (s *sessionService) selectChatModelID(
|
||||
return "", errors.New("no chat model ID available: no knowledge bases configured and no available models")
|
||||
}
|
||||
|
||||
// resolveKnowledgeBasesFromAgent resolves knowledge base IDs based on agent's KBSelectionMode
|
||||
// resolveKnowledgeBasesFromAgent resolves knowledge base IDs based on agent's KBSelectionMode.
|
||||
// sessionTenantID is the tenant of the current session (caller); it is compared with
|
||||
// customAgent.TenantID to detect the shared-agent scenario and avoid leaking the
|
||||
// current user's personal shared KBs into the agent's retrieval scope.
|
||||
//
|
||||
// Returns the resolved knowledge base IDs based on the selection mode:
|
||||
// - "all": fetches all knowledge bases for the tenant
|
||||
// - "selected": uses the explicitly configured knowledge bases
|
||||
@@ -870,6 +874,7 @@ func (s *sessionService) selectChatModelID(
|
||||
func (s *sessionService) resolveKnowledgeBasesFromAgent(
|
||||
ctx context.Context,
|
||||
customAgent *types.CustomAgent,
|
||||
sessionTenantID uint64,
|
||||
) []string {
|
||||
if customAgent == nil {
|
||||
return nil
|
||||
@@ -877,7 +882,7 @@ func (s *sessionService) resolveKnowledgeBasesFromAgent(
|
||||
|
||||
switch customAgent.Config.KBSelectionMode {
|
||||
case "all":
|
||||
// Get own knowledge bases
|
||||
// Get own knowledge bases (uses ctx TenantID = agent's tenant)
|
||||
allKBs, err := s.knowledgeBaseService.ListKnowledgeBases(ctx)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to list all knowledge bases: %v", err)
|
||||
@@ -889,23 +894,31 @@ func (s *sessionService) resolveKnowledgeBasesFromAgent(
|
||||
kbIDSet[kb.ID] = true
|
||||
}
|
||||
|
||||
// Also include shared knowledge bases the user has access to
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint64)
|
||||
userIDVal := ctx.Value(types.UserIDContextKey)
|
||||
if userIDVal != nil {
|
||||
if userID, ok := userIDVal.(string); ok && userID != "" && s.kbShareService != nil {
|
||||
sharedList, err := s.kbShareService.ListSharedKnowledgeBases(ctx, userID, tenantID)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to list shared knowledge bases: %v", err)
|
||||
} else {
|
||||
for _, info := range sharedList {
|
||||
if info != nil && info.KnowledgeBase != nil && !kbIDSet[info.KnowledgeBase.ID] {
|
||||
kbIDs = append(kbIDs, info.KnowledgeBase.ID)
|
||||
kbIDSet[info.KnowledgeBase.ID] = true
|
||||
// For shared agents (session tenant != agent tenant), only use the agent
|
||||
// tenant's own KBs. Including the current user's shared KBs would leak
|
||||
// unrelated KBs from other organisations into the agent's retrieval scope.
|
||||
isSharedAgent := sessionTenantID != 0 && sessionTenantID != customAgent.TenantID
|
||||
if !isSharedAgent {
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint64)
|
||||
userIDVal := ctx.Value(types.UserIDContextKey)
|
||||
if userIDVal != nil {
|
||||
if userID, ok := userIDVal.(string); ok && userID != "" && s.kbShareService != nil {
|
||||
sharedList, err := s.kbShareService.ListSharedKnowledgeBases(ctx, userID, tenantID)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to list shared knowledge bases: %v", err)
|
||||
} else {
|
||||
for _, info := range sharedList {
|
||||
if info != nil && info.KnowledgeBase != nil && !kbIDSet[info.KnowledgeBase.ID] {
|
||||
kbIDs = append(kbIDs, info.KnowledgeBase.ID)
|
||||
kbIDSet[info.KnowledgeBase.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Infof(ctx, "Shared agent detected (session tenant %d != agent tenant %d): skipping user's shared KBs",
|
||||
sessionTenantID, customAgent.TenantID)
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "KBSelectionMode=all: loaded %d knowledge bases (own + shared)", len(kbIDs))
|
||||
@@ -1352,7 +1365,7 @@ func (s *sessionService) AgentQA(
|
||||
logger.Infof(ctx, "RetrieveKBOnlyWhenMentioned is enabled and no @ mention found, KB retrieval disabled for this request")
|
||||
} else {
|
||||
// Use agent's configured knowledge bases based on KBSelectionMode
|
||||
agentConfig.KnowledgeBases = s.resolveKnowledgeBasesFromAgent(ctx, customAgent)
|
||||
agentConfig.KnowledgeBases = s.resolveKnowledgeBasesFromAgent(ctx, customAgent, session.TenantID)
|
||||
}
|
||||
|
||||
// Use custom agent's allowed tools if specified, otherwise use defaults
|
||||
|
||||
@@ -342,6 +342,26 @@ func (h *SystemHandler) isCOSConfigured(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isTOSConfigured checks whether TOS connection info is available from tenant config or env.
|
||||
func (h *SystemHandler) isTOSConfigured(c *gin.Context) bool {
|
||||
if v, exists := c.Get(types.TenantInfoContextKey.String()); exists {
|
||||
if tenant, ok := v.(*types.Tenant); ok && tenant != nil && tenant.StorageEngineConfig != nil && tenant.StorageEngineConfig.TOS != nil {
|
||||
tosConf := tenant.StorageEngineConfig.TOS
|
||||
return tosConf.Endpoint != "" && tosConf.Region != "" && tosConf.AccessKey != "" && tosConf.SecretKey != "" && tosConf.BucketName != ""
|
||||
}
|
||||
}
|
||||
return h.isTOSEnvAvailable()
|
||||
}
|
||||
|
||||
// isTOSEnvAvailable checks whether TOS env vars are set.
|
||||
func (h *SystemHandler) isTOSEnvAvailable() bool {
|
||||
return os.Getenv("TOS_ENDPOINT") != "" &&
|
||||
os.Getenv("TOS_REGION") != "" &&
|
||||
os.Getenv("TOS_ACCESS_KEY") != "" &&
|
||||
os.Getenv("TOS_SECRET_KEY") != "" &&
|
||||
os.Getenv("TOS_BUCKET_NAME") != ""
|
||||
}
|
||||
|
||||
// MinioBucketInfo represents bucket information with access policy
|
||||
type MinioBucketInfo struct {
|
||||
Name string `json:"name"`
|
||||
@@ -356,7 +376,7 @@ type ListMinioBucketsResponse struct {
|
||||
|
||||
// StorageEngineStatusItem describes one storage engine's availability and description.
|
||||
type StorageEngineStatusItem struct {
|
||||
Name string `json:"name"` // "local", "minio", "cos"
|
||||
Name string `json:"name"` // "local", "minio", "cos", "tos"
|
||||
Available bool `json:"available"` // whether the engine can be used
|
||||
Description string `json:"description"` // short description for UI
|
||||
}
|
||||
@@ -378,10 +398,12 @@ func (h *SystemHandler) GetStorageEngineStatus(c *gin.Context) {
|
||||
minioConfigured := h.isMinioConfigured(c)
|
||||
minioEnvAvailable := h.isMinioEnvAvailable()
|
||||
cosConfigured := h.isCOSConfigured(c)
|
||||
tosConfigured := h.isTOSConfigured(c)
|
||||
engines := []StorageEngineStatusItem{
|
||||
{Name: "local", Available: true, Description: "本地文件系统存储,仅适合单机部署"},
|
||||
{Name: "minio", Available: minioConfigured || minioEnvAvailable, Description: "S3 兼容的自托管对象存储,适合内网和私有云部署"},
|
||||
{Name: "cos", Available: cosConfigured, Description: "腾讯云对象存储服务,适合公有云部署,支持 CDN 加速"},
|
||||
{Name: "tos", Available: tosConfigured, Description: "火山引擎对象存储服务,适合公有云部署"},
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"code": 0,
|
||||
@@ -654,9 +676,10 @@ func isBlockedStorageEndpoint(endpoint string) (bool, string) {
|
||||
|
||||
// StorageCheckRequest is the body for POST /system/storage-engine-check.
|
||||
type StorageCheckRequest struct {
|
||||
Provider string `json:"provider"` // "minio" or "cos"
|
||||
Provider string `json:"provider"` // "minio", "cos", or "tos"
|
||||
MinIO *types.MinIOEngineConfig `json:"minio,omitempty"`
|
||||
COS *types.COSEngineConfig `json:"cos,omitempty"`
|
||||
TOS *types.TOSEngineConfig `json:"tos,omitempty"`
|
||||
}
|
||||
|
||||
// StorageCheckResponse is the response for a single-engine connectivity check.
|
||||
@@ -688,6 +711,8 @@ func (h *SystemHandler) CheckStorageEngine(c *gin.Context) {
|
||||
h.checkMinio(c, ctx, req.MinIO)
|
||||
case "cos":
|
||||
h.checkCOS(c, ctx, req.COS)
|
||||
case "tos":
|
||||
h.checkTOS(c, ctx, req.TOS)
|
||||
default:
|
||||
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: true, Message: "本地存储无需检测"}})
|
||||
}
|
||||
@@ -772,3 +797,37 @@ func (h *SystemHandler) checkCOS(c *gin.Context, ctx context.Context, cfg *types
|
||||
}
|
||||
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: true, Message: fmt.Sprintf("连接成功,Bucket「%s」已确认存在", cfg.BucketName)}})
|
||||
}
|
||||
|
||||
func (h *SystemHandler) checkTOS(c *gin.Context, ctx context.Context, cfg *types.TOSEngineConfig) {
|
||||
if cfg == nil {
|
||||
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: false, Message: "未提供 TOS 配置"}})
|
||||
return
|
||||
}
|
||||
if cfg.Endpoint == "" || cfg.Region == "" || cfg.AccessKey == "" || cfg.SecretKey == "" || cfg.BucketName == "" {
|
||||
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: false, Message: "Endpoint、Region、Access Key、Secret Key、Bucket 名称不能为空"}})
|
||||
return
|
||||
}
|
||||
|
||||
if blocked, reason := isBlockedStorageEndpoint(cfg.Endpoint); blocked {
|
||||
logger.Warnf(ctx, "Storage check: TOS endpoint blocked by SSRF protection, endpoint: %s", cfg.Endpoint)
|
||||
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: false, Message: reason}})
|
||||
return
|
||||
}
|
||||
|
||||
err := file.CheckTosConnectivity(ctx, cfg.Endpoint, cfg.Region, cfg.AccessKey, cfg.SecretKey, cfg.BucketName)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Storage check: TOS connectivity failed, bucket: %s, error: %v", cfg.BucketName, err)
|
||||
errMsg := err.Error()
|
||||
if strings.Contains(errMsg, "403") {
|
||||
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: false, Message: "认证失败,请检查 Access Key / Secret Key 是否正确"}})
|
||||
return
|
||||
}
|
||||
if strings.Contains(errMsg, "404") {
|
||||
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: false, Message: fmt.Sprintf("Bucket「%s」不存在,请检查名称和 Region", cfg.BucketName)}})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: false, Message: sanitizeStorageCheckError(err)}})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: true, Message: fmt.Sprintf("连接成功,Bucket「%s」已确认存在", cfg.BucketName)}})
|
||||
}
|
||||
|
||||
@@ -294,13 +294,14 @@ func (c *ParserEngineConfig) Scan(value interface{}) error {
|
||||
return json.Unmarshal(b, c)
|
||||
}
|
||||
|
||||
// StorageEngineConfig holds tenant-level storage engine parameters for Local, MinIO, and COS.
|
||||
// StorageEngineConfig holds tenant-level storage engine parameters for Local, MinIO, COS, and TOS.
|
||||
// Knowledge bases select which provider to use; parameters are read from here.
|
||||
type StorageEngineConfig struct {
|
||||
DefaultProvider string `json:"default_provider"` // "local", "minio", "cos"
|
||||
DefaultProvider string `json:"default_provider"` // "local", "minio", "cos", "tos"
|
||||
Local *LocalEngineConfig `json:"local,omitempty"`
|
||||
MinIO *MinIOEngineConfig `json:"minio,omitempty"`
|
||||
COS *COSEngineConfig `json:"cos,omitempty"`
|
||||
TOS *TOSEngineConfig `json:"tos,omitempty"`
|
||||
}
|
||||
|
||||
// LocalEngineConfig is for local file system storage (single-machine deployment only).
|
||||
@@ -330,6 +331,16 @@ type COSEngineConfig struct {
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
}
|
||||
|
||||
// TOSEngineConfig is for Volcengine TOS (火山引擎对象存储).
|
||||
type TOSEngineConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface for StorageEngineConfig
|
||||
func (c *StorageEngineConfig) Value() (driver.Value, error) {
|
||||
if c == nil {
|
||||
|
||||
@@ -270,6 +270,9 @@ start_app() {
|
||||
log_info "环境变量已设置,启动应用..."
|
||||
log_info "数据库地址: $DB_HOST:${DB_PORT:-5432}"
|
||||
|
||||
export CGO_CFLAGS="-Wno-deprecated-declarations -Wno-gnu-folding-constant"
|
||||
export CGO_LDFLAGS="-Wl,-no_warn_duplicate_libraries"
|
||||
|
||||
# 检查是否安装了 Air(热重载工具)
|
||||
if command -v air &> /dev/null; then
|
||||
log_success "检测到 Air,使用热重载模式启动..."
|
||||
@@ -279,8 +282,7 @@ start_app() {
|
||||
log_info "未检测到 Air,使用普通模式启动"
|
||||
log_warning "提示: 安装 Air 可以实现代码修改后自动重启"
|
||||
log_info "安装命令: go install github.com/air-verse/air@latest"
|
||||
# 运行应用
|
||||
go run cmd/server/main.go
|
||||
go run -ldflags="-X 'google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn'" cmd/server/main.go
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user