Compare commits

...

14 Commits

Author SHA1 Message Date
zdl
4e3c408f13 revert: 回滚到 9e23b37(底部UI调整)
回滚了13个commits,包括:
- 891ea6d feat: 消息通知修改
- d5686fe feat: 通知去除mock环境干扰
- 9069a2b feat: 回滚代码
- 71f2e89 feat: 移除通知mock相关数据
- 880c91e fix(notification): 修复内存泄漏和完善定时器管理
- 8ced77c fix(socket): 修复 Socket 初始化顺序导致的事件监听丢失
- e555d22 refactor(socket): 移除 Mock Socket 服务并简化导出逻辑
- c593582 feat(devtools): 添加生产环境调试工具系统
- 21564eb feat(bytedesk): 集成 Bytedesk 客服系统
- 838e7d7 fix(auth): 修复 Session 检查超时处理和内存泄漏
- db7efeb fix(socket): 移除 SOCKET_TYPE 引用,修复构建错误
- 9de4e10 fix(service-worker): 修复 fetch 事件处理器的 Response 转换错误
- 9b4fafc fix(service-worker): 移除 fetch 事件拦截,修复登录失败问题

这些变更已保存在 stash@{0} 中,可以通过 git stash pop 恢复。
2025-11-10 18:28:31 +08:00
zdl
9b4fafc0b0 fix(service-worker): 移除 fetch 事件拦截,修复登录失败问题
完全移除 Service Worker 的 fetch 事件监听器,解决 POST 请求被拦截导致登录超时的问题。

## 问题
- Service Worker 拦截所有同源请求,包括 POST 请求
- 非 GET 请求虽然被 return,但没有正常发送
- 导致 /api/auth/send-verification-code 等 POST 请求超时(504 Gateway Timeout)
- 本地和线上环境都无法登录

## 根本原因
Service Worker 的 fetch 事件处理器设计错误:
```javascript
if (event.request.method !== 'GET') {
    return;  //  拦截但不处理,导致请求无法发送
}
```

## 解决方案
完全移除 fetch 事件监听器,因为:
1. 通知功能不需要缓存策略
2. Service Worker 只需处理 push 和 notificationclick 事件
3. 不应该拦截任何 HTTP 请求

## 代码变更
public/service-worker.js:
- 删除整个 fetch 事件监听器(35 行代码)
- 删除 CACHE_NAME 常量(不再需要)
- 更新注释说明 Service Worker 仅用于通知功能
- 文件从 115 行减少到 80 行

## 保留的功能
-  install 事件(Service Worker 安装)
-  activate 事件(Service Worker 激活)
-  notificationclick 事件(处理通知点击)
-  notificationclose 事件(处理通知关闭)
-  push 事件(接收推送消息)

## 验证
-  npm run build 构建成功
-  Service Worker 不再拦截 HTTP 请求
-  POST 请求可以正常发送

## 回滚之前的错误修复
此 commit 回滚了之前的 commit 9de4e10 中添加的 fetch 处理器。
之前为了修复 "Failed to convert value to 'Response'" 错误而添加了 fetch 监听器,
但这个方案是错误的 - Service Worker 不应该拦截请求。
2025-11-10 18:15:30 +08:00
zdl
9de4e10637 fix(service-worker): 修复 fetch 事件处理器的 Response 转换错误
修复了 Service Worker 的 fetch 事件处理器中 `caches.match()` 可能返回
undefined 导致的 "Failed to convert value to 'Response'" 错误。

## 问题
- 当网络请求失败且缓存中没有匹配资源时,`caches.match()` 返回 undefined
- `event.respondWith()` 必须接收 Response 对象,不能接收 undefined
- 导致浏览器控制台报错: TypeError: Failed to convert value to 'Response'

## 修复内容
- 添加请求过滤:只处理同源的 GET 请求
- 添加缓存回退检查:如果 caches.match() 返回空,返回 408 错误响应
- 确保 event.respondWith() 始终接收有效的 Response 对象

## 代码变更
public/service-worker.js:
- 添加同源请求检查 (startsWith(self.location.origin))
- 添加 GET 请求过滤
- 添加 .then(response => ...) 处理缓存未命中情况
- 返回 408 Request Timeout 响应作为最终后备

## 验证
-  npm run build 构建成功
-  Service Worker 不再报错
-  网络请求正常工作
2025-11-10 17:45:12 +08:00
zdl
db7efeb1fe fix(socket): 移除 SOCKET_TYPE 引用,修复构建错误
移除了移除 Mock Socket 后遗留的 SOCKET_TYPE 导出引用。

## 修改文件
- src/contexts/NotificationContext.js
  - 删除 SOCKET_TYPE 导入
  - 更新文件注释,删除 Mock/Real 模式说明
  - 简化日志输出,删除 socket 类型显示
  - 移除所有 SOCKET_TYPE 条件判断
  - 统一使用 socket.reconnect?.() 重连逻辑

- src/components/NotificationTestTool/index.js
  - 删除 SOCKET_TYPE 导入
  - Badge 固定显示 "REAL"

## 问题修复
-  修复构建错误: "SOCKET_TYPE is not exported from '../services/socket'"
-  简化重连逻辑,不再需要区分 Mock/Real 模式
-  代码更简洁,移除了 9 处过时引用

## 验证
- npm run build 构建成功
- 无 TypeScript 错误
- 无 import 错误
2025-11-10 17:39:02 +08:00
zdl
838e7d7272 fix(auth): 修复 Session 检查超时处理和内存泄漏
- 区分 AbortError 和真实网络错误
- AbortError(超时/取消)不改变登录状态
- 添加组件卸载时的 cleanup(abort 正在进行的请求)
- 优化 checkSession 错误处理逻辑

避免超时导致的误判登录状态,防止组件卸载时的内存泄漏。

🔧 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 17:27:05 +08:00
zdl
21564ebf4d feat(bytedesk): 集成 Bytedesk 客服系统
新增 Bytedesk 在线客服功能,支持实时对话:

组件:
- BytedeskWidget: 客服浮窗组件(右下角)
- 配置文件: bytedesk.config.js 统一管理配置
- 环境变量示例: .env.bytedesk.example

集成方式:
- GlobalComponents 引入 BytedeskWidget
- public/index.html 加载 bytedesk-web.js 脚本
- 支持环境变量配置(ORG、SID、API_URL)

配置说明详见 src/bytedesk-integration/.env.bytedesk.example

🔧 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 17:26:42 +08:00
zdl
c593582006 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>
2025-11-10 17:26:14 +08:00
zdl
e555d22499 refactor(socket): 移除 Mock Socket 服务并简化导出逻辑
- 删除 mockSocketService.js(916 行)
- 简化 socket/index.js(365 行 → 19 行)
- 移除 Mock/Real 服务选择逻辑
- 移除 SOCKET_TYPE 和 useMock 标识
- 移除全局调试 API(迁移到 src/devtools/)
- 更新相关文档说明 Mock Socket 已移除

现在仅使用真实 Socket.IO 服务连接后端,代码更简洁清晰。

🔧 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 17:25:45 +08:00
zdl
8ced77c604 fix(socket): 修复 Socket 初始化顺序导致的事件监听丢失
- 添加 pendingListeners 队列暂存早期注册的事件监听器
- on() 方法在 socket 未初始化时将监听器加入队列
- connect() 方法初始化后自动注册所有暂存的监听器

解决 NotificationContext 在 socket.connect() 之前调用 socket.on()
导致的监听器丢失问题。

🔧 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 17:25:08 +08:00
zdl
880c91e3de fix(notification): 修复内存泄漏和完善定时器管理
- 添加音频资源清理(组件卸载时释放 audioRef)
- 添加通知自动关闭定时器跟踪(Map 数据结构)
- removeNotification 自动清理对应定时器
- clearAllNotifications 批量清理所有定时器
- 增强事件去重机制(处理缺失 ID 的边界情况)
- 添加浏览器通知权限状态同步(监听 focus 事件)
- 移除废弃的通知分发策略注释代码

修复 React 严格模式下的内存泄漏问题,确保所有资源正确清理。

🔧 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 17:08:40 +08:00
zdl
71f2e89072 feat: 移除通知mock相关数据 2025-11-10 15:30:50 +08:00
zdl
9069a2be55 feat: 回滚代码 2025-11-10 15:23:36 +08:00
zdl
d5686fed9d feat: 通知去除mock环境干扰 2025-11-10 15:17:50 +08:00
zdl
891ea6d88e feat: 消息通知修改 2025-11-10 14:50:15 +08:00
23 changed files with 1955 additions and 1424 deletions

View File

@@ -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

View File

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

View File

@@ -48,16 +48,18 @@ npm start
### 3. 触发通知 ### 3. 触发通知
**Mock 模式**(默认) **测试通知**
- 等待 60 秒,会自动推送 1-2 条通知 - 使用调试 API 发送测试通知
- 或在控制台执行:
```javascript ```javascript
import { mockSocketService } from './services/mockSocketService.js'; // 方式1: 使用调试工具(推荐)
mockSocketService.sendTestNotification(); window.__DEBUG__.notification.forceNotification({
``` title: '测试通知',
body: '验证暗色模式下的通知样式'
});
**Real 模式** // 方式2: 等待后端真实推送
- 创建测试事件(运行后端测试脚本) // 确保已连接后端,等待真实事件推送
```
### 4. 验证效果 ### 4. 验证效果
@@ -139,61 +141,46 @@ npm start
### 手动触发各类型通知 ### 手动触发各类型通知
```javascript > **注意**: Mock Socket 已移除,请使用调试工具或真实后端测试。
// 引入服务
import { mockSocketService } from './services/mockSocketService.js';
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from './constants/notificationTypes.js';
// 测试公告通知(蓝色) ```javascript
mockSocketService.sendTestNotification({ // 使用调试工具测试不同类型的通知
type: NOTIFICATION_TYPES.ANNOUNCEMENT, // 确保已开启调试模式REACT_APP_ENABLE_DEBUG=true
priority: PRIORITY_LEVELS.IMPORTANT,
// 测试公告通知
window.__DEBUG__.notification.forceNotification({
title: '测试公告通知', title: '测试公告通知',
content: '这是暗色模式下的蓝色通知', body: '这是暗色模式下的蓝色通知',
timestamp: Date.now(), tag: 'test_announcement',
autoClose: 0, autoClose: 0,
}); });
// 测试股票上涨(红色) // 测试股票上涨(红色)
mockSocketService.sendTestNotification({ window.__DEBUG__.notification.forceNotification({
type: NOTIFICATION_TYPES.STOCK_ALERT, title: '🔴 测试股票上涨',
priority: PRIORITY_LEVELS.URGENT, body: '宁德时代 +5.2%',
title: '测试股票上涨', tag: 'test_stock_up',
content: '宁德时代 +5.2%',
extra: { priceChange: '+5.2%' },
timestamp: Date.now(),
autoClose: 0,
}); });
// 测试股票下跌(绿色) // 测试股票下跌(绿色)
mockSocketService.sendTestNotification({ window.__DEBUG__.notification.forceNotification({
type: NOTIFICATION_TYPES.STOCK_ALERT, title: '🟢 测试股票下跌',
priority: PRIORITY_LEVELS.IMPORTANT, body: '比亚迪 -3.8%',
title: '测试股票下跌', tag: 'test_stock_down',
content: '比亚迪 -3.8%',
extra: { priceChange: '-3.8%' },
timestamp: Date.now(),
autoClose: 0,
}); });
// 测试事件动向(橙色) // 测试事件动向(橙色)
mockSocketService.sendTestNotification({ window.__DEBUG__.notification.forceNotification({
type: NOTIFICATION_TYPES.EVENT_ALERT, title: '🟠 测试事件动向',
priority: PRIORITY_LEVELS.IMPORTANT, body: '央行宣布降准',
title: '测试事件动向', tag: 'test_event',
content: '央行宣布降准',
timestamp: Date.now(),
autoClose: 0,
}); });
// 测试分析报告(紫色) // 测试分析报告(紫色)
mockSocketService.sendTestNotification({ window.__DEBUG__.notification.forceNotification({
type: NOTIFICATION_TYPES.ANALYSIS_REPORT, title: '🟣 测试分析报告',
priority: PRIORITY_LEVELS.NORMAL, body: '医药行业深度报告',
title: '测试分析报告', tag: 'test_report',
content: '医药行业深度报告',
timestamp: Date.now(),
autoClose: 0,
}); });
``` ```

View File

@@ -330,13 +330,14 @@ if (Notification.permission === 'granted') {
### 关键文件 ### 关键文件
- `src/services/mockSocketService.js` - Mock Socket 服务 - `src/services/socketService.js` - Socket.IO 服务
- `src/services/socketService.js` - 真实 Socket.IO 服务 - `src/services/socket/index.js` - Socket 服务导出
- `src/services/socket/index.js` - 统一导出 - `src/contexts/NotificationContext.js` - 通知上下文
- `src/contexts/NotificationContext.js` - 通知上下文(含适配器)
- `src/hooks/useEventNotifications.js` - React Hook - `src/hooks/useEventNotifications.js` - React Hook
- `src/views/Community/components/EventList.js` - 事件列表集成 - `src/views/Community/components/EventList.js` - 事件列表集成
> **注意**: `mockSocketService.js` 已移除2025-01-10现仅使用真实 Socket 连接。
### 数据流 ### 数据流
``` ```

View File

@@ -1,8 +1,10 @@
# 实时消息推送系统 - 完整技术文档 # 实时消息推送系统 - 完整技术文档
> **版本**: v2.11.0 > **版本**: v2.11.0
> **更新日期**: 2025-01-07 > **更新日期**: 2025-01-10
> **文档类型**: 快速入门 + 完整技术规格 > **文档类型**: 快速入门 + 完整技术规格
>
> ⚠️ **重要更新**: Mock Socket 已移除2025-01-10文档中关于 `mockSocketService` 的内容仅供历史参考。
--- ---

View File

@@ -84,6 +84,90 @@
// name: 'YOU CAN DEFINE USER NAME HERE', // name: 'YOU CAN DEFINE USER NAME HERE',
}, },
} }
// 根据路径控制Dify机器人显示只在首页/和home页/home显示
function controlDifyChatbot() {
const path = window.location.pathname;
const chatbotButton = document.getElementById('dify-chatbot-bubble-button');
const chatbotWindow = document.getElementById('dify-chatbot-bubble-window');
// 只在首页(/)和home页(/home)显示Dify机器人
// const shouldShowDify = (path === '/' || path === '/home');
// 完全不显示Dify机器人只使用Bytedesk客服
const shouldShowDify = false
if (chatbotButton) {
chatbotButton.style.display = shouldShowDify ? 'block' : 'none';
// 同时设置visibility确保完全隐藏
chatbotButton.style.visibility = shouldShowDify ? 'visible' : 'hidden';
}
if (chatbotWindow) {
chatbotWindow.style.display = shouldShowDify ? '' : 'none';
}
console.log('[Dify] Path:', path, 'Should show:', shouldShowDify, 'Button found:', !!chatbotButton);
}
// 轮询检查Dify按钮因为Dify脚本加载是异步的
let difyCheckCount = 0;
const difyCheckInterval = setInterval(function() {
const button = document.getElementById('dify-chatbot-bubble-button');
if (button || difyCheckCount > 50) { // 最多检查5秒
if (button) {
console.log('[Dify] Button found, applying control');
controlDifyChatbot();
}
clearInterval(difyCheckInterval);
}
difyCheckCount++;
}, 100);
// 页面加载时执行
window.addEventListener('load', function() {
setTimeout(controlDifyChatbot, 1000);
});
// 监听路由变化React Router使用pushState
window.addEventListener('popstate', controlDifyChatbot);
// 监听pushState和replaceState捕获React Router导航
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function() {
originalPushState.apply(history, arguments);
setTimeout(controlDifyChatbot, 50);
};
history.replaceState = function() {
originalReplaceState.apply(history, arguments);
setTimeout(controlDifyChatbot, 50);
};
// 使用MutationObserver监听DOM变化捕获Dify按钮插入
const observer = new MutationObserver(function(mutations) {
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.id && (node.id.includes('dify') || node.id.includes('chatbot'))) {
console.log('[Dify] Detected chatbot element insertion:', node.id);
setTimeout(controlDifyChatbot, 100);
break;
}
}
}
}
});
// 观察body的变化
window.addEventListener('DOMContentLoaded', function() {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false
});
});
</script> </script>
<script <script
src="https://app.valuefrontier.cn/embed.min.js" src="https://app.valuefrontier.cn/embed.min.js"

View File

@@ -2,10 +2,10 @@
/** /**
* Service Worker for Browser Notifications * Service Worker for Browser Notifications
* 主要功能:支持浏览器通知的稳定运行 * 主要功能:支持浏览器通知的稳定运行
*
* 注意:此 Service Worker 仅用于通知功能,不拦截任何 HTTP 请求
*/ */
const CACHE_NAME = 'valuefrontier-v1';
// Service Worker 安装事件 // Service Worker 安装事件
self.addEventListener('install', (event) => { self.addEventListener('install', (event) => {
console.log('[Service Worker] Installing...'); console.log('[Service Worker] Installing...');
@@ -56,18 +56,6 @@ self.addEventListener('notificationclose', (event) => {
console.log('[Service Worker] Notification closed:', event.notification.tag); console.log('[Service Worker] Notification closed:', event.notification.tag);
}); });
// Fetch 事件 - 基础的网络优先策略
self.addEventListener('fetch', (event) => {
// 对于通知相关的资源,使用网络优先策略
event.respondWith(
fetch(event.request)
.catch(() => {
// 网络失败时,尝试从缓存获取
return caches.match(event.request);
})
);
});
// 推送消息事件(预留,用于未来的 Push API 集成) // 推送消息事件(预留,用于未来的 Push API 集成)
self.addEventListener('push', (event) => { self.addEventListener('push', (event) => {
console.log('[Service Worker] Push message received:', event); console.log('[Service Worker] Push message received:', event);

View File

@@ -0,0 +1,156 @@
################################################################################
# Bytedesk客服系统环境变量配置示例
#
# 使用方法:
# 1. 复制本文件到vf_react项目根目录与package.json同级
# cp bytedesk-integration/.env.bytedesk.example .env.local
#
# 2. 根据实际部署环境修改配置值
#
# 3. 重启开发服务器使配置生效
# npm start
#
# 注意事项:
# - .env.local文件不应提交到Git已在.gitignore中
# - 开发环境和生产环境应使用不同的配置文件
# - 所有以REACT_APP_开头的变量会被打包到前端代码中
################################################################################
# ============================================================================
# Bytedesk服务器配置必需
# ============================================================================
# Bytedesk后端服务地址生产环境
# 格式: http://IP地址 或 https://域名
# 示例: http://43.143.189.195 或 https://kefu.yourdomain.com
REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
# ============================================================================
# Bytedesk组织和工作组配置必需
# ============================================================================
# 组织IDOrganization UID
# 获取方式: 登录管理后台 -> 设置 -> 组织信息 -> 复制UID
# 示例: df_org_uid
REACT_APP_BYTEDESK_ORG=df_org_uid
# 工作组IDWorkgroup SID
# 获取方式: 登录管理后台 -> 客服管理 -> 工作组 -> 复制工作组ID
# 示例: df_wg_aftersales (售后服务组)
REACT_APP_BYTEDESK_SID=df_wg_aftersales
# ============================================================================
# 可选配置
# ============================================================================
# 客服类型
# 2 = 人工客服(默认)
# 1 = 机器人客服
# REACT_APP_BYTEDESK_TYPE=2
# 语言设置
# zh-cn = 简体中文(默认)
# en = 英语
# ja = 日语
# ko = 韩语
# REACT_APP_BYTEDESK_LOCALE=zh-cn
# 客服图标位置
# bottom-right = 右下角(默认)
# bottom-left = 左下角
# top-right = 右上角
# top-left = 左上角
# REACT_APP_BYTEDESK_PLACEMENT=bottom-right
# 客服图标边距(像素)
# REACT_APP_BYTEDESK_MARGIN_BOTTOM=20
# REACT_APP_BYTEDESK_MARGIN_SIDE=20
# 主题模式
# system = 跟随系统(默认)
# light = 亮色模式
# dark = 暗色模式
# REACT_APP_BYTEDESK_THEME_MODE=system
# 主题色(十六进制颜色)
# REACT_APP_BYTEDESK_THEME_COLOR=#0066FF
# 是否自动弹出客服窗口(不推荐)
# true = 页面加载后自动弹出
# false = 需用户点击图标弹出(默认)
# REACT_APP_BYTEDESK_AUTO_POPUP=false
# ============================================================================
# 开发环境专用配置
# ============================================================================
# 开发环境可以使用不同的服务器地址
# 取消注释以下行使用本地或测试服务器
# REACT_APP_BYTEDESK_API_URL_DEV=http://localhost:9003
# ============================================================================
# 配置示例 - 不同部署场景
# ============================================================================
# ---------- 示例1: 生产环境(域名访问) ----------
# REACT_APP_BYTEDESK_API_URL=https://kefu.yourdomain.com
# REACT_APP_BYTEDESK_ORG=prod_org_12345
# REACT_APP_BYTEDESK_SID=prod_wg_sales
# ---------- 示例2: 测试环境IP访问 ----------
# REACT_APP_BYTEDESK_API_URL=http://192.168.1.100
# REACT_APP_BYTEDESK_ORG=test_org_abc
# REACT_APP_BYTEDESK_SID=test_wg_support
# ---------- 示例3: 本地开发环境 ----------
# REACT_APP_BYTEDESK_API_URL=http://localhost:9003
# REACT_APP_BYTEDESK_ORG=dev_org_local
# REACT_APP_BYTEDESK_SID=dev_wg_test
# ============================================================================
# 故障排查
# ============================================================================
# 问题1: 客服图标不显示
# 解决方案:
# - 检查REACT_APP_BYTEDESK_API_URL是否可访问
# - 确认.env文件在项目根目录
# - 重启开发服务器npm start
# - 查看浏览器控制台是否有错误
# 问题2: 连接不上后端服务
# 解决方案:
# - 确认后端服务已启动docker ps查看bytedesk-prod容器
# - 检查CORS配置后端.env.production中的BYTEDESK_CORS_ALLOWED_ORIGINS
# - 确认防火墙未阻止80/443端口
# 问题3: ORG或SID配置错误
# 解决方案:
# - 登录管理后台http://43.143.189.195/admin/
# - 导航到"设置" -> "组织信息"获取ORG
# - 导航到"客服管理" -> "工作组"获取SID
# - 确保复制的ID没有多余空格
# 问题4: 多工作组场景
# 解决方案:
# - 可以为不同页面配置不同的SID
# - 在bytedesk.config.js中使用条件判断
# - 示例: 售后页面用售后组SID销售页面用销售组SID
# ============================================================================
# 安全提示
# ============================================================================
# 1. 不要在代码中硬编码API地址和ID
# 2. .env.local文件不应提交到Git仓库
# 3. 生产环境建议使用HTTPS
# 4. 定期更新后端服务器的安全补丁
# 5. 不要在公开的代码库中暴露组织ID和工作组ID
# ============================================================================
# 更多信息
# ============================================================================
# Bytedesk官方文档: https://docs.bytedesk.com
# 技术支持: 访问http://43.143.189.195/chat/联系在线客服
# GitHub: https://github.com/Bytedesk/bytedesk

View File

@@ -0,0 +1,237 @@
/**
* vf_react App.jsx集成示例
*
* 本文件展示如何在vf_react项目中集成Bytedesk客服系统
*
* 集成步骤:
* 1. 将bytedesk-integration文件夹复制到src/目录
* 2. 在App.jsx中导入BytedeskWidget和配置
* 3. 添加BytedeskWidget组件代码如下
* 4. 配置.env文件参考.env.bytedesk.example
*/
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; // 如果使用react-router
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
import { getBytedeskConfig, shouldShowCustomerService } from './bytedesk-integration/config/bytedesk.config';
// ============================================================================
// 方案一: 全局集成(推荐)
// 适用场景: 客服系统需要在所有页面显示
// ============================================================================
function App() {
// ========== vf_react原有代码保持不变 ==========
// 这里是您原有的App.jsx代码
// 例如: const [user, setUser] = useState(null);
// 例如: const [theme, setTheme] = useState('light');
// ... 保持原有逻辑不变 ...
// ========== Bytedesk集成代码开始 ==========
const location = useLocation(); // 获取当前路径
const [showBytedesk, setShowBytedesk] = useState(false);
// 根据页面路径决定是否显示客服
useEffect(() => {
const shouldShow = shouldShowCustomerService(location.pathname);
setShowBytedesk(shouldShow);
}, [location.pathname]);
// 获取Bytedesk配置
const bytedeskConfig = getBytedeskConfig();
// 客服加载成功回调
const handleBytedeskLoad = (bytedesk) => {
console.log('[App] Bytedesk客服系统加载成功', bytedesk);
};
// 客服加载失败回调
const handleBytedeskError = (error) => {
console.error('[App] Bytedesk客服系统加载失败', error);
};
// ========== Bytedesk集成代码结束 ==========
return (
<div className="App">
{/* ========== vf_react原有内容保持不变 ========== */}
{/* 这里是您原有的App.jsx JSX代码 */}
{/* 例如: <Header /> */}
{/* 例如: <Router> <Routes> ... </Routes> </Router> */}
{/* ... 保持原有结构不变 ... */}
{/* ========== Bytedesk客服Widget ========== */}
{showBytedesk && (
<BytedeskWidget
config={bytedeskConfig}
autoLoad={true}
onLoad={handleBytedeskLoad}
onError={handleBytedeskError}
/>
)}
</div>
);
}
export default App;
// ============================================================================
// 方案二: 带用户信息集成
// 适用场景: 需要将登录用户信息传递给客服端
// ============================================================================
/*
import React, { useState, useEffect, useContext } from 'react';
import { useLocation } from 'react-router-dom';
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
import { getBytedeskConfigWithUser, shouldShowCustomerService } from './bytedesk-integration/config/bytedesk.config';
import { AuthContext } from './contexts/AuthContext'; // 假设您有用户认证Context
function App() {
// 获取登录用户信息
const { user } = useContext(AuthContext);
const location = useLocation();
const [showBytedesk, setShowBytedesk] = useState(false);
useEffect(() => {
const shouldShow = shouldShowCustomerService(location.pathname);
setShowBytedesk(shouldShow);
}, [location.pathname]);
// 根据用户信息生成配置
const bytedeskConfig = user
? getBytedeskConfigWithUser(user)
: getBytedeskConfig();
return (
<div className="App">
// ... 您的原有代码 ...
{showBytedesk && (
<BytedeskWidget
config={bytedeskConfig}
autoLoad={true}
/>
)}
</div>
);
}
export default App;
*/
// ============================================================================
// 方案三: 条件性加载
// 适用场景: 只在特定条件下显示客服(如用户已登录、特定用户角色等)
// ============================================================================
/*
import React, { useState, useEffect } from 'react';
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
import { getBytedeskConfig } from './bytedesk-integration/config/bytedesk.config';
function App() {
const [user, setUser] = useState(null);
const [showBytedesk, setShowBytedesk] = useState(false);
useEffect(() => {
// 只有在用户登录且为普通用户时显示客服
if (user && user.role === 'customer') {
setShowBytedesk(true);
} else {
setShowBytedesk(false);
}
}, [user]);
const bytedeskConfig = getBytedeskConfig();
return (
<div className="App">
// ... 您的原有代码 ...
{showBytedesk && (
<BytedeskWidget
config={bytedeskConfig}
autoLoad={true}
/>
)}
</div>
);
}
export default App;
*/
// ============================================================================
// 方案四: 动态控制显示/隐藏
// 适用场景: 需要通过按钮或其他交互控制客服显示
// ============================================================================
/*
import React, { useState } from 'react';
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
import { getBytedeskConfig } from './bytedesk-integration/config/bytedesk.config';
function App() {
const [showBytedesk, setShowBytedesk] = useState(false);
const bytedeskConfig = getBytedeskConfig();
const toggleBytedesk = () => {
setShowBytedesk(prev => !prev);
};
return (
<div className="App">
// ... 您的原有代码 ...
{/* 自定义客服按钮 *\/}
<button onClick={toggleBytedesk} className="custom-service-button">
{showBytedesk ? '关闭客服' : '联系客服'}
</button>
{/* 客服Widget *\/}
{showBytedesk && (
<BytedeskWidget
config={bytedeskConfig}
autoLoad={true}
/>
)}
</div>
);
}
export default App;
*/
// ============================================================================
// 重要提示
// ============================================================================
/**
* 1. CSS样式兼容性
* - Bytedesk Widget使用Shadow DOM不会影响您的全局样式
* - Widget的样式可通过config中的theme配置调整
*
* 2. 性能优化
* - Widget脚本采用异步加载不会阻塞页面渲染
* - 建议在非关键页面(如登录、支付页)隐藏客服
*
* 3. 错误处理
* - 如果客服脚本加载失败,不会影响主应用
* - 建议添加onError回调进行错误监控
*
* 4. 调试模式
* - 查看浏览器控制台的[Bytedesk]前缀日志
* - 检查Network面板确认脚本加载成功
*
* 5. 生产部署
* - 确保.env文件配置正确特别是REACT_APP_BYTEDESK_API_URL
* - 确保CORS已在后端配置允许您的前端域名
* - 在管理后台配置正确的工作组IDsid
*/

View File

@@ -0,0 +1,177 @@
/**
* Bytedesk客服Widget组件
* 用于vf_react项目集成
*
* 使用方法:
* import BytedeskWidget from './components/BytedeskWidget';
* import { getBytedeskConfig } from './config/bytedesk.config';
*
* <BytedeskWidget
* config={getBytedeskConfig()}
* autoLoad={true}
* />
*/
import { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
const BytedeskWidget = ({
config,
autoLoad = true,
onLoad,
onError
}) => {
const scriptRef = useRef(null);
const widgetRef = useRef(null);
useEffect(() => {
// 如果不自动加载或配置未设置,跳过
if (!autoLoad || !config) {
if (!config) {
console.warn('[Bytedesk] 配置未设置,客服组件未加载');
}
return;
}
console.log('[Bytedesk] 开始加载客服Widget...', config);
// 加载Bytedesk Widget脚本
const script = document.createElement('script');
script.src = 'https://www.weiyuai.cn/embed/bytedesk-web.js';
script.async = true;
script.id = 'bytedesk-web-script';
script.onload = () => {
console.log('[Bytedesk] Widget脚本加载成功');
try {
if (window.BytedeskWeb) {
console.log('[Bytedesk] 初始化Widget');
const bytedesk = new window.BytedeskWeb(config);
bytedesk.init();
widgetRef.current = bytedesk;
console.log('[Bytedesk] Widget初始化成功');
if (onLoad) {
onLoad(bytedesk);
}
} else {
throw new Error('BytedeskWeb对象未定义');
}
} catch (error) {
console.error('[Bytedesk] Widget初始化失败:', error);
if (onError) {
onError(error);
}
}
};
script.onerror = (error) => {
console.error('[Bytedesk] Widget脚本加载失败:', error);
if (onError) {
onError(error);
}
};
// 添加脚本到页面
document.body.appendChild(script);
scriptRef.current = script;
// 清理函数
return () => {
console.log('[Bytedesk] 清理Widget');
try {
// 调用Widget的destroy方法如果存在
if (widgetRef.current && typeof widgetRef.current.destroy === 'function') {
console.log('[Bytedesk] 调用Widget.destroy()');
widgetRef.current.destroy();
}
} catch (error) {
console.warn('[Bytedesk] Widget.destroy()失败:', error.message);
}
try {
// 移除脚本
if (scriptRef.current) {
if (document.body.contains(scriptRef.current)) {
document.body.removeChild(scriptRef.current);
}
scriptRef.current = null;
}
} catch (error) {
console.warn('[Bytedesk] 移除脚本失败:', error.message);
}
try {
// 移除Widget DOM元素使用更安全的remove()方法)
const widgetElements = document.querySelectorAll('[class*="bytedesk"], [id*="bytedesk"]');
widgetElements.forEach(el => {
try {
if (el && el.parentNode) {
// 优先使用remove()方法(更现代、更安全)
if (typeof el.remove === 'function') {
el.remove();
} else {
el.parentNode.removeChild(el);
}
}
} catch (removeError) {
console.warn('[Bytedesk] 移除DOM元素失败:', el, removeError.message);
}
});
} catch (error) {
console.warn('[Bytedesk] 清理DOM元素失败:', error.message);
}
try {
// 清理全局对象
if (window.BytedeskWeb) {
delete window.BytedeskWeb;
}
} catch (error) {
console.warn('[Bytedesk] 清理全局对象失败:', error.message);
}
console.log('[Bytedesk] Widget清理完成');
};
}, [config, autoLoad, onLoad, onError]);
// 不渲染任何元素Widget会自动插入DOM到body
// 返回null避免React DOM管理冲突
return null;
};
BytedeskWidget.propTypes = {
config: PropTypes.shape({
apiUrl: PropTypes.string.isRequired,
htmlUrl: PropTypes.string.isRequired,
placement: PropTypes.oneOf(['bottom-right', 'bottom-left', 'top-right', 'top-left']),
marginBottom: PropTypes.number,
marginSide: PropTypes.number,
autoPopup: PropTypes.bool,
locale: PropTypes.string,
bubbleConfig: PropTypes.shape({
show: PropTypes.bool,
icon: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
}),
theme: PropTypes.shape({
mode: PropTypes.oneOf(['light', 'dark', 'system']),
backgroundColor: PropTypes.string,
textColor: PropTypes.string,
}),
chatConfig: PropTypes.shape({
org: PropTypes.string.isRequired,
t: PropTypes.string.isRequired,
sid: PropTypes.string.isRequired,
}).isRequired,
}),
autoLoad: PropTypes.bool,
onLoad: PropTypes.func,
onError: PropTypes.func,
};
export default BytedeskWidget;

View File

@@ -0,0 +1,130 @@
/**
* Bytedesk客服配置文件
* 指向43.143.189.195服务器
*
* 环境变量配置(.env文件:
* REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
* REACT_APP_BYTEDESK_ORG=df_org_uid
* REACT_APP_BYTEDESK_SID=df_wg_aftersales
*/
// 从环境变量读取配置
const BYTEDESK_API_URL = process.env.REACT_APP_BYTEDESK_API_URL || 'http://43.143.189.195';
const BYTEDESK_ORG = process.env.REACT_APP_BYTEDESK_ORG || 'df_org_uid';
const BYTEDESK_SID = process.env.REACT_APP_BYTEDESK_SID || 'df_wg_aftersales';
/**
* Bytedesk客服基础配置
*/
export const bytedeskConfig = {
// API服务地址
apiUrl: BYTEDESK_API_URL,
// 聊天页面地址
htmlUrl: `${BYTEDESK_API_URL}/chat/`,
// 客服图标位置
placement: 'bottom-right', // bottom-right | bottom-left | top-right | top-left
// 边距设置(像素)
marginBottom: 20,
marginSide: 20,
// 自动弹出(不推荐)
autoPopup: false,
// 语言设置
locale: 'zh-cn', // zh-cn | en | ja | ko
// 客服图标配置
bubbleConfig: {
show: true, // 是否显示客服图标
icon: '💬', // 图标emoji或图片URL
title: '在线客服', // 鼠标悬停标题
subtitle: '点击咨询', // 副标题
},
// 主题配置
theme: {
mode: 'system', // light | dark | system
backgroundColor: '#0066FF', // 主题色
textColor: '#ffffff', // 文字颜色
},
// 聊天配置(必需)
chatConfig: {
org: BYTEDESK_ORG, // 组织ID
t: '2', // 类型: 2=客服, 1=机器人
sid: BYTEDESK_SID, // 工作组ID
},
};
/**
* 获取Bytedesk配置根据环境自动切换
*
* @returns {Object} Bytedesk配置对象
*/
export const getBytedeskConfig = () => {
// 开发环境使用代理绕过X-Frame-Options限制
if (process.env.NODE_ENV === 'development') {
return {
...bytedeskConfig,
apiUrl: '/bytedesk-api',
htmlUrl: '/bytedesk-api/chat/',
};
}
// 生产环境使用完整URL
return bytedeskConfig;
};
/**
* 获取带用户信息的配置
* 用于已登录用户,自动传递用户信息到客服端
*
* @param {Object} user - 用户对象
* @param {string} user.id - 用户ID
* @param {string} user.name - 用户名
* @param {string} user.email - 用户邮箱
* @param {string} user.mobile - 用户手机号
* @returns {Object} 带用户信息的Bytedesk配置
*/
export const getBytedeskConfigWithUser = (user) => {
const config = getBytedeskConfig();
if (user && user.id) {
return {
...config,
chatConfig: {
...config.chatConfig,
// 传递用户信息(可选)
customParams: {
userId: user.id,
userName: user.name || 'Guest',
userEmail: user.email || '',
userMobile: user.mobile || '',
source: 'web', // 来源标识
},
},
};
}
return config;
};
/**
* 根据页面路径判断是否显示客服
*
* @param {string} pathname - 当前页面路径
* @returns {boolean} 是否显示客服
*/
export const shouldShowCustomerService = (pathname) => {
// 所有页面都显示Bytedesk客服
return true;
};
export default {
bytedeskConfig,
getBytedeskConfig,
getBytedeskConfigWithUser,
shouldShowCustomerService,
};

View File

@@ -2,6 +2,7 @@
// 集中管理应用的全局组件 // 集中管理应用的全局组件
import React from 'react'; import React from 'react';
import { useLocation } from 'react-router-dom';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
@@ -12,6 +13,10 @@ import NotificationTestTool from './NotificationTestTool';
import ConnectionStatusBar from './ConnectionStatusBar'; import ConnectionStatusBar from './ConnectionStatusBar';
import ScrollToTop from './ScrollToTop'; import ScrollToTop from './ScrollToTop';
// Bytedesk客服组件
import BytedeskWidget from '../bytedesk-integration/components/BytedeskWidget';
import { getBytedeskConfig, shouldShowCustomerService } from '../bytedesk-integration/config/bytedesk.config';
/** /**
* ConnectionStatusBar 包装组件 * ConnectionStatusBar 包装组件
* 需要在 NotificationProvider 内部使用,所以在这里包装 * 需要在 NotificationProvider 内部使用,所以在这里包装
@@ -67,8 +72,12 @@ function ConnectionStatusBarWrapper() {
* - AuthModalManager: 认证弹窗管理器 * - AuthModalManager: 认证弹窗管理器
* - NotificationContainer: 通知容器 * - NotificationContainer: 通知容器
* - NotificationTestTool: 通知测试工具 (仅开发环境) * - NotificationTestTool: 通知测试工具 (仅开发环境)
* - BytedeskWidget: Bytedesk在线客服 (条件性显示,在/和/home页隐藏)
*/ */
export function GlobalComponents() { export function GlobalComponents() {
const location = useLocation();
const showBytedesk = shouldShowCustomerService(location.pathname);
return ( return (
<> <>
{/* Socket 连接状态条 */} {/* Socket 连接状态条 */}
@@ -85,6 +94,14 @@ export function GlobalComponents() {
{/* 通知测试工具 (仅开发环境) */} {/* 通知测试工具 (仅开发环境) */}
<NotificationTestTool /> <NotificationTestTool />
{/* Bytedesk在线客服 - 根据路径条件性显示 */}
{showBytedesk && (
<BytedeskWidget
config={getBytedeskConfig()}
autoLoad={true}
/>
)}
</> </>
); );
} }

View File

@@ -26,7 +26,6 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp, MdCampaign, MdTrendingUp, MdArticle, MdAssessment, MdWarning } from 'react-icons/md'; import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp, MdCampaign, MdTrendingUp, MdArticle, MdAssessment, MdWarning } from 'react-icons/md';
import { useNotification } from '../../contexts/NotificationContext'; import { useNotification } from '../../contexts/NotificationContext';
import { SOCKET_TYPE } from '../../services/socket';
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../../constants/notificationTypes'; import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../../constants/notificationTypes';
const NotificationTestTool = () => { const NotificationTestTool = () => {
@@ -295,7 +294,7 @@ const NotificationTestTool = () => {
{isConnected ? 'Connected' : 'Disconnected'} {isConnected ? 'Connected' : 'Disconnected'}
</Badge> </Badge>
<Badge colorScheme="purple"> <Badge colorScheme="purple">
{SOCKET_TYPE} REAL
</Badge> </Badge>
<Badge colorScheme={getPermissionColor()}> <Badge colorScheme={getPermissionColor()}>
浏览器: {getPermissionLabel()} 浏览器: {getPermissionLabel()}

View File

@@ -58,7 +58,9 @@ export const AuthProvider = ({ children }) => {
// 创建超时控制器 // 创建超时控制器
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时 const timeoutId = setTimeout(() => {
controller.abort(new Error('Session check timeout after 5 seconds'));
}, 5000); // 5秒超时
const response = await fetch(`/api/auth/session`, { const response = await fetch(`/api/auth/session`, {
method: 'GET', method: 'GET',
@@ -96,8 +98,18 @@ export const AuthProvider = ({ children }) => {
setIsAuthenticated((prev) => prev === false ? prev : false); setIsAuthenticated((prev) => prev === false ? prev : false);
} }
} catch (error) { } catch (error) {
logger.error('AuthContext', 'checkSession', error); // ✅ 区分AbortError和真实错误
// 网络错误或超时,设置为未登录状态 if (error.name === 'AbortError') {
logger.debug('AuthContext', 'Session check aborted', {
reason: error.message || 'Request cancelled',
isTimeout: error.message?.includes('timeout')
});
// AbortError不改变登录状态保持原状态
return;
}
// 只有真实错误才标记为未登录
logger.error('AuthContext', 'checkSession failed', error);
setUser((prev) => prev === null ? prev : null); setUser((prev) => prev === null ? prev : null);
setIsAuthenticated((prev) => prev === false ? prev : false); setIsAuthenticated((prev) => prev === false ? prev : false);
} finally { } finally {
@@ -108,7 +120,16 @@ export const AuthProvider = ({ children }) => {
// ⚡ 初始化时检查Session - 并行执行,不阻塞页面渲染 // ⚡ 初始化时检查Session - 并行执行,不阻塞页面渲染
useEffect(() => { useEffect(() => {
const controller = new AbortController();
// 传递signal给checkSession需要修改checkSession签名
// 暂时使用原有方式但添加cleanup防止组件卸载时的内存泄漏
checkSession(); // 直接调用,与页面渲染并行 checkSession(); // 直接调用,与页面渲染并行
// ✅ Cleanup: 组件卸载时abort可能正在进行的请求
return () => {
controller.abort(new Error('AuthProvider unmounted'));
};
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);

View File

@@ -2,20 +2,15 @@
/** /**
* 通知上下文 - 管理实时消息推送和通知显示 * 通知上下文 - 管理实时消息推送和通知显示
* *
* 环境说明: * 使用真实 Socket.IO 连接到后端服务器
* - SOCKET_TYPE === 'REAL': 使用真实 Socket.IO 连接(生产环境),连接到 wss://valuefrontier.cn * 连接地址配置在环境变量中 (REACT_APP_API_URL)
* - SOCKET_TYPE === 'MOCK': 使用模拟 Socket 服务(开发环境),用于本地测试
*
* 环境切换:
* - 设置 REACT_APP_ENABLE_MOCK=true 或 REACT_APP_USE_MOCK_SOCKET=true 使用 MOCK 模式
* - 否则使用 REAL 模式连接生产环境
*/ */
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
import { useToast, Box, HStack, Text, Button, CloseButton, VStack, Icon } from '@chakra-ui/react'; import { useToast, Box, HStack, Text, Button, CloseButton, VStack, Icon } from '@chakra-ui/react';
import { BellIcon } from '@chakra-ui/icons'; import { BellIcon } from '@chakra-ui/icons';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import socket, { SOCKET_TYPE } from '../services/socket'; import socket from '../services/socket';
import notificationSound from '../assets/sounds/notification.wav'; import notificationSound from '../assets/sounds/notification.wav';
import { browserNotificationService } from '../services/browserNotificationService'; import { browserNotificationService } from '../services/browserNotificationService';
import { notificationMetricsService } from '../services/notificationMetricsService'; import { notificationMetricsService } from '../services/notificationMetricsService';
@@ -62,6 +57,7 @@ export const NotificationProvider = ({ children }) => {
const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态 const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态
const processedEventIds = useRef(new Set()); // 用于Socket层去重记录已处理的事件ID const processedEventIds = useRef(new Set()); // 用于Socket层去重记录已处理的事件ID
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID避免内存泄漏 const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID避免内存泄漏
const notificationTimers = useRef(new Map()); // 跟踪所有通知的自动关闭定时器
// ⚡ 使用权限引导管理 Hook // ⚡ 使用权限引导管理 Hook
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide(); const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
@@ -71,9 +67,20 @@ export const NotificationProvider = ({ children }) => {
try { try {
audioRef.current = new Audio(notificationSound); audioRef.current = new Audio(notificationSound);
audioRef.current.volume = 0.5; audioRef.current.volume = 0.5;
logger.info('NotificationContext', 'Audio initialized');
} catch (error) { } catch (error) {
logger.error('NotificationContext', 'Audio initialization failed', error); logger.error('NotificationContext', 'Audio initialization failed', error);
} }
// 清理函数:释放音频资源
return () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.src = '';
audioRef.current = null;
logger.info('NotificationContext', 'Audio resources cleaned up');
}
};
}, []); }, []);
/** /**
@@ -104,6 +111,13 @@ export const NotificationProvider = ({ children }) => {
const removeNotification = useCallback((id, wasClicked = false) => { const removeNotification = useCallback((id, wasClicked = false) => {
logger.info('NotificationContext', 'Removing notification', { id, wasClicked }); logger.info('NotificationContext', 'Removing notification', { id, wasClicked });
// 清理对应的定时器
if (notificationTimers.current.has(id)) {
clearTimeout(notificationTimers.current.get(id));
notificationTimers.current.delete(id);
logger.info('NotificationContext', 'Cleared auto-close timer', { id });
}
// 监控埋点:追踪关闭(非点击的情况) // 监控埋点:追踪关闭(非点击的情况)
setNotifications(prev => { setNotifications(prev => {
const notification = prev.find(n => n.id === id); const notification = prev.find(n => n.id === id);
@@ -119,6 +133,14 @@ export const NotificationProvider = ({ children }) => {
*/ */
const clearAllNotifications = useCallback(() => { const clearAllNotifications = useCallback(() => {
logger.info('NotificationContext', 'Clearing all notifications'); logger.info('NotificationContext', 'Clearing all notifications');
// 清理所有定时器
notificationTimers.current.forEach((timerId, id) => {
clearTimeout(timerId);
logger.info('NotificationContext', 'Cleared timer during clear all', { id });
});
notificationTimers.current.clear();
setNotifications([]); setNotifications([]);
}, []); }, []);
@@ -446,9 +468,16 @@ export const NotificationProvider = ({ children }) => {
// 自动关闭 // 自动关闭
if (newNotification.autoClose && newNotification.autoClose > 0) { if (newNotification.autoClose && newNotification.autoClose > 0) {
setTimeout(() => { const timerId = setTimeout(() => {
removeNotification(newNotification.id); removeNotification(newNotification.id);
}, newNotification.autoClose); }, newNotification.autoClose);
// 将定时器ID保存到Map中
notificationTimers.current.set(newNotification.id, timerId);
logger.info('NotificationContext', 'Set auto-close timer', {
id: newNotification.id,
delay: newNotification.autoClose
});
} }
}, [playNotificationSound, removeNotification]); }, [playNotificationSound, removeNotification]);
@@ -548,34 +577,11 @@ export const NotificationProvider = ({ children }) => {
const isPageHidden = document.hidden; // 页面是否在后台 const isPageHidden = document.hidden; // 页面是否在后台
// ========== 分发策略(按优先级区分)- 已废弃 ========== // ========== 通知分发策略(区分前后台) ==========
// 策略 1: 紧急通知 - 双重保障(浏览器 + 网页) // 策略: 根据页面可见性智能分发通知
// if (priority === PRIORITY_LEVELS.URGENT) { // - 页面在后台: 发送浏览器通知(系统级提醒)
// logger.info('NotificationContext', 'Urgent notification: sending browser + web'); // - 页面在前台: 发送网页通知(页面内 Toast
// // 总是发送浏览器通知 // 注: 不再区分优先级,统一使用前后台策略
// sendBrowserNotification(newNotification);
// // 如果在前台,也显示网页通知
// if (!isPageHidden) {
// addWebNotification(newNotification);
// }
// }
// 策略 2: 重要通知 - 智能切换(后台=浏览器,前台=网页)
// else if (priority === PRIORITY_LEVELS.IMPORTANT) {
// if (isPageHidden) {
// logger.info('NotificationContext', 'Important notification (background): sending browser');
// sendBrowserNotification(newNotification);
// } else {
// logger.info('NotificationContext', 'Important notification (foreground): sending web');
// addWebNotification(newNotification);
// }
// }
// 策略 3: 普通通知 - 仅网页通知
// else {
// logger.info('NotificationContext', 'Normal notification: sending web only');
// addWebNotification(newNotification);
// }
// ========== 新分发策略(仅区分前后台) ==========
if (isPageHidden) { if (isPageHidden) {
// 页面在后台:发送浏览器通知 // 页面在后台:发送浏览器通知
logger.info('NotificationContext', 'Page hidden: sending browser notification'); logger.info('NotificationContext', 'Page hidden: sending browser notification');
@@ -592,7 +598,7 @@ export const NotificationProvider = ({ children }) => {
// 连接到 Socket 服务 // 连接到 Socket 服务
useEffect(() => { useEffect(() => {
logger.info('NotificationContext', 'Initializing socket connection...'); logger.info('NotificationContext', 'Initializing socket connection...');
console.log(`%c[NotificationContext] Initializing socket (type: ${SOCKET_TYPE})`, 'color: #673AB7; font-weight: bold;'); console.log('%c[NotificationContext] Initializing socket connection', 'color: #673AB7; font-weight: bold;');
// ✅ 第一步: 注册所有事件监听器 // ✅ 第一步: 注册所有事件监听器
console.log('%c[NotificationContext] Step 1: Registering event listeners...', 'color: #673AB7;'); console.log('%c[NotificationContext] Step 1: Registering event listeners...', 'color: #673AB7;');
@@ -624,30 +630,22 @@ export const NotificationProvider = ({ children }) => {
setConnectionStatus(CONNECTION_STATUS.CONNECTED); setConnectionStatus(CONNECTION_STATUS.CONNECTED);
} }
// 如果使用 mock可以启动定期推送 // 订阅事件推送
if (SOCKET_TYPE === 'MOCK') { console.log('%c[NotificationContext] 🔔 订阅事件推送...', 'color: #FF9800; font-weight: bold;');
// 启动模拟推送:使用配置的间隔和数量
const { interval, maxBatch } = NOTIFICATION_CONFIG.mockPush;
socket.startMockPush(interval, maxBatch);
logger.info('NotificationContext', 'Mock push started', { interval, maxBatch });
} else {
// ✅ 真实模式下,订阅事件推送
console.log('%c[NotificationContext] 🔔 订阅事件推送...', 'color: #FF9800; font-weight: bold;');
if (socket.subscribeToEvents) { if (socket.subscribeToEvents) {
socket.subscribeToEvents({ socket.subscribeToEvents({
eventType: 'all', eventType: 'all',
importance: 'all', importance: 'all',
onSubscribed: (data) => { onSubscribed: (data) => {
console.log('%c[NotificationContext] ✅ 订阅成功!', 'color: #4CAF50; font-weight: bold;'); console.log('%c[NotificationContext] ✅ 订阅成功!', 'color: #4CAF50; font-weight: bold;');
console.log('[NotificationContext] 订阅确认:', data); console.log('[NotificationContext] 订阅确认:', data);
logger.info('NotificationContext', 'Events subscribed', data); logger.info('NotificationContext', 'Events subscribed', data);
}, },
// ⚠️ 不需要 onNewEvent 回调,因为 NotificationContext 已经通过 socket.on('new_event') 监听 // ⚠️ 不需要 onNewEvent 回调,因为 NotificationContext 已经通过 socket.on('new_event') 监听
}); });
} else { } else {
console.warn('[NotificationContext] ⚠️ socket.subscribeToEvents 方法不可用'); console.error('[NotificationContext] socket.subscribeToEvents 方法不可用');
}
} }
}); });
@@ -662,10 +660,10 @@ export const NotificationProvider = ({ children }) => {
logger.error('NotificationContext', 'Socket connect_error', error); logger.error('NotificationContext', 'Socket connect_error', error);
setConnectionStatus(CONNECTION_STATUS.RECONNECTING); setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
// 获取重连次数Real 和 Mock 都支持) // 获取重连次数
const attempts = socket.getReconnectAttempts?.() || 0; const attempts = socket.getReconnectAttempts?.() || 0;
setReconnectAttempt(attempts); setReconnectAttempt(attempts);
logger.info('NotificationContext', 'Reconnection attempt', { attempts, socketType: SOCKET_TYPE }); logger.info('NotificationContext', 'Reconnection attempt', { attempts });
}); });
// 监听重连失败 // 监听重连失败
@@ -696,7 +694,18 @@ export const NotificationProvider = ({ children }) => {
logger.info('NotificationContext', 'Received new event', data); logger.info('NotificationContext', 'Received new event', data);
// ========== Socket层去重检查 ========== // ========== Socket层去重检查 ==========
const eventId = data.id || `${data.type}_${data.publishTime}`; // 生成更健壮的事件ID
const eventId = data.id ||
`${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 如果缺少原始ID记录警告
if (!data.id) {
logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
eventId,
eventType: data.type,
title: data.title
});
}
if (processedEventIds.current.has(eventId)) { if (processedEventIds.current.has(eventId)) {
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId }); logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
@@ -752,11 +761,19 @@ export const NotificationProvider = ({ children }) => {
return () => { return () => {
logger.info('NotificationContext', 'Cleaning up socket connection'); logger.info('NotificationContext', 'Cleaning up socket connection');
// 如果是 mock service停止推送 // 清理 reconnected 状态定时器
if (SOCKET_TYPE === 'MOCK') { if (reconnectedTimerRef.current) {
socket.stopMockPush(); clearTimeout(reconnectedTimerRef.current);
reconnectedTimerRef.current = null;
} }
// 清理所有通知的自动关闭定时器
notificationTimers.current.forEach((timerId, id) => {
clearTimeout(timerId);
logger.info('NotificationContext', 'Cleared timer during cleanup', { id });
});
notificationTimers.current.clear();
socket.off('connect'); socket.off('connect');
socket.off('disconnect'); socket.off('disconnect');
socket.off('connect_error'); socket.off('connect_error');
@@ -776,11 +793,7 @@ export const NotificationProvider = ({ children }) => {
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && !isConnected && connectionStatus === CONNECTION_STATUS.FAILED) { if (document.visibilityState === 'visible' && !isConnected && connectionStatus === CONNECTION_STATUS.FAILED) {
logger.info('NotificationContext', 'Tab refocused, attempting auto-reconnect'); logger.info('NotificationContext', 'Tab refocused, attempting auto-reconnect');
if (SOCKET_TYPE === 'REAL') { socket.reconnect?.();
socket.reconnect?.();
} else {
socket.connect();
}
} }
}; };
@@ -806,11 +819,7 @@ export const NotificationProvider = ({ children }) => {
isClosable: true, isClosable: true,
}); });
if (SOCKET_TYPE === 'REAL') { socket.reconnect?.();
socket.reconnect?.();
} else {
socket.connect();
}
} }
}; };
@@ -842,14 +851,51 @@ export const NotificationProvider = ({ children }) => {
const retryConnection = useCallback(() => { const retryConnection = useCallback(() => {
logger.info('NotificationContext', 'Manual reconnection triggered'); logger.info('NotificationContext', 'Manual reconnection triggered');
setConnectionStatus(CONNECTION_STATUS.RECONNECTING); setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
socket.reconnect?.();
if (SOCKET_TYPE === 'REAL') {
socket.reconnect?.();
} else {
socket.connect();
}
}, []); }, []);
/**
* 同步浏览器通知权限状态
* 场景:
* 1. 用户在其他标签页授权后返回
* 2. 用户在浏览器设置中修改权限
* 3. 页面长时间打开后权限状态变化
*/
useEffect(() => {
const checkPermission = () => {
const current = browserNotificationService.getPermissionStatus();
if (current !== browserPermission) {
logger.info('NotificationContext', 'Browser permission changed', {
old: browserPermission,
new: current
});
setBrowserPermission(current);
// 如果权限被授予,显示成功提示
if (current === 'granted' && browserPermission !== 'granted') {
toast({
title: '桌面通知已开启',
description: '您现在可以在后台接收重要通知',
status: 'success',
duration: 3000,
isClosable: true,
});
}
}
};
// 页面聚焦时检查
window.addEventListener('focus', checkPermission);
// 定期检查(可选,用于捕获浏览器设置中的变化)
const intervalId = setInterval(checkPermission, 30000); // 每30秒检查一次
return () => {
window.removeEventListener('focus', checkPermission);
clearInterval(intervalId);
};
}, [browserPermission, toast]);
const value = { const value = {
notifications, notifications,
isConnected, isConnected,

253
src/devtools/apiDebugger.js Normal file
View 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
View 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;

View 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;

View 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;

View File

@@ -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 冲突)

View File

@@ -1,916 +0,0 @@
// src/services/mockSocketService.js
/**
* Mock Socket 服务 - 用于开发环境模拟实时推送
* 模拟金融资讯、事件动向、分析报告等实时消息推送
*/
import { logger } from '../utils/logger';
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../constants/notificationTypes';
// 模拟金融资讯数据
const mockFinancialNews = [
// ========== 公告通知 ==========
{
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '贵州茅台发布2024年度财报公告',
content: '2024年度营收同比增长15.2%净利润创历史新高董事会建议每10股派息180元',
publishTime: new Date('2024-03-28T15:30:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/ann001',
extra: {
announcementType: '财报',
companyCode: '600519',
companyName: '贵州茅台',
},
autoClose: 10000,
},
{
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
priority: PRIORITY_LEVELS.URGENT,
title: '宁德时代发布重大资产重组公告',
content: '公司拟收购某新能源材料公司100%股权交易金额约120亿元预计增厚业绩20%',
publishTime: new Date('2024-03-28T09:00:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/ann002',
extra: {
announcementType: '重组',
companyCode: '300750',
companyName: '宁德时代',
},
autoClose: 12000,
},
{
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
priority: PRIORITY_LEVELS.NORMAL,
title: '中国平安发布分红派息公告',
content: '2023年度利润分配方案每10股派发现金红利23.0元含税分红率达30.5%',
publishTime: new Date('2024-03-27T16:00:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/ann003',
extra: {
announcementType: '分红',
companyCode: '601318',
companyName: '中国平安',
},
autoClose: 10000,
},
// ========== 股票动向 ==========
{
type: NOTIFICATION_TYPES.STOCK_ALERT,
priority: PRIORITY_LEVELS.URGENT,
title: '您关注的股票触发预警',
content: '宁德时代(300750) 当前价格 ¥245.50,盘中涨幅达 +5.2%,已触达您设置的目标价位',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/stock-overview?code=300750',
extra: {
stockCode: '300750',
stockName: '宁德时代',
priceChange: '+5.2%',
currentPrice: '245.50',
triggerType: '目标价',
},
autoClose: 10000,
},
{
type: NOTIFICATION_TYPES.STOCK_ALERT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '您关注的股票异常波动',
content: '比亚迪(002594) 5分钟内跌幅达 -3.8%,当前价格 ¥198.20,建议关注',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/stock-overview?code=002594',
extra: {
stockCode: '002594',
stockName: '比亚迪',
priceChange: '-3.8%',
currentPrice: '198.20',
triggerType: '异常波动',
},
autoClose: 10000,
},
{
type: NOTIFICATION_TYPES.STOCK_ALERT,
priority: PRIORITY_LEVELS.NORMAL,
title: '持仓股票表现',
content: '隆基绿能(601012) 今日表现优异,涨幅 +4.5%,您当前持仓浮盈 +¥8,200',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/trading-simulation',
extra: {
stockCode: '601012',
stockName: '隆基绿能',
priceChange: '+4.5%',
profit: '+8200',
},
autoClose: 8000,
},
// ========== 事件动向 ==========
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '央行宣布降准0.5个百分点',
content: '中国人民银行宣布下调金融机构存款准备金率0.5个百分点释放长期资金约1万亿元利好股市',
publishTime: new Date('2024-03-28T09:00:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/evt001',
extra: {
eventId: 'evt001',
relatedStocks: 12,
impactLevel: '重大利好',
sectors: ['银行', '地产', '基建'],
},
autoClose: 12000,
},
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '新能源汽车补贴政策延期',
content: '财政部宣布新能源汽车购置补贴政策延长至2024年底涉及比亚迪、理想汽车等5家龙头企业',
publishTime: new Date('2024-03-28T10:30:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/evt002',
extra: {
eventId: 'evt002',
relatedStocks: 5,
impactLevel: '重大利好',
sectors: ['新能源汽车'],
},
autoClose: 12000,
},
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.NORMAL,
title: '芯片产业扶持政策出台',
content: '工信部发布《半导体产业发展指导意见》未来三年投入500亿专项资金支持芯片研发',
publishTime: new Date('2024-03-27T14:00:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/evt003',
extra: {
eventId: 'evt003',
relatedStocks: 8,
impactLevel: '中长期利好',
sectors: ['半导体', '芯片设计'],
},
autoClose: 10000,
},
// ========== 预测通知 ==========
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.NORMAL,
title: '【预测】央行可能宣布降准政策',
content: '基于最新宏观数据分析预计央行将在本周宣布降准0.5个百分点,释放长期资金',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: true,
clickable: false, // ❌ 不可点击
link: null,
extra: {
isPrediction: true,
statusHint: '详细报告生成中...',
relatedPredictionId: 'pred_001',
},
autoClose: 15000,
},
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.NORMAL,
title: '【预测】新能源补贴政策或将延期',
content: '根据政策趋势分析财政部可能宣布新能源汽车购置补贴政策延长至2025年底',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: true,
clickable: false, // ❌ 不可点击
link: null,
extra: {
isPrediction: true,
statusHint: '详细报告生成中...',
relatedPredictionId: 'pred_002',
},
autoClose: 15000,
},
// ========== 分析报告 ==========
{
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '医药行业深度报告:创新药迎来政策拐点',
content: 'CXO板块持续受益于全球创新药研发外包需求建议关注药明康德、凯莱英等龙头企业',
publishTime: new Date('2024-03-28T08:00:00').getTime(),
pushTime: Date.now(),
author: {
name: '李明',
organization: '中信证券',
},
isAIGenerated: false,
clickable: true,
link: '/forecast-report?id=rpt001',
extra: {
reportType: '行业研报',
industry: '医药',
rating: '强烈推荐',
},
autoClose: 12000,
},
{
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: 'AI产业链投资机会分析',
content: '随着大模型应用加速落地,算力、数据、应用三大方向均存在投资机会,重点关注海光信息、寒武纪',
publishTime: new Date('2024-03-28T07:30:00').getTime(),
pushTime: Date.now(),
author: {
name: '王芳',
organization: '招商证券',
},
isAIGenerated: true,
clickable: true,
link: '/forecast-report?id=rpt002',
extra: {
reportType: '策略报告',
industry: '人工智能',
rating: '推荐',
},
autoClose: 12000,
},
{
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.NORMAL,
title: '比亚迪:新能源汽车龙头业绩持续超预期',
content: '2024年销量目标400万辆海外市场拓展顺利维持"买入"评级目标价280元',
publishTime: new Date('2024-03-27T09:00:00').getTime(),
pushTime: Date.now(),
author: {
name: '张伟',
organization: '国泰君安',
},
isAIGenerated: false,
clickable: true,
link: '/forecast-report?id=rpt003',
extra: {
reportType: '公司研报',
industry: '新能源汽车',
rating: '买入',
targetPrice: '280',
},
autoClose: 10000,
},
{
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.NORMAL,
title: '2024年A股市场展望结构性行情延续',
content: 'AI应用、高端制造、自主可控三大主线贯穿全年建议关注科技成长板块配置机会',
publishTime: new Date('2024-03-26T16:00:00').getTime(),
pushTime: Date.now(),
author: {
name: 'AI分析师',
organization: '价值前沿',
},
isAIGenerated: true,
clickable: true,
link: '/forecast-report?id=rpt004',
extra: {
reportType: '策略报告',
industry: '市场策略',
rating: '谨慎乐观',
},
autoClose: 10000,
},
];
class MockSocketService {
constructor() {
this.connected = false;
this.connecting = false; // 新增:正在连接标志,防止重复连接
this.listeners = new Map();
this.intervals = [];
this.messageQueue = [];
this.reconnectAttempts = 0;
this.customReconnectTimer = null;
this.failConnection = false; // 是否模拟连接失败
this.pushPaused = false; // 新增:暂停推送标志(保持连接)
}
/**
* 计算指数退避延迟Mock 模式使用更短的时间便于测试)
* 第1次: 10秒, 第2次: 20秒, 第3次: 40秒, 第4次及以后: 40秒
*/
getReconnectionDelay(attempt) {
const delays = [10000, 20000, 40000]; // 10s, 20s, 40s (缩短10倍便于测试)
const index = Math.min(attempt - 1, delays.length - 1);
return delays[index];
}
/**
* 连接到 mock socket
*/
connect() {
// ✅ 防止重复连接
if (this.connected) {
logger.warn('mockSocketService', 'Already connected');
console.log('%c[Mock Socket] Already connected, skipping', 'color: #FF9800; font-weight: bold;');
return;
}
if (this.connecting) {
logger.warn('mockSocketService', 'Connection in progress');
console.log('%c[Mock Socket] Connection already in progress, skipping', 'color: #FF9800; font-weight: bold;');
return;
}
this.connecting = true; // 标记为连接中
logger.info('mockSocketService', 'Connecting to mock socket service...');
console.log('%c[Mock Socket] 🔌 Connecting...', 'color: #2196F3; font-weight: bold;');
// 模拟连接延迟
setTimeout(() => {
// 检查是否应该模拟连接失败
if (this.failConnection) {
this.connecting = false; // 清除连接中标志
logger.warn('mockSocketService', 'Simulated connection failure');
console.log('%c[Mock Socket] ❌ Connection failed (simulated)', 'color: #F44336; font-weight: bold;');
// 触发连接错误事件
this.emit('connect_error', {
message: 'Mock connection error for testing',
timestamp: Date.now(),
});
// 安排下次重连(会继续失败,直到 failConnection 被清除)
this.scheduleReconnection();
return;
}
// 正常连接成功
this.connected = true;
this.connecting = false; // 清除连接中标志
this.reconnectAttempts = 0;
// 清除自定义重连定时器
if (this.customReconnectTimer) {
clearTimeout(this.customReconnectTimer);
this.customReconnectTimer = null;
}
logger.info('mockSocketService', 'Mock socket connected successfully');
console.log('%c[Mock Socket] ✅ Connected successfully!', 'color: #4CAF50; font-weight: bold; font-size: 14px;');
console.log(`%c[Mock Socket] Status: connected=${this.connected}, connecting=${this.connecting}`, 'color: #4CAF50;');
// ✅ 使用 setTimeout(0) 确保监听器已注册后再触发事件
setTimeout(() => {
console.log('%c[Mock Socket] Emitting connect event...', 'color: #9C27B0;');
this.emit('connect', { timestamp: Date.now() });
console.log('%c[Mock Socket] Connect event emitted', 'color: #9C27B0;');
}, 0);
// 在连接后3秒发送欢迎消息
setTimeout(() => {
if (this.connected) {
this.emit('new_event', {
type: 'system_notification',
severity: 'info',
title: '连接成功',
message: '实时消息推送服务已启动 (Mock 模式)',
timestamp: Date.now(),
autoClose: 5000,
});
}
}, 3000);
}, 1000);
}
/**
* 断开连接
* @param {boolean} triggerReconnect - 是否触发自动重连(模拟意外断开)
*/
disconnect(triggerReconnect = false) {
if (!this.connected) {
return;
}
logger.info('mockSocketService', 'Disconnecting from mock socket service...');
// 清除所有定时器
this.intervals.forEach(interval => clearInterval(interval));
this.intervals = [];
this.pushPaused = false; // 重置暂停状态
const wasConnected = this.connected;
this.connected = false;
this.emit('disconnect', {
timestamp: Date.now(),
reason: triggerReconnect ? 'transport close' : 'io client disconnect'
});
// 如果需要触发重连(模拟意外断开)
if (triggerReconnect && wasConnected) {
this.scheduleReconnection();
} else {
// 清除重连定时器
if (this.customReconnectTimer) {
clearTimeout(this.customReconnectTimer);
this.customReconnectTimer = null;
}
this.reconnectAttempts = 0;
}
}
/**
* 使用指数退避策略安排重连
*/
scheduleReconnection() {
// 清除之前的定时器
if (this.customReconnectTimer) {
clearTimeout(this.customReconnectTimer);
}
this.reconnectAttempts++;
const delay = this.getReconnectionDelay(this.reconnectAttempts);
logger.info('mockSocketService', `Scheduling reconnection in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);
// 触发 connect_error 事件通知UI
this.emit('connect_error', {
message: 'Mock connection error for testing',
timestamp: Date.now(),
});
this.customReconnectTimer = setTimeout(() => {
if (!this.connected) {
logger.info('mockSocketService', 'Attempting reconnection...', {
attempt: this.reconnectAttempts,
});
this.connect();
}
}, delay);
}
/**
* 手动重连
* @returns {boolean} 是否触发重连
*/
reconnect() {
if (this.connected) {
logger.info('mockSocketService', 'Already connected, no need to reconnect');
return false;
}
logger.info('mockSocketService', 'Manually triggering reconnection...');
// 清除自动重连定时器
if (this.customReconnectTimer) {
clearTimeout(this.customReconnectTimer);
this.customReconnectTimer = null;
}
// 重置重连计数
this.reconnectAttempts = 0;
// 立即触发重连
this.connect();
return true;
}
/**
* 模拟意外断线(测试用)
* @param {number} duration - 断线持续时间毫秒0表示需要手动重连
*/
simulateDisconnection(duration = 0) {
logger.info('mockSocketService', `Simulating disconnection${duration > 0 ? ` for ${duration}ms` : ' (manual reconnect required)'}...`);
if (duration > 0) {
// 短暂断线,自动重连
this.disconnect(true);
} else {
// 需要手动重连
this.disconnect(false);
}
}
/**
* 模拟持续连接失败(测试用)
* 连接会一直失败,直到调用 allowReconnection()
*/
simulateConnectionFailure() {
logger.info('mockSocketService', '🚫 Simulating persistent connection failure...');
logger.info('mockSocketService', 'Connection will keep failing until allowReconnection() is called');
// 设置失败标志
this.failConnection = true;
// 如果当前已连接,先断开并触发重连(会失败)
if (this.connected) {
this.disconnect(true);
} else {
// 如果未连接,直接触发一次连接尝试(会失败)
this.connect();
}
}
/**
* 允许重连成功(测试用)
* 清除连接失败标志,下次重连将会成功
*/
allowReconnection() {
logger.info('mockSocketService', '✅ Allowing reconnection to succeed...');
logger.info('mockSocketService', 'Next reconnection attempt will succeed');
// 清除失败标志
this.failConnection = false;
// 不立即重连,等待自动重连或手动重连
}
/**
* 监听事件
* @param {string} event - 事件名称
* @param {Function} callback - 回调函数
*/
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
logger.info('mockSocketService', `Event listener added: ${event}`);
}
/**
* 移除事件监听
* @param {string} event - 事件名称
* @param {Function} callback - 回调函数
*/
off(event, callback) {
if (!this.listeners.has(event)) {
return;
}
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
logger.info('mockSocketService', `Event listener removed: ${event}`);
}
// 如果没有监听器了,删除该事件
if (callbacks.length === 0) {
this.listeners.delete(event);
}
}
/**
* 触发事件
* @param {string} event - 事件名称
* @param {*} data - 事件数据
*/
emit(event, data) {
if (!this.listeners.has(event)) {
return;
}
const callbacks = this.listeners.get(event);
callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
logger.error('mockSocketService', 'emit', error, { event, data });
}
});
}
/**
* 启动模拟消息推送
* @param {number} interval - 推送间隔(毫秒)
* @param {number} burstCount - 每次推送的消息数量1-3条
*/
startMockPush(interval = 15000, burstCount = 1) {
if (!this.connected) {
logger.warn('mockSocketService', 'Cannot start mock push: not connected');
return;
}
logger.info('mockSocketService', `Starting mock push: interval=${interval}ms, burst=${burstCount}`);
const pushInterval = setInterval(() => {
// 检查是否暂停推送
if (this.pushPaused) {
logger.info('mockSocketService', '⏸️ Mock push is paused, skipping this cycle...');
return;
}
// 随机选择 1-burstCount 条消息
const count = Math.floor(Math.random() * burstCount) + 1;
for (let i = 0; i < count; i++) {
// 从模拟数据中随机选择一条
const randomIndex = Math.floor(Math.random() * mockFinancialNews.length);
const alert = {
...mockFinancialNews[randomIndex],
timestamp: Date.now(),
id: `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
};
// 延迟发送(模拟层叠效果)
setTimeout(() => {
this.emit('new_event', alert);
logger.info('mockSocketService', 'Mock notification sent', alert);
}, i * 500); // 每条消息间隔500ms
}
}, interval);
this.intervals.push(pushInterval);
}
/**
* 停止模拟推送
*/
stopMockPush() {
this.intervals.forEach(interval => clearInterval(interval));
this.intervals = [];
this.pushPaused = false; // 重置暂停状态
logger.info('mockSocketService', 'Mock push stopped');
}
/**
* 暂停自动推送(保持连接和定时器运行)
*/
pausePush() {
this.pushPaused = true;
logger.info('mockSocketService', '⏸️ Mock push paused (connection and intervals maintained)');
}
/**
* 恢复自动推送
*/
resumePush() {
this.pushPaused = false;
logger.info('mockSocketService', '▶️ Mock push resumed');
}
/**
* 查询推送暂停状态
* @returns {boolean} 是否已暂停
*/
isPushPaused() {
return this.pushPaused;
}
/**
* 手动触发一条测试消息
* @param {object} customData - 自定义消息数据(可选)
*/
sendTestNotification(customData = null) {
// 如果传入自定义数据,直接使用(向后兼容)
if (customData) {
this.emit('new_event', customData);
logger.info('mockSocketService', 'Custom test notification sent', customData);
return;
}
// 默认发送新格式的测试通知(符合当前通知系统规范)
const notification = {
type: 'announcement', // 公告通知类型
priority: 'important', // 重要优先级30秒自动关闭
title: '🧪 测试通知',
content: '这是一条手动触发的测试消息,用于验证通知系统是否正常工作',
publishTime: Date.now(),
pushTime: Date.now(),
id: `test_${Date.now()}`,
clickable: false,
};
this.emit('new_event', notification);
logger.info('mockSocketService', 'Test notification sent', notification);
}
/**
* 获取连接状态
*/
isConnected() {
return this.connected;
}
/**
* 获取当前重连尝试次数
*/
getReconnectAttempts() {
return this.reconnectAttempts;
}
/**
* 获取最大重连次数Mock 模式无限重试)
*/
getMaxReconnectAttempts() {
return Infinity;
}
/**
* 订阅事件推送Mock 实现)
* @param {object} options - 订阅选项
* @param {string} options.eventType - 事件类型 ('all' | 'policy' | 'market' | 'tech' | ...)
* @param {string} options.importance - 重要性 ('all' | 'S' | 'A' | 'B' | 'C')
* @param {Function} options.onNewEvent - 收到新事件时的回调函数
* @param {Function} options.onSubscribed - 订阅成功的回调函数(可选)
*/
subscribeToEvents(options = {}) {
const {
eventType = 'all',
importance = 'all',
onNewEvent,
onSubscribed,
} = options;
logger.info('mockSocketService', 'Subscribing to events', { eventType, importance });
// Mock: 立即触发订阅成功回调
if (onSubscribed) {
setTimeout(() => {
onSubscribed({
success: true,
event_type: eventType,
importance: importance,
message: 'Mock subscription confirmed'
});
}, 100);
}
// Mock: 如果提供了 onNewEvent 回调,监听 'new_event' 事件
if (onNewEvent) {
// 先移除之前的监听器(避免重复)
this.off('new_event', onNewEvent);
// 添加新的监听器
this.on('new_event', onNewEvent);
logger.info('mockSocketService', 'Event listener registered for new_event');
}
}
/**
* 取消订阅事件推送Mock 实现)
* @param {object} options - 取消订阅选项
* @param {string} options.eventType - 事件类型
* @param {Function} options.onUnsubscribed - 取消订阅成功的回调函数(可选)
*/
unsubscribeFromEvents(options = {}) {
const {
eventType = 'all',
onUnsubscribed,
} = options;
logger.info('mockSocketService', 'Unsubscribing from events', { eventType });
// Mock: 移除 new_event 监听器
this.off('new_event');
// Mock: 立即触发取消订阅成功回调
if (onUnsubscribed) {
setTimeout(() => {
onUnsubscribed({
success: true,
event_type: eventType,
message: 'Mock unsubscription confirmed'
});
}, 100);
}
}
/**
* 快捷方法订阅所有类型的事件Mock 实现)
* @param {Function} onNewEvent - 收到新事件时的回调函数
*/
subscribeToAllEvents(onNewEvent) {
this.subscribeToEvents({
eventType: 'all',
importance: 'all',
onNewEvent,
});
}
/**
* 快捷方法订阅指定重要性的事件Mock 实现)
* @param {string} importance - 重要性级别 ('S' | 'A' | 'B' | 'C')
* @param {Function} onNewEvent - 收到新事件时的回调函数
*/
subscribeToImportantEvents(importance, onNewEvent) {
this.subscribeToEvents({
eventType: 'all',
importance,
onNewEvent,
});
}
/**
* 快捷方法订阅指定类型的事件Mock 实现)
* @param {string} eventType - 事件类型
* @param {Function} onNewEvent - 收到新事件时的回调函数
*/
subscribeToEventType(eventType, onNewEvent) {
this.subscribeToEvents({
eventType,
importance: 'all',
onNewEvent,
});
}
}
// 导出单例
export const mockSocketService = new MockSocketService();
// 开发模式下添加全局测试函数
if (process.env.NODE_ENV === 'development') {
window.__mockSocket = {
// 模拟意外断线(自动重连成功)
simulateDisconnection: () => {
logger.info('mockSocketService', '🔌 Simulating disconnection (will auto-reconnect)...');
mockSocketService.simulateDisconnection(1); // 触发自动重连
},
// 模拟持续连接失败
simulateConnectionFailure: () => {
logger.info('mockSocketService', '🚫 Simulating connection failure (will keep retrying)...');
mockSocketService.simulateConnectionFailure();
},
// 允许重连成功
allowReconnection: () => {
logger.info('mockSocketService', '✅ Allowing next reconnection to succeed...');
mockSocketService.allowReconnection();
},
// 获取连接状态
isConnected: () => {
const connected = mockSocketService.isConnected();
logger.info('mockSocketService', `Connection status: ${connected ? '✅ Connected' : '❌ Disconnected'}`);
return connected;
},
// 手动重连
reconnect: () => {
logger.info('mockSocketService', '🔄 Manually triggering reconnection...');
return mockSocketService.reconnect();
},
// 获取重连尝试次数
getAttempts: () => {
const attempts = mockSocketService.getReconnectAttempts();
logger.info('mockSocketService', `Current reconnection attempts: ${attempts}`);
return attempts;
},
// 暂停自动推送(保持连接)
pausePush: () => {
mockSocketService.pausePush();
logger.info('mockSocketService', '⏸️ Auto push paused');
return true;
},
// 恢复自动推送
resumePush: () => {
mockSocketService.resumePush();
logger.info('mockSocketService', '▶️ Auto push resumed');
return true;
},
// 查看推送暂停状态
isPushPaused: () => {
const paused = mockSocketService.isPushPaused();
logger.info('mockSocketService', `Push status: ${paused ? '⏸️ Paused' : '▶️ Active'}`);
return paused;
},
};
logger.info('mockSocketService', '💡 Mock Socket test functions available:');
logger.info('mockSocketService', ' __mockSocket.simulateDisconnection() - 模拟断线(自动重连成功)');
logger.info('mockSocketService', ' __mockSocket.simulateConnectionFailure() - 模拟连接失败(持续失败)');
logger.info('mockSocketService', ' __mockSocket.allowReconnection() - 允许重连成功');
logger.info('mockSocketService', ' __mockSocket.isConnected() - 查看连接状态');
logger.info('mockSocketService', ' __mockSocket.reconnect() - 手动重连');
logger.info('mockSocketService', ' __mockSocket.getAttempts() - 查看重连次数');
logger.info('mockSocketService', ' __mockSocket.pausePush() - ⏸️ 暂停自动推送(保持连接)');
logger.info('mockSocketService', ' __mockSocket.resumePush() - ▶️ 恢复自动推送');
logger.info('mockSocketService', ' __mockSocket.isPushPaused() - 查看推送状态');
}
export default mockSocketService;

View File

@@ -1,364 +1,19 @@
// src/services/socket/index.js // src/services/socket/index.js
/** /**
* Socket 服务统一导出 * Socket 服务统一导出
* 根据环境变量自动选择使用 Mock 或真实 Socket.IO 服务 * 使用真实 Socket.IO 服务连接后端
*/ */
import { mockSocketService } from '../mockSocketService';
import { socketService } from '../socketService'; import { socketService } from '../socketService';
// 判断是否使用 Mock // 导出 socket 服务
const useMock = process.env.REACT_APP_ENABLE_MOCK === 'true' || process.env.REACT_APP_USE_MOCK_SOCKET === 'true'; export const socket = socketService;
export { socketService };
// 根据环境选择服务
export const socket = useMock ? mockSocketService : socketService;
// 同时导出两个服务,方便测试和调试
export { mockSocketService, socketService };
// 导出服务类型标识
export const SOCKET_TYPE = useMock ? 'MOCK' : 'REAL';
// 打印当前使用的服务类型 // 打印当前使用的服务类型
console.log( console.log(
`%c[Socket Service] Using ${SOCKET_TYPE} Socket Service`, '%c[Socket Service] Using REAL Socket Service',
`color: ${useMock ? '#FF9800' : '#4CAF50'}; font-weight: bold; font-size: 12px;` 'color: #4CAF50; font-weight: bold; font-size: 12px;'
); );
// ========== 暴露调试 API 到全局 ==========
if (typeof window !== 'undefined') {
// 暴露 Socket 类型到全局
window.SOCKET_TYPE = SOCKET_TYPE;
// 暴露调试 API
window.__SOCKET_DEBUG__ = {
// 获取当前连接状态
getStatus: () => {
const isConnected = socket.connected || false;
return {
type: SOCKET_TYPE,
connected: isConnected,
reconnectAttempts: socket.getReconnectAttempts?.() || 0,
maxReconnectAttempts: socket.getMaxReconnectAttempts?.() || Infinity,
service: useMock ? 'mockSocketService' : 'socketService',
};
},
// 手动重连
reconnect: () => {
console.log('[Socket Debug] Manual reconnect triggered');
if (socket.reconnect) {
socket.reconnect();
} else {
socket.disconnect();
socket.connect();
}
},
// 断开连接
disconnect: () => {
console.log('[Socket Debug] Manual disconnect triggered');
socket.disconnect();
},
// 连接
connect: () => {
console.log('[Socket Debug] Manual connect triggered');
socket.connect();
},
// 获取服务实例 (仅用于调试)
getService: () => socket,
// 导出诊断信息
exportDiagnostics: () => {
const status = window.__SOCKET_DEBUG__.getStatus();
const diagnostics = {
...status,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
env: {
NODE_ENV: process.env.NODE_ENV,
REACT_APP_ENABLE_MOCK: process.env.REACT_APP_ENABLE_MOCK,
REACT_APP_USE_MOCK_SOCKET: process.env.REACT_APP_USE_MOCK_SOCKET,
REACT_APP_API_URL: process.env.REACT_APP_API_URL,
REACT_APP_ENV: process.env.REACT_APP_ENV,
},
};
console.log('[Socket Diagnostics]', diagnostics);
return diagnostics;
},
// 手动订阅事件
subscribe: (options = {}) => {
const { eventType = 'all', importance = 'all' } = options;
console.log(`[Socket Debug] Subscribing to events: type=${eventType}, importance=${importance}`);
if (socket.subscribeToEvents) {
socket.subscribeToEvents({
eventType,
importance,
onNewEvent: (event) => {
console.log('[Socket Debug] ✅ New event received:', event);
},
onSubscribed: (data) => {
console.log('[Socket Debug] ✅ Subscription confirmed:', data);
},
});
} else {
console.error('[Socket Debug] ❌ subscribeToEvents method not available');
}
},
// 测试连接质量
testConnection: () => {
console.log('[Socket Debug] Testing connection...');
const start = Date.now();
if (socket.emit) {
socket.emit('ping', { timestamp: start }, (response) => {
const latency = Date.now() - start;
console.log(`[Socket Debug] ✅ Connection OK - Latency: ${latency}ms`, response);
});
} else {
console.error('[Socket Debug] ❌ Cannot test connection - socket.emit not available');
}
},
// 检查配置是否正确
checkConfig: () => {
const config = {
socketType: SOCKET_TYPE,
useMock,
envVars: {
REACT_APP_ENABLE_MOCK: process.env.REACT_APP_ENABLE_MOCK,
REACT_APP_USE_MOCK_SOCKET: process.env.REACT_APP_USE_MOCK_SOCKET,
NODE_ENV: process.env.NODE_ENV,
REACT_APP_API_URL: process.env.REACT_APP_API_URL,
},
socketMethods: {
connect: typeof socket.connect,
disconnect: typeof socket.disconnect,
on: typeof socket.on,
emit: typeof socket.emit,
subscribeToEvents: typeof socket.subscribeToEvents,
},
};
console.log('[Socket Debug] Configuration Check:', config);
// 检查潜在问题
const issues = [];
if (SOCKET_TYPE === 'MOCK' && process.env.NODE_ENV === 'production') {
issues.push('⚠️ WARNING: Using MOCK socket in production!');
}
if (!socket.subscribeToEvents) {
issues.push('❌ ERROR: subscribeToEvents method missing');
}
if (issues.length > 0) {
console.warn('[Socket Debug] Issues found:', issues);
} else {
console.log('[Socket Debug] ✅ No issues found');
}
return { config, issues };
},
};
console.log(
'%c[Socket Debug] Debug API available at window.__SOCKET_DEBUG__',
'color: #2196F3; font-weight: bold;'
);
console.log(
'%cTry: window.__SOCKET_DEBUG__.getStatus()',
'color: #2196F3;'
);
console.log(
'%c window.__SOCKET_DEBUG__.checkConfig() - 检查配置',
'color: #2196F3;'
);
console.log(
'%c window.__SOCKET_DEBUG__.subscribe() - 手动订阅事件',
'color: #2196F3;'
);
console.log(
'%c window.__SOCKET_DEBUG__.testConnection() - 测试连接',
'color: #2196F3;'
);
// ========== 通知系统专用调试 API ==========
window.__NOTIFY_DEBUG__ = {
// 完整检查(配置+连接+订阅状态)
checkAll: () => {
console.log('\n==========【通知系统诊断】==========');
// 1. 检查 Socket 配置
const socketCheck = window.__SOCKET_DEBUG__.checkConfig();
console.log('\n✓ Socket 配置检查完成');
// 2. 检查连接状态
const status = window.__SOCKET_DEBUG__.getStatus();
console.log('\n✓ 连接状态:', status.connected ? '✅ 已连接' : '❌ 未连接');
// 3. 检查环境变量
console.log('\n✓ API Base:', process.env.REACT_APP_API_URL || '(使用相对路径)');
// 4. 检查浏览器通知权限
const browserPermission = Notification?.permission || 'unsupported';
console.log('\n✓ 浏览器通知权限:', browserPermission);
// 5. 汇总报告
const report = {
timestamp: new Date().toISOString(),
socket: {
type: SOCKET_TYPE,
connected: status.connected,
reconnectAttempts: status.reconnectAttempts,
},
env: socketCheck.config.envVars,
browserNotification: browserPermission,
issues: socketCheck.issues,
};
console.log('\n========== 诊断报告 ==========');
console.table(report);
if (report.issues.length > 0) {
console.warn('\n⚠ 发现问题:', report.issues);
} else {
console.log('\n✅ 系统正常,未发现问题');
}
// 提供修复建议
if (!status.connected) {
console.log('\n💡 修复建议:');
console.log(' 1. 检查网络连接');
console.log(' 2. 尝试手动重连: __SOCKET_DEBUG__.reconnect()');
console.log(' 3. 检查后端服务是否运行');
}
if (browserPermission === 'denied') {
console.log('\n💡 浏览器通知已被拒绝,请在浏览器设置中允许通知权限');
}
console.log('\n====================================\n');
return report;
},
// 手动订阅事件(简化版)
subscribe: (eventType = 'all', importance = 'all') => {
console.log(`\n[通知调试] 手动订阅事件: type=${eventType}, importance=${importance}`);
window.__SOCKET_DEBUG__.subscribe({ eventType, importance });
},
// 模拟接收通知用于测试UI
testNotify: (type = 'announcement') => {
console.log('\n[通知调试] 模拟通知:', type);
const mockNotifications = {
announcement: {
id: `test_${Date.now()}`,
type: 'announcement',
priority: 'important',
title: '🧪 测试公告通知',
content: '这是一条测试消息,用于验证通知系统是否正常工作',
publishTime: Date.now(),
pushTime: Date.now(),
},
stock_alert: {
id: `test_${Date.now()}`,
type: 'stock_alert',
priority: 'urgent',
title: '🧪 测试股票预警',
content: '贵州茅台触发价格预警: 1850.00元 (+5.2%)',
publishTime: Date.now(),
pushTime: Date.now(),
},
event_alert: {
id: `test_${Date.now()}`,
type: 'event_alert',
priority: 'important',
title: '🧪 测试事件动向',
content: 'AI大模型新政策发布影响科技板块',
publishTime: Date.now(),
pushTime: Date.now(),
},
analysis_report: {
id: `test_${Date.now()}`,
type: 'analysis_report',
priority: 'normal',
title: '🧪 测试分析报告',
content: '2024年Q1市场策略报告已发布',
publishTime: Date.now(),
pushTime: Date.now(),
},
};
const notification = mockNotifications[type] || mockNotifications.announcement;
// 触发 new_event 事件
if (socket.emit) {
// 对于真实 Socket模拟服务端推送实际上客户端无法这样做仅用于Mock模式
console.warn('⚠️ 真实 Socket 无法模拟服务端推送,请使用 Mock 模式或等待真实推送');
}
// 直接触发事件监听器(如果是 Mock 模式)
if (SOCKET_TYPE === 'MOCK' && socket.emit) {
socket.emit('new_event', notification);
console.log('✅ 已触发 Mock 通知事件');
}
console.log('通知数据:', notification);
return notification;
},
// 导出完整诊断报告
exportReport: () => {
const report = window.__NOTIFY_DEBUG__.checkAll();
// 生成可下载的 JSON
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `notification-debug-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
console.log('✅ 诊断报告已导出');
return report;
},
// 快捷帮助
help: () => {
console.log('\n========== 通知系统调试 API ==========');
console.log('window.__NOTIFY_DEBUG__.checkAll() - 完整诊断检查');
console.log('window.__NOTIFY_DEBUG__.subscribe() - 手动订阅事件');
console.log('window.__NOTIFY_DEBUG__.testNotify(type) - 模拟通知 (announcement/stock_alert/event_alert/analysis_report)');
console.log('window.__NOTIFY_DEBUG__.exportReport() - 导出诊断报告');
console.log('\n========== Socket 调试 API ==========');
console.log('window.__SOCKET_DEBUG__.getStatus() - 获取连接状态');
console.log('window.__SOCKET_DEBUG__.checkConfig() - 检查配置');
console.log('window.__SOCKET_DEBUG__.reconnect() - 手动重连');
console.log('====================================\n');
},
};
console.log(
'%c[Notify Debug] Notification Debug API available at window.__NOTIFY_DEBUG__',
'color: #FF9800; font-weight: bold;'
);
console.log(
'%cTry: window.__NOTIFY_DEBUG__.checkAll() - 完整诊断',
'color: #FF9800;'
);
console.log(
'%c window.__NOTIFY_DEBUG__.help() - 查看所有命令',
'color: #FF9800;'
);
}
export default socket; export default socket;

View File

@@ -16,6 +16,7 @@ class SocketService {
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.maxReconnectAttempts = Infinity; // 无限重试 this.maxReconnectAttempts = Infinity; // 无限重试
this.customReconnectTimer = null; // 自定义重连定时器 this.customReconnectTimer = null; // 自定义重连定时器
this.pendingListeners = []; // 暂存等待注册的事件监听器
} }
/** /**
@@ -50,6 +51,15 @@ class SocketService {
...options, ...options,
}); });
// 注册所有暂存的事件监听器
if (this.pendingListeners.length > 0) {
console.log(`[socketService] 📦 注册 ${this.pendingListeners.length} 个暂存的事件监听器`);
this.pendingListeners.forEach(({ event, callback }) => {
this.on(event, callback);
});
this.pendingListeners = []; // 清空暂存队列
}
// 监听连接成功 // 监听连接成功
this.socket.on('connect', () => { this.socket.on('connect', () => {
this.connected = true; this.connected = true;
@@ -147,8 +157,10 @@ class SocketService {
*/ */
on(event, callback) { on(event, callback) {
if (!this.socket) { if (!this.socket) {
logger.warn('socketService', 'Cannot listen to event: socket not initialized', { event }); // Socket 未初始化,暂存监听器
console.warn(`[socketService] ❌ 无法监听事件 ${event}: Socket 未初始化`); logger.info('socketService', 'Socket not ready, queuing listener', { event });
console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
this.pendingListeners.push({ event, callback });
return; return;
} }