feat(nodes): add distinct purple indicator when panel is online but Xray core failed (#5040)

* feat(nodes): add distinct purple indicator when panel is online but Xray core failed

Currently nodes only show binary online/offline based on panel API reachability.

This adds a third state:
- Green: panel reachable + Xray healthy
- Purple pulsing dot + "Online (Xray Error)": panel API works (management actions still available) but the node Xray process is in error or stopped. Tooltip shows the remote xrayError.
- Red: unreachable (unchanged)

Backend now captures xray.state + xray.errorMsg from /panel/api/server/status heartbeats and probes.
New fields on Node + NodeSummary, forwarded for transitive nodes.
Frontend Zod + NodeList rendering + dedicated .xray-error-dot CSS (color #722ED1) + i18n key.

Color chosen purple per feedback after initial implementation.

Refs: worktree xray-failed-in-nodes

* fix: remove invalid JSON comment causing CI failures

* chore: regenerate OpenAPI schemas and types for xray error indicators

* chore: regenerate examples and schemas for xray error indicators

* chore: regenerate missing openapi.json examples

* fix

---------

Co-authored-by: Rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Rouzbeh†
2026-06-08 20:24:00 +02:00
committed by GitHub
parent 0daedd3db9
commit 1c74b995c3
25 changed files with 239 additions and 49 deletions
+12 -6
View File
@@ -478,6 +478,12 @@ type Node struct {
UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
LastError string `json:"lastError"`
// XrayState and XrayError are captured from the remote node's /panel/api/server/status
// during heartbeats. They let the central panel distinguish "panel API reachable"
// (status=online) from "Xray core itself has failed on the node" for monitoring.
XrayState string `json:"xrayState" gorm:"column:xray_state"`
XrayError string `json:"xrayError" gorm:"column:xray_error"`
ConfigDirty bool `json:"configDirty" gorm:"default:false"`
ConfigDirtyAt int64 `json:"configDirtyAt"`
@@ -514,6 +520,9 @@ type NodeSummary struct {
LatencyMs int `json:"latencyMs"`
PanelVersion string `json:"panelVersion"`
XrayVersion string `json:"xrayVersion"`
// XrayState/XrayError forwarded so masters can surface xray failure on transitive sub-nodes too.
XrayState string `json:"xrayState"`
XrayError string `json:"xrayError,omitempty"`
}
type CustomGeoResource struct {
@@ -713,18 +722,15 @@ type OutboundSubscription struct {
AllowPrivate bool `json:"allowPrivate" form:"allowPrivate" gorm:"default:false"`
TagPrefix string `json:"tagPrefix" form:"tagPrefix"`
UpdateInterval int `json:"updateInterval" form:"updateInterval" gorm:"default:600"` // seconds between refreshes
Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
Prepend bool `json:"prepend" form:"prepend" gorm:"default:false"` // place this subscription's outbounds before the manual template outbounds
Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
Prepend bool `json:"prepend" form:"prepend" gorm:"default:false"` // place this subscription's outbounds before the manual template outbounds
LastUpdated int64 `json:"lastUpdated" form:"lastUpdated"`
LastError string `json:"lastError" form:"lastError"`
LastFetchedOutbounds string `json:"lastFetchedOutbounds" form:"lastFetchedOutbounds" gorm:"type:text"`
LinkIdentities string `json:"-" gorm:"type:text;column:link_identities"`
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
// OutboundCount is a derived count of the last fetched outbounds (not
// persisted); List populates it so the UI can show how many outbounds a
// subscription produced without shipping the full payload.
OutboundCount int `json:"outboundCount" gorm:"-"`
OutboundCount int `json:"outboundCount" gorm:"-"`
}
func MergeClientRecord(existing *ClientRecord, incoming *ClientRecord) []ClientMergeConflict {
+22
View File
@@ -1656,6 +1656,13 @@
"example": 86400,
"type": "integer"
},
"xrayError": {
"type": "string"
},
"xrayState": {
"description": "XrayState and XrayError are captured from the remote node's /panel/api/server/status\nduring heartbeats. They let the central panel distinguish \"panel API reachable\"\n(status=online) from \"Xray core itself has failed on the node\" for monitoring.",
"type": "string"
},
"xrayVersion": {
"example": "25.10.31",
"type": "string"
@@ -1691,6 +1698,8 @@
"tlsVerifyMode",
"updatedAt",
"uptimeSecs",
"xrayError",
"xrayState",
"xrayVersion"
],
"type": "object"
@@ -1752,6 +1761,13 @@
"example": 86400,
"type": "integer"
},
"xrayError": {
"type": "string"
},
"xrayState": {
"description": "XrayState/XrayError are populated on successful probes even when the node's\nXray core is not healthy. The UI uses them for a distinct \"panel ok, xray failed\" indicator.",
"type": "string"
},
"xrayVersion": {
"example": "25.10.31",
"type": "string"
@@ -1765,6 +1781,8 @@
"panelVersion",
"status",
"uptimeSecs",
"xrayError",
"xrayState",
"xrayVersion"
],
"type": "object"
@@ -5894,6 +5912,8 @@
"transitive": false,
"updatedAt": 1700000000,
"uptimeSecs": 86400,
"xrayError": "",
"xrayState": "",
"xrayVersion": "25.10.31"
}
]
@@ -6254,6 +6274,8 @@
"panelVersion": "v3.x.x",
"status": "online",
"uptimeSecs": 86400,
"xrayError": "",
"xrayState": "",
"xrayVersion": "25.10.31"
}
}
+4
View File
@@ -364,6 +364,8 @@ export const EXAMPLES: Record<string, unknown> = {
"transitive": false,
"updatedAt": 1700000000,
"uptimeSecs": 86400,
"xrayError": "",
"xrayState": "",
"xrayVersion": "25.10.31"
},
"OutboundTraffics": {
@@ -381,6 +383,8 @@ export const EXAMPLES: Record<string, unknown> = {
"panelVersion": "v3.x.x",
"status": "online",
"uptimeSecs": 86400,
"xrayError": "",
"xrayState": "",
"xrayVersion": "25.10.31"
},
"Setting": {
+18
View File
@@ -1630,6 +1630,13 @@ export const SCHEMAS: Record<string, unknown> = {
"example": 86400,
"type": "integer"
},
"xrayError": {
"type": "string"
},
"xrayState": {
"description": "XrayState and XrayError are captured from the remote node's /panel/api/server/status\nduring heartbeats. They let the central panel distinguish \"panel API reachable\"\n(status=online) from \"Xray core itself has failed on the node\" for monitoring.",
"type": "string"
},
"xrayVersion": {
"example": "25.10.31",
"type": "string"
@@ -1665,6 +1672,8 @@ export const SCHEMAS: Record<string, unknown> = {
"tlsVerifyMode",
"updatedAt",
"uptimeSecs",
"xrayError",
"xrayState",
"xrayVersion"
],
"type": "object"
@@ -1726,6 +1735,13 @@ export const SCHEMAS: Record<string, unknown> = {
"example": 86400,
"type": "integer"
},
"xrayError": {
"type": "string"
},
"xrayState": {
"description": "XrayState/XrayError are populated on successful probes even when the node's\nXray core is not healthy. The UI uses them for a distinct \"panel ok, xray failed\" indicator.",
"type": "string"
},
"xrayVersion": {
"example": "25.10.31",
"type": "string"
@@ -1739,6 +1755,8 @@ export const SCHEMAS: Record<string, unknown> = {
"panelVersion",
"status",
"uptimeSecs",
"xrayError",
"xrayState",
"xrayVersion"
],
"type": "object"
+4
View File
@@ -371,6 +371,8 @@ export interface Node {
transitive?: boolean;
updatedAt: number;
uptimeSecs: number;
xrayError: string;
xrayState: string;
xrayVersion: string;
}
@@ -390,6 +392,8 @@ export interface ProbeResultUI {
panelVersion: string;
status: string;
uptimeSecs: number;
xrayError: string;
xrayState: string;
xrayVersion: string;
}
+4
View File
@@ -398,6 +398,8 @@ export const NodeSchema = z.object({
transitive: z.boolean().optional(),
updatedAt: z.number().int(),
uptimeSecs: z.number().int(),
xrayError: z.string(),
xrayState: z.string(),
xrayVersion: z.string(),
});
export type Node = z.infer<typeof NodeSchema>;
@@ -419,6 +421,8 @@ export const ProbeResultUISchema = z.object({
panelVersion: z.string(),
status: z.string(),
uptimeSecs: z.number().int(),
xrayError: z.string(),
xrayState: z.string(),
xrayVersion: z.string(),
});
export type ProbeResultUI = z.infer<typeof ProbeResultUISchema>;
+79 -28
View File
@@ -68,18 +68,63 @@ function badgeStatus(status?: string): BadgeProps['status'] {
}
}
function StatusDot({ status }: { status?: string }) {
if (status === 'online') return <span className="online-dot" />;
interface HealthProps {
status?: string;
xrayState?: string;
xrayError?: string;
}
// Purple: the node's panel API is reachable (status=online) but its Xray core
// has failed or been stopped. Distinct from a normal offline/unknown node.
const XRAY_ERROR_COLOR = '#722ED1';
// True when the panel is online but Xray itself reports error/stop.
function hasXrayProblem(status?: string, xrayState?: string): boolean {
if (status !== 'online') return false;
const xs = (xrayState || '').toLowerCase().trim();
return xs === 'error' || xs === 'stop';
}
// Tooltip text + icon color for the status cell. A real probe error (lastError)
// is a warning and takes precedence; otherwise an Xray-core problem shows purple.
function statusIssue(record: Pick<NodeRecord, 'status' | 'xrayState' | 'xrayError' | 'lastError'>) {
const tip = record.lastError || (hasXrayProblem(record.status, record.xrayState) ? record.xrayError : '') || '';
const iconColor = !record.lastError && hasXrayProblem(record.status, record.xrayState)
? XRAY_ERROR_COLOR
: 'var(--ant-color-warning)';
return { tip, iconColor };
}
function StatusDot({ status, xrayState }: HealthProps) {
if (status === 'online') {
return hasXrayProblem(status, xrayState)
? <span className="xray-error-dot" />
: <span className="online-dot" />;
}
return <Badge status={badgeStatus(status)} />;
}
function StatusLabel({ status }: { status?: string }) {
function StatusLabel({ status, xrayState }: HealthProps) {
const { t } = useTranslation();
return (
<span style={status === 'online' ? { color: 'var(--ant-color-success)' } : undefined}>
{t(`pages.nodes.statusValues.${status || 'unknown'}`)}
</span>
);
if (status === 'online') {
const xs = (xrayState || '').toLowerCase().trim();
if (xs === 'error' || xs === 'stop') {
const detail = xs === 'error'
? t('pages.nodes.statusValues.xrayError')
: t('pages.nodes.statusValues.xrayStopped');
return (
<span style={{ color: XRAY_ERROR_COLOR }}>
{t('pages.nodes.statusValues.online')} ({detail})
</span>
);
}
return (
<span style={{ color: 'var(--ant-color-success)' }}>
{t('pages.nodes.statusValues.online')}
</span>
);
}
return <span>{t(`pages.nodes.statusValues.${status || 'unknown'}`)}</span>;
}
function formatPct(p?: number): string {
@@ -271,17 +316,20 @@ export default function NodeList({
title: t('pages.nodes.status'),
dataIndex: 'status',
align: 'center',
render: (_value, record) => (
<Space size={4}>
<StatusDot status={record.status} />
<StatusLabel status={record.status} />
{record.lastError && (
<Tooltip title={record.lastError}>
<ExclamationCircleOutlined style={{ color: 'var(--ant-color-warning)' }} />
</Tooltip>
)}
</Space>
),
render: (_value, record) => {
const { tip, iconColor } = statusIssue(record);
return (
<Space size={4}>
<StatusDot status={record.status} xrayState={record.xrayState} />
<StatusLabel status={record.status} xrayState={record.xrayState} />
{tip && (
<Tooltip title={tip}>
<ExclamationCircleOutlined style={{ color: iconColor }} />
</Tooltip>
)}
</Space>
);
},
},
{
title: t('pages.nodes.cpu'),
@@ -389,7 +437,7 @@ export default function NodeList({
<div key={String(record.key)} className="node-card" style={{ paddingInlineStart: 16, opacity: 0.85 }}>
<div className="card-head">
<ApartmentOutlined style={{ opacity: 0.6 }} />
<StatusDot status={record.status} />
<StatusDot status={record.status} xrayState={record.xrayState} />
<span className="node-name">{record.name}</span>
<div className="card-actions">
<Tag icon={<ApartmentOutlined />} style={{ margin: 0 }}>{t('pages.nodes.subNode')}</Tag>
@@ -400,7 +448,7 @@ export default function NodeList({
<div key={record.id} className="node-card">
<div className="card-head" onClick={() => toggleExpanded(record.id)}>
<RightOutlined className={`card-expand${expandedIds.has(record.id) ? ' is-expanded' : ''}`} />
<StatusDot status={record.status} />
<StatusDot status={record.status} xrayState={record.xrayState} />
<span className="node-name">{record.name}</span>
<div className="card-actions" onClick={(e) => e.stopPropagation()}>
<Tooltip title={t('info')}>
@@ -494,13 +542,16 @@ export default function NodeList({
</div>
<div className="stat-row">
<span className="stat-label">{t('pages.nodes.status')}</span>
<StatusDot status={statsNode.status} />
<StatusLabel status={statsNode.status} />
{statsNode.lastError && (
<Tooltip title={statsNode.lastError}>
<ExclamationCircleOutlined style={{ color: 'var(--ant-color-warning)' }} />
</Tooltip>
)}
<StatusDot status={statsNode.status} xrayState={statsNode.xrayState} />
<StatusLabel status={statsNode.status} xrayState={statsNode.xrayState} />
{(() => {
const { tip, iconColor } = statusIssue(statsNode);
return tip ? (
<Tooltip title={tip}>
<ExclamationCircleOutlined style={{ color: iconColor }} />
</Tooltip>
) : null;
})()}
</div>
<div className="stat-row">
<span className="stat-label">{t('pages.nodes.cpu')}</span>
+4 -1
View File
@@ -83,12 +83,15 @@ export default function NodesPage() {
const msg = await probe(node.id);
if (msg?.success && msg.obj) {
if (msg.obj.status === 'online') {
// Even if xray is in error/stop on the node we still reached its panel API.
messageApi.success(t('pages.nodes.connectionOk', { ms: msg.obj.latencyMs }));
} else {
messageApi.error(msg.obj.error || t('pages.nodes.toasts.probeFailed'));
}
}
}, [probe, t, messageApi]);
// Refresh the list so the new xrayState / xrayError (if any) appears immediately in the row.
refetch();
}, [probe, t, messageApi, refetch]);
const onToggleEnable = useCallback(async (node: NodeRecord, next: boolean) => {
await setEnable(node.id, next);
+8
View File
@@ -23,6 +23,11 @@ export const NodeRecordSchema = z.object({
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']).optional(),
pinnedCertSha256: z.string().optional(),
@@ -40,6 +45,9 @@ export const ProbeResultSchema = z.object({
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({
+23
View File
@@ -41,3 +41,26 @@
@media (prefers-reduced-motion: reduce) {
.online-dot { animation: none; }
}
/* Purple indicator for nodes that are reachable via the panel API (status=online)
but have Xray core in "error" or "stop" state. This is the new "xray failed on node"
monitoring state. */
.xray-error-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-inline-end: 5px;
vertical-align: middle;
background: #722ED1;
animation: xray-error-blink 1.1s ease-in-out infinite;
}
@keyframes xray-error-blink {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(114, 46, 209, 0.55); }
50% { opacity: 0.35; box-shadow: 0 0 0 4px rgba(114, 46, 209, 0); }
}
@media (prefers-reduced-motion: reduce) {
.xray-error-dot { animation: none; }
}
+18 -1
View File
@@ -35,6 +35,11 @@ type HeartbeatPatch struct {
MemPct float64
UptimeSecs uint64
LastError string
// XrayState and XrayError come from the remote /panel/api/server/status when the
// panel API is reachable. They allow distinguishing panel connectivity from
// Xray core health on the node.
XrayState string
XrayError string
}
type NodeService struct{}
@@ -474,6 +479,8 @@ func (s *NodeService) UpdateHeartbeat(id int, p HeartbeatPatch) error {
"mem_pct": p.MemPct,
"uptime_secs": p.UptimeSecs,
"last_error": p.LastError,
"xray_state": p.XrayState,
"xray_error": p.XrayError,
}
// Only learn the GUID; never clear a known one if an old-build node (or a
// failed probe) reports none, so the stable identity survives blips.
@@ -607,7 +614,9 @@ func (s *NodeService) Probe(ctx context.Context, n *model.Node) (HeartbeatPatch,
Total uint64 `json:"total"`
} `json:"mem"`
Xray struct {
Version string `json:"version"`
Version string `json:"version"`
State string `json:"state"`
ErrorMsg string `json:"errorMsg"`
} `json:"xray"`
PanelVersion string `json:"panelVersion"`
PanelGuid string `json:"panelGuid"`
@@ -628,6 +637,8 @@ func (s *NodeService) Probe(ctx context.Context, n *model.Node) (HeartbeatPatch,
patch.MemPct = float64(o.Mem.Current) * 100.0 / float64(o.Mem.Total)
}
patch.XrayVersion = o.Xray.Version
patch.XrayState = o.Xray.State
patch.XrayError = o.Xray.ErrorMsg
patch.PanelVersion = o.PanelVersion
patch.Guid = o.PanelGuid
patch.UptimeSecs = o.Uptime
@@ -643,6 +654,10 @@ type ProbeResultUI struct {
MemPct float64 `json:"memPct" example:"45.2"`
UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
Error string `json:"error"`
// XrayState/XrayError are populated on successful probes even when the node's
// Xray core is not healthy. The UI uses them for a distinct "panel ok, xray failed" indicator.
XrayState string `json:"xrayState"`
XrayError string `json:"xrayError"`
}
func (p HeartbeatPatch) ToUI(ok bool) ProbeResultUI {
@@ -654,6 +669,8 @@ func (p HeartbeatPatch) ToUI(ok bool) ProbeResultUI {
MemPct: p.MemPct,
UptimeSecs: p.UptimeSecs,
Error: FriendlyProbeError(p.LastError),
XrayState: p.XrayState,
XrayError: p.XrayError,
}
if ok {
r.Status = "online"
+4
View File
@@ -40,6 +40,8 @@ func (s *NodeService) LocalDescendants() ([]model.NodeSummary, error) {
LatencyMs: n.LatencyMs,
PanelVersion: n.PanelVersion,
XrayVersion: n.XrayVersion,
XrayState: n.XrayState,
XrayError: n.XrayError,
})
}
return out, nil
@@ -140,6 +142,8 @@ func (s *NodeService) GetNodeTree() ([]*model.Node, error) {
LatencyMs: sum.LatencyMs,
PanelVersion: sum.PanelVersion,
XrayVersion: sum.XrayVersion,
XrayState: sum.XrayState,
XrayError: sum.XrayError,
Transitive: true,
})
}
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "متصل",
"offline": "غير متصل",
"unknown": "غير معروف"
"unknown": "غير معروف",
"xrayError": "خطأ Xray",
"xrayStopped": "متوقف"
},
"toasts": {
"list": "فشل تحميل النودز",
+3 -1
View File
@@ -897,7 +897,9 @@
"statusValues": {
"online": "Online",
"offline": "Offline",
"unknown": "Unknown"
"unknown": "Unknown",
"xrayError": "Xray Error",
"xrayStopped": "Stopped"
},
"toasts": {
"list": "Failed to load nodes",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "En línea",
"offline": "Sin conexión",
"unknown": "Desconocido"
"unknown": "Desconocido",
"xrayError": "Error de Xray",
"xrayStopped": "Detenido"
},
"toasts": {
"list": "Error al cargar los nodos",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "آنلاین",
"offline": "آفلاین",
"unknown": "نامشخص"
"unknown": "نامشخص",
"xrayError": "خطای Xray",
"xrayStopped": "متوقف"
},
"toasts": {
"list": "بارگذاری نودها ناموفق",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "Online",
"offline": "Offline",
"unknown": "Tidak diketahui"
"unknown": "Tidak diketahui",
"xrayError": "Kesalahan Xray",
"xrayStopped": "Berhenti"
},
"toasts": {
"list": "Gagal memuat node",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "オンライン",
"offline": "オフライン",
"unknown": "不明"
"unknown": "不明",
"xrayError": "Xray エラー",
"xrayStopped": "停止"
},
"toasts": {
"list": "ノードの読み込みに失敗しました",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "Online",
"offline": "Offline",
"unknown": "Desconhecido"
"unknown": "Desconhecido",
"xrayError": "Erro do Xray",
"xrayStopped": "Parado"
},
"toasts": {
"list": "Falha ao carregar os nós",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "В сети",
"offline": "Не в сети",
"unknown": "Неизвестно"
"unknown": "Неизвестно",
"xrayError": "Ошибка Xray",
"xrayStopped": "Остановлен"
},
"toasts": {
"list": "Не удалось загрузить узлы",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "Çevrimiçi",
"offline": "Çevrimdışı",
"unknown": "Bilinmiyor"
"unknown": "Bilinmiyor",
"xrayError": "Xray Hatası",
"xrayStopped": "Durduruldu"
},
"toasts": {
"list": "Düğümler yüklenemedi",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "У мережі",
"offline": "Не в мережі",
"unknown": "Невідомо"
"unknown": "Невідомо",
"xrayError": "Помилка Xray",
"xrayStopped": "Зупинено"
},
"toasts": {
"list": "Не вдалося завантажити вузли",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "Trực tuyến",
"offline": "Ngoại tuyến",
"unknown": "Không xác định"
"unknown": "Không xác định",
"xrayError": "Lỗi Xray",
"xrayStopped": "Đã dừng"
},
"toasts": {
"list": "Không tải được danh sách nút",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "在线",
"offline": "离线",
"unknown": "未知"
"unknown": "未知",
"xrayError": "Xray 错误",
"xrayStopped": "已停止"
},
"toasts": {
"list": "加载节点失败",
+3 -1
View File
@@ -896,7 +896,9 @@
"statusValues": {
"online": "上線",
"offline": "離線",
"unknown": "未知"
"unknown": "未知",
"xrayError": "Xray 錯誤",
"xrayStopped": "已停止"
},
"toasts": {
"list": "載入節點失敗",