flat: 暂存

This commit is contained in:
Apcallover
2025-11-11 21:11:09 +08:00
parent 2f0dd5ee86
commit 59e04e53b1
13 changed files with 562 additions and 384 deletions

View File

@@ -1,14 +1,8 @@
import { ipcMain, BrowserWindow } from "electron";
import { preload, indexHtml, ELECTRON_RENDERER_URL } from "../config";
import { showPrompt } from "../utils/tools";
import { OllamaClient } from "../utils/ollama-client";
let liveWindow: BrowserWindow | null = null;
const client = new OllamaClient({
baseUrl: "http://127.0.0.1:11434", // 可选,默认值
timeout: 30000, // 可选默认30秒
});
// 直播相关的主进程处理
export function setupLiveHandlers() {
let LiveSessionId = null;
@@ -104,7 +98,7 @@ export function setupLiveHandlers() {
contextIsolation: false,
},
});
// liveWindow.webContents.openDevTools();
liveWindow.webContents.openDevTools();
liveWindow.on("closed", () => {
liveWindow = null;
});
@@ -128,24 +122,6 @@ export function setupLiveHandlers() {
return { success: false, error: error.message };
}
});
ipcMain.handle("ollama-test", async (_, jobInfo) => {
try {
const result = await client.generateText({
model: "qwen:7b",
prompt: `请根据提供的 json 数据:${jobInfo},直接生成一段用于吸引求职者投递的岗位介绍文案。文案需:
1、简洁、有力突出岗位核心价值和吸引力。
2、不包含任何多余的开头、结尾、解释或废话。
3、目标是立即抓住用户眼球并促使他们投递简历。
4、不含任何废话或与岗位无关的内容
**要求:**只输出生成的岗位介绍文案本身。`,
});
return { success: true, data: result };
} catch (error: any) {
console.error("Ollama error:", error);
return { success: false, error: error.message };
}
});
}
async function getSessionId(requestBody: object) {

View File

@@ -2,7 +2,7 @@ import { ipcMain, dialog } from "electron";
export function setupPromptHandlers() {
// 提示信息处理
ipcMain.handle("show-prompt", async (event) => {
ipcMain.handle("show-prompt", async () => {
dialog.showMessageBox({
type: "info",
title: "提示",

329
src/main/ipc/workflow.ts Normal file
View File

@@ -0,0 +1,329 @@
import { ipcMain, BrowserWindow } from "electron";
import { spawn } from "child_process";
import { showPrompt } from "../utils/tools";
import { preload, indexHtml, ELECTRON_RENDERER_URL } from "../config";
import os from "os";
import http from "http";
let InstallWindows: BrowserWindow | null = null;
export function setupWorkflowHandlers() {
let lastJobSummary = "这是我们今天介绍的第一个岗位";
// 打开安装窗口
ipcMain.handle("open-install-window", async (_, args) => {
try {
if (InstallWindows) {
InstallWindows.focus();
showPrompt("下载已打开", "info");
return { success: true };
}
const { width, height, path } = args;
let installUrl = `${ELECTRON_RENDERER_URL}/#/${path}`;
console.log(installUrl);
InstallWindows = new BrowserWindow({
title: "模型下载",
width,
height,
minimizable: false, // 是否可以最小化
maximizable: false, // 是否可以最小化
closable: true, // 窗口是否可关闭
alwaysOnTop: false, // 窗口是否永远在别的窗口的上面
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false,
},
});
// InstallWindows.webContents.openDevTools();
InstallWindows.on("closed", () => {
InstallWindows = null;
});
if (ELECTRON_RENDERER_URL) {
InstallWindows.loadURL(installUrl);
} else {
InstallWindows.loadFile(indexHtml, { hash: `/${path}` });
}
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
});
// 监听来自渲染器进程的 'install-ollama' 事件
ipcMain.handle("install-ollama-and-model", async (event) => {
const webContents = event.sender; // 获取发送事件的窗口
const platform = os.platform();
const modelToPull = "qwen3:8b";
const sendStatus = (status) => {
if (webContents && !webContents.isDestroyed()) {
webContents.send("install-progress", { status });
}
};
try {
sendStatus("Checking Ollama installation...");
try {
await streamCommand(
"ollama",
["-v"],
webContents,
"install-progress",
);
sendStatus("Ollama is already installed.");
} catch (error) {
// Ollama 未安装,执行安装
sendStatus("Ollama not found. Starting installation...");
if (platform === "darwin" || platform === "linux") {
// macOS / Linux - 使用官方的 curl 脚本
const installCommand =
"curl -fsSL https://ollama.com/install.sh | sh";
await streamCommand(
"sh",
["-c", installCommand],
webContents,
"install-progress",
);
} else if (platform === "win32") {
// Windows - 使用 PowerShell 下载并静默安装
const psScript = `
$ProgressPreference = 'SilentlyContinue';
$tempPath = [System.IO.Path]::Combine($env:TEMP, 'OllamaSetup.exe');
Write-Host 'Downloading OllamaSetup.exe...';
Invoke-WebRequest -Uri 'https://ollama.com/download/OllamaSetup.exe' -OutFile $tempPath;
Write-Host 'Download complete. Starting silent installer...';
Start-Process -FilePath $tempPath -ArgumentList '/S' -Wait;
Write-Host 'Installation complete. Cleaning up...';
Remove-Item $tempPath;
Write-Host 'Done.';
`;
await streamCommand(
"powershell",
[
"-ExecutionPolicy",
"Bypass",
"-NoProfile",
"-Command",
psScript,
],
webContents,
"install-progress",
);
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
sendStatus("Ollama installation complete.");
}
// --- 步骤 2: 拉取模型 ---
sendStatus(
`Pulling model: ${modelToPull}... (This may take a while)`,
);
await streamCommand(
"ollama",
["pull", modelToPull],
webContents,
"install-progress",
);
sendStatus(`Model ${modelToPull} pull complete.`);
return {
success: true,
message: "Installation and model pull successful.",
};
} catch (error: any) {
console.error(error);
sendStatus(`Error: ${error.message}`);
return { success: false, message: error.message };
}
});
// 将整个工作流封装在 IPC Handler 中
ipcMain.handle("run-job-workflow", async (_, userQuery) => {
let currentJobData = userQuery || {};
let answerText = "";
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为“当前岗位”生成的简短新摘要]`;
answerText = await runOllamaNonStream(
systemPromptTemplate,
"qwen3:8b",
);
if (!answerText) {
throw new Error("Ollama 返回为空");
}
} catch (e) {
return "抱歉AI 模型在生成脚本时出错。";
}
try {
console.log("工作流: 正在解析 AI 输出...");
let script = "抱歉AI没有按预定格式返回脚本请稍后重试。";
let summary = "这是我们今天介绍的第一个岗位。"; // 这是一个安全的“重置”摘要
if (answerText && typeof answerText === "string") {
const parts = answerText.split("---NEXT_SUMMARY---");
if (parts[0] && parts[0].trim() !== "") {
script = parts[0].trim();
}
if (parts[1] && parts[1].trim() !== "") {
summary = parts[1].trim();
}
}
console.log("工作流: 正在更新状态...");
lastJobSummary = summary; // 关键:更新主进程中的状态
console.log("工作流: 完成,返回口播稿。");
return { success: true, data: script }; // 将最终的“口播稿”返回给渲染进程
} catch (e: any) {
console.error("代码运行或变量更新节点出错:", e);
return "抱歉,处理 AI 响应时出错。";
}
});
// 检查 Ollama 服务器是否正在运行
ipcMain.handle("check-ollama-status", async () => {
return await checkOllamaServer();
});
// 处理器:检查服务,如果没运行,就用一个轻量命令唤醒它
ipcMain.handle("ensure-ollama-running", async () => {
let isRunning = await checkOllamaServer();
if (isRunning) {
return {
success: true,
message: "Ollama服务器已在运行.",
};
}
// 服务未运行。
// 我们运行 'ollama ps'。这个命令会与服务通信,
// 如果服务没启动Ollama CLI 会自动启动它。
try {
await runCommand("ollama", ["ps"]);
// 给服务一点启动时间 (例如 2 秒)
await new Promise((resolve) => setTimeout(resolve, 2000));
// 再次检查
isRunning = await checkOllamaServer();
if (isRunning) {
return {
success: true,
message: "Ollama 已在后台启动",
};
} else {
return {
success: false,
message: "服务启动失败",
};
}
} catch (error: any) {
console.error("错误:", error);
return {
success: false,
message: `错误: ${error.message}`,
};
}
});
}
// 辅助函数:检查 Ollama API 是否可访问
function checkOllamaServer() {
return new Promise((resolve) => {
// 默认端口是 11434
const req = http.get("http://127.0.0.1:11434/", (res) => {
// "Ollama is running" 的响应码是 200
resolve(res.statusCode === 200);
});
// 如果连接被拒绝 (ECONNREFUSED),则服务未运行
req.on("error", () => {
resolve(false);
});
});
}
// 辅助函数:运行一个简单的命令并等待它完成
function runCommand(command, args) {
return new Promise((resolve, reject) => {
const process = spawn(command, args, { shell: true });
process.on("close", (code) => {
if (code === 0) {
resolve(null);
} else {
reject(new Error(`Command failed with code ${code}`));
}
});
process.on("error", (err) => reject(err));
});
}
// 这是一个非流式的 Ollama 助手函数
async function runOllamaNonStream(prompt, model = "qwen3:8b") {
try {
const response = await fetch("http://127.0.0.1:11434/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: model,
messages: [{ role: "user", content: prompt }],
stream: false, // 关键:关闭流式
}),
});
if (!response.ok) {
throw new Error(`Ollama API error: ${response.statusText}`);
}
const data = await response.json();
// data.message.content 包含了完整的 AI 回复
return data.message.content;
} catch (error) {
console.error("Ollama Chat Error:", error);
return null; // 返回 null 以便后续逻辑处理
}
}
function streamCommand(command, args, webContents, eventName) {
return new Promise((resolve, reject) => {
const process = spawn(command, args, { shell: true });
const send = (channel, data) => {
if (webContents && !webContents.isDestroyed()) {
webContents.send(channel, data);
}
};
process.stdout.on("data", (data) => {
send(eventName, { type: "stdout", data: data.toString() });
});
process.stderr.on("data", (data) => {
send(eventName, { type: "stderr", data: data.toString() });
});
process.on("close", (code) => {
if (code === 0) {
resolve(null);
} else {
reject(new Error(`Process exited with code ${code}`));
}
});
process.on("error", (err) => {
reject(err);
});
});
}