node.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import { z } from 'zod';
  2. export const NodeRecordSchema = z
  3. .object({
  4. id: z.number(),
  5. name: z.string().optional(),
  6. remark: z.string().optional(),
  7. scheme: z.string().optional(),
  8. address: z.string().optional(),
  9. port: z.number().optional(),
  10. basePath: z.string().optional(),
  11. apiToken: z.string().optional(),
  12. hasApiToken: z.boolean().optional(),
  13. enable: z.boolean().optional(),
  14. status: z.string().optional(),
  15. latencyMs: z.number().optional(),
  16. cpuPct: z.number().optional(),
  17. memPct: z.number().optional(),
  18. xrayVersion: z.string().optional(),
  19. panelVersion: z.string().optional(),
  20. uptimeSecs: z.number().optional(),
  21. inboundCount: z.number().optional(),
  22. clientCount: z.number().optional(),
  23. onlineCount: z.number().optional(),
  24. activeCount: z.number().optional(),
  25. disabledCount: z.number().optional(),
  26. depletedCount: z.number().optional(),
  27. lastHeartbeat: z.number().optional(),
  28. lastError: z.string().optional(),
  29. // Xray state captured from the remote node's own /panel/api/server/status.
  30. // Lets the nodes list show a distinct indicator when the panel API is reachable
  31. // (status=online) but the Xray core on that node has failed.
  32. xrayState: z.string().optional(),
  33. xrayError: z.string().optional(),
  34. allowPrivateAddress: z.boolean().optional(),
  35. tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']).optional(),
  36. pinnedCertSha256: z.string().optional(),
  37. inboundSyncMode: z.enum(['all', 'selected']).optional(),
  38. // Backend serializes a nil []string as null for nodes saved before #5178.
  39. inboundTags: z.array(z.string()).nullish(),
  40. outboundTag: z.string().optional(),
  41. // Multi-hop node tree (#4983): a node's stable GUID, its parent's GUID, and
  42. // whether it's a read-only transitive sub-node surfaced from a downstream node.
  43. guid: z.string().optional(),
  44. parentGuid: z.string().optional(),
  45. transitive: z.boolean().optional(),
  46. })
  47. .loose();
  48. export const NodeListSchema = z.array(NodeRecordSchema);
  49. export const ProbeResultSchema = z
  50. .object({
  51. status: z.string(),
  52. latencyMs: z.number().optional(),
  53. xrayVersion: z.string().optional(),
  54. error: z.string().optional(),
  55. // Present on successful probe; used to surface "connected to panel, but xray failed on node".
  56. xrayState: z.string().optional(),
  57. xrayError: z.string().optional(),
  58. })
  59. .loose();
  60. export const NodeFormSchema = z
  61. .object({
  62. id: z.number().optional(),
  63. name: z.string().trim().min(1, 'pages.nodes.toasts.fillRequired'),
  64. remark: z.string().optional(),
  65. scheme: z.enum(['http', 'https']),
  66. address: z.string().trim().min(1, 'pages.nodes.toasts.fillRequired'),
  67. port: z.number().int().min(1).max(65535),
  68. basePath: z.string(),
  69. // mTLS nodes authenticate via the client certificate, so the token is optional
  70. // there; every other verify mode still requires one (matches remote.do()).
  71. apiToken: z.string().trim(),
  72. hasStoredToken: z.boolean().optional().default(false),
  73. enable: z.boolean(),
  74. allowPrivateAddress: z.boolean(),
  75. tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']),
  76. pinnedCertSha256: z.string().optional().default(''),
  77. inboundSyncMode: z.enum(['all', 'selected']).optional().default('all'),
  78. // Unmounted when sync mode is "all" (absent from antd onFinish values) and
  79. // serialized as null by the backend for a nil slice — tolerate both.
  80. inboundTags: z
  81. .array(z.string())
  82. .nullish()
  83. .transform((tags) => tags ?? []),
  84. outboundTag: z.string().optional(),
  85. })
  86. .superRefine((val, ctx) => {
  87. if (val.tlsVerifyMode !== 'mtls' && val.apiToken.length === 0 && !val.hasStoredToken) {
  88. ctx.addIssue({
  89. code: 'custom',
  90. path: ['apiToken'],
  91. message: 'pages.nodes.toasts.fillRequired',
  92. });
  93. }
  94. });
  95. export type NodeRecord = z.infer<typeof NodeRecordSchema>;
  96. export type ProbeResult = z.infer<typeof ProbeResultSchema>;
  97. export type NodeFormValues = z.infer<typeof NodeFormSchema>;