flat: 暂存

This commit is contained in:
Apcallover
2025-12-22 16:32:42 +08:00
parent e9c90d759b
commit 92ec6504f9
18 changed files with 275 additions and 187 deletions

View File

@@ -0,0 +1,9 @@
---
name: 直播带岗平台
description: 一个 electron 的项目, 一个通过文本实时数字人直播推流软件,主要有三个页面,和一些 交互页面设计1、一个登录页面 2、一个控制中台页面 (文本润色功能、图片上传功能、快捷回复功能、粘贴 json 或文本 岗位信息进行润色优化功能、岗位播放列表功能、打开直播窗口按钮、关闭直播按钮、摄像头全屏插播、摄像头窗口插播、文本阅读分段正在播放的文本区域显示已正在播放和已播放的特殊颜色表示未开始的文本呈默认颜色、设置按钮、查看岗位详情、AI 模型管理、Ai 模型检测、爱模型下载、设置图标、查看岗位详情等 3、一个直播窗口移动端窗口
tools: list_files, search_file, search_content, read_file, read_lints, replace_in_file, write_to_file, execute_command, create_rule, delete_files, web_fetch, use_skill, web_search
agentMode: agentic
enabled: true
enabledAutoRun: true
---
请不要过多废话,直接更改代码

View File

@@ -0,0 +1,10 @@
---
description:
alwaysApply: true
enabled: true
updatedAt: 2025-12-20T10:05:42.556Z
provider:
---
1、本项目是 electron-builder + vue3 开发的 pc 端应用
2、禁止编写说明文档直接给出代码

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 703 KiB

9
package-lock.json generated
View File

@@ -29,7 +29,7 @@
"@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
"electron": "^28.2.0", "electron": "^28.3.3",
"electron-builder": "^24.9.1", "electron-builder": "^24.9.1",
"electron-reloader": "^1.2.3", "electron-reloader": "^1.2.3",
"electron-vite": "^2.0.0", "electron-vite": "^2.0.0",
@@ -4053,10 +4053,11 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "28.2.0", "version": "28.3.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-28.2.0.tgz", "resolved": "https://registry.npmmirror.com/electron/-/electron-28.3.3.tgz",
"integrity": "sha512-22SylXQQ9IHtwLw4D+Z4Si7OUpeDtpHfJVTjy3yv53iLg5zJKKPOCWT4ZwgYGHQZ0eldyBrYBHF/P9FPd2CcVQ==", "integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",

View File

@@ -1,5 +1,6 @@
{ {
"name": "electron-app", "name": "electron-app",
"productName": "直播控制台",
"version": "1.0.0", "version": "1.0.0",
"description": "An Electron application with Vue and TypeScript", "description": "An Electron application with Vue and TypeScript",
"main": "./out/main/index.js", "main": "./out/main/index.js",
@@ -41,7 +42,7 @@
"@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
"electron": "^28.2.0", "electron": "^28.3.3",
"electron-builder": "^24.9.1", "electron-builder": "^24.9.1",
"electron-reloader": "^1.2.3", "electron-reloader": "^1.2.3",
"electron-vite": "^2.0.0", "electron-vite": "^2.0.0",

View File

@@ -1,11 +1,18 @@
import { app, shell, BrowserWindow } from "electron"; import { app, shell, BrowserWindow } from "electron";
import { setupLiveHandlers } from "./ipc/live"; import { setupLiveHandlers } from "./ipc/live";
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";
// 必须在 app ready 之前调用
app.commandLine.appendSwitch("disable-site-isolation-trials");
app.commandLine.appendSwitch(
"disable-features",
"IsolateOrigins,site-per-process",
);
// 简单的开发环境检测 // 简单的开发环境检测
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === "development";
/** /**
* 创建主窗口 * 创建主窗口
@@ -15,6 +22,7 @@ function createWindow(): void {
width: 1080, width: 1080,
height: 670, height: 670,
show: false, show: false,
titleBarStyle: "hidden",
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
preload, preload,
@@ -59,8 +67,6 @@ app.whenReady().then(() => {
// 直播相关处理 // 直播相关处理
setupLiveHandlers(); setupLiveHandlers();
setupPromptHandlers();
setupWorkflowHandlers(); setupWorkflowHandlers();
createWindow(); createWindow();
@@ -84,7 +90,7 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => { app.on("before-quit", () => {
// 清理所有资源 // 清理所有资源
const windows = BrowserWindow.getAllWindows(); const windows = BrowserWindow.getAllWindows();
windows.forEach(window => { windows.forEach((window) => {
window.removeAllListeners(); window.removeAllListeners();
window.close(); window.close();
}); });

View File

@@ -91,7 +91,7 @@ export function setupLiveHandlers() {
} }
// 3. 等待网页加载完成 // 3. 等待网页加载完成
// await waitForPageLoad(); await waitForIframeVideoPlaying(liveState.liveWindow!.webContents);
// 4. 获取 sessionId // 4. 获取 sessionId
console.log("获取 sessionId"); console.log("获取 sessionId");
@@ -115,7 +115,7 @@ export function setupLiveHandlers() {
interrupt: true, interrupt: true,
}); });
showPrompt("直播已开始", "info"); // showPrompt("直播已开始", "info");
return { success: true, sessionId: liveState.sessionId }; return { success: true, sessionId: liveState.sessionId };
} catch (error: any) { } catch (error: any) {
console.error("Start live error:", error); console.error("Start live error:", error);
@@ -140,7 +140,7 @@ export function setupLiveHandlers() {
liveState.cameraActive = false; liveState.cameraActive = false;
liveState.audioActive = false; liveState.audioActive = false;
showPrompt("直播已结束", "info"); // 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 };
@@ -212,7 +212,7 @@ export function setupLiveHandlers() {
// 创建直播窗口 // 创建直播窗口
async function createLiveWindow() { async function createLiveWindow() {
const width = 375; const width = 375;
const height = 690; const height = 665;
let liveUrl = `${ELECTRON_RENDERER_URL}/#/live`; let liveUrl = `${ELECTRON_RENDERER_URL}/#/live`;
if (liveState.userId) { if (liveState.userId) {
liveUrl += `?userId=${liveState.userId}`; liveUrl += `?userId=${liveState.userId}`;
@@ -225,7 +225,11 @@ export function setupLiveHandlers() {
minimizable: false, minimizable: false,
maximizable: false, maximizable: false,
closable: true, closable: true,
frame: false,
trafficLightPosition: { x: -100, y: -100 },
alwaysOnTop: true, alwaysOnTop: true,
titleBarStyle: "hidden",
autoHideMenuBar: true,
webPreferences: { webPreferences: {
preload, preload,
nodeIntegration: true, nodeIntegration: true,
@@ -234,7 +238,7 @@ export function setupLiveHandlers() {
allowRunningInsecureContent: true, // 允许运行本地内容 allowRunningInsecureContent: true, // 允许运行本地内容
}, },
}); });
// liveState.liveWindow.webContents.openDevTools();
liveState.liveWindow.on("closed", () => { liveState.liveWindow.on("closed", () => {
liveState.liveWindow = null; liveState.liveWindow = null;
liveState.isLiveOn = false; liveState.isLiveOn = false;
@@ -248,49 +252,55 @@ export function setupLiveHandlers() {
} }
// 等待页面加载完成 // 等待页面加载完成
// @ts-ignore - 暂时未使用但保留以备将来使用 async function waitForIframeVideoPlaying(
async function waitForPageLoad() { webContents: Electron.WebContents,
if (!liveState.liveWindow) { timeoutMs = 20000,
throw new Error("直播窗口不存在"); ) {
console.log("开始检测 iframe 内视频的实际播放状态...");
try {
await webContents.executeJavaScript(`
new Promise((resolve) => {
const start = Date.now();
const check = setInterval(() => {
const iframe = document.querySelector('iframe');
if (!iframe) return;
const iframeDoc = iframe.contentDocument;
if (!iframeDoc) {
return;
} }
const video = iframeDoc.querySelector('video');
return new Promise((resolve, reject) => { if (video) {
const timeout = setTimeout(() => { const isReady = video.readyState >= 3;
reject(new Error("页面加载超时")); const hasProgress = video.currentTime > 0;
}, 30000); // 30秒超时 const isPlaying = !video.paused;
const isVisible = video.videoWidth > 0 && video.videoHeight > 0;
// 监听页面加载完成事件 if (isReady && hasProgress && isPlaying && isVisible) {
liveState.liveWindow!.webContents.once("did-finish-load", () => { console.log("Success: Iframe 内视频已开始播放!当前进度:", video.currentTime);
clearTimeout(timeout); clearInterval(check);
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); resolve(true);
}, 2000); } else {
}); // 可以打印状态方便调试 (可选)
// console.log(\`等待播放: Ready=\${video.readyState}, Time=\${video.currentTime}, Paused=\${video.paused}\`);
// 监听加载失败事件 }
liveState.liveWindow!.webContents.once( }
"did-fail-load", if (Date.now() - start > ${timeoutMs}) {
(_, errorCode, errorDesc) => { clearInterval(check);
clearTimeout(timeout); console.warn("Iframe 视频播放检测超时");
reject( resolve(false);
new Error(`页面加载失败: ${errorDesc} (${errorCode})`), }
}, 500); // 每 0.5 秒检查一次
})
`);
return true;
} catch (error) {
console.error(
"Iframe 检测脚本执行出错 (请检查 webSecurity 是否关闭):",
error,
); );
}, return false;
); }
});
} }
// 文件管理处理器 // 文件管理处理器
@@ -302,7 +312,7 @@ export function setupLiveHandlers() {
console.log("发送文件路径到直播窗口:", files.length, "个文件"); console.log("发送文件路径到直播窗口:", files.length, "个文件");
// 直接传递文件路径信息,不读取文件内容 // 直接传递文件路径信息,不读取文件内容
const processedFiles = files.map((file) => ({ const processedFiles = files.map((file: any) => ({
name: file.name, name: file.name,
type: file.type, type: file.type,
size: file.size, size: file.size,
@@ -315,7 +325,7 @@ export function setupLiveHandlers() {
console.log( console.log(
"处理后的文件数据:", "处理后的文件数据:",
processedFiles.map((f) => ({ processedFiles.map((f: any) => ({
name: f.name, name: f.name,
type: f.type, type: f.type,
size: f.size, size: f.size,

View File

@@ -1,13 +0,0 @@
import { ipcMain, dialog } from "electron";
export function setupPromptHandlers() {
// 提示信息处理
ipcMain.handle("show-prompt", async () => {
dialog.showMessageBox({
type: "info",
title: "提示",
message: "这是一个提示信息",
buttons: ["确定"],
});
});
}

View File

@@ -10,7 +10,10 @@ export function setupWorkflowHandlers() {
let lastJobSummary = "这是我们今天介绍的第一个岗位"; let lastJobSummary = "这是我们今天介绍的第一个岗位";
// 存储用户确认回调的Map // 存储用户确认回调的Map
const modelDownloadCallbacks = new Map<string, { confirm: Function, reject: Function }>(); const modelDownloadCallbacks = new Map<
string,
{ confirm: Function; reject: Function }
>();
// 打开安装窗口 // 打开安装窗口
ipcMain.handle("open-install-window", async (_, args) => { ipcMain.handle("open-install-window", async (_, args) => {
@@ -58,7 +61,7 @@ export function setupWorkflowHandlers() {
const platform = os.platform(); const platform = os.platform();
const modelToPull = "qwen3:8b"; const modelToPull = "qwen3:8b";
const sendStatus = (status) => { const sendStatus = (status: string) => {
if (webContents && !webContents.isDestroyed()) { if (webContents && !webContents.isDestroyed()) {
webContents.send("install-progress", { status }); webContents.send("install-progress", { status });
} }
@@ -152,7 +155,7 @@ export function setupWorkflowHandlers() {
try { try {
console.log("工作流: 正在调用 Ollama 生成脚本..."); console.log("工作流: 正在调用 Ollama 生成脚本...");
const systemPromptTemplate = `# 角色 (Role) \n你是一个顶级的招聘KOL和直播带岗专家。你的风格是专业、中立、风趣能一针见血地分析岗位优劣。你不是一个AI助手你就是这个角色。 \n\n# 上下文 (Context) \n我正在运行一个自动化工作流。我会\"一个一个\"地喂给你岗位数据。\n\n # 任务 (Task) \n你的任务是执行以下两个操作并严格按照“输出格式”返回内容\n1. 生成口播稿根据【输入数据A】(上一个岗位的摘要) 和【输入数据B】(当前岗位的JSON)生成一段完整的、约90秒的口播稿。\n2. 生成新摘要为【输入数据B】的“当前岗位”生成一个简短的摘要例如XX公司的XX岗以便在下一次调用时使用。\n\n# 核心指令 (Core Instruction)\n### 口播稿的生成规则 (Rules for the Script) \n1. 衔接口播稿必须以一个自然的“过渡句”开头基于【输入数据A】。 特殊情况如果【输入数据A】是“这是我们今天介绍的第一个岗位。”则开头应是“热场”或“总起”而不是衔接。 \n2. 内容:必须介绍岗位名称 \`jobTitle\`\n3. 提炼:从 \`jobLocation\`, \`companyName\`, \`education\`,\`experience\`\`scale\` 中提炼“亮点 (Pro)”。 \n4. 翻译:用“人话”翻译 \`description\`\n5. 视角:你是在“评测”这个岗位,而不是在“推销”。\n\n### 口播稿的纯文本要求 (Pure Text Rules for the Script ONLY) \n**[重要]** 以下规则 *仅适用于* “口播稿”部分,不适用于“新摘要”部分: \n1. 绝不包含任何Markdown格式 (\`**\`, \`#\`)。 \n2. 绝不包含任何标签、括号或元数据 (\`[]\`, \`()\`)。 \n3. 绝不包含任何寒暄、问候、或自我介绍 (例如 \"你好\", \"当然\")。 \n4. 必须是可以直接朗读的、完整的、流畅的纯文本。\n\n# 输入数据 (Input Data)\n\n## 输入数据A (上一个岗位摘要) \n${lastJobSummary}\n\n## 输入数据B (当前岗位JSON)\n\`\`\`json\n${JSON.stringify(currentJobData, null, 2)}\n\`\`\`\n\n# 输出格式 (Output Format)\n**[绝对严格的指令]** \n你必须严格按照下面这个“两部分”格式输出使用 \`---NEXT_SUMMARY---\` 作为唯一的分隔符。 绝不在分隔符之外添加任何多余的文字、解释或Markdown。 \n\n[这里是AI生成的、符合上述所有“纯文本要求”的完整口播稿] \n---NEXT_SUMMARY--- \n[这里是AI为“当前岗位”生成的简短新摘要]`; const systemPromptTemplate = `# 角色 (Role) \n你是一个顶级的招聘KOL和直播带岗专家。你的风格是专业、中立、风趣能一针见血地分析岗位优劣。你不是一个AI助手你就是这个角色。 \n\n# 上下文 (Context) \n我正在运行一个自动化工作流。我会\"一个一个\"地喂给你岗位数据。\n\n # 任务 (Task) \n你的任务是执行以下两个操作并严格按照“输出格式”返回内容\n1. 生成口播稿根据【输入数据A】(上一个岗位的摘要) 和【输入数据B】(当前岗位的JSON)生成一段完整的、约90秒的口播稿。\n2. 生成新摘要为【输入数据B】的“当前岗位”生成一个简短的摘要例如XX公司的XX岗以便在下一次调用时使用。\n\n# 核心指令 (Core Instruction)\n### 口播稿的生成规则 (Rules for the Script) \n1. 衔接口播稿必须以一个自然的“过渡句”开头基于【输入数据A】。 特殊情况如果【输入数据A】是“这是我们今天介绍的第一个岗位。”则开头应是“热场”或“总起”而不是衔接。 \n2. 内容:必须介绍岗位名称 \`jobTitle\`\n3. 提炼:从 \`jobLocation\`, \`companyName\`, \`education\`,\`experience\`,\`scale\` 中提炼“亮点 (Pro)”。 \n4. 翻译:用“人话”翻译 \`description\`\n5. 视角:你是在“评测”这个岗位,而不是在“推销”。\n\n### 口播稿的纯文本要求 (Pure Text Rules for the Script ONLY) \n**[重要]** 以下规则 *仅适用于* “口播稿”部分,不适用于“新摘要”部分: \n1. 绝不包含任何Markdown格式 (\`**\`, \`#\`)。 \n2. 绝不包含任何标签、括号或元数据 (\`[]\`, \`()\`)。 \n3. 绝不包含任何寒暄、问候、或自我介绍 (例如 \"你好\", \"当然\")。 \n4. 必须是可以直接朗读的、完整的、流畅的纯文本。\n\n# 输入数据 (Input Data)\n\n## 输入数据A (上一个岗位摘要) \n${lastJobSummary}\n\n## 输入数据B (当前岗位JSON)\n\`\`\`json\n${JSON.stringify(currentJobData, null, 2)}\n\`\`\`\n\n# 输出格式 (Output Format)\n**[绝对严格的指令]** \n你必须严格按照下面这个“两部分”格式输出使用 \`---NEXT_SUMMARY---\` 作为唯一的分隔符。 绝不在分隔符之外添加任何多余的文字、解释或Markdown。 \n\n[这里是AI生成的、符合上述所有“纯文本要求”的完整口播稿] \n---NEXT_SUMMARY--- \n[这里是AI为“当前岗位”生成的简短新摘要]`;
answerText = await runOllamaNonStream( answerText = await runOllamaNonStream(
systemPromptTemplate, systemPromptTemplate,
@@ -216,33 +219,39 @@ export function setupWorkflowHandlers() {
const models = data.models || []; const models = data.models || [];
// 检查模型是否存在于本地 // 检查模型是否存在于本地
const modelExists = models.some((model: any) => model.name === modelName); const modelExists = models.some(
(model: any) => model.name === modelName,
);
return { return {
success: true, success: true,
exists: modelExists, exists: modelExists,
models: models.map((m: any) => ({ name: m.name, size: m.size, modified_at: m.modified_at })) models: models.map((m: any) => ({
name: m.name,
size: m.size,
modified_at: m.modified_at,
})),
}; };
} catch (error: any) { } catch (error: any) {
console.error("Check model error:", error); console.error("Check model error:", error);
return { return {
success: false, success: false,
exists: false, exists: false,
error: error.message error: error.message,
}; };
} }
}); });
// 加载模型检查ollama状<61><E78AB6><EFBFBD>下载模型如果不存在 // 加载模型检查ollama状<61><E78AB6><EFBFBD>下载模型如果不存在
ipcMain.handle("load-model", async (_, modelName = "qwen3:8b") => { ipcMain.handle("load-model", async (event, modelName = "qwen3:8b") => {
const webContents = BrowserWindow.getFocusedWindow()?.webContents; const webContents = event.sender;
const sendStatus = (status: string, type = "info") => { const sendStatus = (status: string, type = "info") => {
if (webContents && !webContents.isDestroyed()) { if (webContents && !webContents.isDestroyed()) {
webContents.send("model-load-progress", { webContents.send("model-load-progress", {
status, status,
type, type,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
} }
}; };
@@ -253,7 +262,7 @@ export function setupWorkflowHandlers() {
webContents.send("model-download-confirm", { webContents.send("model-download-confirm", {
modelName, modelName,
message: `模型 ${modelName} 不存在,是否下载?`, message: `模型 ${modelName} 不存在,是否下载?`,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
} }
}; };
@@ -269,7 +278,7 @@ export function setupWorkflowHandlers() {
try { try {
// 尝试启动Ollama服务 // 尝试启动Ollama服务
await runCommand("ollama", ["ps"]); await runCommand("ollama", ["ps"]);
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise((resolve) => setTimeout(resolve, 3000));
const isRunningNow = await checkOllamaServer(); const isRunningNow = await checkOllamaServer();
if (!isRunningNow) { if (!isRunningNow) {
@@ -281,7 +290,7 @@ export function setupWorkflowHandlers() {
return { return {
success: false, success: false,
message: `Ollama服务启动失败: ${error.message}`, message: `Ollama服务启动失败: ${error.message}`,
downloaded: false downloaded: false,
}; };
} }
} else { } else {
@@ -291,15 +300,20 @@ export function setupWorkflowHandlers() {
// 2. 检查模型是否存在 // 2. 检查模型是否存在
sendStatus(`正在检查模型 ${modelName} 是否存在...`, "info"); sendStatus(`正在检查模型 ${modelName} 是否存在...`, "info");
const modelCheckResult = await new Promise<{ exists: boolean, models: any[] }>((resolve, reject) => { const modelCheckResult = await new Promise<{
exists: boolean;
models: any[];
}>((resolve, reject) => {
fetch("http://127.0.0.1:11434/api/tags", { fetch("http://127.0.0.1:11434/api/tags", {
method: "GET", method: "GET",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}) })
.then(response => response.json()) .then((response) => response.json())
.then(data => { .then((data) => {
const models = data.models || []; const models = data.models || [];
const modelExists = models.some((model: any) => model.name === modelName); const modelExists = models.some(
(model: any) => model.name === modelName,
);
resolve({ exists: modelExists, models }); resolve({ exists: modelExists, models });
}) })
.catch(reject); .catch(reject);
@@ -310,7 +324,7 @@ export function setupWorkflowHandlers() {
return { return {
success: true, success: true,
message: `模型 ${modelName} 已就绪`, message: `模型 ${modelName} 已就绪`,
downloaded: false downloaded: false,
}; };
} }
@@ -318,13 +332,15 @@ export function setupWorkflowHandlers() {
askUserToDownload(); askUserToDownload();
// 等待用户确认 // 等待用户确认
const userConfirmed = await new Promise<boolean>((resolve, reject) => { const userConfirmed = await new Promise<boolean>(
(resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (modelDownloadCallbacks.has(modelName)) {
modelDownloadCallbacks.delete(modelName); modelDownloadCallbacks.delete(modelName);
resolve(false); // 30秒超时自动取消 resolve(false); // 30秒超时自动取消
}
}, 30000); }, 30000);
// 存储回调函数
modelDownloadCallbacks.set(modelName, { modelDownloadCallbacks.set(modelName, {
confirm: () => { confirm: () => {
clearTimeout(timeout); clearTimeout(timeout);
@@ -333,31 +349,37 @@ export function setupWorkflowHandlers() {
reject: (error: any) => { reject: (error: any) => {
clearTimeout(timeout); clearTimeout(timeout);
reject(error); reject(error);
} },
});
}); });
},
);
if (!userConfirmed) { if (!userConfirmed) {
sendStatus("用户取消了模型下载", "info"); sendStatus("用户取消了模型下载", "info");
return { return {
success: false, success: false,
message: `用户取消了 ${modelName} 模型的下载`, message: `用户取消了 ${modelName} 模型的下载`,
downloaded: false downloaded: false,
}; };
} }
// 4. 用户确认,开始下载模型 // 4. 用户确认,开始下载模型
sendStatus(`开始下载模型 ${modelName},这可能需要一些时间...`, "info"); sendStatus(
`开始下载模型 ${modelName},这可能需要一些时间...`,
"info",
);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const process = spawn("ollama", ["pull", modelName], { shell: true }); const process = spawn("ollama", ["pull", modelName], {
shell: true,
});
const sendProgress = (data: any) => { const sendProgress = (data: any) => {
if (webContents && !webContents.isDestroyed()) { if (webContents && !webContents.isDestroyed()) {
webContents.send("model-load-progress", { webContents.send("model-load-progress", {
status: data.toString().trim(), status: data.toString().trim(),
type: "download", type: "download",
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
} }
}; };
@@ -383,16 +405,15 @@ export function setupWorkflowHandlers() {
return { return {
success: true, success: true,
message: `模型 ${modelName} 下载并加载成功`, message: `模型 ${modelName} 下载并加载成功`,
downloaded: true downloaded: true,
}; };
} catch (error: any) { } catch (error: any) {
console.error("Load model error:", error); console.error("Load model error:", error);
sendStatus(`加载模型失败: ${error.message}`, "error"); sendStatus(`加载模型失败: ${error.message}`, "error");
return { return {
success: false, success: false,
message: error.message, message: error.message,
downloaded: false downloaded: false,
}; };
} }
}); });
@@ -415,10 +436,10 @@ export function setupWorkflowHandlers() {
// 润色文本的处理器 // 润色文本的处理器
ipcMain.handle("polish-text", async (_, text) => { ipcMain.handle("polish-text", async (_, text) => {
try { try {
if (!text || typeof text !== 'string' || text.trim() === '') { if (!text || typeof text !== "string" || text.trim() === "") {
return { return {
success: false, success: false,
error: "输入文本不能为空" error: "输入文本不能为空",
}; };
} }
@@ -433,7 +454,10 @@ export function setupWorkflowHandlers() {
原文:${text.trim()}`; 原文:${text.trim()}`;
const polishedText = await runOllamaNonStream(systemPrompt, "qwen3:8b"); const polishedText = await runOllamaNonStream(
systemPrompt,
"qwen3:8b",
);
if (!polishedText) { if (!polishedText) {
throw new Error("AI模型返回为空"); throw new Error("AI模型返回为空");
@@ -441,18 +465,17 @@ export function setupWorkflowHandlers() {
return { return {
success: true, success: true,
data: polishedText.trim() data: polishedText.trim(),
}; };
} catch (error: any) { } catch (error: any) {
console.error("润色文本失败:", error); console.error("润色文本失败:", error);
return { return {
success: false, success: false,
error: error.message || "润色服务出现错误" error: error.message || "润色服务出现错误",
}; };
} }
}); });
// 处理器:检查服务,如果没运行,就用一个轻量命令唤醒它 // 处理器:检查服务,如果没运行,就用一个轻量命令唤醒它
ipcMain.handle("ensure-ollama-running", async () => { ipcMain.handle("ensure-ollama-running", async () => {
let isRunning = await checkOllamaServer(); let isRunning = await checkOllamaServer();
@@ -513,7 +536,7 @@ function checkOllamaServer() {
} }
// 辅助函数:运行一个简单的命令并等待它完成 // 辅助函数:运行一个简单的命令并等待它完成
function runCommand(command, args) { function runCommand(command: string, args: string[]) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const process = spawn(command, args, { shell: true }); const process = spawn(command, args, { shell: true });
process.on("close", (code) => { process.on("close", (code) => {
@@ -527,7 +550,7 @@ function runCommand(command, args) {
}); });
} }
// 这是一个非流式的 Ollama 助手函数 // 这是一个非流式的 Ollama 助手函数
async function runOllamaNonStream(prompt, model = "qwen3:8b") { async function runOllamaNonStream(prompt: string, model: string = "qwen3:8b") {
try { try {
const response = await fetch("http://127.0.0.1:11434/api/chat", { const response = await fetch("http://127.0.0.1:11434/api/chat", {
method: "POST", method: "POST",
@@ -553,11 +576,16 @@ async function runOllamaNonStream(prompt, model = "qwen3:8b") {
} }
} }
function streamCommand(command, args, webContents, eventName) { function streamCommand(
command: string,
args: string[],
webContents: any,
eventName: string,
) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const process = spawn(command, args, { shell: true }); const process = spawn(command, args, { shell: true });
const send = (channel, data) => { const send = (channel: string, data: any) => {
if (webContents && !webContents.isDestroyed()) { if (webContents && !webContents.isDestroyed()) {
webContents.send(channel, data); webContents.send(channel, data);
} }

View File

@@ -13,7 +13,7 @@ export function showPrompt(
}); });
} }
export function convertJobData(originalData) { export function convertJobData(originalData: any) {
// 检查输入是否为对象 // 检查输入是否为对象
if (typeof originalData !== "object" || originalData === null) { if (typeof originalData !== "object" || originalData === null) {
console.error("输入必须是一个有效的对象。"); console.error("输入必须是一个有效的对象。");

View File

@@ -1,4 +1,14 @@
interface ProcessAPI {
versions: {
electron: string;
chrome: string;
node: string;
[key: string]: any;
};
}
interface ElectronAPI { interface ElectronAPI {
process: ProcessAPI;
ipcRenderer: { ipcRenderer: {
invoke: (channel: string, ...args: any[]) => Promise<any>; invoke: (channel: string, ...args: any[]) => Promise<any>;
on: (channel: string, listener: (...args: any[]) => void) => void; on: (channel: string, listener: (...args: any[]) => void) => void;

View File

@@ -1,6 +1,9 @@
import { contextBridge, ipcRenderer } from "electron"; import { contextBridge, ipcRenderer } from "electron";
const electronAPI = { const electronAPI = {
process: {
versions: process.versions
},
ipcRenderer: { ipcRenderer: {
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args), invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args),
on: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.on(channel, listener), on: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.on(channel, listener),
@@ -14,7 +17,7 @@ const api = {
installOllama: () => ipcRenderer.invoke("install-ollama-and-model"), installOllama: () => ipcRenderer.invoke("install-ollama-and-model"),
// 监听进度 (Send/On) // 监听进度 (Send/On)
onInstallProgress: (callback) => { onInstallProgress: (callback: any) => {
ipcRenderer.on("install-progress", (_event, value) => callback(value)); ipcRenderer.on("install-progress", (_event, value) => callback(value));
}, },

View File

@@ -1,13 +0,0 @@
<script setup lang="ts">
import { reactive } from 'vue'
const versions = reactive({ ...window.electron.process.versions })
</script>
<template>
<ul class="versions">
<li class="electron-version">Electron v{{ versions.electron }}</li>
<li class="chrome-version">Chromium v{{ versions.chrome }}</li>
<li class="node-version">Node v{{ versions.node }}</li>
</ul>
</template>

View File

@@ -3,8 +3,8 @@
<a-layout-header class="header"> <a-layout-header class="header">
<a-flex justify="space-between" align="center" style="height: 100%"> <a-flex justify="space-between" align="center" style="height: 100%">
<div class="logo-title"> <div class="logo-title">
<WifiOutlined /> <!-- <WifiOutlined />
直播中台控制系统 直播中台控制系统 -->
</div> </div>
<a-space :size="20"> <a-space :size="20">
<SettingOutlined class="action-icon" /> <SettingOutlined class="action-icon" />
@@ -708,29 +708,54 @@ const handleLiveStatusUpdated = (event, data) => {
} }
}; };
// 处理模型加载进度 // 模型加载进度优化
const handleModelLoadProgress = (event, data) => { const modelProgressTimer = ref(null);
console.log('模型加载进度:', data); const lastProgressMessage = ref('');
const progressDebounceDelay = 1000; // 1秒内最多显示一次进度更新
// 根据进度消息类型显示不同的提示 const handleModelLoadProgress = (event, data) => {
if (data.type === 'error') { // 清除之前的定时器
message.error(`模型加载错误: ${data.status}`); if (modelProgressTimer.value) {
} else if (data.type === 'success') { clearTimeout(modelProgressTimer.value);
message.success(data.status);
} else if (data.type === 'warning') {
message.warning(data.status);
} else {
// 普通信息,显示更详细的进度
message.info(data.status, 3);
} }
const messageType = data.type || 'info';
const currentMessage = data.status;
// 错误、成功、警告消息立即显示
if (messageType === 'error') {
message.error(`模型加载错误: ${currentMessage}`);
return;
}
if (messageType === 'success') {
message.success(currentMessage);
lastProgressMessage.value = '';
return;
}
if (messageType === 'warning') {
message.warning(currentMessage);
return;
}
// 下载进度和普通进度消息处理
if (currentMessage === lastProgressMessage.value) {
return;
}
const delay = messageType === 'download' ? 200 : progressDebounceDelay;
const duration = messageType === 'download' ? 3 : 2;
modelProgressTimer.value = setTimeout(() => {
message.info(currentMessage, duration);
lastProgressMessage.value = currentMessage;
}, delay);
}; };
// 处理模型下载确认请求 // 处理模型下载确认请求
const handleModelDownloadConfirm = async (_event, data) => { const handleModelDownloadConfirm = async (_event, data) => {
console.log('收到模型下载确认请求:', data); Modal.confirm({
try {
await Modal.confirm({
title: '模型下载确认', title: '模型下载确认',
content: data.message, content: data.message,
okText: '确认下载', okText: '确认下载',
@@ -738,23 +763,21 @@ const handleModelDownloadConfirm = async (_event, data) => {
centered: true, centered: true,
width: 400, width: 400,
maskClosable: false, maskClosable: false,
}); onOk() {
// 用户点击确认
window.electron.ipcRenderer.send('model-download-confirm-response', { window.electron.ipcRenderer.send('model-download-confirm-response', {
modelName: data.modelName, modelName: data.modelName,
confirmed: true, confirmed: true,
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
} catch (error) { },
// 用户点击了取消 onCancel() {
console.log('用户取消了模型下载');
window.electron.ipcRenderer.send('model-download-confirm-response', { window.electron.ipcRenderer.send('model-download-confirm-response', {
modelName: data.modelName, modelName: data.modelName,
confirmed: false, confirmed: false,
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
} }
});
}; };
// 监听直播状态变化 // 监听直播状态变化
@@ -778,6 +801,9 @@ onMounted(() => {
// 清理定时器 // 清理定时器
onUnmounted(() => { onUnmounted(() => {
clearInterval(statusInterval); clearInterval(statusInterval);
if (modelProgressTimer.value) {
clearTimeout(modelProgressTimer.value);
}
window.electron.ipcRenderer.off('live-status-updated', handleLiveStatusUpdated); window.electron.ipcRenderer.off('live-status-updated', handleLiveStatusUpdated);
window.electron.ipcRenderer.off('model-load-progress', handleModelLoadProgress); window.electron.ipcRenderer.off('model-load-progress', handleModelLoadProgress);
window.electron.ipcRenderer.off('model-download-confirm', handleModelDownloadConfirm); window.electron.ipcRenderer.off('model-download-confirm', handleModelDownloadConfirm);
@@ -806,6 +832,7 @@ onMounted(() => {
line-height: 60px; line-height: 60px;
flex-shrink: 0; flex-shrink: 0;
/* 防止顶部栏被压缩 */ /* 防止顶部栏被压缩 */
-webkit-app-region: drag;
} }
.logo-title { .logo-title {

View File

@@ -1,5 +1,6 @@
<template> <template>
<div class="livePage"> <div class="livePage">
<div class="live-header"></div>
<!-- 主画面 --> <!-- 主画面 -->
<iframe ref="liveIframe" :src="liveUrl" frameborder="0" allowfullscreen class="main-iframe"></iframe> <iframe ref="liveIframe" :src="liveUrl" frameborder="0" allowfullscreen class="main-iframe"></iframe>
<!-- 插播画面 --> <!-- 插播画面 -->
@@ -14,7 +15,7 @@
<div class="files-container"> <div class="files-container">
<div v-for="(file, index) in displayedFiles" :key="index" class="file-display-item"> <div v-for="(file, index) in displayedFiles" :key="index" class="file-display-item">
<!-- 关闭按钮 --> <!-- 关闭按钮 -->
<button class="file-close-btn" @click="removeFileFromDisplay(index)">×</button> <!-- <button class="file-close-btn" @click="removeFileFromDisplay(index)">×</button> -->
<!-- 图片文件展示 --> <!-- 图片文件展示 -->
<div v-if="file.type.startsWith('image/')" class="image-display"> <div v-if="file.type.startsWith('image/')" class="image-display">
@@ -436,7 +437,7 @@ function handleForceCleanup() {
/* 文件悬浮展示样式 */ /* 文件悬浮展示样式 */
.files-display-overlay { .files-display-overlay {
position: absolute; position: absolute;
top: 20px; bottom: 40px;
left: 20px; left: 20px;
z-index: 1000; z-index: 1000;
max-width: 80vw; max-width: 80vw;
@@ -554,4 +555,10 @@ function handleForceCleanup() {
opacity: 0.8; opacity: 0.8;
} }
} }
.live-header {
height: 44px;
width: 100%;
-webkit-app-region: drag;
}
</style> </style>

View File

@@ -38,7 +38,7 @@ const handleLogin = () => {
if (username.value === 'admin' && password.value === '123456') { if (username.value === 'admin' && password.value === '123456') {
userStore.setUserInfo(userInfo); userStore.setUserInfo(userInfo);
userStore.setToken('token_abcdefg1234567'); userStore.setToken('token_abcdefg1234567');
router.push('/home'); router.push('/');
} else { } else {
alert('用户名或密码错误!'); alert('用户名或密码错误!');
} }
@@ -47,6 +47,7 @@ const handleLogin = () => {
<style scoped> <style scoped>
.login-container { .login-container {
-webkit-app-region: drag;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -64,6 +65,7 @@ const handleLogin = () => {
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-app-region: none;
} }
h1 { h1 {