diff --git a/gateway.mjs b/gateway.mjs index 96c7602..44d2db2 100644 --- a/gateway.mjs +++ b/gateway.mjs @@ -1117,7 +1117,8 @@ function buildLogsSnapshot(monitor, sinceSeq = null) { }; } -async function buildPersistentLogsSnapshot(runtime, sinceSeq = null) { +async function buildPersistentLogsSnapshot(runtime, sinceSeq = null, limit = 500) { + const safeLimit = Number.isInteger(limit) && limit > 0 ? Math.min(limit, 1000) : 500; const text = runtime.logPath ? await readOptionalText(runtime.logPath) : null; if (!text) { return buildLogsSnapshot(runtime.monitor, sinceSeq); @@ -1129,7 +1130,7 @@ async function buildPersistentLogsSnapshot(runtime, sinceSeq = null) { .map((line, index) => parseLogLine(line, index + 1)); const entries = Number.isInteger(sinceSeq) ? allEntries.filter((entry) => entry.seq > sinceSeq) - : allEntries.slice(-500); + : allEntries.slice(-safeLimit); return { total_entries: allEntries.length, @@ -1945,10 +1946,16 @@ async function handleManagementRequest(runtime, req, res, requestUrl) { if (pathname === LOGS_API_PATH && req.method === "GET") { const sinceSeqRaw = requestUrl.searchParams.get("since_seq"); + const limitRaw = requestUrl.searchParams.get("limit"); const sinceSeq = sinceSeqRaw === null ? null : Number.parseInt(sinceSeqRaw, 10); + const limit = limitRaw === null ? 500 : Number.parseInt(limitRaw, 10); jsonResponse(res, 200, { ok: true, - ...await buildPersistentLogsSnapshot(runtime, Number.isInteger(sinceSeq) ? sinceSeq : null), + ...await buildPersistentLogsSnapshot( + runtime, + Number.isInteger(sinceSeq) ? sinceSeq : null, + Number.isInteger(limit) ? limit : 500, + ), }); return true; } diff --git a/ui-src/src/App.tsx b/ui-src/src/App.tsx index 5948c26..5dab092 100644 --- a/ui-src/src/App.tsx +++ b/ui-src/src/App.tsx @@ -1,4 +1,4 @@ -import { FormEvent, useEffect, useState } from "react"; +import { FormEvent, startTransition, useEffect, useState } from "react"; type PageKey = "overview" | "requests" | "profiles" | "rules" | "logs"; type Tone = "" | "success" | "error"; @@ -222,6 +222,20 @@ const api = { restore: "/__codex_retry_gateway/api/restore", }; +const REQUEST_PAGE_SIZE = 40; +const LOG_PAGE_SIZE = 200; + +const zhNumberFormatter = new Intl.NumberFormat("zh-CN"); +const zhTimestampFormatter = new Intl.DateTimeFormat("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23", +}); + type ProfileProbePayload = { ok: boolean; profile?: string; @@ -294,7 +308,7 @@ const defaultProfileForm: ProfileFormState = { function numberFormat(value: unknown) { return typeof value === "number" && Number.isFinite(value) - ? new Intl.NumberFormat("zh-CN").format(value) + ? zhNumberFormatter.format(value) : "-"; } @@ -303,7 +317,7 @@ function timestamp(value?: string | null) { return "-"; } const date = new Date(value); - return Number.isNaN(date.getTime()) ? value : date.toLocaleString("zh-CN", { hour12: false }); + return Number.isNaN(date.getTime()) ? value : zhTimestampFormatter.format(date); } function durationSeconds(value?: number | null) { @@ -482,6 +496,7 @@ export default function App() { }); const [status, setStatus] = useState(null); const [requests, setRequests] = useState([]); + const [requestsTotal, setRequestsTotal] = useState(0); const [requestsMeta, setRequestsMeta] = useState("正在读取请求记录..."); const [profiles, setProfiles] = useState([]); const [profilesMeta, setProfilesMeta] = useState("正在读取 profiles..."); @@ -490,6 +505,7 @@ export default function App() { const [latestLogSeq, setLatestLogSeq] = useState(0); const [requestQuery, setRequestQuery] = useState(""); const [requestFilter, setRequestFilter] = useState("all"); + const [requestLimit, setRequestLimit] = useState(REQUEST_PAGE_SIZE); const [ruleForm, setRuleForm] = useState(ruleFormFromStatus(null)); const [profileForm, setProfileForm] = useState(defaultProfileForm); const [ruleMessage, setRuleMessage] = useState({ text: "", tone: "" }); @@ -513,16 +529,18 @@ export default function App() { async function loadStatus(refreshRuleForm = false) { const payload = await fetchJson(api.status); - setStatus(payload); - if (refreshRuleForm) { - setRuleForm(ruleFormFromStatus(payload)); - setProfileForm(profileFormFromStatus(payload)); - } + startTransition(() => { + setStatus(payload); + if (refreshRuleForm) { + setRuleForm(ruleFormFromStatus(payload)); + setProfileForm(profileFormFromStatus(payload)); + } + }); } - async function loadRequests() { + async function loadRequests(limitOverride = requestLimit) { const url = new URL(api.requests, window.location.origin); - url.searchParams.set("limit", "100"); + url.searchParams.set("limit", String(limitOverride)); if (requestQuery.trim()) { url.searchParams.set("query", requestQuery.trim()); } @@ -531,36 +549,47 @@ export default function App() { } const payload = await fetchJson(url.toString()); const entries = payload.entries || []; - setRequests(entries); - setRequestsMeta(`查询命中 ${payload.total_entries ?? entries.length} 条,最新序号 ${payload.latest_seq ?? 0}。`); + startTransition(() => { + setRequests(entries); + setRequestsTotal(payload.total_entries ?? entries.length); + setRequestsMeta( + `查询命中 ${payload.total_entries ?? entries.length} 条,当前展示最近 ${entries.length} 条,最新序号 ${payload.latest_seq ?? 0}。`, + ); + }); } async function loadProfiles() { const payload = await fetchJson(api.profiles); const items = payload.profiles || []; - setProfiles(items); - setProfilesMeta(`目录:${payload.profiles_dir || "-"};当前运行:${payload.active_profile || "-"}。`); + startTransition(() => { + setProfiles(items); + setProfilesMeta(`目录:${payload.profiles_dir || "-"};当前运行:${payload.active_profile || "-"}。`); + }); } async function loadLogs(incremental = false) { const url = new URL(api.logs, window.location.origin); if (incremental && latestLogSeq > 0) { url.searchParams.set("since_seq", String(latestLogSeq)); + } else { + url.searchParams.set("limit", String(LOG_PAGE_SIZE)); } const payload = await fetchJson(url.toString()); const rendered = (payload.entries || []) .map((entry) => `${entry.at || "-"} ${entry.message || ""}`) .join("\n"); - setLogs((current) => { - if (!incremental || latestLogSeq === 0) { - return rendered || "当前还没有日志。"; + startTransition(() => { + setLogs((current) => { + if (!incremental || latestLogSeq === 0) { + return rendered || "当前还没有日志。"; + } + return rendered ? `${current.trim()}\n${rendered}` : current; + }); + setLogsMeta(`已载入 ${payload.total_entries ?? payload.entries?.length ?? 0} 条日志,最新序号 ${payload.latest_seq ?? latestLogSeq}。`); + if (typeof payload.latest_seq === "number") { + setLatestLogSeq(payload.latest_seq); } - return rendered ? `${current.trim()}\n${rendered}` : current; }); - setLogsMeta(`已载入 ${payload.total_entries ?? payload.entries?.length ?? 0} 条日志,最新序号 ${payload.latest_seq ?? latestLogSeq}。`); - if (typeof payload.latest_seq === "number") { - setLatestLogSeq(payload.latest_seq); - } } async function loadPageData(targetPage: PageKey, { incrementalLogs = true } = {}) { @@ -617,12 +646,24 @@ export default function App() { useEffect(() => { const timer = window.setInterval(() => { - refreshLiveData().catch((error) => { + loadStatus(false).catch((error) => { + setRuleMessage({ text: error?.message || String(error), tone: "error" }); + }); + }, 10000); + return () => window.clearInterval(timer); + }, [restoreRequested, switchingTo]); + + useEffect(() => { + if (page !== "requests" && page !== "logs") { + return; + } + const timer = window.setInterval(() => { + loadPageData(page, { incrementalLogs: true }).catch((error) => { setRuleMessage({ text: error?.message || String(error), tone: "error" }); }); }, 2500); return () => window.clearInterval(timer); - }, [page, requestQuery, requestFilter, latestLogSeq, restoreRequested, switchingTo]); + }, [page, requestQuery, requestFilter, latestLogSeq, requestLimit]); useEffect(() => { if (page === "overview" || page === "rules") { @@ -676,17 +717,21 @@ export default function App() { } } + useEffect(() => { + setRequestLimit(REQUEST_PAGE_SIZE); + }, [requestQuery, requestFilter]); + useEffect(() => { if (page !== "requests") { return; } const timer = window.setTimeout(() => { - loadRequests().catch((error) => { + loadRequests(requestLimit).catch((error) => { setRuleMessage({ text: error?.message || String(error), tone: "error" }); }); }, 180); return () => window.clearTimeout(timer); - }, [page, requestQuery, requestFilter]); + }, [page, requestQuery, requestFilter, requestLimit]); async function saveProfile(event: FormEvent) { event.preventDefault(); @@ -987,6 +1032,14 @@ export default function App() { + } >