feat: 日志模块 + netRequest 封装 + PROJECT.md 更新
日志模块(文件持久化 + 事件总线联动 + 脱敏)
新增:
- shared/types/logging.ts — LogLevel / LogCategory / LogEntry 类型
- electron/main/logger.ts — 主进程日志核心(JSON Lines / 每日轮转 / 7天清理)
- electron/main/log-ipc.ts — 渲染进程日志 IPC 通道接收
- src/utils/logger.ts — 渲染进程日志器(IPC 转发 / DEV 控制台 / 事件总线钩子)
- shared/utils/sanitize.ts — 脱敏工具(ID哈希 / 邮箱 / 手机 / 名称 / Token)
修改:
- electron/main.ts — initLogger + flushLogger + uncaughtException
- src/main.tsx — renderer 全局错误捕获
- src/utils/event-bus.ts — setEventLogListener 钩子,emit 自动审计
- src/services/request.ts — 拦截器结构化错误分级记录
- electron/main/updater.ts — 14处 console → logger 迁移
- src/components/AppProvider.tsx — 登录/登出/自动登录 auth 事件日志
- shared/constants/ipc-channels.ts — 新增 LOG_MESSAGE 通道
netRequest 封装(Electron net.request 的 axios 风格 API)
新增:
- electron/main/net-request.ts — netRequest.get/post/put/del 命名空间
修改:
- electron/main/updater.ts — fetchUpdateInfo 改用 netRequest(22→12行)
文档更新
- PROJECT.md — 目录结构 / 状态管理 / 日志系统 / IPC 章节同步现状
- TODO.md — 增量更新改造 + 日志模块待办
- UpdateA.md — API blockmap 字段 / 数据库字段说明
This commit is contained in:
@@ -18,6 +18,7 @@ import axios, {
|
||||
import type { ApiResponse } from './types';
|
||||
import { RequestError, RequestErrorType, RefreshError, isRefreshError } from './types';
|
||||
import { emit, EVENTS } from '../utils/event-bus';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// ---------- 配置 ----------
|
||||
|
||||
@@ -187,6 +188,13 @@ instance.interceptors.response.use(
|
||||
return handle401(response.config);
|
||||
}
|
||||
|
||||
logger.warn('request', `Business error: ${data.message || '请求失败'}`, undefined, {
|
||||
url: response.config.url,
|
||||
method: response.config.method?.toUpperCase(),
|
||||
code: data.code,
|
||||
traceId: data.traceId,
|
||||
});
|
||||
|
||||
return Promise.reject(
|
||||
new RequestError(RequestErrorType.BUSINESS, data.message || '请求失败', {
|
||||
code: data.code,
|
||||
@@ -199,8 +207,19 @@ instance.interceptors.response.use(
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// 全局捕获 RefreshError → 自动触发登录 Modal,直接返回不继续走后续判断
|
||||
// 提取请求上下文(所有日志共用)
|
||||
const reqCtx = {
|
||||
url: error.config?.url,
|
||||
method: error.config?.method?.toUpperCase(),
|
||||
};
|
||||
|
||||
// 全局捕获 RefreshError → 自动触发登录 Modal
|
||||
if (isRefreshError(error)) {
|
||||
logger.debug('request', 'Auth expired (RefreshError caught)', {
|
||||
...reqCtx,
|
||||
errorType: error.name,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
clearToken();
|
||||
emit(EVENTS.AUTH_REQUIRED);
|
||||
return Promise.reject(error);
|
||||
@@ -208,26 +227,37 @@ instance.interceptors.response.use(
|
||||
|
||||
// 请求已被取消
|
||||
if (axios.isCancel(error)) {
|
||||
return Promise.reject(new RequestError(RequestErrorType.CANCELLED, '请求已取消'));
|
||||
const cancelErr = new RequestError(RequestErrorType.CANCELLED, '请求已取消');
|
||||
logger.debug('request', 'Request cancelled', reqCtx);
|
||||
return Promise.reject(cancelErr);
|
||||
}
|
||||
|
||||
// 超时
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return Promise.reject(new RequestError(RequestErrorType.TIMEOUT, '请求超时,请检查网络'));
|
||||
const timeoutErr = new RequestError(RequestErrorType.TIMEOUT, '请求超时,请检查网络');
|
||||
logger.warn('request', 'Request timeout', timeoutErr, reqCtx);
|
||||
return Promise.reject(timeoutErr);
|
||||
}
|
||||
|
||||
// 无网络
|
||||
if (!error.response) {
|
||||
return Promise.reject(new RequestError(RequestErrorType.NETWORK, '网络异常,请检查连接'));
|
||||
const netErr = new RequestError(RequestErrorType.NETWORK, '网络异常,请检查连接');
|
||||
logger.warn('request', 'Network error', netErr, reqCtx);
|
||||
return Promise.reject(netErr);
|
||||
}
|
||||
|
||||
// HTTP 状态码错误
|
||||
const { status, data } = error.response;
|
||||
|
||||
// 401 → 尝试刷新 token 并重试(但 refresh 接口自身 401 必须直接拒绝,否则死循环)
|
||||
// 401 → 尝试刷新 token 并重试(refresh 接口自身 401 直接拒绝,防死循环)
|
||||
if (status === 401 && error.config) {
|
||||
if (isRefreshRequest(error.config)) {
|
||||
return Promise.reject(new RefreshError());
|
||||
const refreshErr = new RefreshError();
|
||||
logger.debug('request', 'Refresh endpoint returned 401', {
|
||||
...reqCtx,
|
||||
errorType: refreshErr.name,
|
||||
});
|
||||
return Promise.reject(refreshErr);
|
||||
}
|
||||
return handle401(error.config);
|
||||
}
|
||||
@@ -255,12 +285,19 @@ instance.interceptors.response.use(
|
||||
break;
|
||||
}
|
||||
|
||||
return Promise.reject(
|
||||
new RequestError(RequestErrorType.HTTP, data?.message || errMsg, {
|
||||
httpStatus: status,
|
||||
code: data?.code,
|
||||
}),
|
||||
);
|
||||
const httpErr = new RequestError(RequestErrorType.HTTP, data?.message || errMsg, {
|
||||
httpStatus: status,
|
||||
code: data?.code,
|
||||
});
|
||||
|
||||
// HTTP 5xx → error 级别,4xx → warn 级别
|
||||
if (status >= 500) {
|
||||
logger.error('request', `HTTP ${status}: ${errMsg}`, httpErr, reqCtx);
|
||||
} else {
|
||||
logger.warn('request', `HTTP ${status}: ${errMsg}`, httpErr, reqCtx);
|
||||
}
|
||||
|
||||
return Promise.reject(httpErr);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user