mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat(logs): enhance log fetching with incremental support using cursor and after parameters
This commit is contained in:
+56
-20
@@ -52,9 +52,27 @@ const MAX_BUFFER_LINES = 10000;
|
|||||||
const LONG_PRESS_MS = 650;
|
const LONG_PRESS_MS = 650;
|
||||||
const LONG_PRESS_MOVE_THRESHOLD = 10;
|
const LONG_PRESS_MOVE_THRESHOLD = 10;
|
||||||
|
|
||||||
const getIncrementalAfter = (cursor: LogsQuery['after']): LogsQuery['after'] => {
|
type LogPosition = Pick<LogsQuery, 'after' | 'cursor'>;
|
||||||
if (typeof cursor !== 'number') return cursor;
|
|
||||||
return cursor > 1 ? cursor - 1 : undefined;
|
const getIncrementalAfter = (after: LogsQuery['after']): LogsQuery['after'] => {
|
||||||
|
if (typeof after !== 'number') return after;
|
||||||
|
return after > 1 ? after - 1 : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLogsQuery = (incremental: boolean, position: LogPosition): LogsQuery => {
|
||||||
|
const params: LogsQuery = { limit: MAX_BUFFER_LINES };
|
||||||
|
if (!incremental) return params;
|
||||||
|
|
||||||
|
if (position.cursor) {
|
||||||
|
params.cursor = position.cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const after = getIncrementalAfter(position.after);
|
||||||
|
if (after !== undefined) {
|
||||||
|
params.after = after;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findLineOverlap = (currentLines: string[], incomingLines: string[]): number => {
|
const findLineOverlap = (currentLines: string[], incomingLines: string[]): number => {
|
||||||
@@ -174,8 +192,29 @@ export function LogsPage() {
|
|||||||
const logRequestInFlightRef = useRef(false);
|
const logRequestInFlightRef = useRef(false);
|
||||||
const pendingFullReloadRef = useRef(false);
|
const pendingFullReloadRef = useRef(false);
|
||||||
|
|
||||||
// 保存最新游标用于增量获取
|
// 保存最新游标用于增量获取;新 CPA 后端优先使用 cursor,旧接口和 Home 继续使用 after。
|
||||||
const latestCursorRef = useRef<LogsQuery['after']>(undefined);
|
const logPositionRef = useRef<LogPosition>({});
|
||||||
|
|
||||||
|
const resetLogPosition = () => {
|
||||||
|
logPositionRef.current = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLogPosition = (
|
||||||
|
data: Awaited<ReturnType<typeof logsApi.fetchLogs>>,
|
||||||
|
incremental: boolean
|
||||||
|
) => {
|
||||||
|
const currentPosition = logPositionRef.current;
|
||||||
|
const nextPosition: LogPosition = {};
|
||||||
|
if (data.nextCursor) {
|
||||||
|
nextPosition.cursor = data.nextCursor;
|
||||||
|
}
|
||||||
|
if (data.latestAfter !== undefined) {
|
||||||
|
nextPosition.after = data.latestAfter;
|
||||||
|
} else if (incremental && currentPosition.after !== undefined) {
|
||||||
|
nextPosition.after = currentPosition.after;
|
||||||
|
}
|
||||||
|
logPositionRef.current = nextPosition;
|
||||||
|
};
|
||||||
|
|
||||||
const disableControls = connectionStatus !== 'connected';
|
const disableControls = connectionStatus !== 'connected';
|
||||||
const refreshDisabled = disableControls || loading || cpaNeedsFileLogging;
|
const refreshDisabled = disableControls || loading || cpaNeedsFileLogging;
|
||||||
@@ -190,7 +229,7 @@ export function LogsPage() {
|
|||||||
|
|
||||||
if (cpaNeedsFileLogging) {
|
if (cpaNeedsFileLogging) {
|
||||||
if (!incremental) {
|
if (!incremental) {
|
||||||
latestCursorRef.current = undefined;
|
resetLogPosition();
|
||||||
requestLogHomeIpByIdRef.current = {};
|
requestLogHomeIpByIdRef.current = {};
|
||||||
setFileLoggingRequired(false);
|
setFileLoggingRequired(false);
|
||||||
setLogState({ buffer: [], visibleFrom: 0 });
|
setLogState({ buffer: [], visibleFrom: 0 });
|
||||||
@@ -222,19 +261,12 @@ export function LogsPage() {
|
|||||||
scrollerInstance?.requestScrollToBottom();
|
scrollerInstance?.requestScrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
const params: LogsQuery =
|
const params = buildLogsQuery(incremental, logPositionRef.current);
|
||||||
incremental && latestCursorRef.current
|
|
||||||
? { after: getIncrementalAfter(latestCursorRef.current), limit: MAX_BUFFER_LINES }
|
|
||||||
: { limit: MAX_BUFFER_LINES };
|
|
||||||
const data = await logsApi.fetchLogs(params);
|
const data = await logsApi.fetchLogs(params);
|
||||||
setFileLoggingRequired(false);
|
setFileLoggingRequired(false);
|
||||||
|
|
||||||
// 更新游标
|
updateLogPosition(data, incremental);
|
||||||
if (data.latestCursor) {
|
|
||||||
latestCursorRef.current = data.latestCursor;
|
|
||||||
} else if (!incremental) {
|
|
||||||
latestCursorRef.current = undefined;
|
|
||||||
}
|
|
||||||
if (data.requestLogHomeIpById) {
|
if (data.requestLogHomeIpById) {
|
||||||
requestLogHomeIpByIdRef.current = incremental
|
requestLogHomeIpByIdRef.current = incremental
|
||||||
? { ...requestLogHomeIpByIdRef.current, ...data.requestLogHomeIpById }
|
? { ...requestLogHomeIpByIdRef.current, ...data.requestLogHomeIpById }
|
||||||
@@ -245,7 +277,11 @@ export function LogsPage() {
|
|||||||
|
|
||||||
const newLines = Array.isArray(data.lines) ? data.lines : [];
|
const newLines = Array.isArray(data.lines) ? data.lines : [];
|
||||||
|
|
||||||
if (incremental && newLines.length > 0) {
|
if (incremental && data.cursorReset) {
|
||||||
|
const buffer = newLines.slice(-MAX_BUFFER_LINES);
|
||||||
|
const visibleFrom = Math.max(buffer.length - INITIAL_DISPLAY_LINES, 0);
|
||||||
|
setLogState({ buffer, visibleFrom });
|
||||||
|
} else if (incremental && newLines.length > 0) {
|
||||||
// 增量更新:追加新日志并限制缓冲区大小(避免内存与渲染膨胀)
|
// 增量更新:追加新日志并限制缓冲区大小(避免内存与渲染膨胀)
|
||||||
setLogState((prev) => {
|
setLogState((prev) => {
|
||||||
const prevRenderedCount = prev.buffer.length - prev.visibleFrom;
|
const prevRenderedCount = prev.buffer.length - prev.visibleFrom;
|
||||||
@@ -271,7 +307,7 @@ export function LogsPage() {
|
|||||||
console.error('Failed to load logs:', err);
|
console.error('Failed to load logs:', err);
|
||||||
if (isLoggingToFileDisabledError(err)) {
|
if (isLoggingToFileDisabledError(err)) {
|
||||||
if (!incremental) {
|
if (!incremental) {
|
||||||
latestCursorRef.current = undefined;
|
resetLogPosition();
|
||||||
requestLogHomeIpByIdRef.current = {};
|
requestLogHomeIpByIdRef.current = {};
|
||||||
setFileLoggingRequired(true);
|
setFileLoggingRequired(true);
|
||||||
setLogState({ buffer: [], visibleFrom: 0 });
|
setLogState({ buffer: [], visibleFrom: 0 });
|
||||||
@@ -318,7 +354,7 @@ export function LogsPage() {
|
|||||||
try {
|
try {
|
||||||
await logsApi.clearLogs();
|
await logsApi.clearLogs();
|
||||||
setLogState({ buffer: [], visibleFrom: 0 });
|
setLogState({ buffer: [], visibleFrom: 0 });
|
||||||
latestCursorRef.current = undefined;
|
resetLogPosition();
|
||||||
requestLogHomeIpByIdRef.current = {};
|
requestLogHomeIpByIdRef.current = {};
|
||||||
setFileLoggingRequired(false);
|
setFileLoggingRequired(false);
|
||||||
showNotification(t('logs.clear_success'), 'success');
|
showNotification(t('logs.clear_success'), 'success');
|
||||||
@@ -429,7 +465,7 @@ export function LogsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connectionStatus === 'connected') {
|
if (connectionStatus === 'connected') {
|
||||||
latestCursorRef.current = undefined;
|
resetLogPosition();
|
||||||
requestLogHomeIpByIdRef.current = {};
|
requestLogHomeIpByIdRef.current = {};
|
||||||
setFileLoggingRequired(false);
|
setFileLoggingRequired(false);
|
||||||
loadLogs(false);
|
loadLogs(false);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type LogBackendKind = 'unknown' | 'file' | 'home-db';
|
|||||||
|
|
||||||
export interface LogsQuery {
|
export interface LogsQuery {
|
||||||
after?: LogCursor;
|
after?: LogCursor;
|
||||||
|
cursor?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
}
|
}
|
||||||
@@ -42,7 +43,9 @@ export interface HomeLogsResponse {
|
|||||||
export interface LogsResponse {
|
export interface LogsResponse {
|
||||||
lines: string[];
|
lines: string[];
|
||||||
lineCount: number;
|
lineCount: number;
|
||||||
latestCursor?: LogCursor;
|
latestAfter?: LogCursor;
|
||||||
|
nextCursor?: string;
|
||||||
|
cursorReset?: boolean;
|
||||||
logBackendKind: LogBackendKind;
|
logBackendKind: LogBackendKind;
|
||||||
requestLogHomeIpById?: Record<string, string>;
|
requestLogHomeIpById?: Record<string, string>;
|
||||||
total?: number;
|
total?: number;
|
||||||
@@ -67,6 +70,9 @@ const numberValue = (value: unknown): number | undefined => {
|
|||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const booleanValue = (value: unknown): boolean =>
|
||||||
|
value === true || (typeof value === 'string' && value.trim().toLowerCase() === 'true');
|
||||||
|
|
||||||
const positiveNumberValue = (value: unknown): number | undefined => {
|
const positiveNumberValue = (value: unknown): number | undefined => {
|
||||||
const parsed = numberValue(value);
|
const parsed = numberValue(value);
|
||||||
return parsed !== undefined && parsed > 0 ? parsed : undefined;
|
return parsed !== undefined && parsed > 0 ? parsed : undefined;
|
||||||
@@ -104,8 +110,10 @@ const normalizeCPALogs = (data: Record<string, unknown>): LogsResponse => {
|
|||||||
return {
|
return {
|
||||||
lines,
|
lines,
|
||||||
lineCount: Number.isFinite(lineCount) ? lineCount : lines.length,
|
lineCount: Number.isFinite(lineCount) ? lineCount : lines.length,
|
||||||
latestCursor: latestTimestamp > 0 ? latestTimestamp : undefined,
|
latestAfter: latestTimestamp > 0 ? latestTimestamp : undefined,
|
||||||
logBackendKind: 'file'
|
nextCursor: stringValue(data['next-cursor']) || undefined,
|
||||||
|
cursorReset: booleanValue(data['cursor-reset']),
|
||||||
|
logBackendKind: 'file',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,12 +148,12 @@ const normalizeHomeLogs = (data: Record<string, unknown>): LogsResponse => {
|
|||||||
return {
|
return {
|
||||||
lines,
|
lines,
|
||||||
lineCount: Number.isFinite(total) ? total : lines.length,
|
lineCount: Number.isFinite(total) ? total : lines.length,
|
||||||
latestCursor,
|
latestAfter: latestCursor,
|
||||||
logBackendKind: 'home-db',
|
logBackendKind: 'home-db',
|
||||||
requestLogHomeIpById,
|
requestLogHomeIpById,
|
||||||
total: Number.isFinite(total) ? total : undefined,
|
total: Number.isFinite(total) ? total : undefined,
|
||||||
limit: Number.isFinite(limit) ? limit : undefined,
|
limit: Number.isFinite(limit) ? limit : undefined,
|
||||||
offset: Number.isFinite(offset) ? offset : undefined
|
offset: Number.isFinite(offset) ? offset : undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user