diff --git a/docs/api-reference.md b/docs/api-reference.md index eb317a0..5d341c4 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -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 之间循环 diff --git a/example/config.yaml b/example/config.yaml index b0d5b2f..c0a82e7 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -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: "" diff --git a/internal/acp/config.go b/internal/acp/config.go index 39054ff..e6d3111 100644 --- a/internal/acp/config.go +++ b/internal/acp/config.go @@ -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()) } diff --git a/internal/acp/distributed_config_test.go b/internal/acp/distributed_config_test.go new file mode 100644 index 0000000..a4033e6 --- /dev/null +++ b/internal/acp/distributed_config_test.go @@ -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) + } +}