之前看到一个UP制作了一个明日方舟的聊天APP

我下载了之后感觉非常有意思,用的是Deepseek的API,就是让明日方舟的角色和玩家聊天

不过这个APP界面很简陋,有点像早期的QQ界面

我产生了一个想法就是能不能制作一个终末地的APP,现在我看B站很多卖课的都在大力推荐Java,说实话,现在的APP基本上都是用的Kotlin来写的,而且谷歌官方也建议Kotlin

这个APP的界面可以像微信和QQ那样自定义聊天界面,然后聊天的角色就是每个角色提前设置好角色的语气和性格,还有记忆存储

当然肯定不是纯聊天,我看看能不能内置一些小游戏,比如斗地主,俄罗斯方块,还有五子棋,音游,模拟经营等等等等

UI和画风的话我可以模仿一下taptap里的一个二次元斗地主游戏的风格

image-20260317133301764

这里我设想的是尽量增加AI在游戏中的参与度,让玩家感觉自己被“真人陪伴”

比如斗地主就让AI角色和玩家玩,俄罗斯方块的话可以将俄罗斯方块和开心消消乐结合,五子棋的话就是将五子棋和galgame结合,

音游就是字面意思,可以搞一个人机PK(当然人类肯定打不过AI,所以需要给AI降智操作),模拟经营就是田园生存或者商店经营就像这个早年我喜欢玩的动物餐厅一样

image-20260317134254513

总之AI时代,没有做不到,只有想不到,反正借这个机会学学Android开发也不是不行

至于Unity的学习,以后再说吧—要不是Unity总是反复搞事搞得我不敢学这玩意了

思路是这样的

先制作聊天APP->再加入类似QQ的相册功能->再塞进去几个小游戏

所以重点就是聊天APP,我需要让AI更像真人,每个角色性格各异,会聊天,会说梗,会互相@,会发表情包,会水群,如果挂后台的话会自己聊天,玩家被@到时会像QQ微信一样消息提醒用户

image-20260317140426315

于是Chatgpt强烈反对我一上来就要造汽车这种方式,只要求从最简单的做起

ChatGPT的建议

这是你现在最应该走的路线 👍
我给你讲清楚:网页 → APP(手机 + 电脑)是完全可行的,而且是主流做法


一、网页真的可以变成APP吗?

👉 可以,有三种方式(从简单到专业)


① PWA(最推荐🔥)

👉 本质:网页伪装成APP

能做到:

  • 添加到手机桌面(像APP图标)
  • 全屏打开(没有浏览器UI)
  • 支持通知(部分)
  • 可离线(缓存)

📱 手机上的效果:

就像:

  • 微信里打开的小程序
  • 或一个简单APP

👉 用户基本分不出来


💻 电脑也可以:

  • Chrome / Edge → “安装应用”
  • 直接变成一个窗口应用

👉 适合你现在阶段:完美


② Electron(电脑端)

👉 本质:用浏览器包一层APP壳


例子(你肯定用过):

  • Visual Studio Code
  • Discord

👉 都是网页做的


能做到:

  • 真·桌面软件
  • 系统通知
  • 文件读写
  • 后台运行

👉 缺点:

  • 占内存大
  • 但开发简单

③ 原生APP(Android Studio)

👉 最正统,但最难

  • Android Studio

👉 适合后期,不适合你现在


二、你这个项目的最佳路线(直接照做就行)


🟢 阶段1(现在就做)

👉 做一个:

手机能打开的聊天网页

  • Vue / React
  • WebSocket
  • AI聊天

🟢 阶段2

👉 变成APP:

  • 加 PWA 支持
  • 手机 → 添加到桌面

🟢 阶段3(电脑)

👉 用 Electron 打包:

  • 直接变成PC客户端

🟢 阶段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 = ["阿刀", "小白", "老哥"];

// 废话库(先不用AI)
const texts = [
"笑死",
"?",
"你在干嘛",
"6",
"离谱",
"草",
"真的假的"
];

// API:返回一条消息
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;
}

// 每2秒请求一条消息
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

image-20260317143406490

然后点开这个html网页就可以看到

image-20260317143516562

但是这个是自定义的聊天内容,说来说去也就几句话,而且服务器跟假的一样

所以需要下载这个东西

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 });
});
});

// AI自动聊天
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>

然后启动服务器

1
node server.js

image-20260317144704135

image-20260317144923475

非常像了我去

然后就是接入AI公司的API实现聊天功能

首先下载这个来接入deepseek的api实现聊天功能

1
npm install openai

然后下载这个实现什么https连接什么的

1
npm install axios

然后再更新一下两个文件

第三版本

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);

// 📝 DeepSeek REST API 配置
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);

// 1️⃣ 显示用户消息
io.emit("msg", { name: "你", text: msg });

// 2️⃣ 随机选一个角色回复
const roleName = roleNames[Math.floor(Math.random() * roleNames.length)];

// 3️⃣ 构造 Prompt
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);
}
});
});

// ❌ 关闭自动水群,如果只想 AI 回答用户

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>

然后重新启动服务器

于是你就可以得到这几个魔丸了

image-20260317153932219

image-20260317154001599

image-20260317154019973

image-20260317154239967

image-20260317154814297

但是从对话你可以看出,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);

// ================= 🤖 DeepSeek =================
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);

// 👉 多AI回应(关键)
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

image-20260317215156731

image-20260317215214734

image-20260317215233292

image-20260317215307601

image-20260317215320856

image-20260317215342388

所以废话不多说,直接迭代下一版本

第五版本

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);

// ================= 🔑 API =================
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字
- 不要复读
- @概率很低
- 可以接话或换话题

你说:
`;
}

// ================= 🤖 AI =================
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;
}

image-20260317222610338

image-20260317222645089

image-20260317222720413

image-20260317222739035

image-20260317222757314

可以看到这次明显产生了质的飞跃,每个角色很明确自己是谁,也能记住自己说的话,交谈具有了一定的逻辑性,但是—-为什么角色都不理我呀我去,而且他们会纠结于一个无聊透顶的问题,而且说话非常无厘头,这点很不好,所以还要迭代

第六版本

这次需要所有人优先处理我的话,不能出现无视群主的现象

这次只需要修改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);

// ================= 🔑 API =================
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;
}

// 分离 System Prompt,利用好 DeepSeek 的角色扮演能力
function buildSystemPrompt(role) {
return `
${WORLD_SETTING}

当前你要扮演的角色是:【${role.name}
【你的设定】:${role.setting}
【你的说话风格】:${role.style}

任务:
根据群聊记录,以【${role.name}】的身份回复一句话。
要求:
1. 字数控制在15字以内,极简。
2. 绝对不带引号,绝对不要在开头加上自己的名字(如不要输出"阿刀:xxx")。
3. 如果“你”刚发了言,必须针对“你”的话进行回复!
`;
}

// ================= 🤖 AI =================
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();

// 过滤掉AI可能自己加的名字前缀,比如 "阿刀:别吵了" -> "别吵了"
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);

// 用户说话时,必定触发1~2个人马上回应
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); // 间隔拉长到8秒

// ================= 💾 存档 =================
setInterval(() => {
fs.writeFileSync("./chatlog.json", JSON.stringify(world, null, 2));
}, 10000);

// ================= 🚀 启动 =================
server.listen(3000, () => {
console.log("服务器启动成功: http://localhost:3000");
});

可以看出这次的效果更像QQ群了

image-20260317224927920

image-20260317224942416

image-20260317225001339

image-20260317225024042

image-20260317225113124

image-20260317225146411

image-20260317225207927

image-20260317225228693

非常好,现在可以大胆尝试将角色替换为终末地的干员,这个非常非常容易,因为二游里每个角色语音还有还有人物设定都非常的详细

第七版本

这里为了方便管理,我想将每个角色的设定放到一个单独的文件,将人物集中在character文件夹里

这里我先放3个最具有代表性的角色—–佩丽卡,陈千语,汤汤

这里我提前编辑一下人物设定提示词

下次在搞吧,反正目前雏形已经出来了,以后再升级估计也没什么问题了