flat: 暂存
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
import { app, shell, BrowserWindow } from "electron";
|
||||
import { setupLiveHandlers } from "./ipc/live";
|
||||
import { setupPromptHandlers } from "./ipc/prompt";
|
||||
import { setupWorkflowHandlers } from "./ipc/workflow";
|
||||
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,
|
||||
height: 670,
|
||||
show: false,
|
||||
titleBarStyle: "hidden",
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload,
|
||||
@@ -59,8 +67,6 @@ app.whenReady().then(() => {
|
||||
// 直播相关处理
|
||||
setupLiveHandlers();
|
||||
|
||||
setupPromptHandlers();
|
||||
|
||||
setupWorkflowHandlers();
|
||||
|
||||
createWindow();
|
||||
@@ -84,7 +90,7 @@ app.on("window-all-closed", () => {
|
||||
app.on("before-quit", () => {
|
||||
// 清理所有资源
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach(window => {
|
||||
windows.forEach((window) => {
|
||||
window.removeAllListeners();
|
||||
window.close();
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ export function setupLiveHandlers() {
|
||||
}
|
||||
|
||||
// 3. 等待网页加载完成
|
||||
// await waitForPageLoad();
|
||||
await waitForIframeVideoPlaying(liveState.liveWindow!.webContents);
|
||||
|
||||
// 4. 获取 sessionId
|
||||
console.log("获取 sessionId");
|
||||
@@ -115,7 +115,7 @@ export function setupLiveHandlers() {
|
||||
interrupt: true,
|
||||
});
|
||||
|
||||
showPrompt("直播已开始", "info");
|
||||
// showPrompt("直播已开始", "info");
|
||||
return { success: true, sessionId: liveState.sessionId };
|
||||
} catch (error: any) {
|
||||
console.error("Start live error:", error);
|
||||
@@ -140,7 +140,7 @@ export function setupLiveHandlers() {
|
||||
liveState.cameraActive = false;
|
||||
liveState.audioActive = false;
|
||||
|
||||
showPrompt("直播已结束", "info");
|
||||
// showPrompt("直播已结束", "info");
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
@@ -212,7 +212,7 @@ export function setupLiveHandlers() {
|
||||
// 创建直播窗口
|
||||
async function createLiveWindow() {
|
||||
const width = 375;
|
||||
const height = 690;
|
||||
const height = 665;
|
||||
let liveUrl = `${ELECTRON_RENDERER_URL}/#/live`;
|
||||
if (liveState.userId) {
|
||||
liveUrl += `?userId=${liveState.userId}`;
|
||||
@@ -225,7 +225,11 @@ export function setupLiveHandlers() {
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
closable: true,
|
||||
frame: false,
|
||||
trafficLightPosition: { x: -100, y: -100 },
|
||||
alwaysOnTop: true,
|
||||
titleBarStyle: "hidden",
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload,
|
||||
nodeIntegration: true,
|
||||
@@ -234,7 +238,7 @@ export function setupLiveHandlers() {
|
||||
allowRunningInsecureContent: true, // 允许运行本地内容
|
||||
},
|
||||
});
|
||||
|
||||
// liveState.liveWindow.webContents.openDevTools();
|
||||
liveState.liveWindow.on("closed", () => {
|
||||
liveState.liveWindow = null;
|
||||
liveState.isLiveOn = false;
|
||||
@@ -248,49 +252,55 @@ export function setupLiveHandlers() {
|
||||
}
|
||||
|
||||
// 等待页面加载完成
|
||||
// @ts-ignore - 暂时未使用但保留以备将来使用
|
||||
async function waitForPageLoad() {
|
||||
if (!liveState.liveWindow) {
|
||||
throw new Error("直播窗口不存在");
|
||||
}
|
||||
async function waitForIframeVideoPlaying(
|
||||
webContents: Electron.WebContents,
|
||||
timeoutMs = 20000,
|
||||
) {
|
||||
console.log("开始检测 iframe 内视频的实际播放状态...");
|
||||
|
||||
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,
|
||||
});
|
||||
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');
|
||||
if (video) {
|
||||
const isReady = video.readyState >= 3;
|
||||
const hasProgress = video.currentTime > 0;
|
||||
const isPlaying = !video.paused;
|
||||
const isVisible = video.videoWidth > 0 && video.videoHeight > 0;
|
||||
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);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// 监听加载失败事件
|
||||
liveState.liveWindow!.webContents.once(
|
||||
"did-fail-load",
|
||||
(_, errorCode, errorDesc) => {
|
||||
clearTimeout(timeout);
|
||||
reject(
|
||||
new Error(`页面加载失败: ${errorDesc} (${errorCode})`),
|
||||
);
|
||||
},
|
||||
}
|
||||
if (Date.now() - start > ${timeoutMs}) {
|
||||
clearInterval(check);
|
||||
console.warn("Iframe 视频播放检测超时");
|
||||
resolve(false);
|
||||
}
|
||||
}, 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, "个文件");
|
||||
|
||||
// 直接传递文件路径信息,不读取文件内容
|
||||
const processedFiles = files.map((file) => ({
|
||||
const processedFiles = files.map((file: any) => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
@@ -315,7 +325,7 @@ export function setupLiveHandlers() {
|
||||
|
||||
console.log(
|
||||
"处理后的文件数据:",
|
||||
processedFiles.map((f) => ({
|
||||
processedFiles.map((f: any) => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
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 = "这是我们今天介绍的第一个岗位";
|
||||
|
||||
// 存储用户确认回调的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) => {
|
||||
@@ -58,7 +61,7 @@ export function setupWorkflowHandlers() {
|
||||
const platform = os.platform();
|
||||
const modelToPull = "qwen3:8b";
|
||||
|
||||
const sendStatus = (status) => {
|
||||
const sendStatus = (status: string) => {
|
||||
if (webContents && !webContents.isDestroyed()) {
|
||||
webContents.send("install-progress", { status });
|
||||
}
|
||||
@@ -152,7 +155,7 @@ export function setupWorkflowHandlers() {
|
||||
try {
|
||||
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(
|
||||
systemPromptTemplate,
|
||||
@@ -216,33 +219,39 @@ export function setupWorkflowHandlers() {
|
||||
const models = data.models || [];
|
||||
|
||||
// 检查模型是否存在于本地
|
||||
const modelExists = models.some((model: any) => model.name === modelName);
|
||||
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 }))
|
||||
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
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 加载模型(检查ollama状<61><E78AB6><EFBFBD>,下载模型如果不存在)
|
||||
ipcMain.handle("load-model", async (_, modelName = "qwen3:8b") => {
|
||||
const webContents = BrowserWindow.getFocusedWindow()?.webContents;
|
||||
ipcMain.handle("load-model", async (event, modelName = "qwen3:8b") => {
|
||||
const webContents = event.sender;
|
||||
|
||||
const sendStatus = (status: string, type = "info") => {
|
||||
if (webContents && !webContents.isDestroyed()) {
|
||||
webContents.send("model-load-progress", {
|
||||
status,
|
||||
type,
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -253,7 +262,7 @@ export function setupWorkflowHandlers() {
|
||||
webContents.send("model-download-confirm", {
|
||||
modelName,
|
||||
message: `模型 ${modelName} 不存在,是否下载?`,
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -269,7 +278,7 @@ export function setupWorkflowHandlers() {
|
||||
try {
|
||||
// 尝试启动Ollama服务
|
||||
await runCommand("ollama", ["ps"]);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
const isRunningNow = await checkOllamaServer();
|
||||
if (!isRunningNow) {
|
||||
@@ -281,7 +290,7 @@ export function setupWorkflowHandlers() {
|
||||
return {
|
||||
success: false,
|
||||
message: `Ollama服务启动失败: ${error.message}`,
|
||||
downloaded: false
|
||||
downloaded: false,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@@ -291,18 +300,23 @@ export function setupWorkflowHandlers() {
|
||||
// 2. 检查模型是否存在
|
||||
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", {
|
||||
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);
|
||||
.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) {
|
||||
@@ -310,7 +324,7 @@ export function setupWorkflowHandlers() {
|
||||
return {
|
||||
success: true,
|
||||
message: `模型 ${modelName} 已就绪`,
|
||||
downloaded: false
|
||||
downloaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -318,46 +332,54 @@ export function setupWorkflowHandlers() {
|
||||
askUserToDownload();
|
||||
|
||||
// 等待用户确认
|
||||
const userConfirmed = await new Promise<boolean>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
modelDownloadCallbacks.delete(modelName);
|
||||
resolve(false); // 30秒超时自动取消
|
||||
}, 30000);
|
||||
const userConfirmed = await new Promise<boolean>(
|
||||
(resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (modelDownloadCallbacks.has(modelName)) {
|
||||
modelDownloadCallbacks.delete(modelName);
|
||||
resolve(false); // 30秒超时自动取消
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// 存储回调函数
|
||||
modelDownloadCallbacks.set(modelName, {
|
||||
confirm: () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(true);
|
||||
},
|
||||
reject: (error: any) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
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
|
||||
downloaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 用户确认,开始下载模型
|
||||
sendStatus(`开始下载模型 ${modelName},这可能需要一些时间...`, "info");
|
||||
sendStatus(
|
||||
`开始下载模型 ${modelName},这可能需要一些时间...`,
|
||||
"info",
|
||||
);
|
||||
|
||||
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) => {
|
||||
if (webContents && !webContents.isDestroyed()) {
|
||||
webContents.send("model-load-progress", {
|
||||
status: data.toString().trim(),
|
||||
type: "download",
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -383,16 +405,15 @@ export function setupWorkflowHandlers() {
|
||||
return {
|
||||
success: true,
|
||||
message: `模型 ${modelName} 下载并加载成功`,
|
||||
downloaded: true
|
||||
downloaded: true,
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Load model error:", error);
|
||||
sendStatus(`加载模型失败: ${error.message}`, "error");
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
downloaded: false
|
||||
downloaded: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -415,10 +436,10 @@ export function setupWorkflowHandlers() {
|
||||
// 润色文本的处理器
|
||||
ipcMain.handle("polish-text", async (_, text) => {
|
||||
try {
|
||||
if (!text || typeof text !== 'string' || text.trim() === '') {
|
||||
if (!text || typeof text !== "string" || text.trim() === "") {
|
||||
return {
|
||||
success: false,
|
||||
error: "输入文本不能为空"
|
||||
error: "输入文本不能为空",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -433,7 +454,10 @@ export function setupWorkflowHandlers() {
|
||||
|
||||
原文:${text.trim()}`;
|
||||
|
||||
const polishedText = await runOllamaNonStream(systemPrompt, "qwen3:8b");
|
||||
const polishedText = await runOllamaNonStream(
|
||||
systemPrompt,
|
||||
"qwen3:8b",
|
||||
);
|
||||
|
||||
if (!polishedText) {
|
||||
throw new Error("AI模型返回为空");
|
||||
@@ -441,18 +465,17 @@ export function setupWorkflowHandlers() {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: polishedText.trim()
|
||||
data: polishedText.trim(),
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("润色文本失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || "润色服务出现错误"
|
||||
error: error.message || "润色服务出现错误",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 处理器:检查服务,如果没运行,就用一个轻量命令唤醒它
|
||||
ipcMain.handle("ensure-ollama-running", async () => {
|
||||
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) => {
|
||||
const process = spawn(command, args, { shell: true });
|
||||
process.on("close", (code) => {
|
||||
@@ -527,7 +550,7 @@ function runCommand(command, args) {
|
||||
});
|
||||
}
|
||||
// 这是一个非流式的 Ollama 助手函数
|
||||
async function runOllamaNonStream(prompt, model = "qwen3:8b") {
|
||||
async function runOllamaNonStream(prompt: string, model: string = "qwen3:8b") {
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:11434/api/chat", {
|
||||
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) => {
|
||||
const process = spawn(command, args, { shell: true });
|
||||
|
||||
const send = (channel, data) => {
|
||||
const send = (channel: string, data: any) => {
|
||||
if (webContents && !webContents.isDestroyed()) {
|
||||
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) {
|
||||
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 {
|
||||
process: ProcessAPI;
|
||||
ipcRenderer: {
|
||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||
on: (channel: string, listener: (...args: any[]) => void) => void;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
|
||||
const electronAPI = {
|
||||
process: {
|
||||
versions: process.versions
|
||||
},
|
||||
ipcRenderer: {
|
||||
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args),
|
||||
on: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.on(channel, listener),
|
||||
@@ -14,7 +17,7 @@ const api = {
|
||||
installOllama: () => ipcRenderer.invoke("install-ollama-and-model"),
|
||||
|
||||
// 监听进度 (Send/On)
|
||||
onInstallProgress: (callback) => {
|
||||
onInstallProgress: (callback: any) => {
|
||||
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-flex justify="space-between" align="center" style="height: 100%">
|
||||
<div class="logo-title">
|
||||
<WifiOutlined />
|
||||
直播中台控制系统
|
||||
<!-- <WifiOutlined />
|
||||
直播中台控制系统 -->
|
||||
</div>
|
||||
<a-space :size="20">
|
||||
<SettingOutlined class="action-icon" />
|
||||
@@ -172,7 +172,7 @@
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<a-tag :color="getFileTagColor(file.type)" size="small">{{
|
||||
getFileTypeText(file.type)
|
||||
}}</a-tag>
|
||||
}}</a-tag>
|
||||
</div>
|
||||
<a-button size="small" danger @click="removeFile(index)">
|
||||
<template #icon>
|
||||
@@ -708,53 +708,76 @@ const handleLiveStatusUpdated = (event, data) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理模型加载进度
|
||||
const handleModelLoadProgress = (event, data) => {
|
||||
console.log('模型加载进度:', data);
|
||||
// 模型加载进度优化
|
||||
const modelProgressTimer = ref(null);
|
||||
const lastProgressMessage = ref('');
|
||||
const progressDebounceDelay = 1000; // 1秒内最多显示一次进度更新
|
||||
|
||||
// 根据进度消息类型显示不同的提示
|
||||
if (data.type === 'error') {
|
||||
message.error(`模型加载错误: ${data.status}`);
|
||||
} else if (data.type === 'success') {
|
||||
message.success(data.status);
|
||||
} else if (data.type === 'warning') {
|
||||
message.warning(data.status);
|
||||
} else {
|
||||
// 普通信息,显示更详细的进度
|
||||
message.info(data.status, 3);
|
||||
const handleModelLoadProgress = (event, data) => {
|
||||
// 清除之前的定时器
|
||||
if (modelProgressTimer.value) {
|
||||
clearTimeout(modelProgressTimer.value);
|
||||
}
|
||||
|
||||
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) => {
|
||||
console.log('收到模型下载确认请求:', data);
|
||||
|
||||
try {
|
||||
await Modal.confirm({
|
||||
title: '模型下载确认',
|
||||
content: data.message,
|
||||
okText: '确认下载',
|
||||
cancelText: '取消',
|
||||
centered: true,
|
||||
width: 400,
|
||||
maskClosable: false,
|
||||
});
|
||||
|
||||
// 用户点击确认
|
||||
window.electron.ipcRenderer.send('model-download-confirm-response', {
|
||||
modelName: data.modelName,
|
||||
confirmed: true,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
// 用户点击了取消
|
||||
console.log('用户取消了模型下载');
|
||||
window.electron.ipcRenderer.send('model-download-confirm-response', {
|
||||
modelName: data.modelName,
|
||||
confirmed: false,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '模型下载确认',
|
||||
content: data.message,
|
||||
okText: '确认下载',
|
||||
cancelText: '取消',
|
||||
centered: true,
|
||||
width: 400,
|
||||
maskClosable: false,
|
||||
onOk() {
|
||||
window.electron.ipcRenderer.send('model-download-confirm-response', {
|
||||
modelName: data.modelName,
|
||||
confirmed: true,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
window.electron.ipcRenderer.send('model-download-confirm-response', {
|
||||
modelName: data.modelName,
|
||||
confirmed: false,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 监听直播状态变化
|
||||
@@ -778,6 +801,9 @@ onMounted(() => {
|
||||
// 清理定时器
|
||||
onUnmounted(() => {
|
||||
clearInterval(statusInterval);
|
||||
if (modelProgressTimer.value) {
|
||||
clearTimeout(modelProgressTimer.value);
|
||||
}
|
||||
window.electron.ipcRenderer.off('live-status-updated', handleLiveStatusUpdated);
|
||||
window.electron.ipcRenderer.off('model-load-progress', handleModelLoadProgress);
|
||||
window.electron.ipcRenderer.off('model-download-confirm', handleModelDownloadConfirm);
|
||||
@@ -806,6 +832,7 @@ onMounted(() => {
|
||||
line-height: 60px;
|
||||
flex-shrink: 0;
|
||||
/* 防止顶部栏被压缩 */
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="livePage">
|
||||
<div class="live-header"></div>
|
||||
<!-- 主画面 -->
|
||||
<iframe ref="liveIframe" :src="liveUrl" frameborder="0" allowfullscreen class="main-iframe"></iframe>
|
||||
<!-- 插播画面 -->
|
||||
@@ -14,7 +15,7 @@
|
||||
<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>
|
||||
<!-- <button class="file-close-btn" @click="removeFileFromDisplay(index)">×</button> -->
|
||||
|
||||
<!-- 图片文件展示 -->
|
||||
<div v-if="file.type.startsWith('image/')" class="image-display">
|
||||
@@ -436,7 +437,7 @@ function handleForceCleanup() {
|
||||
/* 文件悬浮展示样式 */
|
||||
.files-display-overlay {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 40px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
max-width: 80vw;
|
||||
@@ -554,4 +555,10 @@ function handleForceCleanup() {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.live-header {
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
</style>
|
||||
@@ -38,7 +38,7 @@ const handleLogin = () => {
|
||||
if (username.value === 'admin' && password.value === '123456') {
|
||||
userStore.setUserInfo(userInfo);
|
||||
userStore.setToken('token_abcdefg1234567');
|
||||
router.push('/home');
|
||||
router.push('/');
|
||||
} else {
|
||||
alert('用户名或密码错误!');
|
||||
}
|
||||
@@ -47,6 +47,7 @@ const handleLogin = () => {
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
-webkit-app-region: drag;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -64,6 +65,7 @@ const handleLogin = () => {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-app-region: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
Reference in New Issue
Block a user