feat: Refactor agent configuration by removing deprecated parameters and enhancing conversation model management
- Removed the `thinkingModelId` and `rerankModelId` from agent configuration, streamlining the settings structure. - Introduced a new `conversationModels` interface to manage summary and rerank model IDs. - Updated related UI components and API logic to accommodate the changes in agent configuration. - Enhanced localization for new conversation model settings in English, Russian, and Chinese.
This commit is contained in:
@@ -55,7 +55,6 @@ type AgentConfig struct {
|
||||
ReflectionEnabled bool `json:"reflection_enabled"` // Whether to enable reflection
|
||||
AllowedTools []string `json:"allowed_tools"` // List of allowed tool names
|
||||
Temperature float64 `json:"temperature"` // LLM temperature for agent
|
||||
ThinkingModelID string `json:"thinking_model_id"` // Model ID for reasoning
|
||||
KnowledgeBases []string `json:"knowledge_bases"` // Accessible knowledge base IDs
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,13 @@ export function createModel(data: ModelConfig): Promise<ModelConfig> {
|
||||
// 获取模型列表
|
||||
export function listModels(type?: string): Promise<ModelConfig[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = type ? `/api/v1/models?type=${type}` : '/api/v1/models';
|
||||
const url = `/api/v1/models`;
|
||||
get(url)
|
||||
.then((response: any) => {
|
||||
if (response.success && response.data) {
|
||||
if (type) {
|
||||
response.data = response.data.filter((item: ModelConfig) => item.type === type);
|
||||
}
|
||||
resolve(response.data);
|
||||
} else {
|
||||
resolve([]);
|
||||
|
||||
@@ -29,8 +29,6 @@ export interface AgentConfig {
|
||||
reflection_enabled: boolean
|
||||
allowed_tools: string[]
|
||||
temperature: number
|
||||
thinking_model_id: string
|
||||
rerank_model_id: string
|
||||
knowledge_bases?: string[]
|
||||
system_prompt_web_enabled?: string // Custom system prompt when web search is enabled
|
||||
system_prompt_web_disabled?: string // Custom system prompt when web search is disabled
|
||||
|
||||
@@ -5,11 +5,12 @@ import { onBeforeRouteUpdate } from 'vue-router';
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
import { listKnowledgeBases, getKnowledgeBaseById } from '@/api/knowledge-base';
|
||||
import { listKnowledgeBases } from '@/api/knowledge-base';
|
||||
import { stopSession } from '@/api/chat';
|
||||
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue';
|
||||
import { getModel, type ModelConfig } from '@/api/model';
|
||||
import { listModels, type ModelConfig } from '@/api/model';
|
||||
import { getTenantWebSearchConfig } from '@/api/web-search';
|
||||
import { getConversationConfig, type ConversationConfig } from '@/api/system';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const route = useRoute();
|
||||
@@ -52,7 +53,8 @@ const selectedKbs = computed(() => {
|
||||
// 模型相关状态
|
||||
const availableModels = ref<ModelConfig[]>([]);
|
||||
const selectedModelId = ref<string>('');
|
||||
const thinkingModelName = ref<string>('');
|
||||
const conversationConfig = ref<ConversationConfig | null>(null);
|
||||
const modelsLoading = ref(false);
|
||||
const showModelSelector = ref(false);
|
||||
const modelButtonRef = ref<HTMLElement>();
|
||||
const modelDropdownStyle = ref<Record<string, string>>({});
|
||||
@@ -108,115 +110,126 @@ const loadWebSearchConfig = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 解析模型参数大小 (e.g., "7B", "13B", "70B" -> 7, 13, 70)
|
||||
const parseParameterSize = (paramSize: string | undefined): number => {
|
||||
if (!paramSize) return 0;
|
||||
const match = paramSize.match(/(\d+\.?\d*)/);
|
||||
return match ? parseFloat(match[1]) : 0;
|
||||
const loadConversationConfig = async () => {
|
||||
try {
|
||||
const response = await getConversationConfig();
|
||||
conversationConfig.value = response.data;
|
||||
settingsStore.updateConversationModels({
|
||||
summaryModelId: response.data?.summary_model_id || '',
|
||||
rerankModelId: response.data?.rerank_model_id || '',
|
||||
});
|
||||
if (!selectedModelId.value) {
|
||||
selectedModelId.value = response.data?.summary_model_id || '';
|
||||
}
|
||||
ensureModelSelection();
|
||||
} catch (error) {
|
||||
console.error('Failed to load conversation config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载并选择模型
|
||||
const loadModelsFromKnowledgeBases = async () => {
|
||||
if (selectedKbIds.value.length === 0) {
|
||||
const loadChatModels = async () => {
|
||||
if (modelsLoading.value) return;
|
||||
modelsLoading.value = true;
|
||||
try {
|
||||
const models = await listModels('KnowledgeQA');
|
||||
availableModels.value = Array.isArray(models) ? models : [];
|
||||
ensureModelSelection();
|
||||
} catch (error) {
|
||||
console.error('Failed to load chat models:', error);
|
||||
availableModels.value = [];
|
||||
} finally {
|
||||
modelsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const ensureModelSelection = () => {
|
||||
if (selectedModelId.value) {
|
||||
return;
|
||||
}
|
||||
if (conversationConfig.value?.summary_model_id) {
|
||||
selectedModelId.value = conversationConfig.value.summary_model_id;
|
||||
return;
|
||||
}
|
||||
if (availableModels.value.length > 0) {
|
||||
selectedModelId.value = availableModels.value[0].id || '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoToConversationModels = () => {
|
||||
showModelSelector.value = false;
|
||||
router.push('/platform/settings');
|
||||
setTimeout(() => {
|
||||
const event = new CustomEvent('settings-nav', {
|
||||
detail: { section: 'models', subsection: 'chat' },
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleModelChange = (value: string | number | Array<string | number> | undefined) => {
|
||||
const normalized = Array.isArray(value) ? value[0] : value;
|
||||
const val = normalized !== undefined && normalized !== null ? String(normalized) : '';
|
||||
|
||||
if (!val) {
|
||||
selectedModelId.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取所有选中知识库的 summary_model_id
|
||||
const modelIds = new Set<string>();
|
||||
for (const kbId of selectedKbIds.value) {
|
||||
try {
|
||||
const kb: any = await getKnowledgeBaseById(kbId);
|
||||
if (kb.data?.summary_model_id) {
|
||||
modelIds.add(kb.data.summary_model_id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch KB ${kbId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有模型的详细信息
|
||||
const models: ModelConfig[] = [];
|
||||
for (const modelId of Array.from(modelIds)) {
|
||||
try {
|
||||
const model = await getModel(modelId);
|
||||
models.push(model);
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch model ${modelId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
availableModels.value = models;
|
||||
|
||||
// 应用优先级选择算法
|
||||
if (models.length > 0) {
|
||||
selectModelByPriority(models);
|
||||
} else {
|
||||
selectedModelId.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load models from knowledge bases:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 根据优先级选择模型
|
||||
const selectModelByPriority = (models: ModelConfig[]) => {
|
||||
// 1. 优先选择远程模型,按创建时间降序
|
||||
const remoteModels = models
|
||||
.filter(m => m.source === 'remote')
|
||||
.sort((a, b) => {
|
||||
const timeA = new Date(a.created_at || 0).getTime();
|
||||
const timeB = new Date(b.created_at || 0).getTime();
|
||||
return timeB - timeA;
|
||||
});
|
||||
|
||||
if (remoteModels.length > 0) {
|
||||
selectedModelId.value = remoteModels[0].id || '';
|
||||
if (val === '__add_model__') {
|
||||
selectedModelId.value = conversationConfig.value?.summary_model_id || '';
|
||||
handleGoToConversationModels();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 否则选择 Ollama 模型,按参数大小降序
|
||||
const ollamaModels = models
|
||||
.filter(m => m.source === 'local')
|
||||
.sort((a, b) => {
|
||||
const sizeA = parseParameterSize(a.parameters?.parameter_size);
|
||||
const sizeB = parseParameterSize(b.parameters?.parameter_size);
|
||||
return sizeB - sizeA;
|
||||
});
|
||||
|
||||
if (ollamaModels.length > 0) {
|
||||
selectedModelId.value = ollamaModels[0].id || '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 如果都没有,选择第一个
|
||||
selectedModelId.value = models[0]?.id || '';
|
||||
selectedModelId.value = val;
|
||||
showModelSelector.value = false;
|
||||
};
|
||||
|
||||
// 获取 Thinking 模型名称(Agent 模式)
|
||||
const loadThinkingModelName = async () => {
|
||||
const thinkingModelId = settingsStore.agentConfig.thinkingModelId;
|
||||
if (thinkingModelId) {
|
||||
try {
|
||||
const model = await getModel(thinkingModelId);
|
||||
thinkingModelName.value = model.name || thinkingModelId;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch thinking model:', error);
|
||||
thinkingModelName.value = thinkingModelId;
|
||||
}
|
||||
} else {
|
||||
thinkingModelName.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 计算当前选中的模型
|
||||
const selectedModel = computed(() => {
|
||||
return availableModels.value.find(m => m.id === selectedModelId.value);
|
||||
return availableModels.value.find(model => model.id === selectedModelId.value);
|
||||
});
|
||||
|
||||
// 关闭模型选择器(点击外部)
|
||||
const updateModelDropdownPosition = () => {
|
||||
const anchor = modelButtonRef.value;
|
||||
if (!anchor) {
|
||||
modelDropdownStyle.value = {
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
};
|
||||
return;
|
||||
}
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const dropdownWidth = 280;
|
||||
const offsetY = 8;
|
||||
let top = rect.bottom + offsetY;
|
||||
let left = rect.left;
|
||||
if (left + dropdownWidth > window.innerWidth) {
|
||||
left = window.innerWidth - dropdownWidth - 12;
|
||||
}
|
||||
const dropdownHeight = 360;
|
||||
if (top + dropdownHeight > window.innerHeight) {
|
||||
top = rect.top - dropdownHeight - offsetY;
|
||||
if (top < 10) top = 10;
|
||||
}
|
||||
modelDropdownStyle.value = {
|
||||
top: `${Math.round(top)}px`,
|
||||
left: `${Math.round(left)}px`,
|
||||
width: `${dropdownWidth}px`,
|
||||
};
|
||||
};
|
||||
|
||||
const toggleModelSelector = () => {
|
||||
if (selectedKbIds.value.length === 0) return;
|
||||
showModelSelector.value = !showModelSelector.value;
|
||||
if (showModelSelector.value) {
|
||||
if (!availableModels.value.length) {
|
||||
loadChatModels();
|
||||
}
|
||||
nextTick(() => {
|
||||
updateModelDropdownPosition();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const closeModelSelector = () => {
|
||||
showModelSelector.value = false;
|
||||
};
|
||||
@@ -229,6 +242,8 @@ const closeAgentModeSelector = () => {
|
||||
onMounted(() => {
|
||||
loadKnowledgeBases();
|
||||
loadWebSearchConfig();
|
||||
loadConversationConfig();
|
||||
loadChatModels();
|
||||
|
||||
// 如果从知识库内部进入,自动选中该知识库
|
||||
const kbId = (route.params as any)?.kbId as string;
|
||||
@@ -236,16 +251,9 @@ onMounted(() => {
|
||||
settingsStore.addKnowledgeBase(kbId);
|
||||
}
|
||||
|
||||
// 加载模型
|
||||
if (isAgentEnabled.value) {
|
||||
loadThinkingModelName();
|
||||
} else {
|
||||
loadModelsFromKnowledgeBases();
|
||||
}
|
||||
|
||||
// 监听点击外部关闭下拉菜单
|
||||
document.addEventListener('click', closeModelSelector);
|
||||
document.addEventListener('click', closeAgentModeSelector);
|
||||
document.addEventListener('click', closeModelSelector);
|
||||
// 监听窗口大小变化,重新计算位置
|
||||
window.addEventListener('resize', () => {
|
||||
if (showModelSelector.value) {
|
||||
@@ -258,8 +266,8 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeModelSelector);
|
||||
document.removeEventListener('click', closeAgentModeSelector);
|
||||
document.removeEventListener('click', closeModelSelector);
|
||||
});
|
||||
|
||||
// 监听路由变化
|
||||
@@ -275,29 +283,12 @@ watch(() => uiStore.showSettingsModal, (visible, prevVisible) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 监听知识库选择变化,重新加载模型
|
||||
watch(selectedKbIds, () => {
|
||||
if (!isAgentEnabled.value) {
|
||||
loadModelsFromKnowledgeBases();
|
||||
watch(selectedKbIds, (ids) => {
|
||||
if (!ids.length) {
|
||||
closeModelSelector();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 监听 Agent 模式变化
|
||||
watch(isAgentEnabled, (newVal) => {
|
||||
if (newVal) {
|
||||
loadThinkingModelName();
|
||||
} else {
|
||||
loadModelsFromKnowledgeBases();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 Thinking 模型变化
|
||||
watch(() => settingsStore.agentConfig.thinkingModelId, () => {
|
||||
if (isAgentEnabled.value) {
|
||||
loadThinkingModelName();
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['send-msg', 'stop-generation']);
|
||||
|
||||
const createSession = (val: string) => {
|
||||
@@ -317,51 +308,6 @@ const createSession = (val: string) => {
|
||||
}
|
||||
|
||||
// 计算模型下拉菜单位置
|
||||
const updateModelDropdownPosition = () => {
|
||||
const anchor = modelButtonRef.value;
|
||||
|
||||
if (!anchor) {
|
||||
modelDropdownStyle.value = {
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const dropdownWidth = 250;
|
||||
const offsetY = 8;
|
||||
|
||||
// 计算位置:默认在按钮下方
|
||||
let top = rect.bottom + offsetY;
|
||||
let left = rect.left;
|
||||
|
||||
// 检查右侧边界
|
||||
if (left + dropdownWidth > window.innerWidth) {
|
||||
left = window.innerWidth - dropdownWidth - 10;
|
||||
}
|
||||
|
||||
// 检查下方空间
|
||||
const dropdownMaxHeight = 400;
|
||||
if (top + dropdownMaxHeight > window.innerHeight) {
|
||||
// 如果下方空间不足,显示在按钮上方
|
||||
top = rect.top - dropdownMaxHeight - offsetY;
|
||||
if (top < 10) {
|
||||
// 如果上方也不够,显示在屏幕中间
|
||||
top = 10;
|
||||
}
|
||||
}
|
||||
|
||||
modelDropdownStyle.value = {
|
||||
position: 'fixed',
|
||||
top: `${Math.round(top)}px`,
|
||||
left: `${Math.round(left)}px`,
|
||||
width: `${dropdownWidth}px`
|
||||
};
|
||||
};
|
||||
|
||||
// 计算 Agent 模式下拉菜单位置
|
||||
const updateAgentModeDropdownPosition = () => {
|
||||
const anchor = agentModeButtonRef.value;
|
||||
@@ -407,15 +353,6 @@ const updateAgentModeDropdownPosition = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const toggleModelSelector = () => {
|
||||
showModelSelector.value = !showModelSelector.value;
|
||||
if (showModelSelector.value) {
|
||||
nextTick(() => {
|
||||
updateModelDropdownPosition();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const toggleAgentModeSelector = () => {
|
||||
// 如果 Agent 未就绪,显示提示
|
||||
if (!settingsStore.isAgentReady && !isAgentEnabled.value) {
|
||||
@@ -778,73 +715,57 @@ onBeforeRouteUpdate((to, from, next) => {
|
||||
</div>
|
||||
|
||||
<!-- 模型显示 -->
|
||||
<div v-if="selectedKbIds.length > 0" class="model-display">
|
||||
<!-- Agent 模式:只读显示 Thinking 模型 -->
|
||||
<div v-if="isAgentEnabled" class="model-badge">
|
||||
<span class="model-label">{{ $t('input.thinkingLabel') }}</span>
|
||||
<span class="model-name">{{ thinkingModelName || $t('input.notConfigured') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 非 Agent 模式:可选择的模型下拉 -->
|
||||
<div
|
||||
v-else
|
||||
ref="modelButtonRef"
|
||||
class="control-btn model-btn"
|
||||
:class="{ 'active': selectedModel }"
|
||||
<div class="model-display">
|
||||
<div
|
||||
ref="modelButtonRef"
|
||||
class="model-selector-trigger"
|
||||
:class="{ disabled: selectedKbIds.length === 0 }"
|
||||
@click.stop="toggleModelSelector"
|
||||
>
|
||||
<span class="model-btn-text">
|
||||
{{ selectedModel ? selectedModel.name : $t('input.model') }}
|
||||
<span class="model-selector-name">
|
||||
{{ selectedModel?.name || $t('input.notConfigured') }}
|
||||
</span>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="currentColor"
|
||||
class="dropdown-arrow"
|
||||
:class="{ 'rotate': showModelSelector }"
|
||||
>
|
||||
<path d="M2.5 4.5L6 8L9.5 4.5H2.5Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 模型选择下拉菜单 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showModelSelector && !isAgentEnabled" class="model-selector-overlay">
|
||||
<div
|
||||
class="model-selector-dropdown"
|
||||
:style="modelDropdownStyle"
|
||||
@click.stop
|
||||
>
|
||||
<div class="model-selector-content">
|
||||
<div
|
||||
v-for="model in availableModels"
|
||||
:key="model.id"
|
||||
class="model-option"
|
||||
:class="{ 'selected': model.id === selectedModelId }"
|
||||
@click="selectedModelId = model.id || ''; showModelSelector = false"
|
||||
>
|
||||
<div class="model-option-main">
|
||||
<span class="model-option-name">{{ model.name }}</span>
|
||||
<span v-if="model.source === 'remote'" class="model-badge-remote">{{ $t('input.remote') }}</span>
|
||||
<span v-else-if="model.parameters?.parameter_size" class="model-badge-local">
|
||||
{{ model.parameters.parameter_size }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="model.description" class="model-option-desc">
|
||||
{{ model.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="availableModels.length === 0" class="model-option empty">
|
||||
{{ $t('input.noModel') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="showModelSelector" class="model-selector-overlay" @click="closeModelSelector">
|
||||
<div class="model-selector-dropdown" :style="modelDropdownStyle" @click.stop>
|
||||
<div class="model-selector-header">
|
||||
<span>{{ $t('conversationSettings.models.chatGroupLabel') }}</span>
|
||||
<button class="model-selector-add" type="button" @click="handleModelChange('__add_model__')">
|
||||
<span class="add-icon">+</span>
|
||||
<span class="add-text">{{ $t('input.addModel') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="model-selector-content">
|
||||
<div
|
||||
v-for="model in availableModels"
|
||||
:key="model.id"
|
||||
class="model-option"
|
||||
:class="{ selected: model.id === selectedModelId }"
|
||||
@click="handleModelChange(model.id || '')"
|
||||
>
|
||||
<div class="model-option-main">
|
||||
<span class="model-option-name">{{ model.name }}</span>
|
||||
<span v-if="model.source === 'remote'" class="model-badge-remote">{{ $t('input.remote') }}</span>
|
||||
<span v-else-if="model.parameters?.parameter_size" class="model-badge-local">
|
||||
{{ model.parameters.parameter_size }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="model.description" class="model-option-desc">
|
||||
{{ model.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="availableModels.length === 0" class="model-option empty">
|
||||
{{ $t('input.noModel') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 右侧控制按钮组 -->
|
||||
<div class="control-right">
|
||||
<!-- 停止按钮(仅在回复中时显示) -->
|
||||
@@ -1339,90 +1260,130 @@ const getImgSrc = (url: string) => {
|
||||
.model-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
margin-left: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.model-badge {
|
||||
.model-selector-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(7, 192, 95, 0.08);
|
||||
gap: 6px;
|
||||
padding: 2px 8px;
|
||||
min-width: 100px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(7, 192, 95, 0.3);
|
||||
background: rgba(7, 192, 95, 0.1);
|
||||
box-shadow: none;
|
||||
transition: background 0.2s ease, border-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.model-selector-trigger:hover {
|
||||
background: rgba(7, 192, 95, 0.15);
|
||||
border-color: rgba(7, 192, 95, 0.45);
|
||||
}
|
||||
|
||||
.model-selector-trigger.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.model-selector-trigger.disabled:hover {
|
||||
background: rgba(7, 192, 95, 0.1);
|
||||
border-color: rgba(7, 192, 95, 0.3);
|
||||
}
|
||||
|
||||
.model-selector-name {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-label {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
color: #07C05F;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.model-btn {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
min-width: auto;
|
||||
|
||||
&.active {
|
||||
background: rgba(7, 192, 95, 0.1);
|
||||
color: #07C05F;
|
||||
|
||||
&:hover {
|
||||
background: rgba(7, 192, 95, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-btn-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
max-width: 150px;
|
||||
color: #0f172a;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-btn.active .model-btn-text {
|
||||
color: #07C05F;
|
||||
.model-selector-trigger::after {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #07C05F;
|
||||
margin-left: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.model-selector-trigger.disabled::after {
|
||||
border-top-color: rgba(7, 192, 95, 0.4);
|
||||
}
|
||||
|
||||
/* 模型选择下拉菜单 */
|
||||
.model-selector-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.model-selector-dropdown {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
max-width: 350px;
|
||||
max-height: 400px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 25px 45px rgba(15, 23, 42, 0.15);
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
max-height: 360px;
|
||||
}
|
||||
|
||||
.model-selector-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #f8fafc;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.model-selector-content {
|
||||
max-height: 400px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.model-selector-add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(7, 192, 95, 0.4);
|
||||
background: rgba(7, 192, 95, 0.08);
|
||||
color: #057647;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.model-selector-add .add-icon {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.model-selector-add:hover {
|
||||
background: rgba(7, 192, 95, 0.18);
|
||||
border-color: rgba(7, 192, 95, 0.6);
|
||||
}
|
||||
|
||||
.model-option {
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
: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 == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
|
||||
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'setting' ? settingIcon : 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>
|
||||
<!-- 知识库切换下拉箭头 -->
|
||||
@@ -39,19 +39,6 @@
|
||||
{{ kb.name }}
|
||||
</div>
|
||||
</div>
|
||||
<t-dropdown
|
||||
v-if="item.path === 'knowledge-bases' && $route.name === 'knowledgeBaseDetail'"
|
||||
trigger="hover"
|
||||
:options="uploadActionOptions"
|
||||
placement="right"
|
||||
@click="handleUploadAction"
|
||||
class="upload-action-dropdown-trigger"
|
||||
>
|
||||
<div class="upload-file-wrap" variant="outline">
|
||||
<img class="upload-file-icon" :class="[item.path == currentpath ? 'active-upload' : '']"
|
||||
:src="getImgSrc(fileAddIcon)" alt="">
|
||||
</div>
|
||||
</t-dropdown>
|
||||
</div>
|
||||
<div ref="submenuscrollContainer" @scroll="handleScroll" class="submenu" v-if="item.children">
|
||||
<template v-for="(group, groupIndex) in groupedSessions" :key="groupIndex">
|
||||
@@ -66,7 +53,7 @@
|
||||
</span>
|
||||
<t-dropdown
|
||||
:options="[{ content: t('upload.deleteRecord'), value: 'delete' }]"
|
||||
@click="(data) => data.value === 'delete' && delCard(subitem.originalIndex, subitem)"
|
||||
@click="handleSessionMenuClick($event, subitem.originalIndex, subitem)"
|
||||
placement="bottom-right"
|
||||
trigger="click">
|
||||
<div @click.stop class="menu-more-wrap">
|
||||
@@ -85,20 +72,16 @@
|
||||
<div class="menu_bottom">
|
||||
<UserMenu />
|
||||
</div>
|
||||
|
||||
<input type="file" @change="upload" style="display: none" ref="uploadInput"
|
||||
accept=".pdf,.docx,.doc,.txt,.md,.jpg,.jpeg,.png" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, watch, computed, ref, reactive, nextTick, h } 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 { getKnowledgeBaseById, listKnowledgeBases } from '@/api/knowledge-base';
|
||||
import { logout as logoutApi } from '@/api/auth';
|
||||
import { kbFileTypeVerification } from '@/utils/index';
|
||||
import { useMenuStore } from '@/stores/menu';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
@@ -107,7 +90,6 @@ import UserMenu from '@/components/UserMenu.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
let uploadInput = ref();
|
||||
const usemenuStore = useMenuStore();
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUIStore();
|
||||
@@ -270,152 +252,6 @@ const kbMenuItem = computed(() => {
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const uploadActionOptions = computed(() => [
|
||||
{ content: t('upload.uploadDocument'), value: 'upload' },
|
||||
{ content: t('upload.onlineEdit'), value: 'manual' },
|
||||
])
|
||||
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(t('knowledgeBase.notInitialized'));
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取知识库信息失败:', error);
|
||||
MessagePlugin.error(t('knowledgeBase.getInfoFailed'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uploadInput.value.click()
|
||||
}
|
||||
const openManualEditor = async () => {
|
||||
const currentKbId = await getCurrentKbId();
|
||||
if (!currentKbId) {
|
||||
MessagePlugin.warning("请选择知识库");
|
||||
return;
|
||||
}
|
||||
uiStore.openManualEditor({
|
||||
mode: 'create',
|
||||
kbId: currentKbId,
|
||||
status: 'draft',
|
||||
onSuccess: ({ kbId }) => {
|
||||
if (kbId) {
|
||||
window.dispatchEvent(new CustomEvent('knowledgeFileUploaded', {
|
||||
detail: { kbId }
|
||||
}));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
const handleUploadAction = async (data: { value: string }) => {
|
||||
if (data.value === 'upload') {
|
||||
uploadFile();
|
||||
return;
|
||||
}
|
||||
if (data.value === 'manual') {
|
||||
openManualEditor();
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// 如果是 VLM 配置错误,添加跳转链接
|
||||
if (errorMessage.includes('VLM配置') || errorMessage.includes('多模态') || errorMessage.includes('对象存储')) {
|
||||
const messageContent = h('div', { style: 'display: flex; align-items: center; gap: 8px; flex-wrap: wrap;' }, [
|
||||
h('span', { style: 'flex: 1; min-width: 0;' }, errorMessage),
|
||||
h('a', {
|
||||
href: '#',
|
||||
onClick: (e: Event) => {
|
||||
e.preventDefault();
|
||||
// 打开知识库设置弹框,并导航到高级设置部分
|
||||
console.log('Opening KB editor for:', currentKbId, 'section: advanced');
|
||||
uiStore.openEditKB(currentKbId, 'advanced');
|
||||
},
|
||||
style: 'color: #07C05F; text-decoration: none; font-weight: 500; cursor: pointer; white-space: nowrap; flex-shrink: 0;',
|
||||
onMouseenter: (e: Event) => {
|
||||
(e.target as HTMLElement).style.textDecoration = 'underline';
|
||||
},
|
||||
onMouseleave: (e: Event) => {
|
||||
(e.target as HTMLElement).style.textDecoration = 'none';
|
||||
}
|
||||
}, '去设置 →')
|
||||
]);
|
||||
|
||||
MessagePlugin.error({
|
||||
content: messageContent,
|
||||
duration: 5000
|
||||
});
|
||||
} else {
|
||||
MessagePlugin.error(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
uploadInput.value.value = "";
|
||||
}
|
||||
}
|
||||
const mouseenteBotDownr = (val: string) => {
|
||||
activeSubmenu.value = val;
|
||||
}
|
||||
@@ -423,6 +259,12 @@ const mouseleaveBotDown = () => {
|
||||
activeSubmenu.value = '';
|
||||
}
|
||||
|
||||
const handleSessionMenuClick = (data: { value: string }, index: number, item: any) => {
|
||||
if (data?.value === 'delete') {
|
||||
delCard(index, item);
|
||||
}
|
||||
};
|
||||
|
||||
const delCard = (index: number, item: any) => {
|
||||
delSession(item.id).then((res: any) => {
|
||||
if (res && (res as any).success) {
|
||||
@@ -583,7 +425,6 @@ watch([() => route.name, () => route.params], (newvalue, oldvalue) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
let fileAddIcon = ref('file-add-green.svg');
|
||||
let knowledgeIcon = ref('zhishiku-green.svg');
|
||||
let prefixIcon = ref('prefixIcon.svg');
|
||||
let logoutIcon = ref('logout.svg');
|
||||
@@ -595,9 +436,6 @@ let pathPrefix = ref(route.name)
|
||||
const creatChatActiveState = getIconActiveState('creatChat');
|
||||
const settingsActiveState = getIconActiveState('settings');
|
||||
|
||||
// 上传图标:只在知识库相关页面显示绿色
|
||||
fileAddIcon.value = kbActiveState.isKbActive ? 'file-add-green.svg' : 'file-add.svg';
|
||||
|
||||
// 知识库图标:只在知识库页面显示绿色
|
||||
knowledgeIcon.value = kbActiveState.isKbActive ? 'zhishiku-green.svg' : 'zhishiku.svg';
|
||||
|
||||
|
||||
@@ -632,6 +632,7 @@ export default {
|
||||
}
|
||||
},
|
||||
input: {
|
||||
addModel: 'Add Model',
|
||||
placeholder: 'Ask questions based on the knowledge base',
|
||||
agentMode: 'Agent Mode',
|
||||
normalMode: 'Normal Mode',
|
||||
@@ -742,7 +743,7 @@ export default {
|
||||
},
|
||||
document: {
|
||||
title: 'Document Management',
|
||||
subtitle: 'Upload and manage documents, supporting parsing and chunking of multiple file formats',
|
||||
subtitle: 'Click or drag-and-drop to upload documents; multiple formats are parsed automatically with intelligent chunking for a searchable knowledge base',
|
||||
},
|
||||
faq: {
|
||||
title: 'FAQ Configuration',
|
||||
|
||||
@@ -835,6 +835,10 @@ export default {
|
||||
updateSuccess: 'Настройки сохранены',
|
||||
indexModeRequired: 'Выберите режим индексации для FAQ базы знаний'
|
||||
},
|
||||
document: {
|
||||
title: 'Управление документами',
|
||||
subtitle: 'Загружайте файлы кликом или перетаскиванием — поддерживается автоматический разбор разных форматов и умное разбиение на фрагменты для быстрого поиска',
|
||||
},
|
||||
faq: {
|
||||
title: 'Настройки FAQ',
|
||||
subtitle: 'Управление записями FAQ с пакетным импортом, редактированием и тестированием поиска',
|
||||
@@ -1497,6 +1501,7 @@ export default {
|
||||
}
|
||||
},
|
||||
input: {
|
||||
addModel: 'Добавить модель',
|
||||
placeholder: 'Задайте вопрос на основе базы знаний',
|
||||
agentMode: 'Agent режим',
|
||||
normalMode: 'Обычный режим',
|
||||
|
||||
@@ -1061,7 +1061,7 @@ export default {
|
||||
},
|
||||
document: {
|
||||
title: "文档",
|
||||
subtitle: "上传各类文档,系统自动解析内容并智能分块,构建可检索的知识库",
|
||||
subtitle: "支持点击或拖拽上传,多格式文档自动解析并智能分块,快速构建可检索的知识库",
|
||||
},
|
||||
faq: {
|
||||
title: "问答",
|
||||
@@ -1255,6 +1255,7 @@ export default {
|
||||
},
|
||||
},
|
||||
input: {
|
||||
addModel: "添加模型",
|
||||
placeholder: "基于知识库提问",
|
||||
agentMode: "Agent 模式",
|
||||
normalMode: "普通模式",
|
||||
|
||||
@@ -11,20 +11,24 @@ interface Settings {
|
||||
modelConfig: ModelConfig; // 模型配置
|
||||
ollamaConfig: OllamaConfig; // Ollama配置
|
||||
webSearchEnabled: boolean; // 网络搜索是否启用
|
||||
conversationModels: ConversationModels;
|
||||
}
|
||||
|
||||
// Agent 配置接口
|
||||
interface AgentConfig {
|
||||
maxIterations: number;
|
||||
temperature: number;
|
||||
thinkingModelId: string;
|
||||
rerankModelId: string;
|
||||
allowedTools: string[];
|
||||
system_prompt_web_enabled?: string;
|
||||
system_prompt_web_disabled?: string;
|
||||
use_custom_system_prompt?: boolean;
|
||||
}
|
||||
|
||||
interface ConversationModels {
|
||||
summaryModelId: string;
|
||||
rerankModelId: string;
|
||||
}
|
||||
|
||||
// 单个模型项接口
|
||||
interface ModelItem {
|
||||
id: string; // 唯一ID
|
||||
@@ -61,8 +65,6 @@ const defaultSettings: Settings = {
|
||||
agentConfig: {
|
||||
maxIterations: 5,
|
||||
temperature: 0.7,
|
||||
thinkingModelId: "",
|
||||
rerankModelId: "",
|
||||
allowedTools: ["knowledge_search", "multi_kb_search", "list_knowledge_bases"],
|
||||
system_prompt_web_enabled: "",
|
||||
system_prompt_web_disabled: "",
|
||||
@@ -79,7 +81,11 @@ const defaultSettings: Settings = {
|
||||
baseUrl: "http://localhost:11434",
|
||||
enabled: true
|
||||
},
|
||||
webSearchEnabled: false // 默认关闭网络搜索
|
||||
webSearchEnabled: false, // 默认关闭网络搜索
|
||||
conversationModels: {
|
||||
summaryModelId: "",
|
||||
rerankModelId: "",
|
||||
}
|
||||
};
|
||||
|
||||
export const useSettingsStore = defineStore("settings", {
|
||||
@@ -95,13 +101,16 @@ export const useSettingsStore = defineStore("settings", {
|
||||
// Agent 是否就绪(配置完整)
|
||||
isAgentReady: (state) => {
|
||||
const config = state.settings.agentConfig || defaultSettings.agentConfig
|
||||
return config.thinkingModelId !== '' &&
|
||||
config.rerankModelId !== '' &&
|
||||
config.allowedTools.length > 0
|
||||
const models = state.settings.conversationModels || defaultSettings.conversationModels
|
||||
return config.allowedTools.length > 0 &&
|
||||
models.summaryModelId !== '' &&
|
||||
models.rerankModelId !== ''
|
||||
},
|
||||
|
||||
// 获取 Agent 配置
|
||||
agentConfig: (state) => state.settings.agentConfig || defaultSettings.agentConfig,
|
||||
|
||||
conversationModels: (state) => state.settings.conversationModels || defaultSettings.conversationModels,
|
||||
|
||||
// 获取模型配置
|
||||
modelConfig: (state) => state.settings.modelConfig || defaultSettings.modelConfig,
|
||||
@@ -149,6 +158,12 @@ export const useSettingsStore = defineStore("settings", {
|
||||
this.settings.agentConfig = { ...this.settings.agentConfig, ...config };
|
||||
localStorage.setItem("WeKnora_settings", JSON.stringify(this.settings));
|
||||
},
|
||||
|
||||
updateConversationModels(models: Partial<ConversationModels>) {
|
||||
const current = this.settings.conversationModels || defaultSettings.conversationModels;
|
||||
this.settings.conversationModels = { ...current, ...models };
|
||||
localStorage.setItem("WeKnora_settings", JSON.stringify(this.settings));
|
||||
},
|
||||
|
||||
// 更新模型配置
|
||||
updateModelConfig(config: Partial<ModelConfig>) {
|
||||
|
||||
@@ -44,7 +44,6 @@ async function createNewSession(value: string) {
|
||||
enabled: true,
|
||||
max_iterations: settingsStore.agentConfig.maxIterations,
|
||||
temperature: settingsStore.agentConfig.temperature,
|
||||
thinking_model_id: settingsStore.agentConfig.thinkingModelId,
|
||||
knowledge_bases: selectedKbs, // 所有选中的知识库
|
||||
allowed_tools: settingsStore.agentConfig.allowedTools
|
||||
};
|
||||
|
||||
@@ -20,16 +20,21 @@ import {
|
||||
createKnowledgeBaseTag,
|
||||
updateKnowledgeBaseTag,
|
||||
deleteKnowledgeBaseTag,
|
||||
uploadKnowledgeFile,
|
||||
listKnowledgeBases,
|
||||
} from "@/api/knowledge-base/index";
|
||||
import FAQEntryManager from './components/FAQEntryManager.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatStringDate } from '@/utils';
|
||||
import { formatStringDate, kbFileTypeVerification } from '@/utils';
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const kbId = computed(() => (route.params as any).kbId as string || '');
|
||||
const kbInfo = ref<any>(null);
|
||||
const uploadInputRef = ref<HTMLInputElement | null>(null);
|
||||
const uploading = ref(false);
|
||||
const kbLoading = ref(false);
|
||||
const isFAQ = computed(() => (kbInfo.value?.type || '') === 'faq');
|
||||
const knowledgeList = ref<Array<{ id: string; name: string }>>([]);
|
||||
let { cardList, total, moreIndex, details, getKnowled, delKnowledge, openMore, onVisibleChange, getCardDetails, getfDetails } = useKnowledgeBase(kbId.value)
|
||||
let isCardDetails = ref(false);
|
||||
let timeout: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -334,6 +339,18 @@ const loadKnowledgeBaseInfo = async (targetKbId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadKnowledgeList = async () => {
|
||||
try {
|
||||
const res: any = await listKnowledgeBases();
|
||||
knowledgeList.value = (res?.data || []).map((item: any) => ({
|
||||
id: String(item.id),
|
||||
name: item.name,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge list:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由参数变化,重新获取知识库内容
|
||||
watch(() => kbId.value, (newKbId, oldKbId) => {
|
||||
if (newKbId && newKbId !== oldKbId) {
|
||||
@@ -368,6 +385,7 @@ const handleFileUploaded = (event: CustomEvent) => {
|
||||
|
||||
onMounted(() => {
|
||||
loadKnowledgeBaseInfo(kbId.value);
|
||||
loadKnowledgeList();
|
||||
|
||||
// 监听文件上传事件
|
||||
window.addEventListener('knowledgeFileUploaded', handleFileUploaded as EventListener);
|
||||
@@ -451,6 +469,126 @@ const manualEditorSuccess = ({ kbId: savedKbId }: { kbId: string; knowledgeId: s
|
||||
}
|
||||
};
|
||||
|
||||
const documentTitle = computed(() => {
|
||||
if (kbInfo.value?.name) {
|
||||
return `${kbInfo.value.name} · ${t('knowledgeEditor.document.title')}`;
|
||||
}
|
||||
return t('knowledgeEditor.document.title');
|
||||
});
|
||||
|
||||
const ensureDocumentKbReady = () => {
|
||||
if (isFAQ.value) {
|
||||
MessagePlugin.warning('当前知识库类型不支持该操作');
|
||||
return false;
|
||||
}
|
||||
if (!kbId.value) {
|
||||
MessagePlugin.warning(t('knowledgeEditor.messages.missingId'));
|
||||
return false;
|
||||
}
|
||||
if (!kbInfo.value || !kbInfo.value.embedding_model_id || !kbInfo.value.summary_model_id) {
|
||||
MessagePlugin.warning(t('knowledgeBase.notInitialized'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleDocumentUploadClick = () => {
|
||||
if (!ensureDocumentKbReady()) return;
|
||||
uploadInputRef.value?.click();
|
||||
};
|
||||
|
||||
const resetUploadInput = () => {
|
||||
if (uploadInputRef.value) {
|
||||
uploadInputRef.value.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleDocumentUpload = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input?.files?.[0];
|
||||
if (!file) return;
|
||||
if (kbFileTypeVerification(file)) {
|
||||
resetUploadInput();
|
||||
return;
|
||||
}
|
||||
if (!kbId.value) {
|
||||
MessagePlugin.error("缺少知识库ID");
|
||||
resetUploadInput();
|
||||
return;
|
||||
}
|
||||
uploading.value = true;
|
||||
try {
|
||||
const responseData: any = await uploadKnowledgeFile(kbId.value, { file });
|
||||
window.dispatchEvent(new CustomEvent('knowledgeFileUploaded', {
|
||||
detail: { kbId: kbId.value }
|
||||
}));
|
||||
const isSuccess = responseData?.success || responseData?.code === 200 || responseData?.status === 'success' || (!responseData?.error && responseData);
|
||||
if (isSuccess) {
|
||||
MessagePlugin.info("上传成功!");
|
||||
} else {
|
||||
let errorMessage = "上传失败!";
|
||||
if (responseData?.error?.message) {
|
||||
errorMessage = responseData.error.message;
|
||||
} else if (responseData?.message) {
|
||||
errorMessage = responseData.message;
|
||||
}
|
||||
if (responseData?.code === 'duplicate_file' || responseData?.error?.code === 'duplicate_file') {
|
||||
errorMessage = "文件已存在";
|
||||
}
|
||||
MessagePlugin.error(errorMessage);
|
||||
}
|
||||
} catch (error: any) {
|
||||
let errorMessage = error?.error?.message || error?.message || "上传失败!";
|
||||
if (error?.code === 'duplicate_file') {
|
||||
errorMessage = "文件已存在";
|
||||
}
|
||||
MessagePlugin.error(errorMessage);
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
resetUploadInput();
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualCreate = () => {
|
||||
if (!ensureDocumentKbReady()) return;
|
||||
uiStore.openManualEditor({
|
||||
mode: 'create',
|
||||
kbId: kbId.value,
|
||||
status: 'draft',
|
||||
onSuccess: manualEditorSuccess,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenKBSettings = () => {
|
||||
if (!kbId.value) {
|
||||
MessagePlugin.warning(t('knowledgeEditor.messages.missingId'));
|
||||
return;
|
||||
}
|
||||
uiStore.openKBSettings(kbId.value);
|
||||
};
|
||||
|
||||
const handleNavigateToKbList = () => {
|
||||
router.push('/platform/knowledge-bases');
|
||||
};
|
||||
|
||||
const handleNavigateToCurrentKB = () => {
|
||||
if (!kbId.value) return;
|
||||
router.push(`/platform/knowledge-bases/${kbId.value}`);
|
||||
};
|
||||
|
||||
const knowledgeDropdownOptions = computed(() =>
|
||||
knowledgeList.value.map((item) => ({
|
||||
content: item.name,
|
||||
value: item.id,
|
||||
}))
|
||||
);
|
||||
|
||||
const handleKnowledgeDropdownSelect = (data: { value: string }) => {
|
||||
if (!data?.value) return;
|
||||
if (data.value === kbId.value) return;
|
||||
router.push(`/platform/knowledge-bases/${data.value}`);
|
||||
};
|
||||
|
||||
const handleManualEdit = (index: number, item: KnowledgeCard) => {
|
||||
if (isFAQ.value) return;
|
||||
if (cardList.value[index]) {
|
||||
@@ -532,10 +670,86 @@ async function createNewSession(value: string): Promise<void> {
|
||||
<div class="knowledge-layout">
|
||||
<div class="document-header">
|
||||
<div class="document-header-title">
|
||||
<h2>{{ $t('knowledgeEditor.document.title') }}</h2>
|
||||
<div class="document-title-row">
|
||||
<h2 class="document-breadcrumb">
|
||||
<button type="button" class="breadcrumb-link" @click="handleNavigateToKbList">
|
||||
{{ $t('menu.knowledgeBase') }}
|
||||
</button>
|
||||
<t-icon name="chevron-right" class="breadcrumb-separator" />
|
||||
<t-dropdown
|
||||
v-if="knowledgeDropdownOptions.length"
|
||||
:options="knowledgeDropdownOptions"
|
||||
trigger="click"
|
||||
placement="bottom-left"
|
||||
@click="handleKnowledgeDropdownSelect"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="breadcrumb-link dropdown"
|
||||
:disabled="!kbId"
|
||||
@click.stop="handleNavigateToCurrentKB"
|
||||
>
|
||||
<span>{{ kbInfo?.name || '--' }}</span>
|
||||
<t-icon name="chevron-down" />
|
||||
</button>
|
||||
</t-dropdown>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="breadcrumb-link"
|
||||
:disabled="!kbId"
|
||||
@click="handleNavigateToCurrentKB"
|
||||
>
|
||||
{{ kbInfo?.name || '--' }}
|
||||
</button>
|
||||
<t-icon name="chevron-right" class="breadcrumb-separator" />
|
||||
<span class="breadcrumb-current">{{ $t('knowledgeEditor.document.title') }}</span>
|
||||
</h2>
|
||||
<t-tooltip :content="$t('knowledgeBase.settings')" placement="top">
|
||||
<button
|
||||
type="button"
|
||||
class="kb-settings-button"
|
||||
:disabled="!kbId"
|
||||
@click="handleOpenKBSettings"
|
||||
>
|
||||
<t-icon name="setting" size="16px" />
|
||||
</button>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
<p class="document-subtitle">{{ $t('knowledgeEditor.document.subtitle') }}</p>
|
||||
</div>
|
||||
<div class="document-header-actions">
|
||||
<t-button
|
||||
class="document-action-btn primary"
|
||||
size="medium"
|
||||
:loading="uploading"
|
||||
:disabled="uploading"
|
||||
@click="handleDocumentUploadClick"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="upload" />
|
||||
</template>
|
||||
{{ $t('upload.uploadDocument') }}
|
||||
</t-button>
|
||||
<t-button
|
||||
class="document-action-btn secondary"
|
||||
size="medium"
|
||||
@click="handleManualCreate"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="edit" />
|
||||
</template>
|
||||
{{ $t('upload.onlineEdit') }}
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref="uploadInputRef"
|
||||
type="file"
|
||||
class="document-upload-input"
|
||||
accept=".pdf,.docx,.doc,.txt,.md,.jpg,.jpeg,.png"
|
||||
@change="handleDocumentUpload"
|
||||
/>
|
||||
<div class="knowledge-main">
|
||||
<aside class="tag-sidebar">
|
||||
<div class="sidebar-header">
|
||||
@@ -960,28 +1174,32 @@ async function createNewSession(value: string): Promise<void> {
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
overflow: auto;
|
||||
|
||||
.tag-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
color: #4e5969;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
|
||||
.tag-list-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
|
||||
.t-icon {
|
||||
flex-shrink: 0;
|
||||
color: #4e5969;
|
||||
color: #86909c;
|
||||
font-size: 16px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -996,7 +1214,7 @@ async function createNewSession(value: string): Promise<void> {
|
||||
.tag-list-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
margin-left: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -1004,18 +1222,40 @@ async function createNewSession(value: string): Promise<void> {
|
||||
.tag-count {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
font-weight: 500;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
background: #f7f9fc;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f3f5f7;
|
||||
background: #f7f9fc;
|
||||
color: #1d2129;
|
||||
|
||||
.tag-list-left .t-icon {
|
||||
color: #4e5969;
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
background: #e5e9f2;
|
||||
color: #4e5969;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #e6f7ec;
|
||||
color: #00a870;
|
||||
font-weight: 500;
|
||||
|
||||
.tag-list-left .t-icon {
|
||||
color: #00a870;
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
background: #b8f0d3;
|
||||
color: #00a870;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,6 +1358,11 @@ async function createNewSession(value: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
.tag-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tag-more-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@@ -1125,19 +1370,16 @@ async function createNewSession(value: string): Promise<void> {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
color: #6b7280;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
color: #86909c;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
background: #e6f0ff;
|
||||
color: #0052d9;
|
||||
background: #f3f5f7;
|
||||
color: #4e5969;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-empty-state {
|
||||
@@ -1223,9 +1465,11 @@ async function createNewSession(value: string): Promise<void> {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 0 0 16px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #e7ebf0;
|
||||
|
||||
.document-header-title {
|
||||
display: flex;
|
||||
@@ -1233,6 +1477,55 @@ async function createNewSession(value: string): Promise<void> {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.document-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.document-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: #4e5969;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #07c05f;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #c9ced6;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
font-size: 14px;
|
||||
color: #c9ced6;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: #1d2129;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #000000e6;
|
||||
@@ -1250,6 +1543,99 @@ async function createNewSession(value: string): Promise<void> {
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.document-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.document-action-btn {
|
||||
min-width: 148px;
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 0 18px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
:deep(.t-icon) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: linear-gradient(90deg, #07c05f, #05a04f);
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
box-shadow: 0 6px 14px rgba(7, 192, 95, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg, #08d067, #05a04f);
|
||||
box-shadow: 0 8px 16px rgba(7, 192, 95, 0.25);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: #ffffff;
|
||||
border-color: #07c05f;
|
||||
color: #07c05f;
|
||||
|
||||
&:hover {
|
||||
background: #f3fdf7;
|
||||
border-color: #05a04f;
|
||||
color: #059669;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.document-upload-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kb-settings-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #f5f6f8;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #e6f7ec;
|
||||
color: #07c05f;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
:deep(.t-icon) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-filter-bar {
|
||||
|
||||
@@ -4,23 +4,74 @@
|
||||
<!-- Header -->
|
||||
<div class="faq-header">
|
||||
<div class="faq-header-title">
|
||||
<h2>{{ $t('knowledgeEditor.faq.title') }}</h2>
|
||||
<div class="faq-title-row">
|
||||
<h2 class="faq-breadcrumb">
|
||||
<button type="button" class="breadcrumb-link" @click="handleNavigateToKbList">
|
||||
{{ $t('menu.knowledgeBase') }}
|
||||
</button>
|
||||
<t-icon name="chevron-right" class="breadcrumb-separator" />
|
||||
<t-dropdown
|
||||
v-if="knowledgeDropdownOptions.length"
|
||||
:options="knowledgeDropdownOptions"
|
||||
trigger="click"
|
||||
placement="bottom-left"
|
||||
@click="handleKnowledgeDropdownSelect"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="breadcrumb-link dropdown"
|
||||
:disabled="!props.kbId"
|
||||
@click.stop="handleNavigateToCurrentKB"
|
||||
>
|
||||
<span>{{ kbInfo?.name || '--' }}</span>
|
||||
<t-icon name="chevron-down" />
|
||||
</button>
|
||||
</t-dropdown>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="breadcrumb-link"
|
||||
:disabled="!props.kbId"
|
||||
@click="handleNavigateToCurrentKB"
|
||||
>
|
||||
{{ kbInfo?.name || '--' }}
|
||||
</button>
|
||||
<t-icon name="chevron-right" class="breadcrumb-separator" />
|
||||
<span class="breadcrumb-current">{{ $t('knowledgeEditor.faq.title') }}</span>
|
||||
</h2>
|
||||
<t-tooltip :content="$t('knowledgeBase.settings')" placement="top">
|
||||
<button
|
||||
type="button"
|
||||
class="kb-settings-button"
|
||||
:disabled="!props.kbId"
|
||||
@click="handleOpenKBSettings"
|
||||
>
|
||||
<t-icon name="setting" size="16px" />
|
||||
</button>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
<p class="faq-subtitle">{{ $t('knowledgeEditor.faq.subtitle') }}</p>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button class="create-btn ghost" @click="openEditor()">
|
||||
<t-icon name="add" size="16px" class="btn-icon" />
|
||||
<span>{{ $t('knowledgeEditor.faq.editorCreate') }}</span>
|
||||
</button>
|
||||
<div class="faq-header-actions">
|
||||
<t-button
|
||||
class="faq-action-btn primary"
|
||||
size="medium"
|
||||
@click="openEditor()"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
</template>
|
||||
{{ $t('knowledgeEditor.faq.editorCreate') }}
|
||||
</t-button>
|
||||
<t-dropdown
|
||||
:options="toolbarActionOptions"
|
||||
placement="bottom-left"
|
||||
trigger="click"
|
||||
@click="handleToolbarAction"
|
||||
>
|
||||
<div class="toolbar-action-trigger">
|
||||
<t-button class="faq-action-btn secondary icon-only" size="medium">
|
||||
<t-icon name="more" />
|
||||
</div>
|
||||
</t-button>
|
||||
</t-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
@@ -942,6 +993,7 @@ import type { ComponentPublicInstance } from 'vue'
|
||||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||
import type { FormRules, FormInstanceFunctions } from 'tdesign-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
listFAQEntries,
|
||||
upsertFAQEntries,
|
||||
@@ -954,9 +1006,12 @@ import {
|
||||
createKnowledgeBaseTag,
|
||||
updateKnowledgeBaseTag,
|
||||
deleteKnowledgeBaseTag,
|
||||
getKnowledgeBaseById,
|
||||
listKnowledgeBases,
|
||||
} from '@/api/knowledge-base'
|
||||
import * as XLSX from 'xlsx'
|
||||
import FAQTagTooltip from '@/components/FAQTagTooltip.vue'
|
||||
import { useUIStore } from '@/stores/ui'
|
||||
|
||||
interface FAQEntry {
|
||||
id: string
|
||||
@@ -992,6 +1047,8 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const uiStore = useUIStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
@@ -1058,6 +1115,45 @@ const filteredTags = computed(() => {
|
||||
return tagList.value.filter((tag) => (tag.name || '').toLowerCase().includes(query))
|
||||
})
|
||||
|
||||
const kbInfo = ref<any>(null)
|
||||
const knowledgeList = ref<Array<{ id: string; name: string; type?: string }>>([])
|
||||
const knowledgeDropdownOptions = computed(() =>
|
||||
knowledgeList.value
|
||||
.map((item) => ({
|
||||
content: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
)
|
||||
|
||||
const loadKnowledgeInfo = async (kbId: string) => {
|
||||
if (!kbId) {
|
||||
kbInfo.value = null
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res: any = await getKnowledgeBaseById(kbId)
|
||||
kbInfo.value = res?.data || null
|
||||
return kbInfo.value
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge base info:', error)
|
||||
kbInfo.value = null
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const loadKnowledgeList = async () => {
|
||||
try {
|
||||
const res: any = await listKnowledgeBases()
|
||||
knowledgeList.value = (res?.data || []).map((item: any) => ({
|
||||
id: String(item.id),
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge bases:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const editorVisible = ref(false)
|
||||
const editorMode = ref<'create' | 'edit'>('create')
|
||||
const currentEntryId = ref<string | null>(null)
|
||||
@@ -1347,6 +1443,29 @@ const handleEntryTagChange = async (entryId: string, value?: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleNavigateToKbList = () => {
|
||||
router.push('/platform/knowledge-bases')
|
||||
}
|
||||
|
||||
const handleNavigateToCurrentKB = () => {
|
||||
if (!props.kbId) return
|
||||
router.push(`/platform/knowledge-bases/${props.kbId}`)
|
||||
}
|
||||
|
||||
const handleOpenKBSettings = () => {
|
||||
if (!props.kbId) {
|
||||
MessagePlugin.warning(t('knowledgeEditor.messages.missingId'))
|
||||
return
|
||||
}
|
||||
uiStore.openKBSettings(props.kbId)
|
||||
}
|
||||
|
||||
const handleKnowledgeDropdownSelect = (data: { value: string }) => {
|
||||
if (!data?.value || data.value === props.kbId) return
|
||||
const target = knowledgeList.value.find((item) => item.id === data.value)
|
||||
router.push(`/platform/knowledge-bases/${data.value}`)
|
||||
}
|
||||
|
||||
const handleEntryStatusChange = async (entry: FAQEntry, value: boolean) => {
|
||||
if (!props.kbId) {
|
||||
return
|
||||
@@ -1920,13 +2039,24 @@ const downloadExcelExample = () => {
|
||||
|
||||
watch(
|
||||
() => props.kbId,
|
||||
() => {
|
||||
async (newKbId) => {
|
||||
currentPage = 1
|
||||
hasMore.value = true
|
||||
selectedTagId.value = ''
|
||||
cancelCreateTag()
|
||||
cancelEditTag()
|
||||
tagSearchQuery.value = ''
|
||||
|
||||
if (!newKbId) {
|
||||
kbInfo.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const info = await loadKnowledgeInfo(newKbId)
|
||||
if (!info || info.type !== 'faq') {
|
||||
return
|
||||
}
|
||||
|
||||
loadEntries()
|
||||
loadTags()
|
||||
},
|
||||
@@ -2113,6 +2243,7 @@ onMounted(() => {
|
||||
if (props.kbId) {
|
||||
loadEntries()
|
||||
}
|
||||
loadKnowledgeList()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
@@ -2562,7 +2693,11 @@ watch(() => entries.value.map(e => ({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 0 0 16px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #e7ebf0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.faq-header-title {
|
||||
@@ -2571,9 +2706,58 @@ watch(() => entries.value.map(e => ({
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.faq-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.faq-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: #4e5969;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #07c05f;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #c9ced6;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
font-size: 14px;
|
||||
color: #c9ced6;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: #1d2129;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #000000e6;
|
||||
color: #1d2129;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
@@ -2588,6 +2772,14 @@ watch(() => entries.value.map(e => ({
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.faq-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-filter-bar {
|
||||
@@ -2602,68 +2794,84 @@ watch(() => entries.value.map(e => ({
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 20px;
|
||||
height: 36px;
|
||||
border: 1px solid transparent;
|
||||
.faq-action-btn {
|
||||
min-width: 148px;
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: transparent;
|
||||
|
||||
.btn-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.create-btn.ghost {
|
||||
background: transparent;
|
||||
color: #07c05f;
|
||||
border-color: #07c05f;
|
||||
|
||||
&:hover {
|
||||
background: #07c05f1a;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #07c05f33;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-action-trigger {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
gap: 6px;
|
||||
padding: 0 18px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
transition: all 0.2s ease;
|
||||
color: #00000099;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #07c05f;
|
||||
color: #07c05f;
|
||||
}
|
||||
|
||||
:deep(.t-icon) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: linear-gradient(90deg, #07c05f, #05a04f);
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
box-shadow: 0 6px 14px rgba(7, 192, 95, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg, #08d067, #05a04f);
|
||||
box-shadow: 0 8px 16px rgba(7, 192, 95, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: #ffffff;
|
||||
border-color: #07c05f;
|
||||
color: #07c05f;
|
||||
|
||||
&:hover {
|
||||
background: #f3fdf7;
|
||||
border-color: #05a04f;
|
||||
color: #059669;
|
||||
}
|
||||
}
|
||||
|
||||
&.icon-only {
|
||||
min-width: auto;
|
||||
width: 38px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.kb-settings-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #f5f6f8;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #e6f7ec;
|
||||
color: #07c05f;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
:deep(.t-icon) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动容器
|
||||
|
||||
@@ -698,8 +698,6 @@ const conversationSaving = ref(false)
|
||||
// Agent 模式本地状态
|
||||
const localMaxIterations = ref(5)
|
||||
const localTemperature = ref(0.7)
|
||||
const localThinkingModelId = ref('')
|
||||
const localRerankModelId = ref('')
|
||||
const localAllowedTools = ref<string[]>([])
|
||||
|
||||
type SystemPromptTab = 'web-enabled' | 'web-disabled'
|
||||
@@ -776,6 +774,11 @@ const syncConversationLocals = () => {
|
||||
localConversationRerankModelId.value = cfg.rerank_model_id ?? ''
|
||||
localUseCustomSystemPromptNormal.value = cfg.use_custom_system_prompt ?? true
|
||||
localUseCustomContextTemplate.value = cfg.use_custom_context_template ?? true
|
||||
|
||||
settingsStore.updateConversationModels({
|
||||
summaryModelId: localSummaryModelId.value || '',
|
||||
rerankModelId: localConversationRerankModelId.value || '',
|
||||
})
|
||||
}
|
||||
|
||||
const saveConversationConfig = async (partial: Partial<ConversationConfig>, toastMessage?: string) => {
|
||||
@@ -805,10 +808,7 @@ const saveConversationConfig = async (partial: Partial<ConversationConfig>, toas
|
||||
|
||||
// 计算 Agent 是否就绪
|
||||
const isAgentReady = computed(() => {
|
||||
// 必须有思考模型、Rerank 模型 且 至少选择一个工具
|
||||
return localThinkingModelId.value !== '' &&
|
||||
localRerankModelId.value !== '' &&
|
||||
localAllowedTools.value.length > 0
|
||||
return localAllowedTools.value.length > 0
|
||||
})
|
||||
|
||||
const buildAgentConfigPayload = (overrides: Partial<AgentConfig> = {}): AgentConfig => ({
|
||||
@@ -817,8 +817,6 @@ const buildAgentConfigPayload = (overrides: Partial<AgentConfig> = {}): AgentCon
|
||||
reflection_enabled: false,
|
||||
allowed_tools: localAllowedTools.value,
|
||||
temperature: localTemperature.value,
|
||||
thinking_model_id: localThinkingModelId.value,
|
||||
rerank_model_id: localRerankModelId.value,
|
||||
system_prompt_web_enabled: localSystemPromptWebEnabled.value,
|
||||
system_prompt_web_disabled: localSystemPromptWebDisabled.value,
|
||||
use_custom_system_prompt: localUseCustomSystemPrompt.value,
|
||||
@@ -829,12 +827,6 @@ const buildAgentConfigPayload = (overrides: Partial<AgentConfig> = {}): AgentCon
|
||||
const agentStatusMessage = computed(() => {
|
||||
const missing: string[] = []
|
||||
|
||||
if (!localThinkingModelId.value) {
|
||||
missing.push(t('agentSettings.status.missingThinkingModel'))
|
||||
}
|
||||
if (!localRerankModelId.value) {
|
||||
missing.push(t('agentSettings.status.missingRerankModel'))
|
||||
}
|
||||
if (localAllowedTools.value.length === 0) {
|
||||
missing.push(t('agentSettings.status.missingAllowedTools'))
|
||||
}
|
||||
@@ -1003,8 +995,6 @@ onMounted(async () => {
|
||||
localMaxIterations.value = config.max_iterations
|
||||
lastSavedValue = config.max_iterations // 初始化时记录已保存的值
|
||||
localTemperature.value = config.temperature
|
||||
localThinkingModelId.value = config.thinking_model_id
|
||||
localRerankModelId.value = config.rerank_model_id
|
||||
localAllowedTools.value = config.allowed_tools || []
|
||||
const promptWebEnabled = config.system_prompt_web_enabled || ''
|
||||
const promptWebDisabled = config.system_prompt_web_disabled || ''
|
||||
@@ -1022,9 +1012,7 @@ onMounted(async () => {
|
||||
console.log('加载的占位符列表:', availablePlaceholders.value)
|
||||
|
||||
// 统一加载所有模型(只调用一次API)
|
||||
if (config.thinking_model_id || config.rerank_model_id) {
|
||||
await loadAllModels()
|
||||
}
|
||||
|
||||
// 同步到store(只更新本地存储,不触发API保存)
|
||||
// 注意:不自动设置 isAgentEnabled,保持用户之前的选择
|
||||
@@ -1032,8 +1020,6 @@ onMounted(async () => {
|
||||
settingsStore.updateAgentConfig({
|
||||
maxIterations: config.max_iterations,
|
||||
temperature: config.temperature,
|
||||
thinkingModelId: config.thinking_model_id,
|
||||
rerankModelId: config.rerank_model_id,
|
||||
allowedTools: config.allowed_tools || [],
|
||||
system_prompt_web_enabled: promptWebEnabled,
|
||||
system_prompt_web_disabled: promptWebDisabled,
|
||||
@@ -1072,8 +1058,6 @@ onMounted(async () => {
|
||||
// 失败时从store加载
|
||||
localMaxIterations.value = settingsStore.agentConfig.maxIterations
|
||||
localTemperature.value = settingsStore.agentConfig.temperature
|
||||
localThinkingModelId.value = settingsStore.agentConfig.thinkingModelId
|
||||
localRerankModelId.value = settingsStore.agentConfig.rerankModelId
|
||||
} finally {
|
||||
loadingConfig.value = false
|
||||
isInitializing.value = false // 确保初始化完成,即使失败也要允许后续操作
|
||||
@@ -1179,107 +1163,6 @@ const loadRerankModels = async () => {
|
||||
await loadAllModels()
|
||||
}
|
||||
|
||||
// 处理思考模型变化
|
||||
const handleThinkingModelChange = async (value: string) => {
|
||||
// 如果正在初始化,不触发保存
|
||||
if (isInitializing.value) return
|
||||
|
||||
// 如果选择添加新模型,跳转到模型配置页
|
||||
if (value === '__add_model__') {
|
||||
router.push('/settings?section=models')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const config = buildAgentConfigPayload({ thinking_model_id: value })
|
||||
await updateAgentConfig(config)
|
||||
// 更新 store,确保 isAgentReady 能正确计算
|
||||
settingsStore.updateAgentConfig({ thinkingModelId: value })
|
||||
MessagePlugin.success(t('agentSettings.toasts.thinkingModelSaved'))
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
MessagePlugin.error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// 监听模型选择,处理"添加模型"跳转
|
||||
// 处理 Rerank 模型变化
|
||||
const handleRerankModelChange = async (value: string) => {
|
||||
// 如果正在初始化,不触发保存
|
||||
if (isInitializing.value) return
|
||||
|
||||
// 如果选择添加新模型,跳转到模型配置页
|
||||
if (value === '__add_model__') {
|
||||
router.push('/settings?section=models&subsection=rerank')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const config = buildAgentConfigPayload({ rerank_model_id: value })
|
||||
await updateAgentConfig(config)
|
||||
settingsStore.updateAgentConfig({ rerankModelId: value })
|
||||
MessagePlugin.success(t('agentSettings.toasts.rerankModelSaved'))
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
MessagePlugin.error(getErrorMessage(error))
|
||||
// 回滚
|
||||
localRerankModelId.value = settingsStore.agentConfig.rerankModelId
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => localThinkingModelId.value, (newValue) => {
|
||||
if (newValue === '__add_model__') {
|
||||
// 重置选择
|
||||
localThinkingModelId.value = ''
|
||||
|
||||
// 跳转到模型配置页面的对话模型部分
|
||||
router.push('/platform/settings')
|
||||
|
||||
// 发送导航事件,定位到对话模型
|
||||
setTimeout(() => {
|
||||
const event = new CustomEvent('settings-nav', {
|
||||
detail: { section: 'models', subsection: 'chat' }
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
|
||||
// 滚动到对话模型区域
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector('[data-model-type="chat"]')
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}, 200)
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 Rerank 模型选择,处理"添加模型"跳转
|
||||
watch(() => localRerankModelId.value, (newValue) => {
|
||||
if (newValue === '__add_model__') {
|
||||
// 重置选择
|
||||
localRerankModelId.value = ''
|
||||
|
||||
// 跳转到模型配置页面的 Rerank 模型部分
|
||||
router.push('/platform/settings')
|
||||
|
||||
// 发送导航事件,定位到 Rerank 模型
|
||||
setTimeout(() => {
|
||||
const event = new CustomEvent('settings-nav', {
|
||||
detail: { section: 'models', subsection: 'rerank' }
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
|
||||
// 滚动到 Rerank 模型区域
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector('[data-model-type="rerank"]')
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}, 200)
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
// 处理温度参数变化
|
||||
const handleTemperatureChange = async (value: number) => {
|
||||
// 如果正在初始化,不触发保存
|
||||
|
||||
@@ -64,6 +64,8 @@ func NewAgentService(
|
||||
func (s *agentService) CreateAgentEngine(
|
||||
ctx context.Context,
|
||||
config *types.AgentConfig,
|
||||
chatModel chat.Chat,
|
||||
rerankModel rerank.Reranker,
|
||||
eventBus *event.EventBus,
|
||||
contextManager interfaces.ContextManager,
|
||||
sessionID string,
|
||||
@@ -76,20 +78,10 @@ func (s *agentService) CreateAgentEngine(
|
||||
return nil, fmt.Errorf("invalid agent config: %w", err)
|
||||
}
|
||||
|
||||
chatModel, err := s.modelService.GetChatModel(ctx, config.ThinkingModelID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get thinking model: %w", err)
|
||||
}
|
||||
|
||||
if chatModel == nil {
|
||||
return nil, fmt.Errorf("chat model is nil after initialization")
|
||||
}
|
||||
|
||||
rerankModel, err := s.modelService.GetRerankModel(ctx, config.RerankModelID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rerank model: %w", err)
|
||||
}
|
||||
|
||||
if rerankModel == nil {
|
||||
return nil, fmt.Errorf("rerank model is nil after initialization")
|
||||
}
|
||||
@@ -264,10 +256,6 @@ func (s *agentService) ValidateConfig(config *types.AgentConfig) error {
|
||||
return fmt.Errorf("max iterations too high: %d (max %d)", config.MaxIterations, MAX_ITERATIONS)
|
||||
}
|
||||
|
||||
if config.ThinkingModelID == "" {
|
||||
return fmt.Errorf("ThinkingModelID is required but not set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -878,8 +878,6 @@ func (s *sessionService) AgentQA(ctx context.Context, session *types.Session, qu
|
||||
ReflectionEnabled: tenantInfo.AgentConfig.ReflectionEnabled,
|
||||
AllowedTools: tenantInfo.AgentConfig.AllowedTools,
|
||||
Temperature: tenantInfo.AgentConfig.Temperature,
|
||||
ThinkingModelID: tenantInfo.AgentConfig.ThinkingModelID,
|
||||
RerankModelID: tenantInfo.AgentConfig.RerankModelID,
|
||||
KnowledgeBases: session.AgentConfig.KnowledgeBases, // Use session's knowledge bases
|
||||
WebSearchEnabled: session.AgentConfig.WebSearchEnabled, // Web search enabled from session config
|
||||
}
|
||||
@@ -931,19 +929,36 @@ func (s *sessionService) AgentQA(ctx context.Context, session *types.Session, qu
|
||||
logger.Infof(ctx, "Agent configured with %d knowledge base(s): %v", len(agentConfig.KnowledgeBases), agentConfig.KnowledgeBases)
|
||||
}
|
||||
|
||||
// Set ThinkingModelID from session's SummaryModelID if not already set in tenant config
|
||||
if agentConfig.ThinkingModelID == "" && session.SummaryModelID != "" {
|
||||
agentConfig.ThinkingModelID = session.SummaryModelID
|
||||
logger.Infof(ctx, "Using session's SummaryModelID as ThinkingModelID: %s", session.SummaryModelID)
|
||||
summaryModelID := session.SummaryModelID
|
||||
if summaryModelID == "" && tenantInfo.ConversationConfig != nil {
|
||||
summaryModelID = tenantInfo.ConversationConfig.SummaryModelID
|
||||
}
|
||||
if summaryModelID == "" {
|
||||
logger.Warnf(ctx, "No summary model configured for tenant %d or session %s", tenantInfo.ID, session.ID)
|
||||
return errors.New("summary model is not configured in conversation settings")
|
||||
}
|
||||
|
||||
// Get chat model
|
||||
summaryModel, err := s.modelService.GetChatModel(ctx, agentConfig.ThinkingModelID)
|
||||
summaryModel, err := s.modelService.GetChatModel(ctx, summaryModelID)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to get chat model: %v", err)
|
||||
return fmt.Errorf("failed to get chat model: %w", err)
|
||||
}
|
||||
|
||||
rerankModelID := session.RerankModelID
|
||||
if rerankModelID == "" && tenantInfo.ConversationConfig != nil {
|
||||
rerankModelID = tenantInfo.ConversationConfig.RerankModelID
|
||||
}
|
||||
if rerankModelID == "" {
|
||||
logger.Warnf(ctx, "No rerank model configured for tenant %d or session %s", tenantInfo.ID, session.ID)
|
||||
return errors.New("rerank model is not configured in conversation settings")
|
||||
}
|
||||
|
||||
rerankModel, err := s.modelService.GetRerankModel(ctx, rerankModelID)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to get rerank model: %v", err)
|
||||
return fmt.Errorf("failed to get rerank model: %w", err)
|
||||
}
|
||||
|
||||
// Get or create contextManager for this session
|
||||
contextManager := s.getContextManagerForSession(ctx, session, summaryModel)
|
||||
// Get LLM context from context manager
|
||||
@@ -956,7 +971,7 @@ func (s *sessionService) AgentQA(ctx context.Context, session *types.Session, qu
|
||||
|
||||
// Create agent engine with EventBus and ContextManager
|
||||
logger.Info(ctx, "Creating agent engine")
|
||||
engine, err := s.agentService.CreateAgentEngine(ctx, agentConfig, eventBus, contextManager, session.ID, s)
|
||||
engine, err := s.agentService.CreateAgentEngine(ctx, agentConfig, summaryModel, rerankModel, eventBus, contextManager, session.ID, s)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to create agent engine: %v", err)
|
||||
return err
|
||||
|
||||
@@ -226,7 +226,10 @@ func (h *Handler) AgentQA(c *gin.Context) {
|
||||
}
|
||||
summaryModelID := request.SummaryModelID
|
||||
if summaryModelID == "" {
|
||||
summaryModelID = tenantInfo.AgentConfig.ThinkingModelID
|
||||
summaryModelID = session.SummaryModelID
|
||||
}
|
||||
if summaryModelID == "" && tenantInfo.ConversationConfig != nil {
|
||||
summaryModelID = tenantInfo.ConversationConfig.SummaryModelID
|
||||
}
|
||||
if summaryModelID != session.SummaryModelID {
|
||||
configChanged = true
|
||||
|
||||
@@ -238,8 +238,6 @@ type AgentConfigRequest struct {
|
||||
ReflectionEnabled bool `json:"reflection_enabled"`
|
||||
AllowedTools []string `json:"allowed_tools"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
ThinkingModelID string `json:"thinking_model_id"`
|
||||
RerankModelID string `json:"rerank_model_id"`
|
||||
SystemPromptWebEnabled string `json:"system_prompt_web_enabled,omitempty"`
|
||||
SystemPromptWebDisabled string `json:"system_prompt_web_disabled,omitempty"`
|
||||
UseCustomPrompt *bool `json:"use_custom_system_prompt"`
|
||||
@@ -287,8 +285,6 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
|
||||
"reflection_enabled": agent.DefaultAgentReflectionEnabled,
|
||||
"allowed_tools": agenttools.DefaultAllowedTools(),
|
||||
"temperature": agent.DefaultAgentTemperature,
|
||||
"thinking_model_id": "",
|
||||
"rerank_model_id": "",
|
||||
"system_prompt_web_enabled": agent.ProgressiveRAGSystemPromptWithWeb,
|
||||
"system_prompt_web_disabled": agent.ProgressiveRAGSystemPromptWithoutWeb,
|
||||
"use_custom_system_prompt": false,
|
||||
@@ -320,8 +316,6 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
|
||||
"reflection_enabled": tenant.AgentConfig.ReflectionEnabled,
|
||||
"allowed_tools": tenant.AgentConfig.AllowedTools,
|
||||
"temperature": tenant.AgentConfig.Temperature,
|
||||
"thinking_model_id": tenant.AgentConfig.ThinkingModelID,
|
||||
"rerank_model_id": tenant.AgentConfig.RerankModelID,
|
||||
"system_prompt_web_enabled": systemPromptWithWeb,
|
||||
"system_prompt_web_disabled": systemPromptWithoutWeb,
|
||||
"use_custom_system_prompt": useCustomPrompt,
|
||||
@@ -353,8 +347,6 @@ func (h *TenantHandler) updateTenantAgentConfigInternal(c *gin.Context) {
|
||||
c.Error(errors.NewAgentInvalidTemperatureError())
|
||||
return
|
||||
}
|
||||
// thinking_model_id 不再强制要求,允许先启用 Agent 再设置模型
|
||||
// 实际使用时会在 AgentQA 中进行验证
|
||||
if len(req.AllowedTools) == 0 {
|
||||
c.Error(errors.NewAgentMissingAllowedToolsError())
|
||||
return
|
||||
@@ -378,8 +370,6 @@ func (h *TenantHandler) updateTenantAgentConfigInternal(c *gin.Context) {
|
||||
ReflectionEnabled: req.ReflectionEnabled,
|
||||
AllowedTools: req.AllowedTools,
|
||||
Temperature: req.Temperature,
|
||||
ThinkingModelID: req.ThinkingModelID,
|
||||
RerankModelID: req.RerankModelID,
|
||||
SystemPromptWebEnabled: req.SystemPromptWebEnabled,
|
||||
SystemPromptWebDisabled: req.SystemPromptWebDisabled,
|
||||
UseCustomSystemPrompt: useCustomPrompt,
|
||||
|
||||
@@ -15,8 +15,6 @@ type AgentConfig struct {
|
||||
ReflectionEnabled bool `json:"reflection_enabled"` // Whether to enable reflection
|
||||
AllowedTools []string `json:"allowed_tools"` // List of allowed tool names
|
||||
Temperature float64 `json:"temperature"` // LLM temperature for agent
|
||||
ThinkingModelID string `json:"thinking_model_id"` // Model ID for reasoning
|
||||
RerankModelID string `json:"rerank_model_id"` // Model ID for reranking search results
|
||||
KnowledgeBases []string `json:"knowledge_bases"` // Accessible knowledge base IDs
|
||||
SystemPromptWebEnabled string `json:"system_prompt_web_enabled,omitempty"` // Custom prompt when web search is enabled
|
||||
SystemPromptWebDisabled string `json:"system_prompt_web_disabled,omitempty"` // Custom prompt when web search is disabled
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/event"
|
||||
"github.com/Tencent/WeKnora/internal/models/chat"
|
||||
"github.com/Tencent/WeKnora/internal/models/rerank"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
)
|
||||
|
||||
@@ -26,7 +27,7 @@ type AgentEngine interface {
|
||||
// AgentService defines the interface for agent-related operations
|
||||
type AgentService interface {
|
||||
// CreateAgentEngine creates an agent engine with the given configuration, EventBus, and ContextManager
|
||||
CreateAgentEngine(ctx context.Context, config *types.AgentConfig, eventBus *event.EventBus, contextManager ContextManager, sessionID string, sessionService SessionService) (AgentEngine, error)
|
||||
CreateAgentEngine(ctx context.Context, config *types.AgentConfig, chatModel chat.Chat, rerankModel rerank.Reranker, eventBus *event.EventBus, contextManager ContextManager, sessionID string, sessionService SessionService) (AgentEngine, error)
|
||||
|
||||
// ValidateConfig validates an agent configuration
|
||||
ValidateConfig(config *types.AgentConfig) error
|
||||
|
||||
@@ -71,7 +71,6 @@ TEST_DATA='{
|
||||
"enabled": true,
|
||||
"maxIterations": 8,
|
||||
"temperature": 0.8,
|
||||
"thinkingModelId": "",
|
||||
"allowedTools": ["knowledge_search", "multi_kb_search", "list_knowledge_bases"]
|
||||
}
|
||||
}'
|
||||
|
||||
Reference in New Issue
Block a user