subscription-builder.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. 'use client';
  2. import { useState } from 'react';
  3. import {
  4. buildSubscriptionUrls,
  5. buildShareLinks,
  6. buildBase64Subscription,
  7. buildJsonSubscription,
  8. type SubClient,
  9. type SubUrlInput,
  10. } from '@/lib/xray/subscription';
  11. import type { Network, Security } from '@/lib/xray/outbounds';
  12. import { ToolFrame } from './tool-frame';
  13. import { TextField, SelectField, CheckboxField } from './shared/fields';
  14. import { OutputBlock } from './shared/output-block';
  15. type ClientProtocol = 'vless' | 'vmess' | 'trojan' | 'ss';
  16. interface ClientRow {
  17. protocol: ClientProtocol;
  18. remark: string;
  19. address: string;
  20. port: string;
  21. credential: string; // id (vless/vmess) or password (trojan/ss)
  22. method: string; // ss
  23. network: Network;
  24. security: Security;
  25. sni: string;
  26. }
  27. const PROTOCOLS: readonly ClientProtocol[] = ['vless', 'vmess', 'trojan', 'ss'];
  28. const NETWORKS: readonly Network[] = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp'];
  29. const SECURITIES: readonly Security[] = ['none', 'tls', 'reality'];
  30. const addBtn =
  31. 'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground';
  32. const DEFAULT_CLIENTS: ClientRow[] = [
  33. {
  34. protocol: 'vless',
  35. remark: 'HK-01',
  36. address: 'a.example.com',
  37. port: '443',
  38. credential: '11111111-2222-3333-4444-555555555555',
  39. method: '',
  40. network: 'tcp',
  41. security: 'reality',
  42. sni: 'www.microsoft.com',
  43. },
  44. ];
  45. function toClient(r: ClientRow): SubClient {
  46. const isUuid = r.protocol === 'vless' || r.protocol === 'vmess';
  47. return {
  48. protocol: r.protocol,
  49. remark: r.remark,
  50. address: r.address,
  51. port: Number(r.port),
  52. id: isUuid ? r.credential : undefined,
  53. password: isUuid ? undefined : r.credential,
  54. method: r.protocol === 'ss' ? r.method : undefined,
  55. network: r.network,
  56. security: r.security,
  57. sni: r.sni || undefined,
  58. };
  59. }
  60. export function SubscriptionBuilder() {
  61. const [scheme, setScheme] = useState<'http' | 'https'>('https');
  62. const [host, setHost] = useState('sub.example.com');
  63. const [port, setPort] = useState('2096');
  64. const [subPath, setSubPath] = useState('/sub/');
  65. const [jsonPath, setJsonPath] = useState('/json/');
  66. const [subId, setSubId] = useState('user-1');
  67. const [behindProxy, setBehindProxy] = useState(false);
  68. const [clients, setClients] = useState<ClientRow[]>(DEFAULT_CLIENTS);
  69. function patch(i: number, p: Partial<ClientRow>) {
  70. setClients((prev) => prev.map((c, j) => (i === j ? { ...c, ...p } : c)));
  71. }
  72. const urlInput: SubUrlInput = {
  73. scheme,
  74. host,
  75. port: Number(port),
  76. subPath,
  77. jsonPath,
  78. subId,
  79. behindProxy,
  80. };
  81. const urls = buildSubscriptionUrls(urlInput);
  82. const subClients = clients.filter((c) => c.address.trim()).map(toClient);
  83. function reset() {
  84. setScheme('https');
  85. setHost('sub.example.com');
  86. setPort('2096');
  87. setSubPath('/sub/');
  88. setJsonPath('/json/');
  89. setSubId('user-1');
  90. setBehindProxy(false);
  91. setClients(DEFAULT_CLIENTS);
  92. }
  93. return (
  94. <ToolFrame
  95. title="Subscription & sub-JSON builder"
  96. description="Build the subscription URLs and preview both body formats — the Base64 link list and the JSON (Xray-json) config."
  97. onReset={reset}
  98. >
  99. <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
  100. <SelectField
  101. label="Scheme"
  102. value={scheme}
  103. onChange={(v) => setScheme(v as 'http' | 'https')}
  104. options={['https', 'http']}
  105. />
  106. <TextField label="Host" value={host} onChange={setHost} />
  107. <TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
  108. <TextField label="Sub ID" value={subId} onChange={setSubId} />
  109. <TextField label="Sub path" value={subPath} onChange={setSubPath} />
  110. <TextField label="JSON path" value={jsonPath} onChange={setJsonPath} />
  111. <CheckboxField
  112. label="Behind a reverse proxy (omit the port)"
  113. checked={behindProxy}
  114. onChange={setBehindProxy}
  115. />
  116. </div>
  117. <div className="mt-4 grid grid-cols-1 gap-4">
  118. <OutputBlock label="Base64 subscription URL" value={urls.base64} qr />
  119. <OutputBlock label="JSON subscription URL" value={urls.json} />
  120. </div>
  121. <div className="mt-5 flex items-center justify-between">
  122. <h4 className="text-sm font-semibold">Clients in this subscription</h4>
  123. <button
  124. type="button"
  125. className={addBtn}
  126. onClick={() =>
  127. setClients((p) => [
  128. ...p,
  129. {
  130. protocol: 'vless',
  131. remark: '',
  132. address: '',
  133. port: '443',
  134. credential: '',
  135. method: '',
  136. network: 'tcp',
  137. security: 'reality',
  138. sni: '',
  139. },
  140. ])
  141. }
  142. >
  143. Add client
  144. </button>
  145. </div>
  146. <div className="mt-2 flex flex-col gap-3">
  147. {clients.map((c, i) => (
  148. <div key={i} className="rounded-xl border p-3">
  149. <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
  150. <SelectField
  151. label="Protocol"
  152. value={c.protocol}
  153. onChange={(v) => patch(i, { protocol: v as ClientProtocol })}
  154. options={PROTOCOLS}
  155. />
  156. <TextField
  157. label="Remark"
  158. value={c.remark}
  159. onChange={(v) => patch(i, { remark: v })}
  160. />
  161. <TextField
  162. label="Address"
  163. value={c.address}
  164. onChange={(v) => patch(i, { address: v })}
  165. />
  166. <TextField
  167. label="Port"
  168. value={c.port}
  169. onChange={(v) => patch(i, { port: v })}
  170. inputMode="numeric"
  171. />
  172. <TextField
  173. label={c.protocol === 'vless' || c.protocol === 'vmess' ? 'UUID (id)' : 'Password'}
  174. value={c.credential}
  175. onChange={(v) => patch(i, { credential: v })}
  176. />
  177. {c.protocol === 'ss' ? (
  178. <TextField
  179. label="Method"
  180. value={c.method}
  181. onChange={(v) => patch(i, { method: v })}
  182. />
  183. ) : null}
  184. <SelectField
  185. label="Transport"
  186. value={c.network}
  187. onChange={(v) => patch(i, { network: v as Network })}
  188. options={NETWORKS}
  189. />
  190. <SelectField
  191. label="Security"
  192. value={c.security}
  193. onChange={(v) => patch(i, { security: v as Security })}
  194. options={SECURITIES}
  195. />
  196. {c.security !== 'none' ? (
  197. <TextField label="SNI" value={c.sni} onChange={(v) => patch(i, { sni: v })} />
  198. ) : null}
  199. </div>
  200. <div className="mt-2 flex justify-end">
  201. <button
  202. type="button"
  203. className={addBtn}
  204. onClick={() => setClients((p) => p.filter((_, j) => j !== i))}
  205. >
  206. Remove
  207. </button>
  208. </div>
  209. </div>
  210. ))}
  211. </div>
  212. <div className="mt-4 grid grid-cols-1 gap-4">
  213. <OutputBlock
  214. label="Subscription links (decoded body)"
  215. value={buildShareLinks(subClients).join('\n')}
  216. />
  217. <OutputBlock label="Base64 body" value={buildBase64Subscription(subClients)} />
  218. <OutputBlock
  219. label="JSON subscription (preview)"
  220. value={buildJsonSubscription(subClients)}
  221. />
  222. </div>
  223. </ToolFrame>
  224. );
  225. }