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:
wizardchen
2025-11-19 22:11:44 +08:00
parent fe117af66e
commit 601f73ec5f
20 changed files with 1013 additions and 722 deletions
-1
View File
@@ -55,7 +55,6 @@ type AgentConfig struct {
ReflectionEnabled bool `json:"reflection_enabled"` // Whether to enable reflection ReflectionEnabled bool `json:"reflection_enabled"` // Whether to enable reflection
AllowedTools []string `json:"allowed_tools"` // List of allowed tool names AllowedTools []string `json:"allowed_tools"` // List of allowed tool names
Temperature float64 `json:"temperature"` // LLM temperature for agent 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 KnowledgeBases []string `json:"knowledge_bases"` // Accessible knowledge base IDs
} }
+4 -1
View File
@@ -47,10 +47,13 @@ export function createModel(data: ModelConfig): Promise<ModelConfig> {
// 获取模型列表 // 获取模型列表
export function listModels(type?: string): Promise<ModelConfig[]> { export function listModels(type?: string): Promise<ModelConfig[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = type ? `/api/v1/models?type=${type}` : '/api/v1/models'; const url = `/api/v1/models`;
get(url) get(url)
.then((response: any) => { .then((response: any) => {
if (response.success && response.data) { if (response.success && response.data) {
if (type) {
response.data = response.data.filter((item: ModelConfig) => item.type === type);
}
resolve(response.data); resolve(response.data);
} else { } else {
resolve([]); resolve([]);
-2
View File
@@ -29,8 +29,6 @@ export interface AgentConfig {
reflection_enabled: boolean reflection_enabled: boolean
allowed_tools: string[] allowed_tools: string[]
temperature: number temperature: number
thinking_model_id: string
rerank_model_id: string
knowledge_bases?: string[] knowledge_bases?: string[]
system_prompt_web_enabled?: string // Custom system prompt when web search is enabled 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 system_prompt_web_disabled?: string // Custom system prompt when web search is disabled
+255 -294
View File
@@ -5,11 +5,12 @@ import { onBeforeRouteUpdate } from 'vue-router';
import { MessagePlugin } from "tdesign-vue-next"; import { MessagePlugin } from "tdesign-vue-next";
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { listKnowledgeBases, getKnowledgeBaseById } from '@/api/knowledge-base'; import { listKnowledgeBases } from '@/api/knowledge-base';
import { stopSession } from '@/api/chat'; import { stopSession } from '@/api/chat';
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'; 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 { getTenantWebSearchConfig } from '@/api/web-search';
import { getConversationConfig, type ConversationConfig } from '@/api/system';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const route = useRoute(); const route = useRoute();
@@ -52,7 +53,8 @@ const selectedKbs = computed(() => {
// 模型相关状态 // 模型相关状态
const availableModels = ref<ModelConfig[]>([]); const availableModels = ref<ModelConfig[]>([]);
const selectedModelId = ref<string>(''); const selectedModelId = ref<string>('');
const thinkingModelName = ref<string>(''); const conversationConfig = ref<ConversationConfig | null>(null);
const modelsLoading = ref(false);
const showModelSelector = ref(false); const showModelSelector = ref(false);
const modelButtonRef = ref<HTMLElement>(); const modelButtonRef = ref<HTMLElement>();
const modelDropdownStyle = ref<Record<string, string>>({}); const modelDropdownStyle = ref<Record<string, string>>({});
@@ -108,115 +110,126 @@ const loadWebSearchConfig = async () => {
} }
}; };
// 解析模型参数大小 (e.g., "7B", "13B", "70B" -> 7, 13, 70) const loadConversationConfig = async () => {
const parseParameterSize = (paramSize: string | undefined): number => { try {
if (!paramSize) return 0; const response = await getConversationConfig();
const match = paramSize.match(/(\d+\.?\d*)/); conversationConfig.value = response.data;
return match ? parseFloat(match[1]) : 0; 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 loadChatModels = async () => {
const loadModelsFromKnowledgeBases = async () => { if (modelsLoading.value) return;
if (selectedKbIds.value.length === 0) { 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 = []; 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 = ''; selectedModelId.value = '';
return; return;
} }
if (val === '__add_model__') {
try { selectedModelId.value = conversationConfig.value?.summary_model_id || '';
// 获取所有选中知识库的 summary_model_id handleGoToConversationModels();
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 || '';
return; return;
} }
selectedModelId.value = val;
// 2. 否则选择 Ollama 模型,按参数大小降序 showModelSelector.value = false;
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 || '';
}; };
// 获取 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(() => { 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 = () => { const closeModelSelector = () => {
showModelSelector.value = false; showModelSelector.value = false;
}; };
@@ -229,6 +242,8 @@ const closeAgentModeSelector = () => {
onMounted(() => { onMounted(() => {
loadKnowledgeBases(); loadKnowledgeBases();
loadWebSearchConfig(); loadWebSearchConfig();
loadConversationConfig();
loadChatModels();
// 如果从知识库内部进入,自动选中该知识库 // 如果从知识库内部进入,自动选中该知识库
const kbId = (route.params as any)?.kbId as string; const kbId = (route.params as any)?.kbId as string;
@@ -236,16 +251,9 @@ onMounted(() => {
settingsStore.addKnowledgeBase(kbId); settingsStore.addKnowledgeBase(kbId);
} }
// 加载模型
if (isAgentEnabled.value) {
loadThinkingModelName();
} else {
loadModelsFromKnowledgeBases();
}
// 监听点击外部关闭下拉菜单 // 监听点击外部关闭下拉菜单
document.addEventListener('click', closeModelSelector);
document.addEventListener('click', closeAgentModeSelector); document.addEventListener('click', closeAgentModeSelector);
document.addEventListener('click', closeModelSelector);
// 监听窗口大小变化,重新计算位置 // 监听窗口大小变化,重新计算位置
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
if (showModelSelector.value) { if (showModelSelector.value) {
@@ -258,8 +266,8 @@ onMounted(() => {
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', closeModelSelector);
document.removeEventListener('click', closeAgentModeSelector); document.removeEventListener('click', closeAgentModeSelector);
document.removeEventListener('click', closeModelSelector);
}); });
// 监听路由变化 // 监听路由变化
@@ -275,29 +283,12 @@ watch(() => uiStore.showSettingsModal, (visible, prevVisible) => {
} }
}); });
// 监听知识库选择变化,重新加载模型 watch(selectedKbIds, (ids) => {
watch(selectedKbIds, () => { if (!ids.length) {
if (!isAgentEnabled.value) { closeModelSelector();
loadModelsFromKnowledgeBases();
} }
}, { deep: true }); }, { 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 emit = defineEmits(['send-msg', 'stop-generation']);
const createSession = (val: string) => { 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 模式下拉菜单位置 // 计算 Agent 模式下拉菜单位置
const updateAgentModeDropdownPosition = () => { const updateAgentModeDropdownPosition = () => {
const anchor = agentModeButtonRef.value; const anchor = agentModeButtonRef.value;
@@ -407,15 +353,6 @@ const updateAgentModeDropdownPosition = () => {
}; };
}; };
const toggleModelSelector = () => {
showModelSelector.value = !showModelSelector.value;
if (showModelSelector.value) {
nextTick(() => {
updateModelDropdownPosition();
});
}
}
const toggleAgentModeSelector = () => { const toggleAgentModeSelector = () => {
// 如果 Agent 未就绪,显示提示 // 如果 Agent 未就绪,显示提示
if (!settingsStore.isAgentReady && !isAgentEnabled.value) { if (!settingsStore.isAgentReady && !isAgentEnabled.value) {
@@ -778,73 +715,57 @@ onBeforeRouteUpdate((to, from, next) => {
</div> </div>
<!-- 模型显示 --> <!-- 模型显示 -->
<div v-if="selectedKbIds.length > 0" class="model-display"> <div class="model-display">
<!-- Agent 模式:只读显示 Thinking 模型 --> <div
<div v-if="isAgentEnabled" class="model-badge"> ref="modelButtonRef"
<span class="model-label">{{ $t('input.thinkingLabel') }}</span> class="model-selector-trigger"
<span class="model-name">{{ thinkingModelName || $t('input.notConfigured') }}</span> :class="{ disabled: selectedKbIds.length === 0 }"
</div>
<!-- 非 Agent 模式:可选择的模型下拉 -->
<div
v-else
ref="modelButtonRef"
class="control-btn model-btn"
:class="{ 'active': selectedModel }"
@click.stop="toggleModelSelector" @click.stop="toggleModelSelector"
> >
<span class="model-btn-text"> <span class="model-selector-name">
{{ selectedModel ? selectedModel.name : $t('input.model') }} {{ selectedModel?.name || $t('input.notConfigured') }}
</span> </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> </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>
</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"> <div class="control-right">
<!-- 停止按钮(仅在回复中时显示) --> <!-- 停止按钮(仅在回复中时显示) -->
@@ -1339,90 +1260,130 @@ const getImgSrc = (url: string) => {
.model-display { .model-display {
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: 8px; margin-left: 0;
flex-shrink: 0; flex-shrink: 0;
} }
.model-badge { .model-selector-trigger {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 6px;
padding: 4px 10px; padding: 2px 8px;
background: rgba(7, 192, 95, 0.08); min-width: 100px;
height: 22px;
border-radius: 6px; 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; font-size: 12px;
white-space: nowrap;
}
.model-label {
color: #666;
font-weight: 500;
}
.model-name {
color: #07C05F;
font-weight: 600; font-weight: 600;
} color: #0f172a;
.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;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
} }
.model-btn.active .model-btn-text { .model-selector-trigger::after {
color: #07C05F; 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 { .model-selector-overlay {
position: fixed; position: fixed;
top: 0; inset: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9998; z-index: 9998;
background: transparent;
} }
.model-selector-dropdown { .model-selector-dropdown {
position: fixed;
z-index: 9999; z-index: 9999;
background: white; background: #fff;
border-radius: 8px; border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 25px 45px rgba(15, 23, 42, 0.15);
max-width: 350px; border: 1px solid #e2e8f0;
max-height: 400px;
overflow: hidden; 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 { .model-selector-content {
max-height: 400px; max-height: 300px;
overflow-y: auto; 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 { .model-option {
padding: 12px 16px; padding: 12px 16px;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f1f5f9;
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
+10 -172
View File
@@ -12,7 +12,7 @@
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : isMenuItemActive(item.path) ? '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_item-box">
<div class="menu_icon"> <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> </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> <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 }} {{ kb.name }}
</div> </div>
</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>
<div ref="submenuscrollContainer" @scroll="handleScroll" class="submenu" v-if="item.children"> <div ref="submenuscrollContainer" @scroll="handleScroll" class="submenu" v-if="item.children">
<template v-for="(group, groupIndex) in groupedSessions" :key="groupIndex"> <template v-for="(group, groupIndex) in groupedSessions" :key="groupIndex">
@@ -66,7 +53,7 @@
</span> </span>
<t-dropdown <t-dropdown
:options="[{ content: t('upload.deleteRecord'), value: 'delete' }]" :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" placement="bottom-right"
trigger="click"> trigger="click">
<div @click.stop class="menu-more-wrap"> <div @click.stop class="menu-more-wrap">
@@ -85,20 +72,16 @@
<div class="menu_bottom"> <div class="menu_bottom">
<UserMenu /> <UserMenu />
</div> </div>
<input type="file" @change="upload" style="display: none" ref="uploadInput"
accept=".pdf,.docx,.doc,.txt,.md,.jpg,.jpeg,.png" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia'; 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 { useRoute, useRouter } from 'vue-router';
import { getSessionsList, delSession } from "@/api/chat/index"; 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 { logout as logoutApi } from '@/api/auth';
import { kbFileTypeVerification } from '@/utils/index';
import { useMenuStore } from '@/stores/menu'; import { useMenuStore } from '@/stores/menu';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
@@ -107,7 +90,6 @@ import UserMenu from '@/components/UserMenu.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
let uploadInput = ref();
const usemenuStore = useMenuStore(); const usemenuStore = useMenuStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
@@ -270,152 +252,6 @@ const kbMenuItem = computed(() => {
}) })
const loading = ref(false) 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) => { const mouseenteBotDownr = (val: string) => {
activeSubmenu.value = val; activeSubmenu.value = val;
} }
@@ -423,6 +259,12 @@ const mouseleaveBotDown = () => {
activeSubmenu.value = ''; activeSubmenu.value = '';
} }
const handleSessionMenuClick = (data: { value: string }, index: number, item: any) => {
if (data?.value === 'delete') {
delCard(index, item);
}
};
const delCard = (index: number, item: any) => { const delCard = (index: number, item: any) => {
delSession(item.id).then((res: any) => { delSession(item.id).then((res: any) => {
if (res && (res as any).success) { 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 knowledgeIcon = ref('zhishiku-green.svg');
let prefixIcon = ref('prefixIcon.svg'); let prefixIcon = ref('prefixIcon.svg');
let logoutIcon = ref('logout.svg'); let logoutIcon = ref('logout.svg');
@@ -595,9 +436,6 @@ let pathPrefix = ref(route.name)
const creatChatActiveState = getIconActiveState('creatChat'); const creatChatActiveState = getIconActiveState('creatChat');
const settingsActiveState = getIconActiveState('settings'); const settingsActiveState = getIconActiveState('settings');
// 上传图标:只在知识库相关页面显示绿色
fileAddIcon.value = kbActiveState.isKbActive ? 'file-add-green.svg' : 'file-add.svg';
// 知识库图标:只在知识库页面显示绿色 // 知识库图标:只在知识库页面显示绿色
knowledgeIcon.value = kbActiveState.isKbActive ? 'zhishiku-green.svg' : 'zhishiku.svg'; knowledgeIcon.value = kbActiveState.isKbActive ? 'zhishiku-green.svg' : 'zhishiku.svg';
+2 -1
View File
@@ -632,6 +632,7 @@ export default {
} }
}, },
input: { input: {
addModel: 'Add Model',
placeholder: 'Ask questions based on the knowledge base', placeholder: 'Ask questions based on the knowledge base',
agentMode: 'Agent Mode', agentMode: 'Agent Mode',
normalMode: 'Normal Mode', normalMode: 'Normal Mode',
@@ -742,7 +743,7 @@ export default {
}, },
document: { document: {
title: 'Document Management', 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: { faq: {
title: 'FAQ Configuration', title: 'FAQ Configuration',
+5
View File
@@ -835,6 +835,10 @@ export default {
updateSuccess: 'Настройки сохранены', updateSuccess: 'Настройки сохранены',
indexModeRequired: 'Выберите режим индексации для FAQ базы знаний' indexModeRequired: 'Выберите режим индексации для FAQ базы знаний'
}, },
document: {
title: 'Управление документами',
subtitle: 'Загружайте файлы кликом или перетаскиванием — поддерживается автоматический разбор разных форматов и умное разбиение на фрагменты для быстрого поиска',
},
faq: { faq: {
title: 'Настройки FAQ', title: 'Настройки FAQ',
subtitle: 'Управление записями FAQ с пакетным импортом, редактированием и тестированием поиска', subtitle: 'Управление записями FAQ с пакетным импортом, редактированием и тестированием поиска',
@@ -1497,6 +1501,7 @@ export default {
} }
}, },
input: { input: {
addModel: 'Добавить модель',
placeholder: 'Задайте вопрос на основе базы знаний', placeholder: 'Задайте вопрос на основе базы знаний',
agentMode: 'Agent режим', agentMode: 'Agent режим',
normalMode: 'Обычный режим', normalMode: 'Обычный режим',
+2 -1
View File
@@ -1061,7 +1061,7 @@ export default {
}, },
document: { document: {
title: "文档", title: "文档",
subtitle: "上传各类文档,系统自动解析内容并智能分块,构建可检索的知识库", subtitle: "支持点击或拖拽上传,多格式文档自动解析并智能分块,快速构建可检索的知识库",
}, },
faq: { faq: {
title: "问答", title: "问答",
@@ -1255,6 +1255,7 @@ export default {
}, },
}, },
input: { input: {
addModel: "添加模型",
placeholder: "基于知识库提问", placeholder: "基于知识库提问",
agentMode: "Agent 模式", agentMode: "Agent 模式",
normalMode: "普通模式", normalMode: "普通模式",
+23 -8
View File
@@ -11,20 +11,24 @@ interface Settings {
modelConfig: ModelConfig; // 模型配置 modelConfig: ModelConfig; // 模型配置
ollamaConfig: OllamaConfig; // Ollama配置 ollamaConfig: OllamaConfig; // Ollama配置
webSearchEnabled: boolean; // 网络搜索是否启用 webSearchEnabled: boolean; // 网络搜索是否启用
conversationModels: ConversationModels;
} }
// Agent 配置接口 // Agent 配置接口
interface AgentConfig { interface AgentConfig {
maxIterations: number; maxIterations: number;
temperature: number; temperature: number;
thinkingModelId: string;
rerankModelId: string;
allowedTools: string[]; allowedTools: string[];
system_prompt_web_enabled?: string; system_prompt_web_enabled?: string;
system_prompt_web_disabled?: string; system_prompt_web_disabled?: string;
use_custom_system_prompt?: boolean; use_custom_system_prompt?: boolean;
} }
interface ConversationModels {
summaryModelId: string;
rerankModelId: string;
}
// 单个模型项接口 // 单个模型项接口
interface ModelItem { interface ModelItem {
id: string; // 唯一ID id: string; // 唯一ID
@@ -61,8 +65,6 @@ const defaultSettings: Settings = {
agentConfig: { agentConfig: {
maxIterations: 5, maxIterations: 5,
temperature: 0.7, temperature: 0.7,
thinkingModelId: "",
rerankModelId: "",
allowedTools: ["knowledge_search", "multi_kb_search", "list_knowledge_bases"], allowedTools: ["knowledge_search", "multi_kb_search", "list_knowledge_bases"],
system_prompt_web_enabled: "", system_prompt_web_enabled: "",
system_prompt_web_disabled: "", system_prompt_web_disabled: "",
@@ -79,7 +81,11 @@ const defaultSettings: Settings = {
baseUrl: "http://localhost:11434", baseUrl: "http://localhost:11434",
enabled: true enabled: true
}, },
webSearchEnabled: false // 默认关闭网络搜索 webSearchEnabled: false, // 默认关闭网络搜索
conversationModels: {
summaryModelId: "",
rerankModelId: "",
}
}; };
export const useSettingsStore = defineStore("settings", { export const useSettingsStore = defineStore("settings", {
@@ -95,13 +101,16 @@ export const useSettingsStore = defineStore("settings", {
// Agent 是否就绪(配置完整) // Agent 是否就绪(配置完整)
isAgentReady: (state) => { isAgentReady: (state) => {
const config = state.settings.agentConfig || defaultSettings.agentConfig const config = state.settings.agentConfig || defaultSettings.agentConfig
return config.thinkingModelId !== '' && const models = state.settings.conversationModels || defaultSettings.conversationModels
config.rerankModelId !== '' && return config.allowedTools.length > 0 &&
config.allowedTools.length > 0 models.summaryModelId !== '' &&
models.rerankModelId !== ''
}, },
// 获取 Agent 配置 // 获取 Agent 配置
agentConfig: (state) => state.settings.agentConfig || defaultSettings.agentConfig, agentConfig: (state) => state.settings.agentConfig || defaultSettings.agentConfig,
conversationModels: (state) => state.settings.conversationModels || defaultSettings.conversationModels,
// 获取模型配置 // 获取模型配置
modelConfig: (state) => state.settings.modelConfig || defaultSettings.modelConfig, modelConfig: (state) => state.settings.modelConfig || defaultSettings.modelConfig,
@@ -149,6 +158,12 @@ export const useSettingsStore = defineStore("settings", {
this.settings.agentConfig = { ...this.settings.agentConfig, ...config }; this.settings.agentConfig = { ...this.settings.agentConfig, ...config };
localStorage.setItem("WeKnora_settings", JSON.stringify(this.settings)); 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>) { updateModelConfig(config: Partial<ModelConfig>) {
@@ -44,7 +44,6 @@ async function createNewSession(value: string) {
enabled: true, enabled: true,
max_iterations: settingsStore.agentConfig.maxIterations, max_iterations: settingsStore.agentConfig.maxIterations,
temperature: settingsStore.agentConfig.temperature, temperature: settingsStore.agentConfig.temperature,
thinking_model_id: settingsStore.agentConfig.thinkingModelId,
knowledge_bases: selectedKbs, // 所有选中的知识库 knowledge_bases: selectedKbs, // 所有选中的知识库
allowed_tools: settingsStore.agentConfig.allowedTools allowed_tools: settingsStore.agentConfig.allowedTools
}; };
+405 -19
View File
@@ -20,16 +20,21 @@ import {
createKnowledgeBaseTag, createKnowledgeBaseTag,
updateKnowledgeBaseTag, updateKnowledgeBaseTag,
deleteKnowledgeBaseTag, deleteKnowledgeBaseTag,
uploadKnowledgeFile,
listKnowledgeBases,
} from "@/api/knowledge-base/index"; } from "@/api/knowledge-base/index";
import FAQEntryManager from './components/FAQEntryManager.vue'; import FAQEntryManager from './components/FAQEntryManager.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { formatStringDate } from '@/utils'; import { formatStringDate, kbFileTypeVerification } from '@/utils';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const kbId = computed(() => (route.params as any).kbId as string || ''); const kbId = computed(() => (route.params as any).kbId as string || '');
const kbInfo = ref<any>(null); const kbInfo = ref<any>(null);
const uploadInputRef = ref<HTMLInputElement | null>(null);
const uploading = ref(false);
const kbLoading = ref(false); const kbLoading = ref(false);
const isFAQ = computed(() => (kbInfo.value?.type || '') === 'faq'); 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 { cardList, total, moreIndex, details, getKnowled, delKnowledge, openMore, onVisibleChange, getCardDetails, getfDetails } = useKnowledgeBase(kbId.value)
let isCardDetails = ref(false); let isCardDetails = ref(false);
let timeout: ReturnType<typeof setInterval> | null = null; 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) => { watch(() => kbId.value, (newKbId, oldKbId) => {
if (newKbId && newKbId !== oldKbId) { if (newKbId && newKbId !== oldKbId) {
@@ -368,6 +385,7 @@ const handleFileUploaded = (event: CustomEvent) => {
onMounted(() => { onMounted(() => {
loadKnowledgeBaseInfo(kbId.value); loadKnowledgeBaseInfo(kbId.value);
loadKnowledgeList();
// 监听文件上传事件 // 监听文件上传事件
window.addEventListener('knowledgeFileUploaded', handleFileUploaded as EventListener); 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) => { const handleManualEdit = (index: number, item: KnowledgeCard) => {
if (isFAQ.value) return; if (isFAQ.value) return;
if (cardList.value[index]) { if (cardList.value[index]) {
@@ -532,10 +670,86 @@ async function createNewSession(value: string): Promise<void> {
<div class="knowledge-layout"> <div class="knowledge-layout">
<div class="document-header"> <div class="document-header">
<div class="document-header-title"> <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> <p class="document-subtitle">{{ $t('knowledgeEditor.document.subtitle') }}</p>
</div> </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> </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"> <div class="knowledge-main">
<aside class="tag-sidebar"> <aside class="tag-sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
@@ -960,28 +1174,32 @@ async function createNewSession(value: string): Promise<void> {
.tag-list { .tag-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 6px;
overflow: auto; overflow: auto;
.tag-list-item { .tag-list-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px 12px; padding: 8px 10px;
border-radius: 8px; border-radius: 6px;
color: #4e5969; color: #4e5969;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
font-size: 13px;
.tag-list-left { .tag-list-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
min-width: 0; min-width: 0;
flex: 1;
.t-icon { .t-icon {
flex-shrink: 0; 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 { .tag-list-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 6px;
margin-left: 8px; margin-left: 8px;
min-width: 0; min-width: 0;
} }
@@ -1004,18 +1222,40 @@ async function createNewSession(value: string): Promise<void> {
.tag-count { .tag-count {
font-size: 12px; font-size: 12px;
color: #86909c; color: #86909c;
font-weight: 500;
padding: 2px 6px;
border-radius: 10px;
background: #f7f9fc;
transition: all 0.2s ease;
} }
&:hover { &:hover {
background: #f3f5f7; background: #f7f9fc;
color: #1d2129;
.tag-list-left .t-icon {
color: #4e5969;
}
.tag-count {
background: #e5e9f2;
color: #4e5969;
}
} }
&.active { &.active {
background: #e6f7ec; background: #e6f7ec;
color: #00a870; color: #00a870;
font-weight: 500;
.tag-list-left .t-icon {
color: #00a870;
}
.tag-count { .tag-count {
background: #b8f0d3;
color: #00a870; 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 { .tag-more-btn {
width: 24px; width: 24px;
height: 24px; height: 24px;
@@ -1125,19 +1370,16 @@ async function createNewSession(value: string): Promise<void> {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 4px; border-radius: 4px;
color: #6b7280; color: #86909c;
transition: background 0.2s ease, color 0.2s ease; transition: all 0.2s ease;
opacity: 0.6;
&:hover { &:hover {
background: #e6f0ff; background: #f3f5f7;
color: #0052d9; color: #4e5969;
opacity: 1;
} }
} }
.tag-more {
display: flex;
align-items: center;
}
} }
.tag-empty-state { .tag-empty-state {
@@ -1223,9 +1465,11 @@ async function createNewSession(value: string): Promise<void> {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0; flex-wrap: wrap;
margin-bottom: 20px; gap: 16px;
padding: 0 0 16px;
flex-shrink: 0; flex-shrink: 0;
border-bottom: 1px solid #e7ebf0;
.document-header-title { .document-header-title {
display: flex; display: flex;
@@ -1233,6 +1477,55 @@ async function createNewSession(value: string): Promise<void> {
gap: 4px; 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 { h2 {
margin: 0; margin: 0;
color: #000000e6; color: #000000e6;
@@ -1250,6 +1543,99 @@ async function createNewSession(value: string): Promise<void> {
font-weight: 400; font-weight: 400;
line-height: 20px; 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 { .tag-filter-bar {
@@ -4,23 +4,74 @@
<!-- Header --> <!-- Header -->
<div class="faq-header"> <div class="faq-header">
<div class="faq-header-title"> <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> <p class="faq-subtitle">{{ $t('knowledgeEditor.faq.subtitle') }}</p>
</div> </div>
<div class="action-buttons"> <div class="faq-header-actions">
<button class="create-btn ghost" @click="openEditor()"> <t-button
<t-icon name="add" size="16px" class="btn-icon" /> class="faq-action-btn primary"
<span>{{ $t('knowledgeEditor.faq.editorCreate') }}</span> size="medium"
</button> @click="openEditor()"
>
<template #icon>
<t-icon name="add" />
</template>
{{ $t('knowledgeEditor.faq.editorCreate') }}
</t-button>
<t-dropdown <t-dropdown
:options="toolbarActionOptions" :options="toolbarActionOptions"
placement="bottom-left" placement="bottom-left"
trigger="click" trigger="click"
@click="handleToolbarAction" @click="handleToolbarAction"
> >
<div class="toolbar-action-trigger"> <t-button class="faq-action-btn secondary icon-only" size="medium">
<t-icon name="more" /> <t-icon name="more" />
</div> </t-button>
</t-dropdown> </t-dropdown>
</div> </div>
</div> </div>
@@ -942,6 +993,7 @@ import type { ComponentPublicInstance } from 'vue'
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next' import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
import type { FormRules, FormInstanceFunctions } from 'tdesign-vue-next' import type { FormRules, FormInstanceFunctions } from 'tdesign-vue-next'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { import {
listFAQEntries, listFAQEntries,
upsertFAQEntries, upsertFAQEntries,
@@ -954,9 +1006,12 @@ import {
createKnowledgeBaseTag, createKnowledgeBaseTag,
updateKnowledgeBaseTag, updateKnowledgeBaseTag,
deleteKnowledgeBaseTag, deleteKnowledgeBaseTag,
getKnowledgeBaseById,
listKnowledgeBases,
} from '@/api/knowledge-base' } from '@/api/knowledge-base'
import * as XLSX from 'xlsx' import * as XLSX from 'xlsx'
import FAQTagTooltip from '@/components/FAQTagTooltip.vue' import FAQTagTooltip from '@/components/FAQTagTooltip.vue'
import { useUIStore } from '@/stores/ui'
interface FAQEntry { interface FAQEntry {
id: string id: string
@@ -992,6 +1047,8 @@ const props = defineProps<{
}>() }>()
const { t } = useI18n() const { t } = useI18n()
const router = useRouter()
const uiStore = useUIStore()
const loading = ref(false) const loading = ref(false)
const loadingMore = ref(false) const loadingMore = ref(false)
@@ -1058,6 +1115,45 @@ const filteredTags = computed(() => {
return tagList.value.filter((tag) => (tag.name || '').toLowerCase().includes(query)) 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 editorVisible = ref(false)
const editorMode = ref<'create' | 'edit'>('create') const editorMode = ref<'create' | 'edit'>('create')
const currentEntryId = ref<string | null>(null) 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) => { const handleEntryStatusChange = async (entry: FAQEntry, value: boolean) => {
if (!props.kbId) { if (!props.kbId) {
return return
@@ -1920,13 +2039,24 @@ const downloadExcelExample = () => {
watch( watch(
() => props.kbId, () => props.kbId,
() => { async (newKbId) => {
currentPage = 1 currentPage = 1
hasMore.value = true hasMore.value = true
selectedTagId.value = '' selectedTagId.value = ''
cancelCreateTag() cancelCreateTag()
cancelEditTag() cancelEditTag()
tagSearchQuery.value = '' tagSearchQuery.value = ''
if (!newKbId) {
kbInfo.value = null
return
}
const info = await loadKnowledgeInfo(newKbId)
if (!info || info.type !== 'faq') {
return
}
loadEntries() loadEntries()
loadTags() loadTags()
}, },
@@ -2113,6 +2243,7 @@ onMounted(() => {
if (props.kbId) { if (props.kbId) {
loadEntries() loadEntries()
} }
loadKnowledgeList()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
}) })
@@ -2562,7 +2693,11 @@ watch(() => entries.value.map(e => ({
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
padding: 0 0 16px;
margin-bottom: 20px; margin-bottom: 20px;
border-bottom: 1px solid #e7ebf0;
flex-shrink: 0; flex-shrink: 0;
.faq-header-title { .faq-header-title {
@@ -2571,9 +2706,58 @@ watch(() => entries.value.map(e => ({
gap: 4px; 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 { h2 {
margin: 0; margin: 0;
color: #000000e6; color: #1d2129;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
@@ -2588,6 +2772,14 @@ watch(() => entries.value.map(e => ({
font-weight: 400; font-weight: 400;
line-height: 20px; line-height: 20px;
} }
.faq-header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
flex-wrap: wrap;
}
} }
.tag-filter-bar { .tag-filter-bar {
@@ -2602,68 +2794,84 @@ watch(() => entries.value.map(e => ({
} }
} }
.action-buttons { .faq-action-btn {
display: flex; min-width: 148px;
gap: 12px; height: 38px;
align-items: center;
}
.create-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 20px;
height: 36px;
border: 1px solid transparent;
border-radius: 8px; border-radius: 8px;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; display: inline-flex;
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;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 32px; gap: 6px;
height: 32px; padding: 0 18px;
border: 1px solid #d9d9d9; border-width: 1px;
border-radius: 8px; border-style: solid;
background: #ffffff;
cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
color: #00000099;
&:hover {
background-color: #f5f5f5;
border-color: #07c05f;
color: #07c05f;
}
:deep(.t-icon) { :deep(.t-icon) {
font-size: 16px; 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;
}
} }
// 滚动容器 // 滚动容器
+6 -123
View File
@@ -698,8 +698,6 @@ const conversationSaving = ref(false)
// Agent 模式本地状态 // Agent 模式本地状态
const localMaxIterations = ref(5) const localMaxIterations = ref(5)
const localTemperature = ref(0.7) const localTemperature = ref(0.7)
const localThinkingModelId = ref('')
const localRerankModelId = ref('')
const localAllowedTools = ref<string[]>([]) const localAllowedTools = ref<string[]>([])
type SystemPromptTab = 'web-enabled' | 'web-disabled' type SystemPromptTab = 'web-enabled' | 'web-disabled'
@@ -776,6 +774,11 @@ const syncConversationLocals = () => {
localConversationRerankModelId.value = cfg.rerank_model_id ?? '' localConversationRerankModelId.value = cfg.rerank_model_id ?? ''
localUseCustomSystemPromptNormal.value = cfg.use_custom_system_prompt ?? true localUseCustomSystemPromptNormal.value = cfg.use_custom_system_prompt ?? true
localUseCustomContextTemplate.value = cfg.use_custom_context_template ?? 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) => { const saveConversationConfig = async (partial: Partial<ConversationConfig>, toastMessage?: string) => {
@@ -805,10 +808,7 @@ const saveConversationConfig = async (partial: Partial<ConversationConfig>, toas
// 计算 Agent 是否就绪 // 计算 Agent 是否就绪
const isAgentReady = computed(() => { const isAgentReady = computed(() => {
// 必须有思考模型、Rerank 模型 且 至少选择一个工具 return localAllowedTools.value.length > 0
return localThinkingModelId.value !== '' &&
localRerankModelId.value !== '' &&
localAllowedTools.value.length > 0
}) })
const buildAgentConfigPayload = (overrides: Partial<AgentConfig> = {}): AgentConfig => ({ const buildAgentConfigPayload = (overrides: Partial<AgentConfig> = {}): AgentConfig => ({
@@ -817,8 +817,6 @@ const buildAgentConfigPayload = (overrides: Partial<AgentConfig> = {}): AgentCon
reflection_enabled: false, reflection_enabled: false,
allowed_tools: localAllowedTools.value, allowed_tools: localAllowedTools.value,
temperature: localTemperature.value, temperature: localTemperature.value,
thinking_model_id: localThinkingModelId.value,
rerank_model_id: localRerankModelId.value,
system_prompt_web_enabled: localSystemPromptWebEnabled.value, system_prompt_web_enabled: localSystemPromptWebEnabled.value,
system_prompt_web_disabled: localSystemPromptWebDisabled.value, system_prompt_web_disabled: localSystemPromptWebDisabled.value,
use_custom_system_prompt: localUseCustomSystemPrompt.value, use_custom_system_prompt: localUseCustomSystemPrompt.value,
@@ -829,12 +827,6 @@ const buildAgentConfigPayload = (overrides: Partial<AgentConfig> = {}): AgentCon
const agentStatusMessage = computed(() => { const agentStatusMessage = computed(() => {
const missing: string[] = [] 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) { if (localAllowedTools.value.length === 0) {
missing.push(t('agentSettings.status.missingAllowedTools')) missing.push(t('agentSettings.status.missingAllowedTools'))
} }
@@ -1003,8 +995,6 @@ onMounted(async () => {
localMaxIterations.value = config.max_iterations localMaxIterations.value = config.max_iterations
lastSavedValue = config.max_iterations // 初始化时记录已保存的值 lastSavedValue = config.max_iterations // 初始化时记录已保存的值
localTemperature.value = config.temperature localTemperature.value = config.temperature
localThinkingModelId.value = config.thinking_model_id
localRerankModelId.value = config.rerank_model_id
localAllowedTools.value = config.allowed_tools || [] localAllowedTools.value = config.allowed_tools || []
const promptWebEnabled = config.system_prompt_web_enabled || '' const promptWebEnabled = config.system_prompt_web_enabled || ''
const promptWebDisabled = config.system_prompt_web_disabled || '' const promptWebDisabled = config.system_prompt_web_disabled || ''
@@ -1022,9 +1012,7 @@ onMounted(async () => {
console.log('加载的占位符列表:', availablePlaceholders.value) console.log('加载的占位符列表:', availablePlaceholders.value)
// 统一加载所有模型(只调用一次API) // 统一加载所有模型(只调用一次API)
if (config.thinking_model_id || config.rerank_model_id) {
await loadAllModels() await loadAllModels()
}
// 同步到store(只更新本地存储,不触发API保存) // 同步到store(只更新本地存储,不触发API保存)
// 注意:不自动设置 isAgentEnabled,保持用户之前的选择 // 注意:不自动设置 isAgentEnabled,保持用户之前的选择
@@ -1032,8 +1020,6 @@ onMounted(async () => {
settingsStore.updateAgentConfig({ settingsStore.updateAgentConfig({
maxIterations: config.max_iterations, maxIterations: config.max_iterations,
temperature: config.temperature, temperature: config.temperature,
thinkingModelId: config.thinking_model_id,
rerankModelId: config.rerank_model_id,
allowedTools: config.allowed_tools || [], allowedTools: config.allowed_tools || [],
system_prompt_web_enabled: promptWebEnabled, system_prompt_web_enabled: promptWebEnabled,
system_prompt_web_disabled: promptWebDisabled, system_prompt_web_disabled: promptWebDisabled,
@@ -1072,8 +1058,6 @@ onMounted(async () => {
// 失败时从store加载 // 失败时从store加载
localMaxIterations.value = settingsStore.agentConfig.maxIterations localMaxIterations.value = settingsStore.agentConfig.maxIterations
localTemperature.value = settingsStore.agentConfig.temperature localTemperature.value = settingsStore.agentConfig.temperature
localThinkingModelId.value = settingsStore.agentConfig.thinkingModelId
localRerankModelId.value = settingsStore.agentConfig.rerankModelId
} finally { } finally {
loadingConfig.value = false loadingConfig.value = false
isInitializing.value = false // 确保初始化完成,即使失败也要允许后续操作 isInitializing.value = false // 确保初始化完成,即使失败也要允许后续操作
@@ -1179,107 +1163,6 @@ const loadRerankModels = async () => {
await loadAllModels() 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) => { const handleTemperatureChange = async (value: number) => {
// 如果正在初始化,不触发保存 // 如果正在初始化,不触发保存
+2 -14
View File
@@ -64,6 +64,8 @@ func NewAgentService(
func (s *agentService) CreateAgentEngine( func (s *agentService) CreateAgentEngine(
ctx context.Context, ctx context.Context,
config *types.AgentConfig, config *types.AgentConfig,
chatModel chat.Chat,
rerankModel rerank.Reranker,
eventBus *event.EventBus, eventBus *event.EventBus,
contextManager interfaces.ContextManager, contextManager interfaces.ContextManager,
sessionID string, sessionID string,
@@ -76,20 +78,10 @@ func (s *agentService) CreateAgentEngine(
return nil, fmt.Errorf("invalid agent config: %w", err) 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 { if chatModel == nil {
return nil, fmt.Errorf("chat model is nil after initialization") 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 { if rerankModel == nil {
return nil, fmt.Errorf("rerank model is nil after initialization") 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) 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 return nil
} }
+24 -9
View File
@@ -878,8 +878,6 @@ func (s *sessionService) AgentQA(ctx context.Context, session *types.Session, qu
ReflectionEnabled: tenantInfo.AgentConfig.ReflectionEnabled, ReflectionEnabled: tenantInfo.AgentConfig.ReflectionEnabled,
AllowedTools: tenantInfo.AgentConfig.AllowedTools, AllowedTools: tenantInfo.AgentConfig.AllowedTools,
Temperature: tenantInfo.AgentConfig.Temperature, Temperature: tenantInfo.AgentConfig.Temperature,
ThinkingModelID: tenantInfo.AgentConfig.ThinkingModelID,
RerankModelID: tenantInfo.AgentConfig.RerankModelID,
KnowledgeBases: session.AgentConfig.KnowledgeBases, // Use session's knowledge bases KnowledgeBases: session.AgentConfig.KnowledgeBases, // Use session's knowledge bases
WebSearchEnabled: session.AgentConfig.WebSearchEnabled, // Web search enabled from session config 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) 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 summaryModelID := session.SummaryModelID
if agentConfig.ThinkingModelID == "" && session.SummaryModelID != "" { if summaryModelID == "" && tenantInfo.ConversationConfig != nil {
agentConfig.ThinkingModelID = session.SummaryModelID summaryModelID = tenantInfo.ConversationConfig.SummaryModelID
logger.Infof(ctx, "Using session's SummaryModelID as ThinkingModelID: %s", session.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, summaryModelID)
summaryModel, err := s.modelService.GetChatModel(ctx, agentConfig.ThinkingModelID)
if err != nil { if err != nil {
logger.Warnf(ctx, "Failed to get chat model: %v", err) logger.Warnf(ctx, "Failed to get chat model: %v", err)
return fmt.Errorf("failed to get chat model: %w", 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 // Get or create contextManager for this session
contextManager := s.getContextManagerForSession(ctx, session, summaryModel) contextManager := s.getContextManagerForSession(ctx, session, summaryModel)
// Get LLM context from context manager // 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 // Create agent engine with EventBus and ContextManager
logger.Info(ctx, "Creating agent engine") 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 { if err != nil {
logger.Errorf(ctx, "Failed to create agent engine: %v", err) logger.Errorf(ctx, "Failed to create agent engine: %v", err)
return err return err
+4 -1
View File
@@ -226,7 +226,10 @@ func (h *Handler) AgentQA(c *gin.Context) {
} }
summaryModelID := request.SummaryModelID summaryModelID := request.SummaryModelID
if summaryModelID == "" { if summaryModelID == "" {
summaryModelID = tenantInfo.AgentConfig.ThinkingModelID summaryModelID = session.SummaryModelID
}
if summaryModelID == "" && tenantInfo.ConversationConfig != nil {
summaryModelID = tenantInfo.ConversationConfig.SummaryModelID
} }
if summaryModelID != session.SummaryModelID { if summaryModelID != session.SummaryModelID {
configChanged = true configChanged = true
-10
View File
@@ -238,8 +238,6 @@ type AgentConfigRequest struct {
ReflectionEnabled bool `json:"reflection_enabled"` ReflectionEnabled bool `json:"reflection_enabled"`
AllowedTools []string `json:"allowed_tools"` AllowedTools []string `json:"allowed_tools"`
Temperature float64 `json:"temperature"` Temperature float64 `json:"temperature"`
ThinkingModelID string `json:"thinking_model_id"`
RerankModelID string `json:"rerank_model_id"`
SystemPromptWebEnabled string `json:"system_prompt_web_enabled,omitempty"` SystemPromptWebEnabled string `json:"system_prompt_web_enabled,omitempty"`
SystemPromptWebDisabled string `json:"system_prompt_web_disabled,omitempty"` SystemPromptWebDisabled string `json:"system_prompt_web_disabled,omitempty"`
UseCustomPrompt *bool `json:"use_custom_system_prompt"` UseCustomPrompt *bool `json:"use_custom_system_prompt"`
@@ -287,8 +285,6 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
"reflection_enabled": agent.DefaultAgentReflectionEnabled, "reflection_enabled": agent.DefaultAgentReflectionEnabled,
"allowed_tools": agenttools.DefaultAllowedTools(), "allowed_tools": agenttools.DefaultAllowedTools(),
"temperature": agent.DefaultAgentTemperature, "temperature": agent.DefaultAgentTemperature,
"thinking_model_id": "",
"rerank_model_id": "",
"system_prompt_web_enabled": agent.ProgressiveRAGSystemPromptWithWeb, "system_prompt_web_enabled": agent.ProgressiveRAGSystemPromptWithWeb,
"system_prompt_web_disabled": agent.ProgressiveRAGSystemPromptWithoutWeb, "system_prompt_web_disabled": agent.ProgressiveRAGSystemPromptWithoutWeb,
"use_custom_system_prompt": false, "use_custom_system_prompt": false,
@@ -320,8 +316,6 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
"reflection_enabled": tenant.AgentConfig.ReflectionEnabled, "reflection_enabled": tenant.AgentConfig.ReflectionEnabled,
"allowed_tools": tenant.AgentConfig.AllowedTools, "allowed_tools": tenant.AgentConfig.AllowedTools,
"temperature": tenant.AgentConfig.Temperature, "temperature": tenant.AgentConfig.Temperature,
"thinking_model_id": tenant.AgentConfig.ThinkingModelID,
"rerank_model_id": tenant.AgentConfig.RerankModelID,
"system_prompt_web_enabled": systemPromptWithWeb, "system_prompt_web_enabled": systemPromptWithWeb,
"system_prompt_web_disabled": systemPromptWithoutWeb, "system_prompt_web_disabled": systemPromptWithoutWeb,
"use_custom_system_prompt": useCustomPrompt, "use_custom_system_prompt": useCustomPrompt,
@@ -353,8 +347,6 @@ func (h *TenantHandler) updateTenantAgentConfigInternal(c *gin.Context) {
c.Error(errors.NewAgentInvalidTemperatureError()) c.Error(errors.NewAgentInvalidTemperatureError())
return return
} }
// thinking_model_id 不再强制要求,允许先启用 Agent 再设置模型
// 实际使用时会在 AgentQA 中进行验证
if len(req.AllowedTools) == 0 { if len(req.AllowedTools) == 0 {
c.Error(errors.NewAgentMissingAllowedToolsError()) c.Error(errors.NewAgentMissingAllowedToolsError())
return return
@@ -378,8 +370,6 @@ func (h *TenantHandler) updateTenantAgentConfigInternal(c *gin.Context) {
ReflectionEnabled: req.ReflectionEnabled, ReflectionEnabled: req.ReflectionEnabled,
AllowedTools: req.AllowedTools, AllowedTools: req.AllowedTools,
Temperature: req.Temperature, Temperature: req.Temperature,
ThinkingModelID: req.ThinkingModelID,
RerankModelID: req.RerankModelID,
SystemPromptWebEnabled: req.SystemPromptWebEnabled, SystemPromptWebEnabled: req.SystemPromptWebEnabled,
SystemPromptWebDisabled: req.SystemPromptWebDisabled, SystemPromptWebDisabled: req.SystemPromptWebDisabled,
UseCustomSystemPrompt: useCustomPrompt, UseCustomSystemPrompt: useCustomPrompt,
-2
View File
@@ -15,8 +15,6 @@ type AgentConfig struct {
ReflectionEnabled bool `json:"reflection_enabled"` // Whether to enable reflection ReflectionEnabled bool `json:"reflection_enabled"` // Whether to enable reflection
AllowedTools []string `json:"allowed_tools"` // List of allowed tool names AllowedTools []string `json:"allowed_tools"` // List of allowed tool names
Temperature float64 `json:"temperature"` // LLM temperature for agent 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 KnowledgeBases []string `json:"knowledge_bases"` // Accessible knowledge base IDs
SystemPromptWebEnabled string `json:"system_prompt_web_enabled,omitempty"` // Custom prompt when web search is enabled 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 SystemPromptWebDisabled string `json:"system_prompt_web_disabled,omitempty"` // Custom prompt when web search is disabled
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"github.com/Tencent/WeKnora/internal/event" "github.com/Tencent/WeKnora/internal/event"
"github.com/Tencent/WeKnora/internal/models/chat" "github.com/Tencent/WeKnora/internal/models/chat"
"github.com/Tencent/WeKnora/internal/models/rerank"
"github.com/Tencent/WeKnora/internal/types" "github.com/Tencent/WeKnora/internal/types"
) )
@@ -26,7 +27,7 @@ type AgentEngine interface {
// AgentService defines the interface for agent-related operations // AgentService defines the interface for agent-related operations
type AgentService interface { type AgentService interface {
// CreateAgentEngine creates an agent engine with the given configuration, EventBus, and ContextManager // 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 validates an agent configuration
ValidateConfig(config *types.AgentConfig) error ValidateConfig(config *types.AgentConfig) error
-1
View File
@@ -71,7 +71,6 @@ TEST_DATA='{
"enabled": true, "enabled": true,
"maxIterations": 8, "maxIterations": 8,
"temperature": 0.8, "temperature": 0.8,
"thinkingModelId": "",
"allowedTools": ["knowledge_search", "multi_kb_search", "list_knowledge_bases"] "allowedTools": ["knowledge_search", "multi_kb_search", "list_knowledge_bases"]
} }
}' }'