diff --git a/frontend/src/api/system/index.ts b/frontend/src/api/system/index.ts
index 1a261186..652269fc 100644
--- a/frontend/src/api/system/index.ts
+++ b/frontend/src/api/system/index.ts
@@ -38,9 +38,9 @@ export function getSystemInfo(): Promise<{ data: SystemInfo }> {
}
export function getAgentConfig(): Promise<{ data: AgentConfig }> {
- return get('/api/v1/tenants/agent-config')
+ return get('/api/v1/tenants/kv/agent-config')
}
export function updateAgentConfig(config: AgentConfig): Promise<{ data: AgentConfig }> {
- return put('/api/v1/tenants/agent-config', config)
+ return put('/api/v1/tenants/kv/agent-config', config)
}
diff --git a/frontend/src/api/web-search.ts b/frontend/src/api/web-search.ts
index ec391d73..1f515fa2 100644
--- a/frontend/src/api/web-search.ts
+++ b/frontend/src/api/web-search.ts
@@ -29,13 +29,13 @@ export function getWebSearchProviders() {
return get('/api/v1/web-search/providers')
}
-// Get tenant web search config
+// Get tenant web search config via KV API
export function getTenantWebSearchConfig() {
- return get('/api/v1/tenants/web-search-config')
+ return get('/api/v1/tenants/kv/web-search-config')
}
-// Update tenant web search config
+// Update tenant web search config via KV API
export function updateTenantWebSearchConfig(config: WebSearchConfig) {
- return put('/api/v1/tenants/web-search-config', config)
+ return put('/api/v1/tenants/kv/web-search-config', config)
}
diff --git a/frontend/src/views/settings/WebSearchSettings.vue b/frontend/src/views/settings/WebSearchSettings.vue
index ad91422e..1faa30e1 100644
--- a/frontend/src/views/settings/WebSearchSettings.vue
+++ b/frontend/src/views/settings/WebSearchSettings.vue
@@ -31,11 +31,6 @@
{{ provider.name }}
-
- 免费
- 付费
- 需API密钥
-
{{ provider.description }}
@@ -401,5 +396,35 @@ onMounted(async () => {
line-height: 1.4;
margin-top: 2px;
}
+
+/* 修复下拉项描述与条目重叠:让选项支持多行自适应高度 */
+:deep(.t-select-option) {
+ height: auto;
+ align-items: flex-start;
+ padding-top: 6px;
+ padding-bottom: 6px;
+}
+
+:deep(.t-select-option__content) {
+ white-space: normal;
+}
+
+
+
diff --git a/internal/agent/prompts.go b/internal/agent/prompts.go
index 91f78c4f..20a549c5 100644
--- a/internal/agent/prompts.go
+++ b/internal/agent/prompts.go
@@ -5,6 +5,12 @@ import (
"strings"
)
+const (
+ DefaultAgentTemperature = 0.7
+ DefaultAgentMaxIterations = 20
+ DefaultAgentReflectionEnabled = false
+)
+
// formatFileSize formats file size in human-readable format
func formatFileSize(size int64) string {
const (
@@ -42,6 +48,24 @@ type KnowledgeBaseInfo struct {
RecentDocs []RecentDocInfo // Recently added documents (up to 10)
}
+// PlaceholderDefinition defines a placeholder exposed to UI/configuration
+type PlaceholderDefinition struct {
+ Name string `json:"name"`
+ Label string `json:"label"`
+ Description string `json:"description"`
+}
+
+// AvailablePlaceholders lists all supported prompt placeholders for UI hints
+func AvailablePlaceholders() []PlaceholderDefinition {
+ return []PlaceholderDefinition{
+ {
+ Name: "knowledge_bases",
+ Label: "知识库列表",
+ Description: "自动格式化为表格形式的知识库列表,包含知识库名称、描述、文档数量、最近添加的文档等信息",
+ },
+ }
+}
+
// formatKnowledgeBaseList formats knowledge base information for the prompt
func formatKnowledgeBaseList(kbInfos []*KnowledgeBaseInfo) string {
if len(kbInfos) == 0 {
diff --git a/internal/agent/tools/definitions.go b/internal/agent/tools/definitions.go
new file mode 100644
index 00000000..3ce299ce
--- /dev/null
+++ b/internal/agent/tools/definitions.go
@@ -0,0 +1,35 @@
+package tools
+
+// AvailableTool defines a simple tool metadata used by settings APIs.
+type AvailableTool struct {
+ Name string `json:"name"`
+ Label string `json:"label"`
+ Description string `json:"description"`
+}
+
+// AvailableToolDefinitions returns the list of tools exposed to the UI.
+// Keep this in sync with registered tools in this package.
+func AvailableToolDefinitions() []AvailableTool {
+ return []AvailableTool{
+ {Name: "thinking", Label: "思考", Description: "AI 进行深度思考和推理"},
+ {Name: "todo_write", Label: "制定计划", Description: "为复杂任务制定执行计划"},
+ {Name: "knowledge_search", Label: "知识搜索", Description: "在知识库中搜索相关信息"},
+ {Name: "get_related_chunks", Label: "获取相关片段", Description: "查找相关的知识片段"},
+ {Name: "query_knowledge_graph", Label: "查询知识图谱", Description: "从知识图谱中查询关系"},
+ {Name: "get_document_info", Label: "获取文档信息", Description: "查看文档元数据"},
+ {Name: "database_query", Label: "查询数据库", Description: "查询数据库中的信息"},
+ }
+}
+
+// DefaultAllowedTools returns the default allowed tools list.
+func DefaultAllowedTools() []string {
+ return []string{
+ "thinking",
+ "todo_write",
+ "knowledge_search",
+ "get_related_chunks",
+ "query_knowledge_graph",
+ "get_document_info",
+ "database_query",
+ }
+}
diff --git a/internal/handler/tenant.go b/internal/handler/tenant.go
index d32af3d8..d4344053 100644
--- a/internal/handler/tenant.go
+++ b/internal/handler/tenant.go
@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/Tencent/WeKnora/internal/agent"
+ agenttools "github.com/Tencent/WeKnora/internal/agent/tools"
"github.com/Tencent/WeKnora/internal/errors"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
@@ -240,45 +241,34 @@ type AgentConfigRequest struct {
// GetTenantAgentConfig retrieves the agent configuration for a tenant
// This is the global agent configuration that applies to all sessions by default
-// Tenant ID is obtained from the authentication context
func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
ctx := c.Request.Context()
-
logger.Info(ctx, "Start retrieving tenant agent config")
- // Get tenant ID from authentication context
- tenantID := c.GetUint(types.TenantIDContextKey.String())
- if tenantID == 0 {
- logger.Error(ctx, "Tenant ID is empty")
- c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
+ tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
+ if tenant == nil {
+ logger.Error(ctx, "Tenant is empty")
+ c.Error(errors.NewBadRequestError("Tenant is empty"))
return
}
-
- tenant, err := h.service.GetTenantByID(ctx, tenantID)
- if err != nil {
- if appErr, ok := errors.IsAppError(err); ok {
- logger.Error(ctx, "Failed to retrieve tenant: application error", appErr)
- c.Error(appErr)
- } else {
- logger.ErrorWithFields(ctx, err, nil)
- c.Error(errors.NewInternalServerError("Failed to retrieve tenant").WithDetails(err.Error()))
- }
- return
- }
- // 定义所有可用工具及其描述(与 internal/agent/tools 注册的工具对应)
- availableTools := []gin.H{
- {"name": "thinking", "label": "思考", "description": "AI 进行深度思考和推理"},
- {"name": "todo_write", "label": "制定计划", "description": "为复杂任务制定执行计划"},
- {"name": "knowledge_search", "label": "知识搜索", "description": "在知识库中搜索相关信息"},
- {"name": "get_related_chunks", "label": "获取相关片段", "description": "查找相关的知识片段"},
- {"name": "query_knowledge_graph", "label": "查询知识图谱", "description": "从知识图谱中查询关系"},
- {"name": "get_document_info", "label": "获取文档信息", "description": "查看文档元数据"},
- {"name": "database_query", "label": "查询数据库", "description": "查询数据库中的信息"},
+ // 从 tools 包集中配置可用工具列表
+ availableTools := make([]gin.H, 0)
+ for _, t := range agenttools.AvailableToolDefinitions() {
+ availableTools = append(availableTools, gin.H{
+ "name": t.Name,
+ "label": t.Label,
+ "description": t.Description,
+ })
}
- // 定义可用的占位符列表
- availablePlaceholders := []gin.H{
- {"name": "knowledge_bases", "label": "知识库列表", "description": "自动格式化为表格形式的知识库列表,包含知识库名称、描述、文档数量、最近添加的文档等信息"},
+ // 从 agent 包获取占位符定义
+ availablePlaceholders := make([]gin.H, 0)
+ for _, p := range agent.AvailablePlaceholders() {
+ availablePlaceholders = append(availablePlaceholders, gin.H{
+ "name": p.Name,
+ "label": p.Label,
+ "description": p.Description,
+ })
}
if tenant.AgentConfig == nil {
// Return default config if not set
@@ -287,10 +277,10 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
- "max_iterations": 20,
- "reflection_enabled": false,
- "allowed_tools": []string{"thinking", "todo_write", "knowledge_search", "get_related_chunks", "query_knowledge_graph", "get_document_info", "database_query"},
- "temperature": 0.7,
+ "max_iterations": agent.DefaultAgentMaxIterations,
+ "reflection_enabled": agent.DefaultAgentReflectionEnabled,
+ "allowed_tools": agenttools.DefaultAllowedTools(),
+ "temperature": agent.DefaultAgentTemperature,
"thinking_model_id": "",
"rerank_model_id": "",
"system_prompt": agent.DefaultReActSystemPrompt,
@@ -307,7 +297,7 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
systemPrompt = agent.DefaultReActSystemPrompt
}
- logger.Infof(ctx, "Retrieved tenant agent config successfully, Tenant ID: %d", tenantID)
+ logger.Infof(ctx, "Retrieved tenant agent config successfully, Tenant ID: %d", tenant.ID)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
@@ -325,22 +315,11 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
})
}
-// UpdateTenantAgentConfig updates the agent configuration for a tenant
+// updateTenantAgentConfigInternal updates the agent configuration for a tenant
// This sets the global agent configuration for all sessions in this tenant
-// Tenant ID is obtained from the authentication context
-func (h *TenantHandler) UpdateTenantAgentConfig(c *gin.Context) {
+func (h *TenantHandler) updateTenantAgentConfigInternal(c *gin.Context) {
ctx := c.Request.Context()
-
logger.Info(ctx, "Start updating tenant agent config")
-
- // Get tenant ID from authentication context
- tenantID := c.GetUint(types.TenantIDContextKey.String())
- if tenantID == 0 {
- logger.Error(ctx, "Tenant ID is empty")
- c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
- return
- }
-
var req AgentConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error(ctx, "Failed to parse request parameters", err)
@@ -367,15 +346,10 @@ func (h *TenantHandler) UpdateTenantAgentConfig(c *gin.Context) {
}
// Get existing tenant
- tenant, err := h.service.GetTenantByID(ctx, tenantID)
- if err != nil {
- if appErr, ok := errors.IsAppError(err); ok {
- logger.Error(ctx, "Failed to retrieve tenant: application error", appErr)
- c.Error(appErr)
- } else {
- logger.ErrorWithFields(ctx, err, nil)
- c.Error(errors.NewInternalServerError("Failed to retrieve tenant").WithDetails(err.Error()))
- }
+ tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
+ if tenant.AgentConfig == nil {
+ logger.Error(ctx, "Tenant has no agent config")
+ c.Error(errors.NewBadRequestError("Tenant has no agent config"))
return
}
@@ -403,7 +377,7 @@ func (h *TenantHandler) UpdateTenantAgentConfig(c *gin.Context) {
return
}
- logger.Infof(ctx, "Tenant agent config updated successfully, Tenant ID: %d", tenantID)
+ logger.Infof(ctx, "Tenant agent config updated successfully, Tenant ID: %d", tenant.ID)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": updatedTenant.AgentConfig,
@@ -411,111 +385,74 @@ func (h *TenantHandler) UpdateTenantAgentConfig(c *gin.Context) {
})
}
-// GetTenantWebSearchConfig returns the web search configuration for a tenant
-// Tenant ID is obtained from the authentication context
-func (h *TenantHandler) GetTenantWebSearchConfig(c *gin.Context) {
+// GetTenantKV provides a generic KV-style getter for tenant-level configurations
+// Supported keys:
+// - "agent-config": returns tenant.AgentConfig with additional available_* fields
+// - "web-search-config": returns masked tenant.WebSearchConfig (API key masked)
+func (h *TenantHandler) GetTenantKV(c *gin.Context) {
ctx := c.Request.Context()
+ key := c.Param("key")
- logger.Info(ctx, "Start getting tenant web search config")
-
- // Get tenant ID from authentication context
- tenantID := c.GetUint(types.TenantIDContextKey.String())
- if tenantID == 0 {
- logger.Error(ctx, "Tenant ID is empty")
- c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
+ switch key {
+ case "agent-config":
+ h.GetTenantAgentConfig(c)
+ return
+ case "web-search-config":
+ h.GetTenantWebSearchConfig(c)
+ return
+ default:
+ logger.Info(ctx, "KV key not supported", "key", key)
+ c.Error(errors.NewBadRequestError("unsupported key"))
return
}
-
- // Get tenant
- tenant, err := h.service.GetTenantByID(ctx, tenantID)
- if err != nil {
- if appErr, ok := errors.IsAppError(err); ok {
- logger.Error(ctx, "Failed to retrieve tenant: application error", appErr)
- c.Error(appErr)
- } else {
- logger.ErrorWithFields(ctx, err, nil)
- c.Error(errors.NewInternalServerError("Failed to retrieve tenant").WithDetails(err.Error()))
- }
- return
- }
-
- // Hide API key in response
- var responseConfig *types.WebSearchConfig
- if tenant.WebSearchConfig != nil {
- responseConfig = &types.WebSearchConfig{
- Provider: tenant.WebSearchConfig.Provider,
- APIKey: "", // Hide API key
- MaxResults: tenant.WebSearchConfig.MaxResults,
- IncludeDate: tenant.WebSearchConfig.IncludeDate,
- CompressionMethod: tenant.WebSearchConfig.CompressionMethod,
- Blacklist: tenant.WebSearchConfig.Blacklist,
- EmbeddingModelID: tenant.WebSearchConfig.EmbeddingModelID,
- EmbeddingDimension: tenant.WebSearchConfig.EmbeddingDimension,
- RerankModelID: tenant.WebSearchConfig.RerankModelID,
- DocumentFragments: tenant.WebSearchConfig.DocumentFragments,
- }
- // If API key exists, show a masked version
- if tenant.WebSearchConfig.APIKey != "" {
- responseConfig.APIKey = "***" // Masked API key
- }
- }
-
- logger.Infof(ctx, "Tenant web search config retrieved successfully, Tenant ID: %d", tenantID)
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "data": responseConfig,
- })
}
-// UpdateTenantWebSearchConfig updates the web search configuration for a tenant
-// Tenant ID is obtained from the authentication context
-func (h *TenantHandler) UpdateTenantWebSearchConfig(c *gin.Context) {
+// UpdateTenantKV provides a generic KV-style updater for tenant-level configurations
+// Body is the JSON value to set for the key.
+func (h *TenantHandler) UpdateTenantKV(c *gin.Context) {
ctx := c.Request.Context()
+ key := c.Param("key")
- logger.Info(ctx, "Start updating tenant web search config")
-
- // Get tenant ID from authentication context
- tenantID := c.GetUint(types.TenantIDContextKey.String())
- if tenantID == 0 {
- logger.Error(ctx, "Tenant ID is empty")
- c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
+ switch key {
+ case "agent-config":
+ h.updateTenantAgentConfigInternal(c)
+ return
+ case "web-search-config":
+ h.updateTenantWebSearchConfigInternal(c)
+ return
+ default:
+ logger.Info(ctx, "KV key not supported", "key", key)
+ c.Error(errors.NewBadRequestError("unsupported key"))
return
}
+}
- var req types.WebSearchConfig
- if err := c.ShouldBindJSON(&req); err != nil {
+// updateTenantWebSearchConfigInternal updates tenant's web search config
+func (h *TenantHandler) updateTenantWebSearchConfigInternal(c *gin.Context) {
+ ctx := c.Request.Context()
+
+ // Bind directly into the strong typed struct
+ var cfg types.WebSearchConfig
+ if err := c.ShouldBindJSON(&cfg); err != nil {
logger.Error(ctx, "Failed to parse request parameters", err)
c.Error(errors.NewValidationError("Invalid request data").WithDetails(err.Error()))
return
}
// Validate configuration
- if req.MaxResults < 1 || req.MaxResults > 50 {
+ if cfg.MaxResults < 1 || cfg.MaxResults > 50 {
c.Error(errors.NewBadRequestError("max_results must be between 1 and 50"))
return
}
- // Get existing tenant
- tenant, err := h.service.GetTenantByID(ctx, tenantID)
- if err != nil {
- if appErr, ok := errors.IsAppError(err); ok {
- logger.Error(ctx, "Failed to retrieve tenant: application error", appErr)
- c.Error(appErr)
- } else {
- logger.ErrorWithFields(ctx, err, nil)
- c.Error(errors.NewInternalServerError("Failed to retrieve tenant").WithDetails(err.Error()))
- }
+ tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
+ if tenant == nil {
+ logger.Error(ctx, "Tenant is empty")
+ c.Error(errors.NewBadRequestError("Tenant is empty"))
return
}
- // Update web search configuration
- // If API key is "***", keep the existing API key
- if req.APIKey == "***" && tenant.WebSearchConfig != nil {
- req.APIKey = tenant.WebSearchConfig.APIKey
- }
-
- tenant.WebSearchConfig = &req
-
+ tenant.WebSearchConfig = &cfg
updatedTenant, err := h.service.UpdateTenant(ctx, tenant)
if err != nil {
if appErr, ok := errors.IsAppError(err); ok {
@@ -527,28 +464,28 @@ func (h *TenantHandler) UpdateTenantWebSearchConfig(c *gin.Context) {
}
return
}
-
- // Hide API key in response
- responseConfig := &types.WebSearchConfig{
- Provider: updatedTenant.WebSearchConfig.Provider,
- APIKey: "", // Hide API key
- MaxResults: updatedTenant.WebSearchConfig.MaxResults,
- IncludeDate: updatedTenant.WebSearchConfig.IncludeDate,
- CompressionMethod: updatedTenant.WebSearchConfig.CompressionMethod,
- Blacklist: updatedTenant.WebSearchConfig.Blacklist,
- EmbeddingModelID: updatedTenant.WebSearchConfig.EmbeddingModelID,
- EmbeddingDimension: updatedTenant.WebSearchConfig.EmbeddingDimension,
- RerankModelID: updatedTenant.WebSearchConfig.RerankModelID,
- DocumentFragments: updatedTenant.WebSearchConfig.DocumentFragments,
- }
- if updatedTenant.WebSearchConfig.APIKey != "" {
- responseConfig.APIKey = "***" // Masked API key
- }
-
- logger.Infof(ctx, "Tenant web search config updated successfully, Tenant ID: %d", tenantID)
c.JSON(http.StatusOK, gin.H{
"success": true,
- "data": responseConfig,
+ "data": updatedTenant.WebSearchConfig,
"message": "Web search configuration updated successfully",
})
}
+
+// GetTenantWebSearchConfig returns the web search configuration for a tenant
+func (h *TenantHandler) GetTenantWebSearchConfig(c *gin.Context) {
+ ctx := c.Request.Context()
+ logger.Info(ctx, "Start getting tenant web search config")
+ // Get tenant
+ tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
+ if tenant == nil {
+ logger.Error(ctx, "Tenant is empty")
+ c.Error(errors.NewBadRequestError("Tenant is empty"))
+ return
+ }
+
+ logger.Infof(ctx, "Tenant web search config retrieved successfully, Tenant ID: %d", tenant.ID)
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "data": tenant.WebSearchConfig,
+ })
+}
diff --git a/internal/router/router.go b/internal/router/router.go
index d9b6d2e1..fe6db0cf 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -222,14 +222,10 @@ func RegisterTenantRoutes(r *gin.RouterGroup, handler *handler.TenantHandler) {
tenantRoutes.DELETE("/:id", handler.DeleteTenant)
tenantRoutes.GET("", handler.ListTenants)
- // Agent configuration management (tenant-level)
+ // Generic KV configuration management (tenant-level)
// Tenant ID is obtained from authentication context
- tenantRoutes.GET("/agent-config", handler.GetTenantAgentConfig)
- tenantRoutes.PUT("/agent-config", handler.UpdateTenantAgentConfig)
- // Web search configuration management (tenant-level)
- // Tenant ID is obtained from authentication context
- tenantRoutes.GET("/web-search-config", handler.GetTenantWebSearchConfig)
- tenantRoutes.PUT("/web-search-config", handler.UpdateTenantWebSearchConfig)
+ tenantRoutes.GET("/kv/:key", handler.GetTenantKV)
+ tenantRoutes.PUT("/kv/:key", handler.UpdateTenantKV)
}
}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..c75d517a
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "WeKnora",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}