1
0
Эх сурвалжийг харах

feat(frontend): stream tab skeleton with TCP + KCP (Pattern A)

Opens the stream tab on the sibling-file rewrite. Tab visibility is
driven by canEnableStream from lib/xray/protocol-capabilities — same
gate the legacy modal used, now schema-aware.

Transmission picker (network select) is hidden for HYSTERIA since
that protocol's network is implicit. onNetworkChange clears any stale
per-network settings keys (tcpSettings/kcpSettings/...) and seeds an
empty object for the new branch so AntD Form.Items don't read from
undefined nested paths.

TCP section: acceptProxyProtocol Switch (literal-true-optional on the
wire — the form stores true/false but Zod's strip behavior keeps
false-as-omission round-trips clean) plus an HTTP-camouflage toggle
that flips header.type between 'none' and 'http'. The full HTTP
camouflage request/response sub-form lands in a follow-up commit.

KCP section: six numeric knobs (mtu, tti, upCap, downCap,
cwndMultiplier, maxSendingWindow).

WS / gRPC / HTTPUpgrade / XHTTP / external-proxy / sockopt / hysteria
stream / FinalMaskForm hookup all still pending.
MHSanaei 22 цаг өмнө
parent
commit
985e647d6e

+ 109 - 1
frontend/src/pages/inbounds/InboundFormModal.new.tsx

@@ -24,7 +24,7 @@ import {
   formValuesToWirePayload,
 } from '@/lib/xray/inbound-form-adapter';
 import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
-import { isSS2022 } from '@/lib/xray/protocol-capabilities';
+import { canEnableStream, isSS2022 } from '@/lib/xray/protocol-capabilities';
 import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
 import {
   InboundFormBaseSchema,
@@ -105,6 +105,8 @@ export default function InboundFormModalNew({
     settings: typeof ssMethod === 'string' ? { method: ssMethod } : {},
   });
   const mixedUdpOn = Form.useWatch(['settings', 'udp'], form) ?? false;
+  const network = Form.useWatch(['streamSettings', 'network'], form) ?? '';
+  const streamEnabled = canEnableStream({ protocol });
   const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form);
   const wgPubKey = typeof wgSecretKey === 'string' && wgSecretKey.length > 0
     ? Wireguard.generateKeypair(wgSecretKey).publicKey
@@ -744,6 +746,109 @@ export default function InboundFormModalNew({
     </>
   );
 
+  // Switching `network` swaps which per-network key (tcpSettings, wsSettings,
+  // grpcSettings, ...) appears on the wire. We clear the previously selected
+  // network's settings blob and seed a default empty object for the new one
+  // so AntD's Form.Items aren't pointed at undefined nested paths.
+  const onNetworkChange = (next: string) => {
+    const ALL = ['tcpSettings', 'kcpSettings', 'wsSettings', 'grpcSettings', 'httpupgradeSettings', 'xhttpSettings'];
+    const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
+    const cleaned: Record<string, unknown> = { ...current, network: next };
+    for (const k of ALL) {
+      if (k !== `${next}Settings`) delete cleaned[k];
+    }
+    cleaned[`${next}Settings`] = {};
+    form.setFieldValue('streamSettings', cleaned);
+  };
+
+  const streamTab = (
+    <>
+      {protocol !== Protocols.HYSTERIA && (
+        <Form.Item label="Transmission">
+          <Select
+            value={network}
+            style={{ width: '75%' }}
+            onChange={onNetworkChange}
+          >
+            <Select.Option value="tcp">TCP (RAW)</Select.Option>
+            <Select.Option value="kcp">mKCP</Select.Option>
+            <Select.Option value="ws">WebSocket</Select.Option>
+            <Select.Option value="grpc">gRPC</Select.Option>
+            <Select.Option value="httpupgrade">HTTPUpgrade</Select.Option>
+            <Select.Option value="xhttp">XHTTP</Select.Option>
+          </Select>
+        </Form.Item>
+      )}
+
+      {network === 'tcp' && (
+        <>
+          <Form.Item
+            name={['streamSettings', 'tcpSettings', 'acceptProxyProtocol']}
+            label="Proxy Protocol"
+            valuePropName="checked"
+          >
+            <Switch />
+          </Form.Item>
+          <Form.Item label={`HTTP ${t('camouflage')}`}>
+            <Form.Item
+              noStyle
+              shouldUpdate={(prev, curr) =>
+                prev.streamSettings?.tcpSettings?.header?.type
+                !== curr.streamSettings?.tcpSettings?.header?.type
+              }
+            >
+              {({ getFieldValue, setFieldValue }) => {
+                const headerType = getFieldValue(
+                  ['streamSettings', 'tcpSettings', 'header', 'type'],
+                ) as string | undefined;
+                return (
+                  <Switch
+                    checked={headerType === 'http'}
+                    onChange={(v) => {
+                      setFieldValue(
+                        ['streamSettings', 'tcpSettings', 'header'],
+                        v ? { type: 'http' } : { type: 'none' },
+                      );
+                    }}
+                  />
+                );
+              }}
+            </Form.Item>
+          </Form.Item>
+        </>
+      )}
+
+      {network === 'kcp' && (
+        <>
+          <Form.Item name={['streamSettings', 'kcpSettings', 'mtu']} label="MTU">
+            <InputNumber min={576} max={1460} />
+          </Form.Item>
+          <Form.Item name={['streamSettings', 'kcpSettings', 'tti']} label="TTI (ms)">
+            <InputNumber min={10} max={100} />
+          </Form.Item>
+          <Form.Item name={['streamSettings', 'kcpSettings', 'upCap']} label="Uplink (MB/s)">
+            <InputNumber min={0} />
+          </Form.Item>
+          <Form.Item name={['streamSettings', 'kcpSettings', 'downCap']} label="Downlink (MB/s)">
+            <InputNumber min={0} />
+          </Form.Item>
+          <Form.Item
+            name={['streamSettings', 'kcpSettings', 'cwndMultiplier']}
+            label="CWND Multiplier"
+          >
+            <InputNumber min={1} />
+          </Form.Item>
+          <Form.Item
+            name={['streamSettings', 'kcpSettings', 'maxSendingWindow']}
+            label="Max Sending Window"
+          >
+            <InputNumber min={0} />
+          </Form.Item>
+        </>
+      )}
+    </>
+  );
+
   const sniffingTab = (
     <>
       <Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
@@ -839,6 +944,9 @@ export default function InboundFormModalNew({
             ] as string[]).includes(protocol)
               ? [{ key: 'protocol', label: t('pages.inbounds.protocol'), children: protocolTab }]
               : []),
+            ...(streamEnabled
+              ? [{ key: 'stream', label: t('pages.inbounds.streamTab'), children: streamTab }]
+              : []),
             { key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab },
           ]} />
         </Form>