refactor(web): 重构代理配置侧边栏和输入组件

- 更新了 AgentConfigSidebar.vue 以增强配置管理界面,包括添加用于创建新配置的模态框和改进只读状态的处理。
- 重构了 AgentInputArea.vue 以简化操作按钮并提高响应性。
- 简化了 AgentView.vue 中选择代理配置的下拉菜单,将其替换为切换侧边栏的按钮。
- 增强了 agent.js 中数值的处理,确保正确的类型转换。
- 清理了多个组件中的 CSS 样式,以获得更好的一致性和响应性。
- 删除了不必要的代码并提高了各种组件的可读性。
This commit is contained in:
Wenjie Zhang 2026-03-20 03:21:52 +08:00
parent 69f23ae66e
commit 32968259e1
10 changed files with 441 additions and 433 deletions

View File

@ -14,7 +14,7 @@ if not _LITE_MODE:
# 注册知识库类型 # 注册知识库类型
KnowledgeBaseFactory.register("milvus", MilvusKB, {"description": "基于 Milvus 的生产级向量知识库,适合高性能部署"}) KnowledgeBaseFactory.register("milvus", MilvusKB, {"description": "基于 Milvus 的生产级向量知识库,适合高性能部署"})
KnowledgeBaseFactory.register("lightrag", LightRagKB, {"description": "基于图检索的知识库,支持实体关系构建和复杂查询"}) KnowledgeBaseFactory.register("lightrag", LightRagKB, {"description": "基于图的知识库,支持实体关系构建和复杂查询"})
KnowledgeBaseFactory.register("dify", DifyKB, {"description": "连接 Dify Dataset 的只读检索知识库"}) KnowledgeBaseFactory.register("dify", DifyKB, {"description": "连接 Dify Dataset 的只读检索知识库"})

View File

