feat: configure distributed bridge topology
This commit is contained in:
parent
26560afa5c
commit
995e89cece
@ -54,21 +54,33 @@
|
||||
|
||||
## 3.1 Lightweight Distributed Task Forwarding
|
||||
|
||||
bridge 可以把本机收到的 HTTP 任务提交转发到另一个 bridge endpoint,用于轻量分布式部署。例如:
|
||||
bridge 可以把本机收到的 HTTP 任务提交转发到另一个 bridge endpoint,用于轻量分布式部署。当前实现落地的是双节点分布式拓扑:
|
||||
|
||||
- `cn-xworkmate-bridge.svc.plus` 负责本地入口和鉴权
|
||||
- `xworkmate-bridge.svc.plus` 负责实际 OpenClaw task runtime
|
||||
- cn bridge 配置 peer endpoint 后,`POST /gateway/openclaw` 的 `session.start` / `session.message` 会转发到 peer 的同一路径
|
||||
- cn bridge 配置 `task_forward_peer_id` 后,`POST /acp/rpc` 的 `session.start` / `session.message` 会转发到 peer 的同一路径
|
||||
- 主 bridge 不配置 `task_forward_peer_id`,因此不会把任务反向转发到 CN
|
||||
|
||||
配置:
|
||||
|
||||
```yaml
|
||||
distributed:
|
||||
task_forward_endpoint: "https://xworkmate-bridge.svc.plus"
|
||||
topology: "dual-node"
|
||||
local_node_id: "cn-xworkmate-bridge"
|
||||
task_forward_peer_id: "xworkmate-bridge"
|
||||
nodes:
|
||||
- id: "xworkmate-bridge"
|
||||
role: "primary"
|
||||
public_base_url: "https://xworkmate-bridge.svc.plus"
|
||||
bridge_endpoint: "http://172.29.10.1:8787"
|
||||
- id: "cn-xworkmate-bridge"
|
||||
role: "edge"
|
||||
public_base_url: "https://cn-xworkmate-bridge.svc.plus"
|
||||
bridge_endpoint: "http://172.29.10.2:8787"
|
||||
task_forward_token: ""
|
||||
```
|
||||
|
||||
等价环境变量:
|
||||
单 endpoint 覆盖仍可用于临时验证:
|
||||
|
||||
```text
|
||||
XWORKMATE_BRIDGE_TASK_FORWARD_ENDPOINT=https://xworkmate-bridge.svc.plus
|
||||
@ -77,10 +89,10 @@ XWORKMATE_BRIDGE_TASK_FORWARD_TOKEN=$PEER_BRIDGE_AUTH_TOKEN
|
||||
|
||||
规则:
|
||||
|
||||
- endpoint 是 peer bridge base URL,bridge 会按当前请求路径拼接 `/gateway/openclaw` 或 `/acp/rpc`
|
||||
- `task_forward_peer_id` 指向的节点 `bridge_endpoint` 是 peer bridge base URL,bridge 会按当前请求路径拼接 `/acp/rpc`
|
||||
- 同步消息不能走公网明文:公网 endpoint 必须使用 `https://`
|
||||
- `http://` 只允许 loopback、private、link-local 这类本机或 VPN 内网地址,用于 WireGuard 等隧道已经提供加密的场景
|
||||
- endpoint 可以是公网 HTTPS,也可以是 VPN 内网 HTTP(S)。bridge 明确支持把 `task_forward_endpoint` 配成 WireGuard、WireGuard over VLESS/TCP/TLS、WebSocket/TLS 等隧道后的本机或私网地址
|
||||
- endpoint 可以是公网 HTTPS,也可以是 VPN 内网 HTTP(S)。bridge 明确支持把 `bridge_endpoint` 配成 WireGuard、WireGuard over VLESS/TCP/TLS、WebSocket/TLS 等隧道后的本机或私网地址
|
||||
- 只要求本机网络能路由到 endpoint;bridge 不依赖 config center 或额外注册中心
|
||||
- `task_forward_token` 为空时复用本机 `BRIDGE_AUTH_TOKEN`
|
||||
- 转发请求会带 `X-XWorkmate-Bridge-Forwarded: 1`,收到该 header 后不会再次转发,避免 bridge 之间循环
|
||||
|
||||
@ -18,14 +18,26 @@ openclaw_gateway:
|
||||
queue_timeout: "10m"
|
||||
|
||||
# Lightweight distributed bridge forwarding.
|
||||
# cn-xworkmate-bridge.svc.plus can forward task submissions to a peer bridge,
|
||||
# including VPN-only endpoints such as WireGuard or WireGuard-over-VLESS
|
||||
# addresses that are reachable from this host.
|
||||
# A dual-node topology can keep both bridge nodes explicit while only enabling
|
||||
# task forwarding from the node that sets task_forward_peer_id.
|
||||
# Public plaintext HTTP is rejected; use HTTPS on public paths, or HTTP only on
|
||||
# loopback/private/link-local VPN addresses where the tunnel provides encryption.
|
||||
# The endpoint is the peer bridge base URL; /gateway/openclaw or /acp/rpc is
|
||||
# appended from the inbound request path.
|
||||
# bridge_endpoint is the peer bridge base URL; /gateway/openclaw or /acp/rpc is
|
||||
# appended from the inbound request path when forwarding is enabled.
|
||||
distributed:
|
||||
topology: "dual-node"
|
||||
local_node_id: "cn-xworkmate-bridge"
|
||||
task_forward_peer_id: "xworkmate-bridge"
|
||||
nodes:
|
||||
- id: "xworkmate-bridge"
|
||||
role: "primary"
|
||||
public_base_url: "https://xworkmate-bridge.svc.plus"
|
||||
bridge_endpoint: "http://172.29.10.1:8787"
|
||||
- id: "cn-xworkmate-bridge"
|
||||
role: "edge"
|
||||
public_base_url: "https://cn-xworkmate-bridge.svc.plus"
|
||||
bridge_endpoint: "http://172.29.10.2:8787"
|
||||
# Optional direct override. Leave empty when using the topology above.
|
||||
task_forward_endpoint: ""
|
||||
# Optional. Defaults to BRIDGE_AUTH_TOKEN when omitted.
|
||||
task_forward_token: ""
|
||||
|
||||
@ -29,8 +29,19 @@ type BridgeConfig struct {
|
||||
}
|
||||
|
||||
type DistributedConfig struct {
|
||||
TaskForwardEndpoint string `yaml:"task_forward_endpoint"`
|
||||
TaskForwardToken string `yaml:"task_forward_token"`
|
||||
TaskForwardEndpoint string `yaml:"task_forward_endpoint"`
|
||||
TaskForwardToken string `yaml:"task_forward_token"`
|
||||
Topology string `yaml:"topology"`
|
||||
LocalNodeID string `yaml:"local_node_id"`
|
||||
TaskForwardPeerID string `yaml:"task_forward_peer_id"`
|
||||
Nodes []DistributedNodeConfig `yaml:"nodes"`
|
||||
}
|
||||
|
||||
type DistributedNodeConfig struct {
|
||||
ID string `yaml:"id"`
|
||||
Role string `yaml:"role"`
|
||||
PublicBaseURL string `yaml:"public_base_url"`
|
||||
BridgeEndpoint string `yaml:"bridge_endpoint"`
|
||||
}
|
||||
|
||||
type OpenClawGatewayConfig struct {
|
||||
@ -82,7 +93,11 @@ func resolveDistributedTaskForwardEndpoint(config *BridgeConfig) string {
|
||||
if config != nil {
|
||||
yamlVal = config.Distributed.TaskForwardEndpoint
|
||||
}
|
||||
return resolveURL(yamlVal, "XWORKMATE_BRIDGE_TASK_FORWARD_ENDPOINT", "BRIDGE_TASK_FORWARD_ENDPOINT")
|
||||
return resolveURL(
|
||||
firstNonEmpty(yamlVal, resolveDistributedTopologyTaskForwardEndpoint(config)),
|
||||
"XWORKMATE_BRIDGE_TASK_FORWARD_ENDPOINT",
|
||||
"BRIDGE_TASK_FORWARD_ENDPOINT",
|
||||
)
|
||||
}
|
||||
|
||||
func resolveDistributedTaskForwardToken(config *BridgeConfig) string {
|
||||
@ -100,6 +115,40 @@ func resolveDistributedTaskForwardToken(config *BridgeConfig) string {
|
||||
return bridgeSharedAuthToken()
|
||||
}
|
||||
|
||||
func resolveDistributedTopologyTaskForwardEndpoint(config *BridgeConfig) string {
|
||||
if config == nil {
|
||||
return ""
|
||||
}
|
||||
distributed := config.Distributed
|
||||
topology := strings.TrimSpace(distributed.Topology)
|
||||
if topology == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.EqualFold(topology, "dual-node") {
|
||||
return ""
|
||||
}
|
||||
localNodeID := strings.TrimSpace(distributed.LocalNodeID)
|
||||
peerNodeID := strings.TrimSpace(distributed.TaskForwardPeerID)
|
||||
if localNodeID == "" || peerNodeID == "" || localNodeID == peerNodeID {
|
||||
return ""
|
||||
}
|
||||
for _, node := range distributed.Nodes {
|
||||
if strings.TrimSpace(node.ID) == peerNodeID {
|
||||
return strings.TrimSpace(node.BridgeEndpoint)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func newProductionProviderCatalog() (*BridgeConfig, map[string]syncedProvider, []string) {
|
||||
return newProductionProviderCatalogFromConfig(loadBridgeConfig())
|
||||
}
|
||||
|
||||
62
internal/acp/distributed_config_test.go
Normal file
62
internal/acp/distributed_config_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package acp
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveDistributedTaskForwardEndpointFromDualNodeTopology(t *testing.T) {
|
||||
config := &BridgeConfig{}
|
||||
config.Distributed.Topology = "dual-node"
|
||||
config.Distributed.LocalNodeID = "cn-xworkmate-bridge"
|
||||
config.Distributed.TaskForwardPeerID = "xworkmate-bridge"
|
||||
config.Distributed.Nodes = []DistributedNodeConfig{
|
||||
{
|
||||
ID: "xworkmate-bridge",
|
||||
BridgeEndpoint: "http://172.29.10.1:8787",
|
||||
},
|
||||
{
|
||||
ID: "cn-xworkmate-bridge",
|
||||
BridgeEndpoint: "http://172.29.10.2:8787",
|
||||
},
|
||||
}
|
||||
|
||||
if got := resolveDistributedTaskForwardEndpoint(config); got != "http://172.29.10.1:8787" {
|
||||
t.Fatalf("resolveDistributedTaskForwardEndpoint() = %q, want %q", got, "http://172.29.10.1:8787")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDistributedTaskForwardEndpointDisabledWhenPeerUnset(t *testing.T) {
|
||||
config := &BridgeConfig{}
|
||||
config.Distributed.Topology = "dual-node"
|
||||
config.Distributed.LocalNodeID = "xworkmate-bridge"
|
||||
config.Distributed.Nodes = []DistributedNodeConfig{
|
||||
{
|
||||
ID: "xworkmate-bridge",
|
||||
BridgeEndpoint: "http://172.29.10.1:8787",
|
||||
},
|
||||
{
|
||||
ID: "cn-xworkmate-bridge",
|
||||
BridgeEndpoint: "http://172.29.10.2:8787",
|
||||
},
|
||||
}
|
||||
|
||||
if got := resolveDistributedTaskForwardEndpoint(config); got != "" {
|
||||
t.Fatalf("resolveDistributedTaskForwardEndpoint() = %q, want empty endpoint", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDistributedTaskForwardEndpointKeepsExplicitEndpoint(t *testing.T) {
|
||||
config := &BridgeConfig{}
|
||||
config.Distributed.TaskForwardEndpoint = "https://xworkmate-bridge.svc.plus"
|
||||
config.Distributed.Topology = "dual-node"
|
||||
config.Distributed.LocalNodeID = "cn-xworkmate-bridge"
|
||||
config.Distributed.TaskForwardPeerID = "xworkmate-bridge"
|
||||
config.Distributed.Nodes = []DistributedNodeConfig{
|
||||
{
|
||||
ID: "xworkmate-bridge",
|
||||
BridgeEndpoint: "http://172.29.10.1:8787",
|
||||
},
|
||||
}
|
||||
|
||||
if got := resolveDistributedTaskForwardEndpoint(config); got != "https://xworkmate-bridge.svc.plus" {
|
||||
t.Fatalf("resolveDistributedTaskForwardEndpoint() = %q, want explicit endpoint", got)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user