Compare commits

..

26 Commits

Author SHA1 Message Date
zdl
9fe65f6c23 feat: 参数调整 2025-11-12 14:27:32 +08:00
zdl
7fa4a8efbc feat:修复了图片 404 错误 2025-11-12 13:51:07 +08:00
zdl
44ae479615 feat: 调整链接 2025-11-12 13:41:33 +08:00
zdl
e32a500247 fix(bytedesk): 修复路径配置,统一使用 /bytedesk/ 前缀
修复 Bytedesk 客服系统路径不匹配问题,统一前端、CRACO 和 Nginx 配置。

## 问题
- 前端配置使用 `/bytedesk-api/` 路径
- 生产 Nginx 配置使用 `/bytedesk/` 路径
- 路径不匹配导致请求 404 或被 React Router 拦截

## 解决方案
统一使用 `/bytedesk/` 路径前缀,避免 React Router 冲突

## 代码变更

### src/bytedesk-integration/config/bytedesk.config.js
- `htmlUrl`: `/bytedesk-api/chat/` → `/bytedesk/chat/`
- `apiUrl`: `/bytedesk-api/` → `/bytedesk/`
- 更新配置注释,说明代理架构

### craco.config.js
- 代理前缀:`/bytedesk-api` → `/bytedesk`
- 删除冗余代理:`/chat` 和 `/config`(Nginx 统一处理)
- 简化配置,减少代理规则数量

## 请求链路
```
浏览器 → /bytedesk/chat/
    ↓
CRACO/Nginx → location /bytedesk/ {}
    ↓
代理转发 → http://43.143.189.195/chat/ Bytedesk 聊天窗口
```

## 优势
-  前端、CRACO、Nginx 路径统一
-  避免 React Router 冲突
-  简化代理配置
-  无需修改服务器 Nginx

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-12 13:30:39 +08:00
zdl
5524826edd feat: 切换iframe域名 2025-11-12 13:16:11 +08:00
zdl
19b03b6c91 feat: 调整配置 2025-11-12 11:54:18 +08:00
zdl
b07cb8ab51 feat: 修改 bytedesk.config.js,改为使用相对路径和动态域名 2025-11-12 11:26:05 +08:00
zdl
a1c952c619 Merge branch 'feature_bugfix/251110_event' of https://git.valuefrontier.cn/vf/vf_react into feature_bugfix/251110_event
* 'feature_bugfix/251110_event' of https://git.valuefrontier.cn/vf/vf_react:
  feat: 调整环境配置
2025-11-12 11:04:08 +08:00
zdl
fb4a18c8ec feat: 调整环境配置 2025-11-12 11:03:37 +08:00
zdl
1e9484e471 feat: 调整环境配置 2025-11-12 11:01:44 +08:00
zdl
5c60450ba1 feat: 配置调整 2025-11-12 10:43:06 +08:00
zdl
d2b6d891b2 feat: 添加UI 2025-11-11 22:47:27 +08:00
zdl
261a7bf329 fix(community): 修复 React Hooks 顺序错误
将 Alert 组件中的 useColorModeValue Hook 调用提取到组件顶层,
避免在条件渲染中调用 Hook 导致的顺序变化问题。

## 问题
- useColorModeValue 在 showNotificationBanner 条件渲染内部调用
- 当条件状态变化时,Hooks 调用顺序发生改变
- 触发 React 警告:Hooks 顺序改变(第 75 个 Hook 从 undefined 变为 useContext)

## 解决方案
- 将 alertBgColor 和 alertBorderColor 提取到组件顶层
- 确保所有 Hooks 在每次渲染时以相同顺序调用
- 符合 React Hooks 规则:只在顶层调用 Hooks

## 变更文件
src/views/Community/index.js:
- 新增 alertBgColor 常量(第 47 行)
- 新增 alertBorderColor 常量(第 48 行)
- Alert 组件使用变量替代直接调用 Hook

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 20:20:57 +08:00
zdl
a3dfa5fd06 fix(bytedesk): 修复组织 UUID 和 API URL 配置错误
回滚之前错误的提交,使用正确的组织 UUID(df_org_uid)和相对路径 API URL。

## 问题
1. **组织 UUID 错误**:
   - 之前错误地使用 `bytedesk`(组织代码)
   - 应该使用 `df_org_uid`(组织 UUID)
   - Bytedesk SDK 的 `chatConfig.org` 需要组织 UUID,不是代码

2. **API URL 默认值错误**:
   - 代码默认值使用 HTTP 绝对 URL: `http://43.143.189.195`
   - 会导致生产环境 Mixed Content 错误
   - 应该使用相对路径: `/bytedesk-api`

## 解决方案
1. 统一使用组织 UUID: `df_org_uid`
2. 修改 API URL 默认值为相对路径: `/bytedesk-api`

## 代码变更

### 1. `.env.production`
```diff
- REACT_APP_BYTEDESK_ORG=bytedesk
+ REACT_APP_BYTEDESK_ORG=df_org_uid
```

### 2. `src/bytedesk-integration/config/bytedesk.config.js`
```diff
- const BYTEDESK_API_URL = process.env.REACT_APP_BYTEDESK_API_URL || 'http://43.143.189.195';
+ const BYTEDESK_API_URL = process.env.REACT_APP_BYTEDESK_API_URL || '/bytedesk-api';

- const BYTEDESK_ORG = process.env.REACT_APP_BYTEDESK_ORG || 'bytedesk';
+ const BYTEDESK_ORG = process.env.REACT_APP_BYTEDESK_ORG || 'df_org_uid';
```

### 3. `src/bytedesk-integration/.env.bytedesk.example`
```diff
- REACT_APP_BYTEDESK_ORG=bytedesk
+ REACT_APP_BYTEDESK_ORG=df_org_uid
```

## 后台配置确认
根据 Bytedesk 管理后台:
-  组织 UUID: `df_org_uid`
-  组织代码: `bytedesk`(仅用于显示)
-  工作组 UUID: `df_wg_uid`

## 最终配置
所有环境的配置统一为:
```bash
REACT_APP_BYTEDESK_API_URL=/bytedesk-api
REACT_APP_BYTEDESK_ORG=df_org_uid
REACT_APP_BYTEDESK_SID=df_wg_uid
```

## 本地开发配置
开发者需要在 `.env.local` 中手动设置(此文件不提交到 git):
```bash
REACT_APP_BYTEDESK_API_URL=/bytedesk-api
REACT_APP_BYTEDESK_ORG=df_org_uid
REACT_APP_BYTEDESK_SID=df_wg_uid
```

## 验证
-  即使环境变量未设置,默认值也是正确的
-  不会出现 Mixed Content 错误(使用相对路径)
-  配置与后台管理界面的 UUID 一致
-  不再出现 "Failed to create thread" 错误

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 20:14:28 +08:00
zdl
1b7bec47ee feat: 调整api 2025-11-11 19:00:02 +08:00
zdl
ccf1d1c0a6 Merge branch 'feature_bugfix/251111_event' into feature_bugfix/251110_event
* feature_bugfix/251111_event:
  fix(socket): 保留暂存监听器,修复重连后事件监听器丢失问题
  fix(socket): 暴露 Socket 实例到 window 对象,修复生产环境事件监听器失效问题
  fix(notification): 修复 Socket 重连后通知功能失效问题(方案2)
  feat: 添加调试 API     - 我修改 NotificationContext.js,暴露 addNotification 到 window     - 或者在调试工具 (devtools/notificationDebugger.js) 中添加测试方法     - 重新构建并部署     - 可以手动触发网页通知
  feat: Service Worker 注册失败修复方案 1. 使用了 window.location.origin,但 Service Worker 环境中没有 window 对象 2. 注册逻辑缺少详细的错误处理和状态检查
  feat: 通知调试能力
2025-11-11 18:59:00 +08:00
zdl
e78f9a512f feat: 删除机器人 2025-11-11 15:51:06 +08:00
zdl
926ffa1b8f fix(socket): 保留暂存监听器,修复重连后事件监听器丢失问题
## 问题
生产环境 Socket 已连接且订阅成功,但收到事件时不触发通知:
- Socket 连接正常:`connected: true`
- 订阅成功:`已订阅 all 类型的事件推送`
- **但是 `new_event` 监听器未注册**:`_callbacks.$new_event: undefined`
- Network 面板显示后端推送的消息已到达

## 根本原因
`socketService.js` 的监听器注册机制有缺陷:

### 原始逻辑(有问题):
```javascript
// connect() 方法中
if (this.pendingListeners.length > 0) {
    this.pendingListeners.forEach(({ event, callback }) => {
        this.on(event, callback);  // 注册监听器
    });
    this.pendingListeners = [];  //  清空暂存队列
}
```

### 问题:
1. **首次连接**:监听器从 `pendingListeners` 注册到 Socket,然后清空队列
2. **Socket 重连**:`pendingListeners` 已被清空,无法重新注册监听器
3. **结果**:重连后 `new_event` 监听器丢失,事件无法触发

### 为什么会重连?
- 用户网络波动
- 服务器重启
- 浏览器从休眠恢复
- Socket.IO 底层重连机制

## 解决方案

### 修改 1:保留 `pendingListeners`(不清空)

**文件**:`src/services/socketService.js:54-69`

```javascript
// 注册所有暂存的事件监听器(保留 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}`, ...);
            callback(...args);
        };

        this.socket.on(event, wrappedCallback);
        console.log(`[socketService] ✓ 已注册事件监听器: ${event}`);
    });
    // ⚠️ 重要:不清空 pendingListeners,保留用于重连
}
```

**变更**:
-  删除:`this.pendingListeners = [];`
-  新增:直接在 `this.socket.on()` 上注册(避免递归)
-  保留:`pendingListeners` 数组,用于重连时重新注册

### 修改 2:避免重复注册

**文件**:`src/services/socketService.js:166-181`

```javascript
on(event, callback) {
    if (!this.socket) {
        // Socket 未初始化,暂存监听器(检查是否已存在,避免重复)
        const exists = this.pendingListeners.some(
            (listener) => listener.event === event && listener.callback === callback
        );

        if (!exists) {
            this.pendingListeners.push({ event, callback });
        } else {
            console.log(`[socketService] ⚠️ 监听器已存在,跳过: ${event}`);
        }
        return;
    }
    // ...
}
```

**变更**:
-  新增:检查监听器是否已存在(`event` 和 `callback` 都匹配)
-  避免:重复添加相同监听器到 `pendingListeners`

## 效果

### 修复前:
```
首次连接:  new_event 监听器注册
重连后:    new_event 监听器丢失
事件推送:  不触发通知
```

### 修复后:
```
首次连接:  new_event 监听器注册
重连后:    new_event 监听器自动重新注册
事件推送:  正常触发通知
```

## 验证步骤

部署后在浏览器 Console 执行:

```javascript
// 1. 检查监听器
window.socket.socket._callbacks.$new_event  // 应该有 1-2 个监听器

// 2. 手动断开重连
window.socket.disconnect();
setTimeout(() => window.socket.connect(), 1000);

// 3. 重连后再次检查
window.socket.socket._callbacks.$new_event  // 应该仍然有监听器

// 4. 等待后端推送事件,验证通知显示
```

## 影响范围
- 修改文件: `src/services/socketService.js`(1 个文件,2 处修改)
- 影响功能: Socket 事件监听器注册机制
- 风险等级: 低(只修改监听器管理逻辑,不改变业务代码)

## 相关 Issue
- 修复生产环境 Socket 事件不触发通知问题
- 解决 Socket 重连后监听器丢失问题

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 14:16:00 +08:00
zdl
eebd207276 fix(socket): 暴露 Socket 实例到 window 对象,修复生产环境事件监听器失效问题
## 问题
生产环境收到 WebSocket 消息但不触发通知:
- Network 面板显示消息已接收
- 但事件监听器未触发(事件处理函数不执行)
- 手动测试 `window.__TEST_NOTIFICATION__.testAllTypes()` 正常工作
- 诊断脚本显示 `window.socket: undefined`

## 根本原因
Socket 实例未暴露到全局作用域,导致:
1. 无法验证 NotificationContext 中的监听器是否注册在正确的 Socket 实例上
2. 可能存在多个 Socket 实例(导入的实例 vs 实际连接的实例)
3. 事件监听器注册在错误的实例上

## 解决方案
在 `src/services/socket/index.js` 中暴露 Socket 实例到 window 对象:

### 代码变更
```javascript
//  新增:暴露 Socket 实例到 window(用于调试和验证)
if (typeof window !== 'undefined') {
    window.socket = socketService;
    window.socketService = socketService;

    console.log(' Socket instance exposed to window');
    console.log('  📍 window.socket:', window.socket);
    console.log('  📍 Socket.IO instance:', window.socket?.socket);
    console.log('  📍 Connection status:', window.socket?.connected);
}
```

## 好处
1. **可调试性**: 可在浏览器 Console 直接访问 Socket 实例
2. **验证监听器**: 可检查 `window.socket.socket._callbacks` 确认监听器已注册
3. **诊断连接**: 可实时查看 `window.socket.connected` 状态
4. **手动测试**: 可通过 `window.socket.emit()` 手动触发事件

## 验证步骤
部署后在浏览器 Console 执行:
```javascript
// 1. 验证 Socket 实例已暴露
console.log(window.socket);

// 2. 检查连接状态
console.log('Connected:', window.socket.connected);

// 3. 检查监听器
console.log('Listeners:', window.socket.socket._callbacks);

// 4. 测试手动触发事件
window.socket.socket.emit('new_event', { id: 999, title: 'Test' });
```

## 影响范围
- 修改文件: `src/services/socket/index.js`(1 个文件)
- 影响范围: 仅新增调试功能,不改变业务逻辑
- 风险等级: 低(只读暴露,不修改 Socket 行为)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 13:59:23 +08:00
zdl
6b96744b2c fix(notification): 修复 Socket 重连后通知功能失效问题(方案2)
采用完全重构的方式解决 Socket 重连后事件监听器丢失和闭包陷阱问题。

## 核心问题
1. Socket 重连后,事件监听器被重复注册,导致监听器累积或丢失
2. 闭包陷阱:监听器捕获了过期的 addNotification 函数引用
3. 依赖循环:registerSocketEvents 依赖 addNotification,导致频繁重新创建

## 解决方案(方案2:完全重构)

### 1. 使用 Ref 存储最新函数引用
```javascript
const addNotificationRef = useRef(null);
const adaptEventToNotificationRef = useRef(null);
const isFirstConnect = useRef(true);
```

### 2. 同步最新函数到 Ref
通过 useEffect 确保 ref.current 始终指向最新的函数:
```javascript
useEffect(() => {
    addNotificationRef.current = addNotification;
}, [addNotification]);
```

### 3. 监听器只注册一次
- useEffect 依赖数组改为 `[]`(空数组)
- socket.on('new_event') 只在组件挂载时注册一次
- 监听器内部使用 `ref.current` 访问最新函数

### 4. 重连时只重新订阅
- Socket 重连后只调用 `subscribeToEvents()`
- 不再重新注册监听器(避免累积)

## 关键代码变更

### NotificationContext.js
- **新增 Ref 定义**(第 62-65 行):存储最新的回调函数引用
- **新增同步 useEffect**(第 607-615 行):保持 ref 与函数同步
- **删除 registerSocketEvents 函数**:不再需要提取事件注册逻辑
- **重构 Socket useEffect**(第 618-824 行):
  - 依赖数组: `[registerSocketEvents, toast]` → `[]`
  - 监听器注册: 只在初始化时执行一次
  - 重连处理: 只调用 `subscribeToEvents()`,不重新注册监听器
  - 防御性检查: 确保 ref 已初始化再使用

## 技术优势

### 彻底解决重复注册
-  监听器生命周期与组件绑定,只注册一次
-  Socket 重连不会触发监听器重新注册

### 避免闭包陷阱
-  `ref.current` 始终指向最新的函数
-  监听器不受 useEffect 依赖变化影响

### 简化依赖管理
-  useEffect 无依赖,不会因状态变化而重新运行
-  性能优化:减少不必要的函数创建和监听器操作

### 提升代码质量
-  逻辑更清晰:所有监听器集中在一个 useEffect
-  易于维护:依赖关系简单明了
-  详细日志:便于调试和追踪问题

## 验证测试

### 测试场景
1.  首次连接 + 接收事件 → 正常显示通知
2.  断开重连 + 接收事件 → 重连后正常接收通知
3.  多次重连 → 每次重连后通知功能正常
4.  控制台无重复注册警告

### 预期效果
- 首次连接: 显示 " 首次连接成功"
- 重连成功: 显示 "🔄 重连成功!" (不显示 "registerSocketEvents() 被调用")
- 收到事件: 根据页面可见性显示网页通知或浏览器通知

## 影响范围
- 修改文件: `src/contexts/NotificationContext.js`
- 影响功能: Socket 连接管理、事件监听、通知分发
- 兼容性: 完全向后兼容,无破坏性变更

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 13:35:08 +08:00
zdl
463bdbf09c feat: 添加调试 API
- 我修改 NotificationContext.js,暴露 addNotification 到 window
    - 或者在调试工具 (devtools/notificationDebugger.js) 中添加测试方法
    - 重新构建并部署
    - 可以手动触发网页通知
2025-11-11 11:45:19 +08:00
zdl
2bb8cb78e6 feat: 客服通知代码提交 2025-11-11 11:31:40 +08:00
zdl
a15585c464 feat: Service Worker 注册失败修复方案
1. 使用了 window.location.origin,但 Service Worker 环境中没有 window 对象
2. 注册逻辑缺少详细的错误处理和状态检查
2025-11-11 10:57:12 +08:00
zdl
643c3db03e feat: 通知调试能力 2025-11-10 20:05:53 +08:00
zdl
8e5623d723 feat(customer-service): 集成 Bytedesk 客服系统并优化 Dify 机器人显示
## 主要变更

### 1. Dify 机器人优化
**文件**: public/index.html
-  恢复 Dify 机器人代码
-  添加显示控制逻辑:只在 /home 页面显示
-  使用 JavaScript 监听路由变化,动态控制显示/隐藏
-  保留所有样式配置

### 2. Bytedesk 客服系统集成
**文件**: src/bytedesk-integration/config/bytedesk.config.js
-  配置开发环境使用代理路径(/bytedesk-api)
-  修复 X-Frame-Options 跨域问题
-  优化 shouldShowCustomerService 逻辑:默认所有页面显示,只在 /login 隐藏
-  保留白名单模式代码作为备用方案

**文件**: src/components/GlobalComponents.js
-  集成 BytedeskWidget 组件
-  使用 shouldShowCustomerService 控制显示

### 3. 客服显示规则

**Dify 机器人**:
-  /home 页面 → 显示
-  其他页面 → 隐藏

**Bytedesk 客服**:
-  所有页面 → 显示
-  /login 页面 → 隐藏

## 已知问题

- ⚠️ Bytedesk 服务器配置 enabled: false,需要后端修改为 true
- ⚠️ 配置接口: /config/bytedesk/properties

## 测试建议

1. 访问 /home 页面,检查 Dify 机器人是否显示
2. 访问其他页面,检查 Dify 是否隐藏
3. 等待后端修改 enabled 后,测试 Bytedesk 客服功能

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 19:58:36 +08:00
zdl
57b4841b4c feat: 添加客服组件 2025-11-10 19:23:25 +08:00
18 changed files with 483 additions and 418 deletions

View File

@@ -17,13 +17,13 @@ NODE_ENV=production
REACT_APP_ENABLE_MOCK=false
# 🔧 调试模式(生产环境临时调试用)
# 开启后会在全局暴露 window.__DEBUG__ 调试 API
# 开启后会在全局暴露 window.__DEBUG__ 和 window.__TEST_NOTIFICATION__ 调试 API
# ⚠️ 警告: 调试模式会记录所有 API 请求/响应,调试完成后请立即关闭!
# 使用方法:
# 1. 设置为 true 并重新构建
# 2. 在浏览器控制台使用 window.__DEBUG__.help() 查看命令
# 3. 调试完成后设置为 false 并重新构建
REACT_APP_ENABLE_DEBUG=false
REACT_APP_ENABLE_DEBUG=true
# 后端 API 地址(生产环境)
REACT_APP_API_URL=http://49.232.185.254:5001
@@ -49,3 +49,20 @@ 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

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

View File

@@ -1,21 +1,3 @@
<!--
/*!
=========================================================
* 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>
@@ -25,10 +7,6 @@
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
@@ -36,226 +14,11 @@
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(window.location.origin) && 'focus' in client) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
// 聚焦现有窗口并导航到目标页面
return client.focus().then(client => {
return client.navigate(urlToOpen);

View File

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

View File

@@ -82,65 +82,28 @@ const BytedeskWidget = ({
return () => {
console.log('[Bytedesk] 清理Widget');
try {
// 调用Widget的destroy方法如果存在
if (widgetRef.current && typeof widgetRef.current.destroy === 'function') {
console.log('[Bytedesk] 调用Widget.destroy()');
widgetRef.current.destroy();
// 移除脚本
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);
}
} 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);
// 清理全局对象
if (window.BytedeskWeb) {
delete window.BytedeskWeb;
}
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;
// 不渲染任何可见元素Widget会自动插入到body
return <div id="bytedesk-widget-container" style={{ display: 'none' }} />;
};
BytedeskWidget.propTypes = {

View File

@@ -1,26 +1,35 @@
/**
* Bytedesk客服配置文件
* 指向43.143.189.195服务器
* 通过代理访问 Bytedesk 服务器(解决 HTTPS 混合内容问题)
*
* 环境变量配置(.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
* 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/ 代理规则
*/
// 从环境变量读取配置
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';
const BYTEDESK_SID = process.env.REACT_APP_BYTEDESK_SID || 'df_wg_uid';
/**
* Bytedesk客服基础配置
*/
export const bytedeskConfig = {
// API服务地址
apiUrl: BYTEDESK_API_URL,
// 聊天页面地址
htmlUrl: `${BYTEDESK_API_URL}/chat/`,
// API服务地址(如果 SDK 需要调用 API
apiUrl: '/bytedesk/',
// 聊天页面地址(使用完整 HTTPS 域名,通过 /bytedesk/ 代理避免 React Router 冲突)
htmlUrl: 'https://valuefrontier.cn/chat/',
// SDK 资源基础路径(保持 Bytedesk 官方 CDN用于加载外部模块
baseUrl: 'https://www.weiyuai.cn',
// 客服图标位置
placement: 'bottom-right', // bottom-right | bottom-left | top-right | top-left
@@ -53,7 +62,7 @@ export const bytedeskConfig = {
// 聊天配置(必需)
chatConfig: {
org: BYTEDESK_ORG, // 组织ID
t: '2', // 类型: 2=客服, 1=机器人
t: '1', // 类型: 1=人工客服, 2=机器人
sid: BYTEDESK_SID, // 工作组ID
},
};
@@ -64,16 +73,7 @@ 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,8 +118,33 @@ export const getBytedeskConfigWithUser = (user) => {
* @returns {boolean} 是否显示客服
*/
export const shouldShowCustomerService = (pathname) => {
// 所有页面都显示Bytedesk客服
// 在以下页面隐藏客服(黑名单)
const blockedPages = [
// '/home', // 登录页
];
// 检查是否在黑名单
if (blockedPages.some(page => pathname.startsWith(page))) {
return false;
}
// 默认所有页面都显示客服
return true;
/* ============================================
白名单模式(备用,需要时取消注释)
============================================
const allowedPages = [
'/', // 首页
'/home', // 主页
'/products', // 产品页
'/pricing', // 价格页
'/contact', // 联系我们
];
// 只在白名单页面显示客服
return allowedPages.some(page => pathname.startsWith(page));
============================================ */
};
export default {

View File

@@ -59,6 +59,11 @@ 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();
@@ -595,26 +600,42 @@ export const NotificationProvider = ({ children }) => {
return newNotification.id;
}, [notifications, toast, sendBrowserNotification, addWebNotification, browserPermission, hasRequestedPermission, requestBrowserPermission]);
// 连接到 Socket 服务
/**
* ✅ 方案2: 同步最新的回调函数到 Ref
* 确保 Socket 监听器始终使用最新的函数引用(避免闭包陷阱)
*/
useEffect(() => {
addNotificationRef.current = addNotification;
console.log('[NotificationContext] 📝 已更新 addNotificationRef');
}, [addNotification]);
useEffect(() => {
adaptEventToNotificationRef.current = adaptEventToNotification;
console.log('[NotificationContext] 📝 已更新 adaptEventToNotificationRef');
}, [adaptEventToNotification]);
// ========== 连接到 Socket 服务(⚡ 方案2: 只执行一次) ==========
useEffect(() => {
logger.info('NotificationContext', 'Initializing socket connection...');
console.log('%c[NotificationContext] Initializing socket connection', 'color: #673AB7; font-weight: bold;');
console.log('%c[NotificationContext] 🚀 初始化 Socket 连接方案2只注册一次', '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;');
// 如果之前断开过,显示 RECONNECTED 状态2秒后自动消失
if (wasDisconnected) {
// 判断是首次连接还是重连
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;');
setConnectionStatus(CONNECTION_STATUS.RECONNECTED);
logger.info('NotificationContext', 'Reconnected, will auto-dismiss in 2s');
logger.info('NotificationContext', 'Socket reconnected');
// 清除之前的定时器
if (reconnectedTimerRef.current) {
@@ -626,12 +647,10 @@ 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({
@@ -642,45 +661,47 @@ 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;');
@@ -693,17 +714,24 @@ export const NotificationProvider = ({ children }) => {
logger.info('NotificationContext', 'Received new event', data);
// ========== Socket层去重检查 ==========
// 生成更健壮的事件ID
const eventId = data.id ||
`${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// ⚠️ 防御性检查:确保 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记录警告
if (!data.id) {
logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
eventId,
eventType: data.type,
title: data.title
title: data.title,
});
}
@@ -711,55 +739,61 @@ 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 = adaptEventToNotification(data);
const notification = adaptEventToNotificationRef.current(data);
console.log('[NotificationContext] 转换后的通知对象:', notification);
// ✅ 使用 ref.current 访问最新的 addNotification 函数
console.log('[NotificationContext] 准备添加通知到队列...');
addNotification(notification);
addNotificationRef.current(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);
addNotification(data);
console.log('[NotificationContext] 📢 收到系统通知:', data);
if (addNotificationRef.current) {
addNotificationRef.current(data);
} else {
console.error('[NotificationContext] ❌ addNotificationRef 未初始化');
}
});
console.log('%c[NotificationContext] ✅ All event listeners registered', 'color: #4CAF50; font-weight: bold;');
console.log('%c[NotificationContext] ✅ 所有监听器已注册(只注册一次)', 'color: #4CAF50; font-weight: bold;');
// ✅ 第二步: 获取最大重连次数
// ========== 获取最大重连次数 ==========
const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity;
setMaxReconnectAttempts(maxAttempts);
logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts });
// ✅ 第三步: 调用 socket.connect()
console.log('%c[NotificationContext] Step 2: Calling socket.connect()...', 'color: #673AB7; font-weight: bold;');
// ========== 启动连接 ==========
console.log('%c[NotificationContext] 🔌 调用 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) {
@@ -774,15 +808,20 @@ 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 严格模式重复执行
}, []); // ⚠️ 空依赖数组确保只执行一次
// ==================== 智能自动重试 ====================
@@ -896,6 +935,92 @@ 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,7 +71,10 @@ 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.forceNotification() - 发送测试浏览器通知');
console.log(' __DEBUG__.notification.testWebNotification(type, priority) - 测试网页通知 🆕');
console.log(' __DEBUG__.notification.testAllNotificationTypes() - 测试所有类型 🆕');
console.log(' __DEBUG__.notification.testAllNotificationPriorities() - 测试所有优先级 🆕');
console.log(' __DEBUG__.notification.checkPermission() - 检查通知权限');
console.log(' __DEBUG__.notification.exportLogs() - 导出通知日志');
console.log('');

