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实现高效通信。
|
||||||
14815
package-lock.json
generated
14815
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
|
"ant-design-vue": "^4.2.6",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.5.0",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"sass": "^1.93.2",
|
"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 { 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 { setupLiveHandlers } from "./ipc/live";
|
||||||
import { setupPromptHandlers } from "./ipc/prompt";
|
import { setupPromptHandlers } from "./ipc/prompt";
|
||||||
import { setupWorkflowHandlers } from "./ipc/workflow";
|
import { setupWorkflowHandlers } from "./ipc/workflow";
|
||||||
import { preload, indexHtml, ELECTRON_RENDERER_URL } from "./config";
|
import { preload, indexHtml, ELECTRON_RENDERER_URL } from "./config";
|
||||||
|
|
||||||
// 在开发环境下启用热重载
|
// 简单的开发环境检测
|
||||||
if (is.dev) {
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建主窗口
|
* 创建主窗口
|
||||||
@@ -36,7 +16,6 @@ function createWindow(): void {
|
|||||||
height: 670,
|
height: 670,
|
||||||
show: false,
|
show: false,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
...(process.platform === "linux" ? { icon } : {}),
|
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload,
|
preload,
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
@@ -47,14 +26,24 @@ function createWindow(): void {
|
|||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url);
|
shell.openExternal(details.url);
|
||||||
return { action: "deny" };
|
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);
|
mainWindow.loadURL(ELECTRON_RENDERER_URL);
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(indexHtml);
|
mainWindow.loadFile(indexHtml);
|
||||||
@@ -65,13 +54,9 @@ function createWindow(): void {
|
|||||||
* 准备好后
|
* 准备好后
|
||||||
*/
|
*/
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
electronApp.setAppUserModelId("com.electron");
|
app.setAppUserModelId("com.electron");
|
||||||
|
|
||||||
app.on("browser-window-created", (_, window) => {
|
// 直播相关处理
|
||||||
optimizer.watchWindowShortcuts(window);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 直播相关处理
|
|
||||||
setupLiveHandlers();
|
setupLiveHandlers();
|
||||||
|
|
||||||
setupPromptHandlers();
|
setupPromptHandlers();
|
||||||
@@ -89,7 +74,27 @@ app.whenReady().then(() => {
|
|||||||
* 退出应用
|
* 退出应用
|
||||||
*/
|
*/
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
|
// 强制清理所有资源
|
||||||
if (process.platform !== "darwin") {
|
if (process.platform !== "darwin") {
|
||||||
app.quit();
|
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 { ipcMain, BrowserWindow } from "electron";
|
||||||
import { preload, indexHtml, ELECTRON_RENDERER_URL } from "../config";
|
import { preload, indexHtml, ELECTRON_RENDERER_URL } from "../config";
|
||||||
|
import { getSessionId, sendMessage } from "../Api/api";
|
||||||
import { showPrompt } from "../utils/tools";
|
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() {
|
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 () => {
|
ipcMain.on("toggle-camera-insert", async () => {
|
||||||
if (liveWindow) {
|
if (liveState.liveWindow) {
|
||||||
isVideoInserted = !isVideoInserted;
|
liveState.isVideoInserted = !liveState.isVideoInserted;
|
||||||
liveWindow.webContents.send(
|
liveState.liveWindow.webContents.send(
|
||||||
"toggle-camera-insert",
|
"toggle-camera-insert",
|
||||||
isVideoInserted,
|
liveState.isVideoInserted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 插入摄像头视频
|
// 插入摄像头视频
|
||||||
ipcMain.on("insert-camera-video", async (_) => {
|
ipcMain.on("insert-camera-video", async () => {
|
||||||
if (liveWindow) {
|
if (liveState.liveWindow) {
|
||||||
liveWindow.webContents.send("insert-camera-video");
|
liveState.liveWindow.webContents.send("insert-camera-video");
|
||||||
|
liveState.cameraActive = true;
|
||||||
|
} else {
|
||||||
|
showPrompt("直播窗口未打开", "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 插入音频
|
// 插入音频
|
||||||
ipcMain.on("insert-video-audio", async (_) => {
|
ipcMain.on("insert-video-audio", async () => {
|
||||||
if (liveWindow) {
|
if (liveState.liveWindow) {
|
||||||
liveWindow.webContents.send("insert-video-audio");
|
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 {
|
try {
|
||||||
console.log("Starting live stream...");
|
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) {
|
} catch (error: any) {
|
||||||
|
console.error("Start live error:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -47,151 +126,273 @@ export function setupLiveHandlers() {
|
|||||||
// 结束直播
|
// 结束直播
|
||||||
ipcMain.handle("stop-live", async () => {
|
ipcMain.handle("stop-live", async () => {
|
||||||
try {
|
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 };
|
return { success: true };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("explain-position", async (_, content: string) => {
|
// 强制关闭直播窗口
|
||||||
|
ipcMain.handle("force-close-live", async () => {
|
||||||
try {
|
try {
|
||||||
let params = {
|
console.log("Force closing live window...");
|
||||||
sessionid: LiveSessionId,
|
|
||||||
|
// 通知直播窗口清理所有媒体资源
|
||||||
|
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,
|
text: content,
|
||||||
type: "echo",
|
type: "echo",
|
||||||
interrupt: true,
|
interrupt: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
await sendMessage(params);
|
await sendMessage(params);
|
||||||
// TODO: 实现结束直播逻辑
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error("Send message error:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 打开直播窗口
|
// 创建直播窗口
|
||||||
ipcMain.handle("open-live-window", async (_, args) => {
|
async function createLiveWindow() {
|
||||||
try {
|
const width = 375;
|
||||||
if (liveWindow) {
|
const height = 690;
|
||||||
liveWindow.focus();
|
let liveUrl = `${ELECTRON_RENDERER_URL}/#/live`;
|
||||||
showPrompt("直播窗口已打开", "info");
|
if (liveState.userId) {
|
||||||
return { success: true };
|
liveUrl += `?userId=${liveState.userId}`;
|
||||||
}
|
}
|
||||||
const { width, height, path, userId } = args;
|
|
||||||
let liveUrl = `${ELECTRON_RENDERER_URL}/#/${path}`;
|
liveState.liveWindow = new BrowserWindow({
|
||||||
if (userId) {
|
title: "直播窗口",
|
||||||
liveUrl += `?userId=${userId}`;
|
width,
|
||||||
}
|
height,
|
||||||
// TODO: 实现打开直播窗口逻辑
|
minimizable: false,
|
||||||
liveWindow = new BrowserWindow({
|
maximizable: false,
|
||||||
title: "直播窗口",
|
closable: true,
|
||||||
width,
|
alwaysOnTop: true,
|
||||||
height,
|
webPreferences: {
|
||||||
minimizable: false, // 是否可以最小化
|
preload,
|
||||||
maximizable: false, // 是否可以最小化
|
nodeIntegration: true,
|
||||||
closable: true, // 窗口是否可关闭
|
contextIsolation: false,
|
||||||
alwaysOnTop: true, // 窗口是否永远在别的窗口的上面
|
webSecurity: false, // 允许访问本地文件
|
||||||
webPreferences: {
|
allowRunningInsecureContent: true, // 允许运行本地内容
|
||||||
preload,
|
},
|
||||||
nodeIntegration: true,
|
});
|
||||||
contextIsolation: false,
|
|
||||||
|
liveState.liveWindow.on("closed", () => {
|
||||||
|
liveState.liveWindow = null;
|
||||||
|
liveState.isLiveOn = false;
|
||||||
|
});
|
||||||
|
console.log(liveUrl);
|
||||||
|
if (ELECTRON_RENDERER_URL) {
|
||||||
|
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})`),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
// liveWindow.webContents.openDevTools();
|
});
|
||||||
liveWindow.on("closed", () => {
|
}
|
||||||
liveWindow = null;
|
|
||||||
});
|
// 文件管理处理器
|
||||||
if (ELECTRON_RENDERER_URL) {
|
ipcMain.handle("show-files-in-live", async (_, { files }) => {
|
||||||
liveWindow.loadURL(liveUrl);
|
console.log("收到文件", files);
|
||||||
setTimeout(async () => {
|
try {
|
||||||
const res = await getSessionId({ userId: userId });
|
// 使用当前直播窗口的 webContents
|
||||||
if (res.success) {
|
if (liveState.liveWindow && !liveState.liveWindow.isDestroyed()) {
|
||||||
LiveSessionId = res.sessionId;
|
console.log("发送文件路径到直播窗口:", files.length, "个文件");
|
||||||
console.log(
|
|
||||||
"Session ID obtained successfully",
|
// 直接传递文件路径信息,不读取文件内容
|
||||||
LiveSessionId,
|
const processedFiles = files.map((file) => ({
|
||||||
);
|
name: file.name,
|
||||||
}
|
type: file.type,
|
||||||
}, 3000);
|
size: file.size,
|
||||||
|
path: file.path,
|
||||||
|
// 为支持的文件类型生成 file:// URL
|
||||||
|
url: file.path
|
||||||
|
? `file://${file.path.replace(/\\/g, "/")}`
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"处理后的文件数据:",
|
||||||
|
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 {
|
} else {
|
||||||
liveWindow.loadFile(indexHtml, { hash: `/${path}` });
|
throw new Error("直播窗口未找到或已关闭");
|
||||||
}
|
}
|
||||||
return { success: true };
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { success: false, error: error.message };
|
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(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "已通知直播窗口清空所有文件",
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("清空文件展示失败:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || "清空文件展示失败",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSessionId(requestBody: object) {
|
// 导出状态(用于调试)
|
||||||
try {
|
export const getLiveState = () => ({ ...liveState });
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ let InstallWindows: BrowserWindow | null = null;
|
|||||||
export function setupWorkflowHandlers() {
|
export function setupWorkflowHandlers() {
|
||||||
let lastJobSummary = "这是我们今天介绍的第一个岗位";
|
let lastJobSummary = "这是我们今天介绍的第一个岗位";
|
||||||
|
|
||||||
|
// 存储用户确认回调的Map
|
||||||
|
const modelDownloadCallbacks = new Map<string, { confirm: Function, reject: Function }>();
|
||||||
|
|
||||||
// 打开安装窗口
|
// 打开安装窗口
|
||||||
ipcMain.handle("open-install-window", async (_, args) => {
|
ipcMain.handle("open-install-window", async (_, args) => {
|
||||||
try {
|
try {
|
||||||
@@ -197,6 +200,259 @@ export function setupWorkflowHandlers() {
|
|||||||
return await checkOllamaServer();
|
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 () => {
|
ipcMain.handle("ensure-ollama-running", async () => {
|
||||||
let isRunning = await checkOllamaServer();
|
let isRunning = await checkOllamaServer();
|
||||||
|
|||||||
@@ -12,3 +12,41 @@ export function showPrompt(
|
|||||||
buttons: ["确定"],
|
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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: ElectronAPI
|
electron: ElectronAPI
|
||||||
api: unknown
|
api: API
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { contextBridge, ipcRenderer } from "electron";
|
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 = {
|
const api = {
|
||||||
installOllama: () => ipcRenderer.invoke("install-ollama-and-model"),
|
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 router from "./router";
|
||||||
import * as Pinia from "pinia";
|
import * as Pinia from "pinia";
|
||||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||||
|
import Antd from "ant-design-vue";
|
||||||
|
import "ant-design-vue/dist/reset.css";
|
||||||
|
|
||||||
const pinia = Pinia.createPinia();
|
const pinia = Pinia.createPinia();
|
||||||
pinia.use(piniaPluginPersistedstate);
|
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 :src="soundUrl" class="sound" ref="welcome" @ended="welcomeEnd"></audio>
|
||||||
<!-- 隐藏的麦克风播放元素,用于放大麦克风声音 -->
|
<!-- 隐藏的麦克风播放元素,用于放大麦克风声音 -->
|
||||||
<audio ref="micAudio" autoplay style="display:none"></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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
// 不再需要音频和文档图标
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const isCameraActive = ref(false)
|
const isCameraActive = ref(false)
|
||||||
const isfullScreen = ref(false);
|
const isfullScreen = ref(false);
|
||||||
|
|
||||||
|
// 文件展示状态
|
||||||
|
const displayedFiles = ref([]);
|
||||||
const userId = ref(null);
|
const userId = ref(null);
|
||||||
const liveUrl = ref("");
|
const liveUrl = ref("");
|
||||||
const soundUrl = 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(() => {
|
onMounted(() => {
|
||||||
startLive()
|
startLive()
|
||||||
window.electron.ipcRenderer.on('insert-camera-video', handleInsertCameraVideo)
|
window.electron.ipcRenderer.on('insert-camera-video', handleInsertCameraVideo)
|
||||||
window.electron.ipcRenderer.on('insert-video-audio', handleInsertVideoAudio)
|
window.electron.ipcRenderer.on('insert-video-audio', handleInsertVideoAudio)
|
||||||
window.electron.ipcRenderer.on('toggle-camera-insert', handleToggleCameraInsert)
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
window.electron.ipcRenderer.off('insert-camera-video', handleInsertCameraVideo)
|
window.electron.ipcRenderer.off('insert-camera-video', handleInsertCameraVideo)
|
||||||
window.electron.ipcRenderer.off('insert-video-audio', handleInsertVideoAudio)
|
window.electron.ipcRenderer.off('insert-video-audio', handleInsertVideoAudio)
|
||||||
window.electron.ipcRenderer.off('toggle-camera-insert', handleToggleCameraInsert)
|
window.electron.ipcRenderer.off('toggle-camera-insert', handleToggleCameraInsert)
|
||||||
// 清理麦克风流
|
window.electron.ipcRenderer.off('force-cleanup-media', handleForceCleanup)
|
||||||
if (micStream) {
|
|
||||||
try { micStream.getTracks().forEach((t) => t.stop()) } catch (e) { }
|
// 清理文件展示相关事件
|
||||||
micStream = null
|
window.electron.ipcRenderer.off('show-files-display', handleShowFilesDisplay)
|
||||||
}
|
window.electron.ipcRenderer.off('remove-file-display', handleRemoveFileDisplay)
|
||||||
if (micAudio.value) {
|
window.electron.ipcRenderer.off('clear-all-files-display', handleClearAllFilesDisplay)
|
||||||
try { micAudio.value.pause() } catch (e) { }
|
|
||||||
micAudio.value.srcObject = null
|
// 清理媒体资源
|
||||||
}
|
forceCleanupMedia()
|
||||||
})
|
})
|
||||||
|
|
||||||
function startLive() {
|
function startLive() {
|
||||||
@@ -76,8 +193,11 @@ function startLive() {
|
|||||||
console.log(paramsUserId)
|
console.log(paramsUserId)
|
||||||
if (paramsUserId) {
|
if (paramsUserId) {
|
||||||
userId.value = paramsUserId
|
userId.value = paramsUserId
|
||||||
|
console.log(userId.value)
|
||||||
|
// 直播数字人 正式地址
|
||||||
liveUrl.value = `https://dmdemo.hx.cn/dashboard.html?userId=${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) {
|
} catch (error) {
|
||||||
console.error('Error accessing camera:', 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>
|
}</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -251,5 +432,126 @@ async function startCamera() {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
object-fit: cover;
|
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>
|
</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": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"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": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
@@ -14,6 +7,27 @@
|
|||||||
"src/renderer/src/*"
|
"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