feat(ui): Support multi knowledgebases operation

This commit is contained in:
wizardchen
2025-09-17 16:02:08 +08:00
parent 76fbfdf8ac
commit 91e65d6445
39 changed files with 1718 additions and 4839 deletions

View File

@@ -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

View File

@@ -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..."

View File

@@ -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: "抱歉,我无法回答这个问题。"

View File

@@ -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

View File

@@ -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,

View File

@@ -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}`);
}

View File

@@ -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:

View File

@@ -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);
});
});

View File

@@ -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('没有可用的知识库');
}
return testData.knowledge_bases[0].id;
import { get, post, put, del, postUpload, getDown } from "../../utils/request";
// 知识库管理 API列表、创建、获取、更新、删除、复制
export function listKnowledgeBases() {
return get(`/api/v1/knowledge-bases`);
}
export async function uploadKnowledgeBase(data = {}) {
const kbId = await getDefaultKnowledgeBaseId();
export function createKnowledgeBase(data: { name: string; description?: string; chunking_config?: any }) {
return post(`/api/v1/knowledge-bases`, data);
}
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`);
}

View 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')
}

View File

@@ -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);
}

View File

@@ -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;
}
// 获取当前知识库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) => {
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,59 +334,131 @@ 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'; // 使用专门的用户图标
logoutIcon.value = 'logout.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') {
@@ -220,26 +466,84 @@ const gotopage = (path) => {
router.push('/login');
return;
} else {
router.push(`/platform/${path}`);
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 {

View File

@@ -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,

View File

@@ -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()
}
next()
});
export default router

View File

@@ -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 = () => {

View File

@@ -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: {},
});

View File

@@ -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,

View File

@@ -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)) {

View File

@@ -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) => {

View File

@@ -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()

View File

@@ -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'
});
}
// 处理流式数据

View File

@@ -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);
}
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
}
}
async function createNewSession(value: string) {
// 使用测试数据获取知识库ID
const testData = getTestData();
if (!testData || testData.knowledge_bases.length === 0) {
console.error("测试数据未初始化或不包含知识库");
return;
let knowledgeBaseId = await ensureKbId()
if (!knowledgeBaseId) {
// 等待用户在弹窗中选择
pendingValue.value = value
return
}
// 使用第一个知识库ID
const knowledgeBaseId = testData.knowledge_bases[0].id;
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

View File

@@ -1,5 +1,6 @@
<template>
<div class="initialization-content">
<div class="settings-page-container">
<div class="initialization-content">
<!-- 顶部Ollama服务状态 -->
<div class="ollama-summary-card">
<div class="summary-header">
@@ -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>
<!-- 提交状态提示 -->
@@ -725,6 +741,7 @@
</div>
</div>
</t-form>
</div>
</div>
</template>
@@ -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);
}
// 直接使用formDataAPI期望原始结构
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;

View File

@@ -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';
})
clearInterval(timeout);
timeout = null;
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;
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;
// 如果当前页面没有知识库ID尝试从localStorage获取设置中的知识库ID
if (!sessionKbId) {
const settingsStr = localStorage.getItem("WeKnora_settings");
if (settingsStr) {
try {
const settings = JSON.parse(settingsStr);
sessionKbId = settings.knowledgeBaseId;
} catch (e) {
console.error("解析设置失败:", e);
}
} 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 {

View 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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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()
logger.Infof(ctx, "Using default rerank model: %s", rerankModelID)
// 获取默认的重排模型
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)
}

View File

@@ -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)
}

View File

@@ -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))

View File

@@ -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]
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,
}
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,
},
if modelInfo.modelType == types.ModelTypeEmbedding {
model.Parameters.EmbeddingParameters = types.EmbeddingParameters{
Dimension: modelInfo.dimension,
}
existingModel.IsDefault = true
existingModel.Status = types.ModelStatusActive
}
err := h.modelService.UpdateModel(ctx, existingModel)
// 检查模型是否已存在
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,32 +350,31 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
}
}
var kb *types.KnowledgeBase
// 更新知识库配置
kb.SummaryModelID = llmModelID
kb.EmbeddingModelID = embeddingModelID
if req.Rerank.Enabled {
kb.RerankModelID = rerankModelID
} else {
kb.RerankModelID = ""
}
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,
},
// 更新文档分割配置
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
kb.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":
@@ -525,124 +400,24 @@ func (h *InitializationHandler) Initialize(c *gin.Context) {
}
}
}
_, 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
if req.Rerank.Enabled {
kb.RerankModelID = rerankModelID
} else {
kb.RerankModelID = "" // 清空Rerank模型ID
}
if req.Multimodal.Enabled {
kb.VLMModelID = vlmModelID
// 更新VLM配置
kb.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,
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,
}
}
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"),
}
}
}
} else {
kb.VLMModelID = "" // 清空VLM模型ID
// 清空VLM配置
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)
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")
}
kb.VLMModelID = ""
kb.VLMConfig = types.VLMConfig{}
kb.StorageConfig = types.StorageConfig{}
}
logger.Info(ctx, "System initialization completed successfully")
err = h.kbRepository.UpdateKnowledgeBase(ctx, kb)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"kbId": kbIdStr})
c.Error(errors.NewInternalServerError("更新知识库配置失败: " + err.Error()))
return
}
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,

View File

@@ -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,
}
}

View 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,
})
}

View File

@@ -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,
})
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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
View 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