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:
208
electron/main/net-request.ts
Normal file
208
electron/main/net-request.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
// ============================================================
|
||||
// NetRequest — Electron net.request 的 axios 风格封装
|
||||
//
|
||||
// 用法:
|
||||
// import { request, get, post } from './net-request';
|
||||
// const { data } = await get<UpdateCheckResult>('/api/v1/update/check', {
|
||||
// baseURL: 'https://www.heixiu.com',
|
||||
// params: { platform: 'win32', version: '1.0.0' },
|
||||
// });
|
||||
//
|
||||
// 设计:
|
||||
// - 底层使用 Electron net.request(不引入额外依赖)
|
||||
// - Promise 封装,支持 async/await
|
||||
// - JSON 自动解析、查询参数自动拼接
|
||||
// - 流式下载场景仍直接用 net.request(本模块不封装 stream)
|
||||
// ============================================================
|
||||
|
||||
import { net } from 'electron';
|
||||
|
||||
// ---------- 类型 ----------
|
||||
|
||||
export interface RequestConfig {
|
||||
/** 请求方法(默认 GET) */
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
/** 基础 URL(可选,方便切换环境) */
|
||||
baseURL?: string;
|
||||
/** 查询参数(自动拼接 ?key=value&...) */
|
||||
params?: Record<string, string | number | undefined>;
|
||||
/** 请求头 */
|
||||
headers?: Record<string, string>;
|
||||
/** 请求体(对象自动 JSON.stringify,字符串原样发送) */
|
||||
data?: unknown;
|
||||
/** 响应类型(默认 json,设置 text 跳过解析) */
|
||||
responseType?: 'json' | 'text';
|
||||
/** 超时时间(毫秒,默认 30s) */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface RequestResponse<T = unknown> {
|
||||
/** HTTP 状态码 */
|
||||
status: number;
|
||||
/** 状态文本(如 "OK") */
|
||||
statusText: string;
|
||||
/** 响应头 */
|
||||
headers: Record<string, string | string[]>;
|
||||
/** 响应体 */
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** 网络请求错误 */
|
||||
export class RequestError extends Error {
|
||||
status?: number;
|
||||
constructor(message: string, status?: number) {
|
||||
super(message);
|
||||
this.name = 'NetRequestError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 工具 ----------
|
||||
|
||||
const DEFAULT_TIMEOUT = 30_000;
|
||||
|
||||
/** 拼接 URL(baseURL + path + query) */
|
||||
function buildUrl(url: string, config: RequestConfig): string {
|
||||
let fullUrl = url;
|
||||
|
||||
if (config.baseURL) {
|
||||
// url 以 / 开头时为绝对路径,否则拼接
|
||||
const base = config.baseURL.replace(/\/+$/, '');
|
||||
const path = url.startsWith('/') ? url : `/${url}`;
|
||||
fullUrl = `${base}${path}`;
|
||||
}
|
||||
|
||||
if (config.params) {
|
||||
const search = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(config.params)) {
|
||||
if (value !== undefined) {
|
||||
search.append(key, String(value));
|
||||
}
|
||||
}
|
||||
const qs = search.toString();
|
||||
if (qs) {
|
||||
fullUrl += (fullUrl.includes('?') ? '&' : '?') + qs;
|
||||
}
|
||||
}
|
||||
|
||||
return fullUrl;
|
||||
}
|
||||
|
||||
/** 序列化请求体 */
|
||||
function serializeBody(data: unknown): { body: string; contentType: string } {
|
||||
if (typeof data === 'string') {
|
||||
return { body: data, contentType: 'text/plain' };
|
||||
}
|
||||
return { body: JSON.stringify(data), contentType: 'application/json' };
|
||||
}
|
||||
|
||||
// ---------- 核心 ----------
|
||||
|
||||
/**
|
||||
* 发起请求(axios 风格 API)
|
||||
*
|
||||
* 流式下载场景请直接使用 Electron net.request(本模块不封装)
|
||||
*/
|
||||
export async function request<T = unknown>(
|
||||
url: string,
|
||||
config: RequestConfig = {},
|
||||
): Promise<RequestResponse<T>> {
|
||||
const fullUrl = buildUrl(url, config);
|
||||
const method = (config.method || 'GET').toUpperCase();
|
||||
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = net.request({
|
||||
method,
|
||||
url: fullUrl,
|
||||
});
|
||||
|
||||
// 超时处理
|
||||
let timedOut = false;
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
req.abort();
|
||||
reject(new RequestError(`请求超时 (${timeout}ms)`));
|
||||
}, timeout);
|
||||
|
||||
// 请求头
|
||||
if (config.headers) {
|
||||
for (const [key, value] of Object.entries(config.headers)) {
|
||||
req.setHeader(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// 请求体
|
||||
if (config.data !== undefined) {
|
||||
const { body, contentType } = serializeBody(config.data);
|
||||
req.setHeader('Content-Type', contentType);
|
||||
req.write(body);
|
||||
}
|
||||
|
||||
req.on('response', (response) => {
|
||||
clearTimeout(timer);
|
||||
|
||||
const status = response.statusCode;
|
||||
const statusText = response.statusMessage ?? '';
|
||||
|
||||
// 读取响应头
|
||||
const headers: Record<string, string | string[]> = {};
|
||||
// Electron IncomingMessage.headers 是 Record<string, string[]>
|
||||
const rawHeaders = response.headers;
|
||||
if (rawHeaders) {
|
||||
for (const [key, value] of Object.entries(rawHeaders)) {
|
||||
headers[key] = value.length === 1 ? value[0] : value;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取响应体
|
||||
let body = '';
|
||||
response.on('data', (chunk: Buffer) => (body += chunk.toString()));
|
||||
response.on('end', () => {
|
||||
try {
|
||||
const asJson = config.responseType !== 'text';
|
||||
const data = asJson ? JSON.parse(body) : body;
|
||||
resolve({ status, statusText, headers, data: data as T });
|
||||
} catch {
|
||||
// JSON 解析失败 → 返回原始文本
|
||||
resolve({ status, statusText, headers, data: body as T });
|
||||
}
|
||||
});
|
||||
|
||||
response.on('error', (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(new RequestError(`响应读取失败: ${err.message}`));
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
clearTimeout(timer);
|
||||
if (timedOut) return;
|
||||
reject(new RequestError(`网络请求失败: ${err.message}`));
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 默认导出:类 axios 命名空间 ----------
|
||||
|
||||
export const netRequest = {
|
||||
request,
|
||||
|
||||
get<T>(url: string, config?: Omit<RequestConfig, 'method' | 'data'>) {
|
||||
return request<T>(url, { ...config, method: 'GET' });
|
||||
},
|
||||
|
||||
post<T>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'data'>) {
|
||||
return request<T>(url, { ...config, method: 'POST', data });
|
||||
},
|
||||
|
||||
put<T>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'data'>) {
|
||||
return request<T>(url, { ...config, method: 'PUT', data });
|
||||
},
|
||||
|
||||
del<T>(url: string, config?: Omit<RequestConfig, 'method' | 'data'>) {
|
||||
return request<T>(url, { ...config, method: 'DELETE' });
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user