feat(devtools): 添加生产环境调试工具系统
新增调试工具目录 src/devtools/,提供完整的生产环境调试能力: - apiDebugger: 拦截所有 API 请求/响应,记录日志 - notificationDebugger: 测试浏览器通知,检查权限 - socketDebugger: 监听所有 Socket 事件,诊断连接状态 - 全局 API: window.__DEBUG__ 提供便捷的控制台调试命令 功能特性: - 环境变量控制:REACT_APP_ENABLE_DEBUG=true 开启 - 动态导入:不影响生产环境性能 - 完整诊断:diagnose()、performance()、exportAll() - 易于移除:所有代码集中在 src/devtools/ 目录 Webpack 配置: - 添加 'debug' alias 强制解析到 node_modules/debug - 添加 @devtools alias 简化导入路径 - 避免与 npm debug 包的命名冲突 🔧 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,15 @@ NODE_ENV=production
|
|||||||
# Mock 配置(生产环境禁用 Mock)
|
# Mock 配置(生产环境禁用 Mock)
|
||||||
REACT_APP_ENABLE_MOCK=false
|
REACT_APP_ENABLE_MOCK=false
|
||||||
|
|
||||||
|
# 🔧 调试模式(生产环境临时调试用)
|
||||||
|
# 开启后会在全局暴露 window.__DEBUG__ 调试 API
|
||||||
|
# ⚠️ 警告: 调试模式会记录所有 API 请求/响应,调试完成后请立即关闭!
|
||||||
|
# 使用方法:
|
||||||
|
# 1. 设置为 true 并重新构建
|
||||||
|
# 2. 在浏览器控制台使用 window.__DEBUG__.help() 查看命令
|
||||||
|
# 3. 调试完成后设置为 false 并重新构建
|
||||||
|
REACT_APP_ENABLE_DEBUG=false
|
||||||
|
|
||||||
# 后端 API 地址(生产环境)
|
# 后端 API 地址(生产环境)
|
||||||
REACT_APP_API_URL=http://49.232.185.254:5001
|
REACT_APP_API_URL=http://49.232.185.254:5001
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ module.exports = {
|
|||||||
...webpackConfig.resolve,
|
...webpackConfig.resolve,
|
||||||
alias: {
|
alias: {
|
||||||
...webpackConfig.resolve.alias,
|
...webpackConfig.resolve.alias,
|
||||||
|
// 强制 'debug' 模块解析到 node_modules(避免与 src/devtools/ 冲突)
|
||||||
|
'debug': path.resolve(__dirname, 'node_modules/debug'),
|
||||||
|
|
||||||
// 根目录别名
|
// 根目录别名
|
||||||
'@': path.resolve(__dirname, 'src'),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
|
||||||
@@ -119,6 +122,7 @@ module.exports = {
|
|||||||
'@constants': path.resolve(__dirname, 'src/constants'),
|
'@constants': path.resolve(__dirname, 'src/constants'),
|
||||||
'@contexts': path.resolve(__dirname, 'src/contexts'),
|
'@contexts': path.resolve(__dirname, 'src/contexts'),
|
||||||
'@data': path.resolve(__dirname, 'src/data'),
|
'@data': path.resolve(__dirname, 'src/data'),
|
||||||
|
'@devtools': path.resolve(__dirname, 'src/devtools'),
|
||||||
'@hooks': path.resolve(__dirname, 'src/hooks'),
|
'@hooks': path.resolve(__dirname, 'src/hooks'),
|
||||||
'@layouts': path.resolve(__dirname, 'src/layouts'),
|
'@layouts': path.resolve(__dirname, 'src/layouts'),
|
||||||
'@lib': path.resolve(__dirname, 'src/lib'),
|
'@lib': path.resolve(__dirname, 'src/lib'),
|
||||||
@@ -263,6 +267,34 @@ module.exports = {
|
|||||||
logLevel: 'debug',
|
logLevel: 'debug',
|
||||||
pathRewrite: { '^/concept-api': '' },
|
pathRewrite: { '^/concept-api': '' },
|
||||||
},
|
},
|
||||||
|
'/bytedesk-api': {
|
||||||
|
target: 'http://43.143.189.195',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
logLevel: 'debug',
|
||||||
|
pathRewrite: { '^/bytedesk-api': '' },
|
||||||
|
},
|
||||||
|
'/chat': {
|
||||||
|
target: 'http://43.143.189.195',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
logLevel: 'debug',
|
||||||
|
// 不需要pathRewrite,保留/chat路径
|
||||||
|
},
|
||||||
|
'/config': {
|
||||||
|
target: 'http://43.143.189.195',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
logLevel: 'debug',
|
||||||
|
// 不需要pathRewrite,保留/config路径
|
||||||
|
},
|
||||||
|
'/visitor': {
|
||||||
|
target: 'http://43.143.189.195',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
logLevel: 'debug',
|
||||||
|
// 不需要pathRewrite,保留/visitor路径
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
253
src/devtools/apiDebugger.js
Normal file
253
src/devtools/apiDebugger.js
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
// src/debug/apiDebugger.js
|
||||||
|
/**
|
||||||
|
* API 调试工具
|
||||||
|
* 生产环境临时调试使用,后期可整体删除 src/debug/ 目录
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
|
||||||
|
class ApiDebugger {
|
||||||
|
constructor() {
|
||||||
|
this.requestLog = [];
|
||||||
|
this.maxLogSize = 100;
|
||||||
|
this.isLogging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 Axios 拦截器
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
// 请求拦截器
|
||||||
|
axios.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
if (this.isLogging) {
|
||||||
|
const logEntry = {
|
||||||
|
type: 'request',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
method: config.method.toUpperCase(),
|
||||||
|
url: config.url,
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
fullURL: this._getFullURL(config),
|
||||||
|
headers: config.headers,
|
||||||
|
data: config.data,
|
||||||
|
params: config.params,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._addLog(logEntry);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`%c[API Request] ${logEntry.method} ${logEntry.fullURL}`,
|
||||||
|
'color: #2196F3; font-weight: bold;',
|
||||||
|
{
|
||||||
|
headers: config.headers,
|
||||||
|
data: config.data,
|
||||||
|
params: config.params,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('[API Request Error]', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
if (this.isLogging) {
|
||||||
|
const logEntry = {
|
||||||
|
type: 'response',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
method: response.config.method.toUpperCase(),
|
||||||
|
url: response.config.url,
|
||||||
|
fullURL: this._getFullURL(response.config),
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: response.headers,
|
||||||
|
data: response.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._addLog(logEntry);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`%c[API Response] ${logEntry.method} ${logEntry.fullURL} - ${logEntry.status}`,
|
||||||
|
'color: #4CAF50; font-weight: bold;',
|
||||||
|
{
|
||||||
|
status: response.status,
|
||||||
|
data: response.data,
|
||||||
|
headers: response.headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (this.isLogging) {
|
||||||
|
const logEntry = {
|
||||||
|
type: 'error',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
method: error.config?.method?.toUpperCase() || 'UNKNOWN',
|
||||||
|
url: error.config?.url || 'UNKNOWN',
|
||||||
|
fullURL: error.config ? this._getFullURL(error.config) : 'UNKNOWN',
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
message: error.message,
|
||||||
|
data: error.response?.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._addLog(logEntry);
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`%c[API Error] ${logEntry.method} ${logEntry.fullURL}`,
|
||||||
|
'color: #F44336; font-weight: bold;',
|
||||||
|
{
|
||||||
|
status: error.response?.status,
|
||||||
|
message: error.message,
|
||||||
|
data: error.response?.data,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('%c[API Debugger] Initialized', 'color: #FF9800; font-weight: bold;');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取完整 URL
|
||||||
|
*/
|
||||||
|
_getFullURL(config) {
|
||||||
|
const baseURL = config.baseURL || '';
|
||||||
|
const url = config.url || '';
|
||||||
|
const fullURL = baseURL + url;
|
||||||
|
|
||||||
|
// 添加查询参数
|
||||||
|
if (config.params) {
|
||||||
|
const params = new URLSearchParams(config.params).toString();
|
||||||
|
return params ? `${fullURL}?${params}` : fullURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加日志
|
||||||
|
*/
|
||||||
|
_addLog(entry) {
|
||||||
|
this.requestLog.unshift(entry);
|
||||||
|
if (this.requestLog.length > this.maxLogSize) {
|
||||||
|
this.requestLog = this.requestLog.slice(0, this.maxLogSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有日志
|
||||||
|
*/
|
||||||
|
getLogs(type = 'all') {
|
||||||
|
if (type === 'all') {
|
||||||
|
return this.requestLog;
|
||||||
|
}
|
||||||
|
return this.requestLog.filter((log) => log.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
clearLogs() {
|
||||||
|
this.requestLog = [];
|
||||||
|
console.log('[API Debugger] Logs cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出日志为 JSON
|
||||||
|
*/
|
||||||
|
exportLogs() {
|
||||||
|
const blob = new Blob([JSON.stringify(this.requestLog, null, 2)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `api-logs-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
console.log('[API Debugger] Logs exported');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印日志统计
|
||||||
|
*/
|
||||||
|
printStats() {
|
||||||
|
const stats = {
|
||||||
|
total: this.requestLog.length,
|
||||||
|
requests: this.requestLog.filter((log) => log.type === 'request').length,
|
||||||
|
responses: this.requestLog.filter((log) => log.type === 'response').length,
|
||||||
|
errors: this.requestLog.filter((log) => log.type === 'error').length,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.table(stats);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动发送 API 请求(测试用)
|
||||||
|
*/
|
||||||
|
async testRequest(method, endpoint, data = null, config = {}) {
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
const url = `${apiBase}${endpoint}`;
|
||||||
|
|
||||||
|
console.log(`[API Debugger] Testing ${method.toUpperCase()} ${url}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[API Debugger] Test succeeded:', response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API Debugger] Test failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开启/关闭日志记录
|
||||||
|
*/
|
||||||
|
toggleLogging(enabled) {
|
||||||
|
this.isLogging = enabled;
|
||||||
|
console.log(`[API Debugger] Logging ${enabled ? 'enabled' : 'disabled'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近的错误
|
||||||
|
*/
|
||||||
|
getRecentErrors(count = 10) {
|
||||||
|
return this.requestLog.filter((log) => log.type === 'error').slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 URL 过滤日志
|
||||||
|
*/
|
||||||
|
getLogsByURL(urlPattern) {
|
||||||
|
return this.requestLog.filter((log) => log.url && log.url.includes(urlPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按状态码过滤日志
|
||||||
|
*/
|
||||||
|
getLogsByStatus(status) {
|
||||||
|
return this.requestLog.filter((log) => log.status === status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const apiDebugger = new ApiDebugger();
|
||||||
|
export default apiDebugger;
|
||||||
268
src/devtools/index.js
Normal file
268
src/devtools/index.js
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
// src/debug/index.js
|
||||||
|
/**
|
||||||
|
* 调试工具统一入口
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* 1. 开启调试: 在 .env.production 中设置 REACT_APP_ENABLE_DEBUG=true
|
||||||
|
* 2. 使用控制台命令: window.__DEBUG__.api.getLogs()
|
||||||
|
* 3. 后期移除: 删除整个 src/debug/ 目录 + 从 src/index.js 移除导入
|
||||||
|
*
|
||||||
|
* 全局 API:
|
||||||
|
* - window.__DEBUG__ - 调试 API 主对象
|
||||||
|
* - window.__DEBUG__.api - API 调试工具
|
||||||
|
* - window.__DEBUG__.notification - 通知调试工具
|
||||||
|
* - window.__DEBUG__.socket - Socket 调试工具
|
||||||
|
* - window.__DEBUG__.help() - 显示帮助信息
|
||||||
|
* - window.__DEBUG__.exportAll() - 导出所有日志
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiDebugger } from './apiDebugger';
|
||||||
|
import { notificationDebugger } from './notificationDebugger';
|
||||||
|
import { socketDebugger } from './socketDebugger';
|
||||||
|
|
||||||
|
class DebugToolkit {
|
||||||
|
constructor() {
|
||||||
|
this.api = apiDebugger;
|
||||||
|
this.notification = notificationDebugger;
|
||||||
|
this.socket = socketDebugger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化所有调试工具
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
console.log(
|
||||||
|
'%c╔════════════════════════════════════════════════════════════════╗',
|
||||||
|
'color: #FF9800; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'%c║ 🔧 调试模式已启用 (Debug Mode Enabled) ║',
|
||||||
|
'color: #FF9800; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'%c╚════════════════════════════════════════════════════════════════╝',
|
||||||
|
'color: #FF9800; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 初始化各个调试工具
|
||||||
|
this.api.init();
|
||||||
|
this.notification.init();
|
||||||
|
this.socket.init();
|
||||||
|
|
||||||
|
// 暴露到全局
|
||||||
|
window.__DEBUG__ = this;
|
||||||
|
|
||||||
|
// 打印帮助信息
|
||||||
|
this._printWelcome();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印欢迎信息
|
||||||
|
*/
|
||||||
|
_printWelcome() {
|
||||||
|
console.log('%c📚 调试工具使用指南:', 'color: #2196F3; font-weight: bold; font-size: 14px;');
|
||||||
|
console.log('');
|
||||||
|
console.log('%c1️⃣ API 调试:', 'color: #2196F3; font-weight: bold;');
|
||||||
|
console.log(' __DEBUG__.api.getLogs() - 获取所有 API 日志');
|
||||||
|
console.log(' __DEBUG__.api.getRecentErrors() - 获取最近的错误');
|
||||||
|
console.log(' __DEBUG__.api.exportLogs() - 导出 API 日志');
|
||||||
|
console.log(' __DEBUG__.api.testRequest(method, endpoint, data) - 测试 API 请求');
|
||||||
|
console.log('');
|
||||||
|
console.log('%c2️⃣ 通知调试:', 'color: #9C27B0; font-weight: bold;');
|
||||||
|
console.log(' __DEBUG__.notification.getLogs() - 获取所有通知日志');
|
||||||
|
console.log(' __DEBUG__.notification.forceNotification() - 发送测试通知');
|
||||||
|
console.log(' __DEBUG__.notification.checkPermission() - 检查通知权限');
|
||||||
|
console.log(' __DEBUG__.notification.exportLogs() - 导出通知日志');
|
||||||
|
console.log('');
|
||||||
|
console.log('%c3️⃣ Socket 调试:', 'color: #00BCD4; font-weight: bold;');
|
||||||
|
console.log(' __DEBUG__.socket.getLogs() - 获取所有 Socket 日志');
|
||||||
|
console.log(' __DEBUG__.socket.getStatus() - 获取连接状态');
|
||||||
|
console.log(' __DEBUG__.socket.reconnect() - 手动重连');
|
||||||
|
console.log(' __DEBUG__.socket.exportLogs() - 导出 Socket 日志');
|
||||||
|
console.log('');
|
||||||
|
console.log('%c4️⃣ 通用命令:', 'color: #4CAF50; font-weight: bold;');
|
||||||
|
console.log(' __DEBUG__.help() - 显示帮助信息');
|
||||||
|
console.log(' __DEBUG__.exportAll() - 导出所有日志');
|
||||||
|
console.log(' __DEBUG__.printStats() - 打印所有统计信息');
|
||||||
|
console.log(' __DEBUG__.clearAll() - 清空所有日志');
|
||||||
|
console.log('');
|
||||||
|
console.log(
|
||||||
|
'%c⚠️ 警告: 调试模式会记录所有 API 请求和响应,请勿在生产环境长期开启!',
|
||||||
|
'color: #F44336; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示帮助信息
|
||||||
|
*/
|
||||||
|
help() {
|
||||||
|
this._printWelcome();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出所有日志
|
||||||
|
*/
|
||||||
|
exportAll() {
|
||||||
|
console.log('[Debug Toolkit] Exporting all logs...');
|
||||||
|
|
||||||
|
const allLogs = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
api: this.api.getLogs(),
|
||||||
|
notification: this.notification.getLogs(),
|
||||||
|
socket: this.socket.getLogs(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(allLogs, null, 2)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `debug-all-logs-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
console.log('[Debug Toolkit] ✅ All logs exported');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印所有统计信息
|
||||||
|
*/
|
||||||
|
printStats() {
|
||||||
|
console.log('\n%c=== 📊 调试统计信息 ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
|
||||||
|
console.log('\n%c[API 统计]', 'color: #2196F3; font-weight: bold;');
|
||||||
|
const apiStats = this.api.printStats();
|
||||||
|
|
||||||
|
console.log('\n%c[通知统计]', 'color: #9C27B0; font-weight: bold;');
|
||||||
|
const notificationStats = this.notification.printStats();
|
||||||
|
|
||||||
|
console.log('\n%c[Socket 统计]', 'color: #00BCD4; font-weight: bold;');
|
||||||
|
const socketStats = this.socket.printStats();
|
||||||
|
|
||||||
|
return {
|
||||||
|
api: apiStats,
|
||||||
|
notification: notificationStats,
|
||||||
|
socket: socketStats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有日志
|
||||||
|
*/
|
||||||
|
clearAll() {
|
||||||
|
console.log('[Debug Toolkit] Clearing all logs...');
|
||||||
|
this.api.clearLogs();
|
||||||
|
this.notification.clearLogs();
|
||||||
|
this.socket.clearLogs();
|
||||||
|
console.log('[Debug Toolkit] ✅ All logs cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速诊断(检查所有系统状态)
|
||||||
|
*/
|
||||||
|
diagnose() {
|
||||||
|
console.log('\n%c=== 🔍 系统诊断 ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
|
||||||
|
|
||||||
|
// 1. Socket 状态
|
||||||
|
console.log('\n%c[1/3] Socket 状态', 'color: #00BCD4; font-weight: bold;');
|
||||||
|
const socketStatus = this.socket.getStatus();
|
||||||
|
|
||||||
|
// 2. 通知权限
|
||||||
|
console.log('\n%c[2/3] 通知权限', 'color: #9C27B0; font-weight: bold;');
|
||||||
|
const notificationStatus = this.notification.checkPermission();
|
||||||
|
|
||||||
|
// 3. API 错误
|
||||||
|
console.log('\n%c[3/3] 最近的 API 错误', 'color: #F44336; font-weight: bold;');
|
||||||
|
const recentErrors = this.api.getRecentErrors(5);
|
||||||
|
if (recentErrors.length > 0) {
|
||||||
|
console.table(
|
||||||
|
recentErrors.map((err) => ({
|
||||||
|
时间: err.timestamp,
|
||||||
|
方法: err.method,
|
||||||
|
URL: err.url,
|
||||||
|
状态码: err.status,
|
||||||
|
错误信息: err.message,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('✅ 没有 API 错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 汇总报告
|
||||||
|
const report = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
socket: socketStatus,
|
||||||
|
notification: notificationStatus,
|
||||||
|
apiErrors: recentErrors.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('\n%c=== 诊断报告 ===', 'color: #4CAF50; font-weight: bold;');
|
||||||
|
console.table(report);
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性能监控
|
||||||
|
*/
|
||||||
|
performance() {
|
||||||
|
console.log('\n%c=== ⚡ 性能监控 ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
|
||||||
|
|
||||||
|
// 计算 API 平均响应时间
|
||||||
|
const apiLogs = this.api.getLogs();
|
||||||
|
const responseTimes = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < apiLogs.length - 1; i++) {
|
||||||
|
const log = apiLogs[i];
|
||||||
|
const prevLog = apiLogs[i + 1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
log.type === 'response' &&
|
||||||
|
prevLog.type === 'request' &&
|
||||||
|
log.url === prevLog.url
|
||||||
|
) {
|
||||||
|
const responseTime =
|
||||||
|
new Date(log.timestamp).getTime() - new Date(prevLog.timestamp).getTime();
|
||||||
|
responseTimes.push({
|
||||||
|
url: log.url,
|
||||||
|
method: log.method,
|
||||||
|
time: responseTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseTimes.length > 0) {
|
||||||
|
const avgTime =
|
||||||
|
responseTimes.reduce((sum, item) => sum + item.time, 0) / responseTimes.length;
|
||||||
|
const maxTime = Math.max(...responseTimes.map((item) => item.time));
|
||||||
|
const minTime = Math.min(...responseTimes.map((item) => item.time));
|
||||||
|
|
||||||
|
console.log('API 响应时间统计:');
|
||||||
|
console.table({
|
||||||
|
平均响应时间: `${avgTime.toFixed(2)}ms`,
|
||||||
|
最快响应: `${minTime}ms`,
|
||||||
|
最慢响应: `${maxTime}ms`,
|
||||||
|
请求总数: responseTimes.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示最慢的 5 个请求
|
||||||
|
console.log('\n最慢的 5 个请求:');
|
||||||
|
const slowest = responseTimes.sort((a, b) => b.time - a.time).slice(0, 5);
|
||||||
|
console.table(
|
||||||
|
slowest.map((item) => ({
|
||||||
|
方法: item.method,
|
||||||
|
URL: item.url,
|
||||||
|
响应时间: `${item.time}ms`,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('暂无性能数据');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const debugToolkit = new DebugToolkit();
|
||||||
|
export default debugToolkit;
|
||||||
166
src/devtools/notificationDebugger.js
Normal file
166
src/devtools/notificationDebugger.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
// src/debug/notificationDebugger.js
|
||||||
|
/**
|
||||||
|
* 通知系统调试工具
|
||||||
|
* 扩展现有的 window.__NOTIFY_DEBUG__,添加更多生产环境调试能力
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { browserNotificationService } from '@services/browserNotificationService';
|
||||||
|
|
||||||
|
class NotificationDebugger {
|
||||||
|
constructor() {
|
||||||
|
this.eventLog = [];
|
||||||
|
this.maxLogSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化调试工具
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
console.log('%c[Notification Debugger] Initialized', 'color: #FF9800; font-weight: bold;');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录通知事件
|
||||||
|
*/
|
||||||
|
logEvent(eventType, data) {
|
||||||
|
const logEntry = {
|
||||||
|
type: eventType,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventLog.unshift(logEntry);
|
||||||
|
if (this.eventLog.length > this.maxLogSize) {
|
||||||
|
this.eventLog = this.eventLog.slice(0, this.maxLogSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`%c[Notification Event] ${eventType}`,
|
||||||
|
'color: #9C27B0; font-weight: bold;',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有事件日志
|
||||||
|
*/
|
||||||
|
getLogs() {
|
||||||
|
return this.eventLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
clearLogs() {
|
||||||
|
this.eventLog = [];
|
||||||
|
console.log('[Notification Debugger] Logs cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出日志
|
||||||
|
*/
|
||||||
|
exportLogs() {
|
||||||
|
const blob = new Blob([JSON.stringify(this.eventLog, null, 2)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `notification-logs-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
console.log('[Notification Debugger] Logs exported');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制发送浏览器通知(测试用)
|
||||||
|
*/
|
||||||
|
forceNotification(options = {}) {
|
||||||
|
const defaultOptions = {
|
||||||
|
title: '🧪 测试通知',
|
||||||
|
body: `测试时间: ${new Date().toLocaleString()}`,
|
||||||
|
tag: `test_${Date.now()}`,
|
||||||
|
requireInteraction: false,
|
||||||
|
autoClose: 5000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalOptions = { ...defaultOptions, ...options };
|
||||||
|
|
||||||
|
console.log('[Notification Debugger] Sending test notification:', finalOptions);
|
||||||
|
|
||||||
|
const notification = browserNotificationService.sendNotification(finalOptions);
|
||||||
|
|
||||||
|
if (notification) {
|
||||||
|
console.log('[Notification Debugger] ✅ Notification sent successfully');
|
||||||
|
} else {
|
||||||
|
console.error('[Notification Debugger] ❌ Failed to send notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查通知权限状态
|
||||||
|
*/
|
||||||
|
checkPermission() {
|
||||||
|
const permission = browserNotificationService.getPermissionStatus();
|
||||||
|
const isSupported = browserNotificationService.isSupported();
|
||||||
|
|
||||||
|
const status = {
|
||||||
|
supported: isSupported,
|
||||||
|
permission,
|
||||||
|
canSend: isSupported && permission === 'granted',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.table(status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求通知权限
|
||||||
|
*/
|
||||||
|
async requestPermission() {
|
||||||
|
console.log('[Notification Debugger] Requesting notification permission...');
|
||||||
|
const result = await browserNotificationService.requestPermission();
|
||||||
|
console.log(`[Notification Debugger] Permission result: ${result}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印事件统计
|
||||||
|
*/
|
||||||
|
printStats() {
|
||||||
|
const stats = {
|
||||||
|
total: this.eventLog.length,
|
||||||
|
byType: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventLog.forEach((log) => {
|
||||||
|
stats.byType[log.type] = (stats.byType[log.type] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== Notification Stats ===');
|
||||||
|
console.table(stats.byType);
|
||||||
|
console.log(`Total events: ${stats.total}`);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型过滤日志
|
||||||
|
*/
|
||||||
|
getLogsByType(eventType) {
|
||||||
|
return this.eventLog.filter((log) => log.type === eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近的事件
|
||||||
|
*/
|
||||||
|
getRecentEvents(count = 10) {
|
||||||
|
return this.eventLog.slice(0, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const notificationDebugger = new NotificationDebugger();
|
||||||
|
export default notificationDebugger;
|
||||||
194
src/devtools/socketDebugger.js
Normal file
194
src/devtools/socketDebugger.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// src/debug/socketDebugger.js
|
||||||
|
/**
|
||||||
|
* Socket 调试工具
|
||||||
|
* 扩展现有的 window.__SOCKET_DEBUG__,添加更多生产环境调试能力
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { socket } from '@services/socket';
|
||||||
|
|
||||||
|
class SocketDebugger {
|
||||||
|
constructor() {
|
||||||
|
this.eventLog = [];
|
||||||
|
this.maxLogSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化调试工具
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
// 监听所有 Socket 事件
|
||||||
|
this._attachEventListeners();
|
||||||
|
console.log('%c[Socket Debugger] Initialized', 'color: #FF9800; font-weight: bold;');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加事件监听器
|
||||||
|
*/
|
||||||
|
_attachEventListeners() {
|
||||||
|
const events = [
|
||||||
|
'connect',
|
||||||
|
'disconnect',
|
||||||
|
'connect_error',
|
||||||
|
'reconnect',
|
||||||
|
'reconnect_failed',
|
||||||
|
'new_event',
|
||||||
|
'system_notification',
|
||||||
|
];
|
||||||
|
|
||||||
|
events.forEach((event) => {
|
||||||
|
socket.on(event, (data) => {
|
||||||
|
this.logEvent(event, data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 Socket 事件
|
||||||
|
*/
|
||||||
|
logEvent(eventType, data) {
|
||||||
|
const logEntry = {
|
||||||
|
type: eventType,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventLog.unshift(logEntry);
|
||||||
|
if (this.eventLog.length > this.maxLogSize) {
|
||||||
|
this.eventLog = this.eventLog.slice(0, this.maxLogSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`%c[Socket Event] ${eventType}`,
|
||||||
|
'color: #00BCD4; font-weight: bold;',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有事件日志
|
||||||
|
*/
|
||||||
|
getLogs() {
|
||||||
|
return this.eventLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
clearLogs() {
|
||||||
|
this.eventLog = [];
|
||||||
|
console.log('[Socket Debugger] Logs cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出日志
|
||||||
|
*/
|
||||||
|
exportLogs() {
|
||||||
|
const blob = new Blob([JSON.stringify(this.eventLog, null, 2)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `socket-logs-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
console.log('[Socket Debugger] Logs exported');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接状态
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
const status = {
|
||||||
|
connected: socket.connected || false,
|
||||||
|
type: window.SOCKET_TYPE || 'UNKNOWN',
|
||||||
|
reconnectAttempts: socket.getReconnectAttempts?.() || 0,
|
||||||
|
maxReconnectAttempts: socket.getMaxReconnectAttempts?.() || Infinity,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.table(status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发连接
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
console.log('[Socket Debugger] Manually connecting...');
|
||||||
|
socket.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动断开连接
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
console.log('[Socket Debugger] Manually disconnecting...');
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动重连
|
||||||
|
*/
|
||||||
|
reconnect() {
|
||||||
|
console.log('[Socket Debugger] Manually reconnecting...');
|
||||||
|
socket.disconnect();
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.connect();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送测试事件
|
||||||
|
*/
|
||||||
|
emitTest(eventName, data = {}) {
|
||||||
|
console.log(`[Socket Debugger] Emitting test event: ${eventName}`, data);
|
||||||
|
socket.emit(eventName, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印事件统计
|
||||||
|
*/
|
||||||
|
printStats() {
|
||||||
|
const stats = {
|
||||||
|
total: this.eventLog.length,
|
||||||
|
byType: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventLog.forEach((log) => {
|
||||||
|
stats.byType[log.type] = (stats.byType[log.type] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== Socket Stats ===');
|
||||||
|
console.table(stats.byType);
|
||||||
|
console.log(`Total events: ${stats.total}`);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型过滤日志
|
||||||
|
*/
|
||||||
|
getLogsByType(eventType) {
|
||||||
|
return this.eventLog.filter((log) => log.type === eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近的事件
|
||||||
|
*/
|
||||||
|
getRecentEvents(count = 10) {
|
||||||
|
return this.eventLog.slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误事件
|
||||||
|
*/
|
||||||
|
getErrors() {
|
||||||
|
return this.eventLog.filter(
|
||||||
|
(log) => log.type === 'connect_error' || log.type === 'reconnect_failed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const socketDebugger = new SocketDebugger();
|
||||||
|
export default socketDebugger;
|
||||||
13
src/index.js
13
src/index.js
@@ -13,6 +13,19 @@ import App from './App';
|
|||||||
import { browserNotificationService } from './services/browserNotificationService';
|
import { browserNotificationService } from './services/browserNotificationService';
|
||||||
window.browserNotificationService = browserNotificationService;
|
window.browserNotificationService = browserNotificationService;
|
||||||
|
|
||||||
|
// 🔧 条件导入调试工具(生产环境可选)
|
||||||
|
// 开启方式: 在 .env 文件中设置 REACT_APP_ENABLE_DEBUG=true
|
||||||
|
// 移除方式: 删除此段代码 + 删除 src/devtools/ 目录
|
||||||
|
if (process.env.REACT_APP_ENABLE_DEBUG === 'true') {
|
||||||
|
import('./devtools').then(({ debugToolkit }) => {
|
||||||
|
debugToolkit.init();
|
||||||
|
console.log(
|
||||||
|
'%c✅ 调试工具已加载!使用 window.__DEBUG__.help() 查看命令',
|
||||||
|
'color: #4CAF50; font-weight: bold; font-size: 14px;'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 注册 Service Worker(用于支持浏览器通知)
|
// 注册 Service Worker(用于支持浏览器通知)
|
||||||
function registerServiceWorker() {
|
function registerServiceWorker() {
|
||||||
// ⚠️ Mock 模式下跳过 Service Worker 注册(避免与 MSW 冲突)
|
// ⚠️ Mock 模式下跳过 Service Worker 注册(避免与 MSW 冲突)
|
||||||
|
|||||||
Reference in New Issue
Block a user