Skip to content

Repository files navigation

💌 哄哄模拟器

对象生气了怎么办?让 AI 演一个正在生气的 TA,你只有 10 轮选择题,把人哄回来。

Next.js React TypeScript Tailwind CSS shadcn/ui Postgres pnpm

哄哄模拟器 · 聊天界面

这是什么

一个用聊天界面包装的「情绪管理小游戏」: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 生成,纯属娱乐,请勿真的照着里面的选项说话 😉

后续想做的:战绩分享图、更多雷区场景、对局回放。

About

AI 扮演正在生气的 TA,你用 10 轮选择题把人哄回来|Next.js 16 + React 19 + Tailwind 4 + 豆包 LLM + TTS 语音

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages