flat: 暂存
This commit is contained in:
249
FEATURES.md
Normal file
249
FEATURES.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# 直播中台控制系统 - 功能使用说明
|
||||
|
||||
## 🎯 项目架构
|
||||
|
||||
### 系统组成
|
||||
- **主进程 (Main Process)**: 负责系统管理、数据中心、核心业务逻辑、多窗口协调
|
||||
- **中控平台 (Renderer)**: 渲染UI、处理用户输入,仅包含轻量级IPC通信代码
|
||||
- **数字人窗口 (Renderer)**: 渲染数字人界面和iframe,接收主进程数据
|
||||
|
||||
### 技术架构
|
||||
- **桌面端**: Electron 28 + TypeScript
|
||||
- **前端**: Vue 3 + Composition API + Ant Design Vue
|
||||
- **状态管理**: Pinia (仅UI状态)
|
||||
- **通信机制**: IPC (进程间通信)
|
||||
|
||||
## 🚀 核心功能实现
|
||||
|
||||
### 1. 开始直播
|
||||
**功能描述**: 完整的直播启动流程
|
||||
**实现流程**:
|
||||
1. 从 UserStore 获取用户ID (固定值: 'rs876543')
|
||||
2. 创建/显示直播窗口
|
||||
3. 等待页面和iframe加载完成 (30秒超时)
|
||||
4. 获取数字人平台sessionId
|
||||
5. 设置直播状态
|
||||
6. 发送欢迎消息
|
||||
|
||||
**前端调用**:
|
||||
```javascript
|
||||
// userId 自动从 userStore.user?.userId 获取 (固定值: 'rs876543')
|
||||
await window.electron.ipcRenderer.invoke('start-live', { userId })
|
||||
```
|
||||
|
||||
**主进程处理**: `[src/main/ipc/live.ts:80-118]`
|
||||
|
||||
### 2. 结束直播
|
||||
**功能描述**: 清理直播资源和状态
|
||||
**实现流程**:
|
||||
1. 重置直播状态
|
||||
2. 关闭直播窗口
|
||||
3. 清空sessionId
|
||||
4. 重置所有插播状态
|
||||
|
||||
**前端调用**:
|
||||
```javascript
|
||||
await window.electron.ipcRenderer.invoke('stop-live')
|
||||
```
|
||||
|
||||
**主进程处理**: `[src/main/ipc/live.ts:124-145]`
|
||||
|
||||
### 2.1. 强制关闭
|
||||
**功能描述**: 强制关闭直播窗口并清理所有资源(紧急情况使用)
|
||||
**实现流程**:
|
||||
1. 发送清理指令到直播窗口
|
||||
2. 强制清理所有媒体资源(摄像头、麦克风、音频、视频状态)
|
||||
3. 恢复iframe内视频音量到正常状态
|
||||
4. 强制销毁直播窗口(移除所有监听器)
|
||||
5. 重置所有直播状态和插播状态
|
||||
6. 不管当前直播状态,强制执行关闭
|
||||
|
||||
**前端调用**:
|
||||
```javascript
|
||||
await window.electron.ipcRenderer.invoke('force-close-live')
|
||||
```
|
||||
|
||||
**主进程处理**: `[src/main/ipc/live.ts:147-183]`
|
||||
|
||||
### 2.2. 媒体资源清理
|
||||
**功能描述**: 在强制关闭时彻底清理所有媒体资源
|
||||
**清理内容**:
|
||||
- **摄像头资源**: 停止摄像头视频流,清理video元素
|
||||
- **麦克风资源**: 停止麦克风流,清理audio元素
|
||||
- **音频资源**: 暂停所有播放中的音频,重置播放状态
|
||||
- **视频状态**: 恢复iframe内视频音量,清理插播状态
|
||||
- **应用状态**: 重置所有媒体相关的变量和标志位
|
||||
|
||||
**实现细节**:
|
||||
```javascript
|
||||
// 主进程发送清理指令
|
||||
liveState.liveWindow.webContents.send("force-cleanup-media");
|
||||
|
||||
// 直播窗口接收并处理
|
||||
window.electron.ipcRenderer.on('force-cleanup-media', handleForceCleanup);
|
||||
|
||||
// 清理函数会执行:
|
||||
forceCleanupMedia() // 停止所有媒体流、重置状态
|
||||
```
|
||||
|
||||
### 3. 全屏插播
|
||||
**功能描述**: 切换摄像头全屏显示
|
||||
**实现方式**: 切换视频插入状态,发送指令到直播窗口
|
||||
|
||||
**前端调用**:
|
||||
```javascript
|
||||
window.electron.ipcRenderer.send('toggle-camera-insert')
|
||||
```
|
||||
|
||||
**直播窗口响应**: `[src/renderer/src/views/Live/index.vue:92-113]`
|
||||
|
||||
### 4. 窗口插播
|
||||
**功能描述**: 开启摄像头画中画模式
|
||||
**实现方式**: 在直播窗口右下角显示小窗口摄像头
|
||||
|
||||
**前端调用**:
|
||||
```javascript
|
||||
window.electron.ipcRenderer.send('insert-camera-video')
|
||||
```
|
||||
|
||||
**直播窗口响应**: `[src/renderer/src/views/Live/index.vue:84-90]`
|
||||
|
||||
### 5. 音频插入
|
||||
**功能描述**: 插入音频文件播放
|
||||
**实现方式**: 降低主视频音量,播放插入音频
|
||||
|
||||
**前端调用**:
|
||||
```javascript
|
||||
window.electron.ipcRenderer.send('insert-video-audio')
|
||||
```
|
||||
|
||||
**直播窗口响应**: `[src/renderer/src/views/Live/index.vue:116-147]`
|
||||
|
||||
### 6. 镜头切换
|
||||
**功能描述**: 摄像头开启/关闭切换
|
||||
**实现方式**: 切换摄像头激活状态
|
||||
|
||||
**前端调用**:
|
||||
```javascript
|
||||
window.electron.ipcRenderer.send('toggle-camera-insert')
|
||||
```
|
||||
|
||||
### 7. 发送消息到数字人
|
||||
**功能描述**: 向数字人发送文本内容
|
||||
**实现流程**:
|
||||
1. 验证直播状态和sessionId
|
||||
2. 构建消息参数
|
||||
3. 调用数字人平台API
|
||||
4. 处理响应结果
|
||||
|
||||
**前端调用**:
|
||||
```javascript
|
||||
await window.electron.ipcRenderer.invoke('push-explain-position', '消息内容')
|
||||
```
|
||||
|
||||
**主进程处理**: `[src/main/ipc/live.ts:145-164]`
|
||||
|
||||
## 🔄 状态管理
|
||||
|
||||
### 主进程状态
|
||||
```typescript
|
||||
interface LiveState {
|
||||
sessionId: string | null; // 数字人会话ID
|
||||
userId: string | null; // 用户ID
|
||||
isVideoInserted: boolean; // 视频插入状态
|
||||
isLiveOn: boolean; // 直播状态
|
||||
jobList: any[]; // 岗位列表
|
||||
currentJob: any; // 当前岗位
|
||||
cameraActive: boolean; // 摄像头激活状态
|
||||
audioActive: boolean; // 音频激活状态
|
||||
liveWindow: BrowserWindow | null; // 直播窗口
|
||||
}
|
||||
```
|
||||
|
||||
### 状态同步
|
||||
- **获取直播状态**: `get-live-status` IPC接口
|
||||
- **UI状态自动同步**: 每2秒从主进程同步状态
|
||||
- **状态变化实时响应**: 按钮状态根据直播状态动态更新
|
||||
|
||||
## 🎮 用户界面
|
||||
|
||||
### 中控台布局
|
||||
- **顶部导航**: 应用标题和用户信息
|
||||
- **左侧边栏**: 岗位列表和详情信息
|
||||
- **主内容区**: 直播控制面板、AI模型管理
|
||||
- **底部输入区**: 消息输入和快捷指令
|
||||
|
||||
### 按钮状态管理
|
||||
- **开始直播**: loading状态,直播中时禁用
|
||||
- **结束直播**: 仅直播时可用,loading状态
|
||||
- **插播功能**: 仅直播时可用
|
||||
- **发送消息**: 仅直播时可用
|
||||
|
||||
### 快捷指令
|
||||
- 欢迎新观众
|
||||
- 感谢陪伴
|
||||
- 互动提问
|
||||
- 行业分享
|
||||
|
||||
## 🔗 外部服务集成
|
||||
|
||||
### 数字人平台
|
||||
- **API地址**: `http://ywpt.hx.cn/dmhx/`
|
||||
- **获取会话**: `get_sessionid` 接口
|
||||
- **发送消息**: `human` 接口
|
||||
- **参数格式**: `{ sessionid, text, type: "echo", interrupt: true }`
|
||||
|
||||
### 直播展示
|
||||
- **直播地址**: `https://dmdemo.hx.cn/dashboard.html`
|
||||
- **参数传递**: `?userId=${userId}`
|
||||
- **嵌入方式**: iframe 全屏显示
|
||||
|
||||
## 🛠️ 开发和调试
|
||||
|
||||
### 启动项目
|
||||
```bash
|
||||
npm run dev # 开发模式
|
||||
npm run build # 生产构建
|
||||
npm run typecheck # 类型检查
|
||||
```
|
||||
|
||||
### 调试接口
|
||||
```javascript
|
||||
// 获取主进程实时状态
|
||||
const state = getLiveState();
|
||||
|
||||
// 获取直播状态 (IPC)
|
||||
const status = await window.electron.ipcRenderer.invoke('get-live-status');
|
||||
```
|
||||
|
||||
### 日志输出
|
||||
- 主进程日志在Electron控制台查看
|
||||
- 渲染进程日志在浏览器开发者工具查看
|
||||
- API调用错误会在控制台详细显示
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
### 权限要求
|
||||
- 摄像头权限: 需要用户授权
|
||||
- 麦克风权限: 音频插入功能需要
|
||||
- 网络权限: 访问外部API服务
|
||||
|
||||
### 错误处理
|
||||
- 网络请求失败: 自动重试和错误提示
|
||||
- 权限被拒绝: 友好的权限请求提示
|
||||
- 状态不一致: 自动同步机制修复
|
||||
|
||||
### 性能优化
|
||||
- 状态同步频率适中 (2秒)
|
||||
- IPC通信异步处理
|
||||
- 资源清理及时 (窗口关闭时)
|
||||
|
||||
## 🚀 使用流程
|
||||
|
||||
1. **启动应用**: 运行 `npm run dev` 启动开发环境
|
||||
2. **开始直播**: 点击"开始直播"按钮,系统自动获取sessionId并创建直播窗口
|
||||
3. **发送消息**: 在输入框输入内容或使用快捷指令,数字人会实时播报
|
||||
4. **插播控制**: 使用插播功能切换摄像头或插入音频
|
||||
5. **结束直播**: 点击"结束直播"清理资源
|
||||
|
||||
系统已按照现代化架构设计,主进程负责核心逻辑,渲染进程专注于UI展示,通过IPC实现高效通信。
|
||||
255
package-lock.json
generated
255
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"sass": "^1.93.2",
|
||||
@@ -63,6 +64,34 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/colors": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-6.0.0.tgz",
|
||||
"integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/icons-svg": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
|
||||
"integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ant-design/icons-vue": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
|
||||
"integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^6.0.0",
|
||||
"@ant-design/icons-svg": "^4.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
|
||||
@@ -489,6 +518,15 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
|
||||
@@ -537,6 +575,15 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
||||
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@develar/schema-utils": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
||||
@@ -868,6 +915,18 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/hash": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz",
|
||||
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/unitless": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz",
|
||||
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
|
||||
@@ -2148,6 +2207,16 @@
|
||||
"integrity": "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@simonwep/pickr": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.8.2.tgz",
|
||||
"integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "^3.15.1",
|
||||
"nanopop": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
@@ -2825,6 +2894,46 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ant-design-vue": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-4.2.6.tgz",
|
||||
"integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^6.0.0",
|
||||
"@ant-design/icons-vue": "^7.0.0",
|
||||
"@babel/runtime": "^7.10.5",
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
"@emotion/hash": "^0.9.0",
|
||||
"@emotion/unitless": "^0.8.0",
|
||||
"@simonwep/pickr": "~1.8.0",
|
||||
"array-tree-filter": "^2.1.0",
|
||||
"async-validator": "^4.0.0",
|
||||
"csstype": "^3.1.1",
|
||||
"dayjs": "^1.10.5",
|
||||
"dom-align": "^1.12.1",
|
||||
"dom-scroll-into-view": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.15",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"scroll-into-view-if-needed": "^2.2.25",
|
||||
"shallow-equal": "^1.0.0",
|
||||
"stylis": "^4.1.3",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"vue-types": "^3.0.0",
|
||||
"warning": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ant-design-vue"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
|
||||
@@ -2937,6 +3046,12 @@
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/array-tree-filter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
|
||||
"integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
@@ -2981,6 +3096,12 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -3461,6 +3582,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compute-scroll-into-view": {
|
||||
"version": "1.0.20",
|
||||
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
|
||||
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/computeds": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
|
||||
@@ -3526,6 +3653,17 @@
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.46.0",
|
||||
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.46.0.tgz",
|
||||
"integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
@@ -3588,6 +3726,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.19",
|
||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
|
||||
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
@@ -3860,6 +4004,18 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-align": {
|
||||
"version": "1.12.4",
|
||||
"resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz",
|
||||
"integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-scroll-into-view": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz",
|
||||
"integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
|
||||
@@ -5314,6 +5470,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz",
|
||||
"integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
|
||||
@@ -5405,8 +5570,7 @@
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
@@ -5520,8 +5684,13 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
@@ -5529,6 +5698,18 @@
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
@@ -5741,6 +5922,12 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nanopop": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz",
|
||||
"integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -6230,6 +6417,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-alpn": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||
@@ -6408,6 +6601,15 @@
|
||||
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "2.2.31",
|
||||
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
|
||||
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"compute-scroll-into-view": "^1.0.20"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
@@ -6462,6 +6664,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/shallow-equal": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
|
||||
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -6667,6 +6875,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz",
|
||||
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sumchecker": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
@@ -6792,6 +7006,15 @@
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
|
||||
"integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
}
|
||||
},
|
||||
"node_modules/time-zone": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/time-zone/-/time-zone-1.0.0.tgz",
|
||||
@@ -7130,6 +7353,21 @@
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/vue-types/-/vue-types-3.0.2.tgz",
|
||||
"integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-plain-object": "3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.15.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuedraggable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz",
|
||||
@@ -7148,6 +7386,15 @@
|
||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"sass": "^1.93.2",
|
||||
|
||||
96
src/main/Api/api.ts
Normal file
96
src/main/Api/api.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
export async function getSessionId(requestBody: object) {
|
||||
try {
|
||||
const response = await fetch("http://ywpt.hx.cn/dmhx/get_sessionid", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
// 首先检查响应内容类型
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
// 如果不是JSON,读取原始文本进行调试
|
||||
const rawText = await response.text();
|
||||
console.warn("服务器返回非JSON响应:", rawText);
|
||||
|
||||
// 尝试解析可能的JSON响应(即使Content-Type不正确)
|
||||
try {
|
||||
const data = JSON.parse(rawText);
|
||||
if (response.ok && data.sessionid) {
|
||||
return { success: true, sessionId: data.sessionid };
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message || "服务器返回非JSON格式",
|
||||
};
|
||||
}
|
||||
} catch (parseError) {
|
||||
return {
|
||||
success: false,
|
||||
error: `服务器响应格式错误: ${rawText.substring(0, 100)}...`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是JSON,正常解析
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.sessionid) {
|
||||
return { success: true, sessionId: data.sessionid };
|
||||
} else {
|
||||
return { success: false, error: data.message || "未知错误" };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error in getSessionId:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendMessage(requestBody: object) {
|
||||
try {
|
||||
const response = await fetch(`http://ywpt.hx.cn/dmhx/human`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
return { success: true };
|
||||
} else {
|
||||
return { success: false, error: data.message || "未知错误" };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error in sendMessage:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getJob(requestBody: object) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://qd.zhaopinzao8dian.com/api/app/job/metaInfo`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
},
|
||||
);
|
||||
const resData = await response.json();
|
||||
|
||||
if (resData.code === 200) {
|
||||
return { success: true, data: resData.data };
|
||||
} else {
|
||||
throw new Error(resData.msg);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error in sendMessage:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,11 @@
|
||||
import { app, shell, BrowserWindow } from "electron";
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
||||
import icon from "../../resources/icon.png?asset";
|
||||
import { setupLiveHandlers } from "./ipc/live";
|
||||
import { setupPromptHandlers } from "./ipc/prompt";
|
||||
import { setupWorkflowHandlers } from "./ipc/workflow";
|
||||
import { preload, indexHtml, ELECTRON_RENDERER_URL } from "./config";
|
||||
|
||||
// 在开发环境下启用热重载
|
||||
if (is.dev) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require("electron-reloader")(module, {
|
||||
debug: true,
|
||||
watchRenderer: true, // 同时监视渲染进程文件
|
||||
ignore: [
|
||||
/node_modules/,
|
||||
/dist/,
|
||||
/release/,
|
||||
/\.[\/\\]\./, // 忽略点文件
|
||||
/package(-lock)?\.json/,
|
||||
],
|
||||
});
|
||||
console.log("Electron hot reload enabled");
|
||||
} catch (err) {
|
||||
console.error("Error enabling hot reload:", err);
|
||||
}
|
||||
}
|
||||
// 简单的开发环境检测
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
/**
|
||||
* 创建主窗口
|
||||
@@ -36,7 +16,6 @@ function createWindow(): void {
|
||||
height: 670,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === "linux" ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload,
|
||||
sandbox: false,
|
||||
@@ -47,14 +26,24 @@ function createWindow(): void {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
// mainWindow.webContents.openDevTools();
|
||||
mainWindow.webContents.openDevTools();
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
if (is.dev && ELECTRON_RENDERER_URL) {
|
||||
// 窗口关闭时强制清理资源并退出应用
|
||||
mainWindow.on("closed", () => {
|
||||
// 清理所有资源
|
||||
mainWindow.removeAllListeners();
|
||||
// 强制退出应用
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
if (isDev && ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(ELECTRON_RENDERER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(indexHtml);
|
||||
@@ -65,11 +54,7 @@ function createWindow(): void {
|
||||
* 准备好后
|
||||
*/
|
||||
app.whenReady().then(() => {
|
||||
electronApp.setAppUserModelId("com.electron");
|
||||
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
app.setAppUserModelId("com.electron");
|
||||
|
||||
// 直播相关处理
|
||||
setupLiveHandlers();
|
||||
@@ -89,7 +74,27 @@ app.whenReady().then(() => {
|
||||
* 退出应用
|
||||
*/
|
||||
app.on("window-all-closed", () => {
|
||||
// 强制清理所有资源
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// 在开发环境中,确保进程正确退出
|
||||
app.on("before-quit", () => {
|
||||
// 清理所有资源
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach(window => {
|
||||
window.removeAllListeners();
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
|
||||
// 处理进程退出信号
|
||||
process.on("SIGINT", () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
@@ -1,45 +1,124 @@
|
||||
import { ipcMain, BrowserWindow } from "electron";
|
||||
import { preload, indexHtml, ELECTRON_RENDERER_URL } from "../config";
|
||||
import { getSessionId, sendMessage } from "../Api/api";
|
||||
import { showPrompt } from "../utils/tools";
|
||||
let liveWindow: BrowserWindow | null = null;
|
||||
|
||||
// 直播状态管理
|
||||
interface LiveState {
|
||||
sessionId: string | null;
|
||||
userId: string | null;
|
||||
isVideoInserted: boolean;
|
||||
isLiveOn: boolean;
|
||||
jobList: any[];
|
||||
currentJob: any;
|
||||
cameraActive: boolean;
|
||||
audioActive: boolean;
|
||||
liveWindow: BrowserWindow | null;
|
||||
}
|
||||
|
||||
// 全局直播状态
|
||||
const liveState: LiveState = {
|
||||
sessionId: null,
|
||||
userId: null,
|
||||
isVideoInserted: false,
|
||||
isLiveOn: false,
|
||||
jobList: [],
|
||||
currentJob: null,
|
||||
cameraActive: false,
|
||||
audioActive: false,
|
||||
liveWindow: null,
|
||||
};
|
||||
|
||||
// 直播相关的主进程处理
|
||||
export function setupLiveHandlers() {
|
||||
let LiveSessionId = null;
|
||||
let isVideoInserted = false;
|
||||
// 获取直播状态
|
||||
ipcMain.handle("get-live-status", async () => {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
isLiveOn: liveState.isLiveOn,
|
||||
hasLiveWindow: !!liveState.liveWindow,
|
||||
isVideoInserted: liveState.isVideoInserted,
|
||||
cameraActive: liveState.cameraActive,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 切换摄像头插入状态
|
||||
ipcMain.on("toggle-camera-insert", async () => {
|
||||
if (liveWindow) {
|
||||
isVideoInserted = !isVideoInserted;
|
||||
liveWindow.webContents.send(
|
||||
if (liveState.liveWindow) {
|
||||
liveState.isVideoInserted = !liveState.isVideoInserted;
|
||||
liveState.liveWindow.webContents.send(
|
||||
"toggle-camera-insert",
|
||||
isVideoInserted,
|
||||
liveState.isVideoInserted,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 插入摄像头视频
|
||||
ipcMain.on("insert-camera-video", async (_) => {
|
||||
if (liveWindow) {
|
||||
liveWindow.webContents.send("insert-camera-video");
|
||||
ipcMain.on("insert-camera-video", async () => {
|
||||
if (liveState.liveWindow) {
|
||||
liveState.liveWindow.webContents.send("insert-camera-video");
|
||||
liveState.cameraActive = true;
|
||||
} else {
|
||||
showPrompt("直播窗口未打开", "error");
|
||||
}
|
||||
});
|
||||
|
||||
// 插入音频
|
||||
ipcMain.on("insert-video-audio", async (_) => {
|
||||
if (liveWindow) {
|
||||
liveWindow.webContents.send("insert-video-audio");
|
||||
ipcMain.on("insert-video-audio", async () => {
|
||||
if (liveState.liveWindow) {
|
||||
liveState.liveWindow.webContents.send("insert-video-audio");
|
||||
liveState.audioActive = true;
|
||||
} else {
|
||||
showPrompt("直播窗口未打开", "error");
|
||||
}
|
||||
});
|
||||
|
||||
// 开始直播
|
||||
ipcMain.handle("start-live", async (_) => {
|
||||
// 开始直播 - 完整的直播启动流程
|
||||
ipcMain.handle("start-live", async (_, { userId } = {}) => {
|
||||
try {
|
||||
console.log("Starting live stream...");
|
||||
// TODO: 实现直播推流逻辑
|
||||
return { success: true };
|
||||
|
||||
// 1. 设置用户ID
|
||||
if (userId) {
|
||||
liveState.userId = userId;
|
||||
}
|
||||
|
||||
// 2. 创建或显示直播窗口
|
||||
if (!liveState.liveWindow) {
|
||||
await createLiveWindow();
|
||||
}
|
||||
|
||||
// 3. 等待网页加载完成
|
||||
// await waitForPageLoad();
|
||||
|
||||
// 4. 获取 sessionId
|
||||
console.log("获取 sessionId");
|
||||
const sessionResult = await getSessionId({
|
||||
userId: liveState.userId,
|
||||
});
|
||||
console.log("sessionid", sessionResult);
|
||||
if (!sessionResult.success) {
|
||||
return { success: false, error: "获取会话ID失败" };
|
||||
}
|
||||
liveState.sessionId = sessionResult.sessionId;
|
||||
|
||||
// 5. 设置直播状态
|
||||
liveState.isLiveOn = true;
|
||||
|
||||
// 6. 发送欢迎消息
|
||||
await sendMessage({
|
||||
sessionid: liveState.sessionId,
|
||||
text: "大家好,欢迎来到我的直播间!",
|
||||
type: "echo",
|
||||
interrupt: true,
|
||||
});
|
||||
|
||||
showPrompt("直播已开始", "info");
|
||||
return { success: true, sessionId: liveState.sessionId };
|
||||
} catch (error: any) {
|
||||
console.error("Start live error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
@@ -47,151 +126,273 @@ export function setupLiveHandlers() {
|
||||
// 结束直播
|
||||
ipcMain.handle("stop-live", async () => {
|
||||
try {
|
||||
// TODO: 实现结束直播逻辑
|
||||
liveState.isLiveOn = false;
|
||||
|
||||
// 关闭直播窗口
|
||||
if (liveState.liveWindow) {
|
||||
liveState.liveWindow.close();
|
||||
liveState.liveWindow = null;
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
liveState.sessionId = null;
|
||||
liveState.isVideoInserted = false;
|
||||
liveState.cameraActive = false;
|
||||
liveState.audioActive = false;
|
||||
|
||||
showPrompt("直播已结束", "info");
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("explain-position", async (_, content: string) => {
|
||||
// 强制关闭直播窗口
|
||||
ipcMain.handle("force-close-live", async () => {
|
||||
try {
|
||||
let params = {
|
||||
sessionid: LiveSessionId,
|
||||
console.log("Force closing live window...");
|
||||
|
||||
// 通知直播窗口清理所有媒体资源
|
||||
if (liveState.liveWindow) {
|
||||
try {
|
||||
// 发送清理指令到直播窗口
|
||||
liveState.liveWindow.webContents.send(
|
||||
"force-cleanup-media",
|
||||
);
|
||||
|
||||
// 等待一下确保清理指令被处理
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// 强制关闭窗口
|
||||
liveState.liveWindow.removeAllListeners();
|
||||
liveState.liveWindow.destroy();
|
||||
} catch (error) {
|
||||
console.warn("Error destroying window:", error);
|
||||
}
|
||||
liveState.liveWindow = null;
|
||||
}
|
||||
|
||||
// 强制重置所有状态
|
||||
liveState.isLiveOn = false;
|
||||
liveState.sessionId = null;
|
||||
liveState.isVideoInserted = false;
|
||||
liveState.cameraActive = false;
|
||||
liveState.audioActive = false;
|
||||
|
||||
showPrompt("直播窗口已强制关闭", "info");
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
console.error("Force close error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// 提交消息给数字人平台
|
||||
ipcMain.handle("push-explain-position", async (_, content: string) => {
|
||||
try {
|
||||
if (!liveState.sessionId) {
|
||||
return { success: false, error: "直播未开始,无法发送消息" };
|
||||
}
|
||||
|
||||
const params = {
|
||||
sessionid: liveState.sessionId,
|
||||
text: content,
|
||||
type: "echo",
|
||||
interrupt: true,
|
||||
};
|
||||
|
||||
await sendMessage(params);
|
||||
// TODO: 实现结束直播逻辑
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
console.error("Send message error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// 打开直播窗口
|
||||
ipcMain.handle("open-live-window", async (_, args) => {
|
||||
try {
|
||||
if (liveWindow) {
|
||||
liveWindow.focus();
|
||||
showPrompt("直播窗口已打开", "info");
|
||||
return { success: true };
|
||||
// 创建直播窗口
|
||||
async function createLiveWindow() {
|
||||
const width = 375;
|
||||
const height = 690;
|
||||
let liveUrl = `${ELECTRON_RENDERER_URL}/#/live`;
|
||||
if (liveState.userId) {
|
||||
liveUrl += `?userId=${liveState.userId}`;
|
||||
}
|
||||
const { width, height, path, userId } = args;
|
||||
let liveUrl = `${ELECTRON_RENDERER_URL}/#/${path}`;
|
||||
if (userId) {
|
||||
liveUrl += `?userId=${userId}`;
|
||||
}
|
||||
// TODO: 实现打开直播窗口逻辑
|
||||
liveWindow = new BrowserWindow({
|
||||
|
||||
liveState.liveWindow = new BrowserWindow({
|
||||
title: "直播窗口",
|
||||
width,
|
||||
height,
|
||||
minimizable: false, // 是否可以最小化
|
||||
maximizable: false, // 是否可以最小化
|
||||
closable: true, // 窗口是否可关闭
|
||||
alwaysOnTop: true, // 窗口是否永远在别的窗口的上面
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
closable: true,
|
||||
alwaysOnTop: true,
|
||||
webPreferences: {
|
||||
preload,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
webSecurity: false, // 允许访问本地文件
|
||||
allowRunningInsecureContent: true, // 允许运行本地内容
|
||||
},
|
||||
});
|
||||
// liveWindow.webContents.openDevTools();
|
||||
liveWindow.on("closed", () => {
|
||||
liveWindow = null;
|
||||
|
||||
liveState.liveWindow.on("closed", () => {
|
||||
liveState.liveWindow = null;
|
||||
liveState.isLiveOn = false;
|
||||
});
|
||||
console.log(liveUrl);
|
||||
if (ELECTRON_RENDERER_URL) {
|
||||
liveWindow.loadURL(liveUrl);
|
||||
setTimeout(async () => {
|
||||
const res = await getSessionId({ userId: userId });
|
||||
if (res.success) {
|
||||
LiveSessionId = res.sessionId;
|
||||
await liveState.liveWindow.loadURL(liveUrl);
|
||||
} else {
|
||||
await liveState.liveWindow.loadFile(indexHtml, { hash: "/live" });
|
||||
}
|
||||
}
|
||||
|
||||
// 等待页面加载完成
|
||||
// @ts-ignore - 暂时未使用但保留以备将来使用
|
||||
async function waitForPageLoad() {
|
||||
if (!liveState.liveWindow) {
|
||||
throw new Error("直播窗口不存在");
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("页面加载超时"));
|
||||
}, 30000); // 30秒超时
|
||||
|
||||
// 监听页面加载完成事件
|
||||
liveState.liveWindow!.webContents.once("did-finish-load", () => {
|
||||
clearTimeout(timeout);
|
||||
console.log("页面加载完成");
|
||||
|
||||
// 额外等待一下确保iframe也加载完成
|
||||
setTimeout(() => {
|
||||
// 通知所有渲染进程状态已更新
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
allWindows.forEach((window: BrowserWindow) => {
|
||||
if (window.webContents && !window.isDestroyed()) {
|
||||
window.webContents.send("live-status-updated", {
|
||||
hasLiveWindow: true,
|
||||
windowLoaded: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
resolve(true);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// 监听加载失败事件
|
||||
liveState.liveWindow!.webContents.once(
|
||||
"did-fail-load",
|
||||
(_, errorCode, errorDesc) => {
|
||||
clearTimeout(timeout);
|
||||
reject(
|
||||
new Error(`页面加载失败: ${errorDesc} (${errorCode})`),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 文件管理处理器
|
||||
ipcMain.handle("show-files-in-live", async (_, { files }) => {
|
||||
console.log("收到文件", files);
|
||||
try {
|
||||
// 使用当前直播窗口的 webContents
|
||||
if (liveState.liveWindow && !liveState.liveWindow.isDestroyed()) {
|
||||
console.log("发送文件路径到直播窗口:", files.length, "个文件");
|
||||
|
||||
// 直接传递文件路径信息,不读取文件内容
|
||||
const processedFiles = files.map((file) => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
path: file.path,
|
||||
// 为支持的文件类型生成 file:// URL
|
||||
url: file.path
|
||||
? `file://${file.path.replace(/\\/g, "/")}`
|
||||
: null,
|
||||
}));
|
||||
|
||||
console.log(
|
||||
"Session ID obtained successfully",
|
||||
LiveSessionId,
|
||||
"处理后的文件数据:",
|
||||
processedFiles.map((f) => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
size: f.size,
|
||||
hasPath: !!f.path,
|
||||
hasUrl: !!f.url,
|
||||
})),
|
||||
);
|
||||
|
||||
// 发送文件数据到直播窗口
|
||||
liveState.liveWindow.webContents.send("show-files-display", {
|
||||
files: processedFiles,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `文件已发送到直播窗口 (${files.length}个文件)`,
|
||||
};
|
||||
} else {
|
||||
throw new Error("直播窗口未找到或已关闭");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("展示文件失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || "展示文件失败",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("remove-file-from-live", async (_, { index, fileName }) => {
|
||||
try {
|
||||
if (liveState.liveWindow && !liveState.liveWindow.isDestroyed()) {
|
||||
liveState.liveWindow.webContents.send("remove-file-display", {
|
||||
index,
|
||||
fileName,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "已通知直播窗口移除文件",
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("移除文件展示失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || "移除文件展示失败",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("clear-all-files-from-live", async () => {
|
||||
try {
|
||||
if (liveState.liveWindow && !liveState.liveWindow.isDestroyed()) {
|
||||
liveState.liveWindow.webContents.send(
|
||||
"clear-all-files-display",
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}, 3000);
|
||||
} else {
|
||||
liveWindow.loadFile(indexHtml, { hash: `/${path}` });
|
||||
}
|
||||
return { success: true };
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "已通知直播窗口清空所有文件",
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getSessionId(requestBody: object) {
|
||||
try {
|
||||
const response = await fetch("http://ywpt.hx.cn/dmhx/get_sessionid", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
// 首先检查响应内容类型
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
// 如果不是JSON,读取原始文本进行调试
|
||||
const rawText = await response.text();
|
||||
console.warn("服务器返回非JSON响应:", rawText);
|
||||
|
||||
// 尝试解析可能的JSON响应(即使Content-Type不正确)
|
||||
try {
|
||||
const data = JSON.parse(rawText);
|
||||
if (response.ok && data.sessionid) {
|
||||
return { success: true, sessionId: data.sessionid };
|
||||
} else {
|
||||
console.error("清空文件展示失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: data.message || "服务器返回非JSON格式",
|
||||
error: error.message || "清空文件展示失败",
|
||||
};
|
||||
}
|
||||
} catch (parseError) {
|
||||
return {
|
||||
success: false,
|
||||
error: `服务器响应格式错误: ${rawText.substring(0, 100)}...`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是JSON,正常解析
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.sessionid) {
|
||||
return { success: true, sessionId: data.sessionid };
|
||||
} else {
|
||||
return { success: false, error: data.message || "未知错误" };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error in getSessionId:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(requestBody: object) {
|
||||
try {
|
||||
const response = await fetch(`http://ywpt.hx.cn/dmhx/human`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
return { success: true };
|
||||
} else {
|
||||
return { success: false, error: data.message || "未知错误" };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error in sendMessage:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// 导出状态(用于调试)
|
||||
export const getLiveState = () => ({ ...liveState });
|
||||
|
||||
@@ -9,6 +9,9 @@ let InstallWindows: BrowserWindow | null = null;
|
||||
export function setupWorkflowHandlers() {
|
||||
let lastJobSummary = "这是我们今天介绍的第一个岗位";
|
||||
|
||||
// 存储用户确认回调的Map
|
||||
const modelDownloadCallbacks = new Map<string, { confirm: Function, reject: Function }>();
|
||||
|
||||
// 打开安装窗口
|
||||
ipcMain.handle("open-install-window", async (_, args) => {
|
||||
try {
|
||||
@@ -197,6 +200,259 @@ export function setupWorkflowHandlers() {
|
||||
return await checkOllamaServer();
|
||||
});
|
||||
|
||||
// 检查指定模型是否存在
|
||||
ipcMain.handle("check-model-exists", async (_, modelName = "qwen3:8b") => {
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:11434/api/tags", {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.models || [];
|
||||
|
||||
// 检查模型是否存在于本地
|
||||
const modelExists = models.some((model: any) => model.name === modelName);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
exists: modelExists,
|
||||
models: models.map((m: any) => ({ name: m.name, size: m.size, modified_at: m.modified_at }))
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("Check model error:", error);
|
||||
return {
|
||||
success: false,
|
||||
exists: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 加载模型(检查ollama状<61><E78AB6><EFBFBD>,下载模型如果不存在)
|
||||
ipcMain.handle("load-model", async (_, modelName = "qwen3:8b") => {
|
||||
const webContents = BrowserWindow.getFocusedWindow()?.webContents;
|
||||
|
||||
const sendStatus = (status: string, type = "info") => {
|
||||
if (webContents && !webContents.isDestroyed()) {
|
||||
webContents.send("model-load-progress", {
|
||||
status,
|
||||
type,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 发送询问是否下载模型的消息
|
||||
const askUserToDownload = () => {
|
||||
if (webContents && !webContents.isDestroyed()) {
|
||||
webContents.send("model-download-confirm", {
|
||||
modelName,
|
||||
message: `模型 ${modelName} 不存在,是否下载?`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
sendStatus("正在检查Ollama服务状态...", "info");
|
||||
|
||||
// 1. 检查Ollama是否运行
|
||||
const isOllamaRunning = await checkOllamaServer();
|
||||
if (!isOllamaRunning) {
|
||||
sendStatus("Ollama服务未运行,正在启动...", "warning");
|
||||
|
||||
try {
|
||||
// 尝试启动Ollama服务
|
||||
await runCommand("ollama", ["ps"]);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
const isRunningNow = await checkOllamaServer();
|
||||
if (!isRunningNow) {
|
||||
throw new Error("无法启动Ollama服务,请手动启动");
|
||||
}
|
||||
sendStatus("Ollama服务启动成功", "success");
|
||||
} catch (error: any) {
|
||||
sendStatus(`启动Ollama服务失败: ${error.message}`, "error");
|
||||
return {
|
||||
success: false,
|
||||
message: `Ollama服务启动失败: ${error.message}`,
|
||||
downloaded: false
|
||||
};
|
||||
}
|
||||
} else {
|
||||
sendStatus("Ollama服务正在运行", "success");
|
||||
}
|
||||
|
||||
// 2. 检查模型是否存在
|
||||
sendStatus(`正在检查模型 ${modelName} 是否存在...`, "info");
|
||||
|
||||
const modelCheckResult = await new Promise<{ exists: boolean, models: any[] }>((resolve, reject) => {
|
||||
fetch("http://127.0.0.1:11434/api/tags", {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const models = data.models || [];
|
||||
const modelExists = models.some((model: any) => model.name === modelName);
|
||||
resolve({ exists: modelExists, models });
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
if (modelCheckResult.exists) {
|
||||
sendStatus(`模型 ${modelName} 已存在,无需下载`, "success");
|
||||
return {
|
||||
success: true,
|
||||
message: `模型 ${modelName} 已就绪`,
|
||||
downloaded: false
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 模型不存在,询问用户是否下载
|
||||
askUserToDownload();
|
||||
|
||||
// 等待用户确认
|
||||
const userConfirmed = await new Promise<boolean>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
modelDownloadCallbacks.delete(modelName);
|
||||
resolve(false); // 30秒超时自动取消
|
||||
}, 30000);
|
||||
|
||||
// 存储回调函数
|
||||
modelDownloadCallbacks.set(modelName, {
|
||||
confirm: () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(true);
|
||||
},
|
||||
reject: (error: any) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!userConfirmed) {
|
||||
sendStatus("用户取消了模型下载", "info");
|
||||
return {
|
||||
success: false,
|
||||
message: `用户取消了 ${modelName} 模型的下载`,
|
||||
downloaded: false
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 用户确认,开始下载模型
|
||||
sendStatus(`开始下载模型 ${modelName},这可能需要一些时间...`, "info");
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const process = spawn("ollama", ["pull", modelName], { shell: true });
|
||||
|
||||
const sendProgress = (data: any) => {
|
||||
if (webContents && !webContents.isDestroyed()) {
|
||||
webContents.send("model-load-progress", {
|
||||
status: data.toString().trim(),
|
||||
type: "download",
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
process.stdout.on("data", sendProgress);
|
||||
process.stderr.on("data", sendProgress);
|
||||
|
||||
process.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`模型下载失败,退出码: ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
process.on("error", (err: any) => {
|
||||
reject(new Error(`启动下载进程失败: ${err.message}`));
|
||||
});
|
||||
});
|
||||
|
||||
sendStatus(`模型 ${modelName} 下载完成!`, "success");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `模型 ${modelName} 下载并加载成功`,
|
||||
downloaded: true
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Load model error:", error);
|
||||
sendStatus(`加载模型失败: ${error.message}`, "error");
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
downloaded: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 处理用户对模型下载的确认响应
|
||||
ipcMain.on("model-download-confirm-response", (_event, data) => {
|
||||
const { modelName, confirmed } = data;
|
||||
const callback = modelDownloadCallbacks.get(modelName);
|
||||
|
||||
if (callback) {
|
||||
modelDownloadCallbacks.delete(modelName);
|
||||
if (confirmed) {
|
||||
callback.confirm();
|
||||
} else {
|
||||
callback.reject(new Error("用户取消了模型下载"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 润色文本的处理器
|
||||
ipcMain.handle("polish-text", async (_, text) => {
|
||||
try {
|
||||
if (!text || typeof text !== 'string' || text.trim() === '') {
|
||||
return {
|
||||
success: false,
|
||||
error: "输入文本不能为空"
|
||||
};
|
||||
}
|
||||
|
||||
const systemPrompt = `你是一个专业的文本润色专家。请将以下文本进行润色,使其更加流畅、自然、专业。要求:
|
||||
1. 保持原意不变
|
||||
2. 使语言更加流畅自然
|
||||
3. 提升表达的准确性
|
||||
4. 适合直播场合使用
|
||||
5. 保持简洁明了
|
||||
|
||||
请直接返回润色后的文本,不要添加任何其他说明或解释。
|
||||
|
||||
原文:${text.trim()}`;
|
||||
|
||||
const polishedText = await runOllamaNonStream(systemPrompt, "qwen3:8b");
|
||||
|
||||
if (!polishedText) {
|
||||
throw new Error("AI模型返回为空");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: polishedText.trim()
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("润色文本失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || "润色服务出现错误"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 处理器:检查服务,如果没运行,就用一个轻量命令唤醒它
|
||||
ipcMain.handle("ensure-ollama-running", async () => {
|
||||
let isRunning = await checkOllamaServer();
|
||||
|
||||
@@ -12,3 +12,41 @@ export function showPrompt(
|
||||
buttons: ["确定"],
|
||||
});
|
||||
}
|
||||
|
||||
export function convertJobData(originalData) {
|
||||
// 检查输入是否为对象
|
||||
if (typeof originalData !== "object" || originalData === null) {
|
||||
console.error("输入必须是一个有效的对象。");
|
||||
return {};
|
||||
}
|
||||
|
||||
const convertedData = {
|
||||
jobId: originalData.jobId,
|
||||
jobTitle: originalData.jobTitle,
|
||||
education: originalData.education,
|
||||
experience: originalData.experience,
|
||||
companyName: originalData.companyName,
|
||||
jobLocation: originalData.jobLocation,
|
||||
description: originalData.description,
|
||||
scale: originalData.scale,
|
||||
};
|
||||
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
// async function jobTransformAIobj(job: any) {
|
||||
// // const val = await window.electron.ipcRenderer.invoke("ollama-test", job);
|
||||
// const val = await window.electron.ipcRenderer.invoke(
|
||||
// "run-job-workflow",
|
||||
// job,
|
||||
// );
|
||||
|
||||
// if (val.success === false) {
|
||||
// throw new Error(val.error);
|
||||
// }
|
||||
// let record = {
|
||||
// ...job,
|
||||
// msg: val.data,
|
||||
// };
|
||||
// return record;
|
||||
// }
|
||||
|
||||
18
src/preload/index.d.ts
vendored
18
src/preload/index.d.ts
vendored
@@ -1,8 +1,22 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
interface ElectronAPI {
|
||||
ipcRenderer: {
|
||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||
on: (channel: string, listener: (...args: any[]) => void) => void;
|
||||
off: (channel: string, listener: (...args: any[]) => void) => void;
|
||||
send: (channel: string, ...args: any[]) => void;
|
||||
removeAllListeners: (channel: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface API {
|
||||
installOllama: () => Promise<any>;
|
||||
onInstallProgress: (callback: (value: any) => void) => void;
|
||||
removeInstallProgressListeners: () => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: unknown
|
||||
api: API
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import { electronAPI } from "@electron-toolkit/preload";
|
||||
|
||||
const electronAPI = {
|
||||
ipcRenderer: {
|
||||
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args),
|
||||
on: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.on(channel, listener),
|
||||
off: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.off(channel, listener),
|
||||
send: (channel: string, ...args: any[]) => ipcRenderer.send(channel, ...args),
|
||||
removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel),
|
||||
}
|
||||
};
|
||||
|
||||
const api = {
|
||||
installOllama: () => ipcRenderer.invoke("install-ollama-and-model"),
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// 岗位接口类型定义
|
||||
export interface Job {
|
||||
jobId: number;
|
||||
jobTitle: string;
|
||||
minSalary?: number;
|
||||
maxSalary?: number;
|
||||
education: string;
|
||||
experience: string;
|
||||
companyName: string;
|
||||
jobLocation: string;
|
||||
description: string;
|
||||
scale: string;
|
||||
createTime?: string;
|
||||
industry?: string;
|
||||
jobCategory?: string;
|
||||
companyNature?: string;
|
||||
msg?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: Job;
|
||||
}
|
||||
|
||||
// 模拟分页查询
|
||||
export async function getJob(): Promise<ApiResponse> {
|
||||
return fetch("https://qd.zhaopinzao8dian.com/api/app/job/metaInfo")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
@@ -5,8 +5,10 @@ import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import * as Pinia from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import Antd from "ant-design-vue";
|
||||
import "ant-design-vue/dist/reset.css";
|
||||
|
||||
const pinia = Pinia.createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
createApp(App).use(router).use(pinia).mount("#app");
|
||||
createApp(App).use(Antd).use(router).use(pinia).mount("#app");
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
import { ref, watch } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { getJob, type Job } from "@renderer/api/jobs";
|
||||
|
||||
export interface Position {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export const useLiveStore = defineStore("live", () => {
|
||||
const positions = ref<Job[]>([]);
|
||||
const currentPosition = ref<Job | null>(null);
|
||||
const seePosition = ref<Job | null>(null);
|
||||
const isLiveOn = ref(false);
|
||||
const isLiveWindowOpen = ref(false);
|
||||
const isExplaining = ref(false); // 是否正在讲解中
|
||||
const isStartingLive = ref(false); // 是否正在开始直播中
|
||||
|
||||
// 获取岗位列表
|
||||
async function fetchPositions() {
|
||||
try {
|
||||
const resData = await getJob();
|
||||
if (resData.code === 200) {
|
||||
const job = convertJobData(resData.data);
|
||||
const val = await jobTransformAIobj(job);
|
||||
positions.value = [...positions.value, val];
|
||||
if (positions.value.length < 10) {
|
||||
fetchPositions();
|
||||
}
|
||||
} else {
|
||||
throw new Error(resData.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
alert("获取岗位失败");
|
||||
console.error("获取岗位列表失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始直播
|
||||
async function startLive() {
|
||||
if (isStartingLive.value) return; // 防止重复启动
|
||||
try {
|
||||
isStartingLive.value = true;
|
||||
const result =
|
||||
await window.electron.ipcRenderer.invoke("start-live");
|
||||
|
||||
if (result.success) {
|
||||
isLiveOn.value = true;
|
||||
// 确保有岗位可以讲解,并触发 getNextPosition 来启动讲解
|
||||
if (positions.value.length > 0 && !currentPosition.value) {
|
||||
getNextPosition();
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("开始直播失败:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
isStartingLive.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentPosition(position: Job) {
|
||||
if (position.jobId === seePosition.value?.jobId) {
|
||||
seePosition.value = null;
|
||||
} else {
|
||||
seePosition.value = position;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始讲解岗位
|
||||
async function startExplainingPosition(job: Job) {
|
||||
if (!isLiveOn.value) return;
|
||||
|
||||
try {
|
||||
isExplaining.value = true;
|
||||
// 这里添加讲解岗位的具体实现
|
||||
// 可以调用 AI 接口进行讲解
|
||||
const result = await window.electron.ipcRenderer.invoke(
|
||||
"explain-position",
|
||||
job.msg,
|
||||
);
|
||||
if (result.success) {
|
||||
// 讲解完成后,获取下一个岗位
|
||||
// await completeCurrentPosition();
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("岗位讲解失败:", error);
|
||||
isExplaining.value = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 结束直播
|
||||
async function stopLive() {
|
||||
try {
|
||||
const result =
|
||||
await window.electron.ipcRenderer.invoke("stop-live");
|
||||
if (result.success) {
|
||||
isLiveOn.value = false;
|
||||
isExplaining.value = false;
|
||||
// 只清空当前岗位,不移动列表中的岗位
|
||||
currentPosition.value = null;
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("结束直播失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 打开直播窗口
|
||||
async function openLiveWindow() {
|
||||
try {
|
||||
const result = await window.electron.ipcRenderer.invoke(
|
||||
"open-live-window",
|
||||
{ path: "live", width: 375, height: 682, userId: "rs876543" },
|
||||
);
|
||||
if (result.success) {
|
||||
isLiveWindowOpen.value = true;
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("打开直播窗口失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取下一个要讲解的岗位
|
||||
function getNextPosition() {
|
||||
if (!isLiveOn.value) return; // 只在直播开启时切换岗位
|
||||
|
||||
if (positions.value.length > 0) {
|
||||
// 设置下一个要讲解的岗位
|
||||
currentPosition.value = positions.value[0];
|
||||
// 从列表中移除该岗位
|
||||
positions.value = positions.value.slice(1);
|
||||
|
||||
// 如果直播已经开启,自动开始讲解
|
||||
if (isLiveOn.value && !isExplaining.value) {
|
||||
startExplainingPosition(currentPosition.value).catch(
|
||||
(error) => {
|
||||
console.error("自动开始讲解失败:", error);
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 没有更多岗位时
|
||||
currentPosition.value = null;
|
||||
isExplaining.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 完成当前岗位讲解
|
||||
async function completeCurrentPosition() {
|
||||
if (currentPosition.value) {
|
||||
// 从列表中移除当前岗位
|
||||
positions.value = positions.value.filter(
|
||||
(job) => job.jobId !== currentPosition.value?.jobId,
|
||||
);
|
||||
isExplaining.value = false;
|
||||
// 自动获取下一个岗位
|
||||
getNextPosition();
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 currentPosition 的变化
|
||||
watch(currentPosition, (newVal) => {
|
||||
// 当 currentPosition 为空且不是正在讲解状态时,自动获取下一个岗位
|
||||
if (!newVal && !isExplaining.value && positions.value.length > 0) {
|
||||
getNextPosition();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 positions 的变化
|
||||
watch(
|
||||
positions,
|
||||
(newVal) => {
|
||||
console.log("positions changed:", newVal);
|
||||
if (
|
||||
newVal.length > 0 &&
|
||||
!currentPosition.value &&
|
||||
!isExplaining.value
|
||||
) {
|
||||
getNextPosition();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true, // 深度监听数组内容的变化
|
||||
immediate: true, // 立即执行一次回调
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
positions,
|
||||
currentPosition,
|
||||
isLiveOn,
|
||||
isLiveWindowOpen,
|
||||
setCurrentPosition,
|
||||
seePosition,
|
||||
isExplaining,
|
||||
fetchPositions,
|
||||
completeCurrentPosition,
|
||||
getNextPosition,
|
||||
startLive,
|
||||
stopLive,
|
||||
openLiveWindow,
|
||||
};
|
||||
});
|
||||
|
||||
async function jobTransformAIobj(job: any) {
|
||||
// const val = await window.electron.ipcRenderer.invoke("ollama-test", job);
|
||||
const val = await window.electron.ipcRenderer.invoke(
|
||||
"run-job-workflow",
|
||||
job,
|
||||
);
|
||||
|
||||
if (val.success === false) {
|
||||
throw new Error(val.error);
|
||||
}
|
||||
let record = {
|
||||
...job,
|
||||
msg: val.data,
|
||||
};
|
||||
return record;
|
||||
}
|
||||
|
||||
function convertJobData(originalData) {
|
||||
// 检查输入是否为对象
|
||||
if (typeof originalData !== "object" || originalData === null) {
|
||||
console.error("输入必须是一个有效的对象。");
|
||||
return {};
|
||||
}
|
||||
|
||||
const convertedData = {
|
||||
jobId: originalData.jobId,
|
||||
jobTitle: originalData.jobTitle,
|
||||
education: originalData.education,
|
||||
experience: originalData.experience,
|
||||
companyName: originalData.companyName,
|
||||
jobLocation: originalData.jobLocation,
|
||||
description: originalData.description,
|
||||
scale: originalData.scale,
|
||||
};
|
||||
|
||||
return convertedData;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,16 +8,42 @@
|
||||
<audio :src="soundUrl" class="sound" ref="welcome" @ended="welcomeEnd"></audio>
|
||||
<!-- 隐藏的麦克风播放元素,用于放大麦克风声音 -->
|
||||
<audio ref="micAudio" autoplay style="display:none"></audio>
|
||||
|
||||
<!-- 文件悬浮展示区域 - 仅支持图片和视频 -->
|
||||
<div class="files-display-overlay" v-if="displayedFiles.length > 0">
|
||||
<div class="files-container">
|
||||
<div v-for="(file, index) in displayedFiles" :key="index" class="file-display-item">
|
||||
<!-- 关闭按钮 -->
|
||||
<button class="file-close-btn" @click="removeFileFromDisplay(index)">×</button>
|
||||
|
||||
<!-- 图片文件展示 -->
|
||||
<div v-if="file.type.startsWith('image/')" class="image-display">
|
||||
<img :src="file.data" :alt="file.name" />
|
||||
<!-- <div class="file-name-overlay">{{ file.name }}</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 视频文件展示 -->
|
||||
<div v-else-if="file.type.startsWith('video/')" class="video-display">
|
||||
<video :src="file.data" controls autoplay muted />
|
||||
<!-- <div class="file-name-overlay">{{ file.name }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useRoute } from 'vue-router';
|
||||
// 不再需要音频和文档图标
|
||||
|
||||
const route = useRoute();
|
||||
const isCameraActive = ref(false)
|
||||
const isfullScreen = ref(false);
|
||||
|
||||
// 文件展示状态
|
||||
const displayedFiles = ref([]);
|
||||
const userId = ref(null);
|
||||
const liveUrl = ref("");
|
||||
const soundUrl = ref("");
|
||||
@@ -49,26 +75,117 @@ function welcomeEnd() {
|
||||
}
|
||||
|
||||
|
||||
// 文件展示相关函数
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 处理展示文件的请求 - 仅支持图片和视频
|
||||
const handleShowFilesDisplay = (_event, data) => {
|
||||
console.log('收到文件展示请求:', data);
|
||||
const files = data.files || [];
|
||||
|
||||
// 过滤并处理文件数据,仅保留图片和视频文件
|
||||
const processedFiles = files
|
||||
.filter(file => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
console.log(`文件过滤: ${file.name} (${file.type}) - ${isImage || isVideo ? '保留' : '过滤掉'}`);
|
||||
return isImage || isVideo;
|
||||
})
|
||||
.map(file => {
|
||||
console.log(`处理文件: ${file.name}, 路径: ${file.path}, URL: ${file.url}`);
|
||||
|
||||
if (file.url || file.path) {
|
||||
// 如果有文件路径,直接使用路径
|
||||
const fileUrl = file.url || `file://${file.path.replace(/\\/g, '/')}`;
|
||||
console.log(`使用文件路径: ${fileUrl}`);
|
||||
return {
|
||||
...file,
|
||||
data: fileUrl
|
||||
};
|
||||
} else if (file.data && typeof file.data === 'string') {
|
||||
// 如果没有路径但有base64数据,确保格式正确
|
||||
const base64Data = file.data.includes('base64,') ? file.data : `data:${file.type};base64,${file.data}`;
|
||||
console.log(`使用base64数据: ${base64Data.substring(0, 100)}...`);
|
||||
return {
|
||||
...file,
|
||||
data: base64Data
|
||||
};
|
||||
}
|
||||
return file;
|
||||
});
|
||||
|
||||
displayedFiles.value = processedFiles;
|
||||
console.log('处理后的文件数量:', displayedFiles.value.length);
|
||||
console.log('文件详情:', processedFiles.map(f => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
hasData: !!f.data,
|
||||
hasPath: !!f.path,
|
||||
hasUrl: !!f.url,
|
||||
data: f.data ? f.data.substring(0, 100) : 'no-data' // 显示前100个字符
|
||||
})));
|
||||
};
|
||||
|
||||
// 移除文件展示
|
||||
const removeFileFromDisplay = (index) => {
|
||||
const removedFile = displayedFiles.value[index];
|
||||
displayedFiles.value.splice(index, 1);
|
||||
|
||||
// 通知主进程文件已被移除
|
||||
window.electron.ipcRenderer.send('file-display-removed', {
|
||||
index,
|
||||
fileName: removedFile?.name,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
// 处理移除文件展示的请求
|
||||
const handleRemoveFileDisplay = (_event, data) => {
|
||||
console.log('收到移除文件展示请求:', data);
|
||||
const { index } = data;
|
||||
if (index >= 0 && index < displayedFiles.value.length) {
|
||||
displayedFiles.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理清空所有文件展示的请求
|
||||
const handleClearAllFilesDisplay = (_event, data) => {
|
||||
console.log('收到清空文件展示请求:', data);
|
||||
displayedFiles.value = [];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
startLive()
|
||||
window.electron.ipcRenderer.on('insert-camera-video', handleInsertCameraVideo)
|
||||
window.electron.ipcRenderer.on('insert-video-audio', handleInsertVideoAudio)
|
||||
window.electron.ipcRenderer.on('toggle-camera-insert', handleToggleCameraInsert)
|
||||
window.electron.ipcRenderer.on('force-cleanup-media', handleForceCleanup)
|
||||
|
||||
// 文件展示相关事件
|
||||
window.electron.ipcRenderer.on('show-files-display', handleShowFilesDisplay)
|
||||
window.electron.ipcRenderer.on('remove-file-display', handleRemoveFileDisplay)
|
||||
window.electron.ipcRenderer.on('clear-all-files-display', handleClearAllFilesDisplay)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.electron.ipcRenderer.off('insert-camera-video', handleInsertCameraVideo)
|
||||
window.electron.ipcRenderer.off('insert-video-audio', handleInsertVideoAudio)
|
||||
window.electron.ipcRenderer.off('toggle-camera-insert', handleToggleCameraInsert)
|
||||
// 清理麦克风流
|
||||
if (micStream) {
|
||||
try { micStream.getTracks().forEach((t) => t.stop()) } catch (e) { }
|
||||
micStream = null
|
||||
}
|
||||
if (micAudio.value) {
|
||||
try { micAudio.value.pause() } catch (e) { }
|
||||
micAudio.value.srcObject = null
|
||||
}
|
||||
window.electron.ipcRenderer.off('force-cleanup-media', handleForceCleanup)
|
||||
|
||||
// 清理文件展示相关事件
|
||||
window.electron.ipcRenderer.off('show-files-display', handleShowFilesDisplay)
|
||||
window.electron.ipcRenderer.off('remove-file-display', handleRemoveFileDisplay)
|
||||
window.electron.ipcRenderer.off('clear-all-files-display', handleClearAllFilesDisplay)
|
||||
|
||||
// 清理媒体资源
|
||||
forceCleanupMedia()
|
||||
})
|
||||
|
||||
function startLive() {
|
||||
@@ -76,8 +193,11 @@ function startLive() {
|
||||
console.log(paramsUserId)
|
||||
if (paramsUserId) {
|
||||
userId.value = paramsUserId
|
||||
|
||||
console.log(userId.value)
|
||||
// 直播数字人 正式地址
|
||||
liveUrl.value = `https://dmdemo.hx.cn/dashboard.html?userId=${userId.value}`;
|
||||
// 测试地址
|
||||
// liveUrl.value = "https://www.baidu.com/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +325,67 @@ async function startCamera() {
|
||||
} catch (error) {
|
||||
console.error('Error accessing camera:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 强制清理所有媒体资源
|
||||
function forceCleanupMedia() {
|
||||
try {
|
||||
console.log('强制清理所有媒体资源...');
|
||||
|
||||
// 停止摄像头
|
||||
stopCamera();
|
||||
|
||||
// 停止麦克风
|
||||
if (micStream) {
|
||||
try {
|
||||
micStream.getTracks().forEach(track => track.stop());
|
||||
} catch (e) { console.warn('停止麦克风流失败:', e); }
|
||||
micStream = null;
|
||||
}
|
||||
|
||||
// 清理麦克风音频
|
||||
if (micAudio.value) {
|
||||
try {
|
||||
micAudio.value.pause();
|
||||
micAudio.value.srcObject = null;
|
||||
} catch (e) { console.warn('清理麦克风音频失败:', e); }
|
||||
}
|
||||
|
||||
// 清理欢迎音频
|
||||
if (welcome.value) {
|
||||
try {
|
||||
welcome.value.pause();
|
||||
welcome.value.currentTime = 0;
|
||||
} catch (e) { console.warn('清理欢迎音频失败:', e); }
|
||||
}
|
||||
|
||||
// 恢复iframe内视频音量
|
||||
try {
|
||||
const iframeEl = liveIframe.value;
|
||||
if (iframeEl && iframeEl.contentWindow) {
|
||||
const v = iframeEl.contentWindow.document.querySelector('video');
|
||||
if (v && typeof v.volume === 'number') {
|
||||
v.volume = 1;
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn('恢复iframe音量失败:', e); }
|
||||
|
||||
// 重置状态
|
||||
isCameraActive.value = false;
|
||||
isfullScreen.value = false;
|
||||
micPlaying = false;
|
||||
wasWelcomePlaying = false;
|
||||
|
||||
console.log('媒体资源清理完成');
|
||||
} catch (error) {
|
||||
console.error('强制清理媒体资源出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理强制清理指令
|
||||
function handleForceCleanup() {
|
||||
console.log('收到强制清理指令');
|
||||
forceCleanupMedia();
|
||||
}</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -251,5 +432,126 @@ async function startCamera() {
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 文件悬浮展示样式 */
|
||||
.files-display-overlay {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.files-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
padding: 5px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.file-display-item {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.file-display-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.file-close-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
color: #ff4d4f;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.file-close-btn:hover {
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 图片展示 */
|
||||
.image-display {
|
||||
max-width: 300px;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.image-display img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 视频展示 */
|
||||
.video-display {
|
||||
max-width: 300px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.video-display video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 已移除音频、PDF和其他文档类型的展示样式 */
|
||||
|
||||
/* 文件名覆盖层 */
|
||||
.file-name-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
test-files.txt
Normal file
4
test-files.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
这是一个测试文件
|
||||
用于验证文件拖拽功能
|
||||
内容:Hello World!
|
||||
时间:2025-11-16
|
||||
@@ -1,8 +1,19 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["electron-vite/node"]
|
||||
}
|
||||
"types": ["electron-vite/node"],
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
|
||||
"include": [
|
||||
"src/renderer/src/env.d.ts",
|
||||
"src/renderer/src/**/*.ts",
|
||||
"src/renderer/src/**/*.vue",
|
||||
"src/preload/*.d.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
@@ -14,6 +7,27 @@
|
||||
"src/renderer/src/*"
|
||||
]
|
||||
},
|
||||
"allowJs": true
|
||||
}
|
||||
"allowJs": true,
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src/renderer/src/env.d.ts",
|
||||
"src/renderer/src/**/*.ts",
|
||||
"src/renderer/src/**/*.vue",
|
||||
"src/preload/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user