- 第22条 L-UI:登录页品牌名、侧栏 brand-text(收起态 L)、抽屉品牌、 usePageTitle 默认标题(HTML 标题)、13 个语言文件里用户可见串中的 3X-UI 字样(更新说明×2、TG 版本行)一并改为 L-UI;仓库/二进制/ 服务名/路径/内部包名不动 - 第18条:formatPanelVersion 对 V/A 系显示版本原样返回(新增单测); PanelUpdateModal 与首页动作条改用 display* 字段判更新与显示 - 第14条:首页运行时间卡片删除 Xray 半边与分隔线,只留 OS - 第21条:节点表列宽 操作190→150/已启用80→64/客户端180→150/ 延迟100→84/心跳120→96、地址列 330;agent 行 IPv4·IPv6 单行 (浅点分隔、沿用可见性 class),传统行不变 - 第20条:agent 行云朵按钮与移动端菜单改用「更新 Agent」文案; NodesPage 对 agent 行用无渠道选择的简单确认(传统行不变); schemas 增 agentUpdateState/Error/Target - 第19条:节点编辑页(agent)拉 /panel/api/agent/install/:id 展示 完整安装命令(master 可编辑、含 token 时完整命令+复制按钮); 无 token 时展示无 token 命令+agent.json 路径提示,不报错不空白 - i18n 新增 4 键(updateAgent/updateAgentConfirmContent/ agentInstallCommand/agentTokenNotStored)补齐全部 13 个语言文件 (zh-CN 中文、其余英文),tsc 干净、panel-version 测试全过、 i18n 失败仍为既有 forwarding 键集问题(第10条)零新增
136 lines
5.5 KiB
TypeScript
136 lines
5.5 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
export const NodeRecordSchema = z
|
|
.object({
|
|
id: z.number(),
|
|
name: z.string().optional(),
|
|
remark: z.string().optional(),
|
|
scheme: z.string().optional(),
|
|
address: z.string().optional(),
|
|
port: z.number().optional(),
|
|
basePath: z.string().optional(),
|
|
apiToken: z.string().optional(),
|
|
hasApiToken: z.boolean().optional(),
|
|
enable: z.boolean().optional(),
|
|
status: z.string().optional(),
|
|
latencyMs: z.number().optional(),
|
|
cpuPct: z.number().optional(),
|
|
memPct: z.number().optional(),
|
|
xrayVersion: z.string().optional(),
|
|
panelVersion: z.string().optional(),
|
|
uptimeSecs: z.number().optional(),
|
|
inboundCount: z.number().optional(),
|
|
clientCount: z.number().optional(),
|
|
onlineCount: z.number().optional(),
|
|
activeCount: z.number().optional(),
|
|
disabledCount: z.number().optional(),
|
|
depletedCount: z.number().optional(),
|
|
lastHeartbeat: z.number().optional(),
|
|
lastError: z.string().optional(),
|
|
// Xray state captured from the remote node's own /panel/api/server/status.
|
|
// Lets the nodes list show a distinct indicator when the panel API is reachable
|
|
// (status=online) but the Xray core on that node has failed.
|
|
xrayState: z.string().optional(),
|
|
xrayError: z.string().optional(),
|
|
allowPrivateAddress: z.boolean().optional(),
|
|
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']).optional(),
|
|
pinnedCertSha256: z.string().optional(),
|
|
inboundSyncMode: z.enum(['all', 'selected']).optional(),
|
|
// Backend serializes a nil []string as null for nodes saved before #5178.
|
|
inboundTags: z.array(z.string()).nullish(),
|
|
outboundTag: z.string().optional(),
|
|
// Multi-hop node tree (#4983): a node's stable GUID, its parent's GUID, and
|
|
// whether it's a read-only transitive sub-node surfaced from a downstream node.
|
|
guid: z.string().optional(),
|
|
parentGuid: z.string().optional(),
|
|
transitive: z.boolean().optional(),
|
|
// P7: node kind, annotated by the backend for nodes registered by an
|
|
// agent. Absent/traditional for classic panel-to-panel nodes.
|
|
nodeType: z.enum(['traditional', 'agent']).optional(),
|
|
// P13: host IPv4/IPv6 an agent reports with its heartbeat telemetry.
|
|
// Rendered as plain text in the address column for agent rows.
|
|
ipv4: z.string().optional(),
|
|
ipv6: z.string().optional(),
|
|
// Item 20: last one-click agent update outcome, agent rows only.
|
|
agentUpdateState: z.string().optional(),
|
|
agentUpdateError: z.string().optional(),
|
|
agentUpdateTarget: z.string().optional(),
|
|
})
|
|
.loose();
|
|
|
|
export const NodeListSchema = z.array(NodeRecordSchema);
|
|
|
|
export const ProbeResultSchema = z
|
|
.object({
|
|
status: z.string(),
|
|
latencyMs: z.number().optional(),
|
|
xrayVersion: z.string().optional(),
|
|
error: z.string().optional(),
|
|
// Present on successful probe; used to surface "connected to panel, but xray failed on node".
|
|
xrayState: z.string().optional(),
|
|
xrayError: z.string().optional(),
|
|
})
|
|
.loose();
|
|
|
|
export const NodeFormSchema = z
|
|
.object({
|
|
id: z.number().optional(),
|
|
name: z.string().trim().min(1, 'pages.nodes.toasts.fillRequired'),
|
|
remark: z.string().optional(),
|
|
// Node type: traditional panel-to-panel, or lightweight agent.
|
|
// Agent nodes skip the panel probe and API token; an agent token is
|
|
// issued on creation instead.
|
|
nodeType: z.enum(['traditional', 'agent']).optional().default('traditional'),
|
|
scheme: z.enum(['http', 'https']),
|
|
// For agent nodes the panel never dials the node (agent dials out),
|
|
// so address/port are not needed. Required for traditional nodes.
|
|
address: z.string().trim().optional().default(''),
|
|
port: z.number().int().min(1).max(65535).optional(),
|
|
basePath: z.string(),
|
|
// mTLS nodes authenticate via the client certificate, so the token is optional
|
|
// there; every other verify mode still requires one (matches remote.do()).
|
|
// Agent nodes use an agent token instead of a panel API token.
|
|
apiToken: z.string().trim(),
|
|
hasStoredToken: z.boolean().optional().default(false),
|
|
enable: z.boolean(),
|
|
allowPrivateAddress: z.boolean(),
|
|
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']),
|
|
pinnedCertSha256: z.string().optional().default(''),
|
|
inboundSyncMode: z.enum(['all', 'selected']).optional().default('all'),
|
|
// Unmounted when sync mode is "all" (absent from antd onFinish values) and
|
|
// serialized as null by the backend for a nil slice — tolerate both.
|
|
inboundTags: z
|
|
.array(z.string())
|
|
.nullish()
|
|
.transform((tags) => tags ?? []),
|
|
outboundTag: z.string().optional(),
|
|
})
|
|
.superRefine((val, ctx) => {
|
|
if (val.nodeType === 'agent') return; // no panel credentials needed
|
|
if (!val.address || val.address.trim().length === 0) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['address'],
|
|
message: 'pages.nodes.toasts.fillRequired',
|
|
});
|
|
}
|
|
if (val.port === undefined) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['port'],
|
|
message: 'pages.nodes.toasts.fillRequired',
|
|
});
|
|
}
|
|
if (val.tlsVerifyMode !== 'mtls' && val.apiToken.length === 0 && !val.hasStoredToken) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['apiToken'],
|
|
message: 'pages.nodes.toasts.fillRequired',
|
|
});
|
|
}
|
|
});
|
|
|
|
export type NodeRecord = z.infer<typeof NodeRecordSchema>;
|
|
export type ProbeResult = z.infer<typeof ProbeResultSchema>;
|
|
export type NodeFormValues = z.infer<typeof NodeFormSchema>;
|