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
18 changed files with 418 additions and 483 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,20 +49,3 @@ TSC_COMPILE_ON_ERROR=true
IMAGE_INLINE_SIZE_LIMIT=10000
# Node.js 内存限制(适用于大型项目)
NODE_OPTIONS=--max_old_space_size=4096
# ========================================
# Bytedesk 客服系统配置
# ========================================
# Bytedesk 服务器地址(使用相对路径,通过 Nginx 代理)
# ⚠️ 重要:生产环境必须使用相对路径,避免 Mixed Content 错误
# Nginx 配置location /bytedesk-api/ { proxy_pass http://43.143.189.195/; }
REACT_APP_BYTEDESK_API_URL=/bytedesk-api
# 组织 UUID从管理后台 -> 设置 -> 组织信息 -> 组织UUID
REACT_APP_BYTEDESK_ORG=df_org_uid
# 工作组 UUID从管理后台 -> 客服管理 -> 工作组 -> 工作组UUID
REACT_APP_BYTEDESK_SID=df_wg_uid
# 客服类型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

@@ -110,6 +110,9 @@ module.exports = {
...webpackConfig.resolve,
alias: {
...webpackConfig.resolve.alias,
// 强制 'debug' 模块解析到 node_modules避免与 src/devtools/ 冲突)
'debug': path.resolve(__dirname, 'node_modules/debug'),
// 根目录别名
'@': path.resolve(__dirname, 'src'),
@@ -119,6 +122,7 @@ module.exports = {
'@constants': path.resolve(__dirname, 'src/constants'),
'@contexts': path.resolve(__dirname, 'src/contexts'),
'@data': path.resolve(__dirname, 'src/data'),
'@devtools': path.resolve(__dirname, 'src/devtools'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
'@layouts': path.resolve(__dirname, 'src/layouts'),
'@lib': path.resolve(__dirname, 'src/lib'),
@@ -263,13 +267,33 @@ module.exports = {
logLevel: 'debug',
pathRewrite: { '^/concept-api': '' },
},
'/bytedesk': {
target: 'https://valuefrontier.cn', // 统一使用生产环境 Nginx 代理
'/bytedesk-api': {
target: 'http://43.143.189.195',
changeOrigin: true,
secure: false, // 开发环境禁用 HTTPS 严格验证
secure: false,
logLevel: 'debug',
ws: true, // 支持 WebSocket
// 不使用 pathRewrite保留 /bytedesk 前缀,让生产 Nginx 处理
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

@@ -1,3 +1,21 @@
<!--
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
-->
<!DOCTYPE html>
<html lang="en" dir="ltr" layout="admin">
<head>
@@ -7,6 +25,10 @@
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" />
<link
@@ -14,11 +36,226 @@
sizes="76x76"
href="%PUBLIC_URL%/apple-icon.png"
/>
<link rel="shortcut icon" type="image/x-icon" href="./favicon.png" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.png" or "favicon.png", "%PUBLIC_URL%/favicon.png" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>价值前沿——LLM赋能的分析平台</title>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script>
window.difyChatbotConfig = {
token: 'DwN8qAKtYFQtWskM',
baseUrl: 'https://app.valuefrontier.cn',
inputs: {
// You can define the inputs from the Start node here
// key is the variable name
// e.g.
// name: "NAME"
},
systemVariables: {
// user_id: 'YOU CAN DEFINE USER ID HERE',
// conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
},
userVariables: {
// avatar_url: 'YOU CAN DEFINE USER AVATAR URL 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
src="https://app.valuefrontier.cn/embed.min.js"
id="DwN8qAKtYFQtWskM"
defer>
</script>
<style>
#dify-chatbot-bubble-button {
background-color: #1C64F2 !important;
width: 60px !important;
height: 60px !important;
box-shadow: 0 4px 12px rgba(28, 100, 242, 0.3) !important;
transition: all 0.3s ease !important;
}
#dify-chatbot-bubble-button:hover {
transform: scale(1.1) !important;
box-shadow: 0 6px 16px rgba(28, 100, 242, 0.4) !important;
}
#dify-chatbot-bubble-window {
width: 42rem !important;
height: 80vh !important;
max-height: calc(100vh - 2rem) !important;
position: fixed !important;
bottom: 100px !important;
right: 20px !important;
border-radius: 16px !important;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15) !important;
border: 1px solid rgba(28, 100, 242, 0.1) !important;
z-index: 9999 !important;
}
/* 确保Dify聊天窗口中的超链接正确显示 */
#dify-chatbot-bubble-window a,
#dify-chatbot-bubble-window a:link,
#dify-chatbot-bubble-window a:visited,
#dify-chatbot-bubble-window a:hover,
#dify-chatbot-bubble-window a:active {
color: #1C64F2 !important;
text-decoration: underline !important;
cursor: pointer !important;
pointer-events: auto !important;
}
/* 确保超链接在Dify消息区域中可见 */
#dify-chatbot-bubble-window .message-content a,
#dify-chatbot-bubble-window .markdown-content a,
#dify-chatbot-bubble-window [class*="message"] a {
color: #0066cc !important;
text-decoration: underline !important;
font-weight: 500 !important;
}
/* 桌面端大屏优化 */
@media (min-width: 1440px) {
#dify-chatbot-bubble-window {
width: 45rem !important;
height: 85vh !important;
}
}
/* 平板端适配 */
@media (max-width: 1024px) and (min-width: 641px) {
#dify-chatbot-bubble-window {
width: 38rem !important;
height: 75vh !important;
right: 15px !important;
bottom: 90px !important;
}
}
/* 移动端适配 */
@media (max-width: 640px) {
#dify-chatbot-bubble-window {
width: calc(100vw - 20px) !important;
height: 85vh !important;
max-height: 85vh !important;
right: 10px !important;
bottom: 80px !important;
left: 10px !important;
}
#dify-chatbot-bubble-button {
width: 56px !important;
height: 56px !important;
}
}
</style>
</body>
</html>

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

@@ -29,16 +29,15 @@ REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
# Bytedesk组织和工作组配置必需
# ============================================================================
# 组织 UUIDOrganization UUID
# 获取方式: 登录管理后台 -> 设置 -> 组织信息 -> 组织UUID
# 注意: 不是"组织代码",是"组织UUID"df_org_uid
# 当前配置: df_org_uid默认组织
# 组织IDOrganization UID
# 获取方式: 登录管理后台 -> 设置 -> 组织信息 -> 复制UID
# 示例: df_org_uid
REACT_APP_BYTEDESK_ORG=df_org_uid
# 工作组 UUIDWorkgroup UUID
# 获取方式: 登录管理后台 -> 客服管理 -> 工作组 -> 工作组UUID
# 当前配置: df_wg_uid默认工作组
REACT_APP_BYTEDESK_SID=df_wg_uid
# 工作组IDWorkgroup SID
# 获取方式: 登录管理后台 -> 客服管理 -> 工作组 -> 复制工作组ID
# 示例: df_wg_aftersales (售后服务组)
REACT_APP_BYTEDESK_SID=df_wg_aftersales
# ============================================================================
# 可选配置

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

@@ -1,35 +1,26 @@
/**
* Bytedesk客服配置文件
* 通过代理访问 Bytedesk 服务器(解决 HTTPS 混合内容问题)
* 指向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_uid
*
* 架构说明:
* - iframe 使用完整域名https://valuefrontier.cn/bytedesk/chat/
* - 使用 HTTPS 协议,解决生产环境 Mixed Content 错误
* - 本地CRACO 代理 /bytedesk → valuefrontier.cn/bytedesk
* - 生产:前端 Nginx 代理 /bytedesk → 43.143.189.195
* - baseUrl 保持官方 CDN用于加载 SDK 外部模块)
*
* ⚠️ 注意:需要前端 Nginx 配置 /bytedesk/ 代理规则
* 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_uid';
const BYTEDESK_SID = process.env.REACT_APP_BYTEDESK_SID || 'df_wg_aftersales';
/**
* Bytedesk客服基础配置
*/
export const bytedeskConfig = {
// API服务地址(如果 SDK 需要调用 API
apiUrl: '/bytedesk/',
// 聊天页面地址(使用完整 HTTPS 域名,通过 /bytedesk/ 代理避免 React Router 冲突)
htmlUrl: 'https://valuefrontier.cn/chat/',
// SDK 资源基础路径(保持 Bytedesk 官方 CDN用于加载外部模块
baseUrl: 'https://www.weiyuai.cn',
// API服务地址
apiUrl: BYTEDESK_API_URL,
// 聊天页面地址
htmlUrl: `${BYTEDESK_API_URL}/chat/`,
// 客服图标位置
placement: 'bottom-right', // bottom-right | bottom-left | top-right | top-left
@@ -62,7 +53,7 @@ export const bytedeskConfig = {
// 聊天配置(必需)
chatConfig: {
org: BYTEDESK_ORG, // 组织ID
t: '1', // 类型: 1=人工客服, 2=机器人
t: '2', // 类型: 2=客服, 1=机器人
sid: BYTEDESK_SID, // 工作组ID
},
};
@@ -73,7 +64,16 @@ export const bytedeskConfig = {
* @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;
};
@@ -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

@@ -6,9 +6,6 @@ import { BrowserRouter as Router } from 'react-router-dom';
import './styles/brainwave.css';
import './styles/brainwave-colors.css';
// 导入 Bytedesk 客服系统 z-index 覆盖样式(必须在所有样式之后导入)
import './styles/bytedesk-override.css';
// Import the main App component
import App from './App';
@@ -47,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

@@ -217,7 +217,7 @@ export const REVENUE_EVENTS = {
};
// ============================================================================
// SPECIAL EVENTS (特殊事件) - Errors, performance
// SPECIAL EVENTS (特殊事件) - Errors, performance, chatbot
// ============================================================================
export const SPECIAL_EVENTS = {
// Errors
@@ -229,6 +229,13 @@ export const SPECIAL_EVENTS = {
PAGE_LOAD_TIME: 'Page Load Time',
API_RESPONSE_TIME: 'API Response Time',
// Chatbot (Dify)
CHATBOT_OPENED: 'Chatbot Opened',
CHATBOT_CLOSED: 'Chatbot Closed',
CHATBOT_MESSAGE_SENT: 'Chatbot Message Sent',
CHATBOT_MESSAGE_RECEIVED: 'Chatbot Message Received',
CHATBOT_FEEDBACK_PROVIDED: 'Chatbot Feedback Provided',
// Scroll depth
SCROLL_DEPTH_25: 'Scroll Depth 25%',
SCROLL_DEPTH_50: 'Scroll Depth 50%',

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

View File

@@ -1,35 +0,0 @@
/**
* Bytedesk 客服系统 z-index 覆盖样式
*
* 问题: Bytedesk 默认 z-index 为 10001项目中部分元素使用 z-index: 99999
* 导致客服 iframe 在首页被内容区覆盖,不可见
*
* 解决方案: 将所有 Bytedesk 相关元素的 z-index 提升到 999999
* 确保客服窗口始终显示在最上层
*/
/* Bytedesk 主容器 - 客服图标按钮 */
[class*="bytedesk"],
[id*="bytedesk"],
[class*="BytedeskWeb"] {
z-index: 999999 !important;
}
/* Bytedesk iframe - 聊天窗口 */
iframe[src*="bytedesk"],
iframe[src*="/chat/"],
iframe[src*="/visitor/"] {
z-index: 999999 !important;
}
/* Bytedesk 覆盖层(如果存在) */
.bytedesk-overlay,
[class*="bytedesk-overlay"] {
z-index: 999998 !important;
}
/* Bytedesk 通知气泡 */
.bytedesk-badge,
[class*="bytedesk-badge"] {
z-index: 1000000 !important;
}

View File

@@ -44,8 +44,6 @@ const Community = () => {
// Chakra UI hooks
const bgColor = useColorModeValue('gray.50', 'gray.900');
const alertBgColor = useColorModeValue('blue.50', 'blue.900');
const alertBorderColor = useColorModeValue('blue.200', 'blue.700');
// Ref用于首次滚动到内容区域
const containerRef = useRef(null);
@@ -147,9 +145,9 @@ const Community = () => {
borderRadius="lg"
mb={4}
boxShadow="md"
bg={alertBgColor}
bg={useColorModeValue('blue.50', 'blue.900')}
borderWidth="1px"
borderColor={alertBorderColor}
borderColor={useColorModeValue('blue.200', 'blue.700')}
>
<AlertIcon />
<Box flex="1">

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { logger } from '../../utils/logger';
import defaultEventImage from '../../assets/img/default-event.jpg';
import {
Box,
Container,
@@ -175,7 +174,7 @@ const ConceptCenter = () => {
const [stockMarketData, setStockMarketData] = useState({});
const [loadingStockData, setLoadingStockData] = useState(false);
// 默认图片路径
const defaultImage = defaultEventImage;
const defaultImage = '/assets/img/default-event.jpg';
// 获取最新交易日期
const fetchLatestTradeDate = useCallback(async () => {