Browse Source

Fix video detail link for ids with special chars

视频号 platform content ids contain / + = which break path routing and caused
"request failed" on the detail page. Link the detail route by the clean
content_discovery_id instead of the platform id, and defensively decode route
params so an encoded segment is not double-encoded by the API client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sam Lee 3 weeks ago
parent
commit
0af1343670

+ 9 - 1
web2/app/runs/[runId]/queries/[queryId]/videos/page.tsx

@@ -4,7 +4,15 @@ type Props = {
   params: Promise<{ runId: string; queryId: string }>;
 };
 
+function safeDecode(value: string): string {
+  try {
+    return decodeURIComponent(value);
+  } catch {
+    return value;
+  }
+}
+
 export default async function QueryVideosRoute({ params }: Props) {
   const { runId, queryId } = await params;
-  return <QueryVideosPage runId={runId} queryId={queryId} />;
+  return <QueryVideosPage runId={safeDecode(runId)} queryId={safeDecode(queryId)} />;
 }

+ 9 - 1
web2/app/runs/[runId]/queries/[queryId]/walk/page.tsx

@@ -4,7 +4,15 @@ type Props = {
   params: Promise<{ runId: string; queryId: string }>;
 };
 
+function safeDecode(value: string): string {
+  try {
+    return decodeURIComponent(value);
+  } catch {
+    return value;
+  }
+}
+
 export default async function QueryWalkRoute({ params }: Props) {
   const { runId, queryId } = await params;
-  return <QueryWalkPage runId={runId} queryId={queryId} />;
+  return <QueryWalkPage runId={safeDecode(runId)} queryId={safeDecode(queryId)} />;
 }

+ 9 - 1
web2/app/runs/[runId]/videos/[contentId]/page.tsx

@@ -4,7 +4,15 @@ type Props = {
   params: Promise<{ runId: string; contentId: string }>;
 };
 
+function safeDecode(value: string): string {
+  try {
+    return decodeURIComponent(value);
+  } catch {
+    return value;
+  }
+}
+
 export default async function VideoDetailRoute({ params }: Props) {
   const { runId, contentId } = await params;
-  return <VideoDetailPage runId={runId} contentId={contentId} />;
+  return <VideoDetailPage runId={safeDecode(runId)} contentId={safeDecode(contentId)} />;
 }

+ 1 - 1
web2/features/LedgerPage.tsx

@@ -241,7 +241,7 @@ function VideoDetailCell({ runId, video }: { runId: string; video: VideoRef | nu
   }
   return (
     <td className="videos-cell video-detail-cell">
-      <Link className="video-line-card" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(video.id)}`}>
+      <Link className="video-line-card" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(video.contentDiscoveryId || video.id)}`}>
         <span className="video-thumb small">{platformLabel(video.platform)}</span>
         <span className="video-line-main">
           <b>{video.title}</b>

+ 2 - 2
web2/features/QueryVideosPage.tsx

@@ -103,7 +103,7 @@ function VideoRow({ index, runId, video }: { index: number; runId: string; video
     <tr>
       <td>{index + 1}</td>
       <td>
-        <Link className="title-link" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(video.id)}`}>
+        <Link className="title-link" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(video.contentDiscoveryId || video.id)}`}>
           {video.title}
         </Link>
       </td>
@@ -129,7 +129,7 @@ function VideoRow({ index, runId, video }: { index: number; runId: string; video
         {video.mediaFailureReason ? <div className="muted">{video.mediaFailureReason}</div> : null}
       </td>
       <td>
-        <Link className="mini-button" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(video.id)}`}>
+        <Link className="mini-button" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(video.contentDiscoveryId || video.id)}`}>
           <PanelRightOpen size={13} />
           看详情
         </Link>

+ 5 - 2
web2/features/QueryWalkPage.tsx

@@ -9,7 +9,7 @@ import { EmptyState, ErrorState, LoadingState } from "@/components/StateBlocks";
 import { getFlowLedgerWalk } from "@/lib/api/client";
 import type { FlowLedgerWalkResponse, RawRecord } from "@/lib/api/types";
 import { methodLabel, type BusinessTone } from "@/lib/flow-ledger/business";
-import { asArray, asRecord, text } from "@/lib/flow-ledger/format";
+import { asArray, asRecord, maybeText, text } from "@/lib/flow-ledger/format";
 
 // ---------------------------------------------------------------------------
 // tree model
@@ -296,11 +296,14 @@ function Toggle({ open, disabled, onClick }: { open: boolean; disabled: boolean;
 function RowLinks({ video, runId, }: { video: RawRecord; runId: string }) {
   const id = text(video.id, "");
   if (!id) return null;
+  // platform content ids (视频号 finder ids) can contain / + = which break path routing;
+  // prefer the clean content_discovery_id for the detail route.
+  const detailId = maybeText(video.content_discovery_id) || id;
   const platformUrl = text(video.platform_url, "");
   const platformLabel = text(video.platform_label, "原视频");
   return (
     <span className="wt-links">
-      <Link className="wt-link" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(id)}`} onClick={(e) => e.stopPropagation()}>
+      <Link className="wt-link" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(detailId)}`} onClick={(e) => e.stopPropagation()}>
         详情
       </Link>
       {platformUrl ? (