使用electron开发一个可调用本地ollama大模型的exe应用程序
这个独立的exe支持流式打字输出,多智能体、插件调用、通话、设置、语音唤醒、离线存储扥功能,适合ollama及openai的聊天对话应用搭建。
首先是安装nodejs环境,官网:https://nodejs.org/zh-cn
2、创建项目目录和初始化
mkdir my-electron-app cd my-electron-app npm init -y
2、安装electron
npm install electron --save-dev
如果网速慢比较卡,设置国内npm镜像源
npm config set registry https://registry.npmmirror.co或者使用cnpm
npm install -g cnpm --registry=https://registry.npmmirror.com cnpm install electron --save-dev3、编写相关文件代码
renderer.js代码在文章尾部。
main.js
const { app, BrowserWindow, Menu, globalShortcut } = require('electron'); const path = require('path'); let mainWindow; let isMenuVisible = false; function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: true, contextIsolation: false, }, autoHideMenuBar: true // 隐藏菜单栏 }); mainWindow.loadFile('index.html'); mainWindow.on('closed', function () { mainWindow = null; }); } app.whenReady().then(() => { createWindow(); // 注册全局快捷键 Ctrl+M 显示/隐藏菜单栏 globalShortcut.register('CommandOrControl+M', () => { isMenuVisible = !isMenuVisible; mainWindow.setMenuBarVisibility(isMenuVisible); mainWindow.setAutoHideMenuBar(!isMenuVisible); }); app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('will-quit', () => { // 注销全局快捷键 globalShortcut.unregisterAll(); }); app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit(); });
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="https://repo.bfw.wiki/bfwrepo/js/eruda.js"></script><script>eruda.init();</script> <link type="text/css" rel="stylesheet" href="https://repo.bfw.wiki/bfwrepo/css/font-awesome-4.7.0/css/font-awesome.css"> <script type="text/javascript" src="https://repo.bfw.wiki/bfwrepo/js/chinese_lunar_calendar.min.js"></script> <script type="text/javascript" src="https://repo.bfw.wiki/bfwrepo/js/highlight.js"></script> <link type="text/css" rel="stylesheet" href="https://repo.bfw.wiki/bfwrepo/css/highlight.9.9.css"> <script type="text/javascript" src="https://repo.bfw.wiki/bfwrepo/js/marked.umd.min.js"></script> <script type="text/javascript" src="https://repo.bfw.wiki/bfwrepo/js/localforage.min.js"></script> <script type="text/javascript" src="https://repo.bfw.wiki/bfwrepo/js/vue@2.6.1-dev.js"></script> <link type="text/css" rel="stylesheet" href="https://repo.bfw.wiki/bfwrepo/css/aichat-2024712.css"> </head> <body> <div id="overlay" onclick="hideImage()"> <i id="closeBtn" onclick="hideImage()" class="fa fa-lg fa-times-circle"></i> <img id="overlayImage" src="" alt="Fullscreen Image"> </div> <div id="app" class="bodycont"> <div class="tooldia" v-show="showtooldiaforagent" style="z-index: 113;"> <i @click="showtooldiaforagent=false" class="fa fa-lg fa-times-circle" style="margin: 0; float: right;"></i> <div style="font-weight: bold;">选择你想要的工具</div> <p v-for="item in alltoollist" :key="item.toolname" style="cursor: pointer;;"> <label> <input type="checkbox" v-model="agentformdata.bottoollist" :value="item" /> {{item.toolname}}(<span style="color: grey;font-size: 12px;"> {{item.desc}}</span>) </label> </p> </div> <div class="tooldia" v-show="showaddagentdia" style="z-index: 112;max-height: 60vh;overflow: scroll;"> <i @click="showaddagentdia=false" class="fa fa-lg fa-times-circle" style="margin: 0; float: right;"></i> <div style="font-weight: bold;">添加智能体 </div> <div> <p>智能体名称:</p> <input type="text" class="textinput" v-model="agentformdata.agentname" placeholder="智能体名称" /> </div> <div> <p>智能体描述:</p> <textarea type="text" class="promptinput" v-model="agentformdata.desc" placeholder="智能体描述"></textarea> </div> <div class="select-wrapper"> <p>模型</p> <p> <select v-model="agentformdata.aimodel"> <option v-for="item in modellist" :key="item.model" :value="item.model"> {{ item.model }} </option> </select> </p> </div> <div> <p>随机性:{{agentformdata.temperature}}</p> <p><input type="range" v-model="agentformdata.temperature" min="0.1" max="1" step="0.1" /></p> </div> <div> <p>上下文历史记录轮数:{{agentformdata.maxlun}}</p> <p><input type="range" v-model="agentformdata.maxlun" min="1" max="10" step="1" /></p> </div> <div> <p>topN:{{agentformdata.topn}}</p> <p><input type="range" v-model="agentformdata.topn" min="0.1" max="1" step="0.1" /></p> </div> <div> <p>最大输出:{{agentformdata.maxtokens}}tokens</p> <p><input type="range" v-model="agentformdata.maxtokens" min="100" max="1000" step="100" /></p> </div> <div> <p>提示词</p> <p> <textarea class="promptinput" v-model="agentformdata.prompt" rows="3" placeholder="输入你的提示词"></textarea></p> </div> <div> <p>插件<i @click="showtooldiaforagent=true" class="fa fa-lg fa-plus-circle" style="margin-left: 20px;"></i></p> <p v-for="item in agentformdata.bottoollist"> {{item.toolname}}(<span style="color: grey;font-size: 12px;"> {{item.desc}}</span>) </p> </div> <div class="select-wrapper"> <p>语音音色</p> <p> <select v-model="agentformdata.voiceindex"> <option v-for="(voice,index) in chineseVoices" :key="voice.name" :value="index"> {{ voice.name }} </option> </select> </p> </div> <div> <p>语音唤醒词</p> <p> <input class="textinput" v-model="agentformdata.wakeupword" rows="1" placeholder="输入你的语音唤醒词" /></p> </div> <button class="subbtn" @click="addagentform">提交</button> </div> <div class="tooldia" v-show="showagentlist"> <i @click="showagentlist=false" class="fa fa-lg fa-times-circle" style="margin: 0; float: right;"></i> <div style="font-weight: bold;">选择智能体 <i @click="showaddagentdia=true" class="fa fa-lg fa-plus-circle" style="margin-left: 20px;"></i></div> <p v-for="(item,index) in aiagentlist" :key="index" @click="startchatbyagent(index)" style="cursor: pointer;;"> {{item.agentname}}(<span style="color: grey;font-size: 12px;"> {{item.desc}}</span>) </p> </div> <div class="tooldia" v-show="showtooldia"> <i @click="showtooldia=false" class="fa fa-lg fa-times-circle" style="margin: 0; float: right;"></i> <div style="font-weight: bold;">选择你想要的工具<i @click="showaddtooldia=true" class="fa fa-lg fa-plus-circle" style="margin-left: 20px;"></i></div> <p v-for="item in alltoollist" :key="item.toolname" style="cursor: pointer;;"> <label> <input type="checkbox" v-model="agentconfig.bottoollist" :value="item" /> {{item.toolname}}(<span style="color: grey;font-size: 12px;"> {{item.desc}}</span>) </label> </p> </div> <div id="chatlist" v-show="showhis"> <div style="display: flex;margin-bottom: 10px;;"> <a style="cursor: pointer; flex: 1;margin-top: 20px;border: 1px solid grey; border-radius: 10px;text-align: center; padding: 5px;" @click="newchat"> <i class="fa fa-lg fa-comments" style="margin-right: 10px;;"></i>新会话</a> <i @click="showhis=false" class="fa fa-lg fa-times-circle" style="margin: 0;"></i> </div> <div v-for="(item,index) in hischatlist" :key="item.id" @click="openchat(item.id)" :class="nowchatid==item.id ? 'nowchatitem hischatitem' : 'hischatitem'"> <span class="agentname">{{item.agent.agentname}}</span> <span v-if="item.data.length>0" class="messagetext">{{item.data[0].content}}</span> <span v-if="item.data.length==0" class="messagetext">新聊天</span> <i class="fa fa-trash del" @click.stop="delchat(index)"></i> </div> </div> <div id="bodychat" class="cont"> <div style="display: flex;padding: 10px;line-height: 20px;"> <i v-show="!showhis" class="fa fa-lg fa-comments" @click="showhis=!showhis"></i> <h2 style="margin: 0 auto;padding: 0;">Ollama API实现AI聊天</h2> <i class="fa fa-lg fa-cog" style="margin-right: 0px;" @click="showsetting=!showsetting" v-show="!showsetting&&nowchatid!=''"></i> </div> <div id="voicechat" v-if="vociechating"> <div @click="newvoicechat"> <img v-if="aivoicestate==1" style="height:100px;" src="https://repo.bfw.wiki/bfwrepo/icon/6687d86e43686.png" /> <img v-if="aivoicestate==2" style="height:100px;" src="https://repo.bfw.wiki/bfwrepo/icon/6687d064d941c.gif" /> <img v-if="aivoicestate==3" style="height:100px;" src="https://repo.bfw.wiki/bfwrepo/icon/6687d047c924b.gif" /> <img v-if="aivoicestate==4" style="height:100px;" src="https://repo.bfw.wiki/bfwrepo/icon/6687d07c0a4d8.gif" /> </div> <p v-if="aivoicestate==1"> 请说关键词"{{agentconfig.wakeupword}}"或点击上面图标唤醒我</p> <p v-if="aivoicestate==2">我在听,您请说……</p> <p v-if="aivoicestate==3">思考中……</p> <p v-if="aivoicestate==4">回答中,可说关键字"{{agentconfig.wakeupword}}"或点击上面图标进行提问</p> <p><button class="hangoverbtn" @click="hangout"> <i class="fa fa-stop-circle" style="margin-right: 10px;"></i>挂断 </button></p> </div> <div v-if="nowchatid==''&&modellist.length>0" class="historylist"> <div class="recomlist"> <div class="item" @click="askagentnew('说说《三国演义》有啥?')"> <p><i class="fa fa-lg fa-leanpub"></i></p> <p> 说说《三国演义》有啥? </div> <div class="item" @click="askagentnew('电脑蓝屏怎么办?')"> <p> <i class="fa fa-lg fa-th-large"></i></p> <p> 电脑蓝屏怎么办?</p> </div> <div class="item" @click="askagentnew('如何锻炼身体?')"> <p> <i class="fa fa-lg fa-universal-access"></i></p> <p> 如何锻炼身体?</p> </div> </div> </div> <div v-if="nowchatid==''&&modellist.length==0" class="historylist"> <div class="recomlist"> <div class="item"> <p> <i class="fa fa-lg fa-universal-access"></i></p> <p> 请打开ollama</p> </div> </div> </div> <div id="historylist" @scroll="handleScroll" class="historylist" v-if="nowchatid!=''&&!vociechating"> <div v-for="(msg,index) in newmess" :key="index" :class="{'mesay': msg.role === 'user', 'aisay': msg.role === 'system'}"> <div v-html="msg.content"></div> <div v-if="aistatus==1&&msg.content==''&&msg.role === 'system'"><img style="height:30px;" src='https://repo.bfw.wiki/bfwrepo/icon/667d490a27acd.gif' /></div> <div v-if="aistatus==2&&msg.content==''&&msg.role === 'system'">调用插件中……</div> <div v-if="msg.role === 'system'&&msg.isfinished" class="feedbackpanel"><a title="复制" class="copybtn" @click="copy(index)"><i class="fa fa-copy"></i></a><a title="重新生成" class="regenbtn" @click="regen(index)"><i class="fa fa-rotate-left"></i></a> <a title="非常好" class="regenbtn"><i class="fa fa-thumbs-o-up"></i></a> <a title="不好" class="regenbtn"><i class="fa fa-thumbs-o-down"></i></a> <a @click="speakorstop(index)" v-if="sayingid!=msg.id" title="语音播放" class="regenbtn"><i class="fa fa-play"></i></a> <a @click="speakorstop(index)" v-if="sayingid==msg.id" title="停止播放" class="regenbtn"><i class="fa fa-stop"></i></a> </div> </div> <div class="recomminput"> <ol> <li v-for="msg in recommtips" @click="askai(msg)"> {{msg}} </li> </ol> </div> </div> <div class="footer"> <div class="scrollbar" v-if="!autoscroll" @click="tobottom" title="滚动到底部" style="text-align:center;"> <i class="fa fa-lg fa-angle-double-down"></i> </div> <div v-if="attachfile!=''" class="attachpannel"> <img title="附件" :src="attachfile" style="width:100px;" /> <i @click="attachfile=''" class="fa fa-lg fa-times-circle"></i> </div> <div v-show="micinputing" id="micinput"> <i @click="micinputing=false" class="fa fa-lg fa-times-circle" style="margin-left:180px;"></i> <div @mousedown="startRecognition" @mouseup="stopRecognition" @touchstart="startRecognition" @touchend="stopRecognition"><img v-if="!recognitioning" src='https://repo.bfw.wiki/bfwrepo/icon/6687d86e43686.png' style="margin: 20px auto;" /> <img v-if="recognitioning" src='https://repo.bfw.wiki/bfwrepo/icon/6687d064d941c.gif' style="margin: 20px auto;" /> </div> <p v-if="recognitioning">请说</p> <p v-if="!recognitioning">单击不放开始语音识别输入</p> </div> <div id="inputpannel" class="inputpannel" v-show="nowchatid!=''&&!vociechating"> <div class="file-upload" id="fileUpload"> <input type="file" id="fileInput" name="file" accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"> <i title="上传附件" class="fa fa-lg fa-plus-circle"></i> </div> <textarea class="inputtext" v-model="userInput" oninput="this.style.height = '';this.style.height = this.scrollHeight + 'px'" @keydown.enter.prevent="sendai" placeholder="请输入问题"></textarea> <div class="sendbtn" @click="voicechat"> <i title="语音通话" class=" fa fa-lg fa-phone"> </i></div> <div class="sendbtn" @click="micinputing=true"> <i title="语音输入" class=" fa fa-lg fa-microphone"> </i></div> <div @click="sendai" class="sendbtn"> <i title="发送" v-if="!aireplying" class="fa fa-lg fa-send"> </i> <i title="停止" v-if="aireplying" class="fa fa-lg fa-stop-circle"> </i> </div> </div> </div> </div> <div id="setting" v-show="showsetting&&nowchatid!=''"> <div style="display: flex;margin: 10px;"> <a style="flex: 1;text-align: center; font-weight: bold;">设置</a><i @click="showsetting=false" class="fa fa-lg fa-times-circle" style="margin: 0;"></i> </div> <div class="select-wrapper"> <p>当前模型</p> <p> <select v-model="agentconfig.aimodel" @change="handleChange"> <option v-for="item in modellist" :key="item.model" :value="item.model"> {{ item.model }} </option> </select> </p> </div> <div> <p>随机性:{{agentconfig.temperature}}</p> <p><input type="range" v-model="agentconfig.temperature" min="0.1" max="1" step="0.1" /></p> </div> <div> <p>topN:{{agentconfig.topn}}</p> <p><input type="range" v-model="agentconfig.topn" min="0.1" max="1" step="0.1" /></p> </div> <div> <p>几轮:{{agentconfig.maxlun}}</p> <p><input type="range" v-model="agentconfig.maxlun" min="3" max="10" step="1" /></p> </div> <div> <p>最大输出:{{agentconfig.maxtokens}}</p> <p><input type="range" v-model="agentconfig.maxtokens" min="100" max="1000" step="100" /></p> </div> <div> <p>提示词</p> <p> <textarea class="promptinput" v-model="agentconfig.prompt" rows="3" placeholder="输入你的提示词"></textarea></p> </div> <div> <p>插件<i @click="showtooldia=true" class="fa fa-lg fa-plus-circle" style="margin-left: 20px;"></i></p> <p v-for="item in agentconfig.bottoollist"> {{item.toolname}}(<span style="color: grey;font-size: 12px;"> {{item.desc}}</span>) </p> </div> <div class="select-wrapper"> <p>语音音色</p> <p> <select v-model="agentconfig.voiceindex"> <option v-for="(voice,index) in chineseVoices" :key="voice.name" :value="index"> {{ voice.name }} </option> </select> </p> </div> <div> <p>语音唤醒词</p> <p> <input class="textinput" v-model="agentconfig.wakeupword" rows="1" placeholder="输入你的语音唤醒词" /></p> </div> </div> </div> <script src="renderer.js"></script> </body> </html>
4、运行
npm start
5、打包
以管理员运行cmd,然后在package.json中增加
"build": { "appId": "com.xxx.app", "mac": { "target": [ "dmg", "zip" ] }, "win": { "target": [ "nsis", "zip" ] } }, "scripts": { "start": "electron .", "dist": "electron-builder --win --x64", },5、管理员运行cmd输入命令npm run dist,稍等片刻就会生成exe安装包
renderer.js源码:
点击查看
网友评论