"use client"; import * as React from "react"; import { ArrowUpRightIcon, ChevronDownIcon, ChevronUpIcon, CopyIcon, NetworkIcon } from "lucide-react"; import { toast } from "sonner"; import { HttpCodeBlock } from "@/components/http-code-block"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; import { api } from "@/lib/api"; import { toneClasses } from "@/lib/status"; import type { CredentialAccount, CredentialTraffic, CredentialTrafficDetail, TaskIntelligence } from "@/lib/types"; import { cn, copyText } from "@/lib/utils"; export function intelligencePreview(text: string, limit = 260) { const chars = Array.from(text); return chars.length > limit ? `${chars.slice(0, limit).join("")}…` : text; } const FIELDS = [ ["username", "账号", "blue"], ["password", "密码", "violet"], ["token", "Token / API Key", "violet"], ["cookie", "Cookie", "amber"], ] as const; function HighlightedOriginal({ text, accounts }: { text: string; accounts: CredentialAccount[] }) { const parts = React.useMemo(() => { const values = [ ...new Set(accounts.flatMap((a) => [a.username, a.password, a.token, a.cookie]).filter((v): v is string => !!v)), ] .sort((a, b) => b.length - a.length) .slice(0, 200); if (!values.length) return [text]; const re = new RegExp(`(${values.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "g"); return text.split(re); }, [text, accounts]); return (
      {parts.map((part, i) =>
        i % 2 ? (
          
            {part}
          
        ) : (
          part
        ),
      )}
    
); } function TrafficList({ traffic, detail, onTraffic, onDetails, }: { traffic: CredentialTraffic[]; detail: boolean; onTraffic?: (id: string) => void; onDetails?: () => void; }) { const [expanded, setExpanded] = React.useState(false); const limit = detail ? 3 : 2; const visible = detail && expanded ? traffic : traffic.slice(0, limit); const remaining = traffic.length - limit; return (
关联流量 {traffic.length} 条
{visible.map((ref) => { let host = "", path = ref.url; try { const url = new URL(ref.url); host = url.host; path = `${url.pathname}${url.search}${url.hash}`; } catch { // Retain non-URL references as plain text. } const tone = ref.status >= 500 ? "red" : ref.status >= 400 ? "amber" : ref.status >= 300 ? "blue" : ref.status >= 200 ? "green" : "neutral"; return ( ); })}
{remaining > 0 ? ( ) : null}
); } export function CredentialContent({ item, detail = false, enabled = false, onTraffic, onDetails, }: { item: TaskIntelligence; detail?: boolean; enabled?: boolean; onTraffic?: (id: string) => void; onDetails?: () => void; }) { const analysis = item.kind === "credential" ? item.credentials : undefined; const accounts = analysis?.accounts ?? []; const ready = accounts.length > 0; const visibleAccounts = detail ? accounts : accounts.slice(0, 2); const fieldsTruncated = !detail && visibleAccounts.some((a) => [a.username, a.password, a.token, a.cookie, a.endpoint, a.label, a.protocol].some( (v) => v && Array.from(v).length > 80, ), ); let pretty = item.content; try { pretty = JSON.stringify(JSON.parse(item.content), null, 2); } catch { // Plain-text reports keep their original representation. } return (
{analysis && analysis.state !== "error" ? (
{analysis.state === "processing" ? "正在整理" : analysis.state === "queued" ? "等待整理" : analysis.method === "rules" ? "规则整理" : "Agent 整理"} {analysis.method === "rules" ? 未调用模型 : null} {detail && analysis.model ? ( {analysis.model} ·{" "} {analysis.input_tokens + analysis.output_tokens > 0 ? `输入 ${analysis.input_tokens} / 输出 ${analysis.output_tokens} Token` : "模型未返回 Token 用量"} ) : null}
) : !analysis && enabled && item.kind === "credential" ? ( 等待凭据整理 ) : null} {ready ? (
{visibleAccounts.map((account, index) => (
凭据 {index + 1} {account.label ? ( · {detail ? account.label : intelligencePreview(account.label, 80)} ) : null} {account.protocol ? ( {detail ? account.protocol : intelligencePreview(account.protocol, 80)} ) : null}
{account.endpoint ? ( {detail ? account.endpoint : intelligencePreview(account.endpoint, 80)} ) : null}
{FIELDS.map(([field, label, tone]) => account[field] ? (
{label}
{detail ? account[field] : intelligencePreview(account[field] ?? "", 80)}
) : null, )}
))}
) : null} {!detail && ready && (accounts.length > 2 || fieldsTruncated) ? ( ) : null} {ready && analysis?.note ?

{analysis.note}

: null} {analysis?.traffic?.length ? ( ) : null} {detail && analysis?.traffic_note ? (

{analysis.traffic_note}

) : null} {!ready || detail ? (
{ready ?

原始上报内容

: null} {!detail && Array.from(pretty).length > 260 ? ( ) : null}
) : null}
); } export function CredentialTrafficViewer({ scope, scopeId, item, trafficId, onClose, }: { scope: "tasks" | "assets"; scopeId: string; item: TaskIntelligence | null; trafficId: string | null; onClose: () => void; }) { const [data, setData] = React.useState(null); const [error, setError] = React.useState(""); React.useEffect(() => { if (!item || !trafficId) return; let alive = true; setData(null); setError(""); api .credentialTraffic(scope, scopeId, item.id, trafficId) .then((d) => { if (alive) setData(d); }) .catch((e: Error) => { if (alive) setError(e.message); }); return () => { alive = false; }; }, [scope, scopeId, item, trafficId]); return ( { if (!open) onClose(); }} > 凭据关联流量 {data?.reference.url ?? "读取已绑定的真实流量"} {error ? ( {error} ) : data ? (

{data.reference.method} · HTTP {data.reference.status} ·{" "} {data.reference.side === "request" ? "请求包含匹配凭据" : "响应包含匹配凭据"},不代表认证验证成功。

绑定时保存的匹配片段

{data.unavailable ? ( 原始流量已删除或不可用,仍可查看上方保存的匹配片段。 ) : (

请求 Request

响应 Response

)}
) : ( )}
); }