mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-10 16:23:54 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51df4bd539 | |||
| 5197f5a964 | |||
| 33489f32bd | |||
| c9b3531af7 | |||
| 21b1ef6cf5 | |||
| c88594d478 | |||
| 5810fd7afa | |||
| a38dd2b4a8 | |||
| 49a6936fb3 |
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||||
version: "v1.5.0"
|
version: "v1.5.1"
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
server:
|
server:
|
||||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||||
|
|||||||
@@ -324,6 +324,46 @@ func (a *Agent) AgentLoopWithConversationID(ctx context.Context, userInput strin
|
|||||||
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil, nil, nil)
|
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EinoSingleAgentSystemInstruction 供 Eino adk.ChatModelAgent.Instruction 使用,与 AgentLoopWithProgress 首条 system 对齐(含 system_prompt_path 与 Skills 提示)。
|
||||||
|
func (a *Agent) EinoSingleAgentSystemInstruction(roleSkills []string) string {
|
||||||
|
systemPrompt := DefaultSingleAgentSystemPrompt()
|
||||||
|
if a.agentConfig != nil {
|
||||||
|
if p := strings.TrimSpace(a.agentConfig.SystemPromptPath); p != "" {
|
||||||
|
path := p
|
||||||
|
a.mu.RLock()
|
||||||
|
base := a.promptBaseDir
|
||||||
|
a.mu.RUnlock()
|
||||||
|
if !filepath.IsAbs(path) && base != "" {
|
||||||
|
path = filepath.Join(base, path)
|
||||||
|
}
|
||||||
|
if b, err := os.ReadFile(path); err != nil {
|
||||||
|
a.logger.Warn("读取单代理 system_prompt_path 失败,使用内置提示", zap.String("path", path), zap.Error(err))
|
||||||
|
} else if s := strings.TrimSpace(string(b)); s != "" {
|
||||||
|
systemPrompt = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(roleSkills) > 0 {
|
||||||
|
var skillsHint strings.Builder
|
||||||
|
skillsHint.WriteString("\n\n本角色推荐使用的Skills:\n")
|
||||||
|
for i, skillName := range roleSkills {
|
||||||
|
if i > 0 {
|
||||||
|
skillsHint.WriteString("、")
|
||||||
|
}
|
||||||
|
skillsHint.WriteString("`")
|
||||||
|
skillsHint.WriteString(skillName)
|
||||||
|
skillsHint.WriteString("`")
|
||||||
|
}
|
||||||
|
skillsHint.WriteString("\n- 这些名称与 skills/ 下 SKILL.md 的 `name` 一致。")
|
||||||
|
skillsHint.WriteString("\n- 若当前会话已启用 Eino 内置 `skill` 工具,请按需加载;否则以 MCP 与文本工作流完成。")
|
||||||
|
skillsHint.WriteString("\n- 例如传入 skill 参数为 `")
|
||||||
|
skillsHint.WriteString(roleSkills[0])
|
||||||
|
skillsHint.WriteString("`")
|
||||||
|
systemPrompt += skillsHint.String()
|
||||||
|
}
|
||||||
|
return systemPrompt
|
||||||
|
}
|
||||||
|
|
||||||
// AgentLoopWithProgress 执行Agent循环(带进度回调和对话ID)
|
// AgentLoopWithProgress 执行Agent循环(带进度回调和对话ID)
|
||||||
// roleSkills: 角色配置的skills列表(用于在系统提示词中提示AI,但不硬编码内容)
|
// roleSkills: 角色配置的skills列表(用于在系统提示词中提示AI,但不硬编码内容)
|
||||||
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback, roleTools []string, roleSkills []string) (*AgentLoopResult, error) {
|
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback, roleTools []string, roleSkills []string) (*AgentLoopResult, error) {
|
||||||
|
|||||||
@@ -100,6 +100,6 @@ func DefaultSingleAgentSystemPrompt() string {
|
|||||||
## 技能库(Skills)与知识库
|
## 技能库(Skills)与知识库
|
||||||
|
|
||||||
- 技能包位于服务器 skills/ 目录(各子目录 SKILL.md,遵循 agentskills.io);知识库用于向量检索片段,Skills 为可执行工作流指令。
|
- 技能包位于服务器 skills/ 目录(各子目录 SKILL.md,遵循 agentskills.io);知识库用于向量检索片段,Skills 为可执行工作流指令。
|
||||||
- 单代理本会话通过 MCP 使用知识库与漏洞记录等;Skills 的渐进式加载在「多代理 / Eino DeepAgent」中由内置 skill 工具完成。
|
- 单代理本会话通过 MCP 使用知识库与漏洞记录等;Skills 的渐进式加载在「Eino ADK 单代理(/api/eino-agent)」或「多代理 / Eino DeepAgent」中由内置 skill 工具完成(需在配置中启用 multi_agent.eino_skills)。
|
||||||
- 若当前无 skill 工具,需要完整 Skill 工作流时请使用多代理模式或切换为 Eino 编排会话。`
|
- 若当前无 skill 工具,需要完整 Skill 工作流时请使用 **Eino 单代理** 或 **多代理** 对话模式。`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -609,6 +609,9 @@ func setupRoutes(
|
|||||||
protected.POST("/agent-loop", agentHandler.AgentLoop)
|
protected.POST("/agent-loop", agentHandler.AgentLoop)
|
||||||
// Agent Loop 流式输出
|
// Agent Loop 流式输出
|
||||||
protected.POST("/agent-loop/stream", agentHandler.AgentLoopStream)
|
protected.POST("/agent-loop/stream", agentHandler.AgentLoopStream)
|
||||||
|
// Eino ADK 单代理(ChatModelAgent + Runner;不依赖 multi_agent.enabled)
|
||||||
|
protected.POST("/eino-agent", agentHandler.EinoSingleAgentLoop)
|
||||||
|
protected.POST("/eino-agent/stream", agentHandler.EinoSingleAgentLoopStream)
|
||||||
// Agent Loop 取消与任务列表
|
// Agent Loop 取消与任务列表
|
||||||
protected.POST("/agent-loop/cancel", agentHandler.CancelAgentLoop)
|
protected.POST("/agent-loop/cancel", agentHandler.CancelAgentLoop)
|
||||||
protected.GET("/agent-loop/tasks", agentHandler.ListAgentTasks)
|
protected.GET("/agent-loop/tasks", agentHandler.ListAgentTasks)
|
||||||
|
|||||||
+25
-11
@@ -177,7 +177,7 @@ type ChatRequest struct {
|
|||||||
Role string `json:"role,omitempty"` // 角色名称
|
Role string `json:"role,omitempty"` // 角色名称
|
||||||
Attachments []ChatAttachment `json:"attachments,omitempty"`
|
Attachments []ChatAttachment `json:"attachments,omitempty"`
|
||||||
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
|
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
|
||||||
// Orchestration 仅对 /api/multi-agent、/api/multi-agent/stream:deep | plan_execute | supervisor;空则等同 deep。机器人/批量等无请求体时由服务端默认 deep。
|
// Orchestration 仅对 /api/multi-agent、/api/multi-agent/stream:deep | plan_execute | supervisor;空则等同 deep。机器人/批量等无请求体时由服务端默认 deep。/api/eino-agent* 不使用此字段。
|
||||||
Orchestration string `json:"orchestration,omitempty"`
|
Orchestration string `json:"orchestration,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1619,7 +1619,7 @@ type BatchTaskRequest struct {
|
|||||||
Title string `json:"title"` // 任务标题(可选)
|
Title string `json:"title"` // 任务标题(可选)
|
||||||
Tasks []string `json:"tasks" binding:"required"` // 任务列表,每行一个任务
|
Tasks []string `json:"tasks" binding:"required"` // 任务列表,每行一个任务
|
||||||
Role string `json:"role,omitempty"` // 角色名称(可选,空字符串表示默认角色)
|
Role string `json:"role,omitempty"` // 角色名称(可选,空字符串表示默认角色)
|
||||||
AgentMode string `json:"agentMode,omitempty"` // single | deep | plan_execute | supervisor(旧版 multi 视为 deep)
|
AgentMode string `json:"agentMode,omitempty"` // single | eino_single | deep | plan_execute | supervisor(react 同 single;旧版 multi 视为 deep)
|
||||||
ScheduleMode string `json:"scheduleMode,omitempty"` // manual | cron
|
ScheduleMode string `json:"scheduleMode,omitempty"` // manual | cron
|
||||||
CronExpr string `json:"cronExpr,omitempty"` // scheduleMode=cron 时必填
|
CronExpr string `json:"cronExpr,omitempty"` // scheduleMode=cron 时必填
|
||||||
ExecuteNow bool `json:"executeNow,omitempty"` // 创建后是否立即执行(默认 false)
|
ExecuteNow bool `json:"executeNow,omitempty"` // 创建后是否立即执行(默认 false)
|
||||||
@@ -1630,9 +1630,12 @@ func normalizeBatchQueueAgentMode(mode string) string {
|
|||||||
if m == "multi" {
|
if m == "multi" {
|
||||||
return "deep"
|
return "deep"
|
||||||
}
|
}
|
||||||
if m == "" || m == "single" {
|
if m == "" || m == "single" || m == "react" {
|
||||||
return "single"
|
return "single"
|
||||||
}
|
}
|
||||||
|
if m == "eino_single" {
|
||||||
|
return "eino_single"
|
||||||
|
}
|
||||||
switch config.NormalizeMultiAgentOrchestration(m) {
|
switch config.NormalizeMultiAgentOrchestration(m) {
|
||||||
case "plan_execute":
|
case "plan_execute":
|
||||||
return "plan_execute"
|
return "plan_execute"
|
||||||
@@ -2272,12 +2275,15 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
|||||||
// 使用队列配置的角色工具列表(如果为空,表示使用所有工具)
|
// 使用队列配置的角色工具列表(如果为空,表示使用所有工具)
|
||||||
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
|
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
|
||||||
useBatchMulti := false
|
useBatchMulti := false
|
||||||
|
useEinoSingle := false
|
||||||
batchOrch := "deep"
|
batchOrch := "deep"
|
||||||
am := strings.TrimSpace(strings.ToLower(queue.AgentMode))
|
am := strings.TrimSpace(strings.ToLower(queue.AgentMode))
|
||||||
if am == "multi" {
|
if am == "multi" {
|
||||||
am = "deep"
|
am = "deep"
|
||||||
}
|
}
|
||||||
if batchQueueWantsEino(queue.AgentMode) && h.config != nil && h.config.MultiAgent.Enabled {
|
if am == "eino_single" {
|
||||||
|
useEinoSingle = true
|
||||||
|
} else if batchQueueWantsEino(queue.AgentMode) && h.config != nil && h.config.MultiAgent.Enabled {
|
||||||
useBatchMulti = true
|
useBatchMulti = true
|
||||||
batchOrch = config.NormalizeMultiAgentOrchestration(am)
|
batchOrch = config.NormalizeMultiAgentOrchestration(am)
|
||||||
} else if queue.AgentMode == "" {
|
} else if queue.AgentMode == "" {
|
||||||
@@ -2287,12 +2293,20 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
|||||||
batchOrch = "deep"
|
batchOrch = "deep"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
useRunResult := useBatchMulti || useEinoSingle
|
||||||
var result *agent.AgentLoopResult
|
var result *agent.AgentLoopResult
|
||||||
var resultMA *multiagent.RunResult
|
var resultMA *multiagent.RunResult
|
||||||
var runErr error
|
var runErr error
|
||||||
if useBatchMulti {
|
switch {
|
||||||
|
case useBatchMulti:
|
||||||
resultMA, runErr = multiagent.RunDeepAgent(ctx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback, h.agentsMarkdownDir, batchOrch)
|
resultMA, runErr = multiagent.RunDeepAgent(ctx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, progressCallback, h.agentsMarkdownDir, batchOrch)
|
||||||
} else {
|
case useEinoSingle:
|
||||||
|
if h.config == nil {
|
||||||
|
runErr = fmt.Errorf("服务器配置未加载")
|
||||||
|
} else {
|
||||||
|
resultMA, runErr = multiagent.RunEinoSingleChatModelAgent(ctx, h.config, &h.config.MultiAgent, h.agent, h.logger, conversationID, finalMessage, []agent.ChatMessage{}, roleTools, roleSkills, progressCallback)
|
||||||
|
}
|
||||||
|
default:
|
||||||
result, runErr = h.agent.AgentLoopWithProgress(ctx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools, roleSkills)
|
result, runErr = h.agent.AgentLoopWithProgress(ctx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools, roleSkills)
|
||||||
}
|
}
|
||||||
// 任务执行完成,清理取消函数
|
// 任务执行完成,清理取消函数
|
||||||
@@ -2306,10 +2320,10 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
|||||||
// 3. 检查 result.Response 中是否包含取消相关的消息
|
// 3. 检查 result.Response 中是否包含取消相关的消息
|
||||||
errStr := runErr.Error()
|
errStr := runErr.Error()
|
||||||
partialResp := ""
|
partialResp := ""
|
||||||
if result != nil {
|
if useRunResult && resultMA != nil {
|
||||||
partialResp = result.Response
|
|
||||||
} else if resultMA != nil {
|
|
||||||
partialResp = resultMA.Response
|
partialResp = resultMA.Response
|
||||||
|
} else if result != nil {
|
||||||
|
partialResp = result.Response
|
||||||
}
|
}
|
||||||
isCancelled := errors.Is(runErr, context.Canceled) ||
|
isCancelled := errors.Is(runErr, context.Canceled) ||
|
||||||
strings.Contains(strings.ToLower(errStr), "context canceled") ||
|
strings.Contains(strings.ToLower(errStr), "context canceled") ||
|
||||||
@@ -2348,7 +2362,7 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
|||||||
if err := h.db.SaveReActData(conversationID, result.LastReActInput, result.LastReActOutput); err != nil {
|
if err := h.db.SaveReActData(conversationID, result.LastReActInput, result.LastReActOutput); err != nil {
|
||||||
h.logger.Warn("保存取消任务的ReAct数据失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(err))
|
h.logger.Warn("保存取消任务的ReAct数据失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(err))
|
||||||
}
|
}
|
||||||
} else if resultMA != nil && (resultMA.LastReActInput != "" || resultMA.LastReActOutput != "") {
|
} else if useRunResult && resultMA != nil && (resultMA.LastReActInput != "" || resultMA.LastReActOutput != "") {
|
||||||
if err := h.db.SaveReActData(conversationID, resultMA.LastReActInput, resultMA.LastReActOutput); err != nil {
|
if err := h.db.SaveReActData(conversationID, resultMA.LastReActInput, resultMA.LastReActOutput); err != nil {
|
||||||
h.logger.Warn("保存取消任务的ReAct数据失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(err))
|
h.logger.Warn("保存取消任务的ReAct数据失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(err))
|
||||||
}
|
}
|
||||||
@@ -2379,7 +2393,7 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
|||||||
var resText string
|
var resText string
|
||||||
var mcpIDs []string
|
var mcpIDs []string
|
||||||
var lastIn, lastOut string
|
var lastIn, lastOut string
|
||||||
if useBatchMulti {
|
if useRunResult {
|
||||||
resText = resultMA.Response
|
resText = resultMA.Response
|
||||||
mcpIDs = resultMA.MCPExecutionIDs
|
mcpIDs = resultMA.MCPExecutionIDs
|
||||||
lastIn = resultMA.LastReActInput
|
lastIn = resultMA.LastReActInput
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ type BatchTaskQueue struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Role string `json:"role,omitempty"` // 角色名称(空字符串表示默认角色)
|
Role string `json:"role,omitempty"` // 角色名称(空字符串表示默认角色)
|
||||||
AgentMode string `json:"agentMode"` // single | deep | plan_execute | supervisor
|
AgentMode string `json:"agentMode"` // single | eino_single | deep | plan_execute | supervisor
|
||||||
ScheduleMode string `json:"scheduleMode"` // manual | cron
|
ScheduleMode string `json:"scheduleMode"` // manual | cron
|
||||||
CronExpr string `json:"cronExpr,omitempty"`
|
CronExpr string `json:"cronExpr,omitempty"`
|
||||||
NextRunAt *time.Time `json:"nextRunAt,omitempty"`
|
NextRunAt *time.Time `json:"nextRunAt,omitempty"`
|
||||||
|
|||||||
@@ -128,47 +128,51 @@ func RegisterBatchTaskMCPTools(mcpServer *mcp.Server, h *AgentHandler, logger *z
|
|||||||
// --- create ---
|
// --- create ---
|
||||||
reg(mcp.Tool{
|
reg(mcp.Tool{
|
||||||
Name: builtin.ToolBatchTaskCreate,
|
Name: builtin.ToolBatchTaskCreate,
|
||||||
Description: `创建新的批量任务队列。任务列表使用 tasks(字符串数组)或 tasks_text(多行,每行一条)。
|
Description: `【用途】应用内「任务管理 / 批量任务队列」:把多条彼此独立的用户指令登记成一条队列,便于在界面里查看进度、暂停/继续、定时重跑等。这是队列数据与调度入口,不是再开一个“子代理会话”替你探索当前问题。
|
||||||
agent_mode: single(默认)或 multi(需系统启用多代理)。schedule_mode: manual(默认)或 cron;为 cron 时必须提供 cron_expr(如 "0 */6 * * *")。
|
|
||||||
默认创建后不会立即执行。可通过 execute_now=true 在创建后立即启动;也可后续调用 batch_task_start 手工启动。Cron 队列若需按表达式自动触发下一轮,还需保持调度开关开启(可用 batch_task_schedule_enabled)。`,
|
【何时用】用户明确要批量排队执行、Cron 周期跑同一批指令、或需要与任务管理页面对齐时调用。需要即时追问、强依赖当前对话上下文的分析/编码,应在本对话内直接完成,不要为了“委派”而创建队列。
|
||||||
ShortDescription: "创建批量任务队列(可选立即执行)",
|
|
||||||
|
【参数】tasks(字符串数组)或 tasks_text(多行,每行一条)二选一;每项是一条将来由系统按队列顺序执行的指令文案。agent_mode:single(原生 ReAct,默认)、eino_single(Eino ADK 单代理)、deep / plan_execute / supervisor(需系统启用多代理);兼容旧值 multi(视为 deep)。非“把主对话拆给子代理”。schedule_mode:manual(默认)或 cron;cron 须填 cron_expr(5 段,如 "0 */6 * * *")。
|
||||||
|
|
||||||
|
【执行】默认创建后为 pending,不自动跑。execute_now=true 可创建后立即跑;否则之后调用 batch_task_start。Cron 自动下一轮需 schedule_enabled 为 true(可用 batch_task_schedule_enabled)。`,
|
||||||
|
ShortDescription: "任务管理:创建批量任务队列(登记多条指令,可选立即或 Cron)",
|
||||||
InputSchema: map[string]interface{}{
|
InputSchema: map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
"title": map[string]interface{}{
|
"title": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "可选标题",
|
"description": "可选队列标题,便于在任务管理中识别",
|
||||||
},
|
},
|
||||||
"role": map[string]interface{}{
|
"role": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "角色名称,空表示默认",
|
"description": "队列使用的角色名,空表示默认",
|
||||||
},
|
},
|
||||||
"tasks": map[string]interface{}{
|
"tasks": map[string]interface{}{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "任务指令列表,每项一条",
|
"description": "队列中的子任务指令,每项一条独立待执行文案(与 tasks_text 二选一)",
|
||||||
"items": map[string]interface{}{"type": "string"},
|
"items": map[string]interface{}{"type": "string"},
|
||||||
},
|
},
|
||||||
"tasks_text": map[string]interface{}{
|
"tasks_text": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "多行文本,每行一条任务(与 tasks 二选一)",
|
"description": "多行文本,每行一条子任务指令(与 tasks 二选一)",
|
||||||
},
|
},
|
||||||
"agent_mode": map[string]interface{}{
|
"agent_mode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "single 或 multi",
|
"description": "执行模式:single(原生 ReAct)、eino_single(Eino ADK)、deep/plan_execute/supervisor(Eino 编排,需启用多代理);multi 兼容为 deep",
|
||||||
"enum": []string{"single", "multi"},
|
"enum": []string{"single", "eino_single", "deep", "plan_execute", "supervisor", "multi"},
|
||||||
},
|
},
|
||||||
"schedule_mode": map[string]interface{}{
|
"schedule_mode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "manual 或 cron",
|
"description": "manual(仅手工/启动后跑)或 cron(按表达式触发)",
|
||||||
"enum": []string{"manual", "cron"},
|
"enum": []string{"manual", "cron"},
|
||||||
},
|
},
|
||||||
"cron_expr": map[string]interface{}{
|
"cron_expr": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "schedule_mode 为 cron 时必填。标准 5 段格式:分钟 小时 日 月 星期,例如 \"0 */6 * * *\"(每6小时)、\"30 2 * * 1-5\"(工作日凌晨2:30)",
|
"description": "schedule_mode 为 cron 时必填。标准 5 段:分钟 小时 日 月 星期,例如 \"0 */6 * * *\"、\"30 2 * * 1-5\"",
|
||||||
},
|
},
|
||||||
"execute_now": map[string]interface{}{
|
"execute_now": map[string]interface{}{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "是否创建后立即执行,默认 false",
|
"description": "创建后是否立即开始执行队列,默认 false(pending,需 batch_task_start)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -380,8 +384,8 @@ agent_mode: single(默认)或 multi(需系统启用多代理)。schedule
|
|||||||
},
|
},
|
||||||
"agent_mode": map[string]interface{}{
|
"agent_mode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "代理模式:single(单代理 ReAct)或 multi(多代理)",
|
"description": "代理模式:single、eino_single、deep、plan_execute、supervisor;multi 视为 deep",
|
||||||
"enum": []string{"single", "multi"},
|
"enum": []string{"single", "eino_single", "deep", "plan_execute", "supervisor", "multi"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"queue_id"},
|
"required": []string{"queue_id"},
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/multiagent"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EinoSingleAgentLoopStream Eino ADK 单代理(ChatModelAgent + Runner)流式对话;不依赖 multi_agent.enabled。
|
||||||
|
func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "text/event-stream")
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Header("Connection", "keep-alive")
|
||||||
|
|
||||||
|
var req ChatRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
ev := StreamEvent{Type: "error", Message: "请求参数错误: " + err.Error()}
|
||||||
|
b, _ := json.Marshal(ev)
|
||||||
|
fmt.Fprintf(c.Writer, "data: %s\n\n", b)
|
||||||
|
done := StreamEvent{Type: "done", Message: ""}
|
||||||
|
db, _ := json.Marshal(done)
|
||||||
|
fmt.Fprintf(c.Writer, "data: %s\n\n", db)
|
||||||
|
if flusher, ok := c.Writer.(http.Flusher); ok {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("X-Accel-Buffering", "no")
|
||||||
|
|
||||||
|
var baseCtx context.Context
|
||||||
|
clientDisconnected := false
|
||||||
|
var sseWriteMu sync.Mutex
|
||||||
|
sendEvent := func(eventType, message string, data interface{}) {
|
||||||
|
if clientDisconnected {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if eventType == "error" && baseCtx != nil && errors.Is(context.Cause(baseCtx), ErrTaskCancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-c.Request.Context().Done():
|
||||||
|
clientDisconnected = true
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ev := StreamEvent{Type: eventType, Message: message, Data: data}
|
||||||
|
b, _ := json.Marshal(ev)
|
||||||
|
sseWriteMu.Lock()
|
||||||
|
_, err := fmt.Fprintf(c.Writer, "data: %s\n\n", b)
|
||||||
|
if err != nil {
|
||||||
|
sseWriteMu.Unlock()
|
||||||
|
clientDisconnected = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if flusher, ok := c.Writer.(http.Flusher); ok {
|
||||||
|
flusher.Flush()
|
||||||
|
} else {
|
||||||
|
c.Writer.Flush()
|
||||||
|
}
|
||||||
|
sseWriteMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("收到 Eino ADK 单代理流式请求",
|
||||||
|
zap.String("conversationId", req.ConversationID),
|
||||||
|
)
|
||||||
|
|
||||||
|
prep, err := h.prepareMultiAgentSession(&req)
|
||||||
|
if err != nil {
|
||||||
|
sendEvent("error", err.Error(), nil)
|
||||||
|
sendEvent("done", "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if prep.CreatedNew {
|
||||||
|
sendEvent("conversation", "会话已创建", map[string]interface{}{
|
||||||
|
"conversationId": prep.ConversationID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
conversationID := prep.ConversationID
|
||||||
|
assistantMessageID := prep.AssistantMessageID
|
||||||
|
|
||||||
|
if prep.UserMessageID != "" {
|
||||||
|
sendEvent("message_saved", "", map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"userMessageId": prep.UserMessageID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback := h.createProgressCallback(conversationID, assistantMessageID, sendEvent)
|
||||||
|
|
||||||
|
var cancelWithCause context.CancelCauseFunc
|
||||||
|
baseCtx, cancelWithCause = context.WithCancelCause(context.Background())
|
||||||
|
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
|
||||||
|
defer timeoutCancel()
|
||||||
|
defer cancelWithCause(nil)
|
||||||
|
|
||||||
|
if _, err := h.tasks.StartTask(conversationID, req.Message, cancelWithCause); err != nil {
|
||||||
|
var errorMsg string
|
||||||
|
if errors.Is(err, ErrTaskAlreadyRunning) {
|
||||||
|
errorMsg = "⚠️ 当前会话已有任务正在执行中,请等待当前任务完成或点击「停止任务」后再尝试。"
|
||||||
|
sendEvent("error", errorMsg, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"errorType": "task_already_running",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
errorMsg = "❌ 无法启动任务: " + err.Error()
|
||||||
|
sendEvent("error", errorMsg, nil)
|
||||||
|
}
|
||||||
|
if assistantMessageID != "" {
|
||||||
|
_, _ = h.db.Exec("UPDATE messages SET content = ? WHERE id = ?", errorMsg, assistantMessageID)
|
||||||
|
}
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskStatus := "completed"
|
||||||
|
defer h.tasks.FinishTask(conversationID, taskStatus)
|
||||||
|
|
||||||
|
sendEvent("progress", "正在启动 Eino ADK 单代理(ChatModelAgent)...", map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
})
|
||||||
|
|
||||||
|
stopKeepalive := make(chan struct{})
|
||||||
|
go sseKeepalive(c, stopKeepalive, &sseWriteMu)
|
||||||
|
defer close(stopKeepalive)
|
||||||
|
|
||||||
|
if h.config == nil {
|
||||||
|
sendEvent("error", "服务器配置未加载", nil)
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, runErr := multiagent.RunEinoSingleChatModelAgent(
|
||||||
|
taskCtx,
|
||||||
|
h.config,
|
||||||
|
&h.config.MultiAgent,
|
||||||
|
h.agent,
|
||||||
|
h.logger,
|
||||||
|
conversationID,
|
||||||
|
prep.FinalMessage,
|
||||||
|
prep.History,
|
||||||
|
prep.RoleTools,
|
||||||
|
prep.RoleSkills,
|
||||||
|
progressCallback,
|
||||||
|
)
|
||||||
|
|
||||||
|
if runErr != nil {
|
||||||
|
cause := context.Cause(baseCtx)
|
||||||
|
if errors.Is(cause, ErrTaskCancelled) {
|
||||||
|
taskStatus = "cancelled"
|
||||||
|
h.tasks.UpdateTaskStatus(conversationID, taskStatus)
|
||||||
|
cancelMsg := "任务已被用户取消,后续操作已停止。"
|
||||||
|
if assistantMessageID != "" {
|
||||||
|
_, _ = h.db.Exec("UPDATE messages SET content = ? WHERE id = ?", cancelMsg, assistantMessageID)
|
||||||
|
_ = h.db.AddProcessDetail(assistantMessageID, conversationID, "cancelled", cancelMsg, nil)
|
||||||
|
}
|
||||||
|
sendEvent("cancelled", cancelMsg, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"messageId": assistantMessageID,
|
||||||
|
})
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Error("Eino ADK 单代理执行失败", zap.Error(runErr))
|
||||||
|
taskStatus = "failed"
|
||||||
|
h.tasks.UpdateTaskStatus(conversationID, taskStatus)
|
||||||
|
errMsg := "执行失败: " + runErr.Error()
|
||||||
|
if assistantMessageID != "" {
|
||||||
|
_, _ = h.db.Exec("UPDATE messages SET content = ? WHERE id = ?", errMsg, assistantMessageID)
|
||||||
|
_ = h.db.AddProcessDetail(assistantMessageID, conversationID, "error", errMsg, nil)
|
||||||
|
}
|
||||||
|
sendEvent("error", errMsg, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"messageId": assistantMessageID,
|
||||||
|
})
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if assistantMessageID != "" {
|
||||||
|
mcpIDsJSON := ""
|
||||||
|
if len(result.MCPExecutionIDs) > 0 {
|
||||||
|
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
||||||
|
mcpIDsJSON = string(jsonData)
|
||||||
|
}
|
||||||
|
_, _ = h.db.Exec(
|
||||||
|
"UPDATE messages SET content = ?, mcp_execution_ids = ? WHERE id = ?",
|
||||||
|
result.Response,
|
||||||
|
mcpIDsJSON,
|
||||||
|
assistantMessageID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.LastReActInput != "" || result.LastReActOutput != "" {
|
||||||
|
if err := h.db.SaveReActData(conversationID, result.LastReActInput, result.LastReActOutput); err != nil {
|
||||||
|
h.logger.Warn("保存 ReAct 数据失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent("response", result.Response, map[string]interface{}{
|
||||||
|
"mcpExecutionIds": result.MCPExecutionIDs,
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"messageId": assistantMessageID,
|
||||||
|
"agentMode": "eino_single",
|
||||||
|
})
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// EinoSingleAgentLoop Eino ADK 单代理非流式对话。
|
||||||
|
func (h *AgentHandler) EinoSingleAgentLoop(c *gin.Context) {
|
||||||
|
var req ChatRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("收到 Eino ADK 单代理非流式请求", zap.String("conversationId", req.ConversationID))
|
||||||
|
|
||||||
|
prep, err := h.prepareMultiAgentSession(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressBuf strings.Builder
|
||||||
|
progressCallback := func(eventType, message string, data interface{}) {
|
||||||
|
progressBuf.WriteString(eventType)
|
||||||
|
progressBuf.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.config == nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器配置未加载"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, runErr := multiagent.RunEinoSingleChatModelAgent(
|
||||||
|
c.Request.Context(),
|
||||||
|
h.config,
|
||||||
|
&h.config.MultiAgent,
|
||||||
|
h.agent,
|
||||||
|
h.logger,
|
||||||
|
prep.ConversationID,
|
||||||
|
prep.FinalMessage,
|
||||||
|
prep.History,
|
||||||
|
prep.RoleTools,
|
||||||
|
prep.RoleSkills,
|
||||||
|
progressCallback,
|
||||||
|
)
|
||||||
|
if runErr != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": runErr.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if prep.AssistantMessageID != "" {
|
||||||
|
mcpIDsJSON := ""
|
||||||
|
if len(result.MCPExecutionIDs) > 0 {
|
||||||
|
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
|
||||||
|
mcpIDsJSON = string(jsonData)
|
||||||
|
}
|
||||||
|
_, _ = h.db.Exec(
|
||||||
|
"UPDATE messages SET content = ?, mcp_execution_ids = ? WHERE id = ?",
|
||||||
|
result.Response,
|
||||||
|
mcpIDsJSON,
|
||||||
|
prep.AssistantMessageID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if result.LastReActInput != "" || result.LastReActOutput != "" {
|
||||||
|
_ = h.db.SaveReActData(prep.ConversationID, result.LastReActInput, result.LastReActOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"response": result.Response,
|
||||||
|
"conversationId": prep.ConversationID,
|
||||||
|
"mcpExecutionIds": result.MCPExecutionIDs,
|
||||||
|
"assistantMessageId": prep.AssistantMessageID,
|
||||||
|
"agentMode": "eino_single",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ type multiAgentPrepared struct {
|
|||||||
History []agent.ChatMessage
|
History []agent.ChatMessage
|
||||||
FinalMessage string
|
FinalMessage string
|
||||||
RoleTools []string
|
RoleTools []string
|
||||||
|
RoleSkills []string
|
||||||
AssistantMessageID string
|
AssistantMessageID string
|
||||||
UserMessageID string
|
UserMessageID string
|
||||||
}
|
}
|
||||||
@@ -67,6 +68,7 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
|
|||||||
|
|
||||||
finalMessage := req.Message
|
finalMessage := req.Message
|
||||||
var roleTools []string
|
var roleTools []string
|
||||||
|
var roleSkills []string
|
||||||
if req.WebShellConnectionID != "" {
|
if req.WebShellConnectionID != "" {
|
||||||
conn, errConn := h.db.GetWebshellConnection(strings.TrimSpace(req.WebShellConnectionID))
|
conn, errConn := h.db.GetWebshellConnection(strings.TrimSpace(req.WebShellConnectionID))
|
||||||
if errConn != nil || conn == nil {
|
if errConn != nil || conn == nil {
|
||||||
@@ -94,6 +96,7 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
|
|||||||
finalMessage = role.UserPrompt + "\n\n" + req.Message
|
finalMessage = role.UserPrompt + "\n\n" + req.Message
|
||||||
}
|
}
|
||||||
roleTools = role.Tools
|
roleTools = role.Tools
|
||||||
|
roleSkills = role.Skills
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +135,7 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
|
|||||||
History: agentHistoryMessages,
|
History: agentHistoryMessages,
|
||||||
FinalMessage: finalMessage,
|
FinalMessage: finalMessage,
|
||||||
RoleTools: roleTools,
|
RoleTools: roleTools,
|
||||||
|
RoleSkills: roleSkills,
|
||||||
AssistantMessageID: assistantMessageID,
|
AssistantMessageID: assistantMessageID,
|
||||||
UserMessageID: userMessageID,
|
UserMessageID: userMessageID,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -405,8 +405,8 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
"agentMode": map[string]interface{}{
|
"agentMode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "代理模式:single(ReAct)| deep | plan_execute | supervisor(Eino);旧值 multi 按 deep",
|
"description": "代理模式:single(原生 ReAct)| eino_single(Eino ADK 单代理)| deep | plan_execute | supervisor;react 同 single;旧值 multi 按 deep",
|
||||||
"enum": []string{"single", "deep", "plan_execute", "supervisor", "multi"},
|
"enum": []string{"single", "eino_single", "deep", "plan_execute", "supervisor", "multi", "react"},
|
||||||
},
|
},
|
||||||
"scheduleMode": map[string]interface{}{
|
"scheduleMode": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1499,6 +1499,76 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/api/eino-agent": map[string]interface{}{
|
||||||
|
"post": map[string]interface{}{
|
||||||
|
"tags": []string{"对话交互"},
|
||||||
|
"summary": "发送消息并获取 AI 回复(Eino ADK 单代理,非流式)",
|
||||||
|
"description": "与 `POST /api/agent-loop` 请求体相同,由 **CloudWeGo Eino** `adk.NewChatModelAgent` + `adk.NewRunner.Run` 执行(单代理 MCP 工具链)。**不依赖** `multi_agent.enabled`;`multi_agent.eino_skills` / `eino_middleware` 等与多代理主代理一致时可生效。支持 `webshellConnectionId`。",
|
||||||
|
"operationId": "sendMessageEinoSingleAgent",
|
||||||
|
"requestBody": map[string]interface{}{
|
||||||
|
"required": true,
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"application/json": map[string]interface{}{
|
||||||
|
"schema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"message": map[string]interface{}{"type": "string"},
|
||||||
|
"conversationId": map[string]interface{}{"type": "string"},
|
||||||
|
"role": map[string]interface{}{"type": "string"},
|
||||||
|
"webshellConnectionId": map[string]interface{}{"type": "string"},
|
||||||
|
},
|
||||||
|
"required": []string{"message"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"responses": map[string]interface{}{
|
||||||
|
"200": map[string]interface{}{"description": "成功,响应格式同 /api/agent-loop"},
|
||||||
|
"400": map[string]interface{}{"description": "参数错误"},
|
||||||
|
"401": map[string]interface{}{"description": "未授权"},
|
||||||
|
"500": map[string]interface{}{"description": "执行失败"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/api/eino-agent/stream": map[string]interface{}{
|
||||||
|
"post": map[string]interface{}{
|
||||||
|
"tags": []string{"对话交互"},
|
||||||
|
"summary": "发送消息并获取 AI 回复(Eino ADK 单代理,SSE)",
|
||||||
|
"description": "与 `POST /api/agent-loop/stream` 类似;由 Eino **单代理** ADK 执行。事件类型与多代理流式一致(含 `tool_call` / `response_delta` 等)。**不依赖** `multi_agent.enabled`。",
|
||||||
|
"operationId": "sendMessageEinoSingleAgentStream",
|
||||||
|
"requestBody": map[string]interface{}{
|
||||||
|
"required": true,
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"application/json": map[string]interface{}{
|
||||||
|
"schema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"message": map[string]interface{}{"type": "string"},
|
||||||
|
"conversationId": map[string]interface{}{"type": "string"},
|
||||||
|
"role": map[string]interface{}{"type": "string"},
|
||||||
|
"webshellConnectionId": map[string]interface{}{"type": "string"},
|
||||||
|
},
|
||||||
|
"required": []string{"message"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"responses": map[string]interface{}{
|
||||||
|
"200": map[string]interface{}{
|
||||||
|
"description": "text/event-stream(SSE)",
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"text/event-stream": map[string]interface{}{
|
||||||
|
"schema": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "SSE 流",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"401": map[string]interface{}{"description": "未授权"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"/api/multi-agent": map[string]interface{}{
|
"/api/multi-agent": map[string]interface{}{
|
||||||
"post": map[string]interface{}{
|
"post": map[string]interface{}{
|
||||||
"tags": []string{"对话交互"},
|
"tags": []string{"对话交互"},
|
||||||
|
|||||||
@@ -0,0 +1,545 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/einomcp"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/adk"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// einoADKRunLoopArgs 将 Eino adk.Runner 事件循环从 RunDeepAgent / RunEinoSingleChatModelAgent 中抽出复用。
|
||||||
|
type einoADKRunLoopArgs struct {
|
||||||
|
OrchMode string
|
||||||
|
OrchestratorName string
|
||||||
|
ConversationID string
|
||||||
|
Progress func(eventType, message string, data interface{})
|
||||||
|
Logger *zap.Logger
|
||||||
|
SnapshotMCPIDs func() []string
|
||||||
|
StreamsMainAssistant func(agent string) bool
|
||||||
|
EinoRoleTag func(agent string) string
|
||||||
|
CheckpointDir string
|
||||||
|
|
||||||
|
McpIDsMu *sync.Mutex
|
||||||
|
McpIDs *[]string
|
||||||
|
|
||||||
|
DA adk.Agent
|
||||||
|
|
||||||
|
// EmptyResponseMessage 当未捕获到助手正文时的占位(多代理与单代理文案不同)。
|
||||||
|
EmptyResponseMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
func runEinoADKAgentLoop(ctx context.Context, args *einoADKRunLoopArgs, baseMsgs []adk.Message) (*RunResult, error) {
|
||||||
|
if args == nil || args.DA == nil {
|
||||||
|
return nil, fmt.Errorf("eino run loop: args 或 Agent 为空")
|
||||||
|
}
|
||||||
|
if args.McpIDs == nil {
|
||||||
|
s := []string{}
|
||||||
|
args.McpIDs = &s
|
||||||
|
}
|
||||||
|
if args.McpIDsMu == nil {
|
||||||
|
args.McpIDsMu = &sync.Mutex{}
|
||||||
|
}
|
||||||
|
|
||||||
|
orchMode := args.OrchMode
|
||||||
|
orchestratorName := args.OrchestratorName
|
||||||
|
conversationID := args.ConversationID
|
||||||
|
progress := args.Progress
|
||||||
|
logger := args.Logger
|
||||||
|
snapshotMCPIDs := args.SnapshotMCPIDs
|
||||||
|
if snapshotMCPIDs == nil {
|
||||||
|
snapshotMCPIDs = func() []string { return nil }
|
||||||
|
}
|
||||||
|
streamsMainAssistant := args.StreamsMainAssistant
|
||||||
|
if streamsMainAssistant == nil {
|
||||||
|
streamsMainAssistant = func(agent string) bool {
|
||||||
|
return agent == "" || agent == orchestratorName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
einoRoleTag := args.EinoRoleTag
|
||||||
|
if einoRoleTag == nil {
|
||||||
|
einoRoleTag = func(agent string) string {
|
||||||
|
if streamsMainAssistant(agent) {
|
||||||
|
return "orchestrator"
|
||||||
|
}
|
||||||
|
return "sub"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
da := args.DA
|
||||||
|
mcpIDsMu := args.McpIDsMu
|
||||||
|
mcpIDs := args.McpIDs
|
||||||
|
|
||||||
|
var lastRunMsgs []adk.Message
|
||||||
|
var lastAssistant string
|
||||||
|
var lastPlanExecuteExecutor string
|
||||||
|
var retryHints []adk.Message
|
||||||
|
|
||||||
|
emptyHint := strings.TrimSpace(args.EmptyResponseMessage)
|
||||||
|
if emptyHint == "" {
|
||||||
|
emptyHint = "(Eino 会话已完成,但未捕获到助手文本输出。请查看过程详情或日志。)"
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptLoop:
|
||||||
|
for attempt := 0; attempt < maxToolCallRecoveryAttempts; attempt++ {
|
||||||
|
msgs := make([]adk.Message, 0, len(baseMsgs)+len(retryHints))
|
||||||
|
msgs = append(msgs, baseMsgs...)
|
||||||
|
msgs = append(msgs, retryHints...)
|
||||||
|
|
||||||
|
if attempt > 0 {
|
||||||
|
mcpIDsMu.Lock()
|
||||||
|
*mcpIDs = (*mcpIDs)[:0]
|
||||||
|
mcpIDsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
lastAssistant = ""
|
||||||
|
lastPlanExecuteExecutor = ""
|
||||||
|
var reasoningStreamSeq int64
|
||||||
|
var einoSubReplyStreamSeq int64
|
||||||
|
toolEmitSeen := make(map[string]struct{})
|
||||||
|
var einoMainRound int
|
||||||
|
var einoLastAgent string
|
||||||
|
subAgentToolStep := make(map[string]int)
|
||||||
|
pendingByID := make(map[string]toolCallPendingInfo)
|
||||||
|
pendingQueueByAgent := make(map[string][]string)
|
||||||
|
markPending := func(tc toolCallPendingInfo) {
|
||||||
|
if tc.ToolCallID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingByID[tc.ToolCallID] = tc
|
||||||
|
pendingQueueByAgent[tc.EinoAgent] = append(pendingQueueByAgent[tc.EinoAgent], tc.ToolCallID)
|
||||||
|
}
|
||||||
|
popNextPendingForAgent := func(agentName string) (toolCallPendingInfo, bool) {
|
||||||
|
q := pendingQueueByAgent[agentName]
|
||||||
|
for len(q) > 0 {
|
||||||
|
id := q[0]
|
||||||
|
q = q[1:]
|
||||||
|
pendingQueueByAgent[agentName] = q
|
||||||
|
if tc, ok := pendingByID[id]; ok {
|
||||||
|
delete(pendingByID, id)
|
||||||
|
return tc, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toolCallPendingInfo{}, false
|
||||||
|
}
|
||||||
|
removePendingByID := func(toolCallID string) {
|
||||||
|
if toolCallID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(pendingByID, toolCallID)
|
||||||
|
}
|
||||||
|
flushAllPendingAsFailed := func(err error) {
|
||||||
|
if progress == nil {
|
||||||
|
pendingByID = make(map[string]toolCallPendingInfo)
|
||||||
|
pendingQueueByAgent = make(map[string][]string)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := ""
|
||||||
|
if err != nil {
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
for _, tc := range pendingByID {
|
||||||
|
toolName := tc.ToolName
|
||||||
|
if strings.TrimSpace(toolName) == "" {
|
||||||
|
toolName = "unknown"
|
||||||
|
}
|
||||||
|
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), map[string]interface{}{
|
||||||
|
"toolName": toolName,
|
||||||
|
"success": false,
|
||||||
|
"isError": true,
|
||||||
|
"result": msg,
|
||||||
|
"resultPreview": msg,
|
||||||
|
"toolCallId": tc.ToolCallID,
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"einoAgent": tc.EinoAgent,
|
||||||
|
"einoRole": tc.EinoRole,
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pendingByID = make(map[string]toolCallPendingInfo)
|
||||||
|
pendingQueueByAgent = make(map[string][]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerCfg := adk.RunnerConfig{
|
||||||
|
Agent: da,
|
||||||
|
EnableStreaming: true,
|
||||||
|
}
|
||||||
|
if cp := strings.TrimSpace(args.CheckpointDir); cp != "" {
|
||||||
|
cpDir := filepath.Join(cp, sanitizeEinoPathSegment(conversationID))
|
||||||
|
st, stErr := newFileCheckPointStore(cpDir)
|
||||||
|
if stErr != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Warn("eino checkpoint store disabled", zap.String("dir", cpDir), zap.Error(stErr))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runnerCfg.CheckPointStore = st
|
||||||
|
if logger != nil {
|
||||||
|
logger.Info("eino runner: checkpoint store enabled", zap.String("dir", cpDir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runner := adk.NewRunner(ctx, runnerCfg)
|
||||||
|
iter := runner.Run(ctx, msgs)
|
||||||
|
|
||||||
|
for {
|
||||||
|
ev, ok := iter.Next()
|
||||||
|
if !ok {
|
||||||
|
lastRunMsgs = msgs
|
||||||
|
break attemptLoop
|
||||||
|
}
|
||||||
|
if ev == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ev.Err != nil {
|
||||||
|
canRetry := attempt+1 < maxToolCallRecoveryAttempts
|
||||||
|
|
||||||
|
if canRetry && isRecoverableToolCallArgumentsJSONError(ev.Err) {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Warn("eino: recoverable tool-call JSON error from model/API", zap.Error(ev.Err), zap.Int("attempt", attempt))
|
||||||
|
}
|
||||||
|
retryHints = append(retryHints, toolCallArgumentsJSONRetryHint())
|
||||||
|
if progress != nil {
|
||||||
|
progress("eino_recovery", toolCallArgumentsJSONRecoveryTimelineMessage(attempt), map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"source": "eino",
|
||||||
|
"einoRetry": attempt,
|
||||||
|
"runIndex": attempt + 1,
|
||||||
|
"maxRuns": maxToolCallRecoveryAttempts,
|
||||||
|
"reason": "invalid_tool_arguments_json",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue attemptLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
if canRetry && isRecoverableToolExecutionError(ev.Err) {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Warn("eino: recoverable tool execution error, will retry with corrective hint",
|
||||||
|
zap.Error(ev.Err), zap.Int("attempt", attempt))
|
||||||
|
}
|
||||||
|
flushAllPendingAsFailed(ev.Err)
|
||||||
|
retryHints = append(retryHints, toolExecutionRetryHint())
|
||||||
|
if progress != nil {
|
||||||
|
progress("eino_recovery", toolExecutionRecoveryTimelineMessage(attempt), map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"source": "eino",
|
||||||
|
"einoRetry": attempt,
|
||||||
|
"runIndex": attempt + 1,
|
||||||
|
"maxRuns": maxToolCallRecoveryAttempts,
|
||||||
|
"reason": "tool_execution_error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue attemptLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
flushAllPendingAsFailed(ev.Err)
|
||||||
|
if progress != nil {
|
||||||
|
progress("error", ev.Err.Error(), map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil, ev.Err
|
||||||
|
}
|
||||||
|
if ev.AgentName != "" && progress != nil {
|
||||||
|
iterEinoAgent := orchestratorName
|
||||||
|
if orchMode == "plan_execute" {
|
||||||
|
if a := strings.TrimSpace(ev.AgentName); a != "" {
|
||||||
|
iterEinoAgent = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if streamsMainAssistant(ev.AgentName) {
|
||||||
|
if einoMainRound == 0 {
|
||||||
|
einoMainRound = 1
|
||||||
|
progress("iteration", "", map[string]interface{}{
|
||||||
|
"iteration": 1,
|
||||||
|
"einoScope": "main",
|
||||||
|
"einoRole": "orchestrator",
|
||||||
|
"einoAgent": iterEinoAgent,
|
||||||
|
"orchestration": orchMode,
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
} else if einoLastAgent != "" && !streamsMainAssistant(einoLastAgent) {
|
||||||
|
einoMainRound++
|
||||||
|
progress("iteration", "", map[string]interface{}{
|
||||||
|
"iteration": einoMainRound,
|
||||||
|
"einoScope": "main",
|
||||||
|
"einoRole": "orchestrator",
|
||||||
|
"einoAgent": iterEinoAgent,
|
||||||
|
"orchestration": orchMode,
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
einoLastAgent = ev.AgentName
|
||||||
|
progress("progress", fmt.Sprintf("[Eino] %s", ev.AgentName), map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": einoRoleTag(ev.AgentName),
|
||||||
|
"orchestration": orchMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if ev.Output == nil || ev.Output.MessageOutput == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mv := ev.Output.MessageOutput
|
||||||
|
|
||||||
|
if mv.IsStreaming && mv.MessageStream != nil {
|
||||||
|
streamHeaderSent := false
|
||||||
|
var reasoningStreamID string
|
||||||
|
var toolStreamFragments []schema.ToolCall
|
||||||
|
var subAssistantBuf strings.Builder
|
||||||
|
var subReplyStreamID string
|
||||||
|
var mainAssistantBuf strings.Builder
|
||||||
|
for {
|
||||||
|
chunk, rerr := mv.MessageStream.Recv()
|
||||||
|
if rerr != nil {
|
||||||
|
if errors.Is(rerr, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if logger != nil {
|
||||||
|
logger.Warn("eino stream recv", zap.Error(rerr))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if chunk == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if progress != nil && strings.TrimSpace(chunk.ReasoningContent) != "" {
|
||||||
|
if reasoningStreamID == "" {
|
||||||
|
reasoningStreamID = fmt.Sprintf("eino-reasoning-%s-%d", conversationID, atomic.AddInt64(&reasoningStreamSeq, 1))
|
||||||
|
progress("thinking_stream_start", " ", map[string]interface{}{
|
||||||
|
"streamId": reasoningStreamID,
|
||||||
|
"source": "eino",
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": einoRoleTag(ev.AgentName),
|
||||||
|
"orchestration": orchMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
progress("thinking_stream_delta", chunk.ReasoningContent, map[string]interface{}{
|
||||||
|
"streamId": reasoningStreamID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if chunk.Content != "" {
|
||||||
|
if progress != nil && streamsMainAssistant(ev.AgentName) {
|
||||||
|
if !streamHeaderSent {
|
||||||
|
progress("response_start", "", map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"mcpExecutionIds": snapshotMCPIDs(),
|
||||||
|
"messageGeneratedBy": "eino:" + ev.AgentName,
|
||||||
|
"einoRole": "orchestrator",
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"orchestration": orchMode,
|
||||||
|
})
|
||||||
|
streamHeaderSent = true
|
||||||
|
}
|
||||||
|
progress("response_delta", chunk.Content, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"mcpExecutionIds": snapshotMCPIDs(),
|
||||||
|
"einoRole": "orchestrator",
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"orchestration": orchMode,
|
||||||
|
})
|
||||||
|
mainAssistantBuf.WriteString(chunk.Content)
|
||||||
|
} else if !streamsMainAssistant(ev.AgentName) {
|
||||||
|
if progress != nil {
|
||||||
|
if subReplyStreamID == "" {
|
||||||
|
subReplyStreamID = fmt.Sprintf("eino-sub-reply-%s-%d", conversationID, atomic.AddInt64(&einoSubReplyStreamSeq, 1))
|
||||||
|
progress("eino_agent_reply_stream_start", "", map[string]interface{}{
|
||||||
|
"streamId": subReplyStreamID,
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": "sub",
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
progress("eino_agent_reply_stream_delta", chunk.Content, map[string]interface{}{
|
||||||
|
"streamId": subReplyStreamID,
|
||||||
|
"conversationId": conversationID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
subAssistantBuf.WriteString(chunk.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(chunk.ToolCalls) > 0 {
|
||||||
|
toolStreamFragments = append(toolStreamFragments, chunk.ToolCalls...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if streamsMainAssistant(ev.AgentName) {
|
||||||
|
if s := strings.TrimSpace(mainAssistantBuf.String()); s != "" {
|
||||||
|
lastAssistant = s
|
||||||
|
if orchMode == "plan_execute" && strings.EqualFold(strings.TrimSpace(ev.AgentName), "executor") {
|
||||||
|
lastPlanExecuteExecutor = UnwrapPlanExecuteUserText(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subAssistantBuf.Len() > 0 && progress != nil {
|
||||||
|
if s := strings.TrimSpace(subAssistantBuf.String()); s != "" {
|
||||||
|
if subReplyStreamID != "" {
|
||||||
|
progress("eino_agent_reply_stream_end", s, map[string]interface{}{
|
||||||
|
"streamId": subReplyStreamID,
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": "sub",
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
progress("eino_agent_reply", s, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": "sub",
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var lastToolChunk *schema.Message
|
||||||
|
if merged := mergeStreamingToolCallFragments(toolStreamFragments); len(merged) > 0 {
|
||||||
|
lastToolChunk = &schema.Message{ToolCalls: merged}
|
||||||
|
}
|
||||||
|
tryEmitToolCallsOnce(lastToolChunk, ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, gerr := mv.GetMessage()
|
||||||
|
if gerr != nil || msg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tryEmitToolCallsOnce(mergeMessageToolCalls(msg), ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
|
||||||
|
|
||||||
|
if mv.Role == schema.Assistant {
|
||||||
|
if progress != nil && strings.TrimSpace(msg.ReasoningContent) != "" {
|
||||||
|
progress("thinking", strings.TrimSpace(msg.ReasoningContent), map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"source": "eino",
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": einoRoleTag(ev.AgentName),
|
||||||
|
"orchestration": orchMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
body := strings.TrimSpace(msg.Content)
|
||||||
|
if body != "" {
|
||||||
|
if streamsMainAssistant(ev.AgentName) {
|
||||||
|
if progress != nil {
|
||||||
|
progress("response_start", "", map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"mcpExecutionIds": snapshotMCPIDs(),
|
||||||
|
"messageGeneratedBy": "eino:" + ev.AgentName,
|
||||||
|
"einoRole": "orchestrator",
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"orchestration": orchMode,
|
||||||
|
})
|
||||||
|
progress("response_delta", body, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"mcpExecutionIds": snapshotMCPIDs(),
|
||||||
|
"einoRole": "orchestrator",
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"orchestration": orchMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
lastAssistant = body
|
||||||
|
if orchMode == "plan_execute" && strings.EqualFold(strings.TrimSpace(ev.AgentName), "executor") {
|
||||||
|
lastPlanExecuteExecutor = UnwrapPlanExecuteUserText(body)
|
||||||
|
}
|
||||||
|
} else if progress != nil {
|
||||||
|
progress("eino_agent_reply", body, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": "sub",
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mv.Role == schema.Tool && progress != nil {
|
||||||
|
toolName := msg.ToolName
|
||||||
|
if toolName == "" {
|
||||||
|
toolName = mv.ToolName
|
||||||
|
}
|
||||||
|
|
||||||
|
content := msg.Content
|
||||||
|
isErr := false
|
||||||
|
if strings.HasPrefix(content, einomcp.ToolErrorPrefix) {
|
||||||
|
isErr = true
|
||||||
|
content = strings.TrimPrefix(content, einomcp.ToolErrorPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
preview := content
|
||||||
|
if len(preview) > 200 {
|
||||||
|
preview = preview[:200] + "..."
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"toolName": toolName,
|
||||||
|
"success": !isErr,
|
||||||
|
"isError": isErr,
|
||||||
|
"result": content,
|
||||||
|
"resultPreview": preview,
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"einoAgent": ev.AgentName,
|
||||||
|
"einoRole": einoRoleTag(ev.AgentName),
|
||||||
|
"source": "eino",
|
||||||
|
}
|
||||||
|
toolCallID := strings.TrimSpace(msg.ToolCallID)
|
||||||
|
if toolCallID == "" {
|
||||||
|
if inferred, ok := popNextPendingForAgent(ev.AgentName); ok {
|
||||||
|
toolCallID = inferred.ToolCallID
|
||||||
|
} else if inferred, ok := popNextPendingForAgent(orchestratorName); ok {
|
||||||
|
toolCallID = inferred.ToolCallID
|
||||||
|
} else if inferred, ok := popNextPendingForAgent(""); ok {
|
||||||
|
toolCallID = inferred.ToolCallID
|
||||||
|
} else {
|
||||||
|
for id := range pendingByID {
|
||||||
|
toolCallID = id
|
||||||
|
delete(pendingByID, id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removePendingByID(toolCallID)
|
||||||
|
}
|
||||||
|
if toolCallID != "" {
|
||||||
|
data["toolCallId"] = toolCallID
|
||||||
|
}
|
||||||
|
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpIDsMu.Lock()
|
||||||
|
ids := append([]string(nil), *mcpIDs...)
|
||||||
|
mcpIDsMu.Unlock()
|
||||||
|
|
||||||
|
histJSON, _ := json.Marshal(lastRunMsgs)
|
||||||
|
cleaned := strings.TrimSpace(lastAssistant)
|
||||||
|
if orchMode == "plan_execute" {
|
||||||
|
if e := strings.TrimSpace(lastPlanExecuteExecutor); e != "" {
|
||||||
|
cleaned = e
|
||||||
|
} else {
|
||||||
|
cleaned = UnwrapPlanExecuteUserText(cleaned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleaned = dedupeRepeatedParagraphs(cleaned, 80)
|
||||||
|
cleaned = dedupeParagraphsByLineFingerprint(cleaned, 100)
|
||||||
|
out := &RunResult{
|
||||||
|
Response: cleaned,
|
||||||
|
MCPExecutionIDs: ids,
|
||||||
|
LastReActInput: string(histJSON),
|
||||||
|
LastReActOutput: cleaned,
|
||||||
|
}
|
||||||
|
if out.Response == "" {
|
||||||
|
out.Response = emptyHint
|
||||||
|
out.LastReActOutput = out.Response
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -5,11 +5,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
|
|
||||||
"github.com/cloudwego/eino-ext/components/model/openai"
|
"github.com/cloudwego/eino-ext/components/model/openai"
|
||||||
"github.com/cloudwego/eino/adk"
|
"github.com/cloudwego/eino/adk"
|
||||||
"github.com/cloudwego/eino/adk/prebuilt/planexecute"
|
"github.com/cloudwego/eino/adk/prebuilt/planexecute"
|
||||||
"github.com/cloudwego/eino/components/model"
|
"github.com/cloudwego/eino/components/model"
|
||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PlanExecuteRootArgs 构建 Eino adk/prebuilt/planexecute 根 Agent 所需参数。
|
// PlanExecuteRootArgs 构建 Eino adk/prebuilt/planexecute 根 Agent 所需参数。
|
||||||
@@ -20,6 +23,9 @@ type PlanExecuteRootArgs struct {
|
|||||||
ToolsCfg adk.ToolsConfig
|
ToolsCfg adk.ToolsConfig
|
||||||
ExecMaxIter int
|
ExecMaxIter int
|
||||||
LoopMaxIter int
|
LoopMaxIter int
|
||||||
|
// AppCfg / Logger 非空时为 Executor 挂载与 Deep/Supervisor 一致的 Eino summarization 中间件。
|
||||||
|
AppCfg *config.Config
|
||||||
|
Logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPlanExecuteRoot 返回 plan → execute → replan 预置编排根节点(与 Deep / Supervisor 并列)。
|
// NewPlanExecuteRoot 返回 plan → execute → replan 预置编排根节点(与 Deep / Supervisor 并列)。
|
||||||
@@ -41,17 +47,26 @@ func NewPlanExecuteRoot(ctx context.Context, a *PlanExecuteRootArgs) (adk.Resuma
|
|||||||
return nil, fmt.Errorf("plan_execute planner: %w", err)
|
return nil, fmt.Errorf("plan_execute planner: %w", err)
|
||||||
}
|
}
|
||||||
replanner, err := planexecute.NewReplanner(ctx, &planexecute.ReplannerConfig{
|
replanner, err := planexecute.NewReplanner(ctx, &planexecute.ReplannerConfig{
|
||||||
ChatModel: tcm,
|
ChatModel: tcm,
|
||||||
|
GenInputFn: planExecuteReplannerGenInput,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("plan_execute replanner: %w", err)
|
return nil, fmt.Errorf("plan_execute replanner: %w", err)
|
||||||
}
|
}
|
||||||
executor, err := planexecute.NewExecutor(ctx, &planexecute.ExecutorConfig{
|
var execHandlers []adk.ChatModelAgentMiddleware
|
||||||
|
if a.AppCfg != nil {
|
||||||
|
sumMw, sumErr := newEinoSummarizationMiddleware(ctx, a.ExecModel, a.AppCfg, a.Logger)
|
||||||
|
if sumErr != nil {
|
||||||
|
return nil, fmt.Errorf("plan_execute executor summarization: %w", sumErr)
|
||||||
|
}
|
||||||
|
execHandlers = append(execHandlers, sumMw)
|
||||||
|
}
|
||||||
|
executor, err := newPlanExecuteExecutor(ctx, &planexecute.ExecutorConfig{
|
||||||
Model: a.ExecModel,
|
Model: a.ExecModel,
|
||||||
ToolsConfig: a.ToolsCfg,
|
ToolsConfig: a.ToolsCfg,
|
||||||
MaxIterations: a.ExecMaxIter,
|
MaxIterations: a.ExecMaxIter,
|
||||||
GenInputFn: planExecuteExecutorGenInput(a.OrchInstruction),
|
GenInputFn: planExecuteExecutorGenInput(a.OrchInstruction),
|
||||||
})
|
}, execHandlers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("plan_execute executor: %w", err)
|
return nil, fmt.Errorf("plan_execute executor: %w", err)
|
||||||
}
|
}
|
||||||
@@ -100,13 +115,29 @@ func planExecuteFormatInput(input []adk.Message) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func planExecuteFormatExecutedSteps(results []planexecute.ExecutedStep) string {
|
func planExecuteFormatExecutedSteps(results []planexecute.ExecutedStep) string {
|
||||||
|
capped := capPlanExecuteExecutedSteps(results)
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, result := range results {
|
for _, result := range capped {
|
||||||
sb.WriteString(fmt.Sprintf("Step: %s\nResult: %s\n\n", result.Step, result.Result))
|
sb.WriteString(fmt.Sprintf("Step: %s\nResult: %s\n\n", result.Step, result.Result))
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// planExecuteReplannerGenInput 与 Eino 默认 Replanner 输入一致,但 executed_steps 经 cap 后再写入 prompt。
|
||||||
|
func planExecuteReplannerGenInput(ctx context.Context, in *planexecute.ExecutionContext) ([]adk.Message, error) {
|
||||||
|
planContent, err := in.Plan.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return planexecute.ReplannerPrompt.Format(ctx, map[string]any{
|
||||||
|
"plan": string(planContent),
|
||||||
|
"input": planExecuteFormatInput(in.UserInput),
|
||||||
|
"executed_steps": planExecuteFormatExecutedSteps(in.ExecutedSteps),
|
||||||
|
"plan_tool": planexecute.PlanToolInfo.Name,
|
||||||
|
"respond_tool": planexecute.RespondToolInfo.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// planExecuteStreamsMainAssistant 将规划/执行/重规划各阶段助手流式输出映射到主对话区。
|
// planExecuteStreamsMainAssistant 将规划/执行/重规划各阶段助手流式输出映射到主对话区。
|
||||||
func planExecuteStreamsMainAssistant(agent string) bool {
|
func planExecuteStreamsMainAssistant(agent string) bool {
|
||||||
if agent == "" {
|
if agent == "" {
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/agent"
|
||||||
|
"cyberstrike-ai/internal/config"
|
||||||
|
"cyberstrike-ai/internal/einomcp"
|
||||||
|
"cyberstrike-ai/internal/openai"
|
||||||
|
|
||||||
|
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||||
|
"github.com/cloudwego/eino/adk"
|
||||||
|
"github.com/cloudwego/eino/compose"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// einoSingleAgentName 与 ChatModelAgent.Name 一致,供流式事件映射主对话区。
|
||||||
|
const einoSingleAgentName = "cyberstrike-eino-single"
|
||||||
|
|
||||||
|
// RunEinoSingleChatModelAgent 使用 Eino adk.NewChatModelAgent + adk.NewRunner.Run(官方 Quick Start 的 Query 同属 Runner API;此处用历史 + 用户消息切片等价于多轮 Query)。
|
||||||
|
// 不替代既有原生 ReAct;与 RunDeepAgent 共享 runEinoADKAgentLoop 的 SSE 映射与 MCP 桥。
|
||||||
|
func RunEinoSingleChatModelAgent(
|
||||||
|
ctx context.Context,
|
||||||
|
appCfg *config.Config,
|
||||||
|
ma *config.MultiAgentConfig,
|
||||||
|
ag *agent.Agent,
|
||||||
|
logger *zap.Logger,
|
||||||
|
conversationID string,
|
||||||
|
userMessage string,
|
||||||
|
history []agent.ChatMessage,
|
||||||
|
roleTools []string,
|
||||||
|
roleSkills []string,
|
||||||
|
progress func(eventType, message string, data interface{}),
|
||||||
|
) (*RunResult, error) {
|
||||||
|
if appCfg == nil || ag == nil {
|
||||||
|
return nil, fmt.Errorf("eino single: 配置或 Agent 为空")
|
||||||
|
}
|
||||||
|
if ma == nil {
|
||||||
|
return nil, fmt.Errorf("eino single: multi_agent 配置为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
einoLoc, einoSkillMW, einoFSTools, skillsRoot, einoErr := prepareEinoSkills(ctx, appCfg.SkillsDir, ma, logger)
|
||||||
|
if einoErr != nil {
|
||||||
|
return nil, einoErr
|
||||||
|
}
|
||||||
|
|
||||||
|
holder := &einomcp.ConversationHolder{}
|
||||||
|
holder.Set(conversationID)
|
||||||
|
|
||||||
|
var mcpIDsMu sync.Mutex
|
||||||
|
var mcpIDs []string
|
||||||
|
recorder := func(id string) {
|
||||||
|
if id == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mcpIDsMu.Lock()
|
||||||
|
mcpIDs = append(mcpIDs, id)
|
||||||
|
mcpIDsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotMCPIDs := func() []string {
|
||||||
|
mcpIDsMu.Lock()
|
||||||
|
defer mcpIDsMu.Unlock()
|
||||||
|
out := make([]string, len(mcpIDs))
|
||||||
|
copy(out, mcpIDs)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
toolOutputChunk := func(toolName, toolCallID, chunk string) {
|
||||||
|
if progress == nil || toolCallID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
progress("tool_result_delta", chunk, map[string]interface{}{
|
||||||
|
"toolName": toolName,
|
||||||
|
"toolCallId": toolCallID,
|
||||||
|
"index": 0,
|
||||||
|
"total": 0,
|
||||||
|
"iteration": 0,
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mainDefs := ag.ToolsForRole(roleTools)
|
||||||
|
mainTools, err := einomcp.ToolsFromDefinitions(ag, holder, mainDefs, recorder, toolOutputChunk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mainToolsForCfg, mainOrchestratorPre, err := prependEinoMiddlewares(ctx, &ma.EinoMiddleware, einoMWMain, mainTools, einoLoc, skillsRoot, conversationID, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("eino single eino 中间件: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: 30 * time.Minute,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 300 * time.Second,
|
||||||
|
KeepAlive: 300 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
MaxIdleConnsPerHost: 10,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 30 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 60 * time.Minute,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
httpClient = openai.NewEinoHTTPClient(&appCfg.OpenAI, httpClient)
|
||||||
|
|
||||||
|
baseModelCfg := &einoopenai.ChatModelConfig{
|
||||||
|
APIKey: appCfg.OpenAI.APIKey,
|
||||||
|
BaseURL: strings.TrimSuffix(appCfg.OpenAI.BaseURL, "/"),
|
||||||
|
Model: appCfg.OpenAI.Model,
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
mainModel, err := einoopenai.NewChatModel(ctx, baseModelCfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("eino single 模型: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainSumMw, err := newEinoSummarizationMiddleware(ctx, mainModel, appCfg, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("eino single summarization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers := make([]adk.ChatModelAgentMiddleware, 0, 4)
|
||||||
|
if len(mainOrchestratorPre) > 0 {
|
||||||
|
handlers = append(handlers, mainOrchestratorPre...)
|
||||||
|
}
|
||||||
|
if einoSkillMW != nil {
|
||||||
|
if einoFSTools && einoLoc != nil {
|
||||||
|
fsMw, fsErr := subAgentFilesystemMiddleware(ctx, einoLoc)
|
||||||
|
if fsErr != nil {
|
||||||
|
return nil, fmt.Errorf("eino single filesystem 中间件: %w", fsErr)
|
||||||
|
}
|
||||||
|
handlers = append(handlers, fsMw)
|
||||||
|
}
|
||||||
|
handlers = append(handlers, einoSkillMW)
|
||||||
|
}
|
||||||
|
handlers = append(handlers, mainSumMw)
|
||||||
|
|
||||||
|
maxIter := ma.MaxIteration
|
||||||
|
if maxIter <= 0 {
|
||||||
|
maxIter = appCfg.Agent.MaxIterations
|
||||||
|
}
|
||||||
|
if maxIter <= 0 {
|
||||||
|
maxIter = 40
|
||||||
|
}
|
||||||
|
|
||||||
|
mainToolsCfg := adk.ToolsConfig{
|
||||||
|
ToolsNodeConfig: compose.ToolsNodeConfig{
|
||||||
|
Tools: mainToolsForCfg,
|
||||||
|
UnknownToolsHandler: einomcp.UnknownToolReminderHandler(),
|
||||||
|
ToolCallMiddlewares: []compose.ToolMiddleware{
|
||||||
|
{Invokable: softRecoveryToolCallMiddleware()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EmitInternalEvents: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
chatCfg := &adk.ChatModelAgentConfig{
|
||||||
|
Name: einoSingleAgentName,
|
||||||
|
Description: "Eino ADK ChatModelAgent with MCP tools for authorized security testing.",
|
||||||
|
Instruction: ag.EinoSingleAgentSystemInstruction(roleSkills),
|
||||||
|
Model: mainModel,
|
||||||
|
ToolsConfig: mainToolsCfg,
|
||||||
|
MaxIterations: maxIter,
|
||||||
|
Handlers: handlers,
|
||||||
|
}
|
||||||
|
outKey, modelRetry, _ := deepExtrasFromConfig(ma)
|
||||||
|
if outKey != "" {
|
||||||
|
chatCfg.OutputKey = outKey
|
||||||
|
}
|
||||||
|
if modelRetry != nil {
|
||||||
|
chatCfg.ModelRetryConfig = modelRetry
|
||||||
|
}
|
||||||
|
|
||||||
|
chatAgent, err := adk.NewChatModelAgent(ctx, chatCfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("eino single NewChatModelAgent: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseMsgs := historyToMessages(history)
|
||||||
|
baseMsgs = append(baseMsgs, schema.UserMessage(userMessage))
|
||||||
|
|
||||||
|
streamsMainAssistant := func(agent string) bool {
|
||||||
|
return agent == "" || agent == einoSingleAgentName
|
||||||
|
}
|
||||||
|
einoRoleTag := func(agent string) string {
|
||||||
|
_ = agent
|
||||||
|
return "orchestrator"
|
||||||
|
}
|
||||||
|
|
||||||
|
return runEinoADKAgentLoop(ctx, &einoADKRunLoopArgs{
|
||||||
|
OrchMode: "eino_single",
|
||||||
|
OrchestratorName: einoSingleAgentName,
|
||||||
|
ConversationID: conversationID,
|
||||||
|
Progress: progress,
|
||||||
|
Logger: logger,
|
||||||
|
SnapshotMCPIDs: snapshotMCPIDs,
|
||||||
|
StreamsMainAssistant: streamsMainAssistant,
|
||||||
|
EinoRoleTag: einoRoleTag,
|
||||||
|
CheckpointDir: ma.EinoMiddleware.CheckpointDir,
|
||||||
|
McpIDsMu: &mcpIDsMu,
|
||||||
|
McpIDs: &mcpIDs,
|
||||||
|
DA: chatAgent,
|
||||||
|
EmptyResponseMessage: "(Eino ADK 单代理会话已完成,但未捕获到助手文本输出。请查看过程详情或日志。)",
|
||||||
|
}, baseMsgs)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/adk"
|
||||||
|
"github.com/cloudwego/eino/adk/prebuilt/planexecute"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newPlanExecuteExecutor 与 planexecute.NewExecutor 行为一致,但可为执行器注入 Handlers(例如 summarization 中间件)。
|
||||||
|
func newPlanExecuteExecutor(ctx context.Context, cfg *planexecute.ExecutorConfig, handlers []adk.ChatModelAgentMiddleware) (adk.Agent, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("plan_execute: ExecutorConfig 为空")
|
||||||
|
}
|
||||||
|
if cfg.Model == nil {
|
||||||
|
return nil, fmt.Errorf("plan_execute: Executor Model 为空")
|
||||||
|
}
|
||||||
|
genInputFn := cfg.GenInputFn
|
||||||
|
if genInputFn == nil {
|
||||||
|
genInputFn = planExecuteDefaultGenExecutorInput
|
||||||
|
}
|
||||||
|
genInput := func(ctx context.Context, instruction string, _ *adk.AgentInput) ([]adk.Message, error) {
|
||||||
|
plan, ok := adk.GetSessionValue(ctx, planexecute.PlanSessionKey)
|
||||||
|
if !ok {
|
||||||
|
panic("impossible: plan not found")
|
||||||
|
}
|
||||||
|
plan_ := plan.(planexecute.Plan)
|
||||||
|
|
||||||
|
userInput, ok := adk.GetSessionValue(ctx, planexecute.UserInputSessionKey)
|
||||||
|
if !ok {
|
||||||
|
panic("impossible: user input not found")
|
||||||
|
}
|
||||||
|
userInput_ := userInput.([]adk.Message)
|
||||||
|
|
||||||
|
var executedSteps_ []planexecute.ExecutedStep
|
||||||
|
executedStep, ok := adk.GetSessionValue(ctx, planexecute.ExecutedStepsSessionKey)
|
||||||
|
if ok {
|
||||||
|
executedSteps_ = executedStep.([]planexecute.ExecutedStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
in := &planexecute.ExecutionContext{
|
||||||
|
UserInput: userInput_,
|
||||||
|
Plan: plan_,
|
||||||
|
ExecutedSteps: executedSteps_,
|
||||||
|
}
|
||||||
|
return genInputFn(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
agentCfg := &adk.ChatModelAgentConfig{
|
||||||
|
Name: "executor",
|
||||||
|
Description: "an executor agent",
|
||||||
|
Model: cfg.Model,
|
||||||
|
ToolsConfig: cfg.ToolsConfig,
|
||||||
|
GenModelInput: genInput,
|
||||||
|
MaxIterations: cfg.MaxIterations,
|
||||||
|
OutputKey: planexecute.ExecutedStepSessionKey,
|
||||||
|
}
|
||||||
|
if len(handlers) > 0 {
|
||||||
|
agentCfg.Handlers = handlers
|
||||||
|
}
|
||||||
|
return adk.NewChatModelAgent(ctx, agentCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// planExecuteDefaultGenExecutorInput 对齐 Eino planexecute.defaultGenExecutorInputFn(包外不可引用默认实现)。
|
||||||
|
func planExecuteDefaultGenExecutorInput(ctx context.Context, in *planexecute.ExecutionContext) ([]adk.Message, error) {
|
||||||
|
planContent, err := in.Plan.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return planexecute.ExecutorPrompt.Format(ctx, map[string]any{
|
||||||
|
"input": planExecuteFormatInput(in.UserInput),
|
||||||
|
"plan": string(planContent),
|
||||||
|
"executed_steps": planExecuteFormatExecutedSteps(in.ExecutedSteps),
|
||||||
|
"step": in.Plan.FirstStep(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/adk/prebuilt/planexecute"
|
||||||
|
)
|
||||||
|
|
||||||
|
// plan_execute 的 Replanner / Executor prompt 会线性拼接每步 Result;无界时易撑爆上下文。
|
||||||
|
// 此处仅约束「写入模型 prompt 的视图」,不修改 Eino session 中的原始 ExecutedSteps。
|
||||||
|
|
||||||
|
const (
|
||||||
|
planExecuteMaxStepResultRunes = 12000
|
||||||
|
planExecuteKeepLastSteps = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
func truncateRunesWithSuffix(s string, maxRunes int, suffix string) string {
|
||||||
|
if maxRunes <= 0 || s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
rs := []rune(s)
|
||||||
|
if len(rs) <= maxRunes {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return string(rs[:maxRunes]) + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
// capPlanExecuteExecutedSteps 折叠较早步骤、截断单步过长结果,供 prompt 使用。
|
||||||
|
func capPlanExecuteExecutedSteps(steps []planexecute.ExecutedStep) []planexecute.ExecutedStep {
|
||||||
|
if len(steps) == 0 {
|
||||||
|
return steps
|
||||||
|
}
|
||||||
|
out := make([]planexecute.ExecutedStep, 0, len(steps)+1)
|
||||||
|
start := 0
|
||||||
|
if len(steps) > planExecuteKeepLastSteps {
|
||||||
|
start = len(steps) - planExecuteKeepLastSteps
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf("(上文已完成 %d 步;此处仅保留步骤标题以节省上下文,完整输出已省略。后续 %d 步仍保留正文。)\n",
|
||||||
|
start, planExecuteKeepLastSteps))
|
||||||
|
for i := 0; i < start; i++ {
|
||||||
|
b.WriteString(fmt.Sprintf("- %s\n", steps[i].Step))
|
||||||
|
}
|
||||||
|
out = append(out, planexecute.ExecutedStep{
|
||||||
|
Step: "[Earlier steps — titles only]",
|
||||||
|
Result: strings.TrimRight(b.String(), "\n"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
suffix := "\n…[step result truncated]"
|
||||||
|
for i := start; i < len(steps); i++ {
|
||||||
|
e := steps[i]
|
||||||
|
if utf8.RuneCountInString(e.Result) > planExecuteMaxStepResultRunes {
|
||||||
|
e.Result = truncateRunesWithSuffix(e.Result, planExecuteMaxStepResultRunes, suffix)
|
||||||
|
}
|
||||||
|
out = append(out, e)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package multiagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/adk/prebuilt/planexecute"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCapPlanExecuteExecutedSteps_TruncatesLongResult(t *testing.T) {
|
||||||
|
long := strings.Repeat("x", planExecuteMaxStepResultRunes+500)
|
||||||
|
steps := []planexecute.ExecutedStep{{Step: "s1", Result: long}}
|
||||||
|
out := capPlanExecuteExecutedSteps(steps)
|
||||||
|
if len(out) != 1 {
|
||||||
|
t.Fatalf("len=%d", len(out))
|
||||||
|
}
|
||||||
|
if !strings.Contains(out[0].Result, "truncated") {
|
||||||
|
t.Fatalf("expected truncation marker in %q", out[0].Result[:80])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapPlanExecuteExecutedSteps_FoldsEarlySteps(t *testing.T) {
|
||||||
|
var steps []planexecute.ExecutedStep
|
||||||
|
for i := 0; i < planExecuteKeepLastSteps+5; i++ {
|
||||||
|
steps = append(steps, planexecute.ExecutedStep{Step: "step", Result: "ok"})
|
||||||
|
}
|
||||||
|
out := capPlanExecuteExecutedSteps(steps)
|
||||||
|
if len(out) != planExecuteKeepLastSteps+1 {
|
||||||
|
t.Fatalf("want %d entries, got %d", planExecuteKeepLastSteps+1, len(out))
|
||||||
|
}
|
||||||
|
if out[0].Step != "[Earlier steps — titles only]" {
|
||||||
|
t.Fatalf("first entry: %#v", out[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-483
@@ -4,16 +4,12 @@ package multiagent
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"path/filepath"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cyberstrike-ai/internal/agent"
|
"cyberstrike-ai/internal/agent"
|
||||||
@@ -398,6 +394,8 @@ func RunDeepAgent(
|
|||||||
ToolsCfg: mainToolsCfg,
|
ToolsCfg: mainToolsCfg,
|
||||||
ExecMaxIter: deepMaxIter,
|
ExecMaxIter: deepMaxIter,
|
||||||
LoopMaxIter: ma.PlanExecuteLoopMaxIterations,
|
LoopMaxIter: ma.PlanExecuteLoopMaxIterations,
|
||||||
|
AppCfg: appCfg,
|
||||||
|
Logger: logger,
|
||||||
})
|
})
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return nil, perr
|
return nil, perr
|
||||||
@@ -482,485 +480,21 @@ func RunDeepAgent(
|
|||||||
return "sub"
|
return "sub"
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastRunMsgs []adk.Message
|
return runEinoADKAgentLoop(ctx, &einoADKRunLoopArgs{
|
||||||
var lastAssistant string
|
OrchMode: orchMode,
|
||||||
// plan_execute:最后一轮 assistant 常被 replanner 的 JSON 覆盖,单独保留 executor 对用户文本。
|
OrchestratorName: orchestratorName,
|
||||||
var lastPlanExecuteExecutor string
|
ConversationID: conversationID,
|
||||||
|
Progress: progress,
|
||||||
// retryHints tracks the corrective hint to append for each retry attempt.
|
Logger: logger,
|
||||||
// Index i corresponds to the hint that will be appended on attempt i+1.
|
SnapshotMCPIDs: snapshotMCPIDs,
|
||||||
var retryHints []adk.Message
|
StreamsMainAssistant: streamsMainAssistant,
|
||||||
|
EinoRoleTag: einoRoleTag,
|
||||||
attemptLoop:
|
CheckpointDir: ma.EinoMiddleware.CheckpointDir,
|
||||||
for attempt := 0; attempt < maxToolCallRecoveryAttempts; attempt++ {
|
McpIDsMu: &mcpIDsMu,
|
||||||
msgs := make([]adk.Message, 0, len(baseMsgs)+len(retryHints))
|
McpIDs: &mcpIDs,
|
||||||
msgs = append(msgs, baseMsgs...)
|
DA: da,
|
||||||
msgs = append(msgs, retryHints...)
|
EmptyResponseMessage: "(Eino 多代理编排已完成,但未捕获到助手文本输出。请查看过程详情或日志。)",
|
||||||
|
}, baseMsgs)
|
||||||
if attempt > 0 {
|
|
||||||
mcpIDsMu.Lock()
|
|
||||||
mcpIDs = mcpIDs[:0]
|
|
||||||
mcpIDsMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 仅保留主代理最后一次 assistant 输出;每轮重试重置,避免拼接失败轮次的片段。
|
|
||||||
lastAssistant = ""
|
|
||||||
lastPlanExecuteExecutor = ""
|
|
||||||
var reasoningStreamSeq int64
|
|
||||||
var einoSubReplyStreamSeq int64
|
|
||||||
toolEmitSeen := make(map[string]struct{})
|
|
||||||
var einoMainRound int
|
|
||||||
var einoLastAgent string
|
|
||||||
subAgentToolStep := make(map[string]int)
|
|
||||||
// Track tool calls emitted in this attempt so we can:
|
|
||||||
// - attach toolCallId to tool_result when framework omits it
|
|
||||||
// - flush running tool calls as failed when a recoverable tool execution error happens
|
|
||||||
pendingByID := make(map[string]toolCallPendingInfo)
|
|
||||||
pendingQueueByAgent := make(map[string][]string)
|
|
||||||
markPending := func(tc toolCallPendingInfo) {
|
|
||||||
if tc.ToolCallID == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pendingByID[tc.ToolCallID] = tc
|
|
||||||
pendingQueueByAgent[tc.EinoAgent] = append(pendingQueueByAgent[tc.EinoAgent], tc.ToolCallID)
|
|
||||||
}
|
|
||||||
popNextPendingForAgent := func(agentName string) (toolCallPendingInfo, bool) {
|
|
||||||
q := pendingQueueByAgent[agentName]
|
|
||||||
for len(q) > 0 {
|
|
||||||
id := q[0]
|
|
||||||
q = q[1:]
|
|
||||||
pendingQueueByAgent[agentName] = q
|
|
||||||
if tc, ok := pendingByID[id]; ok {
|
|
||||||
delete(pendingByID, id)
|
|
||||||
return tc, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return toolCallPendingInfo{}, false
|
|
||||||
}
|
|
||||||
removePendingByID := func(toolCallID string) {
|
|
||||||
if toolCallID == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delete(pendingByID, toolCallID)
|
|
||||||
// queue cleanup is lazy in popNextPendingForAgent
|
|
||||||
}
|
|
||||||
flushAllPendingAsFailed := func(err error) {
|
|
||||||
if progress == nil {
|
|
||||||
pendingByID = make(map[string]toolCallPendingInfo)
|
|
||||||
pendingQueueByAgent = make(map[string][]string)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := ""
|
|
||||||
if err != nil {
|
|
||||||
msg = err.Error()
|
|
||||||
}
|
|
||||||
for _, tc := range pendingByID {
|
|
||||||
toolName := tc.ToolName
|
|
||||||
if strings.TrimSpace(toolName) == "" {
|
|
||||||
toolName = "unknown"
|
|
||||||
}
|
|
||||||
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), map[string]interface{}{
|
|
||||||
"toolName": toolName,
|
|
||||||
"success": false,
|
|
||||||
"isError": true,
|
|
||||||
"result": msg,
|
|
||||||
"resultPreview": msg,
|
|
||||||
"toolCallId": tc.ToolCallID,
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"einoAgent": tc.EinoAgent,
|
|
||||||
"einoRole": tc.EinoRole,
|
|
||||||
"source": "eino",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pendingByID = make(map[string]toolCallPendingInfo)
|
|
||||||
pendingQueueByAgent = make(map[string][]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
runnerCfg := adk.RunnerConfig{
|
|
||||||
Agent: da,
|
|
||||||
EnableStreaming: true,
|
|
||||||
}
|
|
||||||
if cp := strings.TrimSpace(ma.EinoMiddleware.CheckpointDir); cp != "" {
|
|
||||||
cpDir := filepath.Join(cp, sanitizeEinoPathSegment(conversationID))
|
|
||||||
st, stErr := newFileCheckPointStore(cpDir)
|
|
||||||
if stErr != nil {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("eino checkpoint store disabled", zap.String("dir", cpDir), zap.Error(stErr))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runnerCfg.CheckPointStore = st
|
|
||||||
if logger != nil {
|
|
||||||
logger.Info("eino runner: checkpoint store enabled", zap.String("dir", cpDir))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runner := adk.NewRunner(ctx, runnerCfg)
|
|
||||||
iter := runner.Run(ctx, msgs)
|
|
||||||
|
|
||||||
for {
|
|
||||||
ev, ok := iter.Next()
|
|
||||||
if !ok {
|
|
||||||
lastRunMsgs = msgs
|
|
||||||
break attemptLoop
|
|
||||||
}
|
|
||||||
if ev == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ev.Err != nil {
|
|
||||||
canRetry := attempt+1 < maxToolCallRecoveryAttempts
|
|
||||||
|
|
||||||
// Recoverable: API-level JSON argument validation error.
|
|
||||||
if canRetry && isRecoverableToolCallArgumentsJSONError(ev.Err) {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("eino: recoverable tool-call JSON error from model/API", zap.Error(ev.Err), zap.Int("attempt", attempt))
|
|
||||||
}
|
|
||||||
retryHints = append(retryHints, toolCallArgumentsJSONRetryHint())
|
|
||||||
if progress != nil {
|
|
||||||
progress("eino_recovery", toolCallArgumentsJSONRecoveryTimelineMessage(attempt), map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"source": "eino",
|
|
||||||
"einoRetry": attempt,
|
|
||||||
"runIndex": attempt + 1,
|
|
||||||
"maxRuns": maxToolCallRecoveryAttempts,
|
|
||||||
"reason": "invalid_tool_arguments_json",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
continue attemptLoop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recoverable: tool execution error (unknown sub-agent, tool not found, bad JSON in args, etc.).
|
|
||||||
if canRetry && isRecoverableToolExecutionError(ev.Err) {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("eino: recoverable tool execution error, will retry with corrective hint",
|
|
||||||
zap.Error(ev.Err), zap.Int("attempt", attempt))
|
|
||||||
}
|
|
||||||
// Ensure UI/tool timeline doesn't get stuck at "running" for tool calls that
|
|
||||||
// will never receive a proper tool_result due to the recoverable error.
|
|
||||||
flushAllPendingAsFailed(ev.Err)
|
|
||||||
retryHints = append(retryHints, toolExecutionRetryHint())
|
|
||||||
if progress != nil {
|
|
||||||
progress("eino_recovery", toolExecutionRecoveryTimelineMessage(attempt), map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"source": "eino",
|
|
||||||
"einoRetry": attempt,
|
|
||||||
"runIndex": attempt + 1,
|
|
||||||
"maxRuns": maxToolCallRecoveryAttempts,
|
|
||||||
"reason": "tool_execution_error",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
continue attemptLoop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-recoverable error.
|
|
||||||
flushAllPendingAsFailed(ev.Err)
|
|
||||||
if progress != nil {
|
|
||||||
progress("error", ev.Err.Error(), map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"source": "eino",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil, ev.Err
|
|
||||||
}
|
|
||||||
if ev.AgentName != "" && progress != nil {
|
|
||||||
iterEinoAgent := orchestratorName
|
|
||||||
if orchMode == "plan_execute" {
|
|
||||||
if a := strings.TrimSpace(ev.AgentName); a != "" {
|
|
||||||
iterEinoAgent = a
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if streamsMainAssistant(ev.AgentName) {
|
|
||||||
if einoMainRound == 0 {
|
|
||||||
einoMainRound = 1
|
|
||||||
progress("iteration", "", map[string]interface{}{
|
|
||||||
"iteration": 1,
|
|
||||||
"einoScope": "main",
|
|
||||||
"einoRole": "orchestrator",
|
|
||||||
"einoAgent": iterEinoAgent,
|
|
||||||
"orchestration": orchMode,
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"source": "eino",
|
|
||||||
})
|
|
||||||
} else if einoLastAgent != "" && !streamsMainAssistant(einoLastAgent) {
|
|
||||||
einoMainRound++
|
|
||||||
progress("iteration", "", map[string]interface{}{
|
|
||||||
"iteration": einoMainRound,
|
|
||||||
"einoScope": "main",
|
|
||||||
"einoRole": "orchestrator",
|
|
||||||
"einoAgent": iterEinoAgent,
|
|
||||||
"orchestration": orchMode,
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"source": "eino",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
einoLastAgent = ev.AgentName
|
|
||||||
progress("progress", fmt.Sprintf("[Eino] %s", ev.AgentName), map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"einoRole": einoRoleTag(ev.AgentName),
|
|
||||||
"orchestration": orchMode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if ev.Output == nil || ev.Output.MessageOutput == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mv := ev.Output.MessageOutput
|
|
||||||
|
|
||||||
if mv.IsStreaming && mv.MessageStream != nil {
|
|
||||||
streamHeaderSent := false
|
|
||||||
var reasoningStreamID string
|
|
||||||
var toolStreamFragments []schema.ToolCall
|
|
||||||
var subAssistantBuf strings.Builder
|
|
||||||
var subReplyStreamID string
|
|
||||||
var mainAssistantBuf strings.Builder
|
|
||||||
for {
|
|
||||||
chunk, rerr := mv.MessageStream.Recv()
|
|
||||||
if rerr != nil {
|
|
||||||
if errors.Is(rerr, io.EOF) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("eino stream recv", zap.Error(rerr))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if chunk == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if progress != nil && strings.TrimSpace(chunk.ReasoningContent) != "" {
|
|
||||||
if reasoningStreamID == "" {
|
|
||||||
reasoningStreamID = fmt.Sprintf("eino-reasoning-%s-%d", conversationID, atomic.AddInt64(&reasoningStreamSeq, 1))
|
|
||||||
progress("thinking_stream_start", " ", map[string]interface{}{
|
|
||||||
"streamId": reasoningStreamID,
|
|
||||||
"source": "eino",
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"einoRole": einoRoleTag(ev.AgentName),
|
|
||||||
"orchestration": orchMode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
progress("thinking_stream_delta", chunk.ReasoningContent, map[string]interface{}{
|
|
||||||
"streamId": reasoningStreamID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if chunk.Content != "" {
|
|
||||||
if progress != nil && streamsMainAssistant(ev.AgentName) {
|
|
||||||
if !streamHeaderSent {
|
|
||||||
progress("response_start", "", map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"mcpExecutionIds": snapshotMCPIDs(),
|
|
||||||
"messageGeneratedBy": "eino:" + ev.AgentName,
|
|
||||||
"einoRole": "orchestrator",
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"orchestration": orchMode,
|
|
||||||
})
|
|
||||||
streamHeaderSent = true
|
|
||||||
}
|
|
||||||
progress("response_delta", chunk.Content, map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"mcpExecutionIds": snapshotMCPIDs(),
|
|
||||||
"einoRole": "orchestrator",
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"orchestration": orchMode,
|
|
||||||
})
|
|
||||||
mainAssistantBuf.WriteString(chunk.Content)
|
|
||||||
} else if !streamsMainAssistant(ev.AgentName) {
|
|
||||||
if progress != nil {
|
|
||||||
if subReplyStreamID == "" {
|
|
||||||
subReplyStreamID = fmt.Sprintf("eino-sub-reply-%s-%d", conversationID, atomic.AddInt64(&einoSubReplyStreamSeq, 1))
|
|
||||||
progress("eino_agent_reply_stream_start", "", map[string]interface{}{
|
|
||||||
"streamId": subReplyStreamID,
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"einoRole": "sub",
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"source": "eino",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
progress("eino_agent_reply_stream_delta", chunk.Content, map[string]interface{}{
|
|
||||||
"streamId": subReplyStreamID,
|
|
||||||
"conversationId": conversationID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
subAssistantBuf.WriteString(chunk.Content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 收集流式 tool_calls 全部分片;arguments 在最后一帧常为 "",需按 index/id 合并后才能展示 subagent_type/description。
|
|
||||||
if len(chunk.ToolCalls) > 0 {
|
|
||||||
toolStreamFragments = append(toolStreamFragments, chunk.ToolCalls...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if streamsMainAssistant(ev.AgentName) {
|
|
||||||
if s := strings.TrimSpace(mainAssistantBuf.String()); s != "" {
|
|
||||||
lastAssistant = s
|
|
||||||
if orchMode == "plan_execute" && strings.EqualFold(strings.TrimSpace(ev.AgentName), "executor") {
|
|
||||||
lastPlanExecuteExecutor = UnwrapPlanExecuteUserText(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if subAssistantBuf.Len() > 0 && progress != nil {
|
|
||||||
if s := strings.TrimSpace(subAssistantBuf.String()); s != "" {
|
|
||||||
if subReplyStreamID != "" {
|
|
||||||
progress("eino_agent_reply_stream_end", s, map[string]interface{}{
|
|
||||||
"streamId": subReplyStreamID,
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"einoRole": "sub",
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"source": "eino",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
progress("eino_agent_reply", s, map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"einoRole": "sub",
|
|
||||||
"source": "eino",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var lastToolChunk *schema.Message
|
|
||||||
if merged := mergeStreamingToolCallFragments(toolStreamFragments); len(merged) > 0 {
|
|
||||||
lastToolChunk = &schema.Message{ToolCalls: merged}
|
|
||||||
}
|
|
||||||
tryEmitToolCallsOnce(lastToolChunk, ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, gerr := mv.GetMessage()
|
|
||||||
if gerr != nil || msg == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tryEmitToolCallsOnce(mergeMessageToolCalls(msg), ev.AgentName, orchestratorName, conversationID, progress, toolEmitSeen, subAgentToolStep, markPending)
|
|
||||||
|
|
||||||
if mv.Role == schema.Assistant {
|
|
||||||
if progress != nil && strings.TrimSpace(msg.ReasoningContent) != "" {
|
|
||||||
progress("thinking", strings.TrimSpace(msg.ReasoningContent), map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"source": "eino",
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"einoRole": einoRoleTag(ev.AgentName),
|
|
||||||
"orchestration": orchMode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
body := strings.TrimSpace(msg.Content)
|
|
||||||
if body != "" {
|
|
||||||
if streamsMainAssistant(ev.AgentName) {
|
|
||||||
if progress != nil {
|
|
||||||
progress("response_start", "", map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"mcpExecutionIds": snapshotMCPIDs(),
|
|
||||||
"messageGeneratedBy": "eino:" + ev.AgentName,
|
|
||||||
"einoRole": "orchestrator",
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"orchestration": orchMode,
|
|
||||||
})
|
|
||||||
progress("response_delta", body, map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"mcpExecutionIds": snapshotMCPIDs(),
|
|
||||||
"einoRole": "orchestrator",
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"orchestration": orchMode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
lastAssistant = body
|
|
||||||
if orchMode == "plan_execute" && strings.EqualFold(strings.TrimSpace(ev.AgentName), "executor") {
|
|
||||||
lastPlanExecuteExecutor = UnwrapPlanExecuteUserText(body)
|
|
||||||
}
|
|
||||||
} else if progress != nil {
|
|
||||||
progress("eino_agent_reply", body, map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"einoRole": "sub",
|
|
||||||
"source": "eino",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mv.Role == schema.Tool && progress != nil {
|
|
||||||
toolName := msg.ToolName
|
|
||||||
if toolName == "" {
|
|
||||||
toolName = mv.ToolName
|
|
||||||
}
|
|
||||||
|
|
||||||
// bridge 工具在 res.IsError=true 时会返回带前缀的内容;这里解析为 success/isError,避免前端误判为成功。
|
|
||||||
content := msg.Content
|
|
||||||
isErr := false
|
|
||||||
if strings.HasPrefix(content, einomcp.ToolErrorPrefix) {
|
|
||||||
isErr = true
|
|
||||||
content = strings.TrimPrefix(content, einomcp.ToolErrorPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
preview := content
|
|
||||||
if len(preview) > 200 {
|
|
||||||
preview = preview[:200] + "..."
|
|
||||||
}
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"toolName": toolName,
|
|
||||||
"success": !isErr,
|
|
||||||
"isError": isErr,
|
|
||||||
"result": content,
|
|
||||||
"resultPreview": preview,
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"einoAgent": ev.AgentName,
|
|
||||||
"einoRole": einoRoleTag(ev.AgentName),
|
|
||||||
"source": "eino",
|
|
||||||
}
|
|
||||||
toolCallID := strings.TrimSpace(msg.ToolCallID)
|
|
||||||
// Some framework paths (e.g. UnknownToolsHandler) may omit ToolCallID on tool messages.
|
|
||||||
// Infer from the tool_call emission order for this agent to keep UI state consistent.
|
|
||||||
if toolCallID == "" {
|
|
||||||
// In some internal tool execution paths, ev.AgentName may be empty for tool-role
|
|
||||||
// messages. Try several fallbacks to avoid leaving UI tool_call status stuck.
|
|
||||||
if inferred, ok := popNextPendingForAgent(ev.AgentName); ok {
|
|
||||||
toolCallID = inferred.ToolCallID
|
|
||||||
} else if inferred, ok := popNextPendingForAgent(orchestratorName); ok {
|
|
||||||
toolCallID = inferred.ToolCallID
|
|
||||||
} else if inferred, ok := popNextPendingForAgent(""); ok {
|
|
||||||
toolCallID = inferred.ToolCallID
|
|
||||||
} else {
|
|
||||||
// last resort: pick any pending toolCallID
|
|
||||||
for id := range pendingByID {
|
|
||||||
toolCallID = id
|
|
||||||
delete(pendingByID, id)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
removePendingByID(toolCallID)
|
|
||||||
}
|
|
||||||
if toolCallID != "" {
|
|
||||||
data["toolCallId"] = toolCallID
|
|
||||||
}
|
|
||||||
progress("tool_result", fmt.Sprintf("工具结果 (%s)", toolName), data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mcpIDsMu.Lock()
|
|
||||||
ids := append([]string(nil), mcpIDs...)
|
|
||||||
mcpIDsMu.Unlock()
|
|
||||||
|
|
||||||
histJSON, _ := json.Marshal(lastRunMsgs)
|
|
||||||
cleaned := strings.TrimSpace(lastAssistant)
|
|
||||||
if orchMode == "plan_execute" {
|
|
||||||
if e := strings.TrimSpace(lastPlanExecuteExecutor); e != "" {
|
|
||||||
cleaned = e
|
|
||||||
} else {
|
|
||||||
cleaned = UnwrapPlanExecuteUserText(cleaned)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cleaned = dedupeRepeatedParagraphs(cleaned, 80)
|
|
||||||
cleaned = dedupeParagraphsByLineFingerprint(cleaned, 100)
|
|
||||||
out := &RunResult{
|
|
||||||
Response: cleaned,
|
|
||||||
MCPExecutionIDs: ids,
|
|
||||||
LastReActInput: string(histJSON),
|
|
||||||
LastReActOutput: cleaned,
|
|
||||||
}
|
|
||||||
if out.Response == "" {
|
|
||||||
out.Response = "(Eino 多代理编排已完成,但未捕获到助手文本输出。请查看过程详情或日志。)"
|
|
||||||
out.LastReActOutput = out.Response
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func historyToMessages(history []agent.ChatMessage) []adk.Message {
|
func historyToMessages(history []agent.ChatMessage) []adk.Message {
|
||||||
|
|||||||
@@ -195,6 +195,8 @@
|
|||||||
"agentModePanelTitle": "Conversation mode",
|
"agentModePanelTitle": "Conversation mode",
|
||||||
"agentModeReactNative": "Native ReAct",
|
"agentModeReactNative": "Native ReAct",
|
||||||
"agentModeReactNativeHint": "Classic single-agent ReAct with MCP tools",
|
"agentModeReactNativeHint": "Classic single-agent ReAct with MCP tools",
|
||||||
|
"agentModeEinoSingle": "Eino single (ADK)",
|
||||||
|
"agentModeEinoSingleHint": "Eino ChatModelAgent + Runner with MCP tools (/api/eino-agent)",
|
||||||
"agentModeDeep": "Deep (DeepAgent)",
|
"agentModeDeep": "Deep (DeepAgent)",
|
||||||
"agentModeDeepHint": "Eino DeepAgent with task delegation to sub-agents",
|
"agentModeDeepHint": "Eino DeepAgent with task delegation to sub-agents",
|
||||||
"agentModePlanExecuteLabel": "Plan-Execute",
|
"agentModePlanExecuteLabel": "Plan-Execute",
|
||||||
@@ -1592,7 +1594,7 @@
|
|||||||
"agentMode": "Agent mode",
|
"agentMode": "Agent mode",
|
||||||
"agentModeSingle": "Single-agent (ReAct)",
|
"agentModeSingle": "Single-agent (ReAct)",
|
||||||
"agentModeMulti": "Multi-agent (Eino)",
|
"agentModeMulti": "Multi-agent (Eino)",
|
||||||
"agentModeHint": "Same as chat: single-agent ReAct or Deep / Plan-Execute / Supervisor (Eino requires multi-agent enabled).",
|
"agentModeHint": "Same as chat: native ReAct, Eino single-agent (ADK), or Deep / Plan-Execute / Supervisor (the last three require multi-agent enabled).",
|
||||||
"scheduleMode": "Schedule mode",
|
"scheduleMode": "Schedule mode",
|
||||||
"scheduleModeManual": "Manual",
|
"scheduleModeManual": "Manual",
|
||||||
"scheduleModeCron": "Cron expression",
|
"scheduleModeCron": "Cron expression",
|
||||||
|
|||||||
@@ -195,6 +195,8 @@
|
|||||||
"agentModePanelTitle": "对话模式",
|
"agentModePanelTitle": "对话模式",
|
||||||
"agentModeReactNative": "原生 ReAct 模式",
|
"agentModeReactNative": "原生 ReAct 模式",
|
||||||
"agentModeReactNativeHint": "经典单代理 ReAct 与 MCP 工具",
|
"agentModeReactNativeHint": "经典单代理 ReAct 与 MCP 工具",
|
||||||
|
"agentModeEinoSingle": "Eino 单代理(ADK)",
|
||||||
|
"agentModeEinoSingleHint": "Eino ChatModelAgent + Runner,MCP 工具(/api/eino-agent)",
|
||||||
"agentModeDeep": "Deep(DeepAgent)",
|
"agentModeDeep": "Deep(DeepAgent)",
|
||||||
"agentModeDeepHint": "Eino DeepAgent,task 调度子代理",
|
"agentModeDeepHint": "Eino DeepAgent,task 调度子代理",
|
||||||
"agentModePlanExecuteLabel": "Plan-Execute",
|
"agentModePlanExecuteLabel": "Plan-Execute",
|
||||||
@@ -1592,7 +1594,7 @@
|
|||||||
"agentMode": "代理模式",
|
"agentMode": "代理模式",
|
||||||
"agentModeSingle": "单代理(ReAct)",
|
"agentModeSingle": "单代理(ReAct)",
|
||||||
"agentModeMulti": "多代理(Eino)",
|
"agentModeMulti": "多代理(Eino)",
|
||||||
"agentModeHint": "与对话页一致:单代理 ReAct 或 Deep / Plan-Execute / Supervisor(Eino 需已启用多代理)。",
|
"agentModeHint": "与对话页一致:原生 ReAct、Eino 单代理(ADK),或 Deep / Plan-Execute / Supervisor(后三种需已启用多代理)。",
|
||||||
"scheduleMode": "调度方式",
|
"scheduleMode": "调度方式",
|
||||||
"scheduleModeManual": "手工执行",
|
"scheduleModeManual": "手工执行",
|
||||||
"scheduleModeCron": "调度表达式(Cron)",
|
"scheduleModeCron": "调度表达式(Cron)",
|
||||||
|
|||||||
+31
-11
@@ -32,9 +32,10 @@ const CHAT_FILE_DEFAULT_PROMPT = '请根据上传的文件内容进行分析。'
|
|||||||
let chatAttachments = [];
|
let chatAttachments = [];
|
||||||
let chatAttachmentSeq = 0;
|
let chatAttachmentSeq = 0;
|
||||||
|
|
||||||
// 对话模式:react = 原生 ReAct(/agent-loop);deep / plan_execute / supervisor = Eino(/api/multi-agent/stream,请求体 orchestration)
|
// 对话模式:react = 原生 ReAct(/agent-loop);eino_single = Eino ADK 单代理(/api/eino-agent/stream);deep / plan_execute / supervisor = Eino 多代理(/api/multi-agent/stream,请求体 orchestration)
|
||||||
const AGENT_MODE_STORAGE_KEY = 'cyberstrike-chat-agent-mode';
|
const AGENT_MODE_STORAGE_KEY = 'cyberstrike-chat-agent-mode';
|
||||||
const CHAT_AGENT_MODE_REACT = 'react';
|
const CHAT_AGENT_MODE_REACT = 'react';
|
||||||
|
const CHAT_AGENT_MODE_EINO_SINGLE = 'eino_single';
|
||||||
const CHAT_AGENT_EINO_MODES = ['deep', 'plan_execute', 'supervisor'];
|
const CHAT_AGENT_EINO_MODES = ['deep', 'plan_execute', 'supervisor'];
|
||||||
let multiAgentAPIEnabled = false;
|
let multiAgentAPIEnabled = false;
|
||||||
|
|
||||||
@@ -49,23 +50,33 @@ function chatAgentModeIsEino(mode) {
|
|||||||
return CHAT_AGENT_EINO_MODES.indexOf(mode) >= 0;
|
return CHAT_AGENT_EINO_MODES.indexOf(mode) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将 localStorage / 历史值规范为 react | deep | plan_execute | supervisor */
|
function chatAgentModeIsEinoSingle(mode) {
|
||||||
|
return mode === CHAT_AGENT_MODE_EINO_SINGLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将 localStorage / 历史值规范为 react | eino_single | deep | plan_execute | supervisor */
|
||||||
function chatAgentModeNormalizeStored(stored, cfg) {
|
function chatAgentModeNormalizeStored(stored, cfg) {
|
||||||
const pub = cfg && cfg.multi_agent ? cfg.multi_agent : null;
|
const pub = cfg && cfg.multi_agent ? cfg.multi_agent : null;
|
||||||
|
const multiOn = !!(pub && pub.enabled);
|
||||||
const defOrch = 'deep';
|
const defOrch = 'deep';
|
||||||
let s = stored;
|
let s = stored;
|
||||||
if (s === 'single') s = CHAT_AGENT_MODE_REACT;
|
if (s === 'single') s = CHAT_AGENT_MODE_REACT;
|
||||||
if (s === 'multi') s = defOrch;
|
if (s === 'multi') s = defOrch;
|
||||||
if (s === CHAT_AGENT_MODE_REACT || chatAgentModeIsEino(s)) return s;
|
if (s === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(s)) return s;
|
||||||
|
if (chatAgentModeIsEino(s)) {
|
||||||
|
return multiOn ? s : CHAT_AGENT_MODE_REACT;
|
||||||
|
}
|
||||||
const defMulti = pub && pub.default_mode === 'multi';
|
const defMulti = pub && pub.default_mode === 'multi';
|
||||||
return defMulti ? defOrch : CHAT_AGENT_MODE_REACT;
|
return defMulti && multiOn ? defOrch : CHAT_AGENT_MODE_REACT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.csaiChatAgentMode = {
|
window.csaiChatAgentMode = {
|
||||||
EINO_MODES: CHAT_AGENT_EINO_MODES,
|
EINO_MODES: CHAT_AGENT_EINO_MODES,
|
||||||
|
EINO_SINGLE: CHAT_AGENT_MODE_EINO_SINGLE,
|
||||||
REACT: CHAT_AGENT_MODE_REACT,
|
REACT: CHAT_AGENT_MODE_REACT,
|
||||||
isEino: chatAgentModeIsEino,
|
isEino: chatAgentModeIsEino,
|
||||||
|
isEinoSingle: chatAgentModeIsEinoSingle,
|
||||||
normalizeStored: chatAgentModeNormalizeStored,
|
normalizeStored: chatAgentModeNormalizeStored,
|
||||||
normalizeOrchestration: normalizeOrchestrationClient
|
normalizeOrchestration: normalizeOrchestrationClient
|
||||||
};
|
};
|
||||||
@@ -82,12 +93,15 @@ function getAgentModeLabelForValue(mode) {
|
|||||||
return window.t('chat.agentModePlanExecuteLabel');
|
return window.t('chat.agentModePlanExecuteLabel');
|
||||||
case 'supervisor':
|
case 'supervisor':
|
||||||
return window.t('chat.agentModeSupervisorLabel');
|
return window.t('chat.agentModeSupervisorLabel');
|
||||||
|
case CHAT_AGENT_MODE_EINO_SINGLE:
|
||||||
|
return window.t('chat.agentModeEinoSingle');
|
||||||
default:
|
default:
|
||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case CHAT_AGENT_MODE_REACT: return '原生 ReAct';
|
case CHAT_AGENT_MODE_REACT: return '原生 ReAct';
|
||||||
|
case CHAT_AGENT_MODE_EINO_SINGLE: return 'Eino 单代理';
|
||||||
case 'deep': return 'Deep';
|
case 'deep': return 'Deep';
|
||||||
case 'plan_execute': return 'Plan-Execute';
|
case 'plan_execute': return 'Plan-Execute';
|
||||||
case 'supervisor': return 'Supervisor';
|
case 'supervisor': return 'Supervisor';
|
||||||
@@ -98,6 +112,7 @@ function getAgentModeLabelForValue(mode) {
|
|||||||
function getAgentModeIconForValue(mode) {
|
function getAgentModeIconForValue(mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case CHAT_AGENT_MODE_REACT: return '🤖';
|
case CHAT_AGENT_MODE_REACT: return '🤖';
|
||||||
|
case CHAT_AGENT_MODE_EINO_SINGLE: return '⚡';
|
||||||
case 'deep': return '🧩';
|
case 'deep': return '🧩';
|
||||||
case 'plan_execute': return '📋';
|
case 'plan_execute': return '📋';
|
||||||
case 'supervisor': return '🎯';
|
case 'supervisor': return '🎯';
|
||||||
@@ -146,7 +161,7 @@ function toggleAgentModePanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectAgentMode(mode) {
|
function selectAgentMode(mode) {
|
||||||
const ok = mode === CHAT_AGENT_MODE_REACT || chatAgentModeIsEino(mode);
|
const ok = mode === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(mode) || chatAgentModeIsEino(mode);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(AGENT_MODE_STORAGE_KEY, mode);
|
localStorage.setItem(AGENT_MODE_STORAGE_KEY, mode);
|
||||||
@@ -167,11 +182,15 @@ async function initChatAgentModeFromConfig() {
|
|||||||
const wrap = document.getElementById('agent-mode-wrapper');
|
const wrap = document.getElementById('agent-mode-wrapper');
|
||||||
const sel = document.getElementById('agent-mode-select');
|
const sel = document.getElementById('agent-mode-select');
|
||||||
if (!wrap || !sel) return;
|
if (!wrap || !sel) return;
|
||||||
if (!multiAgentAPIEnabled) {
|
|
||||||
wrap.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
wrap.style.display = '';
|
wrap.style.display = '';
|
||||||
|
document.querySelectorAll('.agent-mode-option').forEach(function (el) {
|
||||||
|
const v = el.getAttribute('data-value');
|
||||||
|
if (v === 'deep' || v === 'plan_execute' || v === 'supervisor') {
|
||||||
|
el.style.display = multiAgentAPIEnabled ? '' : 'none';
|
||||||
|
} else {
|
||||||
|
el.style.display = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
let stored = localStorage.getItem(AGENT_MODE_STORAGE_KEY);
|
let stored = localStorage.getItem(AGENT_MODE_STORAGE_KEY);
|
||||||
stored = chatAgentModeNormalizeStored(stored, cfg);
|
stored = chatAgentModeNormalizeStored(stored, cfg);
|
||||||
try {
|
try {
|
||||||
@@ -188,7 +207,7 @@ document.addEventListener('languagechange', function () {
|
|||||||
const hid = document.getElementById('agent-mode-select');
|
const hid = document.getElementById('agent-mode-select');
|
||||||
if (!hid) return;
|
if (!hid) return;
|
||||||
const v = hid.value;
|
const v = hid.value;
|
||||||
if (v === CHAT_AGENT_MODE_REACT || chatAgentModeIsEino(v)) {
|
if (v === CHAT_AGENT_MODE_REACT || chatAgentModeIsEinoSingle(v) || chatAgentModeIsEino(v)) {
|
||||||
syncAgentModeFromValue(v);
|
syncAgentModeFromValue(v);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -382,8 +401,9 @@ async function sendMessage() {
|
|||||||
try {
|
try {
|
||||||
const modeSel = document.getElementById('agent-mode-select');
|
const modeSel = document.getElementById('agent-mode-select');
|
||||||
const modeVal = modeSel ? modeSel.value : CHAT_AGENT_MODE_REACT;
|
const modeVal = modeSel ? modeSel.value : CHAT_AGENT_MODE_REACT;
|
||||||
|
const useEinoSingle = chatAgentModeIsEinoSingle(modeVal);
|
||||||
const useMulti = multiAgentAPIEnabled && chatAgentModeIsEino(modeVal);
|
const useMulti = multiAgentAPIEnabled && chatAgentModeIsEino(modeVal);
|
||||||
const streamPath = useMulti ? '/api/multi-agent/stream' : '/api/agent-loop/stream';
|
const streamPath = useEinoSingle ? '/api/eino-agent/stream' : useMulti ? '/api/multi-agent/stream' : '/api/agent-loop/stream';
|
||||||
if (useMulti && modeVal) {
|
if (useMulti && modeVal) {
|
||||||
body.orchestration = modeVal;
|
body.orchestration = modeVal;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-6
@@ -14,14 +14,22 @@ function _tPlain(key, opts) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 与创建队列 / API 一致的合法 agentMode */
|
||||||
|
const BATCH_QUEUE_AGENT_MODES = ['single', 'eino_single', 'deep', 'plan_execute', 'supervisor'];
|
||||||
|
|
||||||
|
function isBatchQueueAgentMode(mode) {
|
||||||
|
return BATCH_QUEUE_AGENT_MODES.indexOf(String(mode || '').toLowerCase()) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
/** 批量队列 agentMode 展示文案(与对话模式命名一致) */
|
/** 批量队列 agentMode 展示文案(与对话模式命名一致) */
|
||||||
function batchQueueAgentModeLabel(mode) {
|
function batchQueueAgentModeLabel(mode) {
|
||||||
const m = String(mode || 'single').toLowerCase();
|
const m = String(mode || 'single').toLowerCase();
|
||||||
if (m === 'single') return _t('batchImportModal.agentModeSingle');
|
if (m === 'single') return _t('chat.agentModeReactNative');
|
||||||
|
if (m === 'eino_single') return _t('chat.agentModeEinoSingle');
|
||||||
if (m === 'multi' || m === 'deep') return _t('chat.agentModeDeep');
|
if (m === 'multi' || m === 'deep') return _t('chat.agentModeDeep');
|
||||||
if (m === 'plan_execute') return _t('chat.agentModePlanExecuteLabel');
|
if (m === 'plan_execute') return _t('chat.agentModePlanExecuteLabel');
|
||||||
if (m === 'supervisor') return _t('chat.agentModeSupervisorLabel');
|
if (m === 'supervisor') return _t('chat.agentModeSupervisorLabel');
|
||||||
return _t('batchImportModal.agentModeSingle');
|
return _t('chat.agentModeReactNative');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cron 队列在「本轮 completed」等状态下的展示文案(底层 status 不变,仅 UI 强调循环调度) */
|
/** Cron 队列在「本轮 completed」等状态下的展示文案(底层 status 不变,仅 UI 强调循环调度) */
|
||||||
@@ -940,7 +948,7 @@ async function createBatchQueue() {
|
|||||||
// 获取角色(可选,空字符串表示默认角色)
|
// 获取角色(可选,空字符串表示默认角色)
|
||||||
const role = roleSelect ? roleSelect.value || '' : '';
|
const role = roleSelect ? roleSelect.value || '' : '';
|
||||||
const rawMode = agentModeSelect ? agentModeSelect.value : 'single';
|
const rawMode = agentModeSelect ? agentModeSelect.value : 'single';
|
||||||
const agentMode = ['single', 'deep', 'plan_execute', 'supervisor'].indexOf(rawMode) >= 0 ? rawMode : 'single';
|
const agentMode = isBatchQueueAgentMode(rawMode) ? rawMode : 'single';
|
||||||
const scheduleMode = scheduleModeSelect ? (scheduleModeSelect.value === 'cron' ? 'cron' : 'manual') : 'manual';
|
const scheduleMode = scheduleModeSelect ? (scheduleModeSelect.value === 'cron' ? 'cron' : 'manual') : 'manual';
|
||||||
const cronExpr = cronExprInput ? cronExprInput.value.trim() : '';
|
const cronExpr = cronExprInput ? cronExprInput.value.trim() : '';
|
||||||
const executeNow = executeNowCheckbox ? !!executeNowCheckbox.checked : false;
|
const executeNow = executeNowCheckbox ? !!executeNowCheckbox.checked : false;
|
||||||
@@ -2138,10 +2146,11 @@ function startInlineEditAgentMode() {
|
|||||||
const queue = detail.queue;
|
const queue = detail.queue;
|
||||||
let currentMode = (queue.agentMode || 'single').toLowerCase();
|
let currentMode = (queue.agentMode || 'single').toLowerCase();
|
||||||
if (currentMode === 'multi') currentMode = 'deep';
|
if (currentMode === 'multi') currentMode = 'deep';
|
||||||
if (['single', 'deep', 'plan_execute', 'supervisor'].indexOf(currentMode) < 0) currentMode = 'single';
|
if (!isBatchQueueAgentMode(currentMode)) currentMode = 'single';
|
||||||
container.innerHTML = `<span class="bq-inline-edit-controls">
|
container.innerHTML = `<span class="bq-inline-edit-controls">
|
||||||
<select id="bq-edit-agentmode">
|
<select id="bq-edit-agentmode">
|
||||||
<option value="single" ${currentMode === 'single' ? 'selected' : ''}>${escapeHtml(_t('batchImportModal.agentModeSingle'))}</option>
|
<option value="single" ${currentMode === 'single' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeReactNative'))}</option>
|
||||||
|
<option value="eino_single" ${currentMode === 'eino_single' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeEinoSingle'))}</option>
|
||||||
<option value="deep" ${currentMode === 'deep' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeDeep'))}</option>
|
<option value="deep" ${currentMode === 'deep' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeDeep'))}</option>
|
||||||
<option value="plan_execute" ${currentMode === 'plan_execute' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModePlanExecuteLabel'))}</option>
|
<option value="plan_execute" ${currentMode === 'plan_execute' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModePlanExecuteLabel'))}</option>
|
||||||
<option value="supervisor" ${currentMode === 'supervisor' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeSupervisorLabel'))}</option>
|
<option value="supervisor" ${currentMode === 'supervisor' ? 'selected' : ''}>${escapeHtml(_t('chat.agentModeSupervisorLabel'))}</option>
|
||||||
@@ -2166,7 +2175,7 @@ async function saveInlineAgentMode() {
|
|||||||
if (!queueId) { _bqInlineSaving = false; return; }
|
if (!queueId) { _bqInlineSaving = false; return; }
|
||||||
const sel = document.getElementById('bq-edit-agentmode');
|
const sel = document.getElementById('bq-edit-agentmode');
|
||||||
const raw = sel ? sel.value : 'single';
|
const raw = sel ? sel.value : 'single';
|
||||||
const agentMode = ['single', 'deep', 'plan_execute', 'supervisor'].indexOf(raw) >= 0 ? raw : 'single';
|
const agentMode = isBatchQueueAgentMode(raw) ? raw : 'single';
|
||||||
try {
|
try {
|
||||||
const detailResp = await apiFetch(`/api/batch-tasks/${queueId}`);
|
const detailResp = await apiFetch(`/api/batch-tasks/${queueId}`);
|
||||||
const detail = await detailResp.json();
|
const detail = await detailResp.json();
|
||||||
|
|||||||
@@ -46,9 +46,6 @@ function resolveWebshellAiStreamRequest() {
|
|||||||
if (!r.ok) return null;
|
if (!r.ok) return null;
|
||||||
return r.json();
|
return r.json();
|
||||||
}).then(function (cfg) {
|
}).then(function (cfg) {
|
||||||
if (!cfg || !cfg.multi_agent || !cfg.multi_agent.enabled) {
|
|
||||||
return { path: '/api/agent-loop/stream', orchestration: null };
|
|
||||||
}
|
|
||||||
var norm = null;
|
var norm = null;
|
||||||
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.normalizeStored === 'function') {
|
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.normalizeStored === 'function') {
|
||||||
norm = window.csaiChatAgentMode.normalizeStored(localStorage.getItem('cyberstrike-chat-agent-mode'), cfg);
|
norm = window.csaiChatAgentMode.normalizeStored(localStorage.getItem('cyberstrike-chat-agent-mode'), cfg);
|
||||||
@@ -58,6 +55,12 @@ function resolveWebshellAiStreamRequest() {
|
|||||||
if (mode === 'multi') mode = 'deep';
|
if (mode === 'multi') mode = 'deep';
|
||||||
norm = mode || 'react';
|
norm = mode || 'react';
|
||||||
}
|
}
|
||||||
|
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.isEinoSingle === 'function' && window.csaiChatAgentMode.isEinoSingle(norm)) {
|
||||||
|
return { path: '/api/eino-agent/stream', orchestration: null };
|
||||||
|
}
|
||||||
|
if (!cfg || !cfg.multi_agent || !cfg.multi_agent.enabled) {
|
||||||
|
return { path: '/api/agent-loop/stream', orchestration: null };
|
||||||
|
}
|
||||||
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.isEino === 'function' && window.csaiChatAgentMode.isEino(norm)) {
|
if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.isEino === 'function' && window.csaiChatAgentMode.isEino(norm)) {
|
||||||
return { path: '/api/multi-agent/stream', orchestration: norm };
|
return { path: '/api/multi-agent/stream', orchestration: norm };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -611,6 +611,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="react">✓</div>
|
<div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="react">✓</div>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="role-selection-item-main agent-mode-option" data-value="eino_single" role="option" onclick="selectAgentMode('eino_single')">
|
||||||
|
<div class="role-selection-item-icon-main" aria-hidden="true">⚡</div>
|
||||||
|
<div class="role-selection-item-content-main">
|
||||||
|
<div class="role-selection-item-name-main" data-i18n="chat.agentModeEinoSingle">Eino 单代理(ADK)</div>
|
||||||
|
<div class="role-selection-item-description-main" data-i18n="chat.agentModeEinoSingleHint">CloudWeGo Eino ChatModelAgent + Runner,MCP 工具(/api/eino-agent)</div>
|
||||||
|
</div>
|
||||||
|
<div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="eino_single">✓</div>
|
||||||
|
</button>
|
||||||
<button type="button" class="role-selection-item-main agent-mode-option" data-value="deep" role="option" onclick="selectAgentMode('deep')">
|
<button type="button" class="role-selection-item-main agent-mode-option" data-value="deep" role="option" onclick="selectAgentMode('deep')">
|
||||||
<div class="role-selection-item-icon-main" aria-hidden="true">🧩</div>
|
<div class="role-selection-item-icon-main" aria-hidden="true">🧩</div>
|
||||||
<div class="role-selection-item-content-main">
|
<div class="role-selection-item-content-main">
|
||||||
@@ -2428,12 +2436,13 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="batch-queue-agent-mode" data-i18n="batchImportModal.agentMode">代理模式</label>
|
<label for="batch-queue-agent-mode" data-i18n="batchImportModal.agentMode">代理模式</label>
|
||||||
<select id="batch-queue-agent-mode" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem;">
|
<select id="batch-queue-agent-mode" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem;">
|
||||||
<option value="single" data-i18n="batchImportModal.agentModeSingle">单代理(ReAct)</option>
|
<option value="single" data-i18n="chat.agentModeReactNative">原生 ReAct 模式</option>
|
||||||
|
<option value="eino_single" data-i18n="chat.agentModeEinoSingle">Eino 单代理(ADK)</option>
|
||||||
<option value="deep" data-i18n="chat.agentModeDeep">Deep(DeepAgent)</option>
|
<option value="deep" data-i18n="chat.agentModeDeep">Deep(DeepAgent)</option>
|
||||||
<option value="plan_execute" data-i18n="chat.agentModePlanExecuteLabel">Plan-Execute</option>
|
<option value="plan_execute" data-i18n="chat.agentModePlanExecuteLabel">Plan-Execute</option>
|
||||||
<option value="supervisor" data-i18n="chat.agentModeSupervisorLabel">Supervisor</option>
|
<option value="supervisor" data-i18n="chat.agentModeSupervisorLabel">Supervisor</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.agentModeHint">与对话页一致:原生 ReAct 或三种 Eino 编排(需已启用多代理)。</div>
|
<div class="form-hint" style="margin-top: 4px;" data-i18n="batchImportModal.agentModeHint">与对话页一致:原生 ReAct、Eino 单代理(ADK),或 Deep / Plan-Execute / Supervisor(后三种需已启用多代理)。</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="batch-queue-schedule-mode" data-i18n="batchImportModal.scheduleMode">调度方式</label>
|
<label for="batch-queue-schedule-mode" data-i18n="batchImportModal.scheduleMode">调度方式</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user