mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-19 19:16:08 +00:00
docs: vendor the documentation site into the monorepo
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to docs.sanaei.dev) into docs/ so the panel and its documentation share a single source of truth, the way sing-box keeps its docs in-tree. The old repo becomes redundant and can be retired. - Import the full site under docs/ (app, components, content, lib, public, scripts, config). The self-contained pnpm project sits alongside the existing engineering notes with no filename collisions. - Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx). - Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to docs/** and run with working-directory: docs, since GitHub only runs workflows from the repo-root .github/. deploy-static.yml's GitHub Pages publish (CNAME docs.sanaei.dev) carries over unchanged. Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to this repository's Pages (or set the Vercel project's root directory to docs), confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
buildOutboundJson,
|
||||
type OutboundInput,
|
||||
type OutboundKind,
|
||||
type Network,
|
||||
type Security,
|
||||
type ProxyServerInput,
|
||||
type StreamInput,
|
||||
type WireguardInput,
|
||||
} from '@/lib/xray/outbounds';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
const KINDS: readonly OutboundKind[] = [
|
||||
'freedom',
|
||||
'blackhole',
|
||||
'vless',
|
||||
'vmess',
|
||||
'trojan',
|
||||
'shadowsocks',
|
||||
'socks',
|
||||
'http',
|
||||
'wireguard',
|
||||
'warp',
|
||||
];
|
||||
const NETWORKS: readonly Network[] = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp'];
|
||||
const SECURITIES: readonly Security[] = ['none', 'tls', 'reality'];
|
||||
const FINGERPRINTS = ['chrome', 'firefox', 'safari', 'ios', 'android', 'edge', 'random'];
|
||||
const DOMAIN_STRATEGIES = ['AsIs', 'UseIP', 'UseIPv4', 'UseIPv6', 'ForceIP'];
|
||||
|
||||
const PROXY_KINDS = new Set<OutboundKind>([
|
||||
'vless',
|
||||
'vmess',
|
||||
'trojan',
|
||||
'shadowsocks',
|
||||
'socks',
|
||||
'http',
|
||||
]);
|
||||
const STREAM_KINDS = new Set<OutboundKind>(['vless', 'vmess', 'trojan', 'shadowsocks']);
|
||||
const WG_KINDS = new Set<OutboundKind>(['wireguard', 'warp']);
|
||||
|
||||
export function OutboundGenerator() {
|
||||
const [kind, setKind] = useState<OutboundKind>('vless');
|
||||
const [tag, setTag] = useState('proxy');
|
||||
|
||||
// proxy server
|
||||
const [address, setAddress] = useState('example.com');
|
||||
const [port, setPort] = useState('443');
|
||||
const [id, setId] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [method, setMethod] = useState('2022-blake3-aes-128-gcm');
|
||||
const [flow, setFlow] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
|
||||
// stream
|
||||
const [network, setNetwork] = useState<Network>('tcp');
|
||||
const [security, setSecurity] = useState<Security>('reality');
|
||||
const [host, setHost] = useState('');
|
||||
const [path, setPath] = useState('/');
|
||||
const [serviceName, setServiceName] = useState('');
|
||||
const [sni, setSni] = useState('www.microsoft.com');
|
||||
const [fingerprint, setFingerprint] = useState('chrome');
|
||||
const [publicKey, setPublicKey] = useState('');
|
||||
const [shortId, setShortId] = useState('');
|
||||
|
||||
// freedom
|
||||
const [domainStrategy, setDomainStrategy] = useState('AsIs');
|
||||
|
||||
// wireguard
|
||||
const [wgSecretKey, setWgSecretKey] = useState('');
|
||||
const [wgAddress, setWgAddress] = useState('172.16.0.2/32');
|
||||
const [wgPublicKey, setWgPublicKey] = useState('');
|
||||
const [wgEndpoint, setWgEndpoint] = useState('');
|
||||
|
||||
const isProxy = PROXY_KINDS.has(kind);
|
||||
const hasStream = STREAM_KINDS.has(kind);
|
||||
const isWg = WG_KINDS.has(kind);
|
||||
const hasPath = network === 'ws' || network === 'httpupgrade' || network === 'xhttp';
|
||||
|
||||
const server: ProxyServerInput = {
|
||||
address,
|
||||
port: Number(port),
|
||||
id,
|
||||
password,
|
||||
method,
|
||||
flow,
|
||||
username,
|
||||
};
|
||||
|
||||
const stream: StreamInput = {
|
||||
network,
|
||||
security,
|
||||
host,
|
||||
path,
|
||||
serviceName,
|
||||
sni,
|
||||
fingerprint,
|
||||
publicKey,
|
||||
shortId,
|
||||
};
|
||||
|
||||
const wireguard: WireguardInput = {
|
||||
secretKey: wgSecretKey,
|
||||
address: wgAddress
|
||||
.split(',')
|
||||
.map((a) => a.trim())
|
||||
.filter(Boolean),
|
||||
publicKey: wgPublicKey,
|
||||
endpoint: wgEndpoint,
|
||||
};
|
||||
|
||||
const input: OutboundInput = {
|
||||
kind,
|
||||
tag,
|
||||
server: isProxy ? server : undefined,
|
||||
wireguard: isWg ? wireguard : undefined,
|
||||
stream: hasStream ? stream : undefined,
|
||||
domainStrategy: kind === 'freedom' ? domainStrategy : undefined,
|
||||
};
|
||||
|
||||
function reset() {
|
||||
setKind('vless');
|
||||
setTag('proxy');
|
||||
setAddress('example.com');
|
||||
setPort('443');
|
||||
setId('');
|
||||
setPassword('');
|
||||
setMethod('2022-blake3-aes-128-gcm');
|
||||
setFlow('');
|
||||
setUsername('');
|
||||
setNetwork('tcp');
|
||||
setSecurity('reality');
|
||||
setHost('');
|
||||
setPath('/');
|
||||
setServiceName('');
|
||||
setSni('www.microsoft.com');
|
||||
setFingerprint('chrome');
|
||||
setPublicKey('');
|
||||
setShortId('');
|
||||
setDomainStrategy('AsIs');
|
||||
setWgSecretKey('');
|
||||
setWgAddress('172.16.0.2/32');
|
||||
setWgPublicKey('');
|
||||
setWgEndpoint('');
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Outbound config generator"
|
||||
description="Build an Xray outbound object — freedom, blackhole, a proxy protocol, WireGuard, or WARP — to paste into your Xray configuration."
|
||||
onReset={reset}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Kind"
|
||||
value={kind}
|
||||
onChange={(v) => setKind(v as OutboundKind)}
|
||||
options={KINDS}
|
||||
/>
|
||||
<TextField label="Tag" value={kind === 'warp' ? 'warp' : tag} onChange={setTag} />
|
||||
|
||||
{kind === 'freedom' ? (
|
||||
<SelectField
|
||||
label="Domain strategy"
|
||||
value={domainStrategy}
|
||||
onChange={setDomainStrategy}
|
||||
options={DOMAIN_STRATEGIES}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isProxy ? (
|
||||
<>
|
||||
<TextField label="Address" value={address} onChange={setAddress} />
|
||||
<TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
|
||||
{(kind === 'vless' || kind === 'vmess') && (
|
||||
<TextField label="UUID (id)" value={id} onChange={setId} />
|
||||
)}
|
||||
{kind === 'vless' && (
|
||||
<TextField
|
||||
label="Flow"
|
||||
value={flow}
|
||||
onChange={setFlow}
|
||||
placeholder="xtls-rprx-vision (optional)"
|
||||
/>
|
||||
)}
|
||||
{(kind === 'trojan' || kind === 'shadowsocks') && (
|
||||
<TextField label="Password" value={password} onChange={setPassword} />
|
||||
)}
|
||||
{kind === 'shadowsocks' && (
|
||||
<TextField label="Method (cipher)" value={method} onChange={setMethod} />
|
||||
)}
|
||||
{(kind === 'socks' || kind === 'http') && (
|
||||
<>
|
||||
<TextField
|
||||
label="Username"
|
||||
value={username}
|
||||
onChange={setUsername}
|
||||
placeholder="optional"
|
||||
/>
|
||||
<TextField label="Password" value={password} onChange={setPassword} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{isWg ? (
|
||||
<>
|
||||
<TextField
|
||||
label="Private key (secretKey)"
|
||||
value={wgSecretKey}
|
||||
onChange={setWgSecretKey}
|
||||
/>
|
||||
<TextField label="Local address" value={wgAddress} onChange={setWgAddress} />
|
||||
<TextField label="Peer public key" value={wgPublicKey} onChange={setWgPublicKey} />
|
||||
<TextField
|
||||
label="Peer endpoint"
|
||||
value={wgEndpoint}
|
||||
onChange={setWgEndpoint}
|
||||
placeholder={kind === 'warp' ? 'engage.cloudflareclient.com:2408' : 'host:51820'}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasStream ? (
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Transport"
|
||||
value={network}
|
||||
onChange={(v) => setNetwork(v as Network)}
|
||||
options={NETWORKS}
|
||||
/>
|
||||
<SelectField
|
||||
label="Security"
|
||||
value={security}
|
||||
onChange={(v) => setSecurity(v as Security)}
|
||||
options={SECURITIES}
|
||||
/>
|
||||
{hasPath ? (
|
||||
<>
|
||||
<TextField label="Path" value={path} onChange={setPath} />
|
||||
<TextField label="Host" value={host} onChange={setHost} placeholder="optional" />
|
||||
</>
|
||||
) : null}
|
||||
{network === 'grpc' ? (
|
||||
<TextField label="serviceName" value={serviceName} onChange={setServiceName} />
|
||||
) : null}
|
||||
{security !== 'none' ? (
|
||||
<>
|
||||
<TextField label="SNI (serverName)" value={sni} onChange={setSni} />
|
||||
<SelectField
|
||||
label="Fingerprint"
|
||||
value={fingerprint}
|
||||
onChange={setFingerprint}
|
||||
options={FINGERPRINTS}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{security === 'reality' ? (
|
||||
<>
|
||||
<TextField label="Public key (pbk)" value={publicKey} onChange={setPublicKey} />
|
||||
<TextField label="Short ID (sid)" value={shortId} onChange={setShortId} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4">
|
||||
<OutputBlock label="Outbound (Xray JSON)" value={buildOutboundJson(input)} />
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user