From ecb95f4cc875d4799251d3d835a613253672ebce Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 28 Jan 2026 14:44:42 +0800 Subject: [PATCH 1/3] Enhance Robot Delivery Logic and Context Management - Introduced manager ID and email fields in the Robot struct to facilitate mandatory delivery to managers. - Updated delivery routing logic to ensure manager email is always included if manager ID is set, improving delivery reliability. - Added JSON serialization handling in the Delivery Center to ensure compatibility with various data types. - Enhanced context ID generation by replacing time-based IDs with NanoID for improved uniqueness and consistency. --- agent/context/context.go | 7 +- agent/robot/api/robot.go | 1 + agent/robot/cache/load.go | 1 + agent/robot/executor/standard/delivery.go | 107 ++++++++++++++++-- .../executor/standard/delivery_center.go | 21 +++- agent/robot/types/robot.go | 6 + trace/manager.go | 5 +- 7 files changed, 133 insertions(+), 15 deletions(-) diff --git a/agent/context/context.go b/agent/context/context.go index ceac1610..5933318f 100644 --- a/agent/context/context.go +++ b/agent/context/context.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "sync" - "time" "github.com/yaoapp/yao/agent/memory" "github.com/yaoapp/yao/agent/output/message" @@ -411,12 +410,12 @@ func SendInterrupt(contextID string, signal *InterruptSignal) error { // generateContextID generates a unique context ID func generateContextID() string { - return fmt.Sprintf("ctx-%d", time.Now().UnixNano()) + return message.GenerateNanoID() } -// RequestID returns the request ID for the context +// RequestID returns a unique request ID using NanoID func (ctx *Context) RequestID() string { - return fmt.Sprintf("%s", ctx.ID) + return message.GenerateNanoID() } // TraceID returns the trace ID for the context diff --git a/agent/robot/api/robot.go b/agent/robot/api/robot.go index d9180885..15432a9c 100644 --- a/agent/robot/api/robot.go +++ b/agent/robot/api/robot.go @@ -167,6 +167,7 @@ func loadRobotFromDB(memberID string) (*types.Robot, error) { "id", "member_id", "team_id", "display_name", "bio", "system_prompt", "robot_status", "autonomous_mode", "robot_config", "robot_email", "agents", "mcp_servers", + "manager_id", }, Wheres: []model.QueryWhere{ {Column: "member_id", Value: memberID}, diff --git a/agent/robot/cache/load.go b/agent/robot/cache/load.go index d9a455c3..600c79b0 100644 --- a/agent/robot/cache/load.go +++ b/agent/robot/cache/load.go @@ -26,6 +26,7 @@ var memberFields = []interface{}{ "robot_email", "agents", "mcp_servers", + "manager_id", } // SetMemberModel sets the member model name diff --git a/agent/robot/executor/standard/delivery.go b/agent/robot/executor/standard/delivery.go index 4d312c69..f80a9ac0 100644 --- a/agent/robot/executor/standard/delivery.go +++ b/agent/robot/executor/standard/delivery.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/yaoapp/gou/model" robottypes "github.com/yaoapp/yao/agent/robot/types" ) @@ -94,20 +95,18 @@ func (e *Executor) RunDelivery(ctx *robottypes.Context, exec *robottypes.Executi // routeToDeliveryCenter sends content to the Delivery Center for actual delivery // The Delivery Center decides which channels to use based on robot/user preferences +// +// Delivery logic: +// 1. Manager email: ALWAYS send to manager if manager_id is set (mandatory) +// 2. Additional targets: Append configured email/webhook/process targets func (e *Executor) routeToDeliveryCenter(ctx *robottypes.Context, exec *robottypes.Execution, robot *robottypes.Robot) error { if exec.Delivery == nil || exec.Delivery.Content == nil { return fmt.Errorf("no delivery content to route") } - // Get delivery preferences from robot config - var prefs *robottypes.DeliveryPreferences - if robot.Config != nil { - prefs = robot.Config.Delivery - } - - // If no preferences configured, skip delivery (content is still saved in exec.Delivery) + // Build final delivery preferences by merging manager email + configured targets + prefs := buildDeliveryPreferences(robot) if prefs == nil || !hasActiveChannels(prefs) { - // No channels configured - mark as success but with no results exec.Delivery.Success = true return nil } @@ -283,6 +282,98 @@ func hasActiveChannels(prefs *robottypes.DeliveryPreferences) bool { return false } +// buildDeliveryPreferences builds the final delivery preferences by: +// 1. Always including manager email if manager_id is set (mandatory) +// 2. Appending all configured email/webhook/process targets +func buildDeliveryPreferences(robot *robottypes.Robot) *robottypes.DeliveryPreferences { + if robot == nil { + return nil + } + + prefs := &robottypes.DeliveryPreferences{} + + // Step 1: Get manager email (mandatory if manager_id is set) + managerEmail := robot.ManagerEmail + if managerEmail == "" && robot.ManagerID != "" { + managerEmail = getManagerEmail(robot.ManagerID) + if managerEmail != "" { + robot.ManagerEmail = managerEmail // Cache for future use + } + } + + // Step 2: Build email targets (manager first, then configured targets) + var emailTargets []robottypes.EmailTarget + + // Add manager email as first target (mandatory) + if managerEmail != "" { + emailTargets = append(emailTargets, robottypes.EmailTarget{ + To: []string{managerEmail}, + }) + } + + // Append configured email targets + if robot.Config != nil && robot.Config.Delivery != nil && robot.Config.Delivery.Email != nil { + for _, target := range robot.Config.Delivery.Email.Targets { + if len(target.To) > 0 { + emailTargets = append(emailTargets, target) + } + } + } + + // Set email preference if we have any targets + if len(emailTargets) > 0 { + prefs.Email = &robottypes.EmailPreference{ + Enabled: true, + Targets: emailTargets, + } + } + + // Step 3: Copy webhook preferences from config (if enabled) + if robot.Config != nil && robot.Config.Delivery != nil && robot.Config.Delivery.Webhook != nil { + if robot.Config.Delivery.Webhook.Enabled && len(robot.Config.Delivery.Webhook.Targets) > 0 { + prefs.Webhook = robot.Config.Delivery.Webhook + } + } + + // Step 4: Copy process preferences from config (if enabled) + if robot.Config != nil && robot.Config.Delivery != nil && robot.Config.Delivery.Process != nil { + if robot.Config.Delivery.Process.Enabled && len(robot.Config.Delivery.Process.Targets) > 0 { + prefs.Process = robot.Config.Delivery.Process + } + } + + return prefs +} + +// getManagerEmail retrieves the manager's email from __yao.member table by member_id +// manager_id in Robot refers to a member_id in __yao.member table +func getManagerEmail(managerID string) string { + if managerID == "" { + return "" + } + + m := model.Select("__yao.member") + if m == nil { + return "" + } + + records, err := m.Get(model.QueryParam{ + Select: []interface{}{"email"}, + Wheres: []model.QueryWhere{ + {Column: "member_id", Value: managerID}, + }, + Limit: 1, + }) + if err != nil || len(records) == 0 { + return "" + } + + if email, ok := records[0]["email"].(string); ok { + return email + } + return "" +} + // FormatDeliveryInput formats the full execution context for the Delivery Agent func (f *InputFormatter) FormatDeliveryInput(exec *robottypes.Execution, robot *robottypes.Robot) string { if exec == nil { diff --git a/agent/robot/executor/standard/delivery_center.go b/agent/robot/executor/standard/delivery_center.go index 0009d7db..f8562d14 100644 --- a/agent/robot/executor/standard/delivery_center.go +++ b/agent/robot/executor/standard/delivery_center.go @@ -295,11 +295,30 @@ func (dc *DeliveryCenter) callProcess( } result.Success = true - result.Details = proc.Value + // Convert proc.Value to JSON-serializable format to avoid func type issues + result.Details = toJSONSerializable(proc.Value) return result } +// toJSONSerializable ensures the value can be JSON serialized +// Returns the original value if serializable, or a string fallback if not +func toJSONSerializable(v interface{}) interface{} { + if v == nil { + return nil + } + + // Try to marshal to check if it's JSON serializable + _, err := json.Marshal(v) + if err != nil { + // If it can't be serialized (e.g., contains func), return a string representation + return fmt.Sprintf("%v", v) + } + + // Return original value if it's serializable + return v +} + // buildEmailSubject builds the email subject line func buildEmailSubject(subject, template string, content *robottypes.DeliveryContent, ctx *robottypes.DeliveryContext) string { // Use explicit subject if provided diff --git a/agent/robot/types/robot.go b/agent/robot/types/robot.go index 768a336a..ffad6da1 100644 --- a/agent/robot/types/robot.go +++ b/agent/robot/types/robot.go @@ -23,6 +23,10 @@ type Robot struct { AutonomousMode bool `json:"autonomous_mode"` RobotEmail string `json:"robot_email"` // Robot's email address for sending emails + // Manager info (from __yao.member) + ManagerID string `json:"manager_id"` // Direct manager user_id (who manages this robot) + ManagerEmail string `json:"manager_email"` // Manager's email address (for default delivery) + // Parsed config (from robot_config JSON field) Config *Config `json:"-"` @@ -393,6 +397,8 @@ func NewRobotFromMap(m map[string]interface{}) (*Robot, error) { SystemPrompt: getString(m, "system_prompt"), AutonomousMode: getBool(m, "autonomous_mode"), RobotEmail: getString(m, "robot_email"), + ManagerID: getString(m, "manager_id"), + ManagerEmail: getString(m, "manager_email"), } // Parse robot_status diff --git a/trace/manager.go b/trace/manager.go index ee249903..01e2d69a 100644 --- a/trace/manager.go +++ b/trace/manager.go @@ -93,9 +93,10 @@ func (m *manager) addUpdateAndBroadcast(update *types.TraceUpdate) { // Persist to driver (synchronous - no race) if err := m.driver.SaveUpdate(context.Background(), m.traceID, update); err != nil { log.Trace("[MANAGER] addUpdateAndBroadcast: failed to save update type=%s for trace %s: %v", update.Type, m.traceID, err) - } else { - log.Trace("[MANAGER] addUpdateAndBroadcast: successfully saved update type=%s for trace %s", update.Type, m.traceID) } + // else { + // log.Trace("[MANAGER] addUpdateAndBroadcast: successfully saved update type=%s for trace %s", update.Type, m.traceID) + // } // Add to in-memory history m.stateAddUpdate(update) From 423d695d0efd9cf9e0c57b68eec8289b6d74a118 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 28 Jan 2026 14:58:10 +0800 Subject: [PATCH 2/3] Add HMAC Signature Support for Webhook Payloads - Implemented HMAC-SHA256 signature generation and verification for webhook requests to enhance security. - Updated the Delivery Center to include the computed signature in the request headers when a secret is configured. - Added utility functions for computing and verifying HMAC signatures, ensuring integrity of webhook payloads. - Improved documentation for the new signature handling process to assist developers in implementing secure webhooks. --- .../executor/standard/delivery_center.go | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/agent/robot/executor/standard/delivery_center.go b/agent/robot/executor/standard/delivery_center.go index f8562d14..e8bb2412 100644 --- a/agent/robot/executor/standard/delivery_center.go +++ b/agent/robot/executor/standard/delivery_center.go @@ -3,6 +3,9 @@ package standard import ( "bytes" "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "io" @@ -217,10 +220,11 @@ func (dc *DeliveryCenter) postWebhook( req.Header.Set(key, value) } - // Add secret if configured (for signature verification) + // Add HMAC signature if secret is configured if target.Secret != "" { - // TODO: Implement HMAC signature - req.Header.Set("X-Webhook-Secret", target.Secret) + signature := computeHMACSignature(payloadBytes, target.Secret) + req.Header.Set("X-Yao-Signature", signature) + req.Header.Set("X-Yao-Signature-Algorithm", "HMAC-SHA256") } // Send request @@ -395,3 +399,26 @@ func convertAttachments(ctx context.Context, attachments []robottypes.DeliveryAt return result } + +// ============================================================================ +// Webhook Signature +// ============================================================================ + +// computeHMACSignature computes HMAC-SHA256 signature for webhook payload +// Returns hex-encoded signature string +func computeHMACSignature(payload []byte, secret string) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write(payload) + return hex.EncodeToString(mac.Sum(nil)) +} + +// VerifyHMACSignature verifies the HMAC-SHA256 signature of a webhook payload +// Headers: +// - X-Yao-Signature: hex-encoded HMAC-SHA256 signature +// - X-Yao-Signature-Algorithm: "HMAC-SHA256" +// +// Returns true if the signature is valid +func VerifyHMACSignature(payload []byte, secret, signature string) bool { + expected := computeHMACSignature(payload, secret) + return hmac.Equal([]byte(expected), []byte(signature)) +} From 274edfe7ccf24930eec777b424d266ca7504ae48 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 28 Jan 2026 15:32:24 +0800 Subject: [PATCH 3/3] Update dependencies and enhance email body handling in Delivery Center - Added new indirect dependencies: `github.com/JohannesKaufmann/dom` and `github.com/JohannesKaufmann/html-to-markdown/v2` for improved HTML processing. - Updated `github.com/sergi/go-diff` to version 1.4.0 for enhanced diff functionality. - Modified the `buildEmailBody` function to return both HTML and plain text versions of the email body, improving email formatting capabilities. - Adjusted the default email channel name in configuration to "default" for better clarity. --- .../executor/standard/delivery_center.go | 28 ++++++++++++++----- agent/robot/types/config_global.go | 4 +-- go.mod | 5 +++- go.sum | 12 ++++++-- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/agent/robot/executor/standard/delivery_center.go b/agent/robot/executor/standard/delivery_center.go index e8bb2412..08fdba6c 100644 --- a/agent/robot/executor/standard/delivery_center.go +++ b/agent/robot/executor/standard/delivery_center.go @@ -14,6 +14,7 @@ import ( "time" "github.com/yaoapp/gou/process" + "github.com/yaoapp/gou/text" robottypes "github.com/yaoapp/yao/agent/robot/types" "github.com/yaoapp/yao/attachment" "github.com/yaoapp/yao/messenger" @@ -119,11 +120,13 @@ func (dc *DeliveryCenter) sendEmail( return result } - // Build email message + // Build email message with HTML content + htmlBody, plainBody := buildEmailBody(target.Template, content) msg := &messengerTypes.Message{ To: target.To, Subject: buildEmailSubject(target.Subject, target.Template, content, deliveryCtx), - Body: buildEmailBody(target.Template, content), + Body: plainBody, // Plain text fallback + HTML: htmlBody, // HTML content for rich email display Type: messengerTypes.MessageTypeEmail, } @@ -345,13 +348,24 @@ func buildEmailSubject(subject, template string, content *robottypes.DeliveryCon } // buildEmailBody builds the email body content -func buildEmailBody(template string, content *robottypes.DeliveryContent) string { +// buildEmailBody returns HTML and plain text versions of the email body +// Returns: (htmlBody, plainBody) +func buildEmailBody(template string, content *robottypes.DeliveryContent) (string, string) { // TODO: Implement template rendering - // For now, just use the body directly - if content.Body != "" { - return content.Body + // Get markdown content (used as plain text fallback) + markdown := content.Body + if markdown == "" { + markdown = content.Summary } - return content.Summary + + // Convert Markdown to HTML for rich email display + html, err := text.MarkdownToHTML(markdown) + if err != nil { + // Fallback: use markdown as both HTML and plain text + return markdown, markdown + } + + return html, markdown } // convertAttachments converts DeliveryAttachment to messenger Attachment format diff --git a/agent/robot/types/config_global.go b/agent/robot/types/config_global.go index e5989c53..fcb6eba6 100644 --- a/agent/robot/types/config_global.go +++ b/agent/robot/types/config_global.go @@ -8,8 +8,8 @@ import "sync" var ( // defaultEmailChannel - default messenger channel name for sending emails // Can be configured via SetDefaultEmailChannel() - // Default: "email" (maps to messengers/channels.yao configuration) - defaultEmailChannel = "email" + // Default: "default" (maps to messengers/channels.yao configuration) + defaultEmailChannel = "default" // configMu protects global configuration configMu sync.RWMutex diff --git a/go.mod b/go.mod index 58da2437..327c8e8f 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,8 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/JohannesKaufmann/dom v0.2.0 // indirect + github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0 // indirect github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect @@ -124,7 +126,7 @@ require ( github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/sergi/go-diff v1.3.1 // indirect + github.com/sergi/go-diff v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect @@ -147,6 +149,7 @@ require ( github.com/xuri/nfp v0.0.1 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yuin/goldmark v1.7.16 // indirect go.opentelemetry.io/otel v1.37.0 // indirect golang.org/x/arch v0.17.0 // indirect golang.org/x/image v0.29.0 // indirect diff --git a/go.sum b/go.sum index 68e1afeb..d0f6a7d8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ= +github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo= +github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0 h1:mklaPbT4f/EiDr1Q+zPrEt9lgKAkVrIBtWf33d9GpVA= +github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0/go.mod h1:D56Cl9r8M5i3UwAchE+LlLc5hPN3kJtdZNVJn06lSHU= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= @@ -272,8 +276,10 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= +github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= @@ -344,6 +350,8 @@ github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=