111 lines
2.6 KiB
Go
111 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// OpenClaw HTTP API client — uses the OpenAI-compatible /v1/chat/completions endpoint
|
|
|
|
type GatewayConfig struct {
|
|
URL string // http://host:port
|
|
Token string
|
|
}
|
|
|
|
type GatewayPool struct {
|
|
mu sync.Mutex
|
|
gateways map[string]*GatewayConfig
|
|
client *http.Client
|
|
}
|
|
|
|
func NewGatewayPool() *GatewayPool {
|
|
return &GatewayPool{
|
|
gateways: make(map[string]*GatewayConfig),
|
|
client: &http.Client{Timeout: 120 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (gp *GatewayPool) Register(host string, cfg *GatewayConfig) {
|
|
gp.mu.Lock()
|
|
defer gp.mu.Unlock()
|
|
gp.gateways[host] = cfg
|
|
}
|
|
|
|
func (gp *GatewayPool) Get(host string) *GatewayConfig {
|
|
gp.mu.Lock()
|
|
defer gp.mu.Unlock()
|
|
return gp.gateways[host]
|
|
}
|
|
|
|
// CallAgent sends a message (optionally with an image) to an agent via the OpenAI-compatible API.
|
|
// imageDataURL, if non-empty, should be a data URL: "data:image/png;base64,..."
|
|
func (gp *GatewayPool) CallAgent(host, agentID, message, session, imageDataURL string) (string, error) {
|
|
cfg := gp.Get(host)
|
|
if cfg == nil {
|
|
return "", fmt.Errorf("no gateway configured for host %s", host)
|
|
}
|
|
|
|
// Build the message content — text only, or text + image
|
|
var content any
|
|
if imageDataURL != "" {
|
|
content = []map[string]any{
|
|
{"type": "text", "text": message},
|
|
{"type": "image_url", "image_url": map[string]string{"url": imageDataURL}},
|
|
}
|
|
} else {
|
|
content = message
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"model": "openclaw:" + agentID,
|
|
"user": session,
|
|
"messages": []map[string]any{
|
|
{"role": "user", "content": content},
|
|
},
|
|
})
|
|
|
|
req, err := http.NewRequest("POST", cfg.URL+"/v1/chat/completions", bytes.NewReader(body))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+cfg.Token)
|
|
|
|
resp, err := gp.client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("http: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read body: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
return "", fmt.Errorf("http %d: %s", resp.StatusCode, string(data))
|
|
}
|
|
|
|
var result struct {
|
|
Choices []struct {
|
|
Message struct {
|
|
Content string `json:"content"`
|
|
} `json:"message"`
|
|
} `json:"choices"`
|
|
}
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return "", fmt.Errorf("parse: %w", err)
|
|
}
|
|
|
|
if len(result.Choices) == 0 || result.Choices[0].Message.Content == "" {
|
|
return "[completed, no text reply]", nil
|
|
}
|
|
|
|
return result.Choices[0].Message.Content, nil
|
|
}
|