【项目实战】第2篇:JavaScript 与 Node.js 教程——换个窗口就能作弊

系列:《用 AI 写出高质量代码:AI 狼人杀开发实录》 本篇对应版本:v1.2 → v1.3|时间:2026-10-22 中午
前言
上一篇把“假的 AI”删掉了,第一版终于每一句话都是真模型生成的。我挺得意,把链接发给自己,打算用另一个窗口“观战”一下。
然后我发现了一件更严重的事:我能看见所有人的身份,还能在控制台里改自己的牌。
问题是,这一版的代码没有一行是错的——它只是全部跑在了不该跑的地方。这一篇讲的就是这件事:为什么“代码写对了”还会被作弊,以及 JS 和 Node.js 到底差在哪。
引子:三个窗口,三种作弊方式
我先开了两个窗口:左边玩,右边观战。右边能看到 6 个座位的身份——因为是同一个浏览器,状态本来就共享。
接着我按了 F12。三条路,一条比一条简单:
- 刷新页面,进度没了。 因为整局状态存在内存里,页面一刷新就归零。
- 改内存,想赢就赢。 控制台里
game.players[0].role = 'wolf'之类的操作,对“跑在浏览器里的规则”来说就是上帝之手。 - 看网络面板,
prompt全在。 AI 的完整提示词、调用参数,每一条请求都明明白白列在那里。
第三点最要命:如果 prompt 在浏览器里拼,那“AI 该知道什么”这件事,就由玩家说了算。

图:v1.2.1 的「事件记录」面板。重构之后,这一屏的内容全部由服务端的事件流回放出来——重构之前,同样的信息是浏览器自己攒的。
问题:规则跑在“玩家的地盘”上
一句话:浏览器是玩家的地盘,服务器才是裁判。
凡是“不能让人改”的东西——身份分配、狼刀结果、投票计数、胜负判定——只要在浏览器里算,就等于把裁判席交给了对手。他改不了你的服务器,但他能改自己的浏览器。
这在单人游戏里看起来只是“自己作弊”,但它同时带来两个真实成本:
- 刷新丢进度:状态在内存,页面一重载就没了。
- AI 调用暴露:模型调用从浏览器直发,意味着提示词、参数、调用频率都在玩家手里。
所以重构的目标不是“加个后端”,而是:把“真相”从浏览器搬到服务器。
问题原因:JS 与 Node.js 到底差在哪

图:一条分界线。左边是玩家能改的一切——内存里的牌、请求参数、网络面板里的 prompt;右边只有一件事:服务器算出来的真相。判断标准只有一条——这条规则能不能被玩家改?能,就必须在右边。
很多人(包括当时的我)把“Node.js 就是服务器版的 JavaScript”当成一件事,其实是三件事被混在了一起:
- 语言:JavaScript,语法一模一样;
- 运行时:谁在执行这段代码——浏览器内置的引擎,还是你电脑上的 Node 程序;
- 能碰到的资源:有没有
DOM、能不能读写文件、能不能监听端口。
同样一段 function sum(a, b) { return a + b },两边跑结果一样。但一旦碰到“环境”,差别立刻出现:
| 维度 | 浏览器里的 JS | Node.js |
|---|---|---|
| 谁在执行 | 浏览器 JS 引擎 | 本机 Node 运行时 |
有没有 DOM | 有(document、window) | 没有,碰 document 直接报错 |
| 全局对象 | window | global |
| 能读写文件 | 不能(沙箱限制) | 可以(fs 模块) |
| 能监听端口 | 不能 | 可以(http.createServer) |
| 模块系统 | 由打包工具统一处理 | ESM(import)/ CJS(require)两种 |
| 装依赖 | — | npm + package.json + node_modules |
| 最终产物 | 打包好的静态资源(dist/) | 可直接运行的 .js(dist-server/) |
举个最小的对照。同一段逻辑,两边写法几乎一样,但“取数据”的方式天差地别:
// 浏览器里:数据来自页面或接口
const players = JSON.parse(localStorage.getItem('players') || '[]');
// Node.js 里:数据来自服务端自己维护的内存
import { readFile } from 'node:fs/promises';
const players = JSON.parse(await readFile('./data/players.json', 'utf8'));
解决方案:先定协议,再搬逻辑
这次我没有直接说“帮我改成服务端”。那种说法会让 AI 一次性重写所有文件,然后你不知道哪一步错了。
我的要求是这样的:
背景:
1、狼人杀网页版目前把规则、随机数、胜负判定都写在前端 `assets/ai.js` 里
目标:
1、把游戏逻辑整体搬到服务端,前端只保留渲染与交互
2、行为与现在完全一致(这是重构,不是重写)
流程:
1、先给接口协议:列出前端要调用的每个接口(方法、路径、请求体、返回体),不要动实现代码
2、我确认协议之后,你按协议搬逻辑
要求:
1、行为必须和现在完全一致,不许顺手优化逻辑
2、拿不准该放哪、或者要改对外行为的地方,列出来问我,不要自己决定
解析: 这段话里有三个“闸门”——
- “先给协议,不要动实现” → 把一次性大改拆成两次小改,出错可定位;
- “行为必须完全一致” → 禁止 AI 顺手优化(它最爱顺手优化);
- “拿不准先问我” → 把决策权收回自己手里。
结果就是现在这套目录:

图:重构后的两个簇。左边是规则与状态机,右边只负责画;中间那条虚线就是全部接触面——一次请求进去,一串事件出来。
还有一个关键设计:前端不再“算”,只“显示”。玩家的每一次操作都变成一次请求,服务端算完再把新状态推回来:
POST /api/games/:id/action { value: 3 } ← 我投票给 3 号
GET /api/games/:id/events?since=42 ← 拉取 42 之后的新事件
用轮询而不是
WebSocket,是个刻意的偷懒:云服务的数据库没有订阅能力,而一局游戏每秒最多几次事件,since游标轮询足够,代码量少一半。
效果:作弊的三条路,一起断了
改动上线后,我把当初那三种作弊方式重放了一遍,三条全断:
| 当初怎么作弊 | 现在的结果 |
|---|---|
| 刷新页面,看进度还在不在 | 进度不丢——状态在服务端,换个标签页打开也接着打 |
控制台改 players 里的牌 | 改不动——前端拿到的是投影,改自己那份不影响裁判 |
网络面板看 prompt | 看不到——prompt 在服务端拼,浏览器只收到文本 |
还白捡了两个能力:断线重连与多端同步。因为"状态在服务端"这一个决定,第 9 篇的观战几乎是顺手就有了。
内容小结
- 重构要拆成“先定协议、再搬实现”两步,并明确禁止 AI 顺手优化。一次大改比十次小改更容易失控。
- 搬到服务端不等于安全,安全取决于每个接口是否校验身份与权限。
- “代码看着对但不生效”时,先打印真实数据,别继续猜。这类问题往往不在代码里,在你不知道的那一层。
- 可迁移吗? 这是所有 AI 编程项目的必经之路:任何“需要权威状态”的应用(记账、审批、答题、下单),规则都必须放在服务端。
参考资料
Node.js官方文档:nodejs.org/api(重点看fs、http)- 项目仓库:
werewolf-ai的src/game、src/server、vite.config.ts - 上一篇:第1篇:AI 编程入门教程——AI 偷偷写了兜底,我还以为它能跑
- 下一篇:第3篇:
SDD规格驱动开发教程——越加功能,AI 越开始「编」
[评论 0]
评论加载中…