@ -30,10 +30,9 @@ class SubAgentRepository:
async def exists_name(self, name: str) -> bool: async def exists_name(self, name: str) -> bool:
"""检查名称是否存在(仅查询计数,不获取完整数据)""" """检查名称是否存在(仅查询计数,不获取完整数据)"""
from sqlalchemy import select, func from sqlalchemy import func, select
result = await self.db.execute(
select(func.count()).select_from(SubAgent).where(SubAgent.name == name) result = await self.db.execute(select(func.count()).select_from(SubAgent).where(SubAgent.name == name))
)
return result.scalar() > 0 return result.scalar() > 0
async def create( async def create(

View File

@ -100,6 +100,7 @@ def clear_specs_cache() -> None:
global _subagent_specs_cache global _subagent_specs_cache
_subagent_specs_cache = None _subagent_specs_cache = None
async def get_subagents_from_names(selected_names: Any, *, db: AsyncSession | None = None) -> list[dict[str, Any]]: async def get_subagents_from_names(selected_names: Any, *, db: AsyncSession | None = None) -> list[dict[str, Any]]:
"""根据名称获取 subagent specs含工具解析""" """根据名称获取 subagent specs含工具解析"""
specs = await get_subagent_specs(db) specs = await get_subagent_specs(db)
@ -125,13 +126,12 @@ async def get_subagents_from_names(selected_names: Any, *, db: AsyncSession | No
for spec in matched: for spec in matched:
resolved_spec = dict(spec) resolved_spec = dict(spec)
tool_names = spec.get("tools", []) tool_names = spec.get("tools", [])
resolved_spec["tools"] = [ resolved_spec["tools"] = [all_tool_names[name] for name in tool_names if name in all_tool_names]
all_tool_names[name] for name in tool_names if name in all_tool_names
]
resolved_specs.append(resolved_spec) resolved_specs.append(resolved_spec)
return resolved_specs return resolved_specs
async def get_all_subagents(db: AsyncSession | None = None) -> list[dict[str, Any]]: async def get_all_subagents(db: AsyncSession | None = None) -> list[dict[str, Any]]:
"""获取所有 SubAgent含禁用的""" """获取所有 SubAgent含禁用的"""
async with _get_session(db) as session: async with _get_session(db) as session:

View File

@ -111,7 +111,6 @@
} }
&.active { &.active {
.item-icon, .item-icon,
.server-icon { .server-icon {
color: var(--main-color); color: var(--main-color);

View File

@ -2,13 +2,51 @@
<div class="agent-config-sidebar" :class="{ open: isOpen }"> <div class="agent-config-sidebar" :class="{ open: isOpen }">
<!-- 侧边栏头部 --> <!-- 侧边栏头部 -->
<div class="sidebar-header"> <div class="sidebar-header">
<div class="header-center"> <div class="header-top-row">
<a-segmented v-model:value="activeTab" :options="segmentedOptions" /> <div v-if="selectedAgentId" class="config-manage-row">
<a-select
:value="selectedAgentConfigId"
:options="configSwitchOptions"
class="config-switch-select"
placeholder="选择配置"
@update:value="handleConfigSwitch"
/>
</div> </div>
<a-button type="text" size="small" @click="closeSidebar" class="close-btn"> <div class="header-actions">
<a-tooltip
v-if="!isEmptyConfig && userStore.isAdmin"
:title="isCurrentDefault ? '当前已是默认配置' : '设为默认配置'"
>
<a-button
type="text"
shape="circle"
class="icon-btn lucide-icon-btn"
:class="{ 'is-default': isCurrentDefault }"
@click="setAsDefault"
>
<Star :size="18" :fill="isCurrentDefault ? 'currentColor' : 'none'" />
</a-button>
</a-tooltip>
<a-tooltip v-if="!isEmptyConfig && userStore.isAdmin" title="删除配置">
<a-button
type="text"
shape="circle"
danger
class="icon-btn lucide-icon-btn"
@click="confirmDeleteConfig"
:disabled="isDeletingConfig"
>
<Trash2 :size="18" />
</a-button>
</a-tooltip>
<a-button type="text" size="small" @click="closeSidebar" class="icon-btn lucide-icon-btn">
<X :size="16" /> <X :size="16" />
</a-button> </a-button>
</div> </div>
</div>
</div>
<!-- 侧边栏内容 --> <!-- 侧边栏内容 -->
<div class="sidebar-content"> <div class="sidebar-content">
@ -19,7 +57,11 @@
<!-- <a-divider /> --> <!-- <a-divider /> -->
<div v-if="selectedAgentId && configurableItems" class="config-form-content"> <div
v-if="selectedAgentId && configurableItems"
class="config-form-content"
:class="{ 'is-readonly': isReadOnlyConfig }"
>
<!-- 配置表单 --> <!-- 配置表单 -->
<a-form :model="agentConfig" layout="vertical" class="config-form"> <a-form :model="agentConfig" layout="vertical" class="config-form">
<a-alert <a-alert
@ -49,7 +91,11 @@
<!-- <div>{{ value }}</div> --> <!-- <div>{{ value }}</div> -->
<!-- 模型选择 --> <!-- 模型选择 -->
<div v-if="value.template_metadata.kind === 'llm'" class="model-selector"> <div
v-if="value.template_metadata.kind === 'llm'"
class="model-selector"
:class="{ 'is-readonly': isReadOnlyConfig }"
>
<ModelSelectorComponent <ModelSelectorComponent
@select-model="(spec) => handleModelChange(key, spec)" @select-model="(spec) => handleModelChange(key, spec)"
:model_spec="agentConfig[key] || ''" :model_spec="agentConfig[key] || ''"
@ -68,52 +114,18 @@
> >
{{ agentConfig[key] || getPlaceholder(key, value) }} {{ agentConfig[key] || getPlaceholder(key, value) }}
</div> </div>
<div class="edit-hint">点击查看并编辑</div> <div class="edit-hint">
{{ isReadOnlyConfig ? '查看' : '点击查看并编辑' }}
</div> </div>
</div> </div>
<!-- 工具选择 -->
<!-- <div v-else-if="value.template_metadata.kind === 'tools'" class="tools-selector">
<div class="tools-summary">
<div class="tools-summary-info">
<span class="tools-count">已选择 {{ getSelectedCount(key) }} 个工具</span>
<a-button
type="link"
size="small"
@click="clearSelection(key)"
v-if="getSelectedCount(key) > 0"
class="clear-btn"
>
清空
</a-button>
</div> </div>
<a-button
type="primary"
@click="openToolsModal"
class="select-tools-btn"
size="small"
>
选择工具
</a-button>
</div>
<div v-if="getSelectedCount(key) > 0" class="selected-tools-preview">
<a-tag
v-for="toolId in agentConfig[key]"
:key="toolId"
closable
@close="removeSelectedTool(toolId)"
class="tool-tag"
>
{{ getToolNameById(toolId) }}
</a-tag>
</div>
</div> -->
<!-- 布尔类型 --> <!-- 布尔类型 -->
<a-switch <a-switch
v-else-if="typeof agentConfig[key] === 'boolean'" v-else-if="typeof agentConfig[key] === 'boolean'"
:checked="agentConfig[key]" :checked="agentConfig[key]"
@update:checked="(val) => agentStore.updateAgentConfig({ [key]: val })" :disabled="isReadOnlyConfig"
@update:checked="(val) => updateConfigValue(key, val)"
/> />
<!-- 单选 --> <!-- 单选 -->
@ -122,7 +134,8 @@
value?.options.length > 0 && (value?.type === 'str' || value?.type === 'select') value?.options.length > 0 && (value?.type === 'str' || value?.type === 'select')
" "
:value="agentConfig[key]" :value="agentConfig[key]"
@update:value="(val) => agentStore.updateAgentConfig({ [key]: val })" :disabled="isReadOnlyConfig"
@update:value="(val) => updateConfigValue(key, val)"
class="config-select" class="config-select"
> >
<a-select-option v-for="option in value.options" :key="option" :value="option"> <a-select-option v-for="option in value.options" :key="option" :value="option">
@ -135,8 +148,11 @@
<!-- Case 1: <= 5 options, inline list --> <!-- Case 1: <= 5 options, inline list -->
<div v-if="getConfigOptions(value).length <= 5" class="multi-select-cards"> <div v-if="getConfigOptions(value).length <= 5" class="multi-select-cards">
<div class="multi-select-label"> <div class="multi-select-label">
<span>已选择 {{ getSelectedCount(key) }} </span> <span
<div class="label-actions"> >已选择 {{ getSelectedCount(key) }} |
{{ getConfigOptions(value).length }} </span
>
<div v-if="!isReadOnlyConfig" class="label-actions">
<a-button <a-button
type="link" type="link"
size="small" size="small"
@ -152,7 +168,7 @@
type="link" type="link"
size="small" size="small"
@click="refreshConfigOptions(key, value.template_metadata.kind)" @click="refreshConfigOptions(key, value.template_metadata.kind)"
class="action-btn" class="inline-action-btn lucide-icon-btn"
> >
<RotateCw :size="12" /> <RotateCw :size="12" />
刷新 刷新
@ -161,7 +177,7 @@
type="link" type="link"
size="small" size="small"
@click="navigateToConfigPage(value.template_metadata.kind)" @click="navigateToConfigPage(value.template_metadata.kind)"
class="action-btn" class="inline-action-btn lucide-icon-btn"
> >
<Settings :size="12" /> <Settings :size="12" />
配置 配置
@ -172,14 +188,19 @@
<div class="options-grid"> <div class="options-grid">
<div <div
v-for="option in getConfigOptions(value)" v-for="option in isReadOnlyConfig
? getConfigOptions(value).filter((opt) =>
isOptionSelected(key, getOptionValue(opt))
)
: getConfigOptions(value)"
:key="getOptionValue(option)" :key="getOptionValue(option)"
class="option-card" class="option-card"
:class="{ :class="{
selected: isOptionSelected(key, getOptionValue(option)), selected: isOptionSelected(key, getOptionValue(option)),
unselected: !isOptionSelected(key, getOptionValue(option)) unselected: !isOptionSelected(key, getOptionValue(option)),
readonly: isReadOnlyConfig
}" }"
@click="toggleOption(key, getOptionValue(option))" @click="!isReadOnlyConfig && toggleOption(key, getOptionValue(option))"
> >
<div class="option-content"> <div class="option-content">
<span class="option-text">{{ getOptionLabel(option) }}</span> <span class="option-text">{{ getOptionLabel(option) }}</span>
@ -200,20 +221,24 @@
<div v-else class="selection-container"> <div v-else class="selection-container">
<div class="selection-summary"> <div class="selection-summary">
<div class="selection-summary-info"> <div class="selection-summary-info">
<span class="selection-count">已选择 {{ getSelectedCount(key) }} </span> <span class="selection-count"
>已选择 {{ getSelectedCount(key) }} |
{{ getConfigOptions(value).length }} </span
>
<a-button <a-button
v-if="!isReadOnlyConfig && getSelectedCount(key) > 0"
type="link" type="link"
size="small" size="small"
class="clear-btn" class="clear-btn"
@click="clearSelection(key)" @click="clearSelection(key)"
v-if="getSelectedCount(key) > 0"
> >
清空 清空
</a-button> </a-button>
</div> </div>
<a-button <a-button
v-if="!isReadOnlyConfig"
type="primary" type="primary"
size="small" size="small"
class="selection-trigger-btn" class="selection-trigger-btn"
@ -229,7 +254,7 @@
<a-tag <a-tag
v-for="val in agentConfig[key]" v-for="val in agentConfig[key]"
:key="val" :key="val"
closable :closable="!isReadOnlyConfig"
@close="toggleOption(key, val)" @close="toggleOption(key, val)"
class="selection-tag" class="selection-tag"
> >
@ -241,9 +266,12 @@
<!-- 数字 --> <!-- 数字 -->
<a-input-number <a-input-number
v-else-if="value?.type === 'number' || value?.type === 'int' || value?.type === 'float'" v-else-if="
value?.type === 'number' || value?.type === 'int' || value?.type === 'float'
"
:value="agentConfig[key]" :value="agentConfig[key]"
@update:value="(val) => agentStore.updateAgentConfig({ [key]: val })" :disabled="isReadOnlyConfig"
@update:value="(val) => updateConfigValue(key, val)"
:placeholder="getPlaceholder(key, value)" :placeholder="getPlaceholder(key, value)"
class="config-input-number" class="config-input-number"
/> />
@ -252,7 +280,8 @@
<a-slider <a-slider
v-else-if="value?.type === 'slider'" v-else-if="value?.type === 'slider'"
:value="agentConfig[key]" :value="agentConfig[key]"
@update:value="(val) => agentStore.updateAgentConfig({ [key]: val })" :disabled="isReadOnlyConfig"
@update:value="(val) => updateConfigValue(key, val)"
:min="value.min" :min="value.min"
:max="value.max" :max="value.max"
:step="value.step" :step="value.step"
@ -263,7 +292,8 @@
<a-input <a-input
v-else v-else
:value="agentConfig[key]" :value="agentConfig[key]"
@update:value="(val) => agentStore.updateAgentConfig({ [key]: val })" :disabled="isReadOnlyConfig"
@update:value="(val) => updateConfigValue(key, val)"
:placeholder="getPlaceholder(key, value)" :placeholder="getPlaceholder(key, value)"
class="config-input" class="config-input"
/> />
@ -275,45 +305,33 @@
</div> </div>
<!-- 固定在底部的操作按钮 --> <!-- 固定在底部的操作按钮 -->
<div class="sidebar-footer" v-if="!isEmptyConfig && userStore.isAdmin"> <div class="sidebar-footer" v-if="userStore.isAdmin && selectedAgentId">
<div class="form-actions"> <div class="form-actions">
<a-button <a-button
type="primary" type="primary"
@click="saveConfig" @click="saveConfig"
class="save-btn" class="footer-main-btn save-btn"
:class="{ changed: agentStore.hasConfigChanges }" :class="{ changed: agentStore.hasConfigChanges }"
:disabled="isSavingConfig" :disabled="isSavingConfig || isEmptyConfig"
> >
保存 保存
</a-button> </a-button>
<a-tooltip :title="isCurrentDefault ? '当前已是默认配置' : '设为默认配置'">
<a-button type="text" shape="circle" class="icon-btn" @click="setAsDefault">
<Star
:size="18"
:fill="isCurrentDefault ? 'currentColor' : 'none'"
:class="{ 'is-default': isCurrentDefault }"
/>
</a-button>
</a-tooltip>
<a-tooltip title="删除配置">
<a-button
type="text"
shape="circle"
danger
class="icon-btn"
@click="confirmDeleteConfig"
:disabled="isDeletingConfig"
>
<Trash2 :size="18" />
</a-button>
</a-tooltip>
</div> </div>
</div> </div>
<!-- 通用选择弹窗 --> <!-- 通用选择弹窗 -->
<a-modal
v-model:open="createConfigModalOpen"
title="新建配置"
:width="360"
:confirmLoading="createConfigLoading"
@ok="handleCreateConfig"
@cancel="closeCreateConfigModal"
>
<a-input v-model:value="createConfigName" placeholder="请输入配置名称" allow-clear />
</a-modal>
<a-modal <a-modal
v-model:open="selectionModalOpen" v-model:open="selectionModalOpen"
:title="`选择${configurableItems[currentConfigKey]?.name || '项目'}`" :title="`选择${configurableItems[currentConfigKey]?.name || '项目'}`"
@ -334,12 +352,12 @@
<Search :size="16" class="search-icon" /> <Search :size="16" class="search-icon" />
</template> </template>
</a-input> </a-input>
<template v-if="isToolsKind(currentConfigKind)"> <template v-if="!isReadOnlyConfig && isToolsKind(currentConfigKind)">
<a-button <a-button
type="text" type="text"
size="small" size="small"
@click="refreshConfigOptions(currentConfigKey, currentConfigKind)" @click="refreshConfigOptions(currentConfigKey, currentConfigKind)"
class="modal-action-btn" class="inline-action-btn lucide-icon-btn"
title="刷新列表" title="刷新列表"
> >
<RotateCw :size="14" /> <RotateCw :size="14" />
@ -349,7 +367,7 @@
type="text" type="text"
size="small" size="small"
@click="navigateToConfigPage(currentConfigKind)" @click="navigateToConfigPage(currentConfigKind)"
class="modal-action-btn" class="inline-action-btn lucide-icon-btn"
title="跳转配置" title="跳转配置"
> >
<Settings :size="14" /> <Settings :size="14" />
@ -364,7 +382,7 @@
:key="getOptionValue(option)" :key="getOptionValue(option)"
class="selection-item" class="selection-item"
:class="{ selected: tempSelectedValues.includes(getOptionValue(option)) }" :class="{ selected: tempSelectedValues.includes(getOptionValue(option)) }"
@click="toggleModalSelection(getOptionValue(option))" @click="!isReadOnlyConfig && toggleModalSelection(getOptionValue(option))"
> >
<div class="selection-item-content"> <div class="selection-item-content">
<div class="selection-item-header"> <div class="selection-item-header">
@ -390,7 +408,9 @@
<div class="modal-actions"> <div class="modal-actions">
<a-button @click="closeSelectionModal">取消</a-button> <a-button @click="closeSelectionModal">取消</a-button>
<a-button type="primary" @click="confirmSelection">确认</a-button> <a-button v-if="!isReadOnlyConfig" type="primary" @click="confirmSelection">
确认
</a-button>
</div> </div>
</div> </div>
</div> </div>
@ -408,14 +428,19 @@
<a-textarea <a-textarea
v-model:value="systemPromptDraft" v-model:value="systemPromptDraft"
:rows="14" :rows="14"
:disabled="isReadOnlyConfig"
:placeholder="systemPromptModalPlaceholder" :placeholder="systemPromptModalPlaceholder"
class="system-prompt-modal-input" class="system-prompt-modal-input"
/> />
</div> </div>
<template #footer> <template #footer>
<a-button @click="closeSystemPromptModal">取消</a-button> <a-button @click="closeSystemPromptModal">{{
<a-button type="primary" @click="saveSystemPrompt">保存</a-button> isReadOnlyConfig ? '关闭' : '取消'
}}</a-button>
<a-button v-if="!isReadOnlyConfig" type="primary" @click="saveSystemPrompt">
保存
</a-button>
</template> </template>
</a-modal> </a-modal>
</div> </div>
@ -460,11 +485,15 @@ watch(
() => props.isOpen, () => props.isOpen,
async (val) => { async (val) => {
if (val) { if (val) {
databaseStore.loadDatabases().catch(() => {}) //
loadLiveSkillOptions().catch(() => {}) databaseStore.loadDatabases(true).catch(() => {})
loadSubagentOptions().catch(() => {}) loadLiveSkillOptions(true).catch(() => {})
loadToolOptions().catch(() => {}) loadSubagentOptions(true).catch(() => {})
loadToolOptions(true).catch(() => {})
if (selectedAgentId.value) { if (selectedAgentId.value) {
agentStore.fetchAgentConfigs(selectedAgentId.value).catch((error) => {
console.error('刷新智能体配置列表失败:', error)
})
try { try {
await agentStore.fetchAgentDetail(selectedAgentId.value, true) await agentStore.fetchAgentDetail(selectedAgentId.value, true)
} catch (error) { } catch (error) {
@ -496,10 +525,13 @@ const selectionSearchText = ref('')
const systemPromptModalOpen = ref(false) const systemPromptModalOpen = ref(false)
const currentSystemPromptKey = ref(null) const currentSystemPromptKey = ref(null)
const systemPromptDraft = ref('') const systemPromptDraft = ref('')
const activeTab = ref('basic')
const liveSkillOptions = ref([]) const liveSkillOptions = ref([])
const liveSubagentOptions = ref([]) const liveSubagentOptions = ref([])
const toolOptionsFromApi = ref([]) const toolOptionsFromApi = ref([])
const createConfigModalOpen = ref(false)
const createConfigLoading = ref(false)
const createConfigName = ref('')
const CREATE_CONFIG_OPTION_VALUE = '__create_config__'
const isEmptyConfig = computed(() => { const isEmptyConfig = computed(() => {
return !selectedAgentId.value || Object.keys(configurableItems.value).length === 0 return !selectedAgentId.value || Object.keys(configurableItems.value).length === 0
@ -509,6 +541,8 @@ const isCurrentDefault = computed(() => {
return !!selectedConfigSummary.value?.is_default return !!selectedConfigSummary.value?.is_default
}) })
const isReadOnlyConfig = computed(() => !userStore.isAdmin)
const isSavingConfig = ref(false) const isSavingConfig = ref(false)
const isDeletingConfig = ref(false) const isDeletingConfig = ref(false)
@ -528,14 +562,19 @@ const hasOtherConfigs = computed(() => {
}) })
}) })
const segmentedOptions = computed(() => { const configSwitchOptions = computed(() => {
const options = [ if (!selectedAgentId.value) return []
{ label: '基础', value: 'basic' }, const list = agentConfigs.value[selectedAgentId.value] || []
{ label: '工具', value: 'tools' } const options = list.map((cfg) => ({
] label: cfg.is_default ? `${cfg.name}(默认)` : cfg.name,
value: cfg.id
}))
if (hasOtherConfigs.value) { if (userStore.isAdmin) {
options.push({ label: '其他', value: 'other' }) options.push({
label: '新建配置',
value: CREATE_CONFIG_OPTION_VALUE
})
} }
return options return options
@ -611,7 +650,8 @@ const isToolsKind = (kind) => {
} }
// //
const refreshConfigOptions = async (key, kind) => { const refreshConfigOptions = async (_key, kind) => {
if (isReadOnlyConfig.value) return
try { try {
switch (kind) { switch (kind) {
case 'knowledges': case 'knowledges':
@ -643,6 +683,7 @@ const refreshConfigOptions = async (key, kind) => {
// //
const navigateToConfigPage = (kind) => { const navigateToConfigPage = (kind) => {
if (isReadOnlyConfig.value) return
// //
closeSelectionModal() closeSelectionModal()
// //
@ -750,28 +791,67 @@ const filteredOptions = computed(() => {
}) })
// //
const shouldShowConfig = (key, value) => { const handleConfigSwitch = async (configId) => {
const isBasic = if (configId === CREATE_CONFIG_OPTION_VALUE) {
value.template_metadata?.kind === 'prompt' || value.template_metadata?.kind === 'llm' openCreateConfigModal()
const isTools = return
value.template_metadata?.kind === 'mcps' ||
value.template_metadata?.kind === 'knowledges' ||
value.template_metadata?.kind === 'tools' ||
value.template_metadata?.kind === 'skills' ||
value.template_metadata?.kind === 'subagents' ||
key === 'skills' ||
key === 'subagents'
if (activeTab.value === 'basic') {
// System Prompt, LLM Model
return isBasic
} else if (activeTab.value === 'tools') {
// Tools, MCPs, Knowledges
return isTools
} else {
//
return !isBasic && !isTools
} }
if (!configId || configId === selectedAgentConfigId.value) return
try {
await agentStore.selectAgentConfig(configId)
} catch (error) {
console.error('切换配置出错:', error)
message.error('切换配置失败')
}
}
const updateConfigValue = (key, value) => {
if (isReadOnlyConfig.value) return
agentStore.updateAgentConfig({
[key]: value
})
}
const openCreateConfigModal = () => {
if (!userStore.isAdmin) return
createConfigName.value = ''
createConfigModalOpen.value = true
}
const closeCreateConfigModal = () => {
createConfigModalOpen.value = false
createConfigName.value = ''
}
const handleCreateConfig = async () => {
if (!userStore.isAdmin) return
if (!selectedAgentId.value) return
const name = createConfigName.value.trim()
if (!name) {
message.error('请输入配置名称')
return
}
createConfigLoading.value = true
try {
await agentStore.createAgentConfigProfile({
name,
setDefault: false,
fromCurrent: false
})
closeCreateConfigModal()
message.success('配置已创建')
} catch (error) {
console.error('创建配置出错:', error)
message.error(error.message || '创建配置失败')
} finally {
createConfigLoading.value = false
}
}
const shouldShowConfig = () => {
return true
} }
const closeSidebar = () => { const closeSidebar = () => {
@ -787,11 +867,12 @@ const getConfigLabel = (key, value) => {
return key return key
} }
const getPlaceholder = (key, value) => { const getPlaceholder = (_key, value) => {
return `(默认: ${value.default}` return `(默认: ${value.default}`
} }
const handleModelChange = (key, spec) => { const handleModelChange = (key, spec) => {
if (isReadOnlyConfig.value) return
if (typeof spec !== 'string' || !spec) return if (typeof spec !== 'string' || !spec) return
agentStore.updateAgentConfig({ agentStore.updateAgentConfig({
[key]: spec [key]: spec
@ -818,6 +899,7 @@ const getSelectedCount = (key) => {
} }
const toggleOption = (key, option) => { const toggleOption = (key, option) => {
if (isReadOnlyConfig.value) return
const currentOptions = [...ensureArray(key)] const currentOptions = [...ensureArray(key)]
const index = currentOptions.indexOf(option) const index = currentOptions.indexOf(option)
@ -833,6 +915,7 @@ const toggleOption = (key, option) => {
} }
const clearSelection = (key) => { const clearSelection = (key) => {
if (isReadOnlyConfig.value) return
agentStore.updateAgentConfig({ agentStore.updateAgentConfig({
[key]: [] [key]: []
}) })
@ -846,6 +929,7 @@ const getOptionLabelFromValue = (key, val) => {
} }
const openSelectionModal = async (key) => { const openSelectionModal = async (key) => {
if (isReadOnlyConfig.value) return
currentConfigKey.value = key currentConfigKey.value = key
// API // API
if (configurableItems.value[key]?.template_metadata?.kind === 'tools') { if (configurableItems.value[key]?.template_metadata?.kind === 'tools') {
@ -871,6 +955,7 @@ const openSelectionModal = async (key) => {
} }
const toggleModalSelection = (optionValue) => { const toggleModalSelection = (optionValue) => {
if (isReadOnlyConfig.value) return
const index = tempSelectedValues.value.indexOf(optionValue) const index = tempSelectedValues.value.indexOf(optionValue)
if (index > -1) { if (index > -1) {
tempSelectedValues.value.splice(index, 1) tempSelectedValues.value.splice(index, 1)
@ -880,6 +965,10 @@ const toggleModalSelection = (optionValue) => {
} }
const confirmSelection = () => { const confirmSelection = () => {
if (isReadOnlyConfig.value) {
closeSelectionModal()
return
}
if (currentConfigKey.value) { if (currentConfigKey.value) {
agentStore.updateAgentConfig({ agentStore.updateAgentConfig({
[currentConfigKey.value]: [...tempSelectedValues.value] [currentConfigKey.value]: [...tempSelectedValues.value]
@ -909,6 +998,7 @@ const closeSystemPromptModal = () => {
} }
const saveSystemPrompt = () => { const saveSystemPrompt = () => {
if (isReadOnlyConfig.value) return
if (!currentSystemPromptKey.value) return if (!currentSystemPromptKey.value) return
agentStore.updateAgentConfig({ agentStore.updateAgentConfig({
[currentSystemPromptKey.value]: systemPromptDraft.value [currentSystemPromptKey.value]: systemPromptDraft.value
@ -1018,7 +1108,6 @@ const confirmDeleteConfig = async () => {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@padding-bottom: 0px;
.agent-config-sidebar { .agent-config-sidebar {
position: relative; position: relative;
width: 0; width: 0;
@ -1037,29 +1126,100 @@ const confirmDeleteConfig = async () => {
.sidebar-header { .sidebar-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding: 0 20px; padding: 0 12px;
height: var(--header-height);
border-bottom: 1px solid var(--gray-150); border-bottom: 1px solid var(--gray-150);
background: var(--gray-0); background: var(--gray-0);
flex-shrink: 0; flex-shrink: 0;
min-width: 400px; min-width: 400px;
height: var(--header-height); z-index: 10;
.header-center { .header-top-row {
flex: 1;
display: flex; display: flex;
justify-content: center; align-items: center;
gap: 8px;
width: 100%;
} }
.close-btn { .config-manage-row {
color: var(--gray-600); display: flex;
border: none; align-items: center;
padding: 4px; flex: 1;
min-width: 0;
&:hover { .config-switch-select {
color: var(--gray-900); flex: 1;
background: var(--gray-100); min-width: 0;
:deep(.ant-select-selector) {
height: 32px;
border-radius: 8px;
border-color: var(--gray-200);
padding: 0 10px;
transition: border-color 0.2s ease;
}
:deep(.ant-select-selection-search-input),
:deep(.ant-select-selection-item),
:deep(.ant-select-selection-placeholder) {
line-height: 30px;
font-size: 13px;
}
:deep(.ant-select.ant-select-focused .ant-select-selector),
:deep(.ant-select-selector:hover) {
border-color: var(--main-color);
box-shadow: none;
}
}
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
}
.icon-btn {
width: 32px;
height: 32px;
border-radius: 8px;
color: var(--gray-600);
border: 1px solid var(--gray-200);
background: var(--gray-0);
padding: 0;
transition:
color 0.2s ease,
border-color 0.2s ease,
background-color 0.2s ease;
&:hover:not(:disabled) {
color: var(--main-600);
border-color: var(--main-200);
background: var(--main-10);
}
&.is-default {
color: var(--color-warning-500);
}
&.ant-btn-dangerous:hover:not(:disabled) {
color: var(--color-error-700);
border-color: var(--color-error-100);
background: var(--color-error-50);
}
&:disabled {
cursor: not-allowed;
background: transparent;
color: var(--gray-400);
border-color: var(--gray-200);
&.is-default {
opacity: 1;
} }
} }
} }
@ -1067,9 +1227,8 @@ const confirmDeleteConfig = async () => {
.sidebar-content { .sidebar-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 8px 12px; padding: 10px 12px 8px;
min-width: 400px; min-width: 400px;
padding-bottom: @padding-bottom;
.agent-info { .agent-info {
.agent-basic-info { .agent-basic-info {
@ -1084,6 +1243,50 @@ const confirmDeleteConfig = async () => {
.config-form-content { .config-form-content {
margin-bottom: 20px; margin-bottom: 20px;
&.is-readonly {
.config-item {
background: var(--gray-20);
.model-selector.is-readonly {
opacity: 0.78;
pointer-events: none;
}
.system-prompt-display {
cursor: default;
&:hover {
border-color: var(--gray-200);
background: transparent;
.edit-hint {
opacity: 1;
}
}
.edit-hint {
color: var(--gray-500);
opacity: 1;
}
}
.option-card.readonly {
cursor: default;
&:hover {
border-color: var(--gray-300);
background: var(--gray-0);
}
&.selected:hover {
border-color: var(--main-color);
background: var(--main-10);
}
}
}
}
.config-form { .config-form {
.config-alert { .config-alert {
margin-bottom: 16px; margin-bottom: 16px;
@ -1153,11 +1356,6 @@ const confirmDeleteConfig = async () => {
color: var(--gray-400); color: var(--gray-400);
font-style: italic; font-style: italic;
} }
&:empty::before {
content: attr(data-placeholder);
color: var(--gray-400);
}
} }
.edit-hint { .edit-hint {
@ -1198,61 +1396,26 @@ const confirmDeleteConfig = async () => {
.form-actions { .form-actions {
display: flex; display: flex;
flex-direction: row; gap: 10px;
gap: 12px;
justify-content: space-between;
align-items: center; align-items: center;
.icon-btn { .footer-main-btn {
width: 36px; width: 100%;
height: 36px; height: 36px;
border-radius: 6px; border-radius: 8px;
color: var(--gray-600); font-size: 14px;
border: 1px solid var(--gray-200); font-weight: 500;
background: var(--gray-0); transition:
display: flex; opacity 0.2s ease,
justify-content: center; border-color 0.2s ease,
align-items: center; background-color 0.2s ease,
color 0.2s ease;
&:hover:not(:disabled) {
color: var(--main-600);
border-color: var(--main-200);
background: var(--main-10);
}
&.is-default {
// color: var(--main-500);
color: var(--color-warning-500);
}
&[danger]:hover:not(:disabled) {
color: var(--error-600);
border-color: var(--error-200);
background: var(--error-10);
}
&:disabled {
cursor: not-allowed;
background: transparent;
color: var(--gray-400);
border-color: var(--gray-200);
&.is-default {
opacity: 1;
}
}
} }
.save-btn { .save-btn {
flex: 1;
height: 36px;
border-radius: 6px;
font-weight: 500;
font-size: 14px;
background-color: var(--gray-100); background-color: var(--gray-100);
border: 1px solid var(--gray-200); border: 1px solid var(--gray-200);
color: var(--gray-600); color: var(--gray-600);
transition: all 0.2s ease;
&.changed { &.changed {
background-color: var(--main-color); background-color: var(--main-color);
@ -1301,17 +1464,10 @@ const confirmDeleteConfig = async () => {
} }
.selection-trigger-btn { .selection-trigger-btn {
background: var(--main-color);
border: none;
border-radius: 4px; border-radius: 4px;
height: 28px; height: 28px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
&:hover {
background: var(--main-color);
opacity: 0.9;
}
} }
} }
@ -1355,21 +1511,6 @@ const confirmDeleteConfig = async () => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
.action-btn {
font-size: 12px;
color: var(--gray-600);
display: flex;
align-items: center;
gap: 2px;
padding: 2px 6px;
height: auto;
line-height: 1;
&:hover {
color: var(--main-color);
}
}
} }
} }
@ -1462,7 +1603,7 @@ const confirmDeleteConfig = async () => {
&:focus-within { &:focus-within {
border-color: var(--main-color); border-color: var(--main-color);
box-shadow: 0 0 0 2px rgba(var(--main-color-rgb), 0.1); box-shadow: 0 0 0 2px rgba(1, 97, 121, 0.1);
.search-icon { .search-icon {
color: var(--main-color); color: var(--main-color);
@ -1473,26 +1614,13 @@ const confirmDeleteConfig = async () => {
border-color: var(--gray-400); border-color: var(--gray-400);
} }
} }
.modal-action-btn {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: var(--gray-600);
white-space: nowrap;
&:hover {
color: var(--main-color);
}
}
} }
.selection-list { .selection-list {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px; gap: 12px;
max-height: max(60vh, 800px); max-height: 60vh;
overflow-y: auto; overflow-y: auto;
border-radius: 8px; border-radius: 8px;
margin-bottom: 16px; margin-bottom: 16px;
@ -1502,31 +1630,11 @@ const confirmDeleteConfig = async () => {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: var(--gray-100);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: var(--gray-400);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--gray-500);
}
.selection-item { .selection-item {
padding: 12px 16px; padding: 12px 16px;
border-bottom: none;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
border-radius: 8px; border-radius: 8px;
margin-bottom: 4px;
background: var(--gray-0); background: var(--gray-0);
border: 1px solid var(--gray-200); border: 1px solid var(--gray-200);
@ -1670,6 +1778,23 @@ const confirmDeleteConfig = async () => {
} }
} }
.inline-action-btn {
padding: 2px 6px;
height: auto;
line-height: 1;
font-size: 12px;
color: var(--gray-600);
white-space: nowrap;
&:hover {
color: var(--main-color);
}
}
.selection-search .inline-action-btn {
font-size: 13px;
}
// //
@media (max-width: 768px) { @media (max-width: 768px) {
.agent-config-sidebar.open { .agent-config-sidebar.open {

View File

@ -31,18 +31,22 @@
</template> </template>
<template #actions-left> <template #actions-left>
<div class="input-actions-left"> <div class="input-actions-left">
<slot name="actions-left-extra"></slot>
<!-- State Toggle Button --> <!-- State Toggle Button -->
<div <button
v-if="hasStateContent" v-if="hasStateContent"
class="state-toggle-btn" class="input-action-btn"
:class="{ active: isPanelOpen }" :class="{ active: isPanelOpen }"
@click="$emit('toggle-panel')" @click="$emit('toggle-panel')"
title="查看工作状态" title="查看工作状态"
> >
<FolderCode :size="18" /> <FolderCode :size="18" />
<span>状态</span> <span>状态</span>
</button>
</div> </div>
</template>
<template #actions-right>
<div class="input-actions-right">
<slot name="actions-left-extra"></slot>
</div> </div>
</template> </template>
</MessageInputComponent> </MessageInputComponent>
@ -184,12 +188,19 @@ defineExpose({
gap: 8px; gap: 8px;
} }
.state-toggle-btn { .input-actions-right {
display: flex;
align-items: center;
margin-right: 8px;
}
// 穿 slot
:deep(.input-action-btn) {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 0 8px; padding: 8px 8px;
height: 28px; // height: 28px;
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 14px;
color: var(--gray-600); color: var(--gray-600);
@ -200,13 +211,13 @@ defineExpose({
border: none; border: none;
&:hover { &:hover {
color: var(--main-color); color: var(--gray-900);
background: var(--gray-100); background: var(--gray-50);
} }
&.active { &.active {
color: var(--main-color); color: var(--gray-900);
background: var(--main-50); background: var(--gray-100);
font-weight: 500; font-weight: 500;
} }
@ -220,4 +231,11 @@ defineExpose({
line-height: 1; line-height: 1;
} }
} }
// slot hide-text
:deep(.hide-text) {
@media (max-width: 768px) {
display: none;
}
}
</style> </style>

View File

@ -189,9 +189,7 @@
v-if="currentServer.env && Object.keys(currentServer.env).length > 0" v-if="currentServer.env && Object.keys(currentServer.env).length > 0"
> >
<label>环境变量</label> <label>环境变量</label>
<pre class="code-pre">{{ <pre class="code-pre">{{ JSON.stringify(currentServer.env, null, 2) }}</pre>
JSON.stringify(currentServer.env, null, 2)
}}</pre>
</div> </div>
</template> </template>

View File

@ -464,5 +464,4 @@ defineExpose({
width: 100%; width: 100%;
} }
} }
</style> </style>

View File

@ -343,6 +343,18 @@ export const useAgentStore = defineStore(
loadedConfig[key] = item.default loadedConfig[key] = item.default
} }
} }
if (
loadedConfig[key] !== undefined &&
loadedConfig[key] !== null &&
loadedConfig[key] !== '' &&
(item?.type === 'number' || item?.type === 'int' || item?.type === 'float')
) {
const numericValue = Number(loadedConfig[key])
if (!Number.isNaN(numericValue)) {
loadedConfig[key] = item.type === 'int' ? Math.trunc(numericValue) : numericValue
}
}
}) })
agentConfig.value = loadedConfig agentConfig.value = loadedConfig

View File

@ -1,17 +1,6 @@
<template> <template>
<div class="agent-view"> <div class="agent-view">
<div class="agent-view-body"> <div class="agent-view-body">
<a-modal
v-model:open="createConfigModalOpen"
title="新建配置"
:width="320"
:confirmLoading="createConfigLoading"
@ok="handleCreateConfig"
@cancel="() => (createConfigModalOpen = false)"
>
<a-input v-model:value="createConfigName" placeholder="请输入配置名称" allow-clear />
</a-modal>
<!-- 中间内容区域 --> <!-- 中间内容区域 -->
<div class="content"> <div class="content">
<AgentChatComponent <AgentChatComponent
@ -20,66 +9,18 @@
@close-config-sidebar="() => (chatUIStore.isConfigSidebarOpen = false)" @close-config-sidebar="() => (chatUIStore.isConfigSidebarOpen = false)"
> >
<template #input-actions-left> <template #input-actions-left>
<a-dropdown <button
v-if="selectedAgentId" v-if="selectedAgentId"
v-model:open="configDropdownOpen" class="input-action-btn"
:trigger="['click']" :class="{ active: chatUIStore.isConfigSidebarOpen }"
> :disabled="isLoadingConfig"
<div
type="button"
class="agent-nav-btn config-toggle-btn"
:class="{ active: configDropdownOpen }"
>
<Settings2 size="18" class="nav-btn-icon" />
<span class="text hide-text">
{{ selectedConfigSummary?.name || '配置' }}
</span>
<ChevronDown size="16" class="nav-btn-icon" />
</div>
<template #overlay>
<a-menu
:selectedKeys="selectedAgentConfigId ? [String(selectedAgentConfigId)] : []"
>
<a-menu-item
v-for="cfg in agentConfigs[selectedAgentId] || []"
:key="String(cfg.id)"
@click="selectAgentConfig(cfg.id)"
>
<div class="menu-item-full">
<Star
:size="14"
:fill="cfg.is_default ? 'currentColor' : 'none'"
:style="{
color: cfg.is_default ? 'var(--color-warning-500)' : 'var(--gray-400)'
}"
/>
<span>{{ cfg.name }}</span>
</div>
</a-menu-item>
<a-menu-divider v-if="userStore.isAdmin" />
<a-menu-item
v-if="userStore.isAdmin"
key="create_config"
@click="openCreateConfigModal"
>
<div class="menu-item-layout">
<CirclePlus :size="16" />
<span>新建配置</span>
</div>
</a-menu-item>
<a-menu-item
v-if="userStore.isAdmin"
key="open_config"
@click="openConfigSidebar" @click="openConfigSidebar"
> >
<div class="menu-item-layout"> <Settings2 size="18" />
<SquarePen :size="16" /> <span class="hide-text">
<span>编辑当前配置</span> {{ isLoadingConfig ? '加载中...' : (selectedConfigSummary?.name || '配置') }}
</div> </span>
</a-menu-item> </button>
</a-menu>
</template>
</a-dropdown>
</template> </template>
<template #header-right v-if="userStore.isAdmin"> <template #header-right v-if="userStore.isAdmin">
@ -140,7 +81,7 @@
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { MessageOutlined, ShareAltOutlined } from '@ant-design/icons-vue' import { MessageOutlined, ShareAltOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { Settings2, Ellipsis, ChevronDown, Star, CirclePlus, SquarePen } from 'lucide-vue-next' import { Settings2, Ellipsis } from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import AgentChatComponent from '@/components/AgentChatComponent.vue' import AgentChatComponent from '@/components/AgentChatComponent.vue'
import AgentConfigSidebar from '@/components/AgentConfigSidebar.vue' import AgentConfigSidebar from '@/components/AgentConfigSidebar.vue'
@ -157,7 +98,6 @@ import { storeToRefs } from 'pinia'
// //
const feedbackModal = ref(null) const feedbackModal = ref(null)
const chatComponentRef = ref(null) const chatComponentRef = ref(null)
const configDropdownOpen = ref(false)
// Stores // Stores
const userStore = useUserStore() const userStore = useUserStore()
@ -167,8 +107,7 @@ const route = useRoute()
const router = useRouter() const router = useRouter()
// agentStore // agentStore
const { agents, selectedAgentId, agentConfigs, selectedAgentConfigId, selectedConfigSummary } = const { agents, selectedAgentId, selectedConfigSummary, isLoadingConfig } = storeToRefs(agentStore)
storeToRefs(agentStore)
const syncingRouteAgent = ref(false) const syncingRouteAgent = ref(false)
@ -224,52 +163,7 @@ watch(selectedAgentId, (newAgentId) => {
}) })
const openConfigSidebar = () => { const openConfigSidebar = () => {
configDropdownOpen.value = false chatUIStore.isConfigSidebarOpen = !chatUIStore.isConfigSidebarOpen
chatUIStore.isConfigSidebarOpen = true
}
const createConfigModalOpen = ref(false)
const createConfigLoading = ref(false)
const createConfigName = ref('')
const openCreateConfigModal = () => {
configDropdownOpen.value = false
createConfigName.value = ''
createConfigModalOpen.value = true
}
const handleCreateConfig = async () => {
if (!selectedAgentId.value) return
if (!createConfigName.value) {
message.error('请输入配置名称')
return
}
createConfigLoading.value = true
try {
await agentStore.createAgentConfigProfile({
name: createConfigName.value,
setDefault: false,
fromCurrent: false
})
createConfigModalOpen.value = false
chatUIStore.isConfigSidebarOpen = true
message.success('配置已创建')
} catch (error) {
console.error('创建配置出错:', error)
message.error(error.message || '创建配置失败')
} finally {
createConfigLoading.value = false
}
}
const selectAgentConfig = async (configId) => {
try {
await agentStore.selectAgentConfig(configId)
} catch (error) {
console.error('切换配置出错:', error)
message.error('切换配置失败')
}
} }
// //
@ -964,40 +858,4 @@ const handleFeedback = () => {
gap: 10px; gap: 10px;
width: 100%; width: 100%;
} }
.agent-nav-btn.config-toggle-btn {
gap: 6px;
padding: 0 8px;
height: 28px;
border-radius: 8px;
font-size: 14px;
color: var(--gray-600);
transition: all 0.2s ease;
user-select: none;
.nav-btn-icon {
height: 16px;
}
.text {
line-height: 1;
}
&:hover {
color: var(--main-color);
background: var(--gray-100);
}
&.active {
color: var(--main-color);
background: var(--main-50);
font-weight: 500;
}
}
@media (max-width: 768px) {
.hide-text {
display: none;
}
}
</style> </style>