flat: 暂存
This commit is contained in:
9
.codebuddy/agents/直播带岗平台.md
Normal file
9
.codebuddy/agents/直播带岗平台.md
Normal 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
|
||||||
|
---
|
||||||
|
请不要过多废话,直接更改代码
|
||||||
10
.codebuddy/rules/开发框架.mdc
Normal file
10
.codebuddy/rules/开发框架.mdc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
alwaysApply: true
|
||||||
|
enabled: true
|
||||||
|
updatedAt: 2025-12-20T10:05:42.556Z
|
||||||
|
provider:
|
||||||
|
---
|
||||||
|
|
||||||
|
1、本项目是 electron-builder + vue3 开发的 pc 端应用
|
||||||
|
2、禁止编写说明文档,直接给出代码
|
||||||
BIN
build/icon.icns
BIN
build/icon.icns
Binary file not shown.
BIN
build/icon.ico
BIN
build/icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 17 KiB |
BIN
build/icon.png
BIN
build/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 703 KiB |
9
package-lock.json
generated
9
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 内视频的实际播放状态...");
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
const timeout = setTimeout(() => {
|
await webContents.executeJavaScript(`
|
||||||
reject(new Error("页面加载超时"));
|
new Promise((resolve) => {
|
||||||
}, 30000); // 30秒超时
|
const start = Date.now();
|
||||||
|
|
||||||
// 监听页面加载完成事件
|
const check = setInterval(() => {
|
||||||
liveState.liveWindow!.webContents.once("did-finish-load", () => {
|
const iframe = document.querySelector('iframe');
|
||||||
clearTimeout(timeout);
|
if (!iframe) return;
|
||||||
console.log("页面加载完成");
|
const iframeDoc = iframe.contentDocument;
|
||||||
|
if (!iframeDoc) {
|
||||||
// 额外等待一下确保iframe也加载完成
|
return;
|
||||||
setTimeout(() => {
|
}
|
||||||
// 通知所有渲染进程状态已更新
|
const video = iframeDoc.querySelector('video');
|
||||||
const allWindows = BrowserWindow.getAllWindows();
|
if (video) {
|
||||||
allWindows.forEach((window: BrowserWindow) => {
|
const isReady = video.readyState >= 3;
|
||||||
if (window.webContents && !window.isDestroyed()) {
|
const hasProgress = video.currentTime > 0;
|
||||||
window.webContents.send("live-status-updated", {
|
const isPlaying = !video.paused;
|
||||||
hasLiveWindow: true,
|
const isVisible = video.videoWidth > 0 && video.videoHeight > 0;
|
||||||
windowLoaded: true,
|
if (isReady && hasProgress && isPlaying && isVisible) {
|
||||||
});
|
console.log("Success: Iframe 内视频已开始播放!当前进度:", video.currentTime);
|
||||||
|
clearInterval(check);
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
// 可以打印状态方便调试 (可选)
|
||||||
|
// console.log(\`等待播放: Ready=\${video.readyState}, Time=\${video.currentTime}, Paused=\${video.paused}\`);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
resolve(true);
|
if (Date.now() - start > ${timeoutMs}) {
|
||||||
}, 2000);
|
clearInterval(check);
|
||||||
});
|
console.warn("Iframe 视频播放检测超时");
|
||||||
|
resolve(false);
|
||||||
// 监听加载失败事件
|
}
|
||||||
liveState.liveWindow!.webContents.once(
|
}, 500); // 每 0.5 秒检查一次
|
||||||
"did-fail-load",
|
})
|
||||||
(_, errorCode, errorDesc) => {
|
`);
|
||||||
clearTimeout(timeout);
|
return true;
|
||||||
reject(
|
} catch (error) {
|
||||||
new Error(`页面加载失败: ${errorDesc} (${errorCode})`),
|
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,
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { ipcMain, dialog } from "electron";
|
|
||||||
|
|
||||||
export function setupPromptHandlers() {
|
|
||||||
// 提示信息处理
|
|
||||||
ipcMain.handle("show-prompt", async () => {
|
|
||||||
dialog.showMessageBox({
|
|
||||||
type: "info",
|
|
||||||
title: "提示",
|
|
||||||
message: "这是一个提示信息",
|
|
||||||
buttons: ["确定"],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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,18 +300,23 @@ 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(
|
||||||
resolve({ exists: modelExists, models });
|
(model: any) => model.name === modelName,
|
||||||
})
|
);
|
||||||
.catch(reject);
|
resolve({ exists: modelExists, models });
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (modelCheckResult.exists) {
|
if (modelCheckResult.exists) {
|
||||||
@@ -310,7 +324,7 @@ export function setupWorkflowHandlers() {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `模型 ${modelName} 已就绪`,
|
message: `模型 ${modelName} 已就绪`,
|
||||||
downloaded: false
|
downloaded: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,46 +332,54 @@ export function setupWorkflowHandlers() {
|
|||||||
askUserToDownload();
|
askUserToDownload();
|
||||||
|
|
||||||
// 等待用户确认
|
// 等待用户确认
|
||||||
const userConfirmed = await new Promise<boolean>((resolve, reject) => {
|
const userConfirmed = await new Promise<boolean>(
|
||||||
const timeout = setTimeout(() => {
|
(resolve, reject) => {
|
||||||
modelDownloadCallbacks.delete(modelName);
|
const timeout = setTimeout(() => {
|
||||||
resolve(false); // 30秒超时自动取消
|
if (modelDownloadCallbacks.has(modelName)) {
|
||||||
}, 30000);
|
modelDownloadCallbacks.delete(modelName);
|
||||||
|
resolve(false); // 30秒超时自动取消
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
// 存储回调函数
|
modelDownloadCallbacks.set(modelName, {
|
||||||
modelDownloadCallbacks.set(modelName, {
|
confirm: () => {
|
||||||
confirm: () => {
|
clearTimeout(timeout);
|
||||||
clearTimeout(timeout);
|
resolve(true);
|
||||||
resolve(true);
|
},
|
||||||
},
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("输入必须是一个有效的对象。");
|
||||||
|
|||||||
10
src/preload/index.d.ts
vendored
10
src/preload/index.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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" />
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
<span class="file-name">{{ file.name }}</span>
|
<span class="file-name">{{ file.name }}</span>
|
||||||
<a-tag :color="getFileTagColor(file.type)" size="small">{{
|
<a-tag :color="getFileTagColor(file.type)" size="small">{{
|
||||||
getFileTypeText(file.type)
|
getFileTypeText(file.type)
|
||||||
}}</a-tag>
|
}}</a-tag>
|
||||||
</div>
|
</div>
|
||||||
<a-button size="small" danger @click="removeFile(index)">
|
<a-button size="small" danger @click="removeFile(index)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -708,53 +708,76 @@ 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({
|
||||||
|
title: '模型下载确认',
|
||||||
try {
|
content: data.message,
|
||||||
await Modal.confirm({
|
okText: '确认下载',
|
||||||
title: '模型下载确认',
|
cancelText: '取消',
|
||||||
content: data.message,
|
centered: true,
|
||||||
okText: '确认下载',
|
width: 400,
|
||||||
cancelText: '取消',
|
maskClosable: false,
|
||||||
centered: true,
|
onOk() {
|
||||||
width: 400,
|
window.electron.ipcRenderer.send('model-download-confirm-response', {
|
||||||
maskClosable: false,
|
modelName: data.modelName,
|
||||||
});
|
confirmed: true,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
// 用户点击确认
|
});
|
||||||
window.electron.ipcRenderer.send('model-download-confirm-response', {
|
},
|
||||||
modelName: data.modelName,
|
onCancel() {
|
||||||
confirmed: true,
|
window.electron.ipcRenderer.send('model-download-confirm-response', {
|
||||||
timestamp: new Date().toISOString()
|
modelName: data.modelName,
|
||||||
});
|
confirmed: false,
|
||||||
} catch (error) {
|
timestamp: new Date().toISOString()
|
||||||
// 用户点击了取消
|
});
|
||||||
console.log('用户取消了模型下载');
|
}
|
||||||
window.electron.ipcRenderer.send('model-download-confirm-response', {
|
});
|
||||||
modelName: data.modelName,
|
|
||||||
confirmed: false,
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user