fix: align gateway routing to openclaw mainline

This commit is contained in:
Haitao Pan 2026-04-13 15:46:07 +08:00
parent 90b39eebbb
commit 647fa2e84a
10 changed files with 78 additions and 36 deletions

View File

@ -134,7 +134,6 @@ Response shape:
{ "providerId": "gemini", "label": "Gemini" }
],
"gatewayProviders": [
{ "providerId": "local", "label": "Local" },
{ "providerId": "openclaw", "label": "OpenClaw" }
],
"capabilities": {
@ -146,7 +145,6 @@ Response shape:
{ "providerId": "gemini", "label": "Gemini" }
],
"gatewayProviders": [
{ "providerId": "local", "label": "Local" },
{ "providerId": "openclaw", "label": "OpenClaw" }
]
}
@ -164,7 +162,8 @@ Notes:
- `gemini` -> `https://acp-server.svc.plus/gemini/acp/rpc`
- APP traffic reaches those upstreams through the bridge's canonical public
ACP path, not by depending on upstream URLs directly
- upstream ACP auth uses `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
- upstream ACP auth prefers bridge-owned service auth and otherwise reuses the
inbound bridge bearer header
- `multiAgent` is controlled by `ACP_MULTI_AGENT_ENABLED`, default `true`
### 3.2 `session.start`
@ -405,7 +404,7 @@ Suggested APP-side view model:
{
"executionTargets": ["single-agent", "multi-agent", "gateway"],
"singleAgentProviders": ["codex", "opencode", "gemini"],
"gatewayProviders": ["local", "openclaw"]
"gatewayProviders": ["openclaw"]
}
```
@ -426,7 +425,8 @@ UI binding guidance:
- provider picker for single-agent mode should be populated from
`providerCatalog`
- gateway picker should be populated from `gatewayProviders`
- gateway UI should display `local` and `openclaw` as selectable providers
- gateway UI should display the bridge-owned `openclaw` provider from
`gatewayProviders`
- disabled or unavailable states should come from `xworkmate.routing.resolve`
response fields such as:
- `unavailable`
@ -509,10 +509,10 @@ Response fields:
Notes:
- empty or unsupported `gatewayProviderId` values are normalized to
`openclaw`
- for `gatewayProviderId=openclaw`, the bridge overrides runtime endpoint
selection to `wss://openclaw.svc.plus`
- for `gatewayProviderId=local`, the bridge keeps the caller-provided local
gateway endpoint configuration
- upstream gateway auth uses `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
- the app does not provide production openclaw endpoint truth

View File

@ -65,7 +65,7 @@ If the bridge reports execution-target metadata such as `single-agent`,
`multi-agent`, or `gateway`, the app should treat those values as routing
results, not as shell-level surface categories.
If the bridge reports gateway provider IDs such as `local` or `openclaw`, the
If the bridge reports gateway provider IDs such as `openclaw`, the
app should treat them as bridge-owned gateway backend identifiers, not as
independent app entrypoints.

View File

@ -16,7 +16,7 @@
- `acp.capabilities` 返回动态 provider 列表。
- 至少覆盖:
- `singleAgentProviders`: `opencode / codex / gemini`
- `gatewayProviders`: `local / openclaw`
- `gatewayProviders`: `openclaw`
- `xworkmate.routing.resolve` 根据 `taskPrompt`、`executionTarget`、`selectedSkills` 返回正确的:
- `resolvedExecutionTarget`
- `resolvedProviderId`
@ -101,7 +101,7 @@ flutter test test/runtime/app_controller_single_agent_workspace_binding_regressi
- `acp.capabilities` 的 provider 列表来自 bridge 当前环境,而不是本地写死。
- bridge 内建生产 catalog 包含 `codex / opencode / gemini`,且不依赖 app 侧预同步。
- bridge 还会暴露 `gatewayProviders = local / openclaw`。
- bridge 还会暴露 `gatewayProviders = openclaw`。
- `xworkmate.routing.resolve` 在 skill / prompt / target 组合下,返回合理的
`resolvedProviderId``resolvedGatewayProviderId`

View File

@ -82,7 +82,7 @@ func (s *Server) runGateway(
_ = ctx
gatewayProvider := strings.TrimSpace(shared.StringArg(params, "gatewayProvider", ""))
if gatewayProvider == "" {
gatewayProvider = router.GatewayProviderLocal
gatewayProvider = router.GatewayProviderOpenClaw
}
result := s.gateway.RequestByMode(
gatewayProvider,
@ -145,10 +145,14 @@ func (s *Server) runSingleAgentViaExternalProvider(
notify(message)
}
}
authorization := firstNonEmptyString(
strings.TrimSpace(provider.AuthorizationHeader),
strings.TrimSpace(shared.StringArg(params, inboundAuthorizationHeaderKey, "")),
)
response, err := requestExternalACP(
ctx,
endpoint,
provider.AuthorizationHeader,
authorization,
method,
forwardParams,
combinedNotify,

View File

@ -54,7 +54,7 @@ func handleGatewayConnect(
},
}
if request.Mode == "" {
request.Mode = "local"
request.Mode = "openclaw"
}
request = applyProductionGatewayRouting(request)
request.ReportedRemoteAddress = resolveGatewayReportedRemoteAddress(server, request)

View File

@ -121,10 +121,6 @@ func providerLabel(provider syncedProvider) string {
func availableGatewayProviderCatalog() []map[string]any {
return []map[string]any{
{
"providerId": router.GatewayProviderLocal,
"label": "Local",
},
{
"providerId": router.GatewayProviderOpenClaw,
"label": "OpenClaw",

View File

@ -45,8 +45,8 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) {
if len(providerCatalog) != 3 {
t.Fatalf("expected 3 built-in providers, got %#v", providerCatalog)
}
if len(gatewayProviders) != 2 {
t.Fatalf("expected 2 built-in gateway providers, got %#v", gatewayProviders)
if len(gatewayProviders) != 1 {
t.Fatalf("expected 1 built-in gateway provider, got %#v", gatewayProviders)
}
wantOrder := []string{"codex", "opencode", "gemini"}
wantLabels := []string{"Codex", "OpenCode", "Gemini"}
@ -58,8 +58,8 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) {
t.Fatalf("expected label %q at index %d, got %#v", wantLabels[index], index, providerCatalog)
}
}
wantGatewayOrder := []string{"local", "openclaw"}
wantGatewayLabels := []string{"Local", "OpenClaw"}
wantGatewayOrder := []string{"openclaw"}
wantGatewayLabels := []string{"OpenClaw"}
for index, want := range wantGatewayOrder {
if got := gatewayProviders[index]["providerId"]; got != want {
t.Fatalf("expected gateway provider %q at index %d, got %#v", want, index, gatewayProviders)
@ -70,6 +70,51 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) {
}
}
func TestBuiltInProviderReusesInboundBridgeBearerWhenUpstreamAuthUnset(t *testing.T) {
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got != "Bearer bridge-token" {
t.Fatalf("expected inbound bridge bearer header, got %q", got)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"jsonrpc": "2.0",
"id": "run-auth-fallback",
"result": map[string]any{
"success": true,
"output": "forwarded-auth-fallback-ok",
},
})
}))
defer externalServer.Close()
t.Setenv("INTERNAL_SERVICE_TOKEN", "")
t.Setenv("BRIDGE_AUTH_TOKEN", "")
server := NewServer()
setTestBridgeProvider(server, syncedProvider{
ProviderID: "codex",
Label: "Codex",
Endpoint: externalServer.URL,
Enabled: true,
})
recorder := httptest.NewRecorder()
request := httptest.NewRequest(
http.MethodPost,
"http://127.0.0.1/acp/rpc",
strings.NewReader(`{"jsonrpc":"2.0","id":"run-auth-fallback","method":"session.start","params":{"sessionId":"s1","threadId":"t1","taskPrompt":"hello","workingDirectory":"`+t.TempDir()+`","routing":{"routingMode":"explicit","explicitExecutionTarget":"singleAgent","explicitProviderId":"codex"}}}`),
)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", "Bearer bridge-token")
server.HandleRPC(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", recorder.Code)
}
if !strings.Contains(recorder.Body.String(), "forwarded-auth-fallback-ok") {
t.Fatalf("expected forwarded provider response, got %q", recorder.Body.String())
}
}
func TestProductionProviderCatalogFallsBackToBridgeAuthToken(t *testing.T) {
t.Setenv("INTERNAL_SERVICE_TOKEN", "")
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-auth-token")

View File

@ -102,7 +102,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
name: "image-cog",
prompt: "use image-cog to generate consistent characters",
expectedExecutionTarget: "gateway",
expectedGatewayProviderID: "local",
expectedGatewayProviderID: "openclaw",
expectedSkillSource: "find_skills",
expectedNeedsSkillInstall: true,
},
@ -110,7 +110,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
name: "image-video-generation-editting",
prompt: "wan 图生视频并做视频编辑",
expectedExecutionTarget: "gateway",
expectedGatewayProviderID: "local",
expectedGatewayProviderID: "openclaw",
expectedSkillSource: "find_skills",
expectedNeedsSkillInstall: true,
},
@ -118,7 +118,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
name: "video-translator",
prompt: "translate video subtitles and dub the clip",
expectedExecutionTarget: "gateway",
expectedGatewayProviderID: "local",
expectedGatewayProviderID: "openclaw",
expectedSkillSource: "find_skills",
expectedNeedsSkillInstall: true,
},
@ -126,7 +126,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
name: "browser-search-news",
prompt: "跨浏览器执行并搜索最新资讯采集结果",
expectedExecutionTarget: "gateway",
expectedGatewayProviderID: "local",
expectedGatewayProviderID: "openclaw",
expectedSkillSource: "local_match",
expectedResolvedSkill: "Browser Automation",
},
@ -139,7 +139,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
"workingDirectory": "/tmp/workspace",
"routing": map[string]any{
"routingMode": "auto",
"preferredGatewayProviderId": "local",
"preferredGatewayProviderId": "openclaw",
"allowSkillInstall": false,
"availableSkills": func() []any {
values := make([]any, 0, len(localAvailableSkills))

View File

@ -17,7 +17,6 @@ const (
ExecutionTargetGateway = "gateway"
ExecutionTargetGatewayChat = "gateway-chat"
GatewayProviderLocal = "local"
GatewayProviderOpenClaw = "openclaw"
)
@ -223,15 +222,13 @@ func mapExplicitTarget(
func resolveGatewayProvider(preferredGatewayProviderID string) string {
providerID := normalizeGatewayProvider(preferredGatewayProviderID)
if providerID == "" {
providerID = GatewayProviderLocal
providerID = GatewayProviderOpenClaw
}
return providerID
}
func normalizeGatewayProvider(value string) string {
switch normalize(value) {
case GatewayProviderLocal:
return GatewayProviderLocal
case GatewayProviderOpenClaw:
return GatewayProviderOpenClaw
default:

View File

@ -84,14 +84,14 @@ func TestResolveAutoOnlineTaskToGateway(t *testing.T) {
result := resolver.Resolve(Request{
Prompt: "跨浏览器执行并搜索最新资讯",
PreferredGatewayProviderID: GatewayProviderLocal,
PreferredGatewayProviderID: GatewayProviderOpenClaw,
})
if result.ResolvedExecutionTarget != ExecutionTargetGateway {
t.Fatalf("expected gateway route, got %#v", result)
}
if result.ResolvedGatewayProviderID != GatewayProviderLocal {
t.Fatalf("expected local gateway provider, got %#v", result)
if result.ResolvedGatewayProviderID != GatewayProviderOpenClaw {
t.Fatalf("expected openclaw gateway provider, got %#v", result)
}
}
@ -121,14 +121,14 @@ func TestResolveUsesClassifierForBoundarySamples(t *testing.T) {
result := resolver.Resolve(Request{
Prompt: "help me handle this ambiguous request",
PreferredGatewayProviderID: GatewayProviderLocal,
PreferredGatewayProviderID: GatewayProviderOpenClaw,
})
if result.ResolvedExecutionTarget != ExecutionTargetGateway {
t.Fatalf("expected classifier to resolve gateway route, got %#v", result)
}
if result.ResolvedGatewayProviderID != GatewayProviderLocal {
t.Fatalf("expected local gateway provider, got %#v", result)
if result.ResolvedGatewayProviderID != GatewayProviderOpenClaw {
t.Fatalf("expected openclaw gateway provider, got %#v", result)
}
}