mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-16 16:10:21 +02:00
Add files via upload
This commit is contained in:
@@ -4,10 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"cyberstrike-ai/internal/agent"
|
"cyberstrike-ai/internal/agent"
|
||||||
|
"cyberstrike-ai/internal/security"
|
||||||
|
|
||||||
"github.com/cloudwego/eino/components/tool"
|
"github.com/cloudwego/eino/components/tool"
|
||||||
|
"github.com/cloudwego/eino/compose"
|
||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
"github.com/eino-contrib/jsonschema"
|
"github.com/eino-contrib/jsonschema"
|
||||||
)
|
)
|
||||||
@@ -15,8 +18,18 @@ import (
|
|||||||
// ExecutionRecorder 可选,在 MCP 工具成功返回且带有 execution id 时回调(用于汇总 mcpExecutionIds)。
|
// ExecutionRecorder 可选,在 MCP 工具成功返回且带有 execution id 时回调(用于汇总 mcpExecutionIds)。
|
||||||
type ExecutionRecorder func(executionID string)
|
type ExecutionRecorder func(executionID string)
|
||||||
|
|
||||||
|
// ToolErrorPrefix 用于把内部 MCP 执行结果中的 IsError 标记传递到多代理上层。
|
||||||
|
// Eino 工具通道目前只支持返回字符串,因此通过前缀标识,随后在多代理 runner 中解析为 success/isError。
|
||||||
|
const ToolErrorPrefix = "__CYBERSTRIKE_AI_TOOL_ERROR__\n"
|
||||||
|
|
||||||
// ToolsFromDefinitions 将单 Agent 使用的 OpenAI 风格工具定义转为 Eino InvokableTool,执行时走 Agent 的 MCP 路径。
|
// ToolsFromDefinitions 将单 Agent 使用的 OpenAI 风格工具定义转为 Eino InvokableTool,执行时走 Agent 的 MCP 路径。
|
||||||
func ToolsFromDefinitions(ag *agent.Agent, holder *ConversationHolder, defs []agent.Tool, rec ExecutionRecorder) ([]tool.BaseTool, error) {
|
func ToolsFromDefinitions(
|
||||||
|
ag *agent.Agent,
|
||||||
|
holder *ConversationHolder,
|
||||||
|
defs []agent.Tool,
|
||||||
|
rec ExecutionRecorder,
|
||||||
|
toolOutputChunk func(toolName, toolCallID, chunk string),
|
||||||
|
) ([]tool.BaseTool, error) {
|
||||||
out := make([]tool.BaseTool, 0, len(defs))
|
out := make([]tool.BaseTool, 0, len(defs))
|
||||||
for _, d := range defs {
|
for _, d := range defs {
|
||||||
if d.Type != "function" || d.Function.Name == "" {
|
if d.Type != "function" || d.Function.Name == "" {
|
||||||
@@ -32,6 +45,7 @@ func ToolsFromDefinitions(ag *agent.Agent, holder *ConversationHolder, defs []ag
|
|||||||
agent: ag,
|
agent: ag,
|
||||||
holder: holder,
|
holder: holder,
|
||||||
record: rec,
|
record: rec,
|
||||||
|
chunk: toolOutputChunk,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
@@ -68,6 +82,7 @@ type mcpBridgeTool struct {
|
|||||||
agent *agent.Agent
|
agent *agent.Agent
|
||||||
holder *ConversationHolder
|
holder *ConversationHolder
|
||||||
record ExecutionRecorder
|
record ExecutionRecorder
|
||||||
|
chunk func(toolName, toolCallID, chunk string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mcpBridgeTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
func (m *mcpBridgeTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||||
@@ -86,6 +101,32 @@ func (m *mcpBridgeTool) InvokableRun(ctx context.Context, argumentsInJSON string
|
|||||||
if args == nil {
|
if args == nil {
|
||||||
args = map[string]interface{}{}
|
args = map[string]interface{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stream tool output (stdout/stderr) to upper layer via security.Executor's callback.
|
||||||
|
// This enables multi-agent mode to show execution progress on the frontend.
|
||||||
|
if m.chunk != nil {
|
||||||
|
toolCallID := compose.GetToolCallID(ctx)
|
||||||
|
if toolCallID != "" {
|
||||||
|
if existing, ok := ctx.Value(security.ToolOutputCallbackCtxKey).(security.ToolOutputCallback); ok && existing != nil {
|
||||||
|
// Chain existing callback (if any) + our progress forwarder.
|
||||||
|
ctx = context.WithValue(ctx, security.ToolOutputCallbackCtxKey, security.ToolOutputCallback(func(c string) {
|
||||||
|
existing(c)
|
||||||
|
if strings.TrimSpace(c) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.chunk(m.name, toolCallID, c)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
ctx = context.WithValue(ctx, security.ToolOutputCallbackCtxKey, security.ToolOutputCallback(func(c string) {
|
||||||
|
if strings.TrimSpace(c) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.chunk(m.name, toolCallID, c)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conv := m.holder.Get()
|
conv := m.holder.Get()
|
||||||
res, err := m.agent.ExecuteMCPToolForConversation(ctx, conv, m.name, args)
|
res, err := m.agent.ExecuteMCPToolForConversation(ctx, conv, m.name, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -97,5 +138,8 @@ func (m *mcpBridgeTool) InvokableRun(ctx context.Context, argumentsInJSON string
|
|||||||
if res.ExecutionID != "" && m.record != nil {
|
if res.ExecutionID != "" && m.record != nil {
|
||||||
m.record(res.ExecutionID)
|
m.record(res.ExecutionID)
|
||||||
}
|
}
|
||||||
|
if res.IsError {
|
||||||
|
return ToolErrorPrefix + res.Result, nil
|
||||||
|
}
|
||||||
return res.Result, nil
|
return res.Result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,23 @@ func RunDeepAgent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
mainDefs := ag.ToolsForRole(roleTools)
|
mainDefs := ag.ToolsForRole(roleTools)
|
||||||
mainTools, err := einomcp.ToolsFromDefinitions(ag, holder, mainDefs, recorder)
|
toolOutputChunk := func(toolName, toolCallID, chunk string) {
|
||||||
|
// When toolCallId is missing, frontend ignores tool_result_delta.
|
||||||
|
if progress == nil || toolCallID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
progress("tool_result_delta", chunk, map[string]interface{}{
|
||||||
|
"toolName": toolName,
|
||||||
|
"toolCallId": toolCallID,
|
||||||
|
// index/total/iteration are optional for UI; we don't know them in this bridge.
|
||||||
|
"index": 0,
|
||||||
|
"total": 0,
|
||||||
|
"iteration": 0,
|
||||||
|
"source": "eino",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mainTools, err := einomcp.ToolsFromDefinitions(ag, holder, mainDefs, recorder, toolOutputChunk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -183,7 +199,7 @@ func RunDeepAgent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
subDefs := ag.ToolsForRole(roleTools)
|
subDefs := ag.ToolsForRole(roleTools)
|
||||||
subTools, err := einomcp.ToolsFromDefinitions(ag, holder, subDefs, recorder)
|
subTools, err := einomcp.ToolsFromDefinitions(ag, holder, subDefs, recorder, toolOutputChunk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("子代理 %q 工具: %w", id, err)
|
return nil, fmt.Errorf("子代理 %q 工具: %w", id, err)
|
||||||
}
|
}
|
||||||
@@ -455,14 +471,24 @@ func RunDeepAgent(
|
|||||||
if toolName == "" {
|
if toolName == "" {
|
||||||
toolName = mv.ToolName
|
toolName = mv.ToolName
|
||||||
}
|
}
|
||||||
preview := msg.Content
|
|
||||||
|
// 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 {
|
if len(preview) > 200 {
|
||||||
preview = preview[:200] + "..."
|
preview = preview[:200] + "..."
|
||||||
}
|
}
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"toolName": toolName,
|
"toolName": toolName,
|
||||||
"success": true,
|
"success": !isErr,
|
||||||
"result": msg.Content,
|
"isError": isErr,
|
||||||
|
"result": content,
|
||||||
"resultPreview": preview,
|
"resultPreview": preview,
|
||||||
"conversationId": conversationID,
|
"conversationId": conversationID,
|
||||||
"einoAgent": ev.AgentName,
|
"einoAgent": ev.AgentName,
|
||||||
|
|||||||
Reference in New Issue
Block a user