Merge branch 'codex/bridge-bootstrap-bridge'

This commit is contained in:
Haitao Pan 2026-04-10 15:36:24 +08:00
commit cb3be97ac6
3 changed files with 220 additions and 5 deletions

144
internal/acp/bootstrap.go Normal file
View File

@ -0,0 +1,144 @@
package acp
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"xworkmate-bridge/internal/shared"
)
type bridgeBootstrapConsumeRequest struct {
Ticket string `json:"ticket"`
Bridge string `json:"bridge"`
}
type accountsBridgeBootstrapConsumeResponse struct {
TicketID string `json:"ticketId"`
TargetBridge string `json:"targetBridge"`
OpenclawURL string `json:"openclawUrl"`
AuthMode string `json:"authMode"`
ExchangeToken string `json:"exchangeToken"`
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
}
type bridgeBootstrapResponse struct {
SetupCode string `json:"setupCode"`
BridgeOrigin string `json:"bridgeOrigin"`
AuthMode string `json:"authMode"`
ExpiresAt string `json:"expiresAt"`
IssuedBy string `json:"issuedBy"`
Scopes []string `json:"scopes"`
}
func (s *Server) HandleBridgeBootstrapHealth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"ok": true,
"bridgeOrigin": bridgePublicBaseURL(),
"issuedBy": "xworkmate-bridge",
})
}
func (s *Server) HandleBridgeBootstrapConsume(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req bridgeBootstrapConsumeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
req.Ticket = strings.TrimSpace(req.Ticket)
req.Bridge = strings.TrimSpace(req.Bridge)
if req.Ticket == "" {
http.Error(w, "ticket is required", http.StatusBadRequest)
return
}
if req.Bridge == "" {
req.Bridge = bridgePublicBaseURL()
}
payload, status, err := consumeBootstrapFromAccounts(req)
if err != nil {
http.Error(w, err.Error(), status)
return
}
setupCodePayload := map[string]any{
"url": payload.OpenclawURL,
"token": payload.ExchangeToken,
"exchangeToken": payload.ExchangeToken,
"authMode": payload.AuthMode,
"expiresAt": payload.ExpiresAt,
"bridgeOrigin": req.Bridge,
"issuedBy": "xworkmate-bridge",
}
setupCodeBytes, err := json.Marshal(setupCodePayload)
if err != nil {
http.Error(w, "failed to encode setup payload", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(bridgeBootstrapResponse{
SetupCode: string(setupCodeBytes),
BridgeOrigin: req.Bridge,
AuthMode: payload.AuthMode,
ExpiresAt: payload.ExpiresAt,
IssuedBy: "xworkmate-bridge",
Scopes: append([]string(nil), payload.Scopes...),
})
}
func consumeBootstrapFromAccounts(req bridgeBootstrapConsumeRequest) (accountsBridgeBootstrapConsumeResponse, int, error) {
baseURL := strings.TrimRight(strings.TrimSpace(shared.EnvOrDefault("ACCOUNTS_BASE_URL", "https://accounts.svc.plus")), "/")
if baseURL == "" {
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("accounts base url is not configured")
}
serviceToken := strings.TrimSpace(shared.EnvOrDefault("INTERNAL_SERVICE_TOKEN", ""))
if serviceToken == "" {
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("internal service token is not configured")
}
body, err := json.Marshal(req)
if err != nil {
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("failed to encode consume request")
}
httpReq, err := http.NewRequest(http.MethodPost, baseURL+"/api/internal/xworkmate/bridge/bootstrap/consume", bytes.NewReader(body))
if err != nil {
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("failed to create consume request")
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("X-Service-Token", serviceToken)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return accountsBridgeBootstrapConsumeResponse{}, http.StatusBadGateway, fmt.Errorf("failed to contact accounts service")
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return accountsBridgeBootstrapConsumeResponse{}, resp.StatusCode, fmt.Errorf("accounts bootstrap consume failed")
}
var payload accountsBridgeBootstrapConsumeResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return accountsBridgeBootstrapConsumeResponse{}, http.StatusBadGateway, fmt.Errorf("failed to decode accounts bootstrap response")
}
return payload, http.StatusOK, nil
}
func bridgePublicBaseURL() string {
value := strings.TrimSpace(shared.EnvOrDefault("BRIDGE_PUBLIC_BASE_URL", "https://xworkmate-bridge.svc.plus"))
if value == "" {
return "https://xworkmate-bridge.svc.plus"
}
return strings.TrimRight(value, "/")
}

View File

@ -0,0 +1,64 @@
package acp
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHandleBridgeBootstrapConsumeReturnsSetupCode(t *testing.T) {
accounts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/internal/xworkmate/bridge/bootstrap/consume" {
http.NotFound(w, r)
return
}
if got := r.Header.Get("X-Service-Token"); got != "internal-test-token" {
http.Error(w, "missing service token", http.StatusUnauthorized)
return
}
_ = json.NewEncoder(w).Encode(accountsBridgeBootstrapConsumeResponse{
TicketID: "ticket-1",
TargetBridge: "https://xworkmate-bridge.svc.plus",
OpenclawURL: "wss://openclaw.svc.plus",
AuthMode: "shared-token",
ExchangeToken: "shared-token-value",
ExpiresAt: "2026-04-10T00:00:00Z",
Scopes: []string{"connect", "pairing.bootstrap"},
})
}))
defer accounts.Close()
t.Setenv("ACCOUNTS_BASE_URL", accounts.URL)
t.Setenv("INTERNAL_SERVICE_TOKEN", "internal-test-token")
t.Setenv("BRIDGE_PUBLIC_BASE_URL", "https://xworkmate-bridge.svc.plus")
server := NewServer()
body := bytes.NewBufferString(`{"ticket":"ticket-1","bridge":"https://xworkmate-bridge.svc.plus"}`)
request := httptest.NewRequest(http.MethodPost, "/bridge/bootstrap/consume", body)
recorder := httptest.NewRecorder()
server.HandleBridgeBootstrapConsume(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected consume success, got %d: %s", recorder.Code, recorder.Body.String())
}
var payload bridgeBootstrapResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.SetupCode == "" {
t.Fatalf("expected non-empty setup code")
}
var decoded map[string]any
if err := json.Unmarshal([]byte(payload.SetupCode), &decoded); err != nil {
t.Fatalf("decode setup code payload: %v", err)
}
if decoded["url"] != "wss://openclaw.svc.plus" {
t.Fatalf("expected openclaw url in setup payload, got %#v", decoded)
}
if decoded["token"] != "shared-token-value" {
t.Fatalf("expected exchange token in setup payload, got %#v", decoded)
}
}

View File

@ -75,6 +75,13 @@ func Serve(args []string) error {
Addr: strings.TrimSpace(*listen),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte("xworkmate-bridge is running"))
case "/bridge/bootstrap/health":
server.HandleBridgeBootstrapHealth(w, r)
case "/bridge/bootstrap/consume":
server.HandleBridgeBootstrapConsume(w, r)
case "/acp/rpc":
server.HandleRPC(w, r)
case "/acp":
@ -848,11 +855,11 @@ func (s *Server) runSingleAgent(
return taskResult{
response: enrichSingleAgentResultArtifacts(map[string]any{
"success": true,
"output": output,
"turnId": turnID,
"mode": "single-agent",
"provider": provider,
"success": true,
"output": output,
"turnId": turnID,
"mode": "single-agent",
"provider": provider,
"effectiveWorkingDirectory": effectiveWorkingDirectory,
}, params),
}