From b97e726237eb34c6e0fa9afffe66dc42b64c69d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:15:51 +0800 Subject: [PATCH] Add files via upload --- internal/handler/agent.go | 22 ++++++++++- internal/handler/config.go | 13 ++++++ internal/handler/hitl.go | 81 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/internal/handler/agent.go b/internal/handler/agent.go index 220ea95b..54972373 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -185,8 +185,9 @@ type AgentHandler struct { agentsMarkdownDir string // 多代理:Markdown 子 Agent 目录(绝对路径,空则不从磁盘合并) batchCronParser cron.Parser // hitlWhitelistSaver 侧栏「应用」HITL 时将会话增量白名单合并写入 config.yaml(可选) - hitlWhitelistSaver HitlToolWhitelistSaver - hitlStrategySaver HitlAuditStrategySaver + hitlWhitelistSaver HitlToolWhitelistSaver + hitlStrategySaver HitlAuditStrategySaver + hitlDefaultReviewerSaver HitlDefaultReviewerSaver auditLLM *openai.Client audit *audit.Service } @@ -288,6 +289,23 @@ func (h *AgentHandler) SetHitlToolWhitelistSaver(s HitlToolWhitelistSaver) { h.hitlWhitelistSaver = s } +// HitlDefaultReviewerSaver 持久化全局默认审批方到 config.yaml。 +type HitlDefaultReviewerSaver interface { + UpdateHitlDefaultReviewer(reviewer string) error +} + +// SetHitlDefaultReviewerSaver 设置 HITL 默认审批方落盘。 +func (h *AgentHandler) SetHitlDefaultReviewerSaver(s HitlDefaultReviewerSaver) { + h.hitlDefaultReviewerSaver = s +} + +func (h *AgentHandler) hitlEffectiveDefaultReviewer() string { + if h != nil && h.config != nil { + return normalizeHitlReviewer(h.config.Hitl.EffectiveDefaultReviewer()) + } + return "human" +} + // HITLNeedsToolApproval 供 C2 危险任务门控:与会话侧人机协同及免审批白名单判定一致。 func (h *AgentHandler) HITLNeedsToolApproval(conversationID, toolName string) bool { if h == nil || h.hitlManager == nil { diff --git a/internal/handler/config.go b/internal/handler/config.go index e2bb8ab7..aa387106 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -1802,10 +1802,23 @@ func updateHitlConfig(doc *yaml.Node, cfg config.HitlConfig) { hitlNode := ensureMap(root, "hitl") // flow 样式 [a, b, c] 单行展示,工具多时比块序列省行数 setFlowStringSliceInMap(hitlNode, "tool_whitelist", cfg.ToolWhitelist) + setStringInMap(hitlNode, "default_reviewer", cfg.EffectiveDefaultReviewer()) setStringInMap(hitlNode, "audit_agent_prompt", cfg.AuditAgentPrompt) setStringInMap(hitlNode, "audit_agent_prompt_review_edit", cfg.AuditAgentPromptReviewEdit) } +// UpdateHitlDefaultReviewer 更新全局默认审批方并写入 config.yaml。 +func (h *ConfigHandler) UpdateHitlDefaultReviewer(reviewer string) error { + h.mu.Lock() + defer h.mu.Unlock() + h.config.Hitl.DefaultReviewer = config.HitlConfig{DefaultReviewer: reviewer}.EffectiveDefaultReviewer() + if err := h.saveConfig(); err != nil { + return err + } + h.logger.Info("HITL 全局默认审批方已写入配置文件", zap.String("default_reviewer", h.config.Hitl.DefaultReviewer)) + return nil +} + // UpdateHitlAuditAgentStrategy 更新审批/审查编辑两套审计 Agent 提示词并写入 config.yaml。 func (h *ConfigHandler) UpdateHitlAuditAgentStrategy(approvalPrompt, reviewEditPrompt string) error { h.mu.Lock() diff --git a/internal/handler/hitl.go b/internal/handler/hitl.go index 21ddcc78..1a0f7146 100644 --- a/internal/handler/hitl.go +++ b/internal/handler/hitl.go @@ -389,6 +389,18 @@ func (m *HITLManager) LoadConversationConfig(conversationID string) (*HITLReques }, nil } +func (m *HITLManager) HasConversationConfig(conversationID string) (bool, error) { + if strings.TrimSpace(conversationID) == "" { + return false, nil + } + var one int + err := m.db.QueryRow(`SELECT 1 FROM hitl_conversation_configs WHERE conversation_id = ? LIMIT 1`, conversationID).Scan(&one) + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return err == nil, err +} + func (m *HITLManager) waitDecision(ctx context.Context, p *pendingInterrupt, timeout time.Duration) (hitlDecision, error) { defer func() { m.mu.Lock() @@ -427,14 +439,32 @@ func (h *AgentHandler) activateHITLForConversation(conversationID string, req *H return } if req == nil { - cfg, err := h.hitlManager.LoadConversationConfig(conversationID) + cfg, err := h.loadHITLConversationConfig(conversationID) if err == nil { req = cfg } } + if req != nil && strings.TrimSpace(req.Reviewer) == "" { + req.Reviewer = h.hitlEffectiveDefaultReviewer() + } h.hitlManager.ActivateConversation(conversationID, h.hitlRequestWithMergedConfigWhitelist(req)) } +func (h *AgentHandler) loadHITLConversationConfig(conversationID string) (*HITLRequest, error) { + cfg, err := h.hitlManager.LoadConversationConfig(conversationID) + if err != nil { + return nil, err + } + has, err := h.hitlManager.HasConversationConfig(conversationID) + if err != nil { + return nil, err + } + if !has { + cfg.Reviewer = h.hitlEffectiveDefaultReviewer() + } + return cfg, nil +} + func (h *AgentHandler) waitHITLApproval(runCtx context.Context, cancelRun context.CancelCauseFunc, conversationID, assistantMessageID, toolName, toolCallID string, payload map[string]interface{}, sendEventFunc func(eventType, message string, data interface{})) (*hitlDecision, error) { cfg, need := h.hitlManager.shouldInterrupt(conversationID, toolName) if !need { @@ -710,7 +740,7 @@ func (h *AgentHandler) GetHITLConversationConfig(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "conversationId is required"}) return } - cfg, err := h.hitlManager.LoadConversationConfig(conversationID) + cfg, err := h.loadHITLConversationConfig(conversationID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -729,6 +759,7 @@ func (h *AgentHandler) GetHITLConversationConfig(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "conversationId": conversationID, "hitl": cfg, + "defaultReviewer": h.hitlEffectiveDefaultReviewer(), "hitlGlobalToolWhitelist": h.hitlConfigGlobalToolWhitelist(), }) } @@ -741,6 +772,9 @@ func (h *AgentHandler) UpsertHITLConversationConfig(c *gin.Context) { } req.Mode = normalizeHitlMode(req.Mode) req.Reviewer = normalizeHitlReviewer(req.Reviewer) + if strings.TrimSpace(req.Reviewer) == "" { + req.Reviewer = h.hitlEffectiveDefaultReviewer() + } if err := h.hitlManager.SaveConversationConfig(req.ConversationID, &req.HITLRequest); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -769,7 +803,48 @@ type setHitlGlobalWhitelistReq struct { // GetHITLGlobalToolWhitelist 返回 config.yaml 中的全局免审批工具白名单。 func (h *AgentHandler) GetHITLGlobalToolWhitelist(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ - "toolWhitelist": h.hitlConfigGlobalToolWhitelist(), + "toolWhitelist": h.hitlConfigGlobalToolWhitelist(), + "defaultReviewer": h.hitlEffectiveDefaultReviewer(), + }) +} + +type setHitlDefaultReviewerReq struct { + Reviewer string `json:"reviewer"` +} + +// GetHITLDefaultReviewer 返回 config.yaml 中的全局默认审批方。 +func (h *AgentHandler) GetHITLDefaultReviewer(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "defaultReviewer": h.hitlEffectiveDefaultReviewer(), + }) +} + +// UpdateHITLDefaultReviewer 将全局默认审批方写入 config.yaml(未选会话时切换审批方)。 +func (h *AgentHandler) UpdateHITLDefaultReviewer(c *gin.Context) { + if h.hitlDefaultReviewerSaver == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "HITL 配置持久化不可用"}) + return + } + var req setHitlDefaultReviewerReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + reviewer := normalizeHitlReviewer(req.Reviewer) + if err := h.hitlDefaultReviewerSaver.UpdateHitlDefaultReviewer(reviewer); err != nil { + h.logger.Warn("写入 HITL 默认审批方到 config.yaml 失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if h.config != nil { + h.config.Hitl.DefaultReviewer = reviewer + } + if h.audit != nil { + h.audit.RecordOK(c, "hitl", "default_reviewer_update", "HITL 全局默认审批方更新", "hitl_config", "default_reviewer", nil) + } + c.JSON(http.StatusOK, gin.H{ + "ok": true, + "defaultReviewer": reviewer, }) }