From 7284fa6cf0540562de9c5e736c8954d0cf5992bd Mon Sep 17 00:00:00 2001 From: Muse Date: Wed, 21 Oct 2026 13:49:57 +0000 Subject: [PATCH] P8(2): inbound node reassignment (move) with backend re-homing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An inbound's 部署到 node can now be changed after creation; the row, clients, client_traffics and subscription output stay untouched (same inbound id throughout). Contract: the update payload's nodeId counts only when the key was explicitly sent — the controller probes the raw JSON body and sets Inbound.NodeIDProvided; the wire adapter now always emits nodeId (null = local) so clearing the picker is an explicit move-to-local, while callers that omit nodeId keep the stored assignment (legacy behaviour). UpdateInbound resolves the target before the port-conflict check (so conflicts are scoped to the target node), rejects moves to a missing node, re-derives auto tags under the target node's prefix, validates the tag against the target node's namespace, and marks BOTH nodes dirty: the source node's next reconcile sweeps the old tag (no silent ghost — the flag persists until a reconcile succeeds, so an offline side just defers), the target's pushes the inbound. Agent-bound nodes converge via GetXrayConfigForAgent on the agent's next config poll. A local side has no reconcile loop, so it is applied directly post-commit (Del/Add on the local runtime, restart fallback). Frontend: the deploy-to Select is no longer disabled in edit mode (protocol stays locked); candidate rules unchanged (online nodes only). Tests: panel->panel move with reconcile sweep/push assertions, auto-tag prefix regeneration, agent-config flip in both directions, omitted-nodeId legacy contract, same-node no-op, missing-node rejection, node->local move. --- frontend/src/lib/xray/inbound-form-adapter.ts | 7 +- .../pages/inbounds/form/InboundFormModal.tsx | 1 - .../src/test/inbound-form-adapter.test.ts | 5 +- internal/database/model/model.go | 7 + internal/web/controller/inbound.go | 34 +- internal/web/service/inbound.go | 99 +++- .../web/service/inbound_node_move_test.go | 432 ++++++++++++++++++ 7 files changed, 572 insertions(+), 13 deletions(-) create mode 100644 internal/web/service/inbound_node_move_test.go diff --git a/frontend/src/lib/xray/inbound-form-adapter.ts b/frontend/src/lib/xray/inbound-form-adapter.ts index c6b5ad5..ec8b107 100644 --- a/frontend/src/lib/xray/inbound-form-adapter.ts +++ b/frontend/src/lib/xray/inbound-form-adapter.ts @@ -77,7 +77,7 @@ export interface WireInboundPayload { sniffing: string; tag: string; clientStats?: unknown; - nodeId?: number; + nodeId?: number | null; shareAddrStrategy: ShareAddrStrategy; shareAddr: string; subSortIndex: number; @@ -384,6 +384,9 @@ export function formValuesToWirePayload(values: InboundFormValues): WireInboundP subSortIndex: values.subSortIndex, disableFlow: values.disableFlow, }; - if (values.nodeId != null) payload.nodeId = values.nodeId; + // nodeId is always emitted (null = local panel): the update endpoint + // distinguishes an explicit null — "move the inbound back to local" — + // from an omitted key, which keeps the stored assignment untouched. + payload.nodeId = values.nodeId ?? null; return payload; } diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx index 65664e3..1f4a50f 100644 --- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/form/InboundFormModal.tsx @@ -618,7 +618,6 @@ export default function InboundFormModal({