WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,777 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/application/service"
|
||||
"github.com/Tencent/WeKnora/internal/errors"
|
||||
"github.com/Tencent/WeKnora/internal/im"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
secutils "github.com/Tencent/WeKnora/internal/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// sandboxConfigLookup is the existence check an agent's sandbox selection needs.
|
||||
// Narrower than the full config service so this handler cannot grow a dependency
|
||||
// on config mutation.
|
||||
type sandboxConfigLookup interface {
|
||||
Get(ctx context.Context, tenantID uint64, id string) (*types.TenantSandboxConfigEntity, error)
|
||||
}
|
||||
|
||||
// CustomAgentHandler defines the HTTP handler for custom agent operations
|
||||
type CustomAgentHandler struct {
|
||||
service interfaces.CustomAgentService
|
||||
imService *im.Service
|
||||
disabledRepo interfaces.TenantDisabledSharedAgentRepository
|
||||
// userService 仅用于 list 接口批量回填 creator_name,作用见
|
||||
// KnowledgeBaseHandler.userService。
|
||||
userService interfaces.UserService
|
||||
// sandboxConfigs validates an agent's sandbox backend selection. Optional —
|
||||
// nil in partially-wired unit tests, where the selection is left unchecked.
|
||||
sandboxConfigs sandboxConfigLookup
|
||||
}
|
||||
|
||||
// NewCustomAgentHandler creates a new custom agent handler instance
|
||||
func NewCustomAgentHandler(
|
||||
service interfaces.CustomAgentService,
|
||||
imService *im.Service,
|
||||
disabledRepo interfaces.TenantDisabledSharedAgentRepository,
|
||||
userService interfaces.UserService,
|
||||
sandboxConfigs *service.TenantSandboxConfigService,
|
||||
) *CustomAgentHandler {
|
||||
return &CustomAgentHandler{
|
||||
service: service,
|
||||
imService: imService,
|
||||
disabledRepo: disabledRepo,
|
||||
userService: userService,
|
||||
sandboxConfigs: sandboxConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAgentRequest defines the request body for creating an agent
|
||||
type CreateAgentRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Avatar string `json:"avatar"`
|
||||
Config types.CustomAgentConfig `json:"config"`
|
||||
}
|
||||
|
||||
// UpdateAgentRequest defines the request body for updating an agent
|
||||
type UpdateAgentRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Avatar string `json:"avatar"`
|
||||
Config types.CustomAgentConfig `json:"config"`
|
||||
}
|
||||
|
||||
// CreateAgent godoc
|
||||
// @Summary 创建智能体
|
||||
// @Description 创建新的自定义智能体
|
||||
// @Tags 智能体
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateAgentRequest true "智能体信息"
|
||||
// @Success 201 {object} map[string]interface{} "创建的智能体"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /agents [post]
|
||||
func (h *CustomAgentHandler) CreateAgent(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Info(ctx, "Start creating custom agent")
|
||||
|
||||
// Parse request body
|
||||
var req CreateAgentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error(ctx, "Failed to parse request parameters", err)
|
||||
c.Error(errors.NewBadRequestError("Invalid request parameters").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := authorizeAgentKnowledgeScope(ctx, req.Config); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
if err := h.validateAgentSandboxConfig(ctx, req.Config); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build agent object
|
||||
agent := &types.CustomAgent{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Avatar: req.Avatar,
|
||||
Config: req.Config,
|
||||
Visibility: "private",
|
||||
Enabled: true,
|
||||
}
|
||||
agent.EnsureDefaults()
|
||||
if err := agent.Config.QuestionSuggestions.Validate(); err != nil {
|
||||
c.Error(errors.NewBadRequestError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Creating custom agent, name: %s, agent_mode: %s",
|
||||
secutils.SanitizeForLog(req.Name), req.Config.AgentMode)
|
||||
|
||||
// Create agent using the service
|
||||
createdAgent, err := h.service.CreateAgent(ctx, agent)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
c.Error(appErr)
|
||||
return
|
||||
}
|
||||
if err == service.ErrAgentNameRequired {
|
||||
c.Error(errors.NewBadRequestError(err.Error()))
|
||||
return
|
||||
}
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Custom agent created successfully, ID: %s, name: %s",
|
||||
secutils.SanitizeForLog(createdAgent.ID), secutils.SanitizeForLog(createdAgent.Name))
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"success": true,
|
||||
"data": createdAgent,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CustomAgentHandler) DraftAgent(c *gin.Context) {
|
||||
var request struct {
|
||||
ModelID string `json:"model_id" binding:"required"`
|
||||
Requirement string `json:"requirement" binding:"required"`
|
||||
}
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 32768)
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.Error(errors.NewBadRequestError("请填写 Agent 用途并选择对话模型"))
|
||||
return
|
||||
}
|
||||
drafter, ok := h.service.(interface {
|
||||
DraftAgent(context.Context, string, string) (*types.CustomAgent, error)
|
||||
})
|
||||
if !ok {
|
||||
c.Error(errors.NewServiceUnavailableError("Agent 草稿生成服务暂不可用"))
|
||||
return
|
||||
}
|
||||
draft, err := drafter.DraftAgent(c.Request.Context(), request.ModelID, request.Requirement)
|
||||
if err != nil {
|
||||
if appErr, ok := errors.IsAppError(err); ok {
|
||||
c.Error(appErr)
|
||||
} else {
|
||||
logger.ErrorWithFields(c.Request.Context(), err, map[string]interface{}{"operation": "agent_draft_generation"})
|
||||
c.Error(errors.NewServiceUnavailableError("AI 生成 Agent 草稿失败,请重试或改为手动创建"))
|
||||
}
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": draft})
|
||||
}
|
||||
|
||||
// GetAgent godoc
|
||||
// @Summary 获取智能体详情
|
||||
// @Description 根据ID获取智能体详情
|
||||
// @Tags 智能体
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "智能体ID"
|
||||
// @Success 200 {object} map[string]interface{} "智能体详情"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 404 {object} errors.AppError "智能体不存在"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /agents/{id} [get]
|
||||
func (h *CustomAgentHandler) GetAgent(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Get agent ID from URL parameter
|
||||
id := secutils.SanitizeForLog(c.Param("id"))
|
||||
if id == "" {
|
||||
logger.Error(ctx, "Agent ID is empty")
|
||||
c.Error(errors.NewBadRequestError("Agent ID cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
agent, err := h.service.GetAgentByID(ctx, id)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"agent_id": id,
|
||||
})
|
||||
if err == service.ErrAgentNotFound {
|
||||
c.Error(errors.NewNotFoundError("Agent not found"))
|
||||
return
|
||||
}
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
c.Error(appErr)
|
||||
return
|
||||
}
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": agent,
|
||||
})
|
||||
}
|
||||
|
||||
// ListAgents godoc
|
||||
// @Summary 获取智能体列表
|
||||
// @Description 获取当前空间的所有智能体(包括内置智能体)
|
||||
// @Tags 智能体
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "智能体列表"
|
||||
// @Failure 500 {object} errors.AppError "服务器错误"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /agents [get]
|
||||
func (h *CustomAgentHandler) ListAgents(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Get all agents for this tenant
|
||||
agents, err := h.service.ListAgents(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Optional creator filter — see the matching block in
|
||||
// KnowledgeBaseHandler.ListKnowledgeBases for rationale. Built-in
|
||||
// agents (IsBuiltin=true, CreatedBy="") are tenant-level fixtures
|
||||
// rather than user creations; we always keep them regardless of the
|
||||
// filter so the conversation dropdown never silently loses
|
||||
// quick-answer / smart-reasoning when a user picks "Created by me".
|
||||
creatorFilter := strings.ToLower(strings.TrimSpace(c.Query("creator")))
|
||||
if creatorFilter == "mine" || creatorFilter == "others" {
|
||||
callerUserID, _ := c.Get(types.UserIDContextKey.String())
|
||||
callerUserIDStr, _ := callerUserID.(string)
|
||||
filtered := make([]*types.CustomAgent, 0, len(agents))
|
||||
for _, ag := range agents {
|
||||
if ag.IsBuiltin {
|
||||
filtered = append(filtered, ag)
|
||||
continue
|
||||
}
|
||||
if ag.CreatedBy == "" {
|
||||
continue
|
||||
}
|
||||
if creatorFilter == "mine" && ag.CreatedBy == callerUserIDStr {
|
||||
filtered = append(filtered, ag)
|
||||
} else if creatorFilter == "others" && ag.CreatedBy != callerUserIDStr {
|
||||
filtered = append(filtered, ag)
|
||||
}
|
||||
}
|
||||
agents = filtered
|
||||
}
|
||||
|
||||
// Per-tenant "disabled by me" for own agents (only affects this tenant's conversation dropdown)
|
||||
tenantIDVal, exists := c.Get(types.TenantIDContextKey.String())
|
||||
if !exists {
|
||||
logger.Error(ctx, "Workspace ID not found in context")
|
||||
c.Error(errors.NewUnauthorizedError("Missing workspace context"))
|
||||
return
|
||||
}
|
||||
tenantID, ok := tenantIDVal.(uint64)
|
||||
if !ok {
|
||||
logger.Errorf(ctx, "Tenant ID has unexpected type %T in context", tenantIDVal)
|
||||
c.Error(errors.NewInternalServerError("Invalid workspace context type"))
|
||||
return
|
||||
}
|
||||
disabledOwnIDs, err := h.disabledRepo.ListDisabledOwnAgentIDs(ctx, tenantID)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"tenant_id": tenantID,
|
||||
})
|
||||
c.Error(errors.NewInternalServerError("Failed to list disabled agent IDs: " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 批量回填 creator_name,作用同 KB 列表:让前端能区分「我创建」与「同空间其他成员」。
|
||||
// 内建 agent(IsBuiltin=true, CreatedBy="")不会有 creator_name,前端按 builtin
|
||||
// 分支单独渲染。
|
||||
enrichAgentCreatorNames(ctx, h.userService, agents)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": agents,
|
||||
"disabled_own_agent_ids": disabledOwnIDs,
|
||||
})
|
||||
}
|
||||
|
||||
// enrichAgentCreatorNames 批量把 agent.CreatedBy 解析成展示名。失败吞掉,
|
||||
// 不影响列表本身可用。与 enrichKBCreatorNames 行为对齐。
|
||||
func enrichAgentCreatorNames(ctx context.Context, userSvc interfaces.UserService, agents []*types.CustomAgent) {
|
||||
if userSvc == nil || len(agents) == 0 {
|
||||
return
|
||||
}
|
||||
idSet := make(map[string]struct{}, len(agents))
|
||||
for _, ag := range agents {
|
||||
if ag.IsBuiltin || ag.CreatedBy == "" {
|
||||
continue
|
||||
}
|
||||
idSet[ag.CreatedBy] = struct{}{}
|
||||
}
|
||||
if len(idSet) == 0 {
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(idSet))
|
||||
for id := range idSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
users, err := userSvc.GetUsersByIDs(ctx, ids)
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to resolve agent creator names: %v", err)
|
||||
return
|
||||
}
|
||||
for _, ag := range agents {
|
||||
if ag.IsBuiltin || ag.CreatedBy == "" {
|
||||
continue
|
||||
}
|
||||
u, ok := users[ag.CreatedBy]
|
||||
if !ok || u == nil {
|
||||
continue
|
||||
}
|
||||
ag.CreatorName = pickUserDisplayName(u)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateAgent godoc
|
||||
// @Summary 更新智能体
|
||||
// @Description 更新智能体的名称、描述和配置
|
||||
// @Tags 智能体
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "智能体ID"
|
||||
// @Param request body UpdateAgentRequest true "更新请求"
|
||||
// @Success 200 {object} map[string]interface{} "更新后的智能体"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 403 {object} errors.AppError "无法修改内置智能体"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /agents/{id} [put]
|
||||
func (h *CustomAgentHandler) UpdateAgent(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Info(ctx, "Start updating custom agent")
|
||||
|
||||
// Get agent ID from URL parameter
|
||||
id := secutils.SanitizeForLog(c.Param("id"))
|
||||
if id == "" {
|
||||
logger.Error(ctx, "Agent ID is empty")
|
||||
c.Error(errors.NewBadRequestError("Agent ID cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req UpdateAgentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error(ctx, "Failed to parse request parameters", err)
|
||||
c.Error(errors.NewBadRequestError("Invalid request parameters").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := authorizeAgentKnowledgeScope(ctx, req.Config); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
if err := h.validateAgentSandboxConfig(ctx, req.Config); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build agent object
|
||||
agent := &types.CustomAgent{
|
||||
ID: id,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Avatar: req.Avatar,
|
||||
Config: req.Config,
|
||||
}
|
||||
agent.EnsureDefaults()
|
||||
if err := agent.Config.QuestionSuggestions.Validate(); err != nil {
|
||||
c.Error(errors.NewBadRequestError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Updating custom agent, ID: %s, name: %s",
|
||||
secutils.SanitizeForLog(id), secutils.SanitizeForLog(req.Name))
|
||||
|
||||
// Update the agent
|
||||
updatedAgent, err := h.service.UpdateAgent(ctx, agent)
|
||||
if err != nil {
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
c.Error(appErr)
|
||||
return
|
||||
}
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"agent_id": id,
|
||||
})
|
||||
switch err {
|
||||
case service.ErrAgentNotFound:
|
||||
c.Error(errors.NewNotFoundError("Agent not found"))
|
||||
case service.ErrCannotModifyBuiltin:
|
||||
c.Error(errors.NewForbiddenError("Cannot modify built-in agent"))
|
||||
case service.ErrAgentNameRequired:
|
||||
c.Error(errors.NewBadRequestError(err.Error()))
|
||||
default:
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Custom agent updated successfully, ID: %s", secutils.SanitizeForLog(id))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": updatedAgent,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAgent godoc
|
||||
// @Summary 删除智能体
|
||||
// @Description 删除指定的智能体
|
||||
// @Tags 智能体
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "智能体ID"
|
||||
// @Success 200 {object} map[string]interface{} "删除成功"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 403 {object} errors.AppError "无法删除内置智能体"
|
||||
// @Failure 404 {object} errors.AppError "智能体不存在"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /agents/{id} [delete]
|
||||
func (h *CustomAgentHandler) DeleteAgent(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Info(ctx, "Start deleting custom agent")
|
||||
|
||||
// Get agent ID from URL parameter
|
||||
id := secutils.SanitizeForLog(c.Param("id"))
|
||||
if id == "" {
|
||||
logger.Error(ctx, "Agent ID is empty")
|
||||
c.Error(errors.NewBadRequestError("Agent ID cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Deleting custom agent, ID: %s", secutils.SanitizeForLog(id))
|
||||
|
||||
_, ok := types.TenantIDFromContext(ctx)
|
||||
if !ok {
|
||||
c.Error(errors.NewUnauthorizedError("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
if guard, ok := h.service.(interface {
|
||||
CheckDigitalHumanAgentDeletion(context.Context, string) error
|
||||
}); ok {
|
||||
if err := guard.CheckDigitalHumanAgentDeletion(ctx, id); err != nil {
|
||||
if err == types.ErrDigitalHumanPrimaryAgent {
|
||||
c.Error(errors.NewConflictError(err.Error()))
|
||||
} else {
|
||||
c.Error(errors.NewInternalServerError("无法检查数字人关联,请稍后重试"))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// Durable channel and agent deletion commit together. Only then stop runtime
|
||||
// adapters, so a failed deletion never disconnects a still-existing agent.
|
||||
var channelIDs []string
|
||||
var err error
|
||||
if deleter, ok := h.service.(interface {
|
||||
DeleteAgentWithIMChannels(context.Context, string) ([]string, error)
|
||||
}); ok {
|
||||
channelIDs, err = deleter.DeleteAgentWithIMChannels(ctx, id)
|
||||
} else {
|
||||
err = h.service.DeleteAgent(ctx, id)
|
||||
}
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"agent_id": id,
|
||||
})
|
||||
switch err {
|
||||
case service.ErrAgentNotFound:
|
||||
c.Error(errors.NewNotFoundError("Agent not found"))
|
||||
case service.ErrCannotDeleteBuiltin:
|
||||
c.Error(errors.NewForbiddenError("Cannot delete built-in agent"))
|
||||
case types.ErrDigitalHumanPrimaryAgent:
|
||||
c.Error(errors.NewConflictError(err.Error()))
|
||||
default:
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
if h.imService != nil {
|
||||
h.imService.InvalidateDeletedChannels(channelIDs)
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Custom agent deleted successfully, ID: %s", secutils.SanitizeForLog(id))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Agent deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// CopyAgent godoc
|
||||
// @Summary 复制智能体
|
||||
// @Description 复制指定的智能体
|
||||
// @Tags 智能体
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "智能体ID"
|
||||
// @Success 201 {object} map[string]interface{} "复制成功"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 404 {object} errors.AppError "智能体不存在"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /agents/{id}/copy [post]
|
||||
func (h *CustomAgentHandler) CopyAgent(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Info(ctx, "Start copying custom agent")
|
||||
|
||||
// Get agent ID from URL parameter
|
||||
id := secutils.SanitizeForLog(c.Param("id"))
|
||||
if id == "" {
|
||||
logger.Error(ctx, "Agent ID is empty")
|
||||
c.Error(errors.NewBadRequestError("Agent ID cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Copying custom agent, ID: %s", secutils.SanitizeForLog(id))
|
||||
sourceAgent, err := h.service.GetAgentByID(ctx, id)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"agent_id": id,
|
||||
})
|
||||
switch err {
|
||||
case service.ErrAgentNotFound:
|
||||
c.Error(errors.NewNotFoundError("Agent not found"))
|
||||
default:
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := authorizeAgentKnowledgeScope(ctx, sourceAgent.Config); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy the agent
|
||||
copiedAgent, err := h.service.CopyAgent(ctx, id)
|
||||
if err != nil {
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
c.Error(appErr)
|
||||
return
|
||||
}
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"agent_id": id,
|
||||
})
|
||||
switch err {
|
||||
case service.ErrAgentNotFound:
|
||||
c.Error(errors.NewNotFoundError("Agent not found"))
|
||||
default:
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Custom agent copied successfully, source ID: %s, new ID: %s",
|
||||
secutils.SanitizeForLog(id), secutils.SanitizeForLog(copiedAgent.ID))
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"success": true,
|
||||
"data": copiedAgent,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPlaceholders godoc
|
||||
// @Summary 获取占位符定义
|
||||
// @Description 获取所有可用的提示词占位符定义,按字段类型分组
|
||||
// @Tags 智能体
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "占位符定义"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /agents/placeholders [get]
|
||||
func (h *CustomAgentHandler) GetPlaceholders(c *gin.Context) {
|
||||
// Return all placeholder definitions grouped by field type
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"all": types.AllPlaceholders(),
|
||||
"system_prompt": types.PlaceholdersByField(types.PromptFieldSystemPrompt),
|
||||
"agent_system_prompt": types.PlaceholdersByField(types.PromptFieldAgentSystemPrompt),
|
||||
"context_template": types.PlaceholdersByField(types.PromptFieldContextTemplate),
|
||||
"rewrite_system_prompt": types.PlaceholdersByField(types.PromptFieldRewriteSystemPrompt),
|
||||
"rewrite_prompt": types.PlaceholdersByField(types.PromptFieldRewritePrompt),
|
||||
"fallback_prompt": types.PlaceholdersByField(types.PromptFieldFallbackPrompt),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetAgentTypePresets godoc
|
||||
// @Summary 获取智能体类型预设列表
|
||||
// @Description 返回所有 smart-reasoning 下可用的智能体类型预设(RAG/Wiki/Hybrid/Custom),用于编辑器自动填充系统提示词、工具和 KB 兼容性
|
||||
// @Tags 智能体
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "预设列表"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /agents/type-presets [get]
|
||||
func (h *CustomAgentHandler) GetAgentTypePresets(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
presets := types.ListAgentTypePresetsWithContext(ctx)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": presets,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSuggestedQuestions godoc
|
||||
// @Summary 获取推荐问题
|
||||
// @Description 基于智能体关联的知识库,返回推荐问题供用户快捷提问
|
||||
// @Tags 智能体
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "智能体ID"
|
||||
// @Param knowledge_base_ids query string false "知识库ID列表(逗号分隔),覆盖智能体默认配置"
|
||||
// @Param knowledge_ids query string false "知识ID列表(逗号分隔),限定到具体文档"
|
||||
// @Param tag_scopes query string false "带知识库归属的标签范围(JSON)"
|
||||
// @Param limit query int false "返回数量上限(未传时使用智能体配置的开场问题数量,最大30)"
|
||||
// @Success 200 {object} map[string]interface{} "推荐问题列表"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 404 {object} errors.AppError "智能体不存在"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /agents/{id}/suggested-questions [get]
|
||||
func (h *CustomAgentHandler) GetSuggestedQuestions(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Get agent ID from URL parameter
|
||||
id := secutils.SanitizeForLog(c.Param("id"))
|
||||
if id == "" {
|
||||
logger.Error(ctx, "Agent ID is empty")
|
||||
c.Error(errors.NewBadRequestError("Agent ID cannot be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse optional query parameters
|
||||
var kbIDs []string
|
||||
if kbIDsStr := strings.TrimSpace(c.Query("knowledge_base_ids")); kbIDsStr != "" {
|
||||
for _, id := range strings.Split(kbIDsStr, ",") {
|
||||
if trimmed := strings.TrimSpace(id); trimmed != "" {
|
||||
kbIDs = append(kbIDs, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var knowledgeIDs []string
|
||||
if kIDsStr := strings.TrimSpace(c.Query("knowledge_ids")); kIDsStr != "" {
|
||||
for _, id := range strings.Split(kIDsStr, ",") {
|
||||
if trimmed := strings.TrimSpace(id); trimmed != "" {
|
||||
knowledgeIDs = append(knowledgeIDs, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tagScopes []types.TagScope
|
||||
if raw := strings.TrimSpace(c.Query("tag_scopes")); raw != "" {
|
||||
if err := json.Unmarshal([]byte(raw), &tagScopes); err != nil {
|
||||
c.Error(errors.NewBadRequestError("tag_scopes must be valid JSON"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// limit == 0 signals "unspecified" so the service falls back to the agent's
|
||||
// configured starter count. A provided value is passed through unchanged and
|
||||
// bounded by the service's safety cap.
|
||||
limit := 0
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Getting suggested questions for agent %s, kbIDs: %v, tagScopes: %d, limit: %d",
|
||||
secutils.SanitizeForLog(id), kbIDs, len(tagScopes), limit)
|
||||
|
||||
questions, err := h.service.GetSuggestedQuestions(ctx, id, kbIDs, knowledgeIDs, tagScopes, limit)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"agent_id": id,
|
||||
})
|
||||
if err == service.ErrAgentNotFound {
|
||||
c.Error(errors.NewNotFoundError("Agent not found"))
|
||||
return
|
||||
}
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
c.Error(appErr)
|
||||
return
|
||||
}
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"questions": questions,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// validateAgentSandboxConfig rejects a selection the workspace does not have.
|
||||
//
|
||||
// Checking at save time is what makes the mistake fixable: a dangling reference
|
||||
// only fails when the agent next runs a skill, mid-conversation, as an opaque
|
||||
// resolution error with no hint about which agent to edit.
|
||||
func (h *CustomAgentHandler) validateAgentSandboxConfig(
|
||||
ctx context.Context, cfg types.CustomAgentConfig,
|
||||
) error {
|
||||
configID := strings.TrimSpace(cfg.SandboxConfigID)
|
||||
if configID == "" || h.sandboxConfigs == nil {
|
||||
// Empty means the deployment-wide default, which always exists.
|
||||
return nil
|
||||
}
|
||||
tenantID, ok := types.TenantIDFromContext(ctx)
|
||||
if !ok {
|
||||
return errors.NewUnauthorizedError("Missing workspace context")
|
||||
}
|
||||
stored, err := h.sandboxConfigs.Get(ctx, tenantID, configID)
|
||||
if err != nil {
|
||||
return errors.NewInternalServerError("Failed to verify sandbox config").
|
||||
WithDetails(err.Error())
|
||||
}
|
||||
if stored == nil {
|
||||
return errors.NewBadRequestError("所选沙箱后端配置不存在,请重新选择")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func authorizeAgentKnowledgeScope(ctx context.Context, cfg types.CustomAgentConfig) error {
|
||||
scope, ok := types.TenantAPIKeyScopeFromContext(ctx)
|
||||
if !ok || !scope.IsKnowledgeBaseRestricted() {
|
||||
return nil
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.KBSelectionMode)) {
|
||||
case "none":
|
||||
return nil
|
||||
case "all":
|
||||
return errors.NewForbiddenError("API key scope does not allow agents that use all knowledge bases")
|
||||
case "selected":
|
||||
return types.AuthorizeTenantAPIKeyKnowledgeBases(ctx, cfg.KnowledgeBases...)
|
||||
default:
|
||||
if len(cfg.KnowledgeBases) == 0 {
|
||||
return nil
|
||||
}
|
||||
return types.AuthorizeTenantAPIKeyKnowledgeBases(ctx, cfg.KnowledgeBases...)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user