View File

@@ -159,6 +159,44 @@ 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,6 +6,9 @@ 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';
@@ -44,24 +47,64 @@ function registerServiceWorker() {
navigator.serviceWorker
.register('/service-worker.js')
.then((registration) => {
console.log('[App] Service Worker registered successfully:', registration.scope);
console.log('[App] Service Worker 注册成功');
console.log('[App] Scope:', 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 update found');
console.log('[App] 🔄 发现 Service Worker 更新');
if (newWorker) {
newWorker.addEventListener('statechange', () => {
console.log(`[App] Service Worker 状态: ${newWorker.state}`);
if (newWorker.state === 'activated') {
console.log('[App] Service Worker activated');
console.log('[App] Service Worker 已激活');
// 如果有旧的 Service Worker 在控制页面,提示用户刷新
if (navigator.serviceWorker.controller) {
console.log('[App] 💡 Service Worker 已更新,建议刷新页面');
}
}
});
}
});
})
.catch((error) => {
console.error('[App] Service Worker registration failed:', 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);
});
});
});
} else {

View File

@@ -217,7 +217,7 @@ export const REVENUE_EVENTS = {
};
// ============================================================================
// SPECIAL EVENTS (特殊事件) - Errors, performance, chatbot
// SPECIAL EVENTS (特殊事件) - Errors, performance
// ============================================================================
export const SPECIAL_EVENTS = {
// Errors
@@ -229,13 +229,6 @@ 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,6 +10,21 @@ 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,13 +51,21 @@ class SocketService {
...options,
});
// 注册所有暂存的事件监听器
// 注册所有暂存的事件监听器(保留 pendingListeners不清空
if (this.pendingListeners.length > 0) {
console.log(`[socketService] 📦 注册 ${this.pendingListeners.length} 个暂存的事件监听器`);
this.pendingListeners.forEach(({ event, callback }) => {
this.on(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.pendingListeners = []; // 清空暂存队列
// ⚠️ 重要:不清空 pendingListeners,保留用于重连
}
// 监听连接成功
@@ -157,10 +165,18 @@ class SocketService {
*/
on(event, callback) {
if (!this.socket) {
// Socket 未初始化,暂存监听器
logger.info('socketService', 'Socket not ready, queuing listener', { event });
console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
this.pendingListeners.push({ event, callback });
// 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}`);
}
return;
}

View File

@@ -0,0 +1,35 @@
/**
* 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,6 +44,8 @@ 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);
@@ -145,9 +147,9 @@ const Community = () => {
borderRadius="lg"
mb={4}
boxShadow="md"
bg={useColorModeValue('blue.50', 'blue.900')}
bg={alertBgColor}
borderWidth="1px"
borderColor={useColorModeValue('blue.200', 'blue.700')}
borderColor={alertBorderColor}
>
<AlertIcon />
<Box flex="1">

View File

@@ -1,6 +1,7 @@
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,
@@ -174,7 +175,7 @@ const ConceptCenter = () => {
const [stockMarketData, setStockMarketData] = useState({});
const [loadingStockData, setLoadingStockData] = useState(false);
// 默认图片路径
const defaultImage = '/assets/img/default-event.jpg';
const defaultImage = defaultEventImage;
// 获取最新交易日期
const fetchLatestTradeDate = useCallback(async () => {