diff --git a/package.json b/package.json index 5aeabfe..f123894 100644 --- a/package.json +++ b/package.json @@ -1,55 +1,55 @@ { - "name": "electron-app", - "version": "1.0.0", - "description": "An Electron application with Vue and TypeScript", - "main": "./out/main/index.js", - "author": "example.com", - "homepage": "https://electron-vite.org", - "scripts": { - "format": "prettier --write .", - "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", - "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", - "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", - "typecheck": "npm run typecheck:node && npm run typecheck:web", - "start": "electron-vite preview", - "dev": "electron-vite dev", - "build": "npm run typecheck && electron-vite build", - "postinstall": "electron-builder install-app-deps", - "build:unpack": "npm run build && electron-builder --dir", - "build:win": "npm run build && electron-builder --win", - "build:mac": "npm run build && electron-builder --mac", - "build:linux": "npm run build && electron-builder --linux" - }, - "dependencies": { - "@electron-toolkit/preload": "^3.0.0", - "@electron-toolkit/utils": "^3.0.0", - "pinia": "^3.0.3", - "pinia-plugin-persistedstate": "^4.5.0", - "sass": "^1.93.2", - "vue-draggable-next": "^2.3.0", - "vue-router": "^4.6.3", - "vuedraggable": "^4.1.0" - }, - "devDependencies": { - "@electron-toolkit/eslint-config": "^1.0.2", - "@electron-toolkit/eslint-config-ts": "^1.0.1", - "@electron-toolkit/tsconfig": "^1.0.1", - "@rushstack/eslint-patch": "^1.7.1", - "@types/electron": "^1.4.38", - "@types/node": "^18.19.9", - "@vitejs/plugin-vue": "^5.0.3", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0", - "electron": "^28.2.0", - "electron-builder": "^24.9.1", - "electron-reloader": "^1.2.3", - "electron-vite": "^2.0.0", - "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.20.1", - "prettier": "^3.2.4", - "typescript": "^5.3.3", - "vite": "^5.0.12", - "vue": "^3.4.15", - "vue-tsc": "^1.8.27" - } + "name": "electron-app", + "version": "1.0.0", + "description": "An Electron application with Vue and TypeScript", + "main": "./out/main/index.js", + "author": "example.com", + "homepage": "https://electron-vite.org", + "scripts": { + "format": "prettier --write .", + "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", + "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", + "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", + "typecheck": "npm run typecheck:node && npm run typecheck:web", + "start": "electron-vite preview", + "dev": "electron-vite dev", + "build": "npm run typecheck && electron-vite build", + "postinstall": "electron-builder install-app-deps", + "build:unpack": "npm run build && electron-builder --dir", + "build:win": "npm run build && electron-builder --win", + "build:mac": "npm run build && electron-builder --mac", + "build:linux": "npm run build && electron-builder --linux" + }, + "dependencies": { + "@electron-toolkit/preload": "^3.0.0", + "@electron-toolkit/utils": "^3.0.0", + "pinia": "^3.0.3", + "pinia-plugin-persistedstate": "^4.5.0", + "sass": "^1.93.2", + "vue-draggable-next": "^2.3.0", + "vue-router": "^4.6.3", + "vuedraggable": "^4.1.0" + }, + "devDependencies": { + "@electron-toolkit/eslint-config": "^1.0.2", + "@electron-toolkit/eslint-config-ts": "^1.0.1", + "@electron-toolkit/tsconfig": "^1.0.1", + "@rushstack/eslint-patch": "^1.7.1", + "@types/electron": "^1.4.38", + "@types/node": "^18.19.9", + "@vitejs/plugin-vue": "^5.0.3", + "@vue/eslint-config-prettier": "^9.0.0", + "@vue/eslint-config-typescript": "^12.0.0", + "electron": "^28.2.0", + "electron-builder": "^24.9.1", + "electron-reloader": "^1.2.3", + "electron-vite": "^2.0.0", + "eslint": "^8.56.0", + "eslint-plugin-vue": "^9.20.1", + "prettier": "^3.2.4", + "typescript": "^5.3.3", + "vite": "^5.0.12", + "vue": "^3.4.15", + "vue-tsc": "^1.8.27" + } } diff --git a/src/main/index.ts b/src/main/index.ts index a51ece7..421cba9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,6 +3,7 @@ import { electronApp, optimizer, is } from "@electron-toolkit/utils"; import icon from "../../resources/icon.png?asset"; import { setupLiveHandlers } from "./ipc/live"; import { setupPromptHandlers } from "./ipc/prompt"; +import { setupWorkflowHandlers } from "./ipc/workflow"; import { preload, indexHtml, ELECTRON_RENDERER_URL } from "./config"; // 在开发环境下启用热重载 @@ -75,6 +76,8 @@ app.whenReady().then(() => { setupPromptHandlers(); + setupWorkflowHandlers(); + createWindow(); app.on("activate", function () { diff --git a/src/main/ipc/live.ts b/src/main/ipc/live.ts index 40234db..8ce2fbb 100644 --- a/src/main/ipc/live.ts +++ b/src/main/ipc/live.ts @@ -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) { diff --git a/src/main/ipc/prompt.ts b/src/main/ipc/prompt.ts index dd19769..f4eaa7b 100644 --- a/src/main/ipc/prompt.ts +++ b/src/main/ipc/prompt.ts @@ -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: "提示", diff --git a/src/main/ipc/workflow.ts b/src/main/ipc/workflow.ts new file mode 100644 index 0000000..92fed04 --- /dev/null +++ b/src/main/ipc/workflow.ts @@ -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); + }); + }); +} diff --git a/src/main/utils/ollama-client.ts b/src/main/utils/ollama-client.ts deleted file mode 100644 index dc95278..0000000 --- a/src/main/utils/ollama-client.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { type ClientRequest, type IncomingMessage } from "http"; -import * as http from "http"; - -interface OllamaOptions { - baseUrl?: string; - timeout?: number; -} - -interface GenerateParams { - model: string; - prompt: string; - system?: string; - template?: string; - context?: number[]; - stream?: boolean; - format?: "json"; - options?: { - temperature?: number; - top_p?: number; - top_k?: number; - seed?: number; - num_predict?: number; - stop?: string[]; - num_ctx?: number; - }; -} - -interface GenerateResponse { - model: string; - created_at: string; - response: string; - context: number[]; - done: boolean; - total_duration: number; - load_duration: number; - prompt_eval_duration: number; - eval_duration: number; - prompt_eval_count: number; - eval_count: number; -} - -interface ModelInfo { - name: string; - size: number; - digest: string; - details: { - format: string; - family: string; - families: string[]; - parameter_size: string; - quantization_level: string; - }; -} - -interface ListModelsResponse { - models: ModelInfo[]; -} - -export class OllamaClient { - private readonly baseUrl: string; - private readonly timeout: number; - - constructor(options: OllamaOptions = {}) { - this.baseUrl = options.baseUrl || "http://localhost:11434"; - this.timeout = options.timeout || 30000; - } - - private async request( - path: string, - method = "GET", - body?: unknown, - ): Promise { - return new Promise((resolve, reject) => { - const url = new URL(path, this.baseUrl); - const options = { - method, - hostname: url.hostname, - port: url.port || "11434", - path: url.pathname, - headers: { - "Content-Type": "application/json", - }, - timeout: this.timeout, - }; - - const req: ClientRequest = http.request( - options, - (res: IncomingMessage) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if ( - res.statusCode && - res.statusCode >= 200 && - res.statusCode < 300 - ) { - try { - resolve(JSON.parse(data)); - } catch (e) { - reject( - new Error("Failed to parse response data"), - ); - } - } else { - reject( - new Error( - `HTTP Error: ${res.statusCode} ${res.statusMessage}`, - ), - ); - } - }); - }, - ); - - req.on("error", reject); - - req.on("timeout", () => { - req.destroy(); - reject(new Error("Request timeout")); - }); - - if (body) { - req.write(JSON.stringify(body)); - } - req.end(); - }); - } - - async generate(params: GenerateParams): Promise { - const result = await this.request( - "/api/generate", - "POST", - { - ...params, - stream: false, - }, - ); - - // 如果响应是JSON格式,尝试解析 - if (params.format === "json" && result.response) { - try { - const parsed = JSON.parse(result.response); - result.response = - parsed.content || parsed.text || result.response; - } catch (e) { - console.warn( - "Failed to parse JSON response, returning raw response", - ); - } - } - - return result; - } - - /** - * 生成文本响应,直接返回文本内容 - */ - async generateText( - params: Omit, - ): Promise { - const result = await this.generate({ - ...params, - format: undefined, // 确保不使用 JSON 格式 - }); - return result.response; - } - - async streamGenerate( - params: GenerateParams, - onResponse: (response: GenerateResponse) => void, - onError?: (error: Error) => void, - ): Promise { - const url = new URL("/api/generate", this.baseUrl); - const options = { - method: "POST", - hostname: url.hostname, - port: url.port || "11434", - path: url.pathname, - headers: { - "Content-Type": "application/json", - }, - }; - - return new Promise((resolve, reject) => { - const req = http.request(options, (res) => { - let buffer = ""; - - res.on("data", (chunk) => { - buffer += chunk; - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (line.trim()) { - try { - const response = JSON.parse( - line, - ) as GenerateResponse; - onResponse(response); - } catch (e) { - const error = new Error( - "Failed to parse streaming response", - ); - onError?.(error); - console.error(error, e); - } - } - } - }); - - res.on("end", () => { - if (buffer.trim()) { - try { - const response = JSON.parse( - buffer, - ) as GenerateResponse; - onResponse(response); - } catch (e) { - const error = new Error( - "Failed to parse final streaming response", - ); - onError?.(error); - console.error(error, e); - } - } - resolve(); - }); - }); - - req.on("error", (error) => { - onError?.(error); - reject(error); - }); - - req.write(JSON.stringify({ ...params, stream: true })); - req.end(); - }); - } - - async listModels(): Promise { - return this.request("/api/tags"); - } - - async pullModel(modelName: string): Promise { - await this.request("/api/pull", "POST", { name: modelName }); - } - - async deleteModel(modelName: string): Promise { - await this.request("/api/delete", "DELETE", { name: modelName }); - } - - async copyModel(sourceModel: string, targetModel: string): Promise { - await this.request("/api/copy", "POST", { - source: sourceModel, - destination: targetModel, - }); - } - - async ping(): Promise { - try { - await this.request("/"); - return true; - } catch { - return false; - } - } -} diff --git a/src/preload/index.ts b/src/preload/index.ts index 2d18524..f19c843 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,22 +1,30 @@ -import { contextBridge } from 'electron' -import { electronAPI } from '@electron-toolkit/preload' +import { contextBridge, ipcRenderer } from "electron"; +import { electronAPI } from "@electron-toolkit/preload"; -// Custom APIs for renderer -const api = {} +const api = { + installOllama: () => ipcRenderer.invoke("install-ollama-and-model"), + + // 监听进度 (Send/On) + onInstallProgress: (callback) => { + ipcRenderer.on("install-progress", (_event, value) => callback(value)); + }, + + // 移除监听器 + removeInstallProgressListeners: () => { + ipcRenderer.removeAllListeners("install-progress"); + }, +}; -// Use `contextBridge` APIs to expose Electron APIs to -// renderer only if context isolation is enabled, otherwise -// just add to the DOM global. if (process.contextIsolated) { - try { - contextBridge.exposeInMainWorld('electron', electronAPI) - contextBridge.exposeInMainWorld('api', api) - } catch (error) { - console.error(error) - } + try { + contextBridge.exposeInMainWorld("electron", electronAPI); + contextBridge.exposeInMainWorld("api", api); + } catch (error) { + console.error(error); + } } else { - // @ts-ignore (define in dts) - window.electron = electronAPI - // @ts-ignore (define in dts) - window.api = api + // @ts-ignore (define in dts) + window.electron = electronAPI; + // @ts-ignore (define in dts) + window.api = api; } diff --git a/src/renderer/src/router/routes.ts b/src/renderer/src/router/routes.ts index ae44ed3..175d929 100644 --- a/src/renderer/src/router/routes.ts +++ b/src/renderer/src/router/routes.ts @@ -19,7 +19,12 @@ const routes: Array = [ component: () => import("../views/Live/index.vue"), meta: { no_login: true }, }, - // Add more routes as needed + { + path: "/install", + name: "Install", + component: () => import("../views/Install/index.vue"), + meta: { no_login: true }, + }, ]; export default routes; diff --git a/src/renderer/src/stores/useLiveStore.ts b/src/renderer/src/stores/useLiveStore.ts index bdb58fa..33df9df 100644 --- a/src/renderer/src/stores/useLiveStore.ts +++ b/src/renderer/src/stores/useLiveStore.ts @@ -74,7 +74,7 @@ export const useLiveStore = defineStore("live", () => { try { isExplaining.value = true; - // TODO: 这里添加讲解岗位的具体实现 + // 这里添加讲解岗位的具体实现 // 可以调用 AI 接口进行讲解 const result = await window.electron.ipcRenderer.invoke( "explain-position", @@ -117,7 +117,7 @@ export const useLiveStore = defineStore("live", () => { try { const result = await window.electron.ipcRenderer.invoke( "open-live-window", - { path: "live", width: 375, height: 692, userId: "rs876543" }, + { path: "live", width: 375, height: 682 }, ); if (result.success) { isLiveWindowOpen.value = true; @@ -181,13 +181,11 @@ export const useLiveStore = defineStore("live", () => { positions, (newVal) => { console.log("positions changed:", newVal); - // 当添加了新岗位,并且当前没有正在讲解的岗位时,自动开始讲解第一个岗位 if ( newVal.length > 0 && !currentPosition.value && !isExplaining.value ) { - console.log("Starting next position from positions watch"); getNextPosition(); } }, @@ -215,7 +213,11 @@ export const useLiveStore = defineStore("live", () => { }); async function jobTransformAIobj(job: any) { - const val = await window.electron.ipcRenderer.invoke("ollama-test", job); + // const val = await window.electron.ipcRenderer.invoke("ollama-test", job); + const val = await window.electron.ipcRenderer.invoke( + "run-job-workflow", + job, + ); if (val.success === false) { throw new Error(val.error); diff --git a/src/renderer/src/views/Home/index.vue b/src/renderer/src/views/Home/index.vue index 1f19cb2..b1dd979 100644 --- a/src/renderer/src/views/Home/index.vue +++ b/src/renderer/src/views/Home/index.vue @@ -36,8 +36,9 @@
-
直播控制
+
直播控制:
+
-
内容控制
+ + +
内容控制:
+ +
工具控制:
+
+ 模型状态:{{ modelStatus }} +
+
+ + + +
@@ -66,6 +79,7 @@ import { useUserStore } from '@renderer/stores/useUserStore'; import { useLiveStore } from '@renderer/stores/useLiveStore' const { positions, currentPosition, seePosition, isLiveOn } = storeToRefs(useLiveStore()) const broadcastOrder = ref([]); +const modelStatus = ref('未知') onMounted(async () => { init(); @@ -73,6 +87,10 @@ onMounted(async () => { }) async function init() { + // useLiveStore().fetchPositions() +} + +function loadData() { useLiveStore().fetchPositions() } @@ -114,6 +132,45 @@ const handleStopLive = async () => { console.error('结束直播失败:', error) } } + +const InstallPlugins = async () => { + try { + await window.electron.ipcRenderer.invoke( + "open-install-window", + { path: "install", width: 375, height: 692, userId: "rs876543" }, + ); + } catch (error) { + console.error('安装插件失败:', error) + } +} + +const updateStatus = async () => { + try { + const status = await window.electron.ipcRenderer.invoke("check-ollama-status"); + if (status) { + modelStatus.value = '已安装' + } else { + modelStatus.value = '未安装' + } + } catch (error) { + console.error('更新状态失败:', error) + } +} + + +const startModel = async () => { + try { + const result = await window.electron.ipcRenderer.invoke("ensure-ollama-running"); + console.log('result', result) + if (result.success) { + alert(result.message) + } else { + alert(result.message) + } + } catch (error) { + console.error('更新状态失败:', error) + } +} function toggleCameraInsert() { window.electron.ipcRenderer.send('toggle-camera-insert') } diff --git a/src/renderer/src/views/Install/index.vue b/src/renderer/src/views/Install/index.vue new file mode 100644 index 0000000..17fb831 --- /dev/null +++ b/src/renderer/src/views/Install/index.vue @@ -0,0 +1,72 @@ + + + + + \ No newline at end of file diff --git a/src/renderer/src/views/Live/index.vue b/src/renderer/src/views/Live/index.vue index f90e3f5..6a10bac 100644 --- a/src/renderer/src/views/Live/index.vue +++ b/src/renderer/src/views/Live/index.vue @@ -23,11 +23,11 @@ const liveUrl = ref(""); const soundUrl = ref("https://dmdemo.hx.cn/sound/welcome.mp3"); const welcome = ref() const live = ref() -let cameraStream = null // 用于存储摄像头媒体流 +let cameraStream = null let wasWelcomePlaying = false const cameraVideo = ref() -const liveIframe = ref(null) // 对应模板中的 iframe -const micAudio = ref(null) // 隐藏的麦克风播放元素 +const liveIframe = ref(null) +const micAudio = ref(null) let micStream = null let prevIframeVideoVolume = 1 let micPlaying = false @@ -45,7 +45,6 @@ function welcomeEnd() { } } } catch (e) { - // 可能跨域,忽略 } } @@ -73,7 +72,6 @@ onBeforeUnmount(() => { }) function startLive() { - // 从 URL 获取 userId const paramsUserId = route.query.userId; if (paramsUserId) { userId.value = paramsUserId @@ -83,10 +81,8 @@ function startLive() { async function handleInsertCameraVideo() { if (isCameraActive.value) { - // 停止摄像头 stopCamera() } else { - // 启动摄像头 await startCamera() } } diff --git a/tsconfig.web.json b/tsconfig.web.json index e9d73a9..aa52488 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -2,7 +2,7 @@ "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", "include": [ "src/renderer/src/env.d.ts", - "src/renderer/src/**/*", + "src/renderer/src/**/*.ts", "src/renderer/src/**/*.vue", "src/preload/*.d.ts" ], @@ -13,6 +13,7 @@ "@renderer/*": [ "src/renderer/src/*" ] - } + }, + "allowJs": true } }