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" }
|
{ "providerId": "gemini", "label": "Gemini" }
|
||||||
],
|
],
|
||||||
"gatewayProviders": [
|
"gatewayProviders": [
|
||||||
{ "providerId": "local", "label": "Local" },
|
|
||||||
{ "providerId": "openclaw", "label": "OpenClaw" }
|
{ "providerId": "openclaw", "label": "OpenClaw" }
|
||||||
],
|
],
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
@ -146,7 +145,6 @@ Response shape:
|
|||||||
{ "providerId": "gemini", "label": "Gemini" }
|
{ "providerId": "gemini", "label": "Gemini" }
|
||||||
],
|
],
|
||||||
"gatewayProviders": [
|
"gatewayProviders": [
|
||||||
{ "providerId": "local", "label": "Local" },
|
|
||||||
{ "providerId": "openclaw", "label": "OpenClaw" }
|
{ "providerId": "openclaw", "label": "OpenClaw" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -164,7 +162,8 @@ Notes:
|
|||||||
- `gemini` -> `https://acp-server.svc.plus/gemini/acp/rpc`
|
- `gemini` -> `https://acp-server.svc.plus/gemini/acp/rpc`
|
||||||
- APP traffic reaches those upstreams through the bridge's canonical public
|
- APP traffic reaches those upstreams through the bridge's canonical public
|
||||||
ACP path, not by depending on upstream URLs directly
|
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`
|
- `multiAgent` is controlled by `ACP_MULTI_AGENT_ENABLED`, default `true`
|
||||||
|
|
||||||
### 3.2 `session.start`
|
### 3.2 `session.start`
|
||||||
@ -405,7 +404,7 @@ Suggested APP-side view model:
|
|||||||
{
|
{
|
||||||
"executionTargets": ["single-agent", "multi-agent", "gateway"],
|
"executionTargets": ["single-agent", "multi-agent", "gateway"],
|
||||||
"singleAgentProviders": ["codex", "opencode", "gemini"],
|
"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
|
- provider picker for single-agent mode should be populated from
|
||||||
`providerCatalog`
|
`providerCatalog`
|
||||||
- gateway picker should be populated from `gatewayProviders`
|
- 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`
|
- disabled or unavailable states should come from `xworkmate.routing.resolve`
|
||||||
response fields such as:
|
response fields such as:
|
||||||
- `unavailable`
|
- `unavailable`
|
||||||
@ -509,10 +509,10 @@ Response fields:
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
|
- empty or unsupported `gatewayProviderId` values are normalized to
|
||||||
|
`openclaw`
|
||||||
- for `gatewayProviderId=openclaw`, the bridge overrides runtime endpoint
|
- for `gatewayProviderId=openclaw`, the bridge overrides runtime endpoint
|
||||||
selection to `wss://openclaw.svc.plus`
|
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`
|
- upstream gateway auth uses `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
|
||||||
- the app does not provide production openclaw endpoint truth
|
- 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
|
`multi-agent`, or `gateway`, the app should treat those values as routing
|
||||||
results, not as shell-level surface categories.
|
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
|
app should treat them as bridge-owned gateway backend identifiers, not as
|
||||||
independent app entrypoints.
|
independent app entrypoints.
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
- `acp.capabilities` 返回动态 provider 列表。
|
- `acp.capabilities` 返回动态 provider 列表。
|
||||||
- 至少覆盖:
|
- 至少覆盖:
|
||||||
- `singleAgentProviders`: `opencode / codex / gemini`
|
- `singleAgentProviders`: `opencode / codex / gemini`
|
||||||
- `gatewayProviders`: `local / openclaw`
|
- `gatewayProviders`: `openclaw`
|
||||||
- `xworkmate.routing.resolve` 根据 `taskPrompt`、`executionTarget`、`selectedSkills` 返回正确的:
|
- `xworkmate.routing.resolve` 根据 `taskPrompt`、`executionTarget`、`selectedSkills` 返回正确的:
|
||||||
- `resolvedExecutionTarget`
|
- `resolvedExecutionTarget`
|
||||||
- `resolvedProviderId`
|
- `resolvedProviderId`
|
||||||
@ -101,7 +101,7 @@ flutter test test/runtime/app_controller_single_agent_workspace_binding_regressi
|
|||||||
|
|
||||||
- `acp.capabilities` 的 provider 列表来自 bridge 当前环境,而不是本地写死。
|
- `acp.capabilities` 的 provider 列表来自 bridge 当前环境,而不是本地写死。
|
||||||
- bridge 内建生产 catalog 包含 `codex / opencode / gemini`,且不依赖 app 侧预同步。
|
- bridge 内建生产 catalog 包含 `codex / opencode / gemini`,且不依赖 app 侧预同步。
|
||||||
- bridge 还会暴露 `gatewayProviders = local / openclaw`。
|
- bridge 还会暴露 `gatewayProviders = openclaw`。
|
||||||
- `xworkmate.routing.resolve` 在 skill / prompt / target 组合下,返回合理的
|
- `xworkmate.routing.resolve` 在 skill / prompt / target 组合下,返回合理的
|
||||||
`resolvedProviderId` 或 `resolvedGatewayProviderId`。
|
`resolvedProviderId` 或 `resolvedGatewayProviderId`。
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,7 @@ func (s *Server) runGateway(
|
|||||||
_ = ctx
|
_ = ctx
|
||||||
gatewayProvider := strings.TrimSpace(shared.StringArg(params, "gatewayProvider", ""))
|
gatewayProvider := strings.TrimSpace(shared.StringArg(params, "gatewayProvider", ""))
|
||||||
if gatewayProvider == "" {
|
if gatewayProvider == "" {
|
||||||
gatewayProvider = router.GatewayProviderLocal
|
gatewayProvider = router.GatewayProviderOpenClaw
|
||||||
}
|
}
|
||||||
result := s.gateway.RequestByMode(
|
result := s.gateway.RequestByMode(
|
||||||
gatewayProvider,
|
gatewayProvider,
|
||||||
@ -145,10 +145,14 @@ func (s *Server) runSingleAgentViaExternalProvider(
|
|||||||
notify(message)
|
notify(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
authorization := firstNonEmptyString(
|
||||||
|
strings.TrimSpace(provider.AuthorizationHeader),
|
||||||
|
strings.TrimSpace(shared.StringArg(params, inboundAuthorizationHeaderKey, "")),
|
||||||
|
)
|
||||||
response, err := requestExternalACP(
|
response, err := requestExternalACP(
|
||||||
ctx,
|
ctx,
|
||||||
endpoint,
|
endpoint,
|
||||||
provider.AuthorizationHeader,
|
authorization,
|
||||||
method,
|
method,
|
||||||
forwardParams,
|
forwardParams,
|
||||||
combinedNotify,
|
combinedNotify,
|
||||||
|
|||||||
@ -54,7 +54,7 @@ func handleGatewayConnect(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
if request.Mode == "" {
|
if request.Mode == "" {
|
||||||
request.Mode = "local"
|
request.Mode = "openclaw"
|
||||||
}
|
}
|
||||||
request = applyProductionGatewayRouting(request)
|
request = applyProductionGatewayRouting(request)
|
||||||
request.ReportedRemoteAddress = resolveGatewayReportedRemoteAddress(server, request)
|
request.ReportedRemoteAddress = resolveGatewayReportedRemoteAddress(server, request)
|
||||||
|
|||||||
@ -121,10 +121,6 @@ func providerLabel(provider syncedProvider) string {
|
|||||||
|
|
||||||
func availableGatewayProviderCatalog() []map[string]any {
|
func availableGatewayProviderCatalog() []map[string]any {
|
||||||
return []map[string]any{
|
return []map[string]any{
|
||||||
{
|
|
||||||
"providerId": router.GatewayProviderLocal,
|
|
||||||
"label": "Local",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"providerId": router.GatewayProviderOpenClaw,
|
"providerId": router.GatewayProviderOpenClaw,
|
||||||
"label": "OpenClaw",
|
"label": "OpenClaw",
|
||||||
|
|||||||
@ -45,8 +45,8 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) {
|
|||||||
if len(providerCatalog) != 3 {
|
if len(providerCatalog) != 3 {
|
||||||
t.Fatalf("expected 3 built-in providers, got %#v", providerCatalog)
|
t.Fatalf("expected 3 built-in providers, got %#v", providerCatalog)
|
||||||
}
|
}
|
||||||
if len(gatewayProviders) != 2 {
|
if len(gatewayProviders) != 1 {
|
||||||
t.Fatalf("expected 2 built-in gateway providers, got %#v", gatewayProviders)
|
t.Fatalf("expected 1 built-in gateway provider, got %#v", gatewayProviders)
|
||||||
}
|
}
|
||||||
wantOrder := []string{"codex", "opencode", "gemini"}
|
wantOrder := []string{"codex", "opencode", "gemini"}
|
||||||
wantLabels := []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)
|
t.Fatalf("expected label %q at index %d, got %#v", wantLabels[index], index, providerCatalog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wantGatewayOrder := []string{"local", "openclaw"}
|
wantGatewayOrder := []string{"openclaw"}
|
||||||
wantGatewayLabels := []string{"Local", "OpenClaw"}
|
wantGatewayLabels := []string{"OpenClaw"}
|
||||||
for index, want := range wantGatewayOrder {
|
for index, want := range wantGatewayOrder {
|
||||||
if got := gatewayProviders[index]["providerId"]; got != want {
|
if got := gatewayProviders[index]["providerId"]; got != want {
|
||||||
t.Fatalf("expected gateway provider %q at index %d, got %#v", want, index, gatewayProviders)
|
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) {
|
func TestProductionProviderCatalogFallsBackToBridgeAuthToken(t *testing.T) {
|
||||||
t.Setenv("INTERNAL_SERVICE_TOKEN", "")
|
t.Setenv("INTERNAL_SERVICE_TOKEN", "")
|
||||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-auth-token")
|
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-auth-token")
|
||||||
|
|||||||
@ -102,7 +102,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
|||||||
name: "image-cog",
|
name: "image-cog",
|
||||||
prompt: "use image-cog to generate consistent characters",
|
prompt: "use image-cog to generate consistent characters",
|
||||||
expectedExecutionTarget: "gateway",
|
expectedExecutionTarget: "gateway",
|
||||||
expectedGatewayProviderID: "local",
|
expectedGatewayProviderID: "openclaw",
|
||||||
expectedSkillSource: "find_skills",
|
expectedSkillSource: "find_skills",
|
||||||
expectedNeedsSkillInstall: true,
|
expectedNeedsSkillInstall: true,
|
||||||
},
|
},
|
||||||
@ -110,7 +110,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
|||||||
name: "image-video-generation-editting",
|
name: "image-video-generation-editting",
|
||||||
prompt: "wan 图生视频并做视频编辑",
|
prompt: "wan 图生视频并做视频编辑",
|
||||||
expectedExecutionTarget: "gateway",
|
expectedExecutionTarget: "gateway",
|
||||||
expectedGatewayProviderID: "local",
|
expectedGatewayProviderID: "openclaw",
|
||||||
expectedSkillSource: "find_skills",
|
expectedSkillSource: "find_skills",
|
||||||
expectedNeedsSkillInstall: true,
|
expectedNeedsSkillInstall: true,
|
||||||
},
|
},
|
||||||
@ -118,7 +118,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
|||||||
name: "video-translator",
|
name: "video-translator",
|
||||||
prompt: "translate video subtitles and dub the clip",
|
prompt: "translate video subtitles and dub the clip",
|
||||||
expectedExecutionTarget: "gateway",
|
expectedExecutionTarget: "gateway",
|
||||||
expectedGatewayProviderID: "local",
|
expectedGatewayProviderID: "openclaw",
|
||||||
expectedSkillSource: "find_skills",
|
expectedSkillSource: "find_skills",
|
||||||
expectedNeedsSkillInstall: true,
|
expectedNeedsSkillInstall: true,
|
||||||
},
|
},
|
||||||
@ -126,7 +126,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
|||||||
name: "browser-search-news",
|
name: "browser-search-news",
|
||||||
prompt: "跨浏览器执行并搜索最新资讯采集结果",
|
prompt: "跨浏览器执行并搜索最新资讯采集结果",
|
||||||
expectedExecutionTarget: "gateway",
|
expectedExecutionTarget: "gateway",
|
||||||
expectedGatewayProviderID: "local",
|
expectedGatewayProviderID: "openclaw",
|
||||||
expectedSkillSource: "local_match",
|
expectedSkillSource: "local_match",
|
||||||
expectedResolvedSkill: "Browser Automation",
|
expectedResolvedSkill: "Browser Automation",
|
||||||
},
|
},
|
||||||
@ -139,7 +139,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) {
|
|||||||
"workingDirectory": "/tmp/workspace",
|
"workingDirectory": "/tmp/workspace",
|
||||||
"routing": map[string]any{
|
"routing": map[string]any{
|
||||||
"routingMode": "auto",
|
"routingMode": "auto",
|
||||||
"preferredGatewayProviderId": "local",
|
"preferredGatewayProviderId": "openclaw",
|
||||||
"allowSkillInstall": false,
|
"allowSkillInstall": false,
|
||||||
"availableSkills": func() []any {
|
"availableSkills": func() []any {
|
||||||
values := make([]any, 0, len(localAvailableSkills))
|
values := make([]any, 0, len(localAvailableSkills))
|
||||||
|
|||||||
@ -17,7 +17,6 @@ const (
|
|||||||
ExecutionTargetGateway = "gateway"
|
ExecutionTargetGateway = "gateway"
|
||||||
ExecutionTargetGatewayChat = "gateway-chat"
|
ExecutionTargetGatewayChat = "gateway-chat"
|
||||||
|
|
||||||
GatewayProviderLocal = "local"
|
|
||||||
GatewayProviderOpenClaw = "openclaw"
|
GatewayProviderOpenClaw = "openclaw"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -223,15 +222,13 @@ func mapExplicitTarget(
|
|||||||
func resolveGatewayProvider(preferredGatewayProviderID string) string {
|
func resolveGatewayProvider(preferredGatewayProviderID string) string {
|
||||||
providerID := normalizeGatewayProvider(preferredGatewayProviderID)
|
providerID := normalizeGatewayProvider(preferredGatewayProviderID)
|
||||||
if providerID == "" {
|
if providerID == "" {
|
||||||
providerID = GatewayProviderLocal
|
providerID = GatewayProviderOpenClaw
|
||||||
}
|
}
|
||||||
return providerID
|
return providerID
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeGatewayProvider(value string) string {
|
func normalizeGatewayProvider(value string) string {
|
||||||
switch normalize(value) {
|
switch normalize(value) {
|
||||||
case GatewayProviderLocal:
|
|
||||||
return GatewayProviderLocal
|
|
||||||
case GatewayProviderOpenClaw:
|
case GatewayProviderOpenClaw:
|
||||||
return GatewayProviderOpenClaw
|
return GatewayProviderOpenClaw
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -84,14 +84,14 @@ func TestResolveAutoOnlineTaskToGateway(t *testing.T) {
|
|||||||
|
|
||||||
result := resolver.Resolve(Request{
|
result := resolver.Resolve(Request{
|
||||||
Prompt: "跨浏览器执行并搜索最新资讯",
|
Prompt: "跨浏览器执行并搜索最新资讯",
|
||||||
PreferredGatewayProviderID: GatewayProviderLocal,
|
PreferredGatewayProviderID: GatewayProviderOpenClaw,
|
||||||
})
|
})
|
||||||
|
|
||||||
if result.ResolvedExecutionTarget != ExecutionTargetGateway {
|
if result.ResolvedExecutionTarget != ExecutionTargetGateway {
|
||||||
t.Fatalf("expected gateway route, got %#v", result)
|
t.Fatalf("expected gateway route, got %#v", result)
|
||||||
}
|
}
|
||||||
if result.ResolvedGatewayProviderID != GatewayProviderLocal {
|
if result.ResolvedGatewayProviderID != GatewayProviderOpenClaw {
|
||||||
t.Fatalf("expected local gateway provider, got %#v", result)
|
t.Fatalf("expected openclaw gateway provider, got %#v", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,14 +121,14 @@ func TestResolveUsesClassifierForBoundarySamples(t *testing.T) {
|
|||||||
|
|
||||||
result := resolver.Resolve(Request{
|
result := resolver.Resolve(Request{
|
||||||
Prompt: "help me handle this ambiguous request",
|
Prompt: "help me handle this ambiguous request",
|
||||||
PreferredGatewayProviderID: GatewayProviderLocal,
|
PreferredGatewayProviderID: GatewayProviderOpenClaw,
|
||||||
})
|
})
|
||||||
|
|
||||||
if result.ResolvedExecutionTarget != ExecutionTargetGateway {
|
if result.ResolvedExecutionTarget != ExecutionTargetGateway {
|
||||||
t.Fatalf("expected classifier to resolve gateway route, got %#v", result)
|
t.Fatalf("expected classifier to resolve gateway route, got %#v", result)
|
||||||
}
|
}
|
||||||
if result.ResolvedGatewayProviderID != GatewayProviderLocal {
|
if result.ResolvedGatewayProviderID != GatewayProviderOpenClaw {
|
||||||
t.Fatalf("expected local gateway provider, got %#v", result)
|
t.Fatalf("expected openclaw gateway provider, got %#v", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user