Files
ele-HeiXiu/shared/utils/sanitize.ts
YoungestSongMo ae733016bb 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 字段 / 数据库字段说明
2026-06-04 11:03:25 +08:00

183 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 日志脱敏工具 — 主进程 + 渲染进程共用
//
// 设计:在 createEntry 阶段统一对 context 字段脱敏,调用方无感知
//
// 脱敏策略(按字段名模式匹配,大小写不敏感):
// *token* / *secret* / *password* / *pwd* → [REDACTED]
// *email* → 保留首字符 + 域名t***@qq.com
// *phone* / *mobile* / *tel* → 保留首 3 尾 4138****5678
// *id / *_id / userId / user_id → SHA256 前 8 位(可追溯但不可读)
// *name / username / display_name → 保留首字符 + ***
// *balance* / *money* / *amount* → 数值替换为 [FILTERED]
// 其他字段 → 原样保留
// ============================================================
import type { LogEntry } from '@shared/types';
// ---------- 脱敏函数 ----------
/** 通过哈希缩短(用于 ID 类字段,可追溯不可读) */
export function hashShort(value: string): string {
// 简易哈希(避免引入 crypto 模块,双端兼容)
let h = 0;
for (let i = 0; i < value.length; i++) {
h = (Math.imul(31, h) + value.charCodeAt(i)) | 0;
}
// 转 hex 前缀,补零到 8 位
const hex = (h >>> 0).toString(16).padStart(8, '0');
return `#${hex}`;
}
/** 邮箱脱敏:首字符 + ***@域名 */
export function sanitizeEmail(value: string): string {
const atIndex = value.indexOf('@');
if (atIndex <= 0) return `${value[0]}***`;
return `${value[0]}***@${value.slice(atIndex + 1)}`;
}
/** 手机号脱敏:保留前 3 后 4 */
export function sanitizePhone(value: string): string {
const digits = value.replace(/\D/g, '');
if (digits.length < 7) return `${digits.slice(0, 3)}****`;
return `${digits.slice(0, 3)}****${digits.slice(-4)}`;
}
/** 名称脱敏:保留首字符 + *** */
export function sanitizeName(value: string): string {
if (!value || value.length <= 1) return '***';
return `${value[0]}***`;
}
/** 判断字段名是否匹配某个模式 */
function matchKey(key: string, patterns: string[]): boolean {
const lower = key.toLowerCase();
return patterns.some((p) => {
if (p.startsWith('*') && p.endsWith('*')) return lower.includes(p.slice(1, -1));
if (p.endsWith('*')) return lower.startsWith(p.slice(0, -1));
if (p.startsWith('*')) return lower.endsWith(p.slice(1));
return lower === p;
});
}
/** 对单个值脱敏(递归处理嵌套对象) */
function sanitizeValue(key: string, value: unknown): unknown {
if (value === null || value === undefined) return value;
// 完全隐藏
if (matchKey(key, ['*token*', '*secret*', '*password*', '*pwd*', 'authorization'])) {
return '[REDACTED]';
}
// 邮箱
if (
matchKey(key, ['*email*']) &&
typeof value === 'string' &&
value.includes('@')
) {
return sanitizeEmail(value);
}
// 手机号
if (matchKey(key, ['*phone*', '*mobile*', '*tel*']) && typeof value === 'string') {
return sanitizePhone(value);
}
// ID 类字段(数字或字符串)
if (
matchKey(key, ['*id', '*_id', 'userId', 'user_id', 'company_id']) &&
(typeof value === 'string' || typeof value === 'number')
) {
return hashShort(String(value));
}
// 名称
if (
matchKey(key, ['*name', 'username', 'display_name', 'real_name']) &&
typeof value === 'string'
) {
return sanitizeName(value);
}
// 金额 / 余额
if (matchKey(key, ['*balance*', '*money*', '*amount*', '*cent*', '*fee*'])) {
return '[FILTERED]';
}
// 递归处理嵌套对象
if (typeof value === 'object' && !Array.isArray(value)) {
return sanitizeRecord(value as Record<string, unknown>);
}
// 数组:逐个脱敏
if (Array.isArray(value)) {
return value.map((v) => sanitizeValue(key, v));
}
return value;
}
/** 脱敏整个 Record */
function sanitizeRecord(record: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(record)) {
result[key] = sanitizeValue(key, value);
}
return result;
}
// ---------- 对外 API ----------
/**
* 脱敏任意对象(返回新对象,不修改原对象)
*
* 用法:
* import { sanitize } from '@shared/utils/sanitize';
* const safe = sanitize({ userId: 42, email: 'test@qq.com', phone: '13812345678' });
* // → { userId: "#a1b2c3d4", email: "t***@qq.com", phone: "138****5678" }
*/
export function sanitize(record: Record<string, unknown>): Record<string, unknown> {
return sanitizeRecord(record);
}
/**
* 对 LogEntry 脱敏(原地不修改,返回新对象)
* 内部也调用了 sanitizeRecord
*/
export function sanitizeLogEntry(entry: LogEntry): LogEntry {
const sanitized = { ...entry };
// 脱敏 context
if (sanitized.context && Object.keys(sanitized.context).length > 0) {
sanitized.context = sanitizeRecord(sanitized.context);
}
// 脱敏 error 中的 message可能含用户信息如 "User xxx not found"
if (sanitized.error) {
sanitized.error = { ...sanitized.error };
// 对 error message 做基础脱敏(邮箱/手机号)
if (sanitized.error.message) {
sanitized.error.message = sanitizeInlineText(sanitized.error.message);
}
}
// 脱敏 message 字段本身
sanitized.message = sanitizeInlineText(sanitized.message);
return sanitized;
}
/**
* 对任意文本中的敏感信息做内联脱敏
* 使用正则匹配邮箱和手机号模式
*/
export function sanitizeInlineText(text: string): string {
// 邮箱(简单模式)
let result = text.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, (match) =>
sanitizeEmail(match),
);
// 手机号(中国大陆 11 位)
result = result.replace(/\b1[3-9]\d{9}\b/g, (match) => sanitizePhone(match));
return result;
}