Add files via upload

This commit is contained in:
公明
2026-07-09 14:38:09 +08:00
committed by GitHub
parent c8fece70ef
commit 64c3f28780
4 changed files with 186 additions and 55 deletions
+48 -20
View File
@@ -20,10 +20,10 @@ import (
"cyberstrike-ai/internal/audit"
"cyberstrike-ai/internal/config"
"cyberstrike-ai/internal/database"
"cyberstrike-ai/internal/reasoning"
"cyberstrike-ai/internal/mcp/builtin"
"cyberstrike-ai/internal/multiagent"
"cyberstrike-ai/internal/openai"
"cyberstrike-ai/internal/reasoning"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
@@ -188,8 +188,8 @@ type AgentHandler struct {
hitlWhitelistSaver HitlToolWhitelistSaver
hitlStrategySaver HitlAuditStrategySaver
hitlDefaultReviewerSaver HitlDefaultReviewerSaver
auditLLM *openai.Client
audit *audit.Service
auditLLM *openai.Client
audit *audit.Service
}
// SetAudit wires platform audit logging.
@@ -332,13 +332,13 @@ type ChatReasoningRequest struct {
// ChatRequest 聊天请求
type ChatRequest struct {
Message string `json:"message" binding:"required"`
ConversationID string `json:"conversationId,omitempty"`
ProjectID string `json:"projectId,omitempty"` // 新对话绑定的项目(可选;未指定时可用 config.project.default_project_id
Role string `json:"role,omitempty"` // 角色名称
Attachments []ChatAttachment `json:"attachments,omitempty"`
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
Hitl *HITLRequest `json:"hitl,omitempty"`
Message string `json:"message" binding:"required"`
ConversationID string `json:"conversationId,omitempty"`
ProjectID string `json:"projectId,omitempty"` // 新对话绑定的项目(可选;未指定时可用 config.project.default_project_id
Role string `json:"role,omitempty"` // 角色名称
Attachments []ChatAttachment `json:"attachments,omitempty"`
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
Hitl *HITLRequest `json:"hitl,omitempty"`
Reasoning *ChatReasoningRequest `json:"reasoning,omitempty"`
// Orchestration 仅对 /api/multi-agent、/api/multi-agent/streamdeep | plan_execute | supervisor;空则等同 deep。机器人/批量等无请求体时由服务端默认 deep。/api/eino-agent* 不使用此字段。
Orchestration string `json:"orchestration,omitempty"`
@@ -988,11 +988,20 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
}
}
// 流式:写 HTTP SSE;非流式(机器人等):镜像到 taskEventBus 供 Web 订阅
if sendEventFunc != nil {
sendEventFunc(eventType, message, data)
} else {
h.publishProgressToTaskEventBus(conversationID, eventType, message, data)
// 工具输出片段不在详情区实时展示;完整结果由 tool_result 落库后按需拉取。
if eventType == "tool_result_delta" {
return
}
deferToolProgressSend := eventType == "tool_call" || eventType == "tool_result"
// 流式:写 HTTP SSE;非流式(机器人等):镜像到 taskEventBus 供 Web 订阅。
// 工具事件需先落库拿 processDetailId,再向前端发送摘要,避免大 payload 默认进入浏览器。
if !deferToolProgressSend {
if sendEventFunc != nil {
sendEventFunc(eventType, message, data)
} else {
h.publishProgressToTaskEventBus(conversationID, eventType, message, data)
}
}
// 保存tool_call事件中的参数
@@ -1360,9 +1369,28 @@ func (h *AgentHandler) createProgressCallback(runCtx context.Context, cancelRun
// 在关键过程事件落库前,先把「规划中」与聚合中的 thinking / reasoning_chain 流落库
flushResponsePlan()
flushThinkingStreams()
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, eventType, message, data); err != nil {
processDetailID, err := h.db.AddProcessDetailWithID(assistantMessageID, conversationID, eventType, message, data)
if err != nil {
h.logger.Warn("保存过程详情失败", zap.Error(err), zap.String("eventType", eventType))
}
if deferToolProgressSend {
clientData := summarizeProcessDetailData(eventType, data)
if m, ok := clientData.(map[string]interface{}); ok {
m["processDetailId"] = processDetailID
}
if sendEventFunc != nil {
sendEventFunc(eventType, message, clientData)
} else {
h.publishProgressToTaskEventBus(conversationID, eventType, message, clientData)
}
}
} else if deferToolProgressSend {
clientData := summarizeProcessDetailData(eventType, data)
if sendEventFunc != nil {
sendEventFunc(eventType, message, clientData)
} else {
h.publishProgressToTaskEventBus(conversationID, eventType, message, clientData)
}
}
}
}
@@ -1495,10 +1523,10 @@ func (h *AgentHandler) CancelAgentLoop(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"status": "cancelling",
"conversationId": req.ConversationID,
"message": msg,
"continueAfter": false,
"status": "cancelling",
"conversationId": req.ConversationID,
"message": msg,
"continueAfter": false,
"interruptWithNote": false,
})
}
+91 -29
View File
@@ -176,10 +176,16 @@ func (h *ConversationHandler) GetConversation(c *gin.Context) {
c.JSON(http.StatusOK, conv)
}
const (
defaultProcessDetailsPageLimit = 50
maxProcessDetailsPageLimit = 500
)
// GetMessageProcessDetails 获取指定消息的过程详情(按需加载)
// 查询参数:
// - summary=1:仅返回摘要(total / iterationCount / maxIteration
// - limit + offset:分页返回 processDetails(未指定 limit 时保持全量兼容
// - limit + offset:分页返回 processDetails(未指定 limit 时默认 50 条
// - full=1:显式返回全量 processDetails(用于导出/兼容旧集成,不建议 UI 展开时使用)
func (h *ConversationHandler) GetMessageProcessDetails(c *gin.Context) {
messageID := c.Param("id")
if messageID == "" {
@@ -199,52 +205,84 @@ func (h *ConversationHandler) GetMessageProcessDetails(c *gin.Context) {
return
}
limitStr := strings.TrimSpace(c.Query("limit"))
if limitStr != "" {
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid limit"})
return
}
if limit > 500 {
limit = 500
}
offset, _ := strconv.Atoi(strings.TrimSpace(c.Query("offset")))
if offset < 0 {
offset = 0
}
details, total, err := h.db.GetProcessDetailsPage(messageID, limit, offset)
fullStr := strings.TrimSpace(c.Query("full"))
if fullStr == "1" || strings.EqualFold(fullStr, "true") || strings.EqualFold(fullStr, "yes") {
details, err := h.db.GetProcessDetails(messageID)
if err != nil {
h.logger.Error("分页获取过程详情失败", zap.Error(err))
h.logger.Error("获取过程详情失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
details = database.DedupeConsecutiveProcessDetails(details)
out := processDetailsToJSON(h.logger, details)
out := processDetailsToJSON(h.logger, details, true)
c.JSON(http.StatusOK, gin.H{
"processDetails": out,
"total": total,
"offset": offset,
"limit": limit,
"hasMore": offset+len(out) < total,
"total": len(out),
"offset": 0,
"limit": len(out),
"hasMore": false,
})
return
}
details, err := h.db.GetProcessDetails(messageID)
limitStr := strings.TrimSpace(c.Query("limit"))
limit := defaultProcessDetailsPageLimit
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil || parsedLimit <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid limit"})
return
}
limit = parsedLimit
}
if limit > maxProcessDetailsPageLimit {
limit = maxProcessDetailsPageLimit
}
offset, _ := strconv.Atoi(strings.TrimSpace(c.Query("offset")))
if offset < 0 {
offset = 0
}
details, total, err := h.db.GetProcessDetailsPage(messageID, limit, offset)
if err != nil {
h.logger.Error("获取过程详情失败", zap.Error(err))
h.logger.Error("分页获取过程详情失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
details = database.DedupeConsecutiveProcessDetails(details)
out := processDetailsToJSON(h.logger, details)
c.JSON(http.StatusOK, gin.H{"processDetails": out, "total": len(out)})
out := processDetailsToJSON(h.logger, details, false)
c.JSON(http.StatusOK, gin.H{
"processDetails": out,
"total": total,
"offset": offset,
"limit": limit,
"hasMore": offset+len(out) < total,
})
}
func processDetailsToJSON(logger *zap.Logger, details []database.ProcessDetail) []map[string]interface{} {
// GetProcessDetail 获取单条完整过程详情。列表接口默认不给工具 payload,用户点开单条工具时再拉这里。
func (h *ConversationHandler) GetProcessDetail(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "process detail id required"})
return
}
detail, err := h.db.GetProcessDetailByID(id)
if err != nil {
h.logger.Error("获取过程详情失败", zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": "过程详情不存在"})
return
}
out := processDetailsToJSON(h.logger, []database.ProcessDetail{*detail}, true)
if len(out) == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "过程详情不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"processDetail": out[0]})
}
func processDetailsToJSON(logger *zap.Logger, details []database.ProcessDetail, includeToolPayload bool) []map[string]interface{} {
out := make([]map[string]interface{}, 0, len(details))
for _, d := range details {
var data interface{}
@@ -253,6 +291,9 @@ func processDetailsToJSON(logger *zap.Logger, details []database.ProcessDetail)
logger.Warn("解析过程详情数据失败", zap.Error(err))
}
}
if !includeToolPayload {
data = summarizeProcessDetailData(d.EventType, data)
}
out = append(out, map[string]interface{}{
"id": d.ID,
"messageId": d.MessageID,
@@ -266,6 +307,27 @@ func processDetailsToJSON(logger *zap.Logger, details []database.ProcessDetail)
return out
}
func summarizeProcessDetailData(eventType string, data interface{}) interface{} {
m, ok := data.(map[string]interface{})
if !ok || (eventType != "tool_call" && eventType != "tool_result") {
return data
}
allow := map[string]bool{
"toolName": true, "toolCallId": true, "index": true, "total": true,
"success": true, "isError": true, "executionId": true,
"einoAgent": true, "einoRole": true, "einoScope": true, "orchestration": true,
"agentFacing": true,
}
out := make(map[string]interface{}, len(allow)+1)
for k, v := range m {
if allow[k] {
out[k] = v
}
}
out["_payloadDeferred"] = true
return out
}
// UpdateConversationRequest 更新对话请求
type UpdateConversationRequest struct {
Title string `json:"title"`
+10 -5
View File
@@ -575,7 +575,7 @@ func (h *MonitorHandler) lookupExecution(id string) *mcp.ToolExecution {
return nil
}
// BatchGetToolNames 批量获取工具执行的工具名称(消除前端 N+1 请求)
// BatchGetToolNames 批量获取工具执行摘要(消除前端 N+1 请求)
func (h *MonitorHandler) BatchGetToolNames(c *gin.Context) {
var req struct {
IDs []string `json:"ids"`
@@ -585,24 +585,29 @@ func (h *MonitorHandler) BatchGetToolNames(c *gin.Context) {
return
}
result := make(map[string]string, len(req.IDs))
type executionSummary struct {
ToolName string `json:"toolName"`
Status string `json:"status"`
}
result := make(map[string]executionSummary, len(req.IDs))
for _, id := range req.IDs {
// 先从内部MCP服务器查找
if exec, exists := h.mcpServer.GetExecution(id); exists {
result[id] = exec.ToolName
result[id] = executionSummary{ToolName: exec.ToolName, Status: exec.Status}
continue
}
// 再从外部MCP管理器查找
if h.externalMCPMgr != nil {
if exec, exists := h.externalMCPMgr.GetExecution(id); exists {
result[id] = exec.ToolName
result[id] = executionSummary{ToolName: exec.ToolName, Status: exec.Status}
continue
}
}
// 最后从数据库查找
if h.db != nil {
if exec, err := h.db.GetToolExecution(id); err == nil && exec != nil {
result[id] = exec.ToolName
result[id] = executionSummary{ToolName: exec.ToolName, Status: exec.Status}
}
}
}
+37 -1
View File
@@ -4735,7 +4735,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"get": map[string]interface{}{
"tags": []string{"对话交互"},
"summary": "获取消息过程详情",
"description": "按需加载指定消息的执行过程详情,包括工具调用、思考过程等事件。",
"description": "按需分页加载指定消息的执行过程详情,包括工具调用、思考过程等事件。默认返回 50 条;导出或旧集成需要全量时可显式传 full=1。",
"operationId": "getMessageProcessDetails",
"parameters": []map[string]interface{}{
{
@@ -4745,6 +4745,34 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"description": "消息ID",
"schema": map[string]interface{}{"type": "string"},
},
{
"name": "summary",
"in": "query",
"required": false,
"description": "仅返回过程详情摘要(total / iterationCount / maxIteration",
"schema": map[string]interface{}{"type": "boolean"},
},
{
"name": "limit",
"in": "query",
"required": false,
"description": "分页大小,默认 50,最大 500",
"schema": map[string]interface{}{"type": "integer", "default": 50, "maximum": 500},
},
{
"name": "offset",
"in": "query",
"required": false,
"description": "分页偏移量,默认 0",
"schema": map[string]interface{}{"type": "integer", "default": 0},
},
{
"name": "full",
"in": "query",
"required": false,
"description": "显式返回全量过程详情;仅建议导出/兼容旧集成使用",
"schema": map[string]interface{}{"type": "boolean"},
},
},
"responses": map[string]interface{}{
"200": map[string]interface{}{
@@ -4769,6 +4797,14 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
},
},
},
"total": map[string]interface{}{"type": "integer", "description": "过程详情总数"},
"offset": map[string]interface{}{"type": "integer", "description": "当前分页偏移量"},
"limit": map[string]interface{}{"type": "integer", "description": "当前分页大小"},
"hasMore": map[string]interface{}{"type": "boolean", "description": "是否还有更多过程详情"},
"summary": map[string]interface{}{
"type": "object",
"description": "summary=1 时返回的摘要",
},
},
},
},