- Updated the GetStepsForResume method to return only steps with valid resume statuses (failed or interrupted), improving recovery context handling. - Enhanced error handling in message processing across various integrations, including friendly error messages for users in different locales. - Modified file handling in message integrations (Dingtalk, Discord, Feishu, Telegram, Weixin) to differentiate between image and file types, ensuring appropriate content delivery.
257 lines
7.7 KiB
Go
257 lines
7.7 KiB
Go
package events
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
|
||
agent "github.com/yaoapp/yao/agent"
|
||
"github.com/yaoapp/yao/agent/assistant"
|
||
agentcontext "github.com/yaoapp/yao/agent/context"
|
||
robotstore "github.com/yaoapp/yao/agent/robot/store"
|
||
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
||
eventtypes "github.com/yaoapp/yao/event/types"
|
||
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||
)
|
||
|
||
// handleMessage processes messages from external integrations (Telegram, etc.).
|
||
// It calls the Host Agent with the provided messages and returns a MessageResult.
|
||
// Action detection is done via the Host Agent's Next hook return value.
|
||
func (h *robotHandler) handleMessage(ctx context.Context, ev *eventtypes.Event, resp chan<- eventtypes.Result) {
|
||
var payload MessagePayload
|
||
if err := ev.Should(&payload); err != nil {
|
||
log.Error("message handler: invalid payload: %v", err)
|
||
if ev.IsCall {
|
||
resp <- eventtypes.Result{Err: err}
|
||
}
|
||
return
|
||
}
|
||
|
||
log.Info("message handler: robot=%s channel=%s msg_id=%s",
|
||
payload.RobotID, payload.Metadata.Channel, payload.Metadata.MessageID)
|
||
|
||
result, err := callHostAgent(ctx, &payload)
|
||
if err != nil {
|
||
log.Error("message handler: host agent call failed robot=%s: %v", payload.RobotID, err)
|
||
|
||
if reply := getReplyFunc(); reply != nil {
|
||
errMsg := friendlyErrorMessage(payload.Metadata.Locale)
|
||
_ = reply(ctx, &agentcontext.Message{
|
||
Role: agentcontext.RoleAssistant,
|
||
Content: errMsg,
|
||
}, payload.Metadata)
|
||
}
|
||
|
||
if ev.IsCall {
|
||
resp <- eventtypes.Result{Err: err}
|
||
}
|
||
return
|
||
}
|
||
|
||
if reply := getReplyFunc(); reply != nil && result.Message != nil {
|
||
if err := reply(ctx, result.Message, payload.Metadata); err != nil {
|
||
log.Error("message handler: reply failed robot=%s channel=%s: %v",
|
||
payload.RobotID, payload.Metadata.Channel, err)
|
||
}
|
||
}
|
||
|
||
if ev.IsCall {
|
||
resp <- eventtypes.Result{Data: result}
|
||
}
|
||
}
|
||
|
||
// callHostAgent resolves the Host Agent for the robot and calls it with messages.
|
||
// This avoids importing executor/standard to prevent import cycles; instead it
|
||
// calls the assistant directly via assistant.Get + ast.Stream (same as AgentCaller.Call).
|
||
func callHostAgent(ctx context.Context, payload *MessagePayload) (*MessageResult, error) {
|
||
hostID, record, err := resolveHostAssistantID(ctx, payload.RobotID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to resolve host agent: %w", err)
|
||
}
|
||
|
||
ast, err := assistant.Get(hostID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("assistant not found: %s: %w", hostID, err)
|
||
}
|
||
|
||
opts := &agentcontext.Options{
|
||
Skip: &agentcontext.Skip{
|
||
Search: false,
|
||
},
|
||
}
|
||
|
||
authorized := &oauthtypes.AuthorizedInfo{
|
||
UserID: payload.Metadata.SenderID,
|
||
TeamID: record.TeamID,
|
||
}
|
||
chatID := fmt.Sprintf("%s:%s", payload.Metadata.Channel, payload.Metadata.ChatID)
|
||
agentCtx := agentcontext.New(ctx, authorized, chatID)
|
||
agentCtx.AssistantID = hostID
|
||
agentCtx.Referer = "integration"
|
||
agentCtx.Locale = payload.Metadata.Locale
|
||
agentCtx.Metadata = map[string]interface{}{
|
||
"robot_id": payload.RobotID,
|
||
"channel": payload.Metadata.Channel,
|
||
}
|
||
|
||
if dsl := agent.GetAgent(); dsl != nil {
|
||
if cache, err := dsl.GetCacheStore(); err == nil {
|
||
agentCtx.Cache = cache
|
||
}
|
||
}
|
||
|
||
defer agentCtx.Release()
|
||
|
||
response, err := ast.Stream(agentCtx, payload.Messages, opts)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("host agent call failed: %w", err)
|
||
}
|
||
|
||
result := &MessageResult{
|
||
Metadata: payload.Metadata,
|
||
}
|
||
|
||
if response.Completion != nil {
|
||
result.Message = &agentcontext.Message{
|
||
Role: agentcontext.RoleAssistant,
|
||
Content: response.Completion.Content,
|
||
}
|
||
}
|
||
|
||
// Detect action from Next hook return value
|
||
log.Debug("response.Next type=%T value=%+v", response.Next, response.Next)
|
||
if action := detectAction(response.Next); action != nil {
|
||
log.Info("action detected: name=%s payload=%+v", action.Name, action.Payload)
|
||
result.Action = action
|
||
if action.Name == "robot.execute" {
|
||
if execID := executeAction(ctx, payload, record, action); execID != "" {
|
||
result.ExecutionID = execID
|
||
result.Message = &agentcontext.Message{
|
||
Role: agentcontext.RoleAssistant,
|
||
Content: taskDeployedMessage(execID, payload.Metadata.Locale),
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
log.Debug("no action detected from response.Next")
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// executeAction triggers robot execution when the Host Agent returns a
|
||
// confirmed action. Uses the injected TriggerFunc to call robotapi.TriggerManual
|
||
// without creating a circular import.
|
||
func executeAction(ctx context.Context, payload *MessagePayload, record *robotstore.RobotRecord, action *ActionResult) string {
|
||
trigger := getTriggerFunc()
|
||
if trigger == nil {
|
||
log.Warn("message handler: trigger func not registered, cannot execute action for robot=%s", payload.RobotID)
|
||
return ""
|
||
}
|
||
|
||
data, _ := action.Payload.(map[string]interface{})
|
||
goals, _ := data["goals"].(string)
|
||
if goals == "" {
|
||
log.Warn("message handler: confirmed action has no goals, robot=%s", payload.RobotID)
|
||
return ""
|
||
}
|
||
|
||
triggerData := &robottypes.TriggerInput{
|
||
Data: map[string]interface{}{
|
||
"goals": goals,
|
||
"channel": payload.Metadata.Channel,
|
||
"chat_id": payload.Metadata.ChatID,
|
||
"extra": payload.Metadata.Extra,
|
||
},
|
||
}
|
||
|
||
authorized := &oauthtypes.AuthorizedInfo{
|
||
UserID: record.MemberID,
|
||
TeamID: record.TeamID,
|
||
}
|
||
rCtx := robottypes.NewContext(ctx, authorized)
|
||
|
||
execID, accepted, err := trigger(rCtx, payload.RobotID, robottypes.TriggerHuman, triggerData)
|
||
if err != nil {
|
||
log.Error("message handler: execute action failed robot=%s: %v", payload.RobotID, err)
|
||
return ""
|
||
}
|
||
if !accepted {
|
||
log.Warn("message handler: execute action not accepted robot=%s", payload.RobotID)
|
||
return ""
|
||
}
|
||
|
||
log.Info("message handler: execution triggered robot=%s exec_id=%s", payload.RobotID, execID)
|
||
return execID
|
||
}
|
||
|
||
// detectAction checks the Next hook return value for a confirmed action.
|
||
// The Host Agent returns { data: { confirmed: true, robot_id: "...", goals: "..." } }
|
||
// when it detects a confirm_task tool call.
|
||
func detectAction(next interface{}) *ActionResult {
|
||
if next == nil {
|
||
return nil
|
||
}
|
||
|
||
m, ok := next.(map[string]interface{})
|
||
if !ok {
|
||
return nil
|
||
}
|
||
|
||
// Next hook may return { data: { confirmed, ... } } or flat { confirmed, ... }
|
||
data, _ := m["data"].(map[string]interface{})
|
||
if data == nil {
|
||
data = m
|
||
}
|
||
|
||
confirmed, _ := data["confirmed"].(bool)
|
||
if !confirmed {
|
||
return nil
|
||
}
|
||
|
||
return &ActionResult{
|
||
Name: "robot.execute",
|
||
Payload: data,
|
||
}
|
||
}
|
||
|
||
// resolveHostAssistantID resolves the host assistant ID from a robot member ID.
|
||
// Mirrors the logic in openapi/agent/robot/completions.go.
|
||
func resolveHostAssistantID(ctx context.Context, memberID string) (string, *robotstore.RobotRecord, error) {
|
||
store := robotstore.NewRobotStore()
|
||
record, err := store.Get(ctx, memberID)
|
||
if err != nil {
|
||
return "", nil, fmt.Errorf("failed to get robot: %w", err)
|
||
}
|
||
if record == nil {
|
||
return "", nil, fmt.Errorf("robot not found: %s", memberID)
|
||
}
|
||
|
||
config, err := robottypes.ParseConfig(record.RobotConfig)
|
||
if err != nil {
|
||
return "", nil, fmt.Errorf("failed to parse robot config: %w", err)
|
||
}
|
||
|
||
var hostID string
|
||
if config != nil && config.Resources != nil {
|
||
hostID = config.Resources.GetPhaseAgent(robottypes.PhaseHost)
|
||
} else {
|
||
hostID = "__yao." + string(robottypes.PhaseHost)
|
||
}
|
||
|
||
return hostID, record, nil
|
||
}
|
||
|
||
func friendlyErrorMessage(locale string) string {
|
||
if strings.HasPrefix(locale, "zh") {
|
||
return "抱歉,处理您的消息时出现了问题,请稍后重试。"
|
||
}
|
||
return "Sorry, there was a problem processing your message. Please try again later."
|
||
}
|
||
|
||
func taskDeployedMessage(execID string, locale string) string {
|
||
if strings.HasPrefix(locale, "zh") {
|
||
return fmt.Sprintf("任务已部署(执行编号: %s),完成后会将结果发送给你。", execID)
|
||
}
|
||
return fmt.Sprintf("Task deployed (execution: %s). You will receive results once completed.", execID)
|
||
}
|