一个用聊天界面包装的「情绪管理小游戏」:AI 扮演正在气头上的男朋友 / 女朋友,你从每一轮现编的选项里挑一句去哄。好感度就藏在进度条里,从「炸毛红」到「融化桃」再到「原谅绿」,10 轮之内冲上 80 分就算哄好了。
全程中文、有语音、有撒花,也有心碎。游客开箱即玩,登录后才会计入排行榜。
| 手机端开局 | 第一轮对话 | 第二轮对话 |
|---|---|---|
![]() |
![]() |
![]() |
| 桌面端首页 | 恋爱攻略 | 排行榜 |
|---|---|---|
![]() |
![]() |
![]() |
截图取自本地真实运行,对话内容由模型实时生成。
- 开局就上头:5 个预设雷区——忘记纪念日 🎂 / 深夜不回消息 🎮 / 被发现和异性聊天 🤳 / 把对方的猫弄丢了 🐱 / 当众让对方没面子 😅,开场白就是一段火力全开的语音。
- 每轮 6 个选项,全部现编:不是固定题库,选项跟着上下文走,离谱但合理。
- 好感度数值隐藏:只看得到进度条和 TA 的表情,加几分、扣几分全靠猜。
- 发火也有层次:
-50~0冷暴力、0~30愿听、30~60软化、60~80快哄好、≥80原谅但还得保证——每一档都会进 prompt,语气逐级变化。 - 语音陪伴:温柔女声 / 霸道御姐 / 可爱软妹 / 低沉男声 / 温柔男声,五选一,每条消息都能点开听。
- 结局有仪式感:哄好撒花 + 心形飘落,翻车则心碎抖动,战况可一键复制分享。
- 恋爱攻略博客:让 AI 现写一篇恋爱沟通技巧并入库,库为空时自动并行生成种子文章。
- 账号 + 排行榜:用户名密码注册(bcrypt 哈希)、HttpOnly 会话 cookie、个人战绩页、按历史最高分取前 20 名。
- 游客不打扰:不登录照样玩,只是成绩不落库、不上榜。
选择对象 / 场景 / 音色
│
▼
POST /api/turn ──► LLM(情绪档位 + 历史对话 + 本轮选择 一起塞进 prompt)
│ │
│ └─► 严格返回 { delta, reply, options[6] }
▼
好感度进度条 / 气泡 / 加减分飞字
│
▼
POST /api/tts ──► 服务端拉取音频字节 ──► base64 data URL ──► 浏览器直接播
几个关键设计:
- 数值藏在服务端逻辑里:前端只拿
delta播动画,界面上永远只显示总分。 - 情绪分层写进 prompt:每轮把当前好感度落到最近档位塞进 system prompt,模型不用猜现在是第几阶段。
- 结构化输出优先:
{delta, reply, options}必须是合法 JSON,解析前先剥离 markdown 代码围栏,失败有兜底内容,不白屏。 - 胜负有两条线:10 轮内好感度
≥ 80通关;< -50或 10 轮打完仍不到 80 就是翻车。
| 层 | 选型 |
|---|---|
| 框架 | Next.js 16(App Router)+ React 19 |
| 语言 | TypeScript 5(strict 心智,不用 any) |
| UI | shadcn/ui(Radix UI)+ Tailwind CSS 4 |
| 模型 | 豆包 doubao-seed-2-0-lite-260215,走 coze-coding-dev-sdk 的 LLMClient |
| 语音 | TTS 合成后由服务端拉字节转 data URL |
| 数据库 | Postgres + drizzle-orm(pg 直连连接池) |
| 鉴权 | 自建用户名密码 + bcryptjs + sha256 会话 token |
| 包管理 | pnpm 9(唯一指定,npm / yarn 会被 preinstall 拦下) |
src/
├── app/
│ ├── api/
│ │ ├── turn/ # 每轮核心:调 LLM 生成 delta / reply / options
│ │ ├── tts/ # 语音合成,返回 base64 data URL
│ │ ├── auth/ # register / login / logout / me
│ │ ├── blog/ # 列表 + 详情 + AI 生成新文章
│ │ ├── game/records/ # 个人战绩读写
│ │ └── leaderboard/ # 排行榜聚合
│ ├── blog/ # 恋爱攻略列表 / 详情
│ ├── leaderboard/ # 排行榜页
│ ├── login/ register/ me/# 登录、注册、我的记录
│ └── globals.css # 微信气泡、打字动画、撒花 / 心碎动效
├── components/ # 游戏主界面、博客、排行榜、导航等业务组件
│ └── ui/ # shadcn/ui 基础组件
├── lib/
│ ├── game.ts # 领域模型:场景、音色、阈值、prompt 组装、JSON 解析
│ ├── llm.ts # 共享 LLM 调用助手(透传鉴权头)
│ ├── auth-db.ts # 用户 / 会话表读写与 token 哈希
│ ├── blog*.ts # 博客类型、CRUD、种子文章
│ └── session.ts # 从 cookie 取当前登录用户
└── storage/database/ # drizzle schema 与 pg 连接
pnpm install
cp .env.example .env.local # Windows: Copy-Item .env.example .env.local
pnpm dev # 打开 http://localhost:5000.env.local 需要的变量:
| 变量 | 说明 |
|---|---|
DATABASE_URL |
Postgres 连接串(drizzle 直接用它建连接池) |
COZE_WORKLOAD_IDENTITY_API_KEY |
扣子编程工作负载身份密钥,供模型 / 语音网关鉴权 |
COZE_INTEGRATION_BASE_URL |
集成服务网关地址 |
COZE_INTEGRATION_MODEL_BASE_URL |
模型网关地址 |
模型调用的鉴权靠请求头透传(
HeaderUtils.extractForwardHeaders+new Config()),不要把密钥硬编码进代码。
常用脚本:
pnpm dev # 开发服务器(tsx watch src/server.ts)
pnpm build # next build + tsup 打包 server
pnpm start # 跑生产构建产物
pnpm ts-check # TypeScript 检查
pnpm validate # ts-check + eslint + stylelint数据库表结构定义在 src/storage/database/shared/schema.ts,改表后用 coze-coding-ai db upgrade 同步、coze-coding-ai db generate-models 拉回本地类型。
| 接口 | 方法 | 作用 |
|---|---|---|
/api/turn |
POST | 传入 {gender, scenarioId, affection, history, chosen, isFirst},返回 {delta, reply, options[6]} |
/api/tts |
POST | 传入 {text, voice},返回可直接播放的音频 data URL |
/api/auth/register |
POST | 注册并自动登录(用户名 2-30 位,密码 6-72 位) |
/api/auth/login |
POST | 登录,种 30 天 HttpOnly cookie |
/api/auth/logout |
POST | 删会话并清 cookie |
/api/auth/me |
GET | 返回当前登录用户或 {user: null} |
/api/blog |
GET | 文章列表(库空时自动生成种子文章) |
/api/blog/[id] |
GET | 文章详情 |
/api/blog/generate |
POST | 让 AI 新写一篇并入库 |
/api/game/records |
GET / POST | 我的战绩列表 / 保存一局结果(未登录返回 401 或空数组) |
/api/leaderboard |
GET | 每人取最高分、前 20 名,并标记 isMe |
| 表 | 关键字段 | 说明 |
|---|---|---|
users |
id / username / password |
密码只存 bcrypt 哈希,永不回传 |
sessions |
token_hash / user_id / expires_at |
只存 sha256(token),库泄漏也拿不到裸 token |
blog_posts |
id / title / summary / content |
恋爱攻略文章,LLM 生成后入库 |
game_records |
user_id / scenario / final_score / result |
登录用户每局结果,排行榜数据源 |
- LLM 的 messages 必须以
user结尾:只给 system 会直接抛错,所以每次都是[system, user]成对传。 - 结构化输出要防脏:解析前先剥 markdown 代码围栏,再兜底,避免模型多说两句话就整局崩。
- 会话安全:cookie 30 天、HttpOnly、SameSite=Lax;服务端只留 token 的 sha256;退出即删会话行。
- 排行榜不做 SQL join:先按分数排序、应用层按用户去重,再批量补用户名,比依赖外键推断的跨表 join 稳得多。
- TTS 走服务端转 data URL:直接回源链接会踩 CORS 和链接过期,转成 base64 一次解决。
- Hydration 安全:涉及
Date.now()/Math.random()的内容一律use client+useEffect后再渲染。
深夜宿舍的书桌、暖光小台灯、塑料手机屏上的微信气泡——配色锚在奶油白 #FAF6F0、炸毛红 #F87171、软化橙 #FBBF24、原谅绿 #34D399 和巧克力棕 #4A3A2E 上。气泡从下往上滑入,加减分做上浮飞字,等待时是三点呼吸打字动画。原则只有一条:生气的台词也要可爱,绝不刻薄。
想看得更细可以翻 DESIGN.md。
- 项目在扣子编程(Coze Code)环境里开发与部署,pnpm 是唯一包管理器。
- 全部对话与文章由 AI 生成,纯属娱乐,请勿真的照着里面的选项说话 😉
后续想做的:战绩分享图、更多雷区场景、对局回放。





