fix: align gateway routing to openclaw mainline
This commit is contained in:
parent
90b39eebbb
commit
647fa2e84a
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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`。
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -54,7 +54,7 @@ func handleGatewayConnect(
|
||||
},
|
||||
}
|
||||
if request.Mode == "" {
|
||||
request.Mode = "local"
|
||||
request.Mode = "openclaw"
|
||||
}
|
||||
request = applyProductionGatewayRouting(request)
|
||||
request.ReportedRemoteAddress = resolveGatewayReportedRemoteAddress(server, request)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user