350 lines
12 KiB
Go
350 lines
12 KiB
Go
package acp
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"xworkmate-bridge/internal/gatewayruntime"
|
|
"xworkmate-bridge/internal/shared"
|
|
)
|
|
|
|
const (
|
|
productionOpenClawGatewayConnectTimeout = 60 * time.Second
|
|
productionOpenClawGatewayChallengeTimeout = 30 * time.Second
|
|
)
|
|
|
|
func (s *Server) handleGatewayMethod(ctx context.Context, method string, params map[string]any, notify func(map[string]any)) (map[string]any, *shared.RPCError) {
|
|
switch method {
|
|
case "xworkmate.gateway.connect":
|
|
return handleGatewayConnect(s, params, notify), nil
|
|
case "xworkmate.gateway.request":
|
|
return handleGatewayRequest(s, params, notify), nil
|
|
case "xworkmate.gateway.disconnect":
|
|
return handleGatewayDisconnect(s, params, notify), nil
|
|
default:
|
|
return nil, &shared.RPCError{Code: -32601, Message: "unknown gateway method: " + method}
|
|
}
|
|
}
|
|
|
|
func handleGatewayConnect(
|
|
server *Server,
|
|
params map[string]any,
|
|
notify func(map[string]any),
|
|
) map[string]any {
|
|
request := gatewayruntime.ConnectRequest{
|
|
RuntimeID: strings.TrimSpace(shared.StringArg(params, "runtimeId", "")),
|
|
Mode: strings.TrimSpace(shared.StringArg(params, "gatewayProviderId", "")),
|
|
ClientID: strings.TrimSpace(shared.StringArg(params, "clientId", "")),
|
|
Locale: strings.TrimSpace(shared.StringArg(params, "locale", "")),
|
|
UserAgent: strings.TrimSpace(shared.StringArg(params, "userAgent", "")),
|
|
ConnectAuthMode: strings.TrimSpace(shared.StringArg(params, "connectAuthMode", "")),
|
|
ConnectAuthFields: parseGatewayRuntimeStringSlice(params["connectAuthFields"]),
|
|
ConnectAuthSources: parseGatewayRuntimeStringSlice(params["connectAuthSources"]),
|
|
HasSharedAuth: parseBool(params["hasSharedAuth"]),
|
|
HasDeviceToken: parseBool(params["hasDeviceToken"]),
|
|
Endpoint: gatewayruntime.Endpoint{
|
|
Host: strings.TrimSpace(shared.StringArg(shared.AsMap(params["endpoint"]), "host", "")),
|
|
Port: parsePositiveInt(shared.AsMap(params["endpoint"])["port"]),
|
|
TLS: parseBool(shared.AsMap(params["endpoint"])["tls"]),
|
|
},
|
|
PackageInfo: gatewayruntime.PackageInfo{
|
|
AppName: strings.TrimSpace(shared.StringArg(shared.AsMap(params["packageInfo"]), "appName", "")),
|
|
PackageName: strings.TrimSpace(shared.StringArg(shared.AsMap(params["packageInfo"]), "packageName", "")),
|
|
Version: strings.TrimSpace(shared.StringArg(shared.AsMap(params["packageInfo"]), "version", "")),
|
|
BuildNumber: strings.TrimSpace(shared.StringArg(shared.AsMap(params["packageInfo"]), "buildNumber", "")),
|
|
},
|
|
DeviceInfo: gatewayruntime.DeviceInfo{
|
|
Platform: strings.TrimSpace(shared.StringArg(shared.AsMap(params["deviceInfo"]), "platform", "")),
|
|
PlatformVersion: strings.TrimSpace(shared.StringArg(shared.AsMap(params["deviceInfo"]), "platformVersion", "")),
|
|
DeviceFamily: strings.TrimSpace(shared.StringArg(shared.AsMap(params["deviceInfo"]), "deviceFamily", "")),
|
|
ModelIdentifier: strings.TrimSpace(shared.StringArg(shared.AsMap(params["deviceInfo"]), "modelIdentifier", "")),
|
|
},
|
|
Identity: gatewayruntime.DeviceIdentity{
|
|
DeviceID: strings.TrimSpace(shared.StringArg(shared.AsMap(params["identity"]), "deviceId", "")),
|
|
PublicKeyBase64URL: strings.TrimSpace(shared.StringArg(shared.AsMap(params["identity"]), "publicKeyBase64Url", "")),
|
|
PrivateKeyBase64URL: strings.TrimSpace(shared.StringArg(shared.AsMap(params["identity"]), "privateKeyBase64Url", "")),
|
|
},
|
|
Auth: gatewayruntime.AuthConfig{
|
|
Token: strings.TrimSpace(shared.StringArg(shared.AsMap(params["auth"]), "token", "")),
|
|
DeviceToken: strings.TrimSpace(shared.StringArg(shared.AsMap(params["auth"]), "deviceToken", "")),
|
|
Password: strings.TrimSpace(shared.StringArg(shared.AsMap(params["auth"]), "password", "")),
|
|
},
|
|
}
|
|
if request.Mode == "" {
|
|
request.Mode = "openclaw"
|
|
}
|
|
request = applyProductionGatewayRouting(server, request)
|
|
usesBridgeIdentity := false
|
|
if isOpenClawMode(request.Mode) && strings.TrimSpace(request.Identity.DeviceID) == "" {
|
|
identity, deviceToken := bridgeGatewayOpenClawCredentials()
|
|
request.Identity = identity
|
|
if strings.TrimSpace(request.Auth.DeviceToken) == "" {
|
|
request.Auth.DeviceToken = deviceToken
|
|
}
|
|
request.HasDeviceToken = strings.TrimSpace(request.Auth.DeviceToken) != ""
|
|
usesBridgeIdentity = true
|
|
}
|
|
request.ReportedRemoteAddress = resolveGatewayReportedRemoteAddress(server, request)
|
|
|
|
if server.gateway == nil {
|
|
server.gateway = gatewayruntime.NewManager()
|
|
}
|
|
|
|
result := server.gateway.Connect(request, notify)
|
|
if usesBridgeIdentity && shouldRetryOpenClawGatewayWithSharedToken(result) {
|
|
clearBridgeGatewayDeviceToken()
|
|
request.Auth.DeviceToken = ""
|
|
request.HasDeviceToken = false
|
|
request.Auth.Token = bridgeSharedAuthToken()
|
|
request.HasSharedAuth = strings.TrimSpace(request.Auth.Token) != ""
|
|
if request.HasSharedAuth {
|
|
request.ConnectAuthMode = "shared-token"
|
|
request.ConnectAuthFields = []string{"token"}
|
|
request.ConnectAuthSources = []string{"bridge:repair"}
|
|
result = server.gateway.Connect(request, notify)
|
|
}
|
|
}
|
|
if result.OK && usesBridgeIdentity {
|
|
saveBridgeGatewayDeviceToken(result.ReturnedDeviceToken)
|
|
}
|
|
return map[string]any{
|
|
"ok": result.OK,
|
|
"snapshot": result.Snapshot,
|
|
"auth": result.Auth,
|
|
"returnedDeviceToken": result.ReturnedDeviceToken,
|
|
"error": result.Error,
|
|
}
|
|
}
|
|
|
|
func applyProductionGatewayRouting(
|
|
server *Server,
|
|
request gatewayruntime.ConnectRequest,
|
|
) gatewayruntime.ConnectRequest {
|
|
if !isOpenClawMode(request.Mode) {
|
|
return request
|
|
}
|
|
|
|
gatewayURL := resolveURL(server.config.Upstream.GatewayURL, "GATEWAY_RPC_URL")
|
|
if gatewayURL == "" {
|
|
return withDefaultOpenClawGatewayEndpoint(request)
|
|
}
|
|
|
|
parsed, err := url.Parse(gatewayURL)
|
|
if err != nil || parsed.Hostname() == "" {
|
|
return withDefaultOpenClawGatewayEndpoint(request)
|
|
}
|
|
|
|
tls := strings.ToLower(parsed.Scheme) == "https" || strings.ToLower(parsed.Scheme) == "wss"
|
|
port := parsePositiveInt(parsed.Port())
|
|
if port == 0 {
|
|
if tls {
|
|
port = 443
|
|
} else {
|
|
port = 80
|
|
}
|
|
}
|
|
|
|
request.Endpoint = gatewayruntime.Endpoint{
|
|
Host: parsed.Hostname(),
|
|
Port: port,
|
|
TLS: tls,
|
|
Path: parsed.EscapedPath(),
|
|
}
|
|
request.Auth.Token = bridgeSharedAuthToken()
|
|
request.Auth.Password = ""
|
|
request.ConnectAuthMode = "shared-token"
|
|
request.ConnectAuthFields = []string{"token"}
|
|
request.ConnectAuthSources = []string{"bridge"}
|
|
request.HasSharedAuth = request.Auth.Token != ""
|
|
return withDefaultOpenClawGatewayEndpoint(request)
|
|
}
|
|
|
|
func gatewayStatusForManager(manager *gatewayruntime.Manager) string {
|
|
if manager != nil && manager.HasConnectedSession() {
|
|
return "connected"
|
|
}
|
|
return "disconnected"
|
|
}
|
|
|
|
func (s *Server) gatewayStatusForSystemLogs() string {
|
|
if gatewayStatusForManager(s.gateway) == "connected" {
|
|
return "connected"
|
|
}
|
|
if rpcErr := ensureProductionGatewayConnected(s, "openclaw", nil); rpcErr != nil {
|
|
return "disconnected"
|
|
}
|
|
return gatewayStatusForManager(s.gateway)
|
|
}
|
|
|
|
func withDefaultOpenClawGatewayEndpoint(
|
|
request gatewayruntime.ConnectRequest,
|
|
) gatewayruntime.ConnectRequest {
|
|
if !isOpenClawMode(request.Mode) {
|
|
return request
|
|
}
|
|
if strings.TrimSpace(request.Endpoint.Host) == "" {
|
|
request.Endpoint.Host = "127.0.0.1"
|
|
}
|
|
if request.Endpoint.Port <= 0 {
|
|
if strings.TrimSpace(request.Endpoint.Host) == "127.0.0.1" ||
|
|
strings.TrimSpace(request.Endpoint.Host) == "localhost" {
|
|
request.Endpoint.Port = 18789
|
|
} else if request.Endpoint.TLS {
|
|
request.Endpoint.Port = 443
|
|
} else {
|
|
request.Endpoint.Port = 80
|
|
}
|
|
}
|
|
return request
|
|
}
|
|
|
|
func handleGatewayRequest(
|
|
server *Server,
|
|
params map[string]any,
|
|
notify func(map[string]any),
|
|
) map[string]any {
|
|
method := strings.TrimSpace(shared.StringArg(params, "method", ""))
|
|
if server.gateway == nil {
|
|
server.gateway = gatewayruntime.NewManager()
|
|
}
|
|
if method == "skills.status" {
|
|
if rpcErr := ensureProductionGatewayConnected(server, "openclaw", notify); rpcErr != nil {
|
|
return map[string]any{
|
|
"ok": false,
|
|
"error": map[string]any{
|
|
"message": rpcErr.Message,
|
|
"code": "OFFLINE",
|
|
},
|
|
}
|
|
}
|
|
timeout := time.Duration(parsePositiveInt(params["timeoutMs"])) * time.Millisecond
|
|
result := server.gateway.RequestByMode(
|
|
"openclaw",
|
|
method,
|
|
shared.AsMap(params["params"]),
|
|
timeout,
|
|
notify,
|
|
)
|
|
return map[string]any{
|
|
"ok": result.OK,
|
|
"payload": result.Payload,
|
|
"error": result.Error,
|
|
}
|
|
}
|
|
timeout := time.Duration(parsePositiveInt(params["timeoutMs"])) * time.Millisecond
|
|
result := server.gateway.Request(
|
|
strings.TrimSpace(shared.StringArg(params, "runtimeId", "")),
|
|
method,
|
|
shared.AsMap(params["params"]),
|
|
timeout,
|
|
notify,
|
|
)
|
|
return map[string]any{
|
|
"ok": result.OK,
|
|
"payload": result.Payload,
|
|
"error": result.Error,
|
|
}
|
|
}
|
|
|
|
func handleGatewayDisconnect(
|
|
server *Server,
|
|
params map[string]any,
|
|
notify func(map[string]any),
|
|
) map[string]any {
|
|
if server.gateway != nil {
|
|
server.gateway.Disconnect(
|
|
strings.TrimSpace(shared.StringArg(params, "runtimeId", "")),
|
|
notify,
|
|
)
|
|
}
|
|
return map[string]any{"accepted": true}
|
|
}
|
|
|
|
func ensureProductionGatewayConnected(
|
|
server *Server,
|
|
mode string,
|
|
notify func(map[string]any),
|
|
) *shared.RPCError {
|
|
normalizedMode := strings.TrimSpace(strings.ToLower(mode))
|
|
if normalizedMode == "" {
|
|
normalizedMode = "openclaw"
|
|
}
|
|
if normalizedMode != "openclaw" {
|
|
return nil
|
|
}
|
|
if server.gateway == nil {
|
|
server.gateway = gatewayruntime.NewManager()
|
|
}
|
|
configureProductionOpenClawGatewayRuntime(server.gateway)
|
|
|
|
request := applyProductionGatewayRouting(
|
|
server,
|
|
gatewayruntime.ConnectRequest{
|
|
RuntimeID: "xworkmate-bridge-openclaw",
|
|
Mode: "openclaw",
|
|
ClientID: "openclaw-macos",
|
|
Locale: "en_US",
|
|
UserAgent: "xworkmate-bridge",
|
|
Endpoint: gatewayruntime.Endpoint{Host: "127.0.0.1", Port: 18789, TLS: false},
|
|
PackageInfo: gatewayruntime.PackageInfo{AppName: "XWorkmate Bridge", PackageName: "xworkmate-bridge", Version: "bridge", BuildNumber: "0"},
|
|
DeviceInfo: gatewayruntime.DeviceInfo{Platform: "macos", DeviceFamily: "Mac", ModelIdentifier: "Mac14,5"},
|
|
},
|
|
)
|
|
identity, deviceToken := bridgeGatewayOpenClawCredentials()
|
|
request.Identity = identity
|
|
request.Auth.DeviceToken = deviceToken
|
|
request.HasDeviceToken = deviceToken != ""
|
|
request.ReportedRemoteAddress = resolveGatewayReportedRemoteAddress(server, request)
|
|
result := server.gateway.Connect(request, notify)
|
|
if shouldRetryOpenClawGatewayWithSharedToken(result) {
|
|
clearBridgeGatewayDeviceToken()
|
|
request.Auth.DeviceToken = ""
|
|
request.HasDeviceToken = false
|
|
request.Auth.Token = bridgeSharedAuthToken()
|
|
request.HasSharedAuth = strings.TrimSpace(request.Auth.Token) != ""
|
|
if request.HasSharedAuth {
|
|
request.ConnectAuthMode = "shared-token"
|
|
request.ConnectAuthFields = []string{"token"}
|
|
request.ConnectAuthSources = []string{"bridge:repair"}
|
|
result = server.gateway.Connect(request, notify)
|
|
}
|
|
}
|
|
if result.OK {
|
|
saveBridgeGatewayDeviceToken(result.ReturnedDeviceToken)
|
|
return nil
|
|
}
|
|
message := strings.TrimSpace(shared.StringArg(result.Error, "message", "gateway connect failed"))
|
|
code := strings.TrimSpace(shared.StringArg(result.Error, "code", ""))
|
|
if code != "" {
|
|
message = code + ": " + message
|
|
}
|
|
return &shared.RPCError{Code: -32002, Message: "GATEWAY_CONNECT_FAILED: " + message}
|
|
}
|
|
|
|
func shouldRetryOpenClawGatewayWithSharedToken(result gatewayruntime.ConnectResult) bool {
|
|
if result.OK || strings.TrimSpace(bridgeSharedAuthToken()) == "" {
|
|
return false
|
|
}
|
|
code := strings.ToUpper(strings.TrimSpace(shared.StringArg(result.Error, "code", "")))
|
|
message := strings.ToLower(strings.TrimSpace(shared.StringArg(result.Error, "message", "")))
|
|
details := shared.AsMap(result.Error["details"])
|
|
detailCode := strings.ToUpper(strings.TrimSpace(shared.StringArg(details, "code", "")))
|
|
return detailCode == "AUTH_DEVICE_TOKEN_MISMATCH" ||
|
|
detailCode == "PAIRING_REQUIRED" ||
|
|
code == "NOT_PAIRED" ||
|
|
strings.Contains(message, "device token mismatch") ||
|
|
strings.Contains(message, "rotate/reissue device token")
|
|
}
|
|
|
|
func configureProductionOpenClawGatewayRuntime(manager *gatewayruntime.Manager) {
|
|
if manager == nil {
|
|
return
|
|
}
|
|
manager.ConnectTimeout = productionOpenClawGatewayConnectTimeout
|
|
manager.ChallengeTimeout = productionOpenClawGatewayChallengeTimeout
|
|
}
|
|
|
|
// Helper functions are now in helpers.go
|