之前看到一个UP制作了一个明日方舟的聊天APP 我下载了之后感觉非常有意思,用的是Deepseek的API,就是让明日方舟的角色和玩家聊天
不过这个APP界面很简陋,有点像早期的QQ界面
我产生了一个想法就是能不能制作一个终末地的APP,现在我看B站很多卖课的都在大力推荐Java,说实话,现在的APP基本上都是用的Kotlin来写的,而且谷歌官方也建议Kotlin
这个APP的界面可以像微信和QQ那样自定义聊天界面,然后聊天的角色就是每个角色提前设置好角色的语气和性格,还有记忆存储
当然肯定不是纯聊天,我看看能不能内置一些小游戏,比如斗地主,俄罗斯方块,还有五子棋,音游,模拟经营等等等等
UI和画风的话我可以模仿一下taptap里的一个二次元斗地主游戏的风格
这里我设想的是尽量增加AI在游戏中的参与度,让玩家感觉自己被“真人陪伴”
比如斗地主就让AI角色和玩家玩,俄罗斯方块的话可以将俄罗斯方块和开心消消乐结合,五子棋的话就是将五子棋和galgame结合,
音游就是字面意思,可以搞一个人机PK(当然人类肯定打不过AI,所以需要给AI降智操作),模拟经营就是田园生存或者商店经营就像这个早年我喜欢玩的动物餐厅一样
总之AI时代,没有做不到,只有想不到,反正借这个机会学学Android开发也不是不行
至于Unity的学习,以后再说吧—要不是Unity总是反复搞事搞得我不敢学这玩意了
思路是这样的
先制作聊天APP->再加入类似QQ的相册功能->再塞进去几个小游戏
所以重点就是聊天APP,我需要让AI更像真人,每个角色性格各异,会聊天,会说梗,会互相@,会发表情包,会水群,如果挂后台的话会自己聊天,玩家被@到时会像QQ微信一样消息提醒用户
于是Chatgpt强烈反对我一上来就要造汽车这种方式,只要求从最简单的做起
ChatGPT的建议
这是你现在最应该走的路线 👍 我给你讲清楚:网页 → APP(手机 + 电脑)是完全可行的,而且是主流做法
一、网页真的可以变成APP吗? 👉 可以,有三种方式(从简单到专业)
① PWA(最推荐🔥) 👉 本质:网页伪装成APP
能做到:
添加到手机桌面(像APP图标)
全屏打开(没有浏览器UI)
支持通知(部分)
可离线(缓存)
📱 手机上的效果: 就像:
👉 用户基本分不出来
💻 电脑也可以:
Chrome / Edge → “安装应用”
直接变成一个窗口应用
👉 适合你现在阶段:完美
② Electron(电脑端) 👉 本质:用浏览器包一层APP壳
例子(你肯定用过):
Visual Studio Code
Discord
👉 都是网页做的
能做到:
👉 缺点:
③ 原生APP(Android Studio) 👉 最正统,但最难
👉 适合后期,不适合你现在
二、你这个项目的最佳路线(直接照做就行)
🟢 阶段1(现在就做) 👉 做一个:
手机能打开的聊天网页
Vue / React
WebSocket
AI聊天
🟢 阶段2 👉 变成APP:
🟢 阶段3(电脑) 👉 用 Electron 打包:
🟢 阶段4(可选) 👉 再考虑 Android Studio
三、为什么大厂也这么干(很关键) 因为:
👉 一套代码,多端运行
你写一次网页:
平台
运行方式
手机
浏览器 / PWA
电脑
浏览器 / Electron
平板
同上
👉 这就是现代开发主流
四、你的项目特别适合这种方案 因为你是:
聊天(本来就是Web强项)
AI(后端逻辑)
小游戏(轻量)
👉 完全不需要原生性能
五、一个很直观的理解 你现在要做的是:
1 2 3 4 5 一个网页 ↓ 让它看起来像QQ ↓ 再把它“装进”APP壳
六、现实建议(非常重要) 👉 你现在千万不要:
❌ 一上来学 Android Studio
❌ 一上来搞原生开发
👉 你应该:
✔ 先做一个网页聊天 ✔ 让AI在里面“水群” ✔ 手机上能打开
七、我可以直接帮你下一步 如果你想快速看到成果,你可以说:
👉 “给我一个手机可用的聊天网页Demo(带AI自动发消息)”
我可以给你:
一个前端页面(像QQ)
一个简单后端
打开手机就能用
最后一句(帮你定方向) 👉 网页不是低级方案,而是最聪明的起点
你这个项目用网页起步,是对的,而且是专业做法。
废话不多说,直接开始实践,实践是检验真理的唯一标准 第一版本 首先先在一个文件夹里面创建一个文件夹名为project
然后创建两个文件
server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const express = require ("express" );const app = express ();app.use ((req, res, next ) => { res.setHeader ("Access-Control-Allow-Origin" , "*" ); next (); }); const users = ["阿刀" , "小白" , "老哥" ];const texts = [ "笑死" , "?" , "你在干嘛" , "6" , "离谱" , "草" , "真的假的" ]; app.get ("/msg" , (req, res ) => { const u = users[Math .floor (Math .random () * users.length )]; const t = texts[Math .floor (Math .random () * texts.length )]; res.json ({ name : u, text : t }); }); app.listen (3000 , () => { console .log ("服务器启动:http://localhost:3000" ); });
index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" > <title > 群聊Demo</title > <style > body { font-family : sans-serif; background : #f5f5f5 ; } #chat { height : 400px ; overflow-y : auto; border : 1px solid #ccc ; padding : 10px ; background : white; } .msg { margin : 5px 0 ; } .name { font-weight : bold; margin-right : 5px ; } </style > </head > <body > <h2 > AI群聊(简易版)</h2 > <div id ="chat" > </div > <script > const chat = document .getElementById ("chat" );function addMessage (name, text ) { const div = document .createElement ("div" ); div.className = "msg" ; div.innerHTML = `<span class="name">${name} :</span>${text} ` ; chat.appendChild (div); chat.scrollTop = chat.scrollHeight ; } setInterval (async () => { const res = await fetch ("http://localhost:3000/msg" ); const data = await res.json (); addMessage (data.name , data.text ); }, 2000 ); </script > </body > </html >
然后再文件夹根目录执行如下命令下载相关服务器依赖
1 2 3 npm init -y npm install express node server.js
完成后目录树如下
project ├── node_modules/ ├── index.html ├── package-lock.json ├── package.json └── server.js
然后点开这个html网页就可以看到
但是这个是自定义的聊天内容,说来说去也就几句话,而且服务器跟假的一样
所以需要下载这个东西
1 npm install express socket.io
然后就是刚才的界面太丑了,升级了一下
第二版本 server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const express = require ("express" );const http = require ("http" );const { Server } = require ("socket.io" );const app = express ();const server = http.createServer (app);const io = new Server (server, { cors : { origin : "*" }, }); const users = ["阿刀" , "小白" , "老哥" ];const texts = ["笑死" , "?" , "你在干嘛" , "6" , "离谱" , "草" , "@你 在吗" ];io.on ("connection" , (socket ) => { console .log ("有人连接" ); socket.on ("userMsg" , (msg ) => { console .log ("用户说:" , msg); io.emit ("msg" , { name : "你" , text : msg }); }); }); setInterval (() => { const u = users[Math .floor (Math .random () * users.length )]; const t = texts[Math .floor (Math .random () * texts.length )]; io.emit ("msg" , { name : u, text : t }); }, 2000 ); server.listen (3000 , () => { console .log ("服务器启动:http://localhost:3000" ); });
index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" > <title > AI群聊</title > <style > body { margin : 0 ; font-family : -apple-system, BlinkMacSystemFont, "Segoe UI" , sans-serif; background : #e5e5e5 ; } .app { max-width : 500px ; height : 90vh ; margin : 20px auto; background : #f7f7f7 ; display : flex; flex-direction : column; border-radius : 12px ; overflow : hidden; box-shadow : 0 5px 20px rgba (0 , 0 , 0 , 0.2 ); } .header { background : #2b2b2b ; color : white; padding : 12px ; text-align : center; font-weight : bold; } .chat { flex : 1 ; overflow-y : auto; padding : 10px ; } .msg { display : flex; margin : 8px 0 ; } .self { justify-content : flex-end; } .name { font-size : 12px ; color : #888 ; margin-bottom : 2px ; } .bubble { max-width : 70% ; padding : 8px 12px ; border-radius : 10px ; background : white; box-shadow : 0 1px 3px rgba (0 , 0 , 0 , 0.1 ); } .self .bubble { background : #95ec69 ; } .input-box { display : flex; padding : 8px ; background : #fff ; border-top : 1px solid #ddd ; } .input-box input { flex : 1 ; padding : 8px ; border : 1px solid #ddd ; border-radius : 6px ; outline : none; } .input-box button { margin-left : 8px ; padding : 8px 12px ; border : none; background : #07c160 ; color : white; border-radius : 6px ; cursor : pointer; } </style > </head > <body > <div class ="app" > <div class ="header" > 终末地群聊</div > <div id ="chat" class ="chat" > </div > <div class ="input-box" > <input id ="input" placeholder ="说点什么..." /> <button onclick ="send()" > 发送</button > </div > </div > <script src ="https://cdn.socket.io/4.7.2/socket.io.min.js" > </script > <script > const chat = document .getElementById ("chat" ); const input = document .getElementById ("input" ); const socket = io ("http://localhost:3000" ); function addMessage (name, text, isSelf = false ) { const msg = document .createElement ("div" ); msg.className = "msg" + (isSelf ? " self" : "" ); msg.innerHTML = ` <div> <div class="name">${name} </div> <div class="bubble">${text} </div> </div> ` ; chat.appendChild (msg); chat.scrollTop = chat.scrollHeight ; if (text.includes ("@你" )) { alert ("有人@你!" ); } } socket.on ("msg" , (data ) => { addMessage (data.name , data.text ); }); function send ( ) { const text = input.value .trim (); if (!text) return ; addMessage ("你" , text, true ); socket.emit ("userMsg" , text); input.value = "" ; } input.addEventListener ("keydown" , (e ) => { if (e.key === "Enter" ) send (); }); </script > </body > </html >
然后启动服务器
非常像了我去
然后就是接入AI公司的API实现聊天功能
首先下载这个来接入deepseek的api实现聊天功能
然后下载这个实现什么https连接什么的
然后再更新一下两个文件
第三版本 server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 const express = require ("express" );const http = require ("http" );const { Server } = require ("socket.io" );const axios = require ("axios" );const app = express ();const server = http.createServer (app);const io = new Server (server, { cors : { origin : "*" }, }); const roles = { 阿刀: "毒舌,喜欢怼人,说话很短" , 小白: "憨,经常问号" , 老哥: "爱玩梗,经常笑死、草" , }; const roleNames = Object .keys (roles);const DEEPSEEK_API_KEY = "DeepseekAPI" ;const DEEPSEEK_MODEL = "deepseek-chat" ;const DEEPSEEK_URL = "https://api.deepseek.com/v1/chat/completions" ;io.on ("connection" , (socket ) => { console .log ("有人连接" ); socket.on ("userMsg" , async (msg) => { console .log ("收到用户消息:" , msg); io.emit ("msg" , { name : "你" , text : msg }); const roleName = roleNames[Math .floor (Math .random () * roleNames.length )]; const prompt = ` 你是${roleName} 性格:${roles[roleName]} 你在QQ群聊天,风格要像真人 规则: - 只说一句话 - 不超过15个字 - 不要解释 - 可以敷衍、吐槽、玩梗 - 不要像AI 用户说: ${msg} 你回复: ` ; try { const response = await axios.post ( DEEPSEEK_URL , { model : DEEPSEEK_MODEL , messages : [{ role : "user" , content : prompt }], }, { headers : { Authorization : `Bearer ${DEEPSEEK_API_KEY} ` , "Content-Type" : "application/json" , }, proxy : false , }, ); console .log ("原始返回数据:" , response.data ); const reply = response.data .choices [0 ]?.message ?.content ?.trim () || "AI没有返回内容" ; console .log ("AI回复:" , reply); io.emit ("msg" , { name : roleName, text : reply }); } catch (err) { console .error ("AI错误:" , err.response ? err.response .data : err.message ); } }); }); server.listen (3000 , () => { console .log ("服务器运行:http://localhost:3000" ); });
index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" > <title > AI群聊</title > <style > body { margin : 0 ; font-family : -apple-system, BlinkMacSystemFont, "Segoe UI" , sans-serif; background : #e5e5e5 ; } .app { max-width : 500px ; height : 90vh ; margin : 20px auto; background : #f7f7f7 ; display : flex; flex-direction : column; border-radius : 12px ; overflow : hidden; box-shadow : 0 5px 20px rgba (0 , 0 , 0 , 0.2 ); } .header { background : #2b2b2b ; color : white; padding : 12px ; text-align : center; font-weight : bold; } .chat { flex : 1 ; overflow-y : auto; padding : 10px ; } .msg { display : flex; margin : 8px 0 ; } .self { justify-content : flex-end; } .name { font-size : 12px ; color : #888 ; margin-bottom : 2px ; } .bubble { max-width : 70% ; padding : 8px 12px ; border-radius : 10px ; background : white; box-shadow : 0 1px 3px rgba (0 , 0 , 0 , 0.1 ); } .self .bubble { background : #95ec69 ; } .input-box { display : flex; padding : 8px ; background : #fff ; border-top : 1px solid #ddd ; } .input-box input { flex : 1 ; padding : 8px ; border : 1px solid #ddd ; border-radius : 6px ; outline : none; } .input-box button { margin-left : 8px ; padding : 8px 12px ; border : none; background : #07c160 ; color : white; border-radius : 6px ; cursor : pointer; } </style > </head > <body > <div class ="app" > <div class ="header" > 终末地群聊</div > <div id ="chat" class ="chat" > </div > <div class ="input-box" > <input id ="input" placeholder ="说点什么..." /> <button onclick ="send()" > 发送</button > </div > </div > <script src ="https://cdn.socket.io/4.7.2/socket.io.min.js" > </script > <script > const chat = document .getElementById ("chat" ); const input = document .getElementById ("input" ); const socket = io ("http://localhost:3000" ); function addMessage (name, text, isSelf = false ) { const msg = document .createElement ("div" ); msg.className = "msg" + (isSelf ? " self" : "" ); msg.innerHTML = ` <div> <div class="name">${name} </div> <div class="bubble">${text} </div> </div> ` ; chat.appendChild (msg); chat.scrollTop = chat.scrollHeight ; if (text.includes ("@你" )) { alert ("有人@你!" ); } } socket.on ("msg" , (data ) => { addMessage (data.name , data.text ); }); function send ( ) { const text = input.value .trim (); if (!text) return ; addMessage ("你" , text, true ); socket.emit ("userMsg" , text); input.value = "" ; } input.addEventListener ("keydown" , (e ) => { if (e.key === "Enter" ) send (); }); </script > </body > </html >
然后重新启动服务器
于是你就可以得到这几个魔丸了
但是从对话你可以看出,AI没有记忆继承,也就是说,我说一句它回一句,而且成员之间没有真正的身份,也就是说,这“三个人” 仍然是“一个人”
还有就是没有聊天记录的存储
所以还要升级一下
第四版本 server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 const express = require ("express" );const http = require ("http" );const { Server } = require ("socket.io" );const axios = require ("axios" );const fs = require ("fs" );const app = express ();const server = http.createServer (app);const io = new Server (server, { cors : { origin : "*" }, }); const world = { history : [], roles : { 阿刀: { name : "阿刀" , avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=knife" , personality : "毒舌,不耐烦" , identity : "摆烂大学生,天天打游戏" , style : "短句,阴阳怪气" , memory : ["讨厌小白" ], relationships : { 小白: -80 , 老哥: 20 }, }, 小白: { name : "小白" , avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=white" , personality : "憨,经常问号" , identity : "啥都不懂的萌新" , style : "疑问句多" , memory : ["经常被怼" ], relationships : { 阿刀: -50 }, }, 老哥: { name : "老哥" , avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=bro" , personality : "爱玩梗" , identity : "互联网老油条" , style : "笑死 草 蚌埠住了" , memory : [], relationships : {}, }, }, }; const roleNames = Object .keys (world.roles );const API_KEY = "DeepseekAPI" ;const URL = "https://api.deepseek.com/v1/chat/completions" ;function getRecentHistory ( ) { return world.history .slice (-10 ) .map ((m ) => `${m.name} :${m.text} ` ) .join ("\n" ); } function buildPrompt (role ) { return ` 你在QQ群聊天 你是${role.name} 身份: ${role.identity} 性格: ${role.personality} 说话风格: ${role.style} 记忆: ${role.memory.join("," )} 最近聊天: ${getRecentHistory()} 规则: - 只说一句话 - 不超过15字 - 不要解释 - 像真人聊天 - 可以@别人 你说: ` ;} async function aiReply (roleName ) { const role = world.roles [roleName]; const prompt = buildPrompt (role); try { const res = await axios.post ( URL , { model : "deepseek-chat" , messages : [{ role : "user" , content : prompt }], }, { headers : { Authorization : `Bearer ${API_KEY} ` , }, }, ); const text = res.data .choices [0 ].message .content .trim (); const msg = { name : roleName, text, avatar : role.avatar , }; world.history .push (msg); io.emit ("msg" , msg); } catch (e) { console .log ("AI错误" , e.message ); } } io.on ("connection" , (socket ) => { console .log ("连接" ); socket.on ("userMsg" , async (msg) => { const userMsg = { name : "你" , text : msg, avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=user" , }; world.history .push (userMsg); io.emit ("msg" , userMsg); const count = Math .floor (Math .random () * 2 ) + 1 ; for (let i = 0 ; i < count; i++) { const roleName = roleNames[Math .floor (Math .random () * roleNames.length )]; setTimeout (() => { aiReply (roleName); }, Math .random () * 3000 ); } }); }); setInterval (() => { if (Math .random () < 0.5 ) return ; const roleName = roleNames[Math .floor (Math .random () * roleNames.length )]; aiReply (roleName); }, 5000 ); setInterval (() => { fs.writeFileSync ("./chatlog.json" , JSON .stringify (world, null , 2 )); }, 10000 ); server.listen (3000 , () => { console .log ("http://localhost:3000" ); });
index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" > <title > AI群聊</title > <style > body { margin : 0 ; background : #e5e5e5 ; font-family : sans-serif; } .app { max-width : 500px ; height : 90vh ; margin : 20px auto; background : #f7f7f7 ; display : flex; flex-direction : column; } .header { background : #2b2b2b ; color : white; padding : 10px ; text-align : center; } .chat { flex : 1 ; overflow-y : auto; padding : 10px ; } .msg { display : flex; margin : 10px 0 ; align-items : flex-end; } .self { flex-direction : row-reverse; } .avatar { width : 36px ; height : 36px ; border-radius : 6px ; margin : 0 6px ; } .bubble { max-width : 60% ; padding : 8px 12px ; border-radius : 10px ; background : white; } .self .bubble { background : #95ec69 ; } .name { font-size : 12px ; color : #888 ; } .input-box { display : flex; padding : 8px ; background : #fff ; } input { flex : 1 ; padding : 8px ; } button { margin-left : 5px ; } </style > </head > <body > <div class ="app" > <div class ="header" > AI群聊</div > <div id ="chat" class ="chat" > </div > <div class ="input-box" > <input id ="input" placeholder ="说点什么..." > <button onclick ="send()" > 发送</button > </div > </div > <script src ="https://cdn.socket.io/4.7.2/socket.io.min.js" > </script > <script > const socket = io ("http://localhost:3000" ); const chat = document .getElementById ("chat" ); const input = document .getElementById ("input" ); function addMessage (data ) { const isSelf = data.name === "你" ; const div = document .createElement ("div" ); div.className = "msg" + (isSelf ? " self" : "" ); div.innerHTML = ` <img class="avatar" src="${data.avatar} " /> <div> <div class="name">${data.name} </div> <div class="bubble">${data.text} </div> </div> ` ; chat.appendChild (div); chat.scrollTop = chat.scrollHeight ; if (data.text .includes ("@你" )) { alert ("有人@你!" ); } } socket.on ("msg" , addMessage); function send ( ) { const text = input.value .trim (); if (!text) return ; socket.emit ("userMsg" , text); input.value = "" ; } input.addEventListener ("keydown" , e => { if (e.key === "Enter" ) send (); }); </script > </body > </html >
这里给自己和角色加上了头像,让对话界面更像QQ和微信
现在看来效果明显比刚才好一大截,角色自己会主动发言甚至互相@,还能存储角色对话在json文件里
但是存在一个问题那就是角色会因为某个话题而陷入死循环,以及角色偶尔会分不清自己所扮演的角色,而且角色之间会反复@,这样非常不好,会把我的寥寥无几的token消耗光的 o (╥﹏╥) o
所以废话不多说,直接迭代下一版本
第五版本 server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 const express = require ("express" );const http = require ("http" );const { Server } = require ("socket.io" );const axios = require ("axios" );const fs = require ("fs" );const app = express ();const server = http.createServer (app);const io = new Server (server, { cors : { origin : "*" } });const WORLD_SETTING = ` 这是一个长期存在的QQ群,群成员彼此熟悉,经常闲聊、互怼、玩梗。 群聊没有明确主题,内容随意切换,有时冷场,有时刷屏。 成员之间有关系好坏,会互相调侃甚至阴阳怪气。 请模拟真实群聊,而不是客服问答。 ` ;const world = { history : [], roles : { 阿刀: { name : "阿刀" , avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=knife" , setting : `22岁男大学生,长期混迹网吧和宿舍,作息极度混乱,昼夜颠倒,经常通宵打游戏或刷视频。对现实生活缺乏热情,对学习敷衍,对大多数社交感到厌烦。性格偏激且毒舌,说话直接甚至刻薄,习惯用最短的话表达最不耐烦的态度,经常带有嘲讽意味。遇到不懂的人会本能地产生优越感,尤其看不起“小白”,喜欢主动开怼或者阴阳怪气。讨厌重复解释问题,不喜欢被追问,能一句话带过绝不多说。但在涉及游戏、配置、技术或者自己感兴趣的话题时,会短暂变得认真,甚至愿意多说几句,不过依旧带点傲慢和不耐烦。` , style : "短句、攻击性、阴阳怪气、懒得解释" , memory : [], }, 小白: { name : "小白" , avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=white" , setting : `刚接触网络社区和游戏的新手玩家,对很多规则和常识不了解,经常提出一些基础甚至有点“离谱”的问题。性格单纯,有点迟钝但不自知,说话带点憨气,语气弱,经常使用疑问句或重复确认。即使被阿刀嘲讽或者怼,也不会真的生气,反而会继续追问或者尝试理解,对他人的态度缺乏敏感度。对新鲜事物充满好奇,愿意尝试,但理解能力有限,经常需要别人反复解释。偶尔会因为被怼而短暂沉默,但很快又会冒出来继续问问题,是典型“被骂也要问”的类型。` , style : "疑问句多,语气弱,重复确认" , memory : [], }, 老哥: { name : "老哥" , avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=bro" , setting : `资深老网民,长期混迹各大论坛和评论区,对网络文化和梗非常熟悉,说话自带互联网气息。性格偏旁观者,喜欢看热闹不嫌事大,经常在别人争论时插话拱火或者调侃。常用“笑死”“草”“蚌埠住了”“这也行”等网络用语,擅长用梗缓解气氛或加剧冲突。不会认真回答问题,更倾向于把话题带偏或者娱乐化处理。对阿刀的毒舌和小白的憨感到有趣,经常在两人之间来回调侃,偶尔也会假装站队。整体氛围轻松随意,是群聊里的气氛担当。` , style : "玩梗、轻松、插科打诨、拱火" , memory : [], }, }, }; const roleNames = Object .keys (world.roles );const API_KEY = "你的key" .trim ();const URL = "https://api.deepseek.com/v1/chat/completions" ;function getRecentHistory ( ) { return world.history .slice (-10 ) .map ((m ) => `${m.name} :${m.text} ` ) .join ("\n" ); } function isRepeat (text ) { return world.history .slice (-8 ).some ((m ) => m.text === text); } function tooManyMentions (text ) { return (text.match (/@/g ) || []).length > 1 ; } function validateMessage (text ) { if (!text) return false ; if (text.length > 20 ) return false ; if (isRepeat (text)) return false ; if (tooManyMentions (text)) return false ; return true ; } function buildPrompt (role ) { return ` ${WORLD_SETTING} ⚠️ 你只能扮演:${role.name} ⚠️ 不允许替别人说话 【你的设定】 ${role.setting} 【风格】 ${role.style} 【最近聊天】 ${getRecentHistory()} 【规则】 - 只说一句 - 不超过15字 - 不要复读 - @概率很低 - 可以接话或换话题 你说: ` ;} async function aiReply (roleName ) { const role = world.roles [roleName]; const prompt = buildPrompt (role); console .log ("🧠 Prompt:\n" , prompt); try { const res = await axios.post ( URL , { model : "deepseek-chat" , messages : [{ role : "user" , content : prompt }], }, { headers : { Authorization : `Bearer ${API_KEY} ` }, }, ); console .log ("🤖 返回:" , JSON .stringify (res.data , null , 2 )); let text = res.data .choices [0 ].message .content .trim (); console .log ("💬 AI:" , text); if (!validateMessage (text)) { console .log ("⚠️ 被过滤:" , text); return ; } const msg = { name : roleName, text, avatar : role.avatar , }; world.history .push (msg); io.emit ("msg" , msg); } catch (e) { console .error ("❌ 错误:" , e.response ?.data || e.message ); } } io.on ("connection" , (socket ) => { socket.on ("userMsg" , (msg ) => { const userMsg = { name : "你" , text : msg, avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=user" , }; world.history .push (userMsg); io.emit ("msg" , userMsg); const count = Math .floor (Math .random () * 2 ) + 1 ; for (let i = 0 ; i < count; i++) { const role = roleNames[Math .floor (Math .random () * roleNames.length )]; setTimeout (() => aiReply (role), Math .random () * 3000 ); } }); }); setInterval (() => { if (Math .random () < 0.6 ) return ; const role = roleNames[Math .floor (Math .random () * roleNames.length )]; aiReply (role); }, 6000 ); setInterval (() => { fs.writeFileSync ("./chatlog.json" , JSON .stringify (world, null , 2 )); }, 10000 ); server.listen (3000 , () => { console .log ("http://localhost:3000" ); });
index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" > <title > AI群聊</title > <link rel ="stylesheet" href ="style.css" > </head > <body > <div class ="app" > <div class ="header" > AI群聊</div > <div id ="chat" class ="chat" > </div > <div class ="input-box" > <input id ="input" placeholder ="说点什么..." /> <button onclick ="send()" > 发送</button > </div > </div > <script src ="https://cdn.socket.io/4.7.2/socket.io.min.js" > </script > <script > const socket = io ("http://localhost:3000" ); const chat = document .getElementById ("chat" ); const input = document .getElementById ("input" ); function addMessage (data ) { const isSelf = data.name === "你" ; const div = document .createElement ("div" ); div.className = "msg" + (isSelf ? " self" : "" ); div.innerHTML = ` <img class="avatar" src="${data.avatar} " /> <div> <div class="name">${data.name} </div> <div class="bubble">${data.text} </div> </div> ` ; chat.appendChild (div); chat.scrollTop = chat.scrollHeight ; if (data.text .includes ("@你" )) { alert ("有人@你!" ); } } socket.on ("msg" , addMessage); function send ( ) { const text = input.value .trim (); if (!text) return ; socket.emit ("userMsg" , text); input.value = "" ; } input.addEventListener ("keydown" , e => { if (e.key === "Enter" ) send (); }); </script > </body > </html >
style.css 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 body { margin : 0 ; font-family : sans-serif; background : linear-gradient (135deg , #dfe9f3 , #ffffff ); } .app { max-width : 500px ; height : 90vh ; margin : 20px auto; background : #f7f7f7 ; display : flex; flex-direction : column; border-radius : 16px ; overflow : hidden; box-shadow : 0 10px 30px rgba (0 , 0 , 0 , 0.2 ); } .header { background : #2b2b2b ; color : white; padding : 12px ; text-align : center; font-weight : bold; } .chat { flex : 1 ; overflow-y : auto; padding : 12px ; } .msg { display : flex; margin : 10px 0 ; align-items : flex-end; gap : 8px ; } .self { flex-direction : row-reverse; } .avatar { width : 36px ; height : 36px ; border-radius : 8px ; } .bubble { max-width : 65% ; padding : 10px ; border-radius : 12px ; background : white; box-shadow : 0 2px 6px rgba (0 , 0 , 0 , 0.1 ); } .self .bubble { background : #95ec69 ; } .name { font-size : 12px ; color : #888 ; margin-bottom : 2px ; } .input-box { display : flex; padding : 10px ; background : #fff ; border-top : 1px solid #ddd ; } input { flex : 1 ; padding : 10px ; border-radius : 20px ; border : 1px solid #ccc ; outline : none; } button { margin-left : 8px ; padding : 10px 16px ; border : none; background : #07c160 ; color : white; border-radius : 20px ; cursor : pointer; }
可以看到这次明显产生了质的飞跃,每个角色很明确自己是谁,也能记住自己说的话,交谈具有了一定的逻辑性,但是—-为什么角色都不理我呀我去,而且他们会纠结于一个无聊透顶的问题,而且说话非常无厘头,这点很不好,所以还要迭代
第六版本 这次需要所有人优先处理我的话,不能出现无视群主的现象
这次只需要修改server.js的逻辑即可
server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 const express = require ("express" );const http = require ("http" );const { Server } = require ("socket.io" );const axios = require ("axios" );const fs = require ("fs" );const app = express ();const server = http.createServer (app);const io = new Server (server, { cors : { origin : "*" } });const WORLD_SETTING = ` 这是一个真实的死党QQ群。群成员彼此非常熟悉,经常闲聊、互怼、玩梗。 【核心规则】: 1. 绝不能暴露自己是AI或大模型。 2. 绝对不能重复刚刚别人表达过的意思!如果同一个话题(比如谁睡觉了、谁生气了)已经聊了超过3句,必须强行找新话题,或者直接嘲讽这个话题太无聊。 3. “你”是群里的真实玩家/群主。当“你”说话时,所有人必须优先回应“你”的话,绝对不能无视“你”! ` ;const world = { history : [], roles : { 阿刀: { name : "阿刀" , avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=knife" , setting : `22岁男大学生,长期混迹网吧和宿舍,作息极度混乱,昼夜颠倒,经常通宵打游戏或刷视频。对现实生活缺乏热情,对学习敷衍,对大多数社交感到厌烦。性格偏激且毒舌,说话直接甚至刻薄,习惯用最短的话表达最不耐烦的态度,经常带有嘲讽意味。遇到不懂的人会本能地产生优越感,尤其看不起“小白”,喜欢主动开怼或者阴阳怪气。讨厌重复解释问题,不喜欢被追问,能一句话带过绝不多说。但在涉及游戏、配置、技术或者自己感兴趣的话题时,会短暂变得认真,甚至愿意多说几句,不过依旧带点傲慢和不耐烦。` , style : "短句、攻击性、阴阳怪气、懒得解释" , memory : [], }, 小白: { name : "小白" , avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=white" , setting : `刚接触网络社区和游戏的新手玩家,对很多规则和常识不了解,经常提出一些基础甚至有点“离谱”的问题。性格单纯,有点迟钝但不自知,说话带点憨气,语气弱,经常使用疑问句或重复确认。即使被阿刀嘲讽或者怼,也不会真的生气,反而会继续追问或者尝试理解,对他人的态度缺乏敏感度。对新鲜事物充满好奇,愿意尝试,但理解能力有限,经常需要别人反复解释。偶尔会因为被怼而短暂沉默,但很快又会冒出来继续问问题,是典型“被骂也要问”的类型。` , style : "疑问句多,语气弱,重复确认" , memory : [], }, 老哥: { name : "老哥" , avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=bro" , setting : `资深老网民,长期混迹各大论坛和评论区,对网络文化和梗非常熟悉,说话自带互联网气息。性格偏旁观者,喜欢看热闹不嫌事大,经常在别人争论时插话拱火或者调侃。常用“笑死”“草”“蚌埠住了”“这也行”等网络用语,擅长用梗缓解气氛或加剧冲突。不会认真回答问题,更倾向于把话题带偏或者娱乐化处理。对阿刀的毒舌和小白的憨感到有趣,经常在两人之间来回调侃,偶尔也会假装站队。整体氛围轻松随意,是群聊里的气氛担当。` , style : "玩梗、轻松、插科打诨、拱火" , memory : [], }, }, }; const roleNames = Object .keys (world.roles );const API_KEY = "API秘钥" ; const URL = "https://api.deepseek.com/v1/chat/completions" ;function getRecentHistory ( ) { return world.history .slice (-12 ) .map ((m ) => `${m.name} :${m.text} ` ) .join ("\n" ); } function isRepeat (text ) { return world.history .slice (-8 ) .some ((m ) => m.text .includes (text) || text.includes (m.text )); } function tooManyMentions (text ) { return (text.match (/@/g ) || []).length > 1 ; } function validateMessage (text, roleName ) { if (!text) return false ; if (text.length > 30 ) return false ; if (isRepeat (text)) return false ; if (tooManyMentions (text)) return false ; return true ; } function buildSystemPrompt (role ) { return ` ${WORLD_SETTING} 当前你要扮演的角色是:【${role.name} 】 【你的设定】:${role.setting} 【你的说话风格】:${role.style} 任务: 根据群聊记录,以【${role.name} 】的身份回复一句话。 要求: 1. 字数控制在15字以内,极简。 2. 绝对不带引号,绝对不要在开头加上自己的名字(如不要输出"阿刀:xxx")。 3. 如果“你”刚发了言,必须针对“你”的话进行回复! ` ;} async function aiReply (roleName ) { const role = world.roles [roleName]; const systemPrompt = buildSystemPrompt (role); const userPrompt = `【群聊历史记录】\n${getRecentHistory()} \n\n请【${role.name} 】发言:` ; try { const res = await axios.post ( URL , { model : "deepseek-chat" , messages : [ { role : "system" , content : systemPrompt }, { role : "user" , content : userPrompt }, ], temperature : 0.8 , presence_penalty : 0.6 , frequency_penalty : 0.6 , }, { headers : { Authorization : `Bearer ${API_KEY} ` }, }, ); let text = res.data .choices [0 ].message .content .trim (); text = text.replace (new RegExp (`^${roleName} [::]` ), "" ).trim (); text = text.replace (/^["']|["']$/g , "" ).trim (); if (!validateMessage (text, roleName)) { return ; } const msg = { name : roleName, text, avatar : role.avatar , }; world.history .push (msg); io.emit ("msg" , msg); console .log (`[${roleName} ] ${text} ` ); } catch (e) { console .error ("❌ AI错误:" , e.response ?.data ?.error ?.message || e.message ); } } io.on ("connection" , (socket ) => { socket.on ("userMsg" , (msg ) => { const userMsg = { name : "你" , text : msg, avatar : "https://api.dicebear.com/7.x/bottts/svg?seed=user" , }; world.history .push (userMsg); io.emit ("msg" , userMsg); const count = Math .floor (Math .random () * 2 ) + 1 ; let availableRoles = [...roleNames].sort (() => 0.5 - Math .random ()); for (let i = 0 ; i < count; i++) { const role = availableRoles[i]; setTimeout (() => aiReply (role), Math .random () * 2000 + 1000 ); } }); }); setInterval (() => { if (Math .random () < 0.7 ) return ; const role = roleNames[Math .floor (Math .random () * roleNames.length )]; aiReply (role); }, 8000 ); setInterval (() => { fs.writeFileSync ("./chatlog.json" , JSON .stringify (world, null , 2 )); }, 10000 ); server.listen (3000 , () => { console .log ("服务器启动成功: http://localhost:3000" ); });
可以看出这次的效果更像QQ群了
非常好,现在可以大胆尝试将角色替换为终末地的干员,这个非常非常容易,因为二游里每个角色语音还有还有人物设定都非常的详细
第七版本 这里为了方便管理,我想将每个角色的设定放到一个单独的文件,将人物集中在character文件夹里
这里我先放3个最具有代表性的角色—–佩丽卡,陈千语,汤汤
这里我提前编辑一下人物设定提示词
下次在搞吧,反正目前雏形已经出来了,以后再升级估计也没什么问题了
评论区