diff --git a/go-backend/internal/http/handler/control_plane.go b/go-backend/internal/http/handler/control_plane.go index 3ed4f0d..93eefe7 100644 --- a/go-backend/internal/http/handler/control_plane.go +++ b/go-backend/internal/http/handler/control_plane.go @@ -58,6 +58,55 @@ type diagnosisProgress struct { type diagnosisItemEmitter func(index int, item map[string]interface{}, progress diagnosisProgress) +func (h *Handler) buildDiagnosisStreamStartItems(workItems []diagnosisWorkItem) []map[string]interface{} { + if len(workItems) == 0 { + return []map[string]interface{}{} + } + + nodeCache := map[int64]*nodeRecord{} + items := make([]map[string]interface{}, 0, len(workItems)) + for _, workItem := range workItems { + targetIP := strings.TrimSpace(workItem.targetIP) + targetPort := workItem.targetPort + if workItem.hasChainHop { + fromNode, _ := h.cachedNode(nodeCache, workItem.fromNodeID) + targetNode, err := h.cachedNode(nodeCache, workItem.toNode.NodeID) + if err == nil { + resolvedIP, resolvedPort, resolveErr := resolveChainProbeTarget(fromNode, targetNode, workItem.toNode.Port, workItem.ipPreference) + if resolveErr == nil { + targetIP = resolvedIP + targetPort = resolvedPort + } + } + } + if targetPort <= 0 { + targetPort = 443 + } + + nodeName := fmt.Sprintf("node_%d", workItem.fromNodeID) + if node, err := h.cachedNode(nodeCache, workItem.fromNodeID); err == nil && strings.TrimSpace(node.Name) != "" { + nodeName = node.Name + } + + item := map[string]interface{}{ + "success": false, + "diagnosing": true, + "description": workItem.description, + "nodeName": nodeName, + "nodeId": strconv.FormatInt(workItem.fromNodeID, 10), + "targetIp": targetIP, + "targetPort": targetPort, + "message": "诊断中...", + } + for key, value := range workItem.metadata { + item[key] = value + } + items = append(items, item) + } + + return items +} + const diagnosisMaxConcurrency = 8 const ( @@ -923,8 +972,10 @@ enqueueLoop: } } close(jobs) - wg.Wait() - close(resultCh) + go func() { + wg.Wait() + close(resultCh) + }() progress := diagnosisProgress{Total: len(workItems)} for result := range resultCh { diff --git a/go-backend/internal/http/handler/diagnosis_stream.go b/go-backend/internal/http/handler/diagnosis_stream.go index c75011b..93823e9 100644 --- a/go-backend/internal/http/handler/diagnosis_stream.go +++ b/go-backend/internal/http/handler/diagnosis_stream.go @@ -81,6 +81,7 @@ func (h *Handler) streamDiagnosisRuntime(ctx context.Context, cancel context.Can payload := map[string]interface{}{ "total": len(workItems), "timestamp": time.Now().UnixMilli(), + "items": h.buildDiagnosisStreamStartItems(workItems), } for key, value := range startPayload { payload[key] = value diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index b1ad76c..b35aa1e 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -890,10 +890,13 @@ export default function ForwardPage() { ? payload.forwardName : forward.name; const startTotal = Number(payload.total); + const startItems = Array.isArray(payload.items) + ? (payload.items as ForwardDiagnosisResult["results"]) + : []; setDiagnosisResult((prev) => ({ forwardName: startForwardName, timestamp: Date.now(), - results: prev?.results || [], + results: startItems.length > 0 ? startItems : prev?.results || [], })); if (Number.isFinite(startTotal) && startTotal >= 0) { setDiagnosisProgress((prev) => ({ @@ -919,9 +922,15 @@ export default function ForwardPage() { ); if (existingIndex >= 0) { - nextResults[existingIndex] = result; + nextResults[existingIndex] = { + ...result, + diagnosing: false, + }; } else { - nextResults.push(result); + nextResults.push({ + ...result, + diagnosing: false, + }); } return { ...base, @@ -3460,15 +3469,7 @@ export default function ForwardPage() { )} - {diagnosisLoading && - (!diagnosisResult || diagnosisResult.results.length === 0) ? ( -
-
- - 正在诊断... -
-
- ) : diagnosisResult ? ( + {diagnosisResult ? (
{diagnosisLoading && (
@@ -3599,6 +3600,10 @@ export default function ForwardPage() { {results.map((result, index) => { + const isDiagnosing = Boolean( + result.diagnosing, + ); + const isSuccess = result.success === true; const quality = getForwardDiagnosisQualityDisplay( result.averageTime, @@ -3609,22 +3614,28 @@ export default function ForwardPage() {
- - {result.success ? "✓" : "✗"} - + {isDiagnosing ? ( + + ) : ( + + {isSuccess ? "✓" : "✗"} + + )}
{result.description} @@ -3639,18 +3650,24 @@ export default function ForwardPage() { - {result.success ? "成功" : "失败"} + {isDiagnosing + ? "诊断中" + : isSuccess + ? "成功" + : "失败"} - {result.success ? ( + {isSuccess ? ( {result.averageTime?.toFixed(0)} @@ -3661,7 +3678,7 @@ export default function ForwardPage() { )} - {result.success ? ( + {isSuccess ? ( 0 @@ -3678,7 +3695,7 @@ export default function ForwardPage() { )} - {result.success && quality ? ( + {isSuccess && quality ? (
{results.map((result, index) => { + const isDiagnosing = Boolean(result.diagnosing); + const isSuccess = result.success === true; const quality = getForwardDiagnosisQualityDisplay( result.averageTime, @@ -3782,21 +3801,27 @@ export default function ForwardPage() {
- - {result.success ? "✓" : "✗"} - + {isDiagnosing ? ( + + ) : ( + + {isSuccess ? "✓" : "✗"} + + )}
{result.description} @@ -3808,16 +3833,24 @@ export default function ForwardPage() { - {result.success ? "成功" : "失败"} + {isDiagnosing + ? "诊断中" + : isSuccess + ? "成功" + : "失败"}
- {result.success ? ( + {isSuccess ? (
@@ -3861,8 +3894,16 @@ export default function ForwardPage() {
) : (
-
- {result.message || "连接失败"} +
+ {isDiagnosing + ? result.message || "诊断中..." + : result.message || "连接失败"}
)} @@ -3903,14 +3944,16 @@ export default function ForwardPage() {
{/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */} - {diagnosisResult.results.some((r) => !r.success) && ( + {diagnosisResult.results.some( + (r) => r.success === false && !r.diagnosing, + ) && (

失败详情

{diagnosisResult.results - .filter((r) => !r.success) + .filter((r) => r.success === false && !r.diagnosing) .map((result, index) => ( ({ tunnelName: startTunnelName, tunnelType: startTunnelType, timestamp: Date.now(), - results: prev?.results || [], + results: startItems.length > 0 ? startItems : prev?.results || [], })); if (Number.isFinite(startTotal) && startTotal >= 0) { setDiagnosisProgress((prev) => ({ @@ -521,9 +524,15 @@ export default function TunnelPage() { ); if (existingIndex >= 0) { - nextResults[existingIndex] = result; + nextResults[existingIndex] = { + ...result, + diagnosing: false, + }; } else { - nextResults.push(result); + nextResults.push({ + ...result, + diagnosing: false, + }); } return { ...base, @@ -970,6 +979,10 @@ export default function TunnelPage() {
{sortedTunnels.map((tunnel) => { const typeDisplay = getTunnelTypeDisplay(tunnel.type); + const tunnelTypeChipClassName = + tunnel.type === 1 + ? "text-xs bg-primary-100 text-primary-800 border-primary-300 dark:bg-primary-900/45 dark:text-primary-200 dark:border-primary-700" + : "text-xs bg-success-100 text-success-800 border-success-300 dark:bg-success-900/35 dark:text-success-200 dark:border-success-700"; return ( @@ -993,7 +1006,7 @@ export default function TunnelPage() {
- {diagnosisLoading && - (!diagnosisResult || diagnosisResult.results.length === 0) ? ( -
-
- - 正在诊断... -
-
- ) : diagnosisResult ? ( + {diagnosisResult ? (
{diagnosisLoading && (
@@ -2245,6 +2250,10 @@ export default function TunnelPage() { {results.map((result, index) => { + const isDiagnosing = Boolean( + result.diagnosing, + ); + const isSuccess = result.success === true; const quality = getDiagnosisQualityDisplay( result.averageTime, result.packetLoss, @@ -2254,22 +2263,28 @@ export default function TunnelPage() {
- - {result.success ? "✓" : "✗"} - + {isDiagnosing ? ( + + ) : ( + + {isSuccess ? "✓" : "✗"} + + )}
{result.description} @@ -2284,18 +2299,24 @@ export default function TunnelPage() { - {result.success ? "成功" : "失败"} + {isDiagnosing + ? "诊断中" + : isSuccess + ? "成功" + : "失败"} - {result.success ? ( + {isSuccess ? ( {result.averageTime?.toFixed(0)} @@ -2306,7 +2327,7 @@ export default function TunnelPage() { )} - {result.success ? ( + {isSuccess ? ( 0 @@ -2323,7 +2344,7 @@ export default function TunnelPage() { )} - {result.success && quality ? ( + {isSuccess && quality ? (
{results.map((result, index) => { + const isDiagnosing = Boolean(result.diagnosing); + const isSuccess = result.success === true; const quality = getDiagnosisQualityDisplay( result.averageTime, result.packetLoss, @@ -2426,21 +2449,27 @@ export default function TunnelPage() {
- - {result.success ? "✓" : "✗"} - + {isDiagnosing ? ( + + ) : ( + + {isSuccess ? "✓" : "✗"} + + )}
{result.description} @@ -2452,16 +2481,24 @@ export default function TunnelPage() { - {result.success ? "成功" : "失败"} + {isDiagnosing + ? "诊断中" + : isSuccess + ? "成功" + : "失败"}
- {result.success ? ( + {isSuccess ? (
@@ -2505,8 +2542,16 @@ export default function TunnelPage() {
) : (
-
- {result.message || "连接失败"} +
+ {isDiagnosing + ? result.message || "诊断中..." + : result.message || "连接失败"}
)} @@ -2547,14 +2592,16 @@ export default function TunnelPage() {
{/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */} - {diagnosisResult.results.some((r) => !r.success) && ( + {diagnosisResult.results.some( + (r) => r.success === false && !r.diagnosing, + ) && (

失败详情

{diagnosisResult.results - .filter((r) => !r.success) + .filter((r) => r.success === false && !r.diagnosing) .map((result, index) => (