From 123b9dcd611704a921e7df9e92da0da8debb1331 Mon Sep 17 00:00:00 2001
From: ji ruo <79234113+RuoJi6@users.noreply.github.com>
Date: Tue, 20 Oct 2026 13:12:51 +0800
Subject: [PATCH] fix: polish credential traffic cards and support copying over
HTTP
---
web/src/components/credential-content.tsx | 142 ++++++++++++++++------
web/src/components/intelligence-panel.tsx | 9 +-
web/src/lib/utils.ts | 28 +++--
3 files changed, 130 insertions(+), 49 deletions(-)
diff --git a/web/src/components/credential-content.tsx b/web/src/components/credential-content.tsx
index a8d1e30..062d36d 100644
--- a/web/src/components/credential-content.tsx
+++ b/web/src/components/credential-content.tsx
@@ -2,7 +2,7 @@
import * as React from "react";
-import { CopyIcon } from "lucide-react";
+import { ArrowUpRightIcon, ChevronDownIcon, ChevronUpIcon, CopyIcon, NetworkIcon } from "lucide-react";
import { toast } from "sonner";
import { HttpCodeBlock } from "@/components/http-code-block";
@@ -14,8 +14,8 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/lib/api";
import { toneClasses } from "@/lib/status";
-import type { CredentialAccount, CredentialTrafficDetail, TaskIntelligence } from "@/lib/types";
-import { cn } from "@/lib/utils";
+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);
@@ -52,6 +52,93 @@ function HighlightedOriginal({ text, accounts }: { text: string; accounts: Crede
);
}
+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,
@@ -84,18 +171,16 @@ export function CredentialContent({
}
return (
- {analysis ? (
+ {analysis && analysis.state !== "error" ? (
{analysis.state === "processing"
? "正在整理"
: analysis.state === "queued"
? "等待整理"
- : analysis.state === "error"
- ? "整理失败"
- : analysis.method === "rules"
- ? "规则整理"
- : "Agent 整理"}
+ : analysis.method === "rules"
+ ? "规则整理"
+ : "Agent 整理"}
{analysis.method === "rules" ? 未调用模型 : null}
{detail && analysis.model ? (
@@ -107,7 +192,7 @@ export function CredentialContent({
) : null}
- ) : enabled && item.kind === "credential" ? (
+ ) : !analysis && enabled && item.kind === "credential" ? (
等待凭据整理
) : null}
{ready ? (
@@ -155,12 +240,10 @@ export function CredentialContent({
variant="ghost"
size="icon-xs"
aria-label={`复制${label} ${index + 1}`}
- onClick={() =>
- void navigator.clipboard
- .writeText(account[field] ?? "")
- .then(() => toast.success(`${label}已复制`))
- .catch(() => toast.error("复制失败"))
- }
+ onClick={async () => {
+ if (await copyText(account[field] ?? "")) toast.success(`${label}已复制`);
+ else toast.error("复制失败,请手动选择内容复制");
+ }}
>
@@ -178,26 +261,15 @@ export function CredentialContent({
{accounts.length > 2 ? `还有 ${accounts.length - 2} 组凭据,查看详情` : "查看完整凭据"}
) : null}
- {analysis?.error ? (
-
- {analysis.error}
-
- ) : null}
- {analysis?.note ?
{analysis.note}
: null}
+ {ready && analysis?.note ?
{analysis.note}
: null}
{analysis?.traffic?.length ? (
-
- 关联流量 · {analysis.traffic.length} 条
- {analysis.traffic.map((ref) => (
-
- ))}
-
+
) : null}
{detail && analysis?.traffic_note ? (
{analysis.traffic_note}
diff --git a/web/src/components/intelligence-panel.tsx b/web/src/components/intelligence-panel.tsx
index 3ac82ed..81022fb 100644
--- a/web/src/components/intelligence-panel.tsx
+++ b/web/src/components/intelligence-panel.tsx
@@ -23,6 +23,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/lib/api";
import { statusMeta } from "@/lib/status";
import type { IntelligenceKind, IntelligencePage, TaskIntelligence } from "@/lib/types";
+import { copyText } from "@/lib/utils";
const KINDS: { value: IntelligenceKind; label: string }[] = [
{ value: "credential", label: "凭据" },
@@ -397,12 +398,8 @@ export function IntelligencePanel({
size="sm"
variant="ghost"
onClick={async () => {
- try {
- await navigator.clipboard.writeText(selected.content);
- toast.success("已复制");
- } catch {
- toast.error("复制失败,请手动选择内容复制");
- }
+ if (await copyText(selected.content)) toast.success("已复制");
+ else toast.error("复制失败,请手动选择内容复制");
}}
>
diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
index e5cbf1d..a5d31dd 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -57,20 +57,32 @@ export async function copyText(text: string): Promise
{
// 继续走降级方案
}
}
+ const active = document.activeElement;
+ const selection = window.getSelection();
+ const ranges = selection ? Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i)) : [];
+ const textarea = document.createElement("textarea");
try {
- const textarea = document.createElement("textarea");
textarea.value = text;
+ textarea.setAttribute("aria-label", "复制内容");
textarea.style.position = "fixed";
- textarea.style.left = "-9999px";
- textarea.style.top = "0";
- document.body.appendChild(textarea);
- textarea.focus();
+ textarea.style.opacity = "0";
+ textarea.style.pointerEvents = "none";
+ // Stay inside the active modal so its focus trap does not steal the selection.
+ const dialogs = document.querySelectorAll('[role="dialog"][data-state="open"]');
+ const container = active?.closest('[role="dialog"]') ?? dialogs.item(dialogs.length - 1) ?? document.body;
+ container.appendChild(textarea);
+ textarea.focus({ preventScroll: true });
textarea.select();
- const ok = document.execCommand("copy");
- document.body.removeChild(textarea);
- return ok;
+ return document.execCommand("copy");
} catch {
return false;
+ } finally {
+ textarea.remove();
+ if (active instanceof HTMLElement && active.isConnected) active.focus({ preventScroll: true });
+ if (selection && ranges.length) {
+ selection.removeAllRanges();
+ for (const range of ranges) selection.addRange(range);
+ }
}
}