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
12 changed files with 203 additions and 449 deletions

View File

@@ -17,13 +17,13 @@ NODE_ENV=production
REACT_APP_ENABLE_MOCK=false
# 🔧 调试模式(生产环境临时调试用)
# 开启后会在全局暴露 window.__DEBUG__ 和 window.__TEST_NOTIFICATION__ 调试 API
# 开启后会在全局暴露 window.__DEBUG__ 调试 API
# ⚠️ 警告: 调试模式会记录所有 API 请求/响应,调试完成后请立即关闭!
# 使用方法:
# 1. 设置为 true 并重新构建
# 2. 在浏览器控制台使用 window.__DEBUG__.help() 查看命令
# 3. 调试完成后设置为 false 并重新构建
REACT_APP_ENABLE_DEBUG=true
REACT_APP_ENABLE_DEBUG=false
# 后端 API 地址(生产环境)
REACT_APP_API_URL=http://49.232.185.254:5001
@@ -49,18 +49,3 @@ TSC_COMPILE_ON_ERROR=true
IMAGE_INLINE_SIZE_LIMIT=10000
# Node.js 内存限制(适用于大型项目)
NODE_OPTIONS=--max_old_space_size=4096
# ========================================
# Bytedesk 客服系统配置
# ========================================
# Bytedesk 服务器地址
REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
# 组织 ID从管理后台获取
REACT_APP_BYTEDESK_ORG=df_org_uid
# 工作组 ID从管理后台获取
REACT_APP_BYTEDESK_SID=df_wg_aftersales
# 客服类型2=人工客服, 1=机器人)
REACT_APP_BYTEDESK_TYPE=2

View File

@@ -1,49 +0,0 @@
# Bytedesk 客服系统集成文件
以下文件和目录属于客服系统集成功能,未提交到当前分支:
## 1. Dify 机器人控制逻辑
**位置**: public/index.html
**状态**: 已存入 stash
**Stash ID**: stash@{0}
**说明**: 根据路径控制 Dify 机器人显示(已设置为完全不显示,只使用 Bytedesk 客服)
## 2. Bytedesk 集成代码
**位置**: src/bytedesk-integration/
**状态**: 未跟踪文件(需要手动管理)
**内容**:
- .env.bytedesk.example - Bytedesk 环境变量配置示例
- App.jsx.example - 集成 Bytedesk 的示例代码
- components/ - Bytedesk 相关组件
- config/ - Bytedesk 配置文件
- 前端工程师集成手册.md - 详细集成文档
## 恢复方法
### 恢复 public/index.html 的改动:
```bash
git stash apply stash@{0}
```
### 使用 Bytedesk 集成代码:
```bash
# 查看集成手册
cat src/bytedesk-integration/前端工程师集成手册.md
# 复制示例配置
cp src/bytedesk-integration/.env.bytedesk.example .env.bytedesk
cp src/bytedesk-integration/App.jsx.example src/App.jsx
```
## 注意事项
⚠️ **重要提示:**
- `src/bytedesk-integration/` 目录中的文件是未跟踪的untracked
- 如果需要提交客服功能,需要先添加到 git:
```bash
git add src/bytedesk-integration/
git commit -m "feat: 集成 Bytedesk 客服系统"
```
- 当前分支feature_bugfix/251110_event专注于非客服功能
- 建议在单独的分支中开发客服功能

View File

@@ -65,9 +65,6 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<!-- ============================================
Dify 机器人配置 - 只在 /home 页面显示
============================================ -->
<script>
window.difyChatbotConfig = {
token: 'DwN8qAKtYFQtWskM',
@@ -87,45 +84,91 @@
// 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>
<!-- Dify 机器人显示控制脚本 -->
<script>
// 控制 Dify 机器人只在 /home 页面显示
function controlDifyVisibility() {
const currentPath = window.location.pathname;
const difyChatButton = document.getElementById('dify-chatbot-bubble-button');
if (difyChatButton) {
// 只在 /home 页面显示
if (currentPath === '/home') {
difyChatButton.style.display = 'flex';
console.log('[Dify] 显示机器人(当前路径: /home');
} else {
difyChatButton.style.display = 'none';
console.log('[Dify] 隐藏机器人(当前路径:', currentPath, '');
}
}
}
// 页面加载完成后执行
window.addEventListener('load', function() {
console.log('[Dify] 初始化显示控制');
// 初始检查(延迟执行,等待 Dify 按钮渲染)
setTimeout(controlDifyVisibility, 500);
setTimeout(controlDifyVisibility, 1500);
// 监听路由变化React Router 使用 pushState
const observer = setInterval(controlDifyVisibility, 1000);
// 清理函数(可选)
window.addEventListener('beforeunload', function() {
clearInterval(observer);
});
});
</script>
<script
src="https://app.valuefrontier.cn/embed.min.js"
id="DwN8qAKtYFQtWskM"
@@ -207,7 +250,7 @@
bottom: 80px !important;
left: 10px !important;
}
#dify-chatbot-bubble-button {
width: 56px !important;
height: 56px !important;

View File

@@ -35,7 +35,7 @@ self.addEventListener('notificationclick', (event) => {
.then((windowClients) => {
// 查找是否已有打开的窗口
for (let client of windowClients) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
if (client.url.includes(window.location.origin) && 'focus' in client) {
// 聚焦现有窗口并导航到目标页面
return client.focus().then(client => {
return client.navigate(urlToOpen);

View File

@@ -82,28 +82,65 @@ const BytedeskWidget = ({
return () => {
console.log('[Bytedesk] 清理Widget');
// 移除脚本
if (scriptRef.current && document.body.contains(scriptRef.current)) {
document.body.removeChild(scriptRef.current);
}
// 移除Widget DOM元素
const widgetElements = document.querySelectorAll('[class*="bytedesk"], [id*="bytedesk"]');
widgetElements.forEach(el => {
if (el && el.parentNode) {
el.parentNode.removeChild(el);
try {
// 调用Widget的destroy方法如果存在
if (widgetRef.current && typeof widgetRef.current.destroy === 'function') {
console.log('[Bytedesk] 调用Widget.destroy()');
widgetRef.current.destroy();
}
});
// 清理全局对象
if (window.BytedeskWeb) {
delete window.BytedeskWeb;
} 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会自动插入到body
return <div id="bytedesk-widget-container" style={{ display: 'none' }} />;
// 不渲染任何元素Widget会自动插入DOM到body
// 返回null避免React DOM管理冲突
return null;
};
BytedeskWidget.propTypes = {

View File

@@ -64,16 +64,16 @@ export const bytedeskConfig = {
* @returns {Object} Bytedesk配置对象
*/
export const getBytedeskConfig = () => {
// 开发环境使用代理(绕过 X-Frame-Options 限制)
// 开发环境使用代理绕过X-Frame-Options限制
if (process.env.NODE_ENV === 'development') {
return {
...bytedeskConfig,
apiUrl: '/bytedesk-api', // 使用 CRACO 代理路径
htmlUrl: '/bytedesk-api/chat/', // 使用 CRACO 代理路径
apiUrl: '/bytedesk-api',
htmlUrl: '/bytedesk-api/chat/',
};
}
// 生产环境使用完整 URL
// 生产环境使用完整URL
return bytedeskConfig;
};
@@ -118,33 +118,8 @@ export const getBytedeskConfigWithUser = (user) => {
* @returns {boolean} 是否显示客服
*/
export const shouldShowCustomerService = (pathname) => {
// 在以下页面隐藏客服(黑名单)
const blockedPages = [
'/home', // 登录页
];
// 检查是否在黑名单
if (blockedPages.some(page => pathname.startsWith(page))) {
return false;
}
// 默认所有页面都显示客服
// 所有页面都显示Bytedesk客服
return true;
/* ============================================
白名单模式(备用,需要时取消注释)
============================================
const allowedPages = [
'/', // 首页
'/home', // 主页
'/products', // 产品页
'/pricing', // 价格页
'/contact', // 联系我们
];
// 只在白名单页面显示客服
return allowedPages.some(page => pathname.startsWith(page));
============================================ */
};
export default {

View File

@@ -59,11 +59,6 @@ export const NotificationProvider = ({ children }) => {
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID避免内存泄漏
const notificationTimers = useRef(new Map()); // 跟踪所有通知的自动关闭定时器
// ⚡ 方案2: 使用 Ref 存储最新的回调函数引用(避免闭包陷阱)
const addNotificationRef = useRef(null);
const adaptEventToNotificationRef = useRef(null);
const isFirstConnect = useRef(true); // 标记是否首次连接
// ⚡ 使用权限引导管理 Hook
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
@@ -600,42 +595,26 @@ export const NotificationProvider = ({ children }) => {
return newNotification.id;
}, [notifications, toast, sendBrowserNotification, addWebNotification, browserPermission, hasRequestedPermission, requestBrowserPermission]);
/**
* ✅ 方案2: 同步最新的回调函数到 Ref
* 确保 Socket 监听器始终使用最新的函数引用(避免闭包陷阱)
*/
useEffect(() => {
addNotificationRef.current = addNotification;
console.log('[NotificationContext] 📝 已更新 addNotificationRef');
}, [addNotification]);
useEffect(() => {
adaptEventToNotificationRef.current = adaptEventToNotification;
console.log('[NotificationContext] 📝 已更新 adaptEventToNotificationRef');
}, [adaptEventToNotification]);
// ========== 连接到 Socket 服务(⚡ 方案2: 只执行一次) ==========
// 连接到 Socket 服务
useEffect(() => {
logger.info('NotificationContext', 'Initializing socket connection...');
console.log('%c[NotificationContext] 🚀 初始化 Socket 连接方案2只注册一次', '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;');
// 监听连接状态
socket.on('connect', () => {
const wasDisconnected = connectionStatus !== CONNECTION_STATUS.CONNECTED;
setIsConnected(true);
setReconnectAttempt(0);
logger.info('NotificationContext', 'Socket connected', { wasDisconnected });
console.log('%c[NotificationContext] ✅ Received connect event, updating state to connected', 'color: #4CAF50; font-weight: bold;');
// 判断是首次连接还是重连
if (isFirstConnect.current) {
console.log('%c[NotificationContext] ✅ 首次连接成功', 'color: #4CAF50; font-weight: bold;');
console.log('[NotificationContext] Socket ID:', socket.getSocketId?.());
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
isFirstConnect.current = false;
logger.info('NotificationContext', 'Socket connected (first time)');
} else {
console.log('%c[NotificationContext] 🔄 重连成功!', 'color: #FF9800; font-weight: bold;');
// 如果之前断开过,显示 RECONNECTED 状态2秒后自动消失
if (wasDisconnected) {
setConnectionStatus(CONNECTION_STATUS.RECONNECTED);
logger.info('NotificationContext', 'Socket reconnected');
logger.info('NotificationContext', 'Reconnected, will auto-dismiss in 2s');
// 清除之前的定时器
if (reconnectedTimerRef.current) {
@@ -647,10 +626,12 @@ export const NotificationProvider = ({ children }) => {
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status');
}, 2000);
} else {
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
}
// ⚡ 重连后只需重新订阅,不需要重新注册监听器
console.log('%c[NotificationContext] 🔔 重新订阅事件推送...', 'color: #FF9800; font-weight: bold;');
// 订阅事件推送
console.log('%c[NotificationContext] 🔔 订阅事件推送...', 'color: #FF9800; font-weight: bold;');
if (socket.subscribeToEvents) {
socket.subscribeToEvents({
@@ -661,47 +642,45 @@ export const NotificationProvider = ({ children }) => {
console.log('[NotificationContext] 订阅确认:', data);
logger.info('NotificationContext', 'Events subscribed', data);
},
// ⚠️ 不需要 onNewEvent 回调,因为 NotificationContext 已经通过 socket.on('new_event') 监听
});
} else {
console.error('[NotificationContext] ❌ socket.subscribeToEvents 方法不可用');
}
});
// ========== 监听断开连接 ==========
socket.on('disconnect', (reason) => {
setIsConnected(false);
setConnectionStatus(CONNECTION_STATUS.DISCONNECTED);
logger.warn('NotificationContext', 'Socket disconnected', { reason });
console.log('%c[NotificationContext] ⚠️ Socket 已断开', 'color: #FF5722;', { reason });
});
// ========== 监听连接错误 ==========
// 监听连接错误
socket.on('connect_error', (error) => {
logger.error('NotificationContext', 'Socket connect_error', error);
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
// 获取重连次数
const attempts = socket.getReconnectAttempts?.() || 0;
setReconnectAttempt(attempts);
logger.info('NotificationContext', 'Reconnection attempt', { attempts });
console.log(`%c[NotificationContext] 🔄 重连中... (第 ${attempts} 次尝试)`, 'color: #FF9800;');
});
// ========== 监听重连失败 ==========
// 监听重连失败
socket.on('reconnect_failed', () => {
logger.error('NotificationContext', 'Socket reconnect_failed');
setConnectionStatus(CONNECTION_STATUS.FAILED);
console.error('[NotificationContext] ❌ 重连失败');
toast({
title: '连接失败',
description: '无法连接到服务器,请检查网络连接',
status: 'error',
duration: null,
duration: null, // 不自动关闭
isClosable: true,
});
});
// ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数) ==========
// 监听新事件推送(统一事件名)
socket.on('new_event', (data) => {
console.log('\n%c════════════════════════════════════════', 'color: #FF9800; font-weight: bold;');
console.log('%c[NotificationContext] 📨 收到 new_event 事件!', 'color: #FF9800; font-weight: bold;');
@@ -714,24 +693,17 @@ export const NotificationProvider = ({ children }) => {
logger.info('NotificationContext', 'Received new event', data);
// ⚠️ 防御性检查:确保 ref 已初始化
if (!addNotificationRef.current || !adaptEventToNotificationRef.current) {
console.error('%c[NotificationContext] ❌ Ref 未初始化,跳过处理', 'color: #F44336; font-weight: bold;');
logger.error('NotificationContext', 'Refs not initialized', {
addNotificationRef: !!addNotificationRef.current,
adaptEventToNotificationRef: !!adaptEventToNotificationRef.current,
});
return;
}
// ========== Socket层去重检查 ==========
const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 生成更健壮的事件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,
title: data.title
});
}
@@ -739,61 +711,55 @@ export const NotificationProvider = ({ children }) => {
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
console.warn('[NotificationContext] ⚠️ 重复事件,已忽略:', eventId);
console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;');
return;
return; // 重复事件,直接忽略
}
// 记录已处理的事件ID
processedEventIds.current.add(eventId);
console.log('[NotificationContext] ✓ 事件已记录,防止重复处理');
// 限制 Set 大小,避免内存泄漏
// 限制Set大小避免内存泄漏
if (processedEventIds.current.size > MAX_PROCESSED_IDS) {
const idsArray = Array.from(processedEventIds.current);
processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS));
logger.debug('NotificationContext', 'Cleaned up old processed event IDs', {
kept: MAX_PROCESSED_IDS,
kept: MAX_PROCESSED_IDS
});
}
// ========== Socket层去重检查结束 ==========
// ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱)
// 使用适配器转换事件格式
console.log('[NotificationContext] 正在转换事件格式...');
const notification = adaptEventToNotificationRef.current(data);
const notification = adaptEventToNotification(data);
console.log('[NotificationContext] 转换后的通知对象:', notification);
// ✅ 使用 ref.current 访问最新的 addNotification 函数
console.log('[NotificationContext] 准备添加通知到队列...');
addNotificationRef.current(notification);
addNotification(notification);
console.log('[NotificationContext] ✅ 通知已添加到队列');
console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;');
});
// ========== 监听系统通知(兼容性) ==========
// 保留系统通知监听(兼容性)
socket.on('system_notification', (data) => {
logger.info('NotificationContext', 'Received system notification', data);
console.log('[NotificationContext] 📢 收到系统通知:', data);
if (addNotificationRef.current) {
addNotificationRef.current(data);
} else {
console.error('[NotificationContext] ❌ addNotificationRef 未初始化');
}
addNotification(data);
});
console.log('%c[NotificationContext] ✅ 所有监听器已注册(只注册一次)', 'color: #4CAF50; font-weight: bold;');
console.log('%c[NotificationContext] ✅ All event listeners registered', 'color: #4CAF50; font-weight: bold;');
// ========== 获取最大重连次数 ==========
// ✅ 第二步: 获取最大重连次数
const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity;
setMaxReconnectAttempts(maxAttempts);
logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts });
// ========== 启动连接 ==========
console.log('%c[NotificationContext] 🔌 调用 socket.connect()...', 'color: #673AB7; font-weight: bold;');
// ✅ 第三步: 调用 socket.connect()
console.log('%c[NotificationContext] Step 2: Calling socket.connect()...', 'color: #673AB7; font-weight: bold;');
socket.connect();
console.log('%c[NotificationContext] socket.connect() completed', 'color: #673AB7;');
// ========== 清理函数(组件卸载时) ==========
// 清理函数
return () => {
logger.info('NotificationContext', 'Cleaning up socket connection');
console.log('%c[NotificationContext] 🧹 清理 Socket 连接', 'color: #9E9E9E;');
// 清理 reconnected 状态定时器
if (reconnectedTimerRef.current) {
@@ -808,20 +774,15 @@ export const NotificationProvider = ({ children }) => {
});
notificationTimers.current.clear();
// 移除所有事件监听器
socket.off('connect');
socket.off('disconnect');
socket.off('connect_error');
socket.off('reconnect_failed');
socket.off('new_event');
socket.off('system_notification');
// 断开连接
socket.disconnect();
console.log('%c[NotificationContext] ✅ 清理完成', 'color: #4CAF50;');
};
}, []); // ⚠️ 空依赖数组确保只执行一次
}, []); // 空依赖数组,确保只执行一次,避免 React 严格模式重复执行
// ==================== 智能自动重试 ====================
@@ -935,92 +896,6 @@ export const NotificationProvider = ({ children }) => {
};
}, [browserPermission, toast]);
// 🔧 开发环境调试:暴露方法到 window
useEffect(() => {
if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_DEBUG === 'true') {
if (typeof window !== 'undefined') {
window.__TEST_NOTIFICATION__ = {
// 手动触发网页通知
testWebNotification: (type = 'event_alert', priority = 'normal') => {
console.log('%c[Debug] 手动触发网页通知', 'color: #FF9800; font-weight: bold;');
const testData = {
id: `test_${Date.now()}`,
type: type,
priority: priority,
title: '🧪 测试网页通知',
content: `这是一条测试${type === 'announcement' ? '公告' : type === 'stock_alert' ? '股票' : type === 'event_alert' ? '事件' : '分析'}通知 (优先级: ${priority})`,
timestamp: Date.now(),
clickable: true,
link: '/home',
};
console.log('测试数据:', testData);
addNotification(testData);
console.log('✅ 通知已添加到队列');
},
// 测试所有类型
testAllTypes: () => {
console.log('%c[Debug] 测试所有通知类型', 'color: #FF9800; font-weight: bold;');
const types = ['announcement', 'stock_alert', 'event_alert', 'analysis_report'];
types.forEach((type, i) => {
setTimeout(() => {
window.__TEST_NOTIFICATION__.testWebNotification(type, 'normal');
}, i * 2000); // 每 2 秒一个
});
},
// 测试所有优先级
testAllPriorities: () => {
console.log('%c[Debug] 测试所有优先级', 'color: #FF9800; font-weight: bold;');
const priorities = ['normal', 'important', 'urgent'];
priorities.forEach((priority, i) => {
setTimeout(() => {
window.__TEST_NOTIFICATION__.testWebNotification('event_alert', priority);
}, i * 2000);
});
},
// 帮助
help: () => {
console.log('\n%c=== 网页通知测试 API ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
console.log('\n%c基础用法:', 'color: #2196F3; font-weight: bold;');
console.log(' window.__TEST_NOTIFICATION__.testWebNotification(type, priority)');
console.log('\n%c参数说明:', 'color: #2196F3; font-weight: bold;');
console.log(' type (通知类型):');
console.log(' - "announcement" 公告通知(蓝色)');
console.log(' - "stock_alert" 股票动向(红色/绿色)');
console.log(' - "event_alert" 事件动向(橙色)');
console.log(' - "analysis_report" 分析报告(紫色)');
console.log('\n priority (优先级):');
console.log(' - "normal" 普通15秒自动关闭');
console.log(' - "important" 重要30秒自动关闭');
console.log(' - "urgent" 紧急(不自动关闭)');
console.log('\n%c示例:', 'color: #4CAF50; font-weight: bold;');
console.log(' // 测试紧急事件通知');
console.log(' window.__TEST_NOTIFICATION__.testWebNotification("event_alert", "urgent")');
console.log('\n // 测试所有类型');
console.log(' window.__TEST_NOTIFICATION__.testAllTypes()');
console.log('\n // 测试所有优先级');
console.log(' window.__TEST_NOTIFICATION__.testAllPriorities()');
console.log('\n');
}
};
console.log('[NotificationContext] 🔧 调试 API 已加载: window.__TEST_NOTIFICATION__');
console.log('[NotificationContext] 💡 使用 window.__TEST_NOTIFICATION__.help() 查看帮助');
}
}
// 清理函数
return () => {
if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
delete window.__TEST_NOTIFICATION__;
}
};
}, [addNotification]); // 依赖 addNotification 函数
const value = {
notifications,
isConnected,

View File

@@ -71,10 +71,7 @@ class DebugToolkit {
console.log('');
console.log('%c2⃣ 通知调试:', 'color: #9C27B0; font-weight: bold;');
console.log(' __DEBUG__.notification.getLogs() - 获取所有通知日志');
console.log(' __DEBUG__.notification.forceNotification() - 发送测试浏览器通知');
console.log(' __DEBUG__.notification.testWebNotification(type, priority) - 测试网页通知 🆕');
console.log(' __DEBUG__.notification.testAllNotificationTypes() - 测试所有类型 🆕');
console.log(' __DEBUG__.notification.testAllNotificationPriorities() - 测试所有优先级 🆕');
console.log(' __DEBUG__.notification.forceNotification() - 发送测试通知');
console.log(' __DEBUG__.notification.checkPermission() - 检查通知权限');
console.log(' __DEBUG__.notification.exportLogs() - 导出通知日志');
console.log('');

View File

@@ -159,44 +159,6 @@ class NotificationDebugger {
getRecentEvents(count = 10) {
return this.eventLog.slice(0, count);
}
/**
* 测试网页通知(需要 window.__TEST_NOTIFICATION__ 可用)
*/
testWebNotification(type = 'event_alert', priority = 'normal') {
if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
console.log('[Notification Debugger] 调用测试 API');
window.__TEST_NOTIFICATION__.testWebNotification(type, priority);
} else {
console.error('[Notification Debugger] ❌ window.__TEST_NOTIFICATION__ 不可用');
console.error('💡 请确保:');
console.error(' 1. REACT_APP_ENABLE_DEBUG=true');
console.error(' 2. NotificationContext 已加载');
console.error(' 3. 页面已刷新');
}
}
/**
* 测试所有通知类型
*/
testAllNotificationTypes() {
if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
window.__TEST_NOTIFICATION__.testAllTypes();
} else {
console.error('[Notification Debugger] ❌ window.__TEST_NOTIFICATION__ 不可用');
}
}
/**
* 测试所有优先级
*/
testAllNotificationPriorities() {
if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
window.__TEST_NOTIFICATION__.testAllPriorities();
} else {
console.error('[Notification Debugger] ❌ window.__TEST_NOTIFICATION__ 不可用');
}
}
}
// 导出单例

View File

@@ -44,64 +44,24 @@ function registerServiceWorker() {
navigator.serviceWorker
.register('/service-worker.js')
.then((registration) => {
console.log('[App] Service Worker 注册成功');
console.log('[App] Scope:', registration.scope);
console.log('[App] Service Worker registered successfully:', registration.scope);
// 检查当前激活状态
if (navigator.serviceWorker.controller) {
console.log('[App] ✅ Service Worker 已激活并控制页面');
} else {
console.log('[App] ⏳ Service Worker 已注册,等待激活...');
console.log('[App] 💡 刷新页面以激活 Service Worker');
// 监听 controller 变化Service Worker 激活后触发)
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('[App] ✅ Service Worker 控制器已更新');
});
}
// 监听 Service Worker 更新
// 监听更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('[App] 🔄 发现 Service Worker 更新');
console.log('[App] Service Worker update found');
if (newWorker) {
newWorker.addEventListener('statechange', () => {
console.log(`[App] Service Worker 状态: ${newWorker.state}`);
if (newWorker.state === 'activated') {
console.log('[App] Service Worker 已激活');
// 如果有旧的 Service Worker 在控制页面,提示用户刷新
if (navigator.serviceWorker.controller) {
console.log('[App] 💡 Service Worker 已更新,建议刷新页面');
}
console.log('[App] Service Worker activated');
}
});
}
});
})
.catch((error) => {
console.error('[App] Service Worker 注册失败');
console.error('[App] 错误类型:', error.name);
console.error('[App] 错误信息:', error.message);
console.error('[App] 完整错误:', error);
// 额外检查:验证文件是否可访问
fetch('/service-worker.js', { method: 'HEAD' })
.then(response => {
if (response.ok) {
console.error('[App] Service Worker 文件存在但注册失败');
console.error('[App] 💡 可能的原因:');
console.error('[App] 1. Service Worker 文件有语法错误');
console.error('[App] 2. 浏览器不支持某些 Service Worker 特性');
console.error('[App] 3. HTTPS 证书问题Service Worker 需要 HTTPS');
} else {
console.error('[App] Service Worker 文件不存在HTTP', response.status, '');
}
})
.catch(fetchError => {
console.error('[App] 无法访问 Service Worker 文件:', fetchError.message);
});
console.error('[App] Service Worker registration failed:', error);
});
});
} else {

View File

@@ -10,21 +10,6 @@ import { socketService } from '../socketService';
export const socket = socketService;
export { socketService };
// ⚡ 新增:暴露 Socket 实例到 window用于调试和验证
if (typeof window !== 'undefined') {
window.socket = socketService;
window.socketService = socketService;
console.log(
'%c[Socket Service] ✅ Socket instance exposed to window',
'color: #4CAF50; font-weight: bold; font-size: 14px;'
);
console.log(' 📍 window.socket:', window.socket);
console.log(' 📍 window.socketService:', window.socketService);
console.log(' 📍 Socket.IO instance:', window.socket?.socket);
console.log(' 📍 Connection status:', window.socket?.connected ? '✅ Connected' : '❌ Disconnected');
}
// 打印当前使用的服务类型
console.log(
'%c[Socket Service] Using REAL Socket Service',

View File

@@ -51,21 +51,13 @@ class SocketService {
...options,
});
// 注册所有暂存的事件监听器(保留 pendingListeners不清空
// 注册所有暂存的事件监听器
if (this.pendingListeners.length > 0) {
console.log(`[socketService] 📦 注册 ${this.pendingListeners.length} 个暂存的事件监听器`);
this.pendingListeners.forEach(({ event, callback }) => {
// 直接在 Socket.IO 实例上注册(避免递归调用 this.on()
const wrappedCallback = (...args) => {
console.log(`%c[socketService] 🔔 收到原始事件: ${event}`, 'color: #2196F3; font-weight: bold;');
console.log(`[socketService] 事件数据 (${event}):`, ...args);
callback(...args);
};
this.socket.on(event, wrappedCallback);
console.log(`[socketService] ✓ 已注册事件监听器: ${event}`);
this.on(event, callback);
});
// ⚠️ 重要:不清空 pendingListeners,保留用于重连
this.pendingListeners = []; // 清空暂存队列
}
// 监听连接成功
@@ -165,18 +157,10 @@ class SocketService {
*/
on(event, callback) {
if (!this.socket) {
// Socket 未初始化,暂存监听器(检查是否已存在,避免重复)
const exists = this.pendingListeners.some(
(listener) => listener.event === event && listener.callback === callback
);
if (!exists) {
logger.info('socketService', 'Socket not ready, queuing listener', { event });
console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
this.pendingListeners.push({ event, callback });
} else {
console.log(`[socketService] ⚠️ 监听器已存在,跳过: ${event}`);
}
// Socket 未初始化,暂存监听器
logger.info('socketService', 'Socket not ready, queuing listener', { event });
console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
this.pendingListeners.push({ event, callback });
return;
}