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:
2026-06-04 11:03:25 +08:00
parent 337234443a
commit ae733016bb
21 changed files with 1245 additions and 154 deletions

View File

@@ -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);
},
);