Add V4 scan run logs

This commit is contained in:
2569718930@qq.com
2026-04-25 01:09:08 +08:00
parent 5177d4be83
commit df6a2c73ed
4 changed files with 407 additions and 7 deletions
@@ -10385,6 +10385,110 @@
font-weight: 700;
}
.root :global(.scan-ai-log-panel) {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 14px;
border: 1px solid rgba(68, 100, 150, 0.18);
border-radius: 16px;
background:
linear-gradient(180deg, rgba(9, 23, 38, 0.92), rgba(7, 17, 30, 0.92)),
radial-gradient(circle at 0 0, rgba(34, 211, 238, 0.12), transparent 34%);
}
.root :global(.scan-ai-log-head) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.root :global(.scan-ai-log-head strong) {
color: #e9f5ff;
font-size: 13px;
font-weight: 900;
}
.root :global(.scan-ai-log-head span) {
color: #8fa8c7;
font-size: 12px;
font-weight: 800;
}
.root :global(.scan-ai-log-list) {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 2px;
}
.root :global(.scan-ai-log-list::-webkit-scrollbar) {
height: 5px;
}
.root :global(.scan-ai-log-list::-webkit-scrollbar-thumb) {
background: rgba(88, 125, 180, 0.26);
border-radius: 999px;
}
.root :global(.scan-ai-log-item) {
display: grid;
grid-template-columns: auto minmax(180px, 1fr);
gap: 8px;
min-width: 260px;
padding: 9px 10px;
border: 1px solid rgba(68, 100, 150, 0.16);
border-radius: 12px;
background: rgba(14, 29, 48, 0.76);
}
.root :global(.scan-ai-log-item b) {
display: block;
color: #edf7ff;
font-size: 12px;
font-weight: 900;
line-height: 1.25;
}
.root :global(.scan-ai-log-item small) {
display: block;
margin-top: 3px;
color: #9fb4d2;
font-size: 11px;
font-weight: 700;
line-height: 1.35;
}
.root :global(.scan-ai-log-time) {
color: #7892b1;
font-size: 11px;
font-weight: 900;
font-variant-numeric: tabular-nums;
}
.root :global(.scan-ai-log-item.info) {
border-color: rgba(56, 189, 248, 0.22);
}
.root :global(.scan-ai-log-item.success) {
border-color: rgba(16, 185, 129, 0.32);
}
.root :global(.scan-ai-log-item.warning) {
border-color: rgba(245, 158, 11, 0.34);
}
.root :global(.scan-ai-log-item.error) {
border-color: rgba(248, 113, 113, 0.34);
}
.root :global(.scan-ai-log-empty) {
color: #8fa8c7;
font-size: 12px;
font-weight: 800;
}
.root :global(.scan-detail-panel) {
width: auto;
min-width: 0;
@@ -10992,6 +11096,47 @@
color: #647a98;
}
.root :global(.scan-terminal.light .scan-ai-log-panel) {
border-color: rgba(35, 72, 118, 0.12);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(246, 250, 255, 0.9));
}
.root :global(.scan-terminal.light .scan-ai-log-head strong),
.root :global(.scan-terminal.light .scan-ai-log-item b) {
color: #122033;
}
.root :global(.scan-terminal.light .scan-ai-log-head span),
.root :global(.scan-terminal.light .scan-ai-log-time),
.root :global(.scan-terminal.light .scan-ai-log-empty) {
color: #647a98;
}
.root :global(.scan-terminal.light .scan-ai-log-item) {
border-color: rgba(35, 72, 118, 0.12);
background: rgba(255, 255, 255, 0.72);
}
.root :global(.scan-terminal.light .scan-ai-log-item small) {
color: #58708f;
}
.root :global(.scan-terminal.light .scan-ai-log-item.info) {
border-color: rgba(14, 165, 233, 0.24);
}
.root :global(.scan-terminal.light .scan-ai-log-item.success) {
border-color: rgba(10, 160, 100, 0.28);
}
.root :global(.scan-terminal.light .scan-ai-log-item.warning) {
border-color: rgba(217, 119, 6, 0.28);
}
.root :global(.scan-terminal.light .scan-ai-log-item.error) {
border-color: rgba(220, 38, 38, 0.28);
}
.root :global(.scan-terminal.light .scan-cta-ghost),
.root :global(.scan-terminal.light .scan-detail-analysis-button) {
border-color: rgba(7, 160, 100, 0.24);
@@ -60,6 +60,29 @@ interface FilterState extends ScanTerminalFilters {}
type ContentView = "list" | "map" | "calendar";
type ThemeMode = "dark" | "light";
type ScanAiLogTone = "info" | "success" | "warning" | "error";
type ScanAiLogEntry = {
id: string;
time: string;
tone: ScanAiLogTone;
title: string;
detail?: string | null;
};
function formatLogTime() {
const now = new Date();
return `${String(now.getHours()).padStart(2, "0")}:${String(
now.getMinutes(),
).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
}
function formatDuration(ms?: number | null) {
if (ms == null || !Number.isFinite(Number(ms))) return "--";
if (Number(ms) < 1000) return `${Math.round(Number(ms))}ms`;
return `${(Number(ms) / 1000).toFixed(1)}s`;
}
function formatShortDate(value?: string | null, locale = "zh-CN") {
const text = String(value || "").trim();
if (!text) return "--";
@@ -245,9 +268,14 @@ function ScanTerminalScreen() {
const [activeView, setActiveView] = useState<ContentView>("map");
const [mapSelectedCityName, setMapSelectedCityName] = useState<string | null>(null);
const [showScanPaywall, setShowScanPaywall] = useState(false);
const [aiLogs, setAiLogs] = useState<ScanAiLogEntry[]>([]);
const [userLocalTime, setUserLocalTime] = useState("--");
const [themeMode, setThemeMode] = useState<ThemeMode>("dark");
const deferredRows = useDeferredValue(terminalData?.rows || []);
const prependAiLogs = (entries: ScanAiLogEntry[]) => {
setAiLogs((current) => [...entries, ...current].slice(0, 8));
};
const timeSortedRows = useMemo(
() => sortRowsByUserTime(deferredRows),
[deferredRows],
@@ -370,6 +398,17 @@ function ScanTerminalScreen() {
return sortRowsByUserTime(response.rows)[0]?.id || response.top_signal?.id || null;
});
});
prependAiLogs([
{
id: `rule-${Date.now()}`,
time: formatLogTime(),
tone: response.rows.length ? "success" : "warning",
title: isEn ? "Rule scan snapshot ready" : "规则扫描快照已就绪",
detail: isEn
? `snapshot ${response.snapshot_id || "--"} · ${response.rows.length} visible rows`
: `快照 ${response.snapshot_id || "--"} · 可见候选 ${response.rows.length}`,
},
]);
} catch (fetchError) {
setError(String(fetchError));
} finally {
@@ -378,14 +417,51 @@ function ScanTerminalScreen() {
};
const runAiReview = async () => {
if (!terminalData?.rows.length || aiLoading) return;
if (aiLoading) return;
if (!terminalData?.rows.length) {
prependAiLogs([
{
id: `no-rows-${Date.now()}`,
time: formatLogTime(),
tone: "warning",
title: isEn ? "V4 not started" : "V4 未启动",
detail: isEn
? "Run the rule scan first. V4 only reviews existing candidate rows."
: "需要先完成规则扫描。V4 只复核已有候选,不会凭空抓市场。",
},
]);
return;
}
const snapshotId = terminalData.snapshot_id || null;
const rowCount = terminalData.rows.length;
prependAiLogs([
{
id: `queued-${Date.now()}`,
time: formatLogTime(),
tone: "info",
title: isEn ? "V4 review queued" : "V4 复核已排队",
detail: isEn
? `snapshot ${snapshotId || "--"} · ${rowCount} rule candidates`
: `快照 ${snapshotId || "--"} · 规则候选 ${rowCount}`,
},
{
id: `send-${Date.now() + 1}`,
time: formatLogTime(),
tone: "info",
title: isEn ? "Sending compact candidate set" : "正在发送精简候选集",
detail: isEn
? "VPS will call DeepSeek; Vercel only proxies the request."
: "由 VPS 调用 DeepSeekVercel 只做短代理。",
},
]);
setAiLoading(true);
setAiError(null);
try {
const response = await dashboardClient.reviewScanTerminalWithAi({
filters: terminalData.filters || activeFilters,
snapshotId: terminalData.snapshot_id || null,
snapshotId,
});
const review = response.ai_scan;
startTransition(() => {
setTerminalData(response);
setActiveFilters(response.filters || activeFilters);
@@ -397,8 +473,55 @@ function ScanTerminalScreen() {
return sortRowsByUserTime(response.rows)[0]?.id || response.top_signal?.id || null;
});
});
prependAiLogs([
{
id: `done-${Date.now()}`,
time: formatLogTime(),
tone: review?.status === "ready" ? "success" : "warning",
title:
review?.status === "ready"
? review.cached
? isEn
? "V4 cache hit"
: "V4 命中缓存"
: isEn
? "V4 review completed"
: "V4 复核完成"
: isEn
? "V4 fell back to rule scan"
: "V4 已回退规则扫描",
detail:
review?.status === "ready"
? isEn
? `${review.model || "V4"} · ${formatDuration(review.duration_ms)} · sent ${review.sent_rows ?? "--"} rows · ${review.recommended_count ?? 0} picks / ${review.vetoed_count ?? 0} veto`
: `${review.model || "V4"} · ${formatDuration(review.duration_ms)} · 发送 ${review.sent_rows ?? "--"} 条 · 推荐 ${review.recommended_count ?? 0} / 排除 ${review.vetoed_count ?? 0}`
: review?.reason || (isEn ? "No AI result returned." : "没有返回 AI 结果。"),
},
...(review?.usage
? [
{
id: `usage-${Date.now() + 1}`,
time: formatLogTime(),
tone: "info" as const,
title: isEn ? "Token usage" : "Token 用量",
detail: isEn
? `input ${review.usage.prompt_tokens ?? "--"} · output ${review.usage.completion_tokens ?? "--"} · total ${review.usage.total_tokens ?? "--"}`
: `输入 ${review.usage.prompt_tokens ?? "--"} · 输出 ${review.usage.completion_tokens ?? "--"} · 总计 ${review.usage.total_tokens ?? "--"}`,
},
]
: []),
]);
} catch (reviewError) {
setAiError(String(reviewError));
prependAiLogs([
{
id: `error-${Date.now()}`,
time: formatLogTime(),
tone: "error",
title: isEn ? "V4 request failed" : "V4 请求失败",
detail: String(reviewError),
},
]);
} finally {
setAiLoading(false);
}
@@ -746,6 +869,43 @@ function ScanTerminalScreen() {
) : null}
</div>
</div>
{isPro ? (
<div className="scan-ai-log-panel">
<div className="scan-ai-log-head">
<strong>{isEn ? "V4 run log" : "V4 运行日志"}</strong>
<span>
{aiLoading
? isEn
? "Waiting for DeepSeek response"
: "正在等待 DeepSeek 返回"
: aiScan?.status
? `${aiScan.model || "V4"} · ${aiScan.status}`
: isEn
? "No V4 request yet"
: "还没有发起 V4 请求"}
</span>
</div>
<div className="scan-ai-log-list">
{aiLogs.length ? (
aiLogs.map((entry) => (
<div key={entry.id} className={`scan-ai-log-item ${entry.tone}`}>
<span className="scan-ai-log-time">{entry.time}</span>
<div>
<b>{entry.title}</b>
{entry.detail ? <small>{entry.detail}</small> : null}
</div>
</div>
))
) : (
<div className="scan-ai-log-empty">
{isEn
? "After a rule scan, click AI Deep Scan to see request and result logs here."
: "规则扫描完成后点击 AI 深度扫描,这里会显示请求和返回日志。"}
</div>
)}
</div>
</div>
) : null}
{scanStatus === "failed" && !terminalData ? (
<div className="scan-empty-state">
+17
View File
@@ -547,9 +547,26 @@ export interface PrimarySignal extends ScanOpportunityRow {}
export interface ScanAiReview {
status?: "ready" | "disabled" | "missing_key" | "failed" | "no_rows" | "no_snapshot" | "snapshot_mismatch" | string;
stage?: "completed" | "fallback" | string | null;
model?: string | null;
cached?: boolean;
generated_at?: string | null;
snapshot_id?: string | null;
input_rows?: number | null;
sent_rows?: number | null;
duration_ms?: number | null;
timeout_sec?: number | null;
cache_ttl_sec?: number | null;
provider?: string | null;
base_url?: string | null;
finish_reason?: string | null;
usage?: {
prompt_tokens?: number | null;
completion_tokens?: number | null;
total_tokens?: number | null;
prompt_cache_hit_tokens?: number | null;
prompt_cache_miss_tokens?: number | null;
} | null;
reason?: string | null;
summary_zh?: string | null;
summary_en?: string | null;