mirror of
https://github.com/Tencent/WeKnora.git
synced 2025-11-25 19:37:45 +08:00
feat(ui): Support multi knowledgebases operation
This commit is contained in:
28
.github/workflows/docker-image.yml
vendored
28
.github/workflows/docker-image.yml
vendored
@@ -42,8 +42,25 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Read VERSION file
|
||||
run: echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
|
||||
- name: Set up Go
|
||||
if: matrix.service_name == 'app'
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Prepare version info
|
||||
id: version
|
||||
run: |
|
||||
# 使用统一的版本管理脚本
|
||||
eval $(./scripts/get_version.sh env)
|
||||
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "commit_id=$COMMIT_ID" >> $GITHUB_OUTPUT
|
||||
echo "build_time=$BUILD_TIME" >> $GITHUB_OUTPUT
|
||||
echo "go_version=$GO_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# 显示版本信息
|
||||
./scripts/get_version.sh info
|
||||
|
||||
- name: Build ${{ matrix.service_name }} Image
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -52,8 +69,13 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
file: ${{ matrix.file }}
|
||||
context: ${{ matrix.context }}
|
||||
build-args: |
|
||||
${{ matrix.service_name == 'app' && format('VERSION_ARG={0}', steps.version.outputs.version) || '' }}
|
||||
${{ matrix.service_name == 'app' && format('COMMIT_ID_ARG={0}', steps.version.outputs.commit_id) || '' }}
|
||||
${{ matrix.service_name == 'app' && format('BUILD_TIME_ARG={0}', steps.version.outputs.build_time) || '' }}
|
||||
${{ matrix.service_name == 'app' && format('GO_VERSION_ARG={0}', steps.version.outputs.go_version) || '' }}
|
||||
tags: |
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/weknora-${{ matrix.service_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/weknora-${{ matrix.service_name }}:${{ env.VERSION }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/weknora-${{ matrix.service_name }}:${{ steps.version.outputs.version }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/weknora-${{ matrix.service_name }}:cache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/weknora-${{ matrix.service_name }}:cache,mode=max
|
||||
17
Makefile
17
Makefile
@@ -85,7 +85,15 @@ clean:
|
||||
|
||||
# Build Docker image
|
||||
docker-build-app:
|
||||
docker build --platform $(PLATFORM) -f docker/Dockerfile.app -t $(DOCKER_IMAGE):$(DOCKER_TAG) .
|
||||
@echo "获取版本信息..."
|
||||
@eval $$(./scripts/get_version.sh env); \
|
||||
./scripts/get_version.sh info; \
|
||||
docker build --platform $(PLATFORM) \
|
||||
--build-arg VERSION_ARG="$$VERSION" \
|
||||
--build-arg COMMIT_ID_ARG="$$COMMIT_ID" \
|
||||
--build-arg BUILD_TIME_ARG="$$BUILD_TIME" \
|
||||
--build-arg GO_VERSION_ARG="$$GO_VERSION" \
|
||||
-f docker/Dockerfile.app -t $(DOCKER_IMAGE):$(DOCKER_TAG) .
|
||||
|
||||
# Build docreader Docker image
|
||||
docker-build-docreader:
|
||||
@@ -168,7 +176,12 @@ deps:
|
||||
|
||||
# Build for production
|
||||
build-prod:
|
||||
GOOS=linux go build -installsuffix cgo -ldflags="-w -s" -o $(BINARY_NAME) $(MAIN_PATH)
|
||||
@VERSION=$${VERSION:-unknown}; \
|
||||
COMMIT_ID=$${COMMIT_ID:-unknown}; \
|
||||
BUILD_TIME=$${BUILD_TIME:-unknown}; \
|
||||
GO_VERSION=$${GO_VERSION:-unknown}; \
|
||||
LDFLAGS="-X 'github.com/Tencent/WeKnora/internal/handler.Version=$$VERSION' -X 'github.com/Tencent/WeKnora/internal/handler.CommitID=$$COMMIT_ID' -X 'github.com/Tencent/WeKnora/internal/handler.BuildTime=$$BUILD_TIME' -X 'github.com/Tencent/WeKnora/internal/handler.GoVersion=$$GO_VERSION'"; \
|
||||
GOOS=linux go build -installsuffix cgo -ldflags="-w -s $$LDFLAGS" -o $(BINARY_NAME) $(MAIN_PATH)
|
||||
|
||||
clean-db:
|
||||
@echo "Cleaning database..."
|
||||
|
||||
@@ -9,7 +9,7 @@ conversation:
|
||||
keyword_threshold: 0.3
|
||||
embedding_top_k: 10
|
||||
vector_threshold: 0.5
|
||||
rerank_threshold: 0.7
|
||||
rerank_threshold: 0.5
|
||||
rerank_top_k: 5
|
||||
fallback_strategy: "fixed"
|
||||
fallback_response: "抱歉,我无法回答这个问题。"
|
||||
|
||||
@@ -28,7 +28,19 @@ RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
# Get version and commit info for build injection
|
||||
ARG VERSION_ARG
|
||||
ARG COMMIT_ID_ARG
|
||||
ARG BUILD_TIME_ARG
|
||||
ARG GO_VERSION_ARG
|
||||
|
||||
# Set build-time variables
|
||||
ENV VERSION=${VERSION_ARG}
|
||||
ENV COMMIT_ID=${COMMIT_ID_ARG}
|
||||
ENV BUILD_TIME=${BUILD_TIME_ARG}
|
||||
ENV GO_VERSION=${GO_VERSION_ARG}
|
||||
|
||||
# Build the application with version info
|
||||
RUN make build-prod
|
||||
|
||||
# Final stage
|
||||
|
||||
@@ -143,10 +143,10 @@ export async function register(data: RegisterRequest): Promise<RegisterResponse>
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
export async function getCurrentUser(): Promise<{ success: boolean; data?: UserInfo; message?: string }> {
|
||||
export async function getCurrentUser(): Promise<{ success: boolean; data?: { user: UserInfo; tenant: TenantInfo }; message?: string }> {
|
||||
try {
|
||||
const response = await get('/api/v1/auth/me')
|
||||
return response as unknown as { success: boolean; data?: UserInfo; message?: string }
|
||||
return response as unknown as { success: boolean; data?: { user: UserInfo; tenant: TenantInfo }; message?: string }
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { get, post, put, del, postChat } from "../../utils/request";
|
||||
import { loadTestData } from "../test-data";
|
||||
|
||||
|
||||
|
||||
export async function createSessions(data = {}) {
|
||||
await loadTestData();
|
||||
return post("/api/v1/sessions", data);
|
||||
}
|
||||
|
||||
export async function getSessionsList(page: number, page_size: number) {
|
||||
await loadTestData();
|
||||
return get(`/api/v1/sessions?page=${page}&page_size=${page_size}`);
|
||||
}
|
||||
|
||||
export async function generateSessionsTitle(session_id: string, data: any) {
|
||||
await loadTestData();
|
||||
return post(`/api/v1/sessions/${session_id}/generate_title`, data);
|
||||
}
|
||||
|
||||
export async function knowledgeChat(data: { session_id: string; query: string; }) {
|
||||
await loadTestData();
|
||||
return postChat(`/api/v1/knowledge-chat/${data.session_id}`, { query: data.query });
|
||||
}
|
||||
|
||||
export async function getMessageList(data: { session_id: string; limit: number, created_at: string }) {
|
||||
await loadTestData();
|
||||
if (data.created_at) {
|
||||
return get(`/api/v1/messages/${data.session_id}/load?before_time=${encodeURIComponent(data.created_at)}&limit=${data.limit}`);
|
||||
} else {
|
||||
@@ -33,6 +27,5 @@ export async function getMessageList(data: { session_id: string; limit: number,
|
||||
}
|
||||
|
||||
export async function delSession(session_id: string) {
|
||||
await loadTestData();
|
||||
return del(`/api/v1/sessions/${session_id}`);
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import { ref, type Ref, onUnmounted, nextTick } from 'vue'
|
||||
import { generateRandomString } from '@/utils/index';
|
||||
import { getTestData } from '@/utils/request';
|
||||
import { loadTestData } from "../test-data";
|
||||
|
||||
|
||||
|
||||
@@ -37,16 +35,16 @@ export function useStream() {
|
||||
isStreaming.value = true;
|
||||
isLoading.value = true;
|
||||
|
||||
// 使用默认配置
|
||||
await loadTestData();
|
||||
const testData = getTestData();
|
||||
if (!testData) {
|
||||
error.value = "测试数据未初始化,无法进行聊天";
|
||||
// 获取API配置
|
||||
const apiUrl = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080";
|
||||
|
||||
// 获取JWT Token
|
||||
const token = localStorage.getItem('weknora_token');
|
||||
if (!token) {
|
||||
error.value = "未找到登录令牌,请重新登录";
|
||||
stopStream();
|
||||
return;
|
||||
}
|
||||
const apiUrl = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080";
|
||||
const apiKey = testData.tenant.api_key;
|
||||
|
||||
try {
|
||||
let url =
|
||||
@@ -57,7 +55,7 @@ export function useStream() {
|
||||
method: params.method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": apiKey,
|
||||
"Authorization": `Bearer ${token}`,
|
||||
"X-Request-ID": `${generateRandomString(12)}`,
|
||||
},
|
||||
body:
|
||||
|
||||
@@ -63,38 +63,19 @@ export interface DownloadTask {
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
// 系统初始化状态检查
|
||||
export function checkInitializationStatus(): Promise<{ initialized: boolean }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
get('/api/v1/initialization/status')
|
||||
.then((response: any) => {
|
||||
resolve(response.data || { initialized: false });
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// 如果是401,交给全局拦截器去处理(重定向登录),这里不要把它当成未初始化
|
||||
if (error && error.status === 401) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
console.warn('检查初始化状态失败,假设需要初始化:', error);
|
||||
resolve({ initialized: false });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 执行系统初始化
|
||||
export function initializeSystem(config: InitializationConfig): Promise<any> {
|
||||
|
||||
// 根据知识库ID执行配置更新
|
||||
export function initializeSystemByKB(kbId: string, config: InitializationConfig): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('开始系统初始化...', config);
|
||||
post('/api/v1/initialization/initialize', config)
|
||||
console.log('开始知识库配置更新...', kbId, config);
|
||||
post(`/api/v1/initialization/initialize/${kbId}`, config)
|
||||
.then((response: any) => {
|
||||
console.log('系统初始化完成', response);
|
||||
// 设置本地初始化状态标记
|
||||
localStorage.setItem('system_initialized', 'true');
|
||||
console.log('知识库配置更新完成', response);
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('系统初始化失败:', error);
|
||||
console.error('知识库配置更新失败:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
@@ -184,15 +165,15 @@ export function listDownloadTasks(): Promise<DownloadTask[]> {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前系统配置
|
||||
export function getCurrentConfig(): Promise<InitializationConfig & { hasFiles: boolean }> {
|
||||
|
||||
export function getCurrentConfigByKB(kbId: string): Promise<InitializationConfig & { hasFiles: boolean }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
get('/api/v1/initialization/config')
|
||||
get(`/api/v1/initialization/config/${kbId}`)
|
||||
.then((response: any) => {
|
||||
resolve(response.data || {});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('获取当前配置失败:', error);
|
||||
console.error('获取知识库配置失败:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,44 +1,55 @@
|
||||
import { get, post, put, del, postUpload, getDown, getTestData } from "../../utils/request";
|
||||
import { loadTestData } from "../test-data";
|
||||
export async function getDefaultKnowledgeBaseId(): Promise<string> {
|
||||
// 如果设置中没有知识库ID,则使用测试数据
|
||||
await loadTestData();
|
||||
const testData = getTestData();
|
||||
if (!testData || testData.knowledge_bases.length === 0) {
|
||||
throw new Error('没有可用的知识库');
|
||||
import { get, post, put, del, postUpload, getDown } from "../../utils/request";
|
||||
|
||||
// 知识库管理 API(列表、创建、获取、更新、删除、复制)
|
||||
export function listKnowledgeBases() {
|
||||
return get(`/api/v1/knowledge-bases`);
|
||||
}
|
||||
|
||||
return testData.knowledge_bases[0].id;
|
||||
export function createKnowledgeBase(data: { name: string; description?: string; chunking_config?: any }) {
|
||||
return post(`/api/v1/knowledge-bases`, data);
|
||||
}
|
||||
|
||||
export async function uploadKnowledgeBase(data = {}) {
|
||||
const kbId = await getDefaultKnowledgeBaseId();
|
||||
export function getKnowledgeBaseById(id: string) {
|
||||
return get(`/api/v1/knowledge-bases/${id}`);
|
||||
}
|
||||
|
||||
export function updateKnowledgeBase(id: string, data: { name: string; description?: string; config: any }) {
|
||||
return put(`/api/v1/knowledge-bases/${id}` , data);
|
||||
}
|
||||
|
||||
export function deleteKnowledgeBase(id: string) {
|
||||
return del(`/api/v1/knowledge-bases/${id}`);
|
||||
}
|
||||
|
||||
export function copyKnowledgeBase(data: { source_id: string; target_id?: string }) {
|
||||
return post(`/api/v1/knowledge-bases/copy`, data);
|
||||
}
|
||||
|
||||
// 知识文件 API(基于具体知识库)
|
||||
export function uploadKnowledgeFile(kbId: string, data = {}) {
|
||||
return postUpload(`/api/v1/knowledge-bases/${kbId}/knowledge/file`, data);
|
||||
}
|
||||
|
||||
export async function getKnowledgeBase({page, page_size}: {page: number, page_size: number}) {
|
||||
const kbId = await getDefaultKnowledgeBaseId();
|
||||
return get(
|
||||
`/api/v1/knowledge-bases/${kbId}/knowledge?page=${page}&page_size=${page_size}`
|
||||
);
|
||||
export function listKnowledgeFiles(kbId: string, { page, page_size }: { page: number; page_size: number }) {
|
||||
return get(`/api/v1/knowledge-bases/${kbId}/knowledge?page=${page}&page_size=${page_size}`);
|
||||
}
|
||||
|
||||
export function getKnowledgeDetails(id: any) {
|
||||
export function getKnowledgeDetails(id: string) {
|
||||
return get(`/api/v1/knowledge/${id}`);
|
||||
}
|
||||
|
||||
export function delKnowledgeDetails(id: any) {
|
||||
export function delKnowledgeDetails(id: string) {
|
||||
return del(`/api/v1/knowledge/${id}`);
|
||||
}
|
||||
|
||||
export function downKnowledgeDetails(id: any) {
|
||||
export function downKnowledgeDetails(id: string) {
|
||||
return getDown(`/api/v1/knowledge/${id}/download`);
|
||||
}
|
||||
|
||||
export function batchQueryKnowledge(ids: any) {
|
||||
return get(`/api/v1/knowledge/batch?${ids}`);
|
||||
export function batchQueryKnowledge(idsQueryString: string) {
|
||||
return get(`/api/v1/knowledge/batch?${idsQueryString}`);
|
||||
}
|
||||
|
||||
export function getKnowledgeDetailsCon(id: any, page: number) {
|
||||
export function getKnowledgeDetailsCon(id: string, page: number) {
|
||||
return get(`/api/v1/chunks/${id}?page=${page}&page_size=25`);
|
||||
}
|
||||
12
frontend/src/api/system/index.ts
Normal file
12
frontend/src/api/system/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
export interface SystemInfo {
|
||||
version: string
|
||||
commit_id?: string
|
||||
build_time?: string
|
||||
go_version?: string
|
||||
}
|
||||
|
||||
export function getSystemInfo(): Promise<{ data: SystemInfo }> {
|
||||
return get('/api/v1/system/info')
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { get, setTestData } from '../../utils/request';
|
||||
|
||||
export interface TestDataResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
tenant: {
|
||||
id: number;
|
||||
name: string;
|
||||
api_key: string;
|
||||
};
|
||||
knowledge_bases: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
// 是否已加载测试数据
|
||||
let isTestDataLoaded = false;
|
||||
|
||||
/**
|
||||
* 加载测试数据
|
||||
* 在API调用前调用此函数以确保测试数据已加载
|
||||
* @returns Promise<boolean> 是否成功加载
|
||||
*/
|
||||
export async function loadTestData(): Promise<boolean> {
|
||||
// 如果已经加载过,直接返回
|
||||
if (isTestDataLoaded) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('开始加载测试数据...');
|
||||
const response = await get('/api/v1/test-data');
|
||||
console.log('测试数据', response);
|
||||
|
||||
if (response && response.data) {
|
||||
// 设置测试数据
|
||||
setTestData({
|
||||
tenant: response.data.tenant,
|
||||
knowledge_bases: response.data.knowledge_bases
|
||||
});
|
||||
isTestDataLoaded = true;
|
||||
console.log('测试数据加载成功');
|
||||
return true;
|
||||
} else {
|
||||
console.warn('测试数据响应为空');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载测试数据失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置测试数据加载状态,在重新登录或需要强制刷新时调用
|
||||
*/
|
||||
export function resetTestDataLoaded() {
|
||||
isTestDataLoaded = false;
|
||||
// 清空已缓存的测试数据,确保下次调用会重新获取
|
||||
setTestData(null);
|
||||
}
|
||||
@@ -1,25 +1,48 @@
|
||||
<template>
|
||||
<div class="aside_box">
|
||||
<div class="logo_box">
|
||||
<div class="logo_box" @click="router.push('/platform/knowledge-bases')" style="cursor: pointer;">
|
||||
<img class="logo" src="@/assets/img/weknora.png" alt="">
|
||||
</div>
|
||||
|
||||
<!-- 上半部分:知识库和对话 -->
|
||||
<div class="menu_top">
|
||||
<div class="menu_box" :class="{ 'has-submenu': item.children }" v-for="(item, index) in topMenuItems" :key="index">
|
||||
<div @click="gotopage(item.path)"
|
||||
<div @click="handleMenuClick(item.path)"
|
||||
@mouseenter="mouseenteMenu(item.path)" @mouseleave="mouseleaveMenu(item.path)"
|
||||
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
|
||||
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : isMenuItemActive(item.path) ? 'menu_item_active' : '']">
|
||||
<div class="menu_item-box">
|
||||
<div class="menu_icon">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
|
||||
</div>
|
||||
<span class="menu_title" :title="item.path === 'knowledge-bases' && kbMenuItem ? kbMenuItem.title : item.title">{{ item.path === 'knowledge-bases' && kbMenuItem ? kbMenuItem.title : item.title }}</span>
|
||||
<!-- 知识库切换下拉箭头 -->
|
||||
<div v-if="item.path === 'knowledge-bases' && isInKnowledgeBase"
|
||||
class="kb-dropdown-icon"
|
||||
:class="{
|
||||
'rotate-180': showKbDropdown,
|
||||
'active': isMenuItemActive(item.path)
|
||||
}"
|
||||
@click.stop="toggleKbDropdown">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
||||
<path d="M2.5 4.5L6 8L9.5 4.5H2.5Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 知识库切换下拉菜单 -->
|
||||
<div v-if="item.path === 'knowledge-bases' && showKbDropdown && isInKnowledgeBase"
|
||||
class="kb-dropdown-menu">
|
||||
<div v-for="kb in initializedKnowledgeBases"
|
||||
:key="kb.id"
|
||||
class="kb-dropdown-item"
|
||||
:class="{ 'active': kb.name === currentKbName }"
|
||||
@click.stop="switchKnowledgeBase(kb.id)">
|
||||
{{ kb.name }}
|
||||
</div>
|
||||
<span class="menu_title">{{ item.title }}</span>
|
||||
</div>
|
||||
<t-popup overlayInnerClassName="upload-popup" class="placement top center" content="上传知识"
|
||||
placement="top" show-arrow destroy-on-close>
|
||||
<div class="upload-file-wrap" @click="uploadFile" variant="outline"
|
||||
v-if="item.path == 'knowledgeBase'">
|
||||
<div class="upload-file-wrap" @click.stop="uploadFile" variant="outline"
|
||||
v-if="item.path === 'knowledge-bases' && $route.name === 'knowledgeBaseDetail'">
|
||||
<img class="upload-file-icon" :class="[item.path == currentpath ? 'active-upload' : '']"
|
||||
:src="getImgSrc(fileAddIcon)" alt="">
|
||||
</div>
|
||||
@@ -55,14 +78,14 @@
|
||||
<!-- 下半部分:账户信息、系统设置、退出登录 -->
|
||||
<div class="menu_bottom">
|
||||
<div class="menu_box" v-for="(item, index) in bottomMenuItems" :key="'bottom-' + index">
|
||||
<div @click="gotopage(item.path)"
|
||||
<div @click="handleMenuClick(item.path)"
|
||||
@mouseenter="mouseenteMenu(item.path)" @mouseleave="mouseleaveMenu(item.path)"
|
||||
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
|
||||
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : (item.path == currentpath) ? 'menu_item_active' : '']">
|
||||
<div class="menu_item-box">
|
||||
<div class="menu_icon">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
|
||||
</div>
|
||||
<span class="menu_title">{{ item.title }}</span>
|
||||
<span class="menu_title">{{ item.path === 'knowledge-bases' && kbMenuItem ? kbMenuItem.title : item.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,16 +96,16 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, watch, computed, ref, reactive } from 'vue';
|
||||
import { onMounted, watch, computed, ref, reactive, nextTick } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getSessionsList, delSession } from "@/api/chat/index";
|
||||
import { getKnowledgeBaseById, listKnowledgeBases, uploadKnowledgeFile } from '@/api/knowledge-base';
|
||||
import { kbFileTypeVerification } from '@/utils/index';
|
||||
import { useMenuStore } from '@/stores/menu';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
let { requestMethod } = useKnowledgeBase()
|
||||
let uploadInput = ref();
|
||||
const usemenuStore = useMenuStore();
|
||||
const authStore = useAuthStore();
|
||||
@@ -97,52 +120,203 @@ const submenuscrollContainer = ref(null);
|
||||
// 计算总页数
|
||||
const totalPages = computed(() => Math.ceil(total.value / page_size.value));
|
||||
const hasMore = computed(() => currentPage.value < totalPages.value);
|
||||
type MenuItem = { title: string; icon: string; path: string; childrenPath?: string; children?: any[] };
|
||||
const { menuArr } = storeToRefs(usemenuStore);
|
||||
let activeSubmenu = ref(-1);
|
||||
let activeSubmenu = ref<number>(-1);
|
||||
|
||||
// 是否处于知识库详情页
|
||||
const isInKnowledgeBase = computed<boolean>(() => {
|
||||
return route.name === 'knowledgeBaseDetail' || route.name === 'kbCreatChat' || route.name === 'chat' || route.name === 'knowledgeBaseSettings';
|
||||
});
|
||||
|
||||
// 统一的菜单项激活状态判断
|
||||
const isMenuItemActive = (itemPath: string): boolean => {
|
||||
const currentRoute = route.name;
|
||||
|
||||
switch (itemPath) {
|
||||
case 'knowledge-bases':
|
||||
return currentRoute === 'knowledgeBaseList' ||
|
||||
currentRoute === 'knowledgeBaseDetail' ||
|
||||
currentRoute === 'knowledgeBaseSettings';
|
||||
case 'creatChat':
|
||||
return currentRoute === 'kbCreatChat';
|
||||
case 'tenant':
|
||||
return currentRoute === 'tenant';
|
||||
default:
|
||||
return itemPath === currentpath.value;
|
||||
}
|
||||
};
|
||||
|
||||
// 统一的图标激活状态判断
|
||||
const getIconActiveState = (itemPath: string) => {
|
||||
const currentRoute = route.name;
|
||||
|
||||
return {
|
||||
isKbActive: itemPath === 'knowledge-bases' && (
|
||||
currentRoute === 'knowledgeBaseList' ||
|
||||
currentRoute === 'knowledgeBaseDetail' ||
|
||||
currentRoute === 'knowledgeBaseSettings'
|
||||
),
|
||||
isCreatChatActive: itemPath === 'creatChat' && currentRoute === 'kbCreatChat',
|
||||
isTenantActive: itemPath === 'tenant' && currentRoute === 'tenant',
|
||||
isChatActive: itemPath === 'chat' && currentRoute === 'chat'
|
||||
};
|
||||
};
|
||||
|
||||
// 分离上下两部分菜单
|
||||
const topMenuItems = computed(() => {
|
||||
return menuArr.value.filter(item =>
|
||||
item.path === 'knowledgeBase' || item.path === 'creatChat'
|
||||
const topMenuItems = computed<MenuItem[]>(() => {
|
||||
return (menuArr.value as unknown as MenuItem[]).filter((item: MenuItem) =>
|
||||
item.path === 'knowledge-bases' || (isInKnowledgeBase.value && item.path === 'creatChat')
|
||||
);
|
||||
});
|
||||
|
||||
const bottomMenuItems = computed(() => {
|
||||
return menuArr.value.filter(item =>
|
||||
item.path !== 'knowledgeBase' && item.path !== 'creatChat'
|
||||
);
|
||||
const bottomMenuItems = computed<MenuItem[]>(() => {
|
||||
return (menuArr.value as unknown as MenuItem[]).filter((item: MenuItem) => {
|
||||
if (item.path === 'knowledge-bases' || item.path === 'creatChat') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// 当前知识库名称和列表
|
||||
const currentKbName = ref<string>('')
|
||||
const allKnowledgeBases = ref<Array<{ id: string; name: string; embedding_model_id?: string; summary_model_id?: string }>>([])
|
||||
const showKbDropdown = ref<boolean>(false)
|
||||
|
||||
// 过滤已初始化的知识库
|
||||
const initializedKnowledgeBases = computed(() => {
|
||||
return allKnowledgeBases.value.filter(kb =>
|
||||
kb.embedding_model_id && kb.embedding_model_id !== '' &&
|
||||
kb.summary_model_id && kb.summary_model_id !== ''
|
||||
)
|
||||
})
|
||||
|
||||
// 动态更新知识库菜单项标题
|
||||
const kbMenuItem = computed(() => {
|
||||
const kbItem = topMenuItems.value.find(item => item.path === 'knowledge-bases')
|
||||
if (kbItem && isInKnowledgeBase.value && currentKbName.value) {
|
||||
return { ...kbItem, title: currentKbName.value }
|
||||
}
|
||||
return kbItem
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const uploadFile = () => {
|
||||
const uploadFile = async () => {
|
||||
// 获取当前知识库ID
|
||||
const currentKbId = await getCurrentKbId();
|
||||
|
||||
// 检查当前知识库的初始化状态
|
||||
if (currentKbId) {
|
||||
try {
|
||||
const kbResponse = await getKnowledgeBaseById(currentKbId);
|
||||
const kb = kbResponse.data;
|
||||
|
||||
// 检查知识库是否已初始化(有 EmbeddingModelID 和 SummaryModelID)
|
||||
if (!kb.embedding_model_id || kb.embedding_model_id === '' ||
|
||||
!kb.summary_model_id || kb.summary_model_id === '') {
|
||||
MessagePlugin.warning("该知识库尚未完成初始化配置,请先前往设置页面配置模型信息后再上传文件");
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取知识库信息失败:', error);
|
||||
MessagePlugin.error("获取知识库信息失败,无法上传文件");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uploadInput.value.click()
|
||||
}
|
||||
const upload = (e) => {
|
||||
requestMethod(e.target.files[0], uploadInput)
|
||||
const upload = async (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// 文件类型验证
|
||||
if (kbFileTypeVerification(file)) {
|
||||
return;
|
||||
}
|
||||
const mouseenteBotDownr = (val) => {
|
||||
|
||||
// 获取当前知识库ID
|
||||
const currentKbId = (route.params as any)?.kbId as string;
|
||||
if (!currentKbId) {
|
||||
MessagePlugin.error("缺少知识库ID");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await uploadKnowledgeFile(currentKbId, { file });
|
||||
const responseData = result as any;
|
||||
console.log('上传API返回结果:', responseData);
|
||||
|
||||
// 如果没有抛出异常,就认为上传成功,先触发刷新事件
|
||||
console.log('文件上传完成,发送事件通知页面刷新,知识库ID:', currentKbId);
|
||||
window.dispatchEvent(new CustomEvent('knowledgeFileUploaded', {
|
||||
detail: { kbId: currentKbId }
|
||||
}));
|
||||
|
||||
// 然后处理UI消息
|
||||
// 判断上传是否成功 - 检查多种可能的成功标识
|
||||
const isSuccess = responseData.success || responseData.code === 200 || responseData.status === 'success' || (!responseData.error && responseData);
|
||||
|
||||
if (isSuccess) {
|
||||
MessagePlugin.info("上传成功!");
|
||||
} else {
|
||||
// 改进错误信息提取逻辑
|
||||
let errorMessage = "上传失败!";
|
||||
if (responseData.error && responseData.error.message) {
|
||||
errorMessage = responseData.error.message;
|
||||
} else if (responseData.message) {
|
||||
errorMessage = responseData.message;
|
||||
}
|
||||
if (responseData.code === 'duplicate_file' || (responseData.error && responseData.error.code === 'duplicate_file')) {
|
||||
errorMessage = "文件已存在";
|
||||
}
|
||||
MessagePlugin.error(errorMessage);
|
||||
}
|
||||
} catch (err: any) {
|
||||
let errorMessage = "上传失败!";
|
||||
if (err.code === 'duplicate_file') {
|
||||
errorMessage = "文件已存在";
|
||||
} else if (err.error && err.error.message) {
|
||||
errorMessage = err.error.message;
|
||||
} else if (err.message) {
|
||||
errorMessage = err.message;
|
||||
}
|
||||
MessagePlugin.error(errorMessage);
|
||||
} finally {
|
||||
uploadInput.value.value = "";
|
||||
}
|
||||
}
|
||||
const mouseenteBotDownr = (val: number) => {
|
||||
activeSubmenu.value = val;
|
||||
}
|
||||
const mouseleaveBotDown = () => {
|
||||
activeSubmenu.value = -1;
|
||||
}
|
||||
const onVisibleChange = (e) => {
|
||||
const onVisibleChange = (_e: any) => {
|
||||
}
|
||||
|
||||
const delCard = (index, item) => {
|
||||
delSession(item.id).then(res => {
|
||||
if (res && res.success) {
|
||||
menuArr.value[1].children.splice(index, 1);
|
||||
const delCard = (index: number, item: any) => {
|
||||
delSession(item.id).then((res: any) => {
|
||||
if (res && (res as any).success) {
|
||||
(menuArr.value as any[])[1]?.children?.splice(index, 1);
|
||||
if (item.id == route.params.chatid) {
|
||||
router.push('/platform/creatChat');
|
||||
// 删除当前会话后,跳转到当前知识库的创建聊天页面
|
||||
const kbId = route.params.kbId;
|
||||
if (kbId) {
|
||||
router.push(`/platform/knowledge-bases/${kbId}/creatChat`);
|
||||
} else {
|
||||
router.push('/platform/knowledge-bases');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MessagePlugin.error("删除失败,请稍后再试!");
|
||||
}
|
||||
})
|
||||
}
|
||||
const debounce = (fn, delay) => {
|
||||
let timer
|
||||
return (...args) => {
|
||||
const debounce = (fn: (...args: any[]) => void, delay: number) => {
|
||||
let timer: ReturnType<typeof setTimeout>
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => fn(...args), delay)
|
||||
}
|
||||
@@ -160,86 +334,216 @@ const checkScrollBottom = () => {
|
||||
}
|
||||
}
|
||||
const handleScroll = debounce(checkScrollBottom, 200)
|
||||
const getMessageList = () => {
|
||||
const getMessageList = async () => {
|
||||
// 仅在知识库内部显示对话列表
|
||||
if (!isInKnowledgeBase.value) {
|
||||
usemenuStore.clearMenuArr();
|
||||
currentKbName.value = '';
|
||||
return;
|
||||
}
|
||||
let kbId = (route.params as any)?.kbId as string
|
||||
// 新的路由格式:/platform/chat/:kbId/:chatid,直接从路由参数获取知识库ID
|
||||
if (!kbId) {
|
||||
usemenuStore.clearMenuArr();
|
||||
currentKbName.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取知识库名称和所有知识库列表
|
||||
try {
|
||||
const [kbRes, allKbRes]: any[] = await Promise.all([
|
||||
getKnowledgeBaseById(kbId),
|
||||
listKnowledgeBases()
|
||||
])
|
||||
if (kbRes?.data?.name) {
|
||||
currentKbName.value = kbRes.data.name
|
||||
}
|
||||
if (allKbRes?.data) {
|
||||
allKnowledgeBases.value = allKbRes.data
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
usemenuStore.clearMenuArr();
|
||||
getSessionsList(currentPage.value, page_size.value).then(res => {
|
||||
getSessionsList(currentPage.value, page_size.value).then((res: any) => {
|
||||
if (res.data && res.data.length) {
|
||||
res.data.forEach(item => {
|
||||
let obj = { title: item.title ? item.title : "新会话", path: `chat/${item.id}`, id: item.id, isMore: false, isNoTitle: item.title ? false : true }
|
||||
// 过滤出当前知识库的会话
|
||||
const filtered = res.data.filter((s: any) => s.knowledge_base_id === kbId)
|
||||
filtered.forEach((item: any) => {
|
||||
let obj = { title: item.title ? item.title : "新会话", path: `chat/${kbId}/${item.id}`, id: item.id, isMore: false, isNoTitle: item.title ? false : true }
|
||||
usemenuStore.updatemenuArr(obj)
|
||||
});
|
||||
loading.value = false;
|
||||
}
|
||||
if (res.total) {
|
||||
total.value = res.total;
|
||||
if ((res as any).total) {
|
||||
total.value = (res as any).total;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openMore = (e) => { }
|
||||
const openMore = (_e: any) => { }
|
||||
onMounted(() => {
|
||||
currentpath.value = route.name;
|
||||
if (route.params.chatid) {
|
||||
currentSecondpath.value = `${route.name}/${route.params.chatid}`;
|
||||
const routeName = typeof route.name === 'string' ? route.name : (route.name ? String(route.name) : '')
|
||||
currentpath.value = routeName;
|
||||
if (route.params.chatid && route.params.kbId) {
|
||||
currentSecondpath.value = `chat/${route.params.kbId}/${route.params.chatid}`;
|
||||
}
|
||||
getMessageList();
|
||||
});
|
||||
|
||||
watch([() => route.name, () => route.params], (newvalue) => {
|
||||
currentpath.value = newvalue[0];
|
||||
if (newvalue[1].chatid) {
|
||||
currentSecondpath.value = `${newvalue[0]}/${newvalue[1].chatid}`;
|
||||
const nameStr = typeof newvalue[0] === 'string' ? (newvalue[0] as string) : (newvalue[0] ? String(newvalue[0]) : '')
|
||||
currentpath.value = nameStr;
|
||||
if (newvalue[1].chatid && newvalue[1].kbId) {
|
||||
currentSecondpath.value = `chat/${newvalue[1].kbId}/${newvalue[1].chatid}`;
|
||||
} else {
|
||||
currentSecondpath.value = "";
|
||||
}
|
||||
|
||||
// 路由变化时刷新对话列表(仅在知识库内部)
|
||||
getMessageList();
|
||||
// 路由变化时更新图标状态
|
||||
getIcon(nameStr);
|
||||
});
|
||||
let fileAddIcon = ref('file-add-green.svg');
|
||||
let knowledgeIcon = ref('zhishiku-green.svg');
|
||||
let prefixIcon = ref('prefixIcon.svg');
|
||||
let settingIcon = ref('setting.svg');
|
||||
let logoutIcon = ref('logout.svg');
|
||||
let tenantIcon = ref('user.svg'); // 使用专门的用户图标
|
||||
let pathPrefix = ref(route.name)
|
||||
const getIcon = (path) => {
|
||||
fileAddIcon.value = path == 'knowledgeBase' ? 'file-add-green.svg' : 'file-add.svg';
|
||||
knowledgeIcon.value = path == 'knowledgeBase' ? 'zhishiku-green.svg' : 'zhishiku.svg';
|
||||
prefixIcon.value = path == 'creatChat' ? 'prefixIcon-green.svg' : path == 'knowledgeBase' ? 'prefixIcon-grey.svg' : 'prefixIcon.svg';
|
||||
settingIcon.value = path == 'settings' ? 'setting-green.svg' : 'setting.svg';
|
||||
tenantIcon.value = path == 'tenant' ? 'user-green.svg' : 'user.svg'; // 使用专门的用户图标
|
||||
const getIcon = (path: string) => {
|
||||
// 根据当前路由状态更新所有图标
|
||||
const kbActiveState = getIconActiveState('knowledge-bases');
|
||||
const creatChatActiveState = getIconActiveState('creatChat');
|
||||
const tenantActiveState = getIconActiveState('tenant');
|
||||
|
||||
// 上传图标:只在知识库相关页面显示绿色
|
||||
fileAddIcon.value = kbActiveState.isKbActive ? 'file-add-green.svg' : 'file-add.svg';
|
||||
|
||||
// 知识库图标:只在知识库页面显示绿色
|
||||
knowledgeIcon.value = kbActiveState.isKbActive ? 'zhishiku-green.svg' : 'zhishiku.svg';
|
||||
|
||||
// 对话图标:只在对话创建页面显示绿色,在知识库页面显示灰色,其他情况显示默认
|
||||
prefixIcon.value = creatChatActiveState.isCreatChatActive ? 'prefixIcon-green.svg' :
|
||||
kbActiveState.isKbActive ? 'prefixIcon-grey.svg' :
|
||||
'prefixIcon.svg';
|
||||
|
||||
// 租户图标:只在租户页面显示绿色
|
||||
tenantIcon.value = tenantActiveState.isTenantActive ? 'user-green.svg' : 'user.svg';
|
||||
|
||||
// 退出图标:始终显示默认
|
||||
logoutIcon.value = 'logout.svg';
|
||||
}
|
||||
getIcon(route.name)
|
||||
const gotopage = (path) => {
|
||||
getIcon(typeof route.name === 'string' ? route.name as string : (route.name ? String(route.name) : ''))
|
||||
const handleMenuClick = async (path: string) => {
|
||||
if (path === 'knowledge-bases') {
|
||||
// 知识库菜单项:如果在知识库内部,跳转到当前知识库文件页;否则跳转到知识库列表
|
||||
const kbId = await getCurrentKbId()
|
||||
if (kbId) {
|
||||
router.push(`/platform/knowledge-bases/${kbId}`)
|
||||
} else {
|
||||
router.push('/platform/knowledge-bases')
|
||||
}
|
||||
} else {
|
||||
gotopage(path)
|
||||
}
|
||||
}
|
||||
|
||||
const getCurrentKbId = async (): Promise<string | null> => {
|
||||
let kbId = (route.params as any)?.kbId as string
|
||||
// 新的路由格式:/platform/chat/:kbId/:chatid,直接从路由参数获取
|
||||
if (!kbId && route.name === 'chat' && (route.params as any)?.kbId) {
|
||||
kbId = (route.params as any).kbId
|
||||
}
|
||||
return kbId || null
|
||||
}
|
||||
|
||||
const gotopage = async (path: string) => {
|
||||
pathPrefix.value = path;
|
||||
// 处理退出登录
|
||||
if (path === 'logout') {
|
||||
authStore.logout();
|
||||
router.push('/login');
|
||||
return;
|
||||
} else {
|
||||
if (path === 'creatChat') {
|
||||
const kbId = await getCurrentKbId()
|
||||
if (kbId) {
|
||||
router.push(`/platform/knowledge-bases/${kbId}/creatChat`)
|
||||
} else {
|
||||
router.push(`/platform/knowledge-bases`)
|
||||
}
|
||||
} else {
|
||||
router.push(`/platform/${path}`);
|
||||
}
|
||||
}
|
||||
getIcon(path)
|
||||
}
|
||||
|
||||
const getImgSrc = (url) => {
|
||||
const getImgSrc = (url: string) => {
|
||||
return new URL(`/src/assets/img/${url}`, import.meta.url).href;
|
||||
}
|
||||
|
||||
const mouseenteMenu = (path) => {
|
||||
if (pathPrefix.value != 'knowledgeBase' && pathPrefix.value != 'creatChat' && path != 'knowledgeBase') {
|
||||
const mouseenteMenu = (path: string) => {
|
||||
if (pathPrefix.value != 'knowledge-bases' && pathPrefix.value != 'creatChat' && path != 'knowledge-bases') {
|
||||
prefixIcon.value = 'prefixIcon-grey.svg';
|
||||
}
|
||||
}
|
||||
const mouseleaveMenu = (path) => {
|
||||
if (pathPrefix.value != 'knowledgeBase' && pathPrefix.value != 'creatChat' && path != 'knowledgeBase') {
|
||||
getIcon(route.name)
|
||||
const mouseleaveMenu = (path: string) => {
|
||||
if (pathPrefix.value != 'knowledge-bases' && pathPrefix.value != 'creatChat' && path != 'knowledge-bases') {
|
||||
const nameStr = typeof route.name === 'string' ? route.name as string : (route.name ? String(route.name) : '')
|
||||
getIcon(nameStr)
|
||||
}
|
||||
}
|
||||
|
||||
// 知识库下拉相关方法
|
||||
const toggleKbDropdown = (event?: Event) => {
|
||||
if (event) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
showKbDropdown.value = !showKbDropdown.value
|
||||
}
|
||||
|
||||
const switchKnowledgeBase = (kbId: string, event?: Event) => {
|
||||
if (event) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
showKbDropdown.value = false
|
||||
const currentRoute = route.name
|
||||
|
||||
// 路由跳转
|
||||
if (currentRoute === 'knowledgeBaseDetail') {
|
||||
router.push(`/platform/knowledge-bases/${kbId}`)
|
||||
} else if (currentRoute === 'kbCreatChat') {
|
||||
router.push(`/platform/knowledge-bases/${kbId}/creatChat`)
|
||||
} else if (currentRoute === 'knowledgeBaseSettings') {
|
||||
router.push(`/platform/knowledge-bases/${kbId}/settings`)
|
||||
} else {
|
||||
router.push(`/platform/knowledge-bases/${kbId}`)
|
||||
}
|
||||
|
||||
// 刷新右侧内容 - 通过触发页面重新加载或发送事件
|
||||
nextTick(() => {
|
||||
// 发送全局事件通知页面刷新知识库内容
|
||||
window.dispatchEvent(new CustomEvent('knowledgeBaseChanged', {
|
||||
detail: { kbId }
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
const handleClickOutside = () => {
|
||||
showKbDropdown.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
watch(() => route.params.kbId, () => {
|
||||
showKbDropdown.value = false
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.del_submenu {
|
||||
@@ -406,6 +710,10 @@ const mouseleaveMenu = (path) => {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
max-width: 120px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
@@ -491,6 +799,92 @@ const mouseleaveMenu = (path) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 知识库下拉菜单样式 */
|
||||
.kb-dropdown-icon {
|
||||
margin-left: auto;
|
||||
color: #666;
|
||||
transition: transform 0.3s ease, color 0.2s ease;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #07c05f;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #07c05f;
|
||||
}
|
||||
|
||||
&.active:hover {
|
||||
color: #05a04f;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transition: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.kb-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.kb-dropdown-item {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #07c05f1a;
|
||||
color: #07c05f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu_item-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu_box {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.upload-popup {
|
||||
|
||||
@@ -3,29 +3,32 @@ import { storeToRefs } from "pinia";
|
||||
import { formatStringDate, kbFileTypeVerification } from "../utils/index";
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
import {
|
||||
uploadKnowledgeBase,
|
||||
getKnowledgeBase,
|
||||
uploadKnowledgeFile,
|
||||
listKnowledgeFiles,
|
||||
getKnowledgeDetails,
|
||||
delKnowledgeDetails,
|
||||
getKnowledgeDetailsCon,
|
||||
} from "@/api/knowledge-base/index";
|
||||
import { knowledgeStore } from "@/stores/knowledge";
|
||||
import { useRoute } from 'vue-router';
|
||||
const usemenuStore = knowledgeStore();
|
||||
export default function () {
|
||||
export default function (knowledgeBaseId?: string) {
|
||||
const route = useRoute();
|
||||
const { cardList, total } = storeToRefs(usemenuStore);
|
||||
let moreIndex = ref(-1);
|
||||
const details = reactive({
|
||||
title: "",
|
||||
time: "",
|
||||
md: [],
|
||||
md: [] as any[],
|
||||
id: "",
|
||||
total: 0
|
||||
});
|
||||
const getKnowled = (query = { page: 1, page_size: 35 }) => {
|
||||
getKnowledgeBase(query)
|
||||
if (!knowledgeBaseId) return;
|
||||
listKnowledgeFiles(knowledgeBaseId, query)
|
||||
.then((result: any) => {
|
||||
let { data, total: totalResult } = result;
|
||||
let cardList_ = data.map((item) => {
|
||||
let cardList_ = data.map((item: any) => {
|
||||
item["file_name"] = item.file_name.substring(
|
||||
0,
|
||||
item.file_name.lastIndexOf(".")
|
||||
@@ -38,16 +41,16 @@ export default function () {
|
||||
};
|
||||
});
|
||||
if (query.page == 1) {
|
||||
cardList.value = cardList_;
|
||||
cardList.value = cardList_ as any[];
|
||||
} else {
|
||||
cardList.value.push(...cardList_);
|
||||
(cardList.value as any[]).push(...cardList_);
|
||||
}
|
||||
total.value = totalResult;
|
||||
})
|
||||
.catch((err) => {});
|
||||
.catch((_err) => {});
|
||||
};
|
||||
const delKnowledge = (index: number, item) => {
|
||||
cardList.value[index].isMore = false;
|
||||
const delKnowledge = (index: number, item: any) => {
|
||||
(cardList.value as any[])[index].isMore = false;
|
||||
moreIndex.value = -1;
|
||||
delKnowledgeDetails(item.id)
|
||||
.then((result: any) => {
|
||||
@@ -58,7 +61,7 @@ export default function () {
|
||||
MessagePlugin.error("知识删除失败!");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch((_err) => {
|
||||
MessagePlugin.error("知识删除失败!");
|
||||
});
|
||||
};
|
||||
@@ -70,12 +73,30 @@ export default function () {
|
||||
moreIndex.value = -1;
|
||||
}
|
||||
};
|
||||
const requestMethod = (file: any, uploadInput) => {
|
||||
const requestMethod = (file: any, uploadInput: any) => {
|
||||
if (file instanceof File && uploadInput) {
|
||||
if (kbFileTypeVerification(file)) {
|
||||
return;
|
||||
}
|
||||
uploadKnowledgeBase({ file })
|
||||
// 每次上传时动态解析当前 kbId(优先:路由 -> URL -> 初始参数)
|
||||
let currentKbId: string | undefined;
|
||||
try {
|
||||
currentKbId = (route.params as any)?.kbId as string;
|
||||
} catch {}
|
||||
if (!currentKbId && typeof window !== 'undefined') {
|
||||
try {
|
||||
const match = window.location.pathname.match(/knowledge-bases\/([^/]+)/);
|
||||
if (match && match[1]) currentKbId = match[1];
|
||||
} catch {}
|
||||
}
|
||||
if (!currentKbId) {
|
||||
currentKbId = knowledgeBaseId;
|
||||
}
|
||||
if (!currentKbId) {
|
||||
MessagePlugin.error("缺少知识库ID");
|
||||
return;
|
||||
}
|
||||
uploadKnowledgeFile(currentKbId, { file })
|
||||
.then((result: any) => {
|
||||
if (result.success) {
|
||||
MessagePlugin.info("上传成功!");
|
||||
@@ -83,27 +104,20 @@ export default function () {
|
||||
} else {
|
||||
// 改进错误信息提取逻辑
|
||||
let errorMessage = "上传失败!";
|
||||
|
||||
// 优先从 error 对象中获取错误信息
|
||||
if (result.error && result.error.message) {
|
||||
errorMessage = result.error.message;
|
||||
} else if (result.message) {
|
||||
errorMessage = result.message;
|
||||
}
|
||||
|
||||
// 检查错误码,如果是重复文件则显示特定提示
|
||||
if (result.code === 'duplicate_file' || (result.error && result.error.code === 'duplicate_file')) {
|
||||
errorMessage = "文件已存在";
|
||||
}
|
||||
|
||||
MessagePlugin.error(errorMessage);
|
||||
}
|
||||
uploadInput.value.value = "";
|
||||
})
|
||||
.catch((err: any) => {
|
||||
// 改进 catch 中的错误处理
|
||||
let errorMessage = "上传失败!";
|
||||
|
||||
if (err.code === 'duplicate_file') {
|
||||
errorMessage = "文件已存在";
|
||||
} else if (err.error && err.error.message) {
|
||||
@@ -111,7 +125,6 @@ export default function () {
|
||||
} else if (err.message) {
|
||||
errorMessage = err.message;
|
||||
}
|
||||
|
||||
MessagePlugin.error(errorMessage);
|
||||
uploadInput.value.value = "";
|
||||
});
|
||||
@@ -119,7 +132,7 @@ export default function () {
|
||||
MessagePlugin.error("file文件类型错误!");
|
||||
}
|
||||
};
|
||||
const getCardDetails = (item) => {
|
||||
const getCardDetails = (item: any) => {
|
||||
Object.assign(details, {
|
||||
title: "",
|
||||
time: "",
|
||||
@@ -137,23 +150,23 @@ export default function () {
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {});
|
||||
.catch((_err) => {});
|
||||
getfDetails(item.id, 1);
|
||||
};
|
||||
const getfDetails = (id, page) => {
|
||||
const getfDetails = (id: string, page: number) => {
|
||||
getKnowledgeDetailsCon(id, page)
|
||||
.then((result: any) => {
|
||||
if (result.success && result.data) {
|
||||
let { data, total: totalResult } = result;
|
||||
if (page == 1) {
|
||||
details.md = data;
|
||||
(details.md as any[]) = data;
|
||||
} else {
|
||||
details.md.push(...data);
|
||||
(details.md as any[]).push(...data);
|
||||
}
|
||||
details.total = totalResult;
|
||||
}
|
||||
})
|
||||
.catch((err) => {});
|
||||
.catch((_err) => {});
|
||||
};
|
||||
return {
|
||||
cardList,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { checkInitializationStatus } from '@/api/initialization'
|
||||
import { listKnowledgeBases } from '@/api/knowledge-base'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { validateToken } from '@/api/auth'
|
||||
|
||||
@@ -8,7 +8,7 @@ const router = createRouter({
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
redirect: "/platform/knowledgeBase",
|
||||
redirect: "/platform/knowledge-bases",
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
@@ -16,12 +16,6 @@ const router = createRouter({
|
||||
component: () => import("../views/auth/Login.vue"),
|
||||
meta: { requiresAuth: false, requiresInit: false }
|
||||
},
|
||||
{
|
||||
path: "/initialization",
|
||||
name: "initialization",
|
||||
component: () => import("../views/initialization/InitializationConfig.vue"),
|
||||
meta: { requiresInit: false } // 初始化页面不需要检查初始化状态
|
||||
},
|
||||
{
|
||||
path: "/knowledgeBase",
|
||||
name: "home",
|
||||
@@ -42,28 +36,35 @@ const router = createRouter({
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "knowledgeBase",
|
||||
name: "knowledgeBase",
|
||||
path: "knowledge-bases",
|
||||
name: "knowledgeBaseList",
|
||||
component: () => import("../views/knowledge/KnowledgeBaseList.vue"),
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "knowledge-bases/:kbId",
|
||||
name: "knowledgeBaseDetail",
|
||||
component: () => import("../views/knowledge/KnowledgeBase.vue"),
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "creatChat",
|
||||
name: "creatChat",
|
||||
path: "knowledge-bases/:kbId/creatChat",
|
||||
name: "kbCreatChat",
|
||||
component: () => import("../views/creatChat/creatChat.vue"),
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "chat/:chatid",
|
||||
name: "chat",
|
||||
component: () => import("../views/chat/index.vue"),
|
||||
path: "knowledge-bases/:kbId/settings",
|
||||
name: "knowledgeBaseSettings",
|
||||
component: () => import("../views/initialization/InitializationContent.vue"),
|
||||
props: { isKbSettings: true },
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
name: "settings",
|
||||
component: () => import("../views/settings/SystemSettings.vue"),
|
||||
meta: { requiresInit: true }
|
||||
path: "chat/:kbId/:chatid",
|
||||
name: "chat",
|
||||
component: () => import("../views/chat/index.vue"),
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -110,33 +111,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
// }
|
||||
}
|
||||
|
||||
// 检查系统初始化状态
|
||||
if (to.meta.requiresInit !== false) {
|
||||
try {
|
||||
const { initialized } = await checkInitializationStatus()
|
||||
|
||||
if (initialized) {
|
||||
// 系统已初始化,记录到本地存储并正常跳转
|
||||
localStorage.setItem('system_initialized', 'true')
|
||||
next()
|
||||
} else {
|
||||
// 系统未初始化,跳转到初始化页面
|
||||
next('/initialization')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查初始化状态失败:', error)
|
||||
// 如果是401,跳转登录,不再误导去初始化
|
||||
const status = (error as any)?.status
|
||||
if (status === 401) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
// 其他错误默认认为需要初始化
|
||||
next('/initialization')
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
});
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { resetTestDataLoaded } from '@/api/test-data'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { UserInfo, TenantInfo, KnowledgeBaseInfo } from '@/api/auth'
|
||||
|
||||
@@ -85,10 +84,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
localStorage.removeItem('weknora_knowledge_bases')
|
||||
localStorage.removeItem('weknora_current_kb')
|
||||
|
||||
// 重置测试数据加载标志,确保重新登录后会重新获取KB列表
|
||||
try {
|
||||
resetTestDataLoaded()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const initFromStorage = () => {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { defineStore } from "pinia";
|
||||
|
||||
export const knowledgeStore = defineStore("knowledge", {
|
||||
state: () => ({
|
||||
cardList: ref([]),
|
||||
total: ref(0),
|
||||
cardList: ref<any[]>([]),
|
||||
total: ref<number>(0),
|
||||
}),
|
||||
actions: {},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defineStore } from 'pinia';
|
||||
export const useMenuStore = defineStore('menuStore', {
|
||||
state: () => ({
|
||||
menuArr: reactive([
|
||||
{ title: '知识库', icon: 'zhishiku', path: 'knowledgeBase' },
|
||||
{ title: '知识库', icon: 'zhishiku', path: 'knowledge-bases' },
|
||||
{
|
||||
title: '对话',
|
||||
icon: 'prefixIcon',
|
||||
@@ -13,8 +13,7 @@ export const useMenuStore = defineStore('menuStore', {
|
||||
childrenPath: 'chat',
|
||||
children: reactive<object[]>([]),
|
||||
},
|
||||
{ title: '账户信息', icon: 'tenant', path: 'tenant' },
|
||||
{ title: '系统设置', icon: 'setting', path: 'settings' },
|
||||
{ title: '系统信息', icon: 'tenant', path: 'tenant' },
|
||||
{ title: '退出登录', icon: 'logout', path: 'logout' }
|
||||
]),
|
||||
isFirstSession: false,
|
||||
|
||||
@@ -10,19 +10,19 @@ export function generateRandomString(length: number) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function formatStringDate(date) {
|
||||
export function formatStringDate(date: any) {
|
||||
let data = new Date(date);
|
||||
let year = data.getFullYear();
|
||||
let month = data.getMonth() + 1;
|
||||
let day = data.getDate();
|
||||
let hour = data.getHours();
|
||||
let minute = data.getMinutes();
|
||||
let second = data.getSeconds();
|
||||
let month = String(data.getMonth() + 1).padStart(2, '0');
|
||||
let day = String(data.getDate()).padStart(2, '0');
|
||||
let hour = String(data.getHours()).padStart(2, '0');
|
||||
let minute = String(data.getMinutes()).padStart(2, '0');
|
||||
let second = String(data.getSeconds()).padStart(2, '0');
|
||||
return (
|
||||
year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second
|
||||
);
|
||||
}
|
||||
export function kbFileTypeVerification(file) {
|
||||
export function kbFileTypeVerification(file: any) {
|
||||
let validTypes = ["pdf", "txt", "md", "docx", "doc", "jpg", "jpeg", "png"];
|
||||
let type = file.name.substring(file.name.lastIndexOf(".") + 1);
|
||||
if (!validTypes.includes(type)) {
|
||||
|
||||
@@ -5,19 +5,6 @@ import { generateRandomString } from "./index";
|
||||
// API基础URL
|
||||
const BASE_URL = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080";
|
||||
|
||||
// 测试数据
|
||||
let testData: {
|
||||
tenant: {
|
||||
id: number;
|
||||
name: string;
|
||||
api_key: string;
|
||||
};
|
||||
knowledge_bases: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
} | null = null;
|
||||
|
||||
// 创建Axios实例
|
||||
const instance = axios.create({
|
||||
@@ -29,15 +16,6 @@ const instance = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
// 设置测试数据
|
||||
export function setTestData(data: typeof testData) {
|
||||
testData = data;
|
||||
}
|
||||
|
||||
// 获取测试数据
|
||||
export function getTestData() {
|
||||
return testData;
|
||||
}
|
||||
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
|
||||
@@ -145,7 +145,6 @@ import { ref, reactive, computed, nextTick, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { login, register } from '@/api/auth'
|
||||
import { loadTestData, resetTestDataLoaded } from '@/api/test-data'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -272,11 +271,6 @@ const handleLogin = async () => {
|
||||
|
||||
MessagePlugin.success('登录成功!')
|
||||
|
||||
// 登录成功后先重置并加载一次测试数据,确保有KB可用
|
||||
try {
|
||||
resetTestDataLoaded()
|
||||
await loadTestData()
|
||||
} catch (_) {}
|
||||
|
||||
// 等待状态更新完成后再跳转
|
||||
await nextTick()
|
||||
|
||||
@@ -38,6 +38,7 @@ const { output, onChunk, isStreaming, isLoading, error, startStream, stopStream
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const session_id = ref(route.params.chatid);
|
||||
const knowledge_base_id = ref(route.params.kbId);
|
||||
const created_at = ref('');
|
||||
const limit = ref(20);
|
||||
const messagesList = reactive([]);
|
||||
@@ -57,6 +58,7 @@ watch([() => route.params], (newvalue) => {
|
||||
}
|
||||
messagesList.splice(0);
|
||||
session_id.value = newvalue[0].chatid;
|
||||
knowledge_base_id.value = newvalue[0].kbId;
|
||||
checkmenuTitle(session_id.value)
|
||||
let data = {
|
||||
session_id: session_id.value,
|
||||
@@ -154,7 +156,14 @@ const sendMsg = async (value) => {
|
||||
loading.value = true;
|
||||
messagesList.push({ content: value, role: 'user' });
|
||||
scrollToBottom();
|
||||
await startStream({ session_id: session_id.value, query: value, method: 'POST', url: '/api/v1/knowledge-chat' });
|
||||
|
||||
await startStream({
|
||||
session_id: session_id.value,
|
||||
knowledge_base_id: knowledge_base_id.value,
|
||||
query: value,
|
||||
method: 'POST',
|
||||
url: '/api/v1/knowledge-chat'
|
||||
});
|
||||
}
|
||||
|
||||
// 处理流式数据
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-show="cardList.length" class="dialogue-wrap">
|
||||
<div class="dialogue-wrap">
|
||||
<div class="dialogue-answers">
|
||||
<div class="dialogue-title">
|
||||
<span>基于知识库内容问答</span>
|
||||
@@ -7,7 +7,17 @@
|
||||
<InputField @send-msg="sendMsg"></InputField>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyKnowledge v-show="!cardList.length"></EmptyKnowledge>
|
||||
|
||||
|
||||
<t-dialog v-model:visible="selectVisible" header="选择知识库" :confirmBtn="{ content: '开始对话', theme: 'primary' }" :onConfirm="confirmSelect" :onCancel="() => selectVisible = false">
|
||||
<t-form :data="{ kb: selectedKbId }">
|
||||
<t-form-item label="知识库">
|
||||
<t-select v-model="selectedKbId" :loading="kbLoading" placeholder="请选择知识库">
|
||||
<t-option v-for="kb in kbList" :key="kb.id" :value="kb.id" :label="kb.name" />
|
||||
</t-select>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</t-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted, watch } from 'vue';
|
||||
@@ -17,29 +27,52 @@ import { getSessionsList, createSessions, generateSessionsTitle } from "@/api/ch
|
||||
import { useMenuStore } from '@/stores/menu';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
|
||||
import { getTestData } from '@/utils/request';
|
||||
import { listKnowledgeBases } from '@/api/knowledge-base';
|
||||
|
||||
let { cardList } = useKnowledgeBase()
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const usemenuStore = useMenuStore();
|
||||
const sendMsg = (value: string) => {
|
||||
createNewSession(value);
|
||||
}
|
||||
|
||||
async function createNewSession(value: string) {
|
||||
// 使用测试数据获取知识库ID
|
||||
const testData = getTestData();
|
||||
if (!testData || testData.knowledge_bases.length === 0) {
|
||||
console.error("测试数据未初始化或不包含知识库");
|
||||
return;
|
||||
const selectVisible = ref(false)
|
||||
const selectedKbId = ref<string>('')
|
||||
const kbList = ref<Array<{ id: string; name: string }>>([])
|
||||
const kbLoading = ref(false)
|
||||
|
||||
const ensureKbId = async (): Promise<string | null> => {
|
||||
// 1) 优先使用当前路由上下文(如果来自某个知识库详情页)
|
||||
const routeKb = (route.params as any)?.kbId as string
|
||||
if (routeKb) return routeKb
|
||||
|
||||
|
||||
// 3) 弹窗选择知识库(从接口拉取)
|
||||
kbLoading.value = true
|
||||
try {
|
||||
const res: any = await listKnowledgeBases()
|
||||
kbList.value = res?.data || []
|
||||
if (kbList.value.length === 0) return null
|
||||
selectedKbId.value = kbList.value[0].id
|
||||
selectVisible.value = true
|
||||
return null
|
||||
} finally {
|
||||
kbLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 使用第一个知识库ID
|
||||
const knowledgeBaseId = testData.knowledge_bases[0].id;
|
||||
async function createNewSession(value: string) {
|
||||
let knowledgeBaseId = await ensureKbId()
|
||||
if (!knowledgeBaseId) {
|
||||
// 等待用户在弹窗中选择
|
||||
pendingValue.value = value
|
||||
return
|
||||
}
|
||||
|
||||
createSessions({ knowledge_base_id: knowledgeBaseId }).then(res => {
|
||||
createSessions({ knowledge_base_id: knowledgeBaseId }).then(async res => {
|
||||
if (res.data && res.data.id) {
|
||||
getTitle(res.data.id, value)
|
||||
await getTitle(res.data.id, value)
|
||||
} else {
|
||||
// 错误处理
|
||||
console.error("创建会话失败");
|
||||
@@ -49,12 +82,33 @@ async function createNewSession(value: string) {
|
||||
})
|
||||
}
|
||||
|
||||
const getTitle = (session_id: string, value: string) => {
|
||||
let obj = { title: '新会话', path: `chat/${session_id}`, id: session_id, isMore: false, isNoTitle: true }
|
||||
const pendingValue = ref<string>('')
|
||||
const confirmSelect = async () => {
|
||||
if (!selectedKbId.value) return
|
||||
const value = pendingValue.value
|
||||
pendingValue.value = ''
|
||||
selectVisible.value = false
|
||||
createSessions({ knowledge_base_id: selectedKbId.value }).then(async res => {
|
||||
if (res.data && res.data.id) {
|
||||
await getTitle(res.data.id, value, selectedKbId.value)
|
||||
} else {
|
||||
console.error('创建会话失败')
|
||||
}
|
||||
}).catch((e:any) => console.error('创建会话出错:', e))
|
||||
}
|
||||
|
||||
const getTitle = async (session_id: string, value: string, kbId?: string) => {
|
||||
const finalKbId = kbId || await ensureKbId();
|
||||
if (!finalKbId) {
|
||||
console.error('无法获取知识库ID');
|
||||
return;
|
||||
}
|
||||
|
||||
let obj = { title: '新会话', path: `chat/${finalKbId}/${session_id}`, id: session_id, isMore: false, isNoTitle: true }
|
||||
usemenuStore.updataMenuChildren(obj);
|
||||
usemenuStore.changeIsFirstSession(true);
|
||||
usemenuStore.changeFirstQuery(value);
|
||||
router.push(`/platform/chat/${session_id}`);
|
||||
router.push(`/platform/chat/${finalKbId}/${session_id}`);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<div class="settings-page-container">
|
||||
<div class="initialization-content">
|
||||
<!-- 顶部Ollama服务状态 -->
|
||||
<div class="ollama-summary-card">
|
||||
@@ -30,6 +31,21 @@
|
||||
|
||||
<!-- 主配置表单 -->
|
||||
<t-form ref="form" :data="formData" :rules="rules" @submit.prevent layout="vertical">
|
||||
<!-- 知识库基本信息配置区域 (仅在知识库设置模式下显示) -->
|
||||
<div v-if="props.isKbSettings" class="config-section">
|
||||
<h3><t-icon name="folder" class="section-icon" />知识库基本信息</h3>
|
||||
<div class="form-row">
|
||||
<t-form-item label="知识库名称" name="kbName" :required="true">
|
||||
<t-input v-model="formData.kbName" placeholder="请输入知识库名称" maxlength="50" show-word-limit />
|
||||
</t-form-item>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<t-form-item label="知识库描述" name="kbDescription">
|
||||
<t-textarea v-model="formData.kbDescription" placeholder="请输入知识库描述" maxlength="200" show-word-limit :autosize="{ minRows: 3, maxRows: 6 }" />
|
||||
</t-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LLM 大语言模型配置区域 -->
|
||||
<div class="config-section">
|
||||
<h3><t-icon name="chat" class="section-icon" />LLM 大语言模型配置</h3>
|
||||
@@ -709,7 +725,7 @@
|
||||
<t-button theme="primary" type="button" size="large"
|
||||
:loading="submitting" :disabled="!canSubmit || isSubmitDebounced"
|
||||
@click="handleSubmit">
|
||||
{{ isUpdateMode ? '更新配置信息' : '完成配置' }}
|
||||
{{ props.isKbSettings ? '更新知识库设置' : (isUpdateMode ? '更新配置信息' : '完成配置') }}
|
||||
</t-button>
|
||||
|
||||
<!-- 提交状态提示 -->
|
||||
@@ -726,6 +742,7 @@
|
||||
</div>
|
||||
</t-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -733,15 +750,15 @@
|
||||
* 导入必要的 Vue 组合式 API 和外部依赖
|
||||
*/
|
||||
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import {
|
||||
initializeSystem,
|
||||
initializeSystemByKB,
|
||||
checkOllamaStatus,
|
||||
checkOllamaModels,
|
||||
downloadOllamaModel,
|
||||
getDownloadProgress,
|
||||
getCurrentConfig,
|
||||
getCurrentConfigByKB,
|
||||
checkRemoteModel,
|
||||
type DownloadTask,
|
||||
checkRerankModel,
|
||||
@@ -749,10 +766,22 @@ import {
|
||||
listOllamaModels,
|
||||
testEmbeddingModel
|
||||
} from '@/api/initialization';
|
||||
import { getKnowledgeBaseById } from '@/api/knowledge-base';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 接收props,判断是否为知识库设置模式
|
||||
const props = defineProps<{
|
||||
isKbSettings?: boolean;
|
||||
}>();
|
||||
|
||||
// 获取当前知识库ID(如果是知识库设置模式)
|
||||
const currentKbId = computed(() => {
|
||||
return props.isKbSettings ? (route.params.kbId as string) : null;
|
||||
});
|
||||
type TFormRef = {
|
||||
validate: (fields?: string[] | undefined) => Promise<true | any>;
|
||||
clearValidate?: (fields?: string | string[]) => void;
|
||||
@@ -824,6 +853,9 @@ const modelStatus = reactive({
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
// 知识库基本信息 (仅在知识库设置模式下使用)
|
||||
kbName: '',
|
||||
kbDescription: '',
|
||||
llm: {
|
||||
source: 'local',
|
||||
modelName: '',
|
||||
@@ -867,8 +899,8 @@ const formData = reactive({
|
||||
}
|
||||
},
|
||||
documentSplitting: {
|
||||
chunkSize: 1000,
|
||||
chunkOverlap: 200,
|
||||
chunkSize: 512,
|
||||
chunkOverlap: 100,
|
||||
separators: ['\n\n', '\n', '。', '!', '?', ';', ';']
|
||||
}
|
||||
});
|
||||
@@ -880,7 +912,7 @@ const inputDebounceTimers = reactive<Record<string, any>>({});
|
||||
const embeddingDimDetecting = ref(false);
|
||||
|
||||
// 预设配置选择
|
||||
const selectedPreset = ref('balanced');
|
||||
const selectedPreset = ref('precision');
|
||||
|
||||
// 分隔符选项
|
||||
const separatorOptions = [
|
||||
@@ -1011,6 +1043,14 @@ const validateEmbeddingDimension = (val: any) => {
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
// 知识库基本信息验证 (仅在知识库设置模式下使用)
|
||||
'kbName': [
|
||||
{ required: (t: any) => props.isKbSettings, message: '请输入知识库名称', type: 'error' },
|
||||
{ min: 1, max: 50, message: '知识库名称长度应在1-50个字符之间', type: 'error' }
|
||||
],
|
||||
'kbDescription': [
|
||||
{ max: 200, message: '知识库描述长度不能超过200个字符', type: 'error' }
|
||||
],
|
||||
'llm.modelName': [{ required: true, message: '请输入LLM模型名称', type: 'error' }],
|
||||
'llm.baseUrl': [
|
||||
{ required: (t: any) => formData.llm.source === 'remote', message: '请输入BaseURL', type: 'error' }
|
||||
@@ -1191,7 +1231,22 @@ const startProgressPolling = (type: 'llm' | 'embedding' | 'vlm', taskId: string,
|
||||
// 配置回填
|
||||
const loadCurrentConfig = async () => {
|
||||
try {
|
||||
const config = await getCurrentConfig();
|
||||
// 如果是知识库设置模式,先加载知识库基本信息
|
||||
if (props.isKbSettings && currentKbId.value) {
|
||||
try {
|
||||
const kbInfo = await getKnowledgeBaseById(currentKbId.value);
|
||||
if (kbInfo && kbInfo.data) {
|
||||
formData.kbName = kbInfo.data.name || '';
|
||||
formData.kbDescription = kbInfo.data.description || '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取知识库信息失败:', error);
|
||||
MessagePlugin.error('获取知识库信息失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 根据是否为知识库设置模式选择不同的API
|
||||
const config = await getCurrentConfigByKB(currentKbId.value);
|
||||
|
||||
// 设置hasFiles状态
|
||||
hasFiles.value = config.hasFiles || false;
|
||||
@@ -1234,6 +1289,9 @@ const loadCurrentConfig = async () => {
|
||||
} else {
|
||||
selectedPreset.value = 'custom';
|
||||
}
|
||||
} else {
|
||||
// 如果没有文档分割配置,确保使用默认的precision模式
|
||||
selectedPreset.value = 'precision';
|
||||
}
|
||||
|
||||
// 在配置加载完成后,检查模型状态
|
||||
@@ -1883,21 +1941,43 @@ const handleSubmit = async () => {
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
// 如果是知识库设置模式,先更新知识库基本信息
|
||||
if (props.isKbSettings && currentKbId.value) {
|
||||
try {
|
||||
const { updateKnowledgeBase } = await import('@/api/knowledge-base');
|
||||
await updateKnowledgeBase(currentKbId.value, {
|
||||
name: formData.kbName,
|
||||
description: formData.kbDescription,
|
||||
config: {} // 空的config对象,因为这里只更新基本信息
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新知识库基本信息失败:', error);
|
||||
MessagePlugin.error('更新知识库基本信息失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保embedding.dimension是数字类型
|
||||
if (formData.embedding.dimension) {
|
||||
formData.embedding.dimension = Number(formData.embedding.dimension);
|
||||
}
|
||||
|
||||
// 直接使用formData,API期望原始结构
|
||||
const result = await initializeSystem(formData);
|
||||
// 根据是否为知识库设置模式选择不同的API
|
||||
const result = await initializeSystemByKB(currentKbId.value, formData);
|
||||
|
||||
if (result.success) {
|
||||
MessagePlugin.success(isUpdateMode.value ? '配置更新成功' : '系统初始化完成');
|
||||
MessagePlugin.success(props.isKbSettings ? '知识库设置更新成功' : (isUpdateMode.value ? '配置更新成功' : '系统初始化完成'));
|
||||
|
||||
// 如果是初始化模式,跳转到主页面
|
||||
if (!isUpdateMode.value) {
|
||||
// 根据不同模式进行跳转
|
||||
if (props.isKbSettings && currentKbId.value) {
|
||||
// 知识库设置模式,跳转回知识库详情页面
|
||||
setTimeout(() => {
|
||||
router.push('/platform/knowledgeBase');
|
||||
router.push(`/platform/knowledge-bases/${currentKbId.value}`);
|
||||
}, 1500);
|
||||
} else if (!isUpdateMode.value) {
|
||||
// 初始化模式,跳转到知识库列表页面
|
||||
setTimeout(() => {
|
||||
router.push('/platform/knowledge-bases');
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
@@ -1940,7 +2020,18 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.settings-page-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f7fa;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.initialization-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
|
||||
.ollama-summary-card {
|
||||
@@ -2168,10 +2259,10 @@ onMounted(async () => {
|
||||
|
||||
.remote-config {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f9fcff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #edf2f7;
|
||||
// padding: 20px;
|
||||
// background: #f9fcff;
|
||||
// border-radius: 12px;
|
||||
// border: 1px solid #edf2f7;
|
||||
}
|
||||
|
||||
.url-input-with-check {
|
||||
@@ -2272,11 +2363,11 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.rerank-config, .multimodal-config {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f9fcff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #edf2f7;
|
||||
// margin-top: 20px;
|
||||
// padding: 20px;
|
||||
// background: #f9fcff;
|
||||
// border-radius: 12px;
|
||||
// border: 1px solid #edf2f7;
|
||||
|
||||
h4 {
|
||||
color: #333;
|
||||
@@ -2436,11 +2527,11 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.multimodal-test {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8fff9;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e8f5e8;
|
||||
// margin-top: 20px;
|
||||
// padding: 20px;
|
||||
// background: #f8fff9;
|
||||
// border-radius: 10px;
|
||||
// border: 1px solid #e8f5e8;
|
||||
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
@@ -2476,12 +2567,12 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e8f5e8;
|
||||
box-shadow: 0 2px 8px rgba(7, 192, 95, 0.08);
|
||||
// text-align: center;
|
||||
// padding: 20px;
|
||||
// background: white;
|
||||
// border-radius: 10px;
|
||||
// border: 1px solid #e8f5e8;
|
||||
// box-shadow: 0 2px 8px rgba(7, 192, 95, 0.08);
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
@@ -2521,11 +2612,11 @@ onMounted(async () => {
|
||||
|
||||
.test-button-wrapper {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
padding: 12px 20px;
|
||||
background: #f8fff9;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e8f5e8;
|
||||
// margin-top: 16px;
|
||||
// padding: 12px 20px;
|
||||
// background: #f8fff9;
|
||||
// border-radius: 8px;
|
||||
// border: 1px solid #e8f5e8;
|
||||
|
||||
.t-button {
|
||||
min-width: 100px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, reactive } from "vue";
|
||||
import { ref, onMounted, onUnmounted, watch, reactive, computed } from "vue";
|
||||
import DocContent from "@/components/doc-content.vue";
|
||||
import InputField from "@/components/Input-field.vue";
|
||||
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
|
||||
@@ -7,17 +7,21 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import EmptyKnowledge from '@/components/empty-knowledge.vue';
|
||||
import { getSessionsList, createSessions, generateSessionsTitle } from "@/api/chat/index";
|
||||
import { useMenuStore } from '@/stores/menu';
|
||||
import { getTestData } from '@/utils/request';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
const usemenuStore = useMenuStore();
|
||||
const router = useRouter();
|
||||
import {
|
||||
batchQueryKnowledge,
|
||||
listKnowledgeFiles,
|
||||
} from "@/api/knowledge-base/index";
|
||||
let { cardList, total, moreIndex, details, getKnowled, delKnowledge, openMore, onVisibleChange, getCardDetails, getfDetails } = useKnowledgeBase()
|
||||
import { formatStringDate } from "@/utils/index";
|
||||
const route = useRoute();
|
||||
const kbId = computed(() => (route.params as any).kbId as string || '');
|
||||
let { cardList, total, moreIndex, details, getKnowled, delKnowledge, openMore, onVisibleChange, getCardDetails, getfDetails } = useKnowledgeBase(kbId.value)
|
||||
let isCardDetails = ref(false);
|
||||
let timeout = null;
|
||||
let timeout: ReturnType<typeof setInterval> | null = null;
|
||||
let delDialog = ref(false)
|
||||
let knowledge = ref({})
|
||||
let knowledge = ref<KnowledgeCard>({ id: '', parse_status: '' })
|
||||
let knowledgeIndex = ref(-1)
|
||||
let knowledgeScroll = ref()
|
||||
let page = 1;
|
||||
@@ -29,29 +33,93 @@ const getPageSize = () => {
|
||||
pageSize = Math.max(35, itemsInView);
|
||||
}
|
||||
getPageSize()
|
||||
// 直接调用 API 获取知识库文件列表
|
||||
const loadKnowledgeFiles = async (kbIdValue: string) => {
|
||||
if (!kbIdValue) return;
|
||||
|
||||
try {
|
||||
const result = await listKnowledgeFiles(kbIdValue, { page: 1, page_size: pageSize });
|
||||
|
||||
// 由于响应拦截器已经返回了 data,所以 result 就是响应的 data 部分
|
||||
// 按照 useKnowledgeBase hook 中的方式处理
|
||||
const { data, total: totalResult } = result as any;
|
||||
|
||||
if (!data || !Array.isArray(data)) {
|
||||
console.error('Invalid data format. Expected array, got:', typeof data, data);
|
||||
return;
|
||||
}
|
||||
|
||||
const cardList_ = data.map((item: any) => {
|
||||
item["file_name"] = item.file_name.substring(
|
||||
0,
|
||||
item.file_name.lastIndexOf(".")
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
updated_at: formatStringDate(new Date(item.updated_at)),
|
||||
isMore: false,
|
||||
file_type: item.file_type.toLocaleUpperCase(),
|
||||
};
|
||||
});
|
||||
|
||||
cardList.value = cardList_ as any[];
|
||||
total.value = totalResult;
|
||||
} catch (err) {
|
||||
console.error('Failed to load knowledge files:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由参数变化,重新获取知识库内容
|
||||
watch(() => kbId.value, (newKbId, oldKbId) => {
|
||||
if (newKbId && newKbId !== oldKbId) {
|
||||
loadKnowledgeFiles(newKbId);
|
||||
}
|
||||
}, { immediate: false });
|
||||
|
||||
// 监听文件上传事件
|
||||
const handleFileUploaded = (event: CustomEvent) => {
|
||||
const uploadedKbId = event.detail.kbId;
|
||||
console.log('接收到文件上传事件,上传的知识库ID:', uploadedKbId, '当前知识库ID:', kbId.value);
|
||||
if (uploadedKbId && uploadedKbId === kbId.value) {
|
||||
console.log('匹配当前知识库,开始刷新文件列表');
|
||||
// 如果上传的文件属于当前知识库,使用 loadKnowledgeFiles 刷新文件列表
|
||||
loadKnowledgeFiles(uploadedKbId);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getKnowled({ page: 1, page_size: pageSize });
|
||||
|
||||
// 监听文件上传事件
|
||||
window.addEventListener('knowledgeFileUploaded', handleFileUploaded as EventListener);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('knowledgeFileUploaded', handleFileUploaded as EventListener);
|
||||
});
|
||||
watch(() => cardList.value, (newValue) => {
|
||||
let analyzeList = [];
|
||||
analyzeList = newValue.filter(item => {
|
||||
return item.parse_status == 'pending' || item.parse_status == 'processing';
|
||||
})
|
||||
if (timeout !== null) {
|
||||
clearInterval(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
if (analyzeList.length) {
|
||||
updateStatus(analyzeList)
|
||||
}
|
||||
}, { deep: true })
|
||||
const updateStatus = (analyzeList) => {
|
||||
type KnowledgeCard = { id: string; parse_status: string; description?: string; file_name?: string; updated_at?: string; file_type?: string; isMore?: boolean };
|
||||
const updateStatus = (analyzeList: KnowledgeCard[]) => {
|
||||
let query = ``;
|
||||
for (let i = 0; i < analyzeList.length; i++) {
|
||||
query += `ids=${analyzeList[i].id}&`;
|
||||
}
|
||||
timeout = setInterval(() => {
|
||||
batchQueryKnowledge(query).then((result) => {
|
||||
batchQueryKnowledge(query).then((result: any) => {
|
||||
if (result.success && result.data) {
|
||||
result.data.forEach(item => {
|
||||
(result.data as KnowledgeCard[]).forEach((item: KnowledgeCard) => {
|
||||
if (item.parse_status == 'failed' || item.parse_status == 'completed') {
|
||||
let index = cardList.value.findIndex(card => card.id == item.id);
|
||||
if (index != -1) {
|
||||
@@ -70,12 +138,12 @@ const updateStatus = (analyzeList) => {
|
||||
const closeDoc = () => {
|
||||
isCardDetails.value = false;
|
||||
};
|
||||
const openCardDetails = (item) => {
|
||||
const openCardDetails = (item: KnowledgeCard) => {
|
||||
isCardDetails.value = true;
|
||||
getCardDetails(item);
|
||||
};
|
||||
|
||||
const delCard = (index, item) => {
|
||||
const delCard = (index: number, item: KnowledgeCard) => {
|
||||
knowledgeIndex.value = index;
|
||||
knowledge.value = item;
|
||||
delDialog.value = true;
|
||||
@@ -94,7 +162,7 @@ const handleScroll = () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
const getDoc = (page) => {
|
||||
const getDoc = (page: number) => {
|
||||
getfDetails(details.id, page)
|
||||
};
|
||||
|
||||
@@ -108,51 +176,36 @@ const sendMsg = (value: string) => {
|
||||
};
|
||||
|
||||
const getTitle = (session_id: string, value: string) => {
|
||||
let obj = { title: '新会话', path: `chat/${session_id}`, id: session_id, isMore: false, isNoTitle: true };
|
||||
let obj = { title: '新会话', path: `chat/${kbId.value}/${session_id}`, id: session_id, isMore: false, isNoTitle: true };
|
||||
usemenuStore.updataMenuChildren(obj);
|
||||
usemenuStore.changeIsFirstSession(true);
|
||||
usemenuStore.changeFirstQuery(value);
|
||||
router.push(`/platform/chat/${session_id}`);
|
||||
router.push(`/platform/chat/${kbId.value}/${session_id}`);
|
||||
};
|
||||
|
||||
async function createNewSession(value: string): Promise<void> {
|
||||
// 从localStorage获取设置中的知识库ID
|
||||
const settingsStr = localStorage.getItem("WeKnora_settings");
|
||||
let knowledgeBaseId = "";
|
||||
// 优先使用当前页面的知识库ID
|
||||
let sessionKbId = kbId.value;
|
||||
|
||||
// 如果当前页面没有知识库ID,尝试从localStorage获取设置中的知识库ID
|
||||
if (!sessionKbId) {
|
||||
const settingsStr = localStorage.getItem("WeKnora_settings");
|
||||
if (settingsStr) {
|
||||
try {
|
||||
const settings = JSON.parse(settingsStr);
|
||||
if (settings.knowledgeBaseId) {
|
||||
knowledgeBaseId = settings.knowledgeBaseId;
|
||||
createSessions({ knowledge_base_id: knowledgeBaseId }).then(res => {
|
||||
if (res.data && res.data.id) {
|
||||
getTitle(res.data.id, value);
|
||||
} else {
|
||||
// 错误处理
|
||||
console.error("创建会话失败");
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("创建会话出错:", error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
sessionKbId = settings.knowledgeBaseId;
|
||||
} catch (e) {
|
||||
console.error("解析设置失败:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果设置中没有知识库ID,则使用测试数据
|
||||
const testData = getTestData();
|
||||
if (!testData || !testData.knowledge_bases || testData.knowledge_bases.length === 0) {
|
||||
console.error("测试数据未初始化或不包含知识库");
|
||||
if (!sessionKbId) {
|
||||
MessagePlugin.warning("请先选择一个知识库");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用第一个知识库ID
|
||||
knowledgeBaseId = testData.knowledge_bases[0].id;
|
||||
|
||||
createSessions({ knowledge_base_id: knowledgeBaseId }).then(res => {
|
||||
createSessions({ knowledge_base_id: sessionKbId }).then(res => {
|
||||
if (res.data && res.data.id) {
|
||||
getTitle(res.data.id, value);
|
||||
} else {
|
||||
|
||||
234
frontend/src/views/knowledge/KnowledgeBaseList.vue
Normal file
234
frontend/src/views/knowledge/KnowledgeBaseList.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div class="kb-list-container">
|
||||
<div class="header">
|
||||
<h2>知识库</h2>
|
||||
<t-button theme="primary" @click="openCreate">新建知识库</t-button>
|
||||
</div>
|
||||
|
||||
<!-- 未初始化知识库提示 -->
|
||||
<div v-if="hasUninitializedKbs" class="warning-banner">
|
||||
<t-icon name="info-circle" size="16px" />
|
||||
<span>部分知识库尚未初始化,需要先在设置中配置模型信息才能添加知识文档</span>
|
||||
</div>
|
||||
<t-table :data="kbs" :columns="columns" row-key="id" size="medium" hover>
|
||||
<template #status="{ row }">
|
||||
<div class="status-cell">
|
||||
<t-tag
|
||||
:theme="isInitialized(row) ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ isInitialized(row) ? '已初始化' : '未初始化' }}
|
||||
</t-tag>
|
||||
<t-tooltip
|
||||
v-if="!isInitialized(row)"
|
||||
content="需要先在设置中配置模型信息才能添加知识"
|
||||
placement="top"
|
||||
>
|
||||
<span class="warning-icon">⚠</span>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template #description="{ row }">
|
||||
<div class="description-text">{{ row.description || '暂无描述' }}</div>
|
||||
</template>
|
||||
<template #op="{ row }">
|
||||
<t-space size="small">
|
||||
<t-button
|
||||
size="small"
|
||||
@click="goDetail(row.id)"
|
||||
:disabled="!isInitialized(row)"
|
||||
:theme="isInitialized(row) ? 'primary' : 'default'"
|
||||
:variant="isInitialized(row) ? 'base' : 'outline'"
|
||||
:title="!isInitialized(row) ? '请先在设置中配置模型信息' : ''"
|
||||
>
|
||||
文档
|
||||
</t-button>
|
||||
<t-button size="small" variant="outline" @click="goSettings(row.id)">设置</t-button>
|
||||
<t-popconfirm content="确认删除该知识库?" @confirm="remove(row.id)">
|
||||
<t-button size="small" theme="danger" variant="text">删除</t-button>
|
||||
</t-popconfirm>
|
||||
</t-space>
|
||||
</template>
|
||||
</t-table>
|
||||
|
||||
<t-dialog v-model:visible="createVisible" header="新建知识库" :footer="false">
|
||||
<t-form :data="createForm" @submit="create">
|
||||
<t-form-item label="名称" name="name" :rules="[{ required: true, message: '请输入名称' }]">
|
||||
<t-input v-model="createForm.name" />
|
||||
</t-form-item>
|
||||
<t-form-item label="描述" name="description">
|
||||
<t-textarea v-model="createForm.description" />
|
||||
</t-form-item>
|
||||
<t-form-item>
|
||||
<t-space>
|
||||
<t-button theme="primary" type="submit" :loading="creating">创建</t-button>
|
||||
<t-button variant="outline" @click="createVisible = false">取消</t-button>
|
||||
</t-space>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</t-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { listKnowledgeBases, createKnowledgeBase, deleteKnowledgeBase } from '@/api/knowledge-base'
|
||||
import { formatStringDate } from '@/utils/index'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface KB {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
updated_at?: string;
|
||||
embedding_model_id?: string;
|
||||
summary_model_id?: string;
|
||||
}
|
||||
const kbs = ref<KB[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'name', title: '名称' },
|
||||
{ colKey: 'description', title: '描述', cell: 'description', width: 300 },
|
||||
{ colKey: 'status', title: '状态', cell: 'status', width: 100 },
|
||||
{ colKey: 'updated_at', title: '更新时间' },
|
||||
{ colKey: 'op', title: '操作', cell: 'op', width: 220 },
|
||||
]
|
||||
|
||||
const fetchList = () => {
|
||||
loading.value = true
|
||||
listKnowledgeBases().then((res: any) => {
|
||||
const data = res.data || []
|
||||
// 格式化时间
|
||||
kbs.value = data.map((kb: KB) => ({
|
||||
...kb,
|
||||
updated_at: kb.updated_at ? formatStringDate(new Date(kb.updated_at)) : ''
|
||||
}))
|
||||
}).finally(() => loading.value = false)
|
||||
}
|
||||
|
||||
onMounted(fetchList)
|
||||
|
||||
const createVisible = ref(false)
|
||||
const creating = ref(false)
|
||||
const createForm = reactive({ name: '', description: '' })
|
||||
const openCreate = () => {
|
||||
createForm.name = ''
|
||||
createForm.description = ''
|
||||
createVisible.value = true
|
||||
}
|
||||
const create = () => {
|
||||
if (!createForm.name) return
|
||||
creating.value = true
|
||||
const chunking_config = {
|
||||
chunk_size: 512,
|
||||
chunk_overlap: 100,
|
||||
separators: ['.', '?', '!', '。', '?', '!'],
|
||||
enable_multimodal: false
|
||||
}
|
||||
createKnowledgeBase({ name: createForm.name, description: createForm.description, chunking_config }).then((res: any) => {
|
||||
if (res.success) {
|
||||
MessagePlugin.success('创建成功')
|
||||
createVisible.value = false
|
||||
fetchList()
|
||||
} else {
|
||||
MessagePlugin.error(res.message || '创建失败')
|
||||
}
|
||||
}).catch((e: any) => {
|
||||
MessagePlugin.error(e?.message || '创建失败')
|
||||
}).finally(() => creating.value = false)
|
||||
}
|
||||
|
||||
const remove = (id: string) => {
|
||||
deleteKnowledgeBase(id).then((res: any) => {
|
||||
if (res.success) {
|
||||
MessagePlugin.success('已删除')
|
||||
fetchList()
|
||||
} else {
|
||||
MessagePlugin.error(res.message || '删除失败')
|
||||
}
|
||||
}).catch((e: any) => MessagePlugin.error(e?.message || '删除失败'))
|
||||
}
|
||||
|
||||
const isInitialized = (kb: KB) => {
|
||||
return !!(kb.embedding_model_id && kb.embedding_model_id !== '' &&
|
||||
kb.summary_model_id && kb.summary_model_id !== '')
|
||||
}
|
||||
|
||||
// 计算是否有未初始化的知识库
|
||||
const hasUninitializedKbs = computed(() => {
|
||||
return kbs.value.some(kb => !isInitialized(kb))
|
||||
})
|
||||
|
||||
const goDetail = (id: string) => {
|
||||
router.push(`/platform/knowledge-bases/${id}`)
|
||||
}
|
||||
const goSettings = (id: string) => {
|
||||
router.push(`/platform/knowledge-bases/${id}/settings`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.kb-list-container {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
margin: 0 20px 0 20px;
|
||||
height: calc(100vh);
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
h2 { margin: 0; font-size: 20px; font-weight: 600; }
|
||||
}
|
||||
|
||||
.warning-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
border-radius: 6px;
|
||||
color: #d46b08;
|
||||
font-size: 14px;
|
||||
|
||||
.t-icon {
|
||||
color: #d46b08;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.warning-icon {
|
||||
color: #ff8800;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #d46b08;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description-cell {
|
||||
.description-text {
|
||||
color: #000000e6;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,23 +2,109 @@
|
||||
<div class="system-settings-container">
|
||||
<!-- 页面标题区域 -->
|
||||
<div class="settings-header">
|
||||
<h2>系统设置</h2>
|
||||
<p class="settings-subtitle">管理和更新系统模型与服务配置</p>
|
||||
<h2>{{ isKbSettings ? '知识库设置' : '系统设置' }}</h2>
|
||||
<p class="settings-subtitle">{{ isKbSettings ? '配置该知识库的模型与文档切分参数' : '管理和更新系统模型与服务配置' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 配置内容 -->
|
||||
<div class="settings-content">
|
||||
<!-- 直接引用初始化配置组件的内容,不使用外层卡片包装 -->
|
||||
<InitializationContent />
|
||||
<!-- 系统设置:使用初始化配置 -->
|
||||
<InitializationContent v-if="!isKbSettings" />
|
||||
<!-- 知识库设置:基础信息与文档切分配置 -->
|
||||
<div v-else>
|
||||
<t-form :data="kbForm" @submit="saveKb">
|
||||
<div class="config-section">
|
||||
<h3><span class="section-icon">⚙️</span>基础信息</h3>
|
||||
<t-form-item label="名称" name="name" :rules="[{ required: true, message: '请输入名称' }]">
|
||||
<t-input v-model="kbForm.name" />
|
||||
</t-form-item>
|
||||
<t-form-item label="描述" name="description">
|
||||
<t-textarea v-model="kbForm.description" />
|
||||
</t-form-item>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<h3><span class="section-icon">📄</span>文档切分</h3>
|
||||
<t-row :gutter="16">
|
||||
<t-col :span="6">
|
||||
<t-form-item label="Chunk Size" name="chunkSize">
|
||||
<t-input-number v-model="kbForm.config.chunking_config.chunk_size" :min="1" />
|
||||
</t-form-item>
|
||||
</t-col>
|
||||
<t-col :span="6">
|
||||
<t-form-item label="Chunk Overlap" name="chunkOverlap">
|
||||
<t-input-number v-model="kbForm.config.chunking_config.chunk_overlap" :min="0" />
|
||||
</t-form-item>
|
||||
</t-col>
|
||||
</t-row>
|
||||
</div>
|
||||
<div class="submit-section">
|
||||
<t-button theme="primary" type="submit" :loading="saving">保存</t-button>
|
||||
</div>
|
||||
</t-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { defineAsyncComponent, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { getKnowledgeBaseById, updateKnowledgeBase } from '@/api/knowledge-base'
|
||||
|
||||
// 异步加载初始化配置组件
|
||||
const InitializationContent = defineAsyncComponent(() => import('../initialization/InitializationContent.vue'))
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const isKbSettings = ref<boolean>(false)
|
||||
|
||||
interface KbForm {
|
||||
name: string
|
||||
description?: string
|
||||
config: { chunking_config: { chunk_size: number; chunk_overlap: number } }
|
||||
}
|
||||
const kbForm = reactive<KbForm>({
|
||||
name: '',
|
||||
description: '',
|
||||
config: { chunking_config: { chunk_size: 512, chunk_overlap: 64 } }
|
||||
})
|
||||
const saving = ref(false)
|
||||
|
||||
const loadKb = () => {
|
||||
const kbId = (route.params as any).kbId as string
|
||||
if (!kbId) return
|
||||
getKnowledgeBaseById(kbId).then((res: any) => {
|
||||
if (res?.data) {
|
||||
kbForm.name = res.data.name
|
||||
kbForm.description = res.data.description
|
||||
const cc = res.data.chunking_config || {}
|
||||
kbForm.config.chunking_config.chunk_size = cc.chunk_size ?? 512
|
||||
kbForm.config.chunking_config.chunk_overlap = cc.chunk_overlap ?? 64
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
isKbSettings.value = route.name === 'knowledgeBaseSettings'
|
||||
if (isKbSettings.value) loadKb()
|
||||
})
|
||||
|
||||
const saveKb = () => {
|
||||
const kbId = (route.params as any).kbId as string
|
||||
if (!kbId) return
|
||||
saving.value = true
|
||||
updateKnowledgeBase(kbId, { name: kbForm.name, description: kbForm.description, config: { chunking_config: { chunk_size: kbForm.config.chunking_config.chunk_size, chunk_overlap: kbForm.config.chunking_config.chunk_overlap, separators: [], enable_multimodal: false }, image_processing_config: { model_id: '' } } })
|
||||
.then((res: any) => {
|
||||
if (res.success) {
|
||||
MessagePlugin.success('保存成功')
|
||||
} else {
|
||||
MessagePlugin.error(res.message || '保存失败')
|
||||
}
|
||||
})
|
||||
.catch((e: any) => MessagePlugin.error(e?.message || '保存失败'))
|
||||
.finally(() => saving.value = false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
<template>
|
||||
<div class="tenant-info-container">
|
||||
<div class="tenant-header">
|
||||
<h2>账户信息</h2>
|
||||
<p class="tenant-subtitle">查看和管理您的用户账户和租户配置信息</p>
|
||||
<h2>系统信息</h2>
|
||||
<p class="tenant-subtitle">查看系统版本信息和用户账户配置</p>
|
||||
</div>
|
||||
|
||||
<div class="tenant-content" v-if="!loading && !error">
|
||||
<!-- 系统信息卡片 -->
|
||||
<t-card class="info-card" :bordered="false">
|
||||
<template #header>
|
||||
<div class="card-title">系统信息</div>
|
||||
</template>
|
||||
<div class="info-content">
|
||||
<t-descriptions :column="1" layout="vertical">
|
||||
<t-descriptions-item label="版本号">
|
||||
{{ systemInfo?.version || '未知' }}
|
||||
<span v-if="systemInfo?.commit_id" class="commit-info">
|
||||
({{ systemInfo.commit_id }})
|
||||
</span>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="构建时间" v-if="systemInfo?.build_time">
|
||||
{{ systemInfo.build_time }}
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="Go版本" v-if="systemInfo?.go_version">
|
||||
{{ systemInfo.go_version }}
|
||||
</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<!-- 用户信息卡片 -->
|
||||
<t-card class="info-card" :bordered="false">
|
||||
<template #header>
|
||||
@@ -161,10 +184,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { getCurrentUser, type TenantInfo, type UserInfo } from '@/api/auth'
|
||||
import { getSystemInfo, type SystemInfo } from '@/api/system'
|
||||
|
||||
// 响应式数据
|
||||
const tenantInfo = ref<TenantInfo | null>(null)
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
const systemInfo = ref<SystemInfo | null>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const showApiKey = ref(false)
|
||||
@@ -192,12 +217,21 @@ const loadTenantInfo = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
const response = await getCurrentUser()
|
||||
if (response.success && response.data) {
|
||||
userInfo.value = response.data.user
|
||||
tenantInfo.value = response.data.tenant
|
||||
// 并行获取用户信息和系统信息
|
||||
const [userResponse, systemResponse] = await Promise.all([
|
||||
getCurrentUser(),
|
||||
getSystemInfo().catch(() => ({ data: null })) // 系统信息获取失败不影响页面显示
|
||||
])
|
||||
|
||||
if (userResponse.success && userResponse.data) {
|
||||
userInfo.value = userResponse.data.user
|
||||
tenantInfo.value = userResponse.data.tenant
|
||||
} else {
|
||||
error.value = response.message || '获取用户信息失败'
|
||||
error.value = userResponse.message || '获取用户信息失败'
|
||||
}
|
||||
|
||||
if (systemResponse.data) {
|
||||
systemInfo.value = systemResponse.data
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '网络错误,请稍后重试'
|
||||
@@ -349,7 +383,7 @@ onMounted(() => {
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
color: #07C05F;
|
||||
}
|
||||
|
||||
.card-header-with-actions {
|
||||
@@ -457,6 +491,12 @@ onMounted(() => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.commit-info {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.doc-actions {
|
||||
:deep(.t-space) {
|
||||
flex-direction: column;
|
||||
@@ -471,7 +511,6 @@ onMounted(() => {
|
||||
|
||||
/* 覆盖TDesign组件样式 */
|
||||
:deep(.t-card) {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -30,7 +31,7 @@ type EvaluationService struct {
|
||||
knowledgeBaseService interfaces.KnowledgeBaseService // Service for knowledge base operations
|
||||
knowledgeService interfaces.KnowledgeService // Service for knowledge operations
|
||||
sessionService interfaces.SessionService // Service for chat sessions
|
||||
testData *TestDataService // Service for test data
|
||||
modelService interfaces.ModelService // Service for model operations
|
||||
|
||||
evaluationMemoryStorage *evaluationMemoryStorage // In-memory storage for evaluation tasks
|
||||
}
|
||||
@@ -41,7 +42,7 @@ func NewEvaluationService(
|
||||
knowledgeBaseService interfaces.KnowledgeBaseService,
|
||||
knowledgeService interfaces.KnowledgeService,
|
||||
sessionService interfaces.SessionService,
|
||||
testData *TestDataService,
|
||||
modelService interfaces.ModelService,
|
||||
) interfaces.EvaluationService {
|
||||
evaluationMemoryStorage := newEvaluationMemoryStorage()
|
||||
return &EvaluationService{
|
||||
@@ -50,7 +51,7 @@ func NewEvaluationService(
|
||||
knowledgeBaseService: knowledgeBaseService,
|
||||
knowledgeService: knowledgeService,
|
||||
sessionService: sessionService,
|
||||
testData: testData,
|
||||
modelService: modelService,
|
||||
evaluationMemoryStorage: evaluationMemoryStorage,
|
||||
}
|
||||
}
|
||||
@@ -144,11 +145,32 @@ func (e *EvaluationService) Evaluation(ctx context.Context,
|
||||
if knowledgeBaseID == "" {
|
||||
logger.Info(ctx, "No knowledge base ID provided, creating new knowledge base")
|
||||
// Create new knowledge base with default evaluation settings
|
||||
// 获取默认的嵌入模型和LLM模型
|
||||
models, err := e.modelService.ListModels(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to list models: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var embeddingModelID, llmModelID string
|
||||
for _, model := range models {
|
||||
if model.Type == types.ModelTypeEmbedding {
|
||||
embeddingModelID = model.ID
|
||||
}
|
||||
if model.Type == types.ModelTypeKnowledgeQA {
|
||||
llmModelID = model.ID
|
||||
}
|
||||
}
|
||||
|
||||
if embeddingModelID == "" || llmModelID == "" {
|
||||
return nil, fmt.Errorf("no default models found for evaluation")
|
||||
}
|
||||
|
||||
kb, err := e.knowledgeBaseService.CreateKnowledgeBase(ctx, &types.KnowledgeBase{
|
||||
Name: "evaluation",
|
||||
Description: "evaluation",
|
||||
EmbeddingModelID: e.testData.EmbedModel.GetModelID(),
|
||||
SummaryModelID: e.testData.LLMModel.GetModelID(),
|
||||
EmbeddingModelID: embeddingModelID,
|
||||
SummaryModelID: llmModelID,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to create knowledge base: %v", err)
|
||||
@@ -186,12 +208,37 @@ func (e *EvaluationService) Evaluation(ctx context.Context,
|
||||
}
|
||||
|
||||
if rerankModelID == "" {
|
||||
rerankModelID = e.testData.RerankModel.GetModelID()
|
||||
// 获取默认的重排模型
|
||||
models, err := e.modelService.ListModels(ctx)
|
||||
if err == nil {
|
||||
for _, model := range models {
|
||||
if model.Type == types.ModelTypeRerank {
|
||||
rerankModelID = model.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if rerankModelID == "" {
|
||||
logger.Warnf(ctx, "No rerank model found, skipping rerank")
|
||||
} else {
|
||||
logger.Infof(ctx, "Using default rerank model: %s", rerankModelID)
|
||||
}
|
||||
}
|
||||
|
||||
if chatModelID == "" {
|
||||
chatModelID = e.testData.LLMModel.GetModelID()
|
||||
// 获取默认的LLM模型
|
||||
models, err := e.modelService.ListModels(ctx)
|
||||
if err == nil {
|
||||
for _, model := range models {
|
||||
if model.Type == types.ModelTypeKnowledgeQA {
|
||||
chatModelID = model.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if chatModelID == "" {
|
||||
return nil, fmt.Errorf("no default chat model found")
|
||||
}
|
||||
logger.Infof(ctx, "Using default chat model: %s", chatModelID)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
// Package service 提供应用程序的核心业务逻辑服务层
|
||||
// 此包包含了知识库管理、用户租户管理、模型服务等核心功能实现
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/models/chat"
|
||||
"github.com/Tencent/WeKnora/internal/models/embedding"
|
||||
"github.com/Tencent/WeKnora/internal/models/rerank"
|
||||
"github.com/Tencent/WeKnora/internal/models/utils/ollama"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
|
||||
// TestDataService 测试数据服务
|
||||
// 负责初始化测试环境所需的数据,包括创建测试租户、测试知识库
|
||||
// 以及配置必要的模型服务实例
|
||||
type TestDataService struct {
|
||||
config *config.Config // 应用程序配置
|
||||
kbRepo interfaces.KnowledgeBaseRepository // 知识库存储库接口
|
||||
tenantService interfaces.TenantService // 租户服务接口
|
||||
ollamaService *ollama.OllamaService // Ollama模型服务
|
||||
modelService interfaces.ModelService // 模型服务接口
|
||||
EmbedModel embedding.Embedder // 嵌入模型实例
|
||||
RerankModel rerank.Reranker // 重排模型实例
|
||||
LLMModel chat.Chat // 大语言模型实例
|
||||
}
|
||||
|
||||
// NewTestDataService 创建测试数据服务
|
||||
// 注入所需的依赖服务和组件
|
||||
func NewTestDataService(
|
||||
config *config.Config,
|
||||
kbRepo interfaces.KnowledgeBaseRepository,
|
||||
tenantService interfaces.TenantService,
|
||||
ollamaService *ollama.OllamaService,
|
||||
modelService interfaces.ModelService,
|
||||
) *TestDataService {
|
||||
return &TestDataService{
|
||||
config: config,
|
||||
kbRepo: kbRepo,
|
||||
tenantService: tenantService,
|
||||
ollamaService: ollamaService,
|
||||
modelService: modelService,
|
||||
}
|
||||
}
|
||||
|
||||
// initTenant 初始化测试租户
|
||||
// 通过环境变量获取租户ID,如果租户不存在则创建新租户,否则更新现有租户
|
||||
// 同时配置租户的检索引擎参数
|
||||
func (s *TestDataService) initTenant(ctx context.Context) error {
|
||||
logger.Info(ctx, "Start initializing test tenant")
|
||||
|
||||
// 从环境变量获取租户ID
|
||||
tenantID := os.Getenv("INIT_TEST_TENANT_ID")
|
||||
logger.Infof(ctx, "Test tenant ID from environment: %s", tenantID)
|
||||
|
||||
// 将字符串ID转换为uint64
|
||||
tenantIDUint, err := strconv.ParseUint(tenantID, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to parse tenant ID: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建租户配置
|
||||
tenantConfig := &types.Tenant{
|
||||
Name: "Test Tenant",
|
||||
Description: "Test Tenant for Testing",
|
||||
RetrieverEngines: types.RetrieverEngines{
|
||||
Engines: []types.RetrieverEngineParams{
|
||||
{
|
||||
RetrieverType: types.KeywordsRetrieverType,
|
||||
RetrieverEngineType: types.PostgresRetrieverEngineType,
|
||||
},
|
||||
{
|
||||
RetrieverType: types.VectorRetrieverType,
|
||||
RetrieverEngineType: types.PostgresRetrieverEngineType,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 获取或创建测试租户
|
||||
logger.Infof(ctx, "Attempting to get tenant with ID: %d", tenantIDUint)
|
||||
tenant, err := s.tenantService.GetTenantByID(ctx, uint(tenantIDUint))
|
||||
if err != nil {
|
||||
// 租户不存在,创建新租户
|
||||
logger.Info(ctx, "Tenant not found, creating a new test tenant")
|
||||
tenant, err = s.tenantService.CreateTenant(ctx, tenantConfig)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to create tenant: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Infof(ctx, "Created new test tenant with ID: %d", tenant.ID)
|
||||
} else {
|
||||
// 租户存在,更新检索引擎配置
|
||||
logger.Info(ctx, "Test tenant found, updating retriever engines")
|
||||
tenant.RetrieverEngines = tenantConfig.RetrieverEngines
|
||||
tenant, err = s.tenantService.UpdateTenant(ctx, tenant)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to update tenant: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Info(ctx, "Test tenant updated successfully")
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Test tenant configured - ID: %d, Name: %s, API Key: %s",
|
||||
tenant.ID, tenant.Name, tenant.APIKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// initKnowledgeBase 初始化测试知识库
|
||||
// 从环境变量获取知识库ID,创建或更新知识库
|
||||
// 配置知识库的分块策略、嵌入模型和摘要模型
|
||||
func (s *TestDataService) initKnowledgeBase(ctx context.Context) error {
|
||||
logger.Info(ctx, "Start initializing test knowledge base")
|
||||
|
||||
// 检查上下文中的租户ID
|
||||
if ctx.Value(types.TenantIDContextKey).(uint) == 0 {
|
||||
logger.Warn(ctx, "Tenant ID is 0, skipping knowledge base initialization")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 从环境变量获取知识库ID
|
||||
knowledgeBaseID := os.Getenv("INIT_TEST_KNOWLEDGE_BASE_ID")
|
||||
logger.Infof(ctx, "Test knowledge base ID from environment: %s", knowledgeBaseID)
|
||||
|
||||
// 创建知识库配置
|
||||
kbConfig := &types.KnowledgeBase{
|
||||
ID: knowledgeBaseID,
|
||||
Name: "Test Knowledge Base",
|
||||
Description: "Knowledge Base for Testing",
|
||||
TenantID: ctx.Value(types.TenantIDContextKey).(uint),
|
||||
ChunkingConfig: types.ChunkingConfig{
|
||||
ChunkSize: s.config.KnowledgeBase.ChunkSize,
|
||||
ChunkOverlap: s.config.KnowledgeBase.ChunkOverlap,
|
||||
Separators: s.config.KnowledgeBase.SplitMarkers,
|
||||
EnableMultimodal: s.config.KnowledgeBase.ImageProcessing.EnableMultimodal,
|
||||
},
|
||||
EmbeddingModelID: s.EmbedModel.GetModelID(),
|
||||
SummaryModelID: s.LLMModel.GetModelID(),
|
||||
RerankModelID: s.RerankModel.GetModelID(),
|
||||
}
|
||||
|
||||
// 初始化测试知识库
|
||||
logger.Info(ctx, "Attempting to get existing knowledge base")
|
||||
_, err := s.kbRepo.GetKnowledgeBaseByID(ctx, knowledgeBaseID)
|
||||
if err != nil {
|
||||
// 知识库不存在,创建新知识库
|
||||
logger.Info(ctx, "Knowledge base not found, creating a new one")
|
||||
logger.Infof(ctx, "Creating knowledge base with ID: %s, tenant ID: %d",
|
||||
kbConfig.ID, kbConfig.TenantID)
|
||||
|
||||
if err := s.kbRepo.CreateKnowledgeBase(ctx, kbConfig); err != nil {
|
||||
logger.Errorf(ctx, "Failed to create knowledge base: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Info(ctx, "Knowledge base created successfully")
|
||||
} else {
|
||||
// 知识库存在,更新配置
|
||||
logger.Info(ctx, "Knowledge base found, updating configuration")
|
||||
logger.Infof(ctx, "Updating knowledge base with ID: %s", kbConfig.ID)
|
||||
|
||||
err = s.kbRepo.UpdateKnowledgeBase(ctx, kbConfig)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to update knowledge base: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Info(ctx, "Knowledge base updated successfully")
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Test knowledge base configured - ID: %s, Name: %s", kbConfig.ID, kbConfig.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitializeTestData 初始化测试数据
|
||||
// 这是对外暴露的主要方法,负责协调所有测试数据的初始化过程
|
||||
// 包括初始化租户、嵌入模型、重排模型、LLM模型和知识库
|
||||
func (s *TestDataService) InitializeTestData(ctx context.Context) error {
|
||||
logger.Info(ctx, "Start initializing test data")
|
||||
|
||||
// 从环境变量获取租户ID
|
||||
tenantID := os.Getenv("INIT_TEST_TENANT_ID")
|
||||
logger.Infof(ctx, "Test tenant ID from environment: %s", tenantID)
|
||||
|
||||
// 解析租户ID
|
||||
tenantIDUint, err := strconv.ParseUint(tenantID, 10, 64)
|
||||
if err != nil {
|
||||
// 解析失败时使用默认值0
|
||||
logger.Warn(ctx, "Failed to parse tenant ID, using default value 0")
|
||||
tenantIDUint = 0
|
||||
} else {
|
||||
// 初始化租户
|
||||
logger.Info(ctx, "Initializing tenant")
|
||||
err = s.initTenant(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to initialize tenant: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Info(ctx, "Tenant initialized successfully")
|
||||
}
|
||||
|
||||
// 创建带有租户ID的新上下文
|
||||
newCtx := context.Background()
|
||||
newCtx = context.WithValue(newCtx, types.TenantIDContextKey, uint(tenantIDUint))
|
||||
logger.Infof(ctx, "Created new context with tenant ID: %d", tenantIDUint)
|
||||
|
||||
// 初始化模型
|
||||
modelInitFuncs := []struct {
|
||||
name string
|
||||
fn func(context.Context) error
|
||||
}{
|
||||
{"embedding model", s.initEmbeddingModel},
|
||||
{"rerank model", s.initRerankModel},
|
||||
{"LLM model", s.initLLMModel},
|
||||
}
|
||||
|
||||
for _, initFunc := range modelInitFuncs {
|
||||
logger.Infof(ctx, "Initializing %s", initFunc.name)
|
||||
if err := initFunc.fn(newCtx); err != nil {
|
||||
logger.Errorf(ctx, "Failed to initialize %s: %v", initFunc.name, err)
|
||||
return err
|
||||
}
|
||||
logger.Infof(ctx, "%s initialized successfully", initFunc.name)
|
||||
}
|
||||
|
||||
// 初始化知识库
|
||||
logger.Info(ctx, "Initializing knowledge base")
|
||||
if err := s.initKnowledgeBase(newCtx); err != nil {
|
||||
logger.Errorf(ctx, "Failed to initialize knowledge base: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Info(ctx, "Knowledge base initialized successfully")
|
||||
|
||||
logger.Info(ctx, "Test data initialization completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEnvOrError 获取环境变量值,如果不存在则返回错误
|
||||
func (s *TestDataService) getEnvOrError(name string) (string, error) {
|
||||
value := os.Getenv(name)
|
||||
if value == "" {
|
||||
return "", fmt.Errorf("%s environment variable is not set", name)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// updateOrCreateModel 更新或创建模型
|
||||
func (s *TestDataService) updateOrCreateModel(ctx context.Context, modelConfig *types.Model) error {
|
||||
model, err := s.modelService.GetModelByID(ctx, modelConfig.ID)
|
||||
if err != nil {
|
||||
// 模型不存在,创建新模型
|
||||
return s.modelService.CreateModel(ctx, modelConfig)
|
||||
}
|
||||
|
||||
// 模型存在,更新属性
|
||||
model.TenantID = modelConfig.TenantID
|
||||
model.Name = modelConfig.Name
|
||||
model.Source = modelConfig.Source
|
||||
model.Type = modelConfig.Type
|
||||
model.Parameters = modelConfig.Parameters
|
||||
model.Status = modelConfig.Status
|
||||
|
||||
return s.modelService.UpdateModel(ctx, model)
|
||||
}
|
||||
|
||||
// initEmbeddingModel 初始化嵌入模型
|
||||
func (s *TestDataService) initEmbeddingModel(ctx context.Context) error {
|
||||
// 从环境变量获取模型参数
|
||||
modelName, err := s.getEnvOrError("INIT_EMBEDDING_MODEL_NAME")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dimensionStr := os.Getenv("INIT_EMBEDDING_MODEL_DIMENSION")
|
||||
dimension, err := strconv.Atoi(dimensionStr)
|
||||
if err != nil || dimension == 0 {
|
||||
return fmt.Errorf("invalid embedding model dimension: %s", dimensionStr)
|
||||
}
|
||||
|
||||
baseURL := os.Getenv("INIT_EMBEDDING_MODEL_BASE_URL")
|
||||
apiKey := os.Getenv("INIT_EMBEDDING_MODEL_API_KEY")
|
||||
|
||||
// 确定模型来源
|
||||
source := types.ModelSourceRemote
|
||||
if baseURL == "" {
|
||||
source = types.ModelSourceLocal
|
||||
}
|
||||
|
||||
// 确定模型ID
|
||||
modelID := os.Getenv("INIT_EMBEDDING_MODEL_ID")
|
||||
if modelID == "" {
|
||||
modelID = fmt.Sprintf("builtin:%s:%d", modelName, dimension)
|
||||
}
|
||||
|
||||
// 创建嵌入模型实例
|
||||
s.EmbedModel, err = embedding.NewEmbedder(embedding.Config{
|
||||
Source: source,
|
||||
BaseURL: baseURL,
|
||||
ModelName: modelName,
|
||||
APIKey: apiKey,
|
||||
Dimensions: dimension,
|
||||
ModelID: modelID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create embedder: %w", err)
|
||||
}
|
||||
|
||||
// 如果是本地模型,使用Ollama拉取模型
|
||||
if source == types.ModelSourceLocal && s.ollamaService != nil {
|
||||
if err := s.ollamaService.PullModel(context.Background(), modelName); err != nil {
|
||||
return fmt.Errorf("failed to pull embedding model: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建模型配置
|
||||
modelConfig := &types.Model{
|
||||
ID: modelID,
|
||||
TenantID: ctx.Value(types.TenantIDContextKey).(uint),
|
||||
Name: modelName,
|
||||
Source: source,
|
||||
Type: types.ModelTypeEmbedding,
|
||||
Parameters: types.ModelParameters{
|
||||
BaseURL: baseURL,
|
||||
APIKey: apiKey,
|
||||
EmbeddingParameters: types.EmbeddingParameters{
|
||||
Dimension: dimension,
|
||||
},
|
||||
},
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
// 更新或创建模型
|
||||
return s.updateOrCreateModel(ctx, modelConfig)
|
||||
}
|
||||
|
||||
// initRerankModel 初始化重排模型
|
||||
func (s *TestDataService) initRerankModel(ctx context.Context) error {
|
||||
// 从环境变量获取模型参数
|
||||
modelName, err := s.getEnvOrError("INIT_RERANK_MODEL_NAME")
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Skip Rerank Model: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
baseURL, err := s.getEnvOrError("INIT_RERANK_MODEL_BASE_URL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiKey := os.Getenv("INIT_RERANK_MODEL_API_KEY")
|
||||
modelID := fmt.Sprintf("builtin:%s:rerank:%s", types.ModelSourceRemote, modelName)
|
||||
|
||||
// 创建重排模型实例
|
||||
s.RerankModel, err = rerank.NewReranker(&rerank.RerankerConfig{
|
||||
Source: types.ModelSourceRemote,
|
||||
BaseURL: baseURL,
|
||||
ModelName: modelName,
|
||||
APIKey: apiKey,
|
||||
ModelID: modelID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create reranker: %w", err)
|
||||
}
|
||||
|
||||
// 创建模型配置
|
||||
modelConfig := &types.Model{
|
||||
ID: modelID,
|
||||
TenantID: ctx.Value(types.TenantIDContextKey).(uint),
|
||||
Name: modelName,
|
||||
Source: types.ModelSourceRemote,
|
||||
Type: types.ModelTypeRerank,
|
||||
Parameters: types.ModelParameters{
|
||||
BaseURL: baseURL,
|
||||
APIKey: apiKey,
|
||||
},
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
// 更新或创建模型
|
||||
return s.updateOrCreateModel(ctx, modelConfig)
|
||||
}
|
||||
|
||||
// initLLMModel 初始化大语言模型
|
||||
func (s *TestDataService) initLLMModel(ctx context.Context) error {
|
||||
// 从环境变量获取模型参数
|
||||
modelName, err := s.getEnvOrError("INIT_LLM_MODEL_NAME")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseURL := os.Getenv("INIT_LLM_MODEL_BASE_URL")
|
||||
apiKey := os.Getenv("INIT_LLM_MODEL_API_KEY")
|
||||
|
||||
// 确定模型来源
|
||||
source := types.ModelSourceRemote
|
||||
if baseURL == "" {
|
||||
source = types.ModelSourceLocal
|
||||
}
|
||||
|
||||
// 确定模型ID
|
||||
modelID := fmt.Sprintf("builtin:%s:llm:%s", source, modelName)
|
||||
|
||||
// 创建大语言模型实例
|
||||
s.LLMModel, err = chat.NewChat(&chat.ChatConfig{
|
||||
Source: source,
|
||||
BaseURL: baseURL,
|
||||
ModelName: modelName,
|
||||
APIKey: apiKey,
|
||||
ModelID: modelID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create llm: %w", err)
|
||||
}
|
||||
|
||||
// 如果是本地模型,使用Ollama拉取模型
|
||||
if source == types.ModelSourceLocal && s.ollamaService != nil {
|
||||
if err := s.ollamaService.PullModel(context.Background(), modelName); err != nil {
|
||||
return fmt.Errorf("failed to pull llm model: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建模型配置
|
||||
modelConfig := &types.Model{
|
||||
ID: modelID,
|
||||
TenantID: ctx.Value(types.TenantIDContextKey).(uint),
|
||||
Name: modelName,
|
||||
Source: source,
|
||||
Type: types.ModelTypeKnowledgeQA,
|
||||
Parameters: types.ModelParameters{
|
||||
BaseURL: baseURL,
|
||||
APIKey: apiKey,
|
||||
},
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
// 更新或创建模型
|
||||
return s.updateOrCreateModel(ctx, modelConfig)
|
||||
}
|
||||
@@ -89,7 +89,6 @@ func BuildContainer(container *dig.Container) *dig.Container {
|
||||
must(container.Provide(service.NewMessageService))
|
||||
must(container.Provide(service.NewChunkService))
|
||||
must(container.Provide(embedding.NewBatchEmbedder))
|
||||
must(container.Provide(service.NewTestDataService))
|
||||
must(container.Provide(service.NewModelService))
|
||||
must(container.Provide(service.NewDatasetService))
|
||||
must(container.Provide(service.NewEvaluationService))
|
||||
@@ -116,11 +115,11 @@ func BuildContainer(container *dig.Container) *dig.Container {
|
||||
must(container.Provide(handler.NewChunkHandler))
|
||||
must(container.Provide(handler.NewSessionHandler))
|
||||
must(container.Provide(handler.NewMessageHandler))
|
||||
must(container.Provide(handler.NewTestDataHandler))
|
||||
must(container.Provide(handler.NewModelHandler))
|
||||
must(container.Provide(handler.NewEvaluationHandler))
|
||||
must(container.Provide(handler.NewInitializationHandler))
|
||||
must(container.Provide(handler.NewAuthHandler))
|
||||
must(container.Provide(handler.NewSystemHandler))
|
||||
|
||||
// Router configuration
|
||||
must(container.Provide(router.NewRouter))
|
||||
|
||||
@@ -130,75 +130,17 @@ type InitializationRequest struct {
|
||||
|
||||
DocumentSplitting struct {
|
||||
ChunkSize int `json:"chunkSize" binding:"required,min=100,max=10000"`
|
||||
ChunkOverlap int `json:"chunkOverlap" binding:"required,min=0"`
|
||||
ChunkOverlap int `json:"chunkOverlap" binding:"min=0"`
|
||||
Separators []string `json:"separators" binding:"required,min=1"`
|
||||
} `json:"documentSplitting" binding:"required"`
|
||||
}
|
||||
|
||||
// CheckStatus 检查系统初始化状态
|
||||
func (h *InitializationHandler) CheckStatus(c *gin.Context) {
|
||||
// InitializeByKB 根据知识库ID执行配置更新
|
||||
func (h *InitializationHandler) InitializeByKB(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
logger.Info(ctx, "Checking system initialization status")
|
||||
kbIdStr := c.Param("kbId")
|
||||
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
||||
|
||||
// 检查是否存在租户
|
||||
tenant, err := h.tenantService.GetTenantByID(ctx, tenantID)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"initialized": false,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有租户,说明系统未初始化
|
||||
if tenant == nil {
|
||||
logger.Info(ctx, "No tenants found, system not initialized")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"initialized": false,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否存在模型
|
||||
models, err := h.modelService.ListModels(ctx)
|
||||
if err != nil || len(models) == 0 {
|
||||
logger.Info(ctx, "No models found, system not initialized")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"initialized": false,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
// ignore api key in response for security
|
||||
for _, model := range models {
|
||||
model.Parameters.APIKey = ""
|
||||
}
|
||||
|
||||
logger.Info(ctx, "System is already initialized")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"initialized": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize 执行系统初始化
|
||||
func (h *InitializationHandler) Initialize(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Info(ctx, "Starting system initialization")
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
||||
logger.Infof(ctx, "Starting knowledge base configuration update, kbId: %s", kbIdStr)
|
||||
|
||||
var req InitializationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -207,7 +149,21 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证多模态配置
|
||||
// 获取指定知识库信息
|
||||
kb, err := h.kbService.GetKnowledgeBaseByID(ctx, kbIdStr)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{"kbId": kbIdStr})
|
||||
c.Error(errors.NewInternalServerError("获取知识库信息失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if kb == nil {
|
||||
logger.Error(ctx, "Knowledge base not found", "kbId", kbIdStr)
|
||||
c.Error(errors.NewNotFoundError("知识库不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证多模态配置(如果启用)
|
||||
if req.Multimodal.Enabled {
|
||||
storageType := strings.ToLower(req.Multimodal.StorageType)
|
||||
if req.Multimodal.VLM == nil {
|
||||
@@ -246,47 +202,12 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
|
||||
if req.Rerank.Enabled {
|
||||
if req.Rerank.ModelName == "" || req.Rerank.BaseURL == "" {
|
||||
logger.Error(ctx, "Rerank configuration incomplete")
|
||||
c.Error(errors.NewBadRequestError("启用Rerank时需要配置模型名称和Base URL"))
|
||||
c.Error(errors.NewBadRequestError("Rerank配置不完整"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证文档分割配置
|
||||
if req.DocumentSplitting.ChunkOverlap >= req.DocumentSplitting.ChunkSize {
|
||||
logger.Error(ctx, "Chunk overlap must be less than chunk size")
|
||||
c.Error(errors.NewBadRequestError("分块重叠大小必须小于分块大小"))
|
||||
return
|
||||
}
|
||||
if len(req.DocumentSplitting.Separators) == 0 {
|
||||
logger.Error(ctx, "Document separators cannot be empty")
|
||||
c.Error(errors.NewBadRequestError("文档分隔符不能为空"))
|
||||
return
|
||||
}
|
||||
var err error
|
||||
// 1. 处理租户 - 检查是否存在,不存在则创建
|
||||
tenant, _ := h.tenantService.GetTenantByID(ctx, tenantID)
|
||||
if tenant == nil {
|
||||
logger.Info(ctx, "Tenant not found, creating tenant")
|
||||
err = errors.NewInternalServerError("Failed to get tenant")
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 处理模型 - 检查现有模型并更新或创建
|
||||
existingModels, err := h.modelService.ListModels(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
// 如果获取失败,继续执行创建流程
|
||||
existingModels = []*types.Model{}
|
||||
}
|
||||
|
||||
// 构建模型映射,按类型分组
|
||||
modelMap := make(map[types.ModelType]*types.Model)
|
||||
for _, model := range existingModels {
|
||||
modelMap[model.Type] = model
|
||||
}
|
||||
|
||||
// 要处理的模型配置
|
||||
// 处理模型创建/更新
|
||||
modelsToProcess := []struct {
|
||||
modelType types.ModelType
|
||||
name string
|
||||
@@ -349,40 +270,49 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
|
||||
modelType: types.ModelTypeVLLM,
|
||||
name: req.Multimodal.VLM.ModelName,
|
||||
source: types.ModelSourceRemote,
|
||||
description: "Vision Language Model",
|
||||
description: "VLM Model",
|
||||
baseURL: req.Multimodal.VLM.BaseURL,
|
||||
apiKey: req.Multimodal.VLM.APIKey,
|
||||
})
|
||||
}
|
||||
|
||||
// 处理每个模型
|
||||
// 处理模型
|
||||
var processedModels []*types.Model
|
||||
for _, modelConfig := range modelsToProcess {
|
||||
existingModel, exists := modelMap[modelConfig.modelType]
|
||||
|
||||
if exists {
|
||||
// 更新现有模型
|
||||
logger.Infof(ctx, "Updating existing model: %s (%s)",
|
||||
modelConfig.name, modelConfig.modelType,
|
||||
)
|
||||
existingModel.Name = modelConfig.name
|
||||
existingModel.Source = modelConfig.source
|
||||
existingModel.Description = modelConfig.description
|
||||
existingModel.Parameters = types.ModelParameters{
|
||||
BaseURL: modelConfig.baseURL,
|
||||
APIKey: modelConfig.apiKey,
|
||||
EmbeddingParameters: types.EmbeddingParameters{
|
||||
Dimension: modelConfig.dimension,
|
||||
for _, modelInfo := range modelsToProcess {
|
||||
model := &types.Model{
|
||||
Type: modelInfo.modelType,
|
||||
Name: modelInfo.name,
|
||||
Source: modelInfo.source,
|
||||
Description: modelInfo.description,
|
||||
Parameters: types.ModelParameters{
|
||||
BaseURL: modelInfo.baseURL,
|
||||
APIKey: modelInfo.apiKey,
|
||||
},
|
||||
IsDefault: false,
|
||||
Status: types.ModelStatusActive,
|
||||
}
|
||||
existingModel.IsDefault = true
|
||||
existingModel.Status = types.ModelStatusActive
|
||||
|
||||
err := h.modelService.UpdateModel(ctx, existingModel)
|
||||
if modelInfo.modelType == types.ModelTypeEmbedding {
|
||||
model.Parameters.EmbeddingParameters = types.EmbeddingParameters{
|
||||
Dimension: modelInfo.dimension,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查模型是否已存在
|
||||
existingModel, err := h.modelService.GetModelByID(ctx, model.ID)
|
||||
if err == nil && existingModel != nil {
|
||||
// 更新现有模型
|
||||
existingModel.Name = model.Name
|
||||
existingModel.Source = model.Source
|
||||
existingModel.Description = model.Description
|
||||
existingModel.Parameters = model.Parameters
|
||||
existingModel.UpdatedAt = time.Now()
|
||||
|
||||
err = h.modelService.UpdateModel(ctx, existingModel)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"model_name": modelConfig.name,
|
||||
"model_type": modelConfig.modelType,
|
||||
"model_id": model.ID,
|
||||
"kb_id": kbIdStr,
|
||||
})
|
||||
c.Error(errors.NewInternalServerError("更新模型失败: " + err.Error()))
|
||||
return
|
||||
@@ -390,74 +320,20 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
|
||||
processedModels = append(processedModels, existingModel)
|
||||
} else {
|
||||
// 创建新模型
|
||||
logger.Infof(ctx,
|
||||
"Creating new model: %s (%s)",
|
||||
modelConfig.name, modelConfig.modelType,
|
||||
)
|
||||
newModel := &types.Model{
|
||||
TenantID: tenantID,
|
||||
Name: modelConfig.name,
|
||||
Type: modelConfig.modelType,
|
||||
Source: modelConfig.source,
|
||||
Description: modelConfig.description,
|
||||
Parameters: types.ModelParameters{
|
||||
BaseURL: modelConfig.baseURL,
|
||||
APIKey: modelConfig.apiKey,
|
||||
EmbeddingParameters: types.EmbeddingParameters{
|
||||
Dimension: modelConfig.dimension,
|
||||
},
|
||||
},
|
||||
IsDefault: true,
|
||||
Status: types.ModelStatusActive,
|
||||
}
|
||||
|
||||
err := h.modelService.CreateModel(ctx, newModel)
|
||||
err = h.modelService.CreateModel(ctx, model)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"model_name": modelConfig.name,
|
||||
"model_type": modelConfig.modelType,
|
||||
"model_id": model.ID,
|
||||
"kb_id": kbIdStr,
|
||||
})
|
||||
c.Error(errors.NewInternalServerError("创建模型失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
processedModels = append(processedModels, newModel)
|
||||
processedModels = append(processedModels, model)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除不需要的VLM模型(如果多模态被禁用)
|
||||
if !req.Multimodal.Enabled {
|
||||
if existingVLM, exists := modelMap[types.ModelTypeVLLM]; exists {
|
||||
logger.Info(ctx, "Deleting VLM model as multimodal is disabled")
|
||||
err := h.modelService.DeleteModel(ctx, existingVLM.ID)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"model_id": existingVLM.ID,
|
||||
})
|
||||
// 记录错误但不阻止流程
|
||||
logger.Warn(ctx, "Failed to delete VLM model, but continuing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除不需要的Rerank模型(如果Rerank被禁用)
|
||||
if !req.Rerank.Enabled {
|
||||
if existingRerank, exists := modelMap[types.ModelTypeRerank]; exists {
|
||||
logger.Info(ctx, "Deleting Rerank model as rerank is disabled")
|
||||
err := h.modelService.DeleteModel(ctx, existingRerank.ID)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"model_id": existingRerank.ID,
|
||||
})
|
||||
// 记录错误但不阻止流程
|
||||
logger.Warn(ctx, "Failed to delete Rerank model, but continuing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 处理知识库 - 检查是否存在,不存在则创建,存在则更新
|
||||
kbs, err := h.kbService.ListKnowledgeBases(ctx)
|
||||
|
||||
// 找到embedding模型ID和LLM模型ID
|
||||
// 找到模型ID
|
||||
var embeddingModelID, llmModelID, rerankModelID, vlmModelID string
|
||||
for _, model := range processedModels {
|
||||
if model.Type == types.ModelTypeEmbedding {
|
||||
@@ -466,7 +342,7 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
|
||||
if model.Type == types.ModelTypeKnowledgeQA {
|
||||
llmModelID = model.ID
|
||||
}
|
||||
if model.Type == types.ModelTypeRerank && req.Rerank.Enabled {
|
||||
if model.Type == types.ModelTypeRerank {
|
||||
rerankModelID = model.ID
|
||||
}
|
||||
if model.Type == types.ModelTypeVLLM {
|
||||
@@ -474,85 +350,26 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var kb *types.KnowledgeBase
|
||||
|
||||
if len(kbs) == 0 {
|
||||
// 创建新知识库
|
||||
logger.Info(ctx, "Creating default knowledge base")
|
||||
kb = &types.KnowledgeBase{
|
||||
ID: uuid.New().String(),
|
||||
Name: "Default Knowledge Base",
|
||||
Description: "System Default Knowledge Base",
|
||||
TenantID: tenantID,
|
||||
ChunkingConfig: types.ChunkingConfig{
|
||||
ChunkSize: req.DocumentSplitting.ChunkSize,
|
||||
ChunkOverlap: req.DocumentSplitting.ChunkOverlap,
|
||||
Separators: req.DocumentSplitting.Separators,
|
||||
EnableMultimodal: req.Multimodal.Enabled,
|
||||
},
|
||||
EmbeddingModelID: embeddingModelID,
|
||||
SummaryModelID: llmModelID,
|
||||
RerankModelID: rerankModelID,
|
||||
VLMModelID: vlmModelID,
|
||||
VLMConfig: types.VLMConfig{
|
||||
ModelName: req.Multimodal.VLM.ModelName,
|
||||
BaseURL: req.Multimodal.VLM.BaseURL,
|
||||
APIKey: req.Multimodal.VLM.APIKey,
|
||||
InterfaceType: req.Multimodal.VLM.InterfaceType,
|
||||
},
|
||||
}
|
||||
switch req.Multimodal.StorageType {
|
||||
case "cos":
|
||||
if req.Multimodal.COS != nil {
|
||||
kb.StorageConfig = types.StorageConfig{
|
||||
Provider: req.Multimodal.StorageType,
|
||||
BucketName: req.Multimodal.COS.BucketName,
|
||||
AppID: req.Multimodal.COS.AppID,
|
||||
PathPrefix: req.Multimodal.COS.PathPrefix,
|
||||
SecretID: req.Multimodal.COS.SecretID,
|
||||
SecretKey: req.Multimodal.COS.SecretKey,
|
||||
Region: req.Multimodal.COS.Region,
|
||||
}
|
||||
}
|
||||
case "minio":
|
||||
if req.Multimodal.Minio != nil {
|
||||
kb.StorageConfig = types.StorageConfig{
|
||||
Provider: req.Multimodal.StorageType,
|
||||
BucketName: req.Multimodal.Minio.BucketName,
|
||||
PathPrefix: req.Multimodal.Minio.PathPrefix,
|
||||
SecretID: os.Getenv("MINIO_ACCESS_KEY_ID"),
|
||||
SecretKey: os.Getenv("MINIO_SECRET_ACCESS_KEY"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = h.kbService.CreateKnowledgeBase(ctx, kb)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(errors.NewInternalServerError("创建知识库失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 更新现有知识库
|
||||
logger.Info(ctx, "Updating existing knowledge base")
|
||||
kb = kbs[0]
|
||||
|
||||
// 检查是否有文件,如果有文件则不允许修改Embedding模型
|
||||
knowledgeList, err := h.knowledgeService.ListKnowledgeByKnowledgeBaseID(
|
||||
ctx, kb.ID,
|
||||
)
|
||||
hasFiles := err == nil && len(knowledgeList) > 0
|
||||
|
||||
// 先更新模型ID(直接在对象上)
|
||||
// 更新知识库配置
|
||||
kb.SummaryModelID = llmModelID
|
||||
kb.EmbeddingModelID = embeddingModelID
|
||||
if req.Rerank.Enabled {
|
||||
kb.RerankModelID = rerankModelID
|
||||
} else {
|
||||
kb.RerankModelID = "" // 清空Rerank模型ID
|
||||
kb.RerankModelID = ""
|
||||
}
|
||||
|
||||
// 更新文档分割配置
|
||||
kb.ChunkingConfig = types.ChunkingConfig{
|
||||
ChunkSize: req.DocumentSplitting.ChunkSize,
|
||||
ChunkOverlap: req.DocumentSplitting.ChunkOverlap,
|
||||
Separators: req.DocumentSplitting.Separators,
|
||||
}
|
||||
|
||||
// 更新多模态配置
|
||||
if req.Multimodal.Enabled {
|
||||
kb.ChunkingConfig.EnableMultimodal = true
|
||||
kb.VLMModelID = vlmModelID
|
||||
// 更新VLM配置
|
||||
kb.VLMConfig = types.VLMConfig{
|
||||
ModelName: req.Multimodal.VLM.ModelName,
|
||||
BaseURL: req.Multimodal.VLM.BaseURL,
|
||||
@@ -564,12 +381,12 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
|
||||
if req.Multimodal.COS != nil {
|
||||
kb.StorageConfig = types.StorageConfig{
|
||||
Provider: req.Multimodal.StorageType,
|
||||
SecretID: req.Multimodal.COS.SecretID,
|
||||
SecretKey: req.Multimodal.COS.SecretKey,
|
||||
Region: req.Multimodal.COS.Region,
|
||||
BucketName: req.Multimodal.COS.BucketName,
|
||||
AppID: req.Multimodal.COS.AppID,
|
||||
PathPrefix: req.Multimodal.COS.PathPrefix,
|
||||
SecretID: req.Multimodal.COS.SecretID,
|
||||
SecretKey: req.Multimodal.COS.SecretKey,
|
||||
Region: req.Multimodal.COS.Region,
|
||||
}
|
||||
}
|
||||
case "minio":
|
||||
@@ -584,65 +401,23 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
kb.VLMModelID = "" // 清空VLM模型ID
|
||||
// 清空VLM配置
|
||||
kb.VLMModelID = ""
|
||||
kb.VLMConfig = types.VLMConfig{}
|
||||
kb.StorageConfig = types.StorageConfig{}
|
||||
}
|
||||
if !hasFiles {
|
||||
kb.EmbeddingModelID = embeddingModelID
|
||||
}
|
||||
kb.ChunkingConfig = types.ChunkingConfig{
|
||||
ChunkSize: req.DocumentSplitting.ChunkSize,
|
||||
ChunkOverlap: req.DocumentSplitting.ChunkOverlap,
|
||||
Separators: req.DocumentSplitting.Separators,
|
||||
EnableMultimodal: req.Multimodal.Enabled,
|
||||
}
|
||||
|
||||
// 更新基本信息和配置
|
||||
err = h.kbRepository.UpdateKnowledgeBase(ctx, kb)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{"kbId": kbIdStr})
|
||||
c.Error(errors.NewInternalServerError("更新知识库配置失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 如果需要更新模型ID,使用repository直接更新
|
||||
if !hasFiles || kb.SummaryModelID != llmModelID {
|
||||
// 刷新知识库对象以获取最新信息
|
||||
kb, err = h.kbService.GetKnowledgeBaseByID(ctx, kb.ID)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(errors.NewInternalServerError("获取更新后的知识库失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 更新模型ID
|
||||
kb.SummaryModelID = llmModelID
|
||||
if req.Rerank.Enabled {
|
||||
kb.RerankModelID = rerankModelID
|
||||
} else {
|
||||
kb.RerankModelID = "" // 清空Rerank模型ID
|
||||
}
|
||||
|
||||
// 使用repository直接更新模型ID
|
||||
err = h.kbRepository.UpdateKnowledgeBase(ctx, kb)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(errors.NewInternalServerError("更新知识库模型ID失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info(ctx, "Model IDs updated successfully")
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info(ctx, "System initialization completed successfully")
|
||||
logger.Info(ctx, "Knowledge base configuration updated successfully", "kbId", kbIdStr)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "系统初始化成功",
|
||||
"message": "知识库配置更新成功",
|
||||
"data": gin.H{
|
||||
"tenant": tenant,
|
||||
"models": processedModels,
|
||||
"knowledge_base": kb,
|
||||
},
|
||||
@@ -1029,39 +804,53 @@ func (h *InitializationHandler) updateTaskStatus(
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentConfig 获取当前系统配置信息
|
||||
func (h *InitializationHandler) GetCurrentConfig(c *gin.Context) {
|
||||
// GetCurrentConfigByKB 根据知识库ID获取配置信息
|
||||
func (h *InitializationHandler) GetCurrentConfigByKB(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
kbIdStr := c.Param("kbId")
|
||||
|
||||
logger.Info(ctx, "Getting current system configuration")
|
||||
logger.Info(ctx, "Getting configuration for knowledge base", "kbId", kbIdStr)
|
||||
|
||||
// 获取模型信息
|
||||
models, err := h.modelService.ListModels(ctx)
|
||||
// 获取指定知识库信息
|
||||
kb, err := h.kbService.GetKnowledgeBaseByID(ctx, kbIdStr)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(errors.NewInternalServerError("获取模型列表失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取知识库信息
|
||||
kbs, err := h.kbService.ListKnowledgeBases(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{"kbId": kbIdStr})
|
||||
c.Error(errors.NewInternalServerError("获取知识库信息失败: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if len(kbs) == 0 {
|
||||
logger.Error(ctx, "No knowledge bases found")
|
||||
c.Error(errors.NewInternalServerError("获取知识库信息失败"))
|
||||
if kb == nil {
|
||||
logger.Error(ctx, "Knowledge base not found", "kbId", kbIdStr)
|
||||
c.Error(errors.NewNotFoundError("知识库不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
kb := kbs[0]
|
||||
// 根据知识库的模型ID获取特定模型
|
||||
var models []*types.Model
|
||||
modelIDs := []string{
|
||||
kb.EmbeddingModelID,
|
||||
kb.SummaryModelID,
|
||||
kb.RerankModelID,
|
||||
kb.VLMModelID,
|
||||
}
|
||||
|
||||
for _, modelID := range modelIDs {
|
||||
if modelID != "" {
|
||||
model, err := h.modelService.GetModelByID(ctx, modelID)
|
||||
if err != nil {
|
||||
logger.Warn(ctx, "Failed to get model", "kbId", kbIdStr, "modelId", modelID, "error", err)
|
||||
// 如果模型不存在或获取失败,继续处理其他模型
|
||||
continue
|
||||
}
|
||||
if model != nil {
|
||||
models = append(models, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查知识库是否有文件
|
||||
knowledgeList, err := h.knowledgeService.ListPagedKnowledgeByKnowledgeBaseID(ctx,
|
||||
kb.ID, &types.Pagination{
|
||||
kbIdStr, &types.Pagination{
|
||||
Page: 1,
|
||||
PageSize: 1,
|
||||
})
|
||||
@@ -1073,7 +862,7 @@ func (h *InitializationHandler) GetCurrentConfig(c *gin.Context) {
|
||||
// 构建配置响应
|
||||
config := buildConfigResponse(models, kb, hasFiles)
|
||||
|
||||
logger.Info(ctx, "Current system configuration retrieved successfully")
|
||||
logger.Info(ctx, "Knowledge base configuration retrieved successfully", "kbId", kbIdStr)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": config,
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/application/service"
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/errors"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
@@ -22,7 +21,6 @@ type SessionHandler struct {
|
||||
sessionService interfaces.SessionService // Service for managing sessions
|
||||
streamManager interfaces.StreamManager // Manager for handling streaming responses
|
||||
config *config.Config // Application configuration
|
||||
testDataService *service.TestDataService // Service for test data (models, etc.)
|
||||
knowledgebaseService interfaces.KnowledgeBaseService
|
||||
}
|
||||
|
||||
@@ -32,7 +30,6 @@ func NewSessionHandler(
|
||||
messageService interfaces.MessageService,
|
||||
streamManager interfaces.StreamManager,
|
||||
config *config.Config,
|
||||
testDataService *service.TestDataService,
|
||||
knowledgebaseService interfaces.KnowledgeBaseService,
|
||||
) *SessionHandler {
|
||||
return &SessionHandler{
|
||||
@@ -40,7 +37,6 @@ func NewSessionHandler(
|
||||
messageService: messageService,
|
||||
streamManager: streamManager,
|
||||
config: config,
|
||||
testDataService: testDataService,
|
||||
knowledgebaseService: knowledgebaseService,
|
||||
}
|
||||
}
|
||||
|
||||
49
internal/handler/system.go
Normal file
49
internal/handler/system.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SystemHandler handles system-related requests
|
||||
type SystemHandler struct{}
|
||||
|
||||
// NewSystemHandler creates a new system handler
|
||||
func NewSystemHandler() *SystemHandler {
|
||||
return &SystemHandler{}
|
||||
}
|
||||
|
||||
// GetSystemInfoResponse defines the response structure for system info
|
||||
type GetSystemInfoResponse struct {
|
||||
Version string `json:"version"`
|
||||
CommitID string `json:"commit_id,omitempty"`
|
||||
BuildTime string `json:"build_time,omitempty"`
|
||||
GoVersion string `json:"go_version,omitempty"`
|
||||
}
|
||||
|
||||
// 编译时注入的版本信息
|
||||
var (
|
||||
Version = "unknown"
|
||||
CommitID = "unknown"
|
||||
BuildTime = "unknown"
|
||||
GoVersion = "unknown"
|
||||
)
|
||||
|
||||
// GetSystemInfo gets system information including version and commit ID
|
||||
func (h *SystemHandler) GetSystemInfo(c *gin.Context) {
|
||||
ctx := logger.CloneContext(c.Request.Context())
|
||||
|
||||
response := GetSystemInfoResponse{
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
BuildTime: BuildTime,
|
||||
GoVersion: GoVersion,
|
||||
}
|
||||
|
||||
logger.Info(ctx, "System info retrieved successfully")
|
||||
c.JSON(200, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": response,
|
||||
})
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/errors"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
|
||||
// TestDataHandler handles HTTP requests related to test data operations
|
||||
// Used for development and testing purposes to provide sample data
|
||||
type TestDataHandler struct {
|
||||
config *config.Config
|
||||
kbService interfaces.KnowledgeBaseService
|
||||
tenantService interfaces.TenantService
|
||||
}
|
||||
|
||||
// NewTestDataHandler creates a new instance of the test data handler
|
||||
// Parameters:
|
||||
// - config: Application configuration instance
|
||||
// - kbService: Knowledge base service for accessing knowledge base data
|
||||
// - tenantService: Tenant service for accessing tenant data
|
||||
//
|
||||
// Returns a pointer to the new TestDataHandler instance
|
||||
func NewTestDataHandler(
|
||||
config *config.Config,
|
||||
kbService interfaces.KnowledgeBaseService,
|
||||
tenantService interfaces.TenantService,
|
||||
) *TestDataHandler {
|
||||
return &TestDataHandler{
|
||||
config: config,
|
||||
kbService: kbService,
|
||||
tenantService: tenantService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTestData handles the HTTP request to retrieve test data for development purposes
|
||||
// It returns predefined test tenant and knowledge base information
|
||||
// This endpoint is only available in non-production environments
|
||||
// Parameters:
|
||||
// - c: Gin context for the HTTP request
|
||||
func (h *TestDataHandler) GetTestData(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Info(ctx, "Start retrieving test data")
|
||||
|
||||
tenantID := c.GetUint(types.TenantIDContextKey.String())
|
||||
logger.Debugf(ctx, "Test tenant ID environment variable: %d", tenantID)
|
||||
|
||||
// Retrieve the test tenant data
|
||||
logger.Infof(ctx, "Retrieving test tenant, ID: %d", tenantID)
|
||||
tenant, err := h.tenantService.GetTenantByID(ctx, tenantID)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve the test knowledge base data
|
||||
kbs, err := h.kbService.ListKnowledgeBases(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(kbs) == 0 {
|
||||
logger.Error(ctx, "No knowledge bases found")
|
||||
c.Error(errors.NewInternalServerError("获取知识库信息失败"))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info(ctx, "Test data retrieved successfully")
|
||||
// Return the test data in the response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": gin.H{
|
||||
"tenant": tenant,
|
||||
"knowledge_bases": kbs,
|
||||
},
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
@@ -85,6 +85,7 @@ func (c *RemoteAPIChat) buildChatCompletionRequest(messages []Message,
|
||||
Messages: c.convertMessages(messages),
|
||||
Stream: isStream,
|
||||
}
|
||||
thinking := false
|
||||
|
||||
// 添加可选参数
|
||||
if opts != nil {
|
||||
@@ -106,6 +107,13 @@ func (c *RemoteAPIChat) buildChatCompletionRequest(messages []Message,
|
||||
if opts.PresencePenalty > 0 {
|
||||
req.PresencePenalty = float32(opts.PresencePenalty)
|
||||
}
|
||||
if opts.Thinking != nil {
|
||||
thinking = *opts.Thinking
|
||||
}
|
||||
}
|
||||
|
||||
req.ChatTemplateKwargs = map[string]interface{}{
|
||||
"enable_thinking": thinking,
|
||||
}
|
||||
|
||||
return req
|
||||
|
||||
@@ -33,11 +33,11 @@ type RouterParams struct {
|
||||
ChunkHandler *handler.ChunkHandler
|
||||
SessionHandler *handler.SessionHandler
|
||||
MessageHandler *handler.MessageHandler
|
||||
TestDataHandler *handler.TestDataHandler
|
||||
ModelHandler *handler.ModelHandler
|
||||
EvaluationHandler *handler.EvaluationHandler
|
||||
AuthHandler *handler.AuthHandler
|
||||
InitializationHandler *handler.InitializationHandler
|
||||
SystemHandler *handler.SystemHandler
|
||||
}
|
||||
|
||||
// NewRouter 创建新的路由
|
||||
@@ -83,7 +83,7 @@ func NewRouter(params RouterParams) *gin.Engine {
|
||||
RegisterModelRoutes(v1, params.ModelHandler)
|
||||
RegisterEvaluationRoutes(v1, params.EvaluationHandler)
|
||||
RegisterInitializationRoutes(v1, params.InitializationHandler)
|
||||
RegisterTestDataRoutes(v1, params.TestDataHandler)
|
||||
RegisterSystemRoutes(v1, params.SystemHandler)
|
||||
}
|
||||
|
||||
return r
|
||||
@@ -238,10 +238,6 @@ func RegisterEvaluationRoutes(r *gin.RouterGroup, handler *handler.EvaluationHan
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterTestDataRoutes(r *gin.RouterGroup, handler *handler.TestDataHandler) {
|
||||
r.GET("/test-data", handler.GetTestData)
|
||||
}
|
||||
|
||||
// RegisterAuthRoutes registers authentication routes
|
||||
func RegisterAuthRoutes(r *gin.RouterGroup, handler *handler.AuthHandler) {
|
||||
r.POST("/auth/register", handler.Register)
|
||||
@@ -255,9 +251,8 @@ func RegisterAuthRoutes(r *gin.RouterGroup, handler *handler.AuthHandler) {
|
||||
|
||||
func RegisterInitializationRoutes(r *gin.RouterGroup, handler *handler.InitializationHandler) {
|
||||
// 初始化接口
|
||||
r.GET("/initialization/status", handler.CheckStatus)
|
||||
r.GET("/initialization/config", handler.GetCurrentConfig)
|
||||
r.POST("/initialization/initialize", handler.Initialize)
|
||||
r.GET("/initialization/config/:kbId", handler.GetCurrentConfigByKB)
|
||||
r.POST("/initialization/initialize/:kbId", handler.InitializeByKB)
|
||||
|
||||
// Ollama相关接口
|
||||
r.GET("/initialization/ollama/status", handler.CheckOllamaStatus)
|
||||
@@ -273,3 +268,11 @@ func RegisterInitializationRoutes(r *gin.RouterGroup, handler *handler.Initializ
|
||||
r.POST("/initialization/rerank/check", handler.CheckRerankModel)
|
||||
r.POST("/initialization/multimodal/test", handler.TestMultimodalFunction)
|
||||
}
|
||||
|
||||
// RegisterSystemRoutes registers system information routes
|
||||
func RegisterSystemRoutes(r *gin.RouterGroup, handler *handler.SystemHandler) {
|
||||
systemRoutes := r.Group("/system")
|
||||
{
|
||||
systemRoutes.GET("/info", handler.GetSystemInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,17 +91,56 @@ check_platform() {
|
||||
log_info "当前架构:$TARGETARCH"
|
||||
}
|
||||
|
||||
# 获取版本信息
|
||||
get_version_info() {
|
||||
# 从VERSION文件获取版本号
|
||||
if [ -f "VERSION" ]; then
|
||||
VERSION=$(cat VERSION | tr -d '\n\r')
|
||||
else
|
||||
VERSION="unknown"
|
||||
fi
|
||||
|
||||
# 获取commit ID
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
COMMIT_ID=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
else
|
||||
COMMIT_ID="unknown"
|
||||
fi
|
||||
|
||||
# 获取构建时间
|
||||
BUILD_TIME=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||
|
||||
# 获取Go版本
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
GO_VERSION=$(go version 2>/dev/null || echo "unknown")
|
||||
else
|
||||
GO_VERSION="unknown"
|
||||
fi
|
||||
|
||||
log_info "版本信息: $VERSION"
|
||||
log_info "Commit ID: $COMMIT_ID"
|
||||
log_info "构建时间: $BUILD_TIME"
|
||||
log_info "Go版本: $GO_VERSION"
|
||||
}
|
||||
|
||||
# 构建应用镜像
|
||||
build_app_image() {
|
||||
log_info "构建应用镜像 (weknora-app)..."
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# 获取版本信息
|
||||
get_version_info
|
||||
|
||||
docker build \
|
||||
--platform $PLATFORM \
|
||||
--build-arg GOPRIVATE_ARG=${GOPRIVATE:-""} \
|
||||
--build-arg GOPROXY_ARG=${GOPROXY:-"https://goproxy.cn,direct"} \
|
||||
--build-arg GOSUMDB_ARG=${GOSUMDB:-"off"} \
|
||||
--build-arg VERSION_ARG="$VERSION" \
|
||||
--build-arg COMMIT_ID_ARG="$COMMIT_ID" \
|
||||
--build-arg BUILD_TIME_ARG="$BUILD_TIME" \
|
||||
--build-arg GO_VERSION_ARG="$GO_VERSION" \
|
||||
-f docker/Dockerfile.app \
|
||||
-t wechatopenai/weknora-app:latest \
|
||||
.
|
||||
|
||||
86
scripts/get_version.sh
Executable file
86
scripts/get_version.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# 统一的版本信息获取脚本
|
||||
# 支持本地构建和CI构建环境
|
||||
|
||||
# 设置默认值
|
||||
VERSION="unknown"
|
||||
COMMIT_ID="unknown"
|
||||
BUILD_TIME="unknown"
|
||||
GO_VERSION="unknown"
|
||||
|
||||
# 获取版本号
|
||||
if [ -f "VERSION" ]; then
|
||||
VERSION=$(cat VERSION | tr -d '\n\r')
|
||||
fi
|
||||
|
||||
# 获取commit ID
|
||||
if [ -n "$GITHUB_SHA" ]; then
|
||||
# GitHub Actions环境
|
||||
COMMIT_ID="${GITHUB_SHA:0:7}"
|
||||
elif command -v git >/dev/null 2>&1; then
|
||||
# 本地环境
|
||||
COMMIT_ID=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
fi
|
||||
|
||||
# 获取构建时间
|
||||
if [ -n "$GITHUB_ACTIONS" ]; then
|
||||
# GitHub Actions环境,使用标准时间格式
|
||||
BUILD_TIME=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||
else
|
||||
# 本地环境
|
||||
BUILD_TIME=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||
fi
|
||||
|
||||
# 获取Go版本
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
GO_VERSION=$(go version 2>/dev/null || echo "unknown")
|
||||
fi
|
||||
|
||||
# 根据参数输出不同格式
|
||||
case "${1:-env}" in
|
||||
"env")
|
||||
# 输出环境变量格式
|
||||
echo "VERSION=$VERSION"
|
||||
echo "COMMIT_ID=$COMMIT_ID"
|
||||
echo "BUILD_TIME=$BUILD_TIME"
|
||||
echo "GO_VERSION=$GO_VERSION"
|
||||
;;
|
||||
"json")
|
||||
# 输出JSON格式
|
||||
cat << EOF
|
||||
{
|
||||
"version": "$VERSION",
|
||||
"commit_id": "$COMMIT_ID",
|
||||
"build_time": "$BUILD_TIME",
|
||||
"go_version": "$GO_VERSION"
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
"docker-args")
|
||||
# 输出Docker构建参数格式
|
||||
echo "--build-arg VERSION_ARG=$VERSION"
|
||||
echo "--build-arg COMMIT_ID_ARG=$COMMIT_ID"
|
||||
echo "--build-arg BUILD_TIME_ARG=$BUILD_TIME"
|
||||
echo "--build-arg GO_VERSION_ARG=$GO_VERSION"
|
||||
;;
|
||||
"ldflags")
|
||||
# 输出Go ldflags格式
|
||||
echo "-X 'github.com/Tencent/WeKnora/internal/handler.Version=$VERSION' -X 'github.com/Tencent/WeKnora/internal/handler.CommitID=$COMMIT_ID' -X 'github.com/Tencent/WeKnora/internal/handler.BuildTime=$BUILD_TIME' -X 'github.com/Tencent/WeKnora/internal/handler.GoVersion=$GO_VERSION'"
|
||||
;;
|
||||
"info")
|
||||
# 输出信息格式
|
||||
echo "版本信息: $VERSION"
|
||||
echo "Commit ID: $COMMIT_ID"
|
||||
echo "构建时间: $BUILD_TIME"
|
||||
echo "Go版本: $GO_VERSION"
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 [env|json|docker-args|ldflags|info]"
|
||||
echo " env - 输出环境变量格式 (默认)"
|
||||
echo " json - 输出JSON格式"
|
||||
echo " docker-args - 输出Docker构建参数格式"
|
||||
echo " ldflags - 输出Go ldflags格式"
|
||||
echo " info - 输出信息格式"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user