package tools import ( "context" "encoding/json" "fmt" "os" "strconv" "strings" "time" "isphere-ai-bridge/internal/helperclient" ) const ( uiaSendMessageOp = "uia_send_message" EnvSendConnectorMode = "ISPHERE_SEND_CONNECTOR_MODE" EnvSendUIAHelperPath = "ISPHERE_SEND_UIA_HELPER_PATH" EnvSendUIAHwnd = "ISPHERE_SEND_UIA_HWND" EnvSendUIAEditorAutomationID = "ISPHERE_SEND_UIA_EDITOR_AUTOMATION_ID" EnvSendUIButtonAutomationID = "ISPHERE_SEND_UIA_BUTTON_AUTOMATION_ID" EnvSendUIATimeoutSeconds = "ISPHERE_SEND_UIA_TIMEOUT_SECONDS" ) type UiaSendMessageAdapterConfig struct { HelperPath string TimeoutSeconds int Hwnd string SendEditorAutomationID string SendButtonAutomationID string Mode string } type UiaHelperCaller interface { CallUiaHelper(ctx context.Context, op string, args map[string]any) (uiaHelperResponse, error) } type uiaHelperResponse struct { OK bool Data map[string]any ErrCode string ErrText string } type helperClientUiaCaller struct { client helperclient.Client } type uiaSendMessageConnector struct { config UiaSendMessageAdapterConfig caller UiaHelperCaller } func NewSendMessageConnectorFromEnv() SendMessageConnector { mode := strings.TrimSpace(strings.ToLower(os.Getenv(EnvSendConnectorMode))) if mode != "uia_rpa" && mode != "uia-rpa" { return nil } timeoutSeconds := 10 if raw := strings.TrimSpace(os.Getenv(EnvSendUIATimeoutSeconds)); raw != "" { if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 { timeoutSeconds = parsed } } return NewUiaSendMessageConnector(UiaSendMessageAdapterConfig{ Mode: "enabled", HelperPath: strings.TrimSpace(os.Getenv(EnvSendUIAHelperPath)), TimeoutSeconds: timeoutSeconds, Hwnd: strings.TrimSpace(os.Getenv(EnvSendUIAHwnd)), SendEditorAutomationID: strings.TrimSpace(os.Getenv(EnvSendUIAEditorAutomationID)), SendButtonAutomationID: strings.TrimSpace(os.Getenv(EnvSendUIButtonAutomationID)), }, nil) } func NewUiaSendMessageConnector(config UiaSendMessageAdapterConfig, caller UiaHelperCaller) SendMessageConnector { config.Mode = strings.TrimSpace(strings.ToLower(config.Mode)) if config.Mode == "" { config.Mode = "disabled" } if config.SendEditorAutomationID == "" { config.SendEditorAutomationID = "rtbSendMessage" } if config.SendButtonAutomationID == "" { config.SendButtonAutomationID = "btnSend" } if caller == nil { timeout := 10 * time.Second if config.TimeoutSeconds > 0 { timeout = time.Duration(config.TimeoutSeconds) * time.Second } caller = helperClientUiaCaller{client: helperclient.Client{HelperPath: config.HelperPath, Timeout: timeout}} } return uiaSendMessageConnector{config: config, caller: caller} } func (c uiaSendMessageConnector) ExecuteSendMessage(ctx context.Context, request SendMessageConnectorRequest) (SendMessageConnectorResult, error) { if c.config.Mode != "enabled" && c.config.Mode != "uia_rpa" { result := SendMessageConnectorResult{ Accepted: false, Status: "blocked", ErrorCode: "uia_rpa_mode_blocked", ErrorMessage: "A-route UIA/RPA send adapter is disabled", ConnectorMode: "uia-rpa-disabled", } return result, fmt.Errorf(result.ErrorMessage) } if err := validateUiaSendMessageConfig(c.config); err != nil { result := SendMessageConnectorResult{ Accepted: false, Status: "blocked", ErrorCode: "uia_rpa_config_missing", ErrorMessage: err.Error(), ConnectorMode: "uia-rpa", } return result, err } if err := validateUiaSendMessageRequest(request); err != nil { return SendMessageConnectorResult{ Accepted: false, Status: "failed", ErrorCode: "uia_rpa_invalid_request", ErrorMessage: err.Error(), ConnectorMode: "uia-rpa", }, nil } args := map[string]any{ "hwnd": c.config.Hwnd, "send_editor_automation_id": c.config.SendEditorAutomationID, "send_button_automation_id": c.config.SendButtonAutomationID, "target_ref": request.TargetRef, "content_text": request.ContentText, "content_sha256": request.ContentSHA256, "idempotency_key_sha256": request.IdempotencyKeySHA256, } response, err := c.caller.CallUiaHelper(ctx, uiaSendMessageOp, args) if err != nil { return SendMessageConnectorResult{ Accepted: false, Status: "failed", ErrorCode: "uia_rpa_helper_error", ErrorMessage: err.Error(), ConnectorMode: "uia-rpa", }, err } if !response.OK { code := strings.TrimSpace(response.ErrCode) if code == "" { code = "uia_rpa_helper_rejected" } message := strings.TrimSpace(response.ErrText) if message == "" { message = "UIA helper rejected send action" } return SendMessageConnectorResult{ Accepted: false, Status: "failed", ErrorCode: code, ErrorMessage: message, ConnectorMode: "uia-rpa", }, nil } ackRef, _ := response.Data["ack_ref"].(string) if strings.TrimSpace(ackRef) == "" { ackRef = "uia:" + c.config.Hwnd + ":" + c.config.SendButtonAutomationID } return SendMessageConnectorResult{ Accepted: true, Status: "accepted", AckRef: ackRef, ConnectorMode: "uia-rpa", ProductionEnabled: true, SideEffects: map[string]any{ "sent_message": true, "typed_text": true, "clicked_ui": true, "uploaded_file": false, "sent_file": false, "captured_network": false, "attached_hook": false, "modified_client_data": false, }, }, nil } func validateUiaSendMessageConfig(config UiaSendMessageAdapterConfig) error { if strings.TrimSpace(config.Hwnd) == "" { return fmt.Errorf("ISPHERE_SEND_UIA_HWND is required for A-route UIA send") } if strings.TrimSpace(config.SendEditorAutomationID) == "" { return fmt.Errorf("send editor automation id is required") } if strings.TrimSpace(config.SendButtonAutomationID) == "" { return fmt.Errorf("send button automation id is required") } return nil } func validateUiaSendMessageRequest(request SendMessageConnectorRequest) error { if strings.TrimSpace(request.TargetRef) == "" { return fmt.Errorf("target_ref is required") } if strings.TrimSpace(request.ContentText) == "" { return fmt.Errorf("content_text is required") } if strings.TrimSpace(request.ContentSHA256) == "" { return fmt.Errorf("content_sha256 is required") } if strings.TrimSpace(request.IdempotencyKeySHA256) == "" { return fmt.Errorf("idempotency_key_sha256 is required") } if request.ExecutionMode != sendMessageProductionMode { return fmt.Errorf("execution_mode must be production") } return nil } func (c helperClientUiaCaller) CallUiaHelper(ctx context.Context, op string, args map[string]any) (uiaHelperResponse, error) { response, err := c.client.Call(ctx, op, args) if err != nil { return uiaHelperResponse{}, err } out := uiaHelperResponse{OK: response != nil && response.OK} if response != nil { out.Data = decodeRawData(response.Data) if response.Error != nil { out.ErrCode = response.Error.Code out.ErrText = response.Error.Message } } if out.Data == nil { out.Data = map[string]any{} } return out, nil } func uiaHelperResponseFromRaw(data map[string]json.RawMessage, ok bool) uiaHelperResponse { return uiaHelperResponse{OK: ok, Data: decodeRawData(data)} }