From 3b7c5c85f5f23955792047da27d1fae858f03e9e Mon Sep 17 00:00:00 2001 From: pmc <740076875@qq.com> Date: Mon, 27 Apr 2026 17:06:21 +0800 Subject: [PATCH] feat: update device interaction views, docs, and CLAUDE.md - Update device_interaction views - Update admin README and CLAUDE.md - Add affinity system design doc - Add device chat record subtitle storage scheme doc Co-Authored-By: Claude Sonnet 4.6 --- docs/好感度系统功能与规则设计.md | 313 ++++++++++++++++ qy-lty-admin/README.md | 333 ++++++++++++----- qy_lty/CLAUDE.md | 457 ++++++++++++----------- qy_lty/device_interaction/views.py | 101 ++++- qy_lty/docs/设备聊天记录_字幕落库方案.md | 215 +++++++++++ 5 files changed, 1103 insertions(+), 316 deletions(-) create mode 100644 docs/好感度系统功能与规则设计.md create mode 100644 qy_lty/docs/设备聊天记录_字幕落库方案.md diff --git a/docs/好感度系统功能与规则设计.md b/docs/好感度系统功能与规则设计.md new file mode 100644 index 0000000..4370d93 --- /dev/null +++ b/docs/好感度系统功能与规则设计.md @@ -0,0 +1,313 @@ +# 好感度系统功能与规则设计 + +> 文档范围:本文档描述**洛天依管理系统**中「好感度系统」的全部功能模块与规则设计,用于产品对齐、联调对接与后续服务端落地。 +> +> 当前状态:管理后台 UI 已完成(见 [qy-lty-admin/app/affinity/page.tsx](../qy-lty-admin/app/affinity/page.tsx)),后端模型骨架已就绪(见 [qy_lty/userapp/models.py](../qy_lty/userapp/models.py) 第 79–115 行),但前端尚未实际调用真实接口,设备端/手机端事件尚未接入好感度逻辑。 + +--- + +## 一、系统定位 + +好感度系统用于刻画**用户与洛天依**之间的亲密度关系。其核心价值: + +- 让用户的互动行为产生**持续、可感知的数值反馈** +- 通过**等级 + 解锁内容**形成长期陪伴动机 +- 对不活跃用户通过**衰减机制**保留流失预警与召回空间 + +好感度是一个 `[0, max_affinity]` 区间的整数(默认上限 100),每个用户独立维护,记录在 `ParadiseUser.favorability` 字段。 + +--- + +## 二、功能模块总览 + +管理后台分 4 个标签页,对应 5 块功能: + +| 模块 | 页签 | 功能说明 | +|---|---|---| +| 系统概览 | 系统概览 | 关键指标卡片 + 全局基础参数设置 | +| 互动规则 | 互动规则 | 管理各类互动行为的好感度变化规则 | +| 衰减规则 | 互动规则页下半 | 配置不活跃用户的好感度衰减策略 | +| 等级奖励 | 等级奖励 | 管理好感度等级划分与奖励发放 | +| 数据统计 | 数据统计 | 好感度分布、互动分析、趋势监控 | + +--- + +## 三、模块 1 — 系统概览与全局参数 + +### 3.1 关键指标卡片 + +| 指标 | 含义 | 数据源 | +|---|---|---| +| 平均好感度 | 所有用户 `favorability` 平均值 | 聚合 `ParadiseUser` | +| 最高好感度 | 系统中达到上限的用户数 | `favorability = max_affinity` 计数 | +| 互动次数/日 | 当天触发的所有规则次数总和 | 聚合 `AffinityLog`(待建) | +| 活跃用户比例 | 近 N 天有互动的用户占比 | 聚合用户活跃状态 | + +### 3.2 全局参数(AffinitySetting) + +| 字段 | 默认值 | 说明 | +|---|---|---| +| `initial_affinity` | 10 | 新用户创建时的初始好感度 | +| `max_affinity` | 100 | 好感度上限(所有规则累计不会超过此值) | +| `daily_cap` | 20 | 单用户每日好感度**净增长**上限(跨规则汇总) | +| `decay_rate` | 2 点/天 | 全局默认衰减速率(衰减规则可覆盖) | +| `decay_threshold` | 3 天 | 不互动多少天后开始衰减 | +| `enable_notify` | true | 好感度变化是否推送通知 | +| `enable_rewards` | true | 是否启用等级奖励发放 | + +**规则**: +- `initial_affinity` 只影响**新用户**,不回溯修改已有用户 +- `max_affinity` 调低后,**已超过的用户保留原值**,但不再增加 +- `daily_cap` 是**跨所有正向规则**的顶层限制,触达后当日所有增益无效(衰减不受此限) + +--- + +## 四、模块 2 — 互动规则 + +### 4.1 规则字段定义 + +| 字段 | 类型 | 示例 | 说明 | +|---|---|---|---| +| `id` | string | `rule-2` | 规则唯一 ID | +| `name` | string | `对话` | 规则展示名 | +| `type` / `rule_key` | enum | `chat` | **代码标识**,服务端事件通过它匹配规则,不可重复 | +| `description` | string | `与洛天依进行对话` | 展示描述 | +| `minChange` | int | 1 | 单次好感度变化最小值 | +| `maxChange` | int | 5 | 单次好感度变化最大值(最终取 `[min, max]` 的随机整数) | +| `singleCap` | int | 5 | 单次变化绝对值上限(保护性钳位,防止数据异常) | +| `dailyCap` | int | 15 | **本规则**每日累计变化上限(绝对值) | +| `isNegative` | bool | false | 是否负向规则。正向用正数,负向用负数 | +| `isEnabled` | bool | true | 是否启用,禁用后事件不触发 | + +### 4.2 默认规则集(8 条) + +| rule_key | 名称 | 范围 | 单次/日 | 正负 | 触发来源 | +|---|---|---|---|---|---| +| `card` | 使用卡片 | +1 ~ +3 | 3 / 10 | 正 | 手机端(使用卡片 API) | +| `chat` | 对话 | +1 ~ +5 | 5 / 15 | 正 | 设备端 + 手机端(聊天消息) | +| `feed` | 喂食 | +2 ~ +8 | 8 / 16 | 正 | 手机端(喂食动作) | +| `touch` | 抚摸 | +1 ~ +3 | 3 / 9 | 正 | 手机端 / 设备端(抚摸信号) | +| `dress` | 换装 | +2 ~ +6 | 6 / 12 | 正 | 手机端(换装 API) | +| `prop` | 使用道具 | +1 ~ +4 | 4 / 12 | 正 | 手机端(道具使用) | +| `gift` | 送礼物 | +5 ~ +15 | 15 / 20 | 正 | 手机端(赠礼 API) | +| `decay` | 无互动衰减 | −1 ~ −3 | 3 / 5 | 负 | 定时任务 | + +### 4.3 触发计算流程 + +``` +收到事件 (user_id, rule_key, 来源上下文) + │ + ▼ +① 取规则 → 若 is_enabled=false,丢弃 + │ + ▼ +② 冷却检查(Redis:affinity:cd:{user}:{rule_key}) + │ 未到冷却 → 丢弃 + ▼ +③ 本规则日上限检查(affinity:daily:{user}:{rule_key}:{date}) + │ 已满 → 丢弃 + ▼ +④ 全局日上限检查(正向事件才检查) + │ 已满 → 丢弃 + ▼ +⑤ 计算变化值 = random(min, max) + ↓ 按 single_cap 钳位 + │ + ▼ +⑥ 原子更新 ParadiseUser.favorability + ↓ 钳位到 [0, max_affinity] + │ + ▼ +⑦ 写 AffinityLog、更新计数器 + │ + ▼ +⑧ 判断是否跨越等级边界 + │ 是 → 触发等级变更事件(发奖励 + 推通知) + ▼ +⑨ 通过 WebSocket 向用户的所有在线端推送 affinity_update +``` + +### 4.4 规则设计约定 + +- **单一写入入口**:所有好感度变化必须经由服务端统一入口,客户端不能直接增减。 +- **rule_key 即契约**:客户端事件不携带分值,只报「我触发了 `gift` 规则」,具体加多少由服务端按规则算。规则可被管理员随时调整,客户端无需改动。 +- **幂等防护**:同一 `rule_key` + 同一设备事件 ID 在冷却窗口内只生效一次,防抖防重复。 +- **禁用规则的兜底**:管理员禁用某规则后,客户端若继续上报该事件,服务端静默丢弃(不报错、不扣冷却)。 + +--- + +## 五、模块 3 — 衰减规则 + +衰减是**本质为 `rule_key=decay` 的负向规则**,但由于业务语义特殊,在后台独立配置。 + +### 5.1 衰减字段 + +| 字段 | 默认值 | 说明 | +|---|---|---| +| `decay_start_days` | 3 | 不互动多少天后开始衰减 | +| `decay_rate_per_day` | 2 点/天 | 平均每日衰减点数 | +| `min_decay` | 1 | 单日衰减最小值 | +| `max_decay` | 3 | 单日衰减最大值 | +| `decay_cap` | 5 点/天 | 单日衰减上限(保护性) | +| `min_floor` | 0 | **衰减下限**,好感度不会低于此值 | +| `notify_decay` | true | 是否通知用户「好感度下降了」 | + +### 5.2 衰减执行 + +- **频次**:每日 00:30 由定时任务统一跑一次 +- **命中对象**:`last_active_at < now - decay_start_days` 的用户 +- **落库**:衰减也写 `AffinityLog`,`source='system_decay'` +- **下限保护**:若用户当前好感度 ≤ `min_floor` 则跳过 +- **与互动的关系**:用户当天有互动即**重置不活跃计数**,次日不衰减 + +### 5.3 设计权衡 + +- 衰减**不占用** `daily_cap` 全局日上限(因为它是扣减,不是增益) +- 衰减日志会产生大量记录,可考虑按天合并写一条,减少 `AffinityLog` 膨胀 +- 若「当日互动」和「当日衰减」同时命中,先执行衰减再执行互动(让用户感受到「我回来了 → 好感度止跌回升」) + +--- + +## 六、模块 4 — 等级奖励 + +### 6.1 等级字段 + +| 字段 | 示例 | 说明 | +|---|---|---| +| `level` | 3 | 等级序号,唯一 | +| `name` | 熟悉 | 等级名 | +| `minAffinity` / `maxAffinity` | 41 / 60 | 等级好感度区间(闭区间) | +| `unlockContent` | `更多服装、特殊对话` | 文案描述,前端展示 | +| `rewardType` | `unlock` / `item` / `currency` / `mixed` | 奖励类型 | +| `rewardCurrency` | 100 | 虚拟货币数量(rewardType 含 currency 时生效) | +| `rewardItems` | `[{item_id, qty}]` | 道具列表(rewardType 含 item 时生效) | +| `isEnabled` | true | 是否启用该等级 | + +### 6.2 默认等级(5 档) + +| 等级 | 名称 | 区间 | 解锁内容 | +|---|---|---|---| +| 1 | 初识 | 0 ~ 20 | 基础对话功能 | +| 2 | 相识 | 21 ~ 40 | 基础服装、道具使用 | +| 3 | 熟悉 | 41 ~ 60 | 更多服装、特殊对话 | +| 4 | 亲密 | 61 ~ 80 | 限定服装、特殊互动 | +| 5 | 挚友 | 81 ~ 100 | 专属内容、特殊剧情 | + +### 6.3 等级变化规则 + +- 等级由好感度区间**自动映射**,不是独立字段 +- **跨级判定**:每次好感度变动后,取当前值所属区间,与上一次等级比较 + - 升级 → 发放目标等级的奖励(**只发最终落点等级**,跳级不补发中间等级) + - 降级(衰减导致) → 不追回奖励,但取消解锁内容访问权限 +- **奖励幂等**:同一用户同一等级的「首次达到奖励」只发一次;降级后再升级不重复发放 + +### 6.4 区间约束 + +- 区间**不得重叠**,管理端保存时做校验 +- 区间**不得有空隙**(如等级 2 的 max=40,则等级 3 的 min 必须是 41) +- 最低等级 `min=0`,最高等级 `max=max_affinity` + +--- + +## 七、模块 5 — 数据统计 + +### 7.1 概览指标 +- 平均好感度 / 中位数 / 最高好感度 +- 活跃用户数(近 7 日有互动) +- 今日互动次数、今日新增好感度总量 + +### 7.2 分布分析 +- 好感度区间分布(0-20、21-40…) +- 各等级用户数占比 +- 各互动规则触发频次 Top N + +### 7.3 趋势分析 +- 日/周/月的平均好感度变化曲线 +- 日互动量趋势 +- 衰减命中用户数趋势 + +### 7.4 用户级查询 +- 按用户 ID 查询其好感度当前值、等级、近期变化日志 +- 管理员手动调整(加减好感度,走 `source='admin_adjust'` 记 log) + +--- + +## 八、多端触发点一览 + +好感度变化可由以下端点触发,所有端都走**同一个服务端入口**: + +| 触发来源 | 规则 | 通道 | 位置参考 | +|---|---|---|---| +| 设备端上报「用户发起对话」 | `chat` | WebSocket | [device_interaction/consumers.py](../qy_lty/device_interaction/consumers.py) `chat_message` | +| 设备端对话结束(陪伴时长) | `chat` 或独立 `companion_time` | WebSocket | `conversation_status` 的 begin/end | +| 手机端点击「唱歌/跳舞/抚摸」 | 对应 rule_key | WebSocket | consumers.py `sing` / `dance` / `touch` | +| 手机端赠礼 / 喂食 / 换装 / 用道具 | 对应 rule_key | HTTP | 对应业务 ViewSet 钩子 | +| 管理员手动调整 | 无 rule | HTTP Admin API | 管理后台 | +| 衰减定时任务 | `decay` | 后台任务 | 定时调度 | + +**身份识别**: +- 设备端:MAC 登录获取 token → 服务端通过 `UserDevice` 找到 `user_id` +- 手机端:Redis token 认证 → 直接拿到 `user_id` +- 服务端只认 `user_id`,与触发端无关 + +--- + +## 九、数据契约(接口层摘要) + +### 9.1 管理端 + +| 接口 | 方法 | 用途 | +|---|---|---| +| `/api/admin/affinity/rules/` | GET/POST/PATCH/DELETE | 互动规则 CRUD | +| `/api/admin/affinity/levels/` | GET/POST/PATCH/DELETE | 等级 CRUD | +| `/api/admin/affinity/settings/` | GET/PUT | 全局参数(单例) | +| `/api/admin/affinity/logs/` | GET | 变化日志查询(可按 user、rule、时间过滤) | +| `/api/admin/affinity/stats/` | GET | 统计聚合 | +| `/api/admin/affinity/adjust/` | POST | 管理员手动调整(必须留审计) | + +### 9.2 客户端(手机端 / 设备端共用) + +| 接口 | 方法 | 用途 | +|---|---|---| +| `/api/user/me/affinity/` | GET | 当前好感度、等级、下一级进度、近期变化 | +| `/api/user/me/affinity/claim-reward/` | POST | 领取等级奖励 | + +### 9.3 WebSocket 实时推送 + +| 事件 | 方向 | payload | +|---|---|---| +| `affinity_update` | 服务端 → 用户 | `{change, before, after, rule_key, source}` | +| `level_up` | 服务端 → 用户 | `{old_level, new_level, reward}` | +| `level_down` | 服务端 → 用户 | `{old_level, new_level}`(衰减导致降级) | + +--- + +## 十、待开发拼板 + +| 模块 | 前端 | 后端模型 | 后端接口 | 触发埋点 | +|---|---|---|---|---| +| 互动规则 CRUD | ✅ UI 完成 | ⚠️ 缺字段 | ⚠️ 需扩展 | — | +| 等级 CRUD | ✅ UI 完成 | ⚠️ 缺字段 | ⚠️ 需扩展 | — | +| 全局设置 | ✅ UI 完成 | ❌ 无表 | ❌ 无接口 | — | +| 衰减配置 | ✅ UI 完成 | ❌ 无表 | ❌ 无接口 | ❌ 无定时任务 | +| 变化日志 | ❌ 无 UI | ❌ 无表 | ❌ 无接口 | — | +| 数据统计 | ⚠️ Mock 展示 | — | ❌ 无聚合接口 | — | +| 客户端查询 | — | — | ❌ 无接口 | — | +| WS 实时推送 | — | — | — | ❌ 未接入 | +| 设备/手机事件埋点 | — | — | — | ❌ 未接入 | + +图例:✅ 已完成 ⚠️ 部分完成 ❌ 未开始 + +--- + +## 十一、术语表 + +| 术语 | 含义 | +|---|---| +| rule_key | 互动规则的代码级标识(如 `chat`),客户端事件通过它匹配规则 | +| single_cap | 单次变化绝对值上限(保护性钳位) | +| daily_cap | 单规则每日累计变化上限 | +| 全局日上限 | 跨所有正向规则的顶层日增长上限(`AffinitySetting.daily_cap`) | +| 冷却 | 同一用户同一规则的最小触发间隔 | +| source | 变化来源:`device_event` / `mobile_event` / `system_decay` / `admin_adjust` | +| 跨级 | 好感度变化使得用户从一个等级区间移动到另一个 | diff --git a/qy-lty-admin/README.md b/qy-lty-admin/README.md index b11d433..28071b0 100644 --- a/qy-lty-admin/README.md +++ b/qy-lty-admin/README.md @@ -1,137 +1,266 @@ -# 洛天依应用管理后台 +# 洛天依应用管理后台 (qy-lty-admin) -这是一个基于 Next.js 和 React 构建的洛天依应用管理后台系统,提供完整的管理功能,包括用户管理、AI模型管理、卡牌管理、内容管理等功能。 +洛天依 APP 配套的 Web 管理后台,基于 **Next.js 15 (App Router) + React 19 + TypeScript** 构建,搭配 Tailwind CSS 与 Radix UI/shadcn 风格组件库,为运营与研发团队提供用户、AI 模型、卡牌、内容、权限等一体化的管理能力。后端 API 由 [qy_lty](../qy_lty/) 服务器项目统一提供(位于 `C:\Users\admin\Desktop\Lila-Server\qy_lty`)。 -## 功能特点 +--- -- **用户管理**:查看用户数据、活跃用户统计 -- **AI模型管理**:大模型框架系统、模型微调、语音克隆与合成、知识库管理 -- **卡牌管理**:服装卡牌、道具卡牌、家居装饰卡牌、食物卡牌 -- **内容管理**:歌曲管理、舞蹈管理、好感度系统、成就系统 -- **数据分析**:用户活跃度统计、系统运行概览 -- **安全认证**:用户登录/注册系统 +## 目录 + +- [功能概览](#功能概览) +- [技术栈](#技术栈) +- [权限矩阵](#权限矩阵) +- [快速开始](#快速开始) +- [环境变量](#环境变量) +- [项目结构](#项目结构) +- [鉴权与路由保护](#鉴权与路由保护) +- [API 集成](#api-集成) +- [构建与部署](#构建与部署) +- [浏览器支持](#浏览器支持) +- [许可证](#许可证) + +--- + +## 功能概览 + +根据左侧导航及 [lib/permissions.ts](lib/permissions.ts) 中声明的权限模块,后台分为三大功能分组: + +### AI 管理 +- **大模型管理** `/ai-model` — 大模型框架接入、微调、语音克隆与合成、知识库管理入口 + +### 内容管理 +- **服装管理** `/outfits` +- **道具管理** `/props` +- **家居装饰管理** `/home-decor` +- **食物管理** `/food` +- **歌曲管理** `/songs` +- **舞蹈管理** `/dances` +- **成就管理** `/achievements` +- **好感度系统** `/affinity` + +### 系统管理 +- **用户管理** `/users` — 用户列表、检索、创建/编辑/删除、登录历史 +- **权限管理** `/permissions` — 角色与模块访问控制 +- **系统设置** `/settings` + +### 仪表盘 +首页 [app/page.tsx](app/page.tsx) 汇总关键指标(总用户、活跃用户、卡牌激活、对话次数)、系统概览图表与最近活动,并提供主要业务模块的快捷入口。 + +### 账户 +- 登录 `/login`、注册 `/register`、忘记密码 `/forgot-password` + +--- ## 技术栈 -- [Next.js](https://nextjs.org/) - React 框架 -- [React](https://reactjs.org/) - UI 库 -- [Tailwind CSS](https://tailwindcss.com/) - 样式框架 -- [Radix UI](https://www.radix-ui.com/) - 无障碍组件库 -- [Lucide React](https://lucide.dev/) - 图标库 -- [Recharts](https://recharts.org/) - 图表库 +| 类别 | 选型 | +|------|------| +| 框架 | [Next.js 15.2.4](https://nextjs.org/)(App Router、standalone 输出)| +| 语言 | TypeScript 5 | +| UI 库 | [React 19](https://react.dev/) | +| 样式 | [Tailwind CSS 3.4](https://tailwindcss.com/) + `tailwindcss-animate` | +| 组件 | [Radix UI](https://www.radix-ui.com/) + shadcn 风格封装(见 [components/ui/](components/ui/))| +| 图标 | [Lucide React](https://lucide.dev/) | +| 图表 | [Recharts](https://recharts.org/) | +| 表单 | [React Hook Form](https://react-hook-form.com/) + [Zod](https://zod.dev/) + `@hookform/resolvers` | +| HTTP 客户端 | [Axios](https://axios-http.com/)(含请求/响应拦截器)| +| 通知 | [Sonner](https://sonner.emilkowal.ski/) + Radix Toast | +| 主题 | [next-themes](https://github.com/pacocoursey/next-themes) | +| 其他 | `js-cookie`、`date-fns`、`react-day-picker`、`embla-carousel-react`、`cmdk`、`vaul` 等 | +| 包管理 | 兼容 npm / yarn / pnpm(Dockerfile 使用 yarn + 淘宝镜像源)| -## 安装与运行 +--- -### 前提条件 +## 权限矩阵 -- Node.js 22.x 或更高版本 -- npm 或 yarn 或 pnpm +基于角色 (RBAC) 控制模块访问,定义位于 [lib/permissions.ts](lib/permissions.ts)。角色信息存储在 `localStorage.user_role`,由侧边栏 [components/sidebar.tsx](components/sidebar.tsx) 在客户端根据角色过滤可见菜单。 -### 安装步骤 +| 模块 / 角色 | 超级管理员 | 内容管理员 | AI模型管理员 | 卡牌管理员 | 查看者 | +|------------|:----------:|:----------:|:------------:|:----------:|:------:| +| 仪表盘 | ✓ | ✓ | ✓ | ✓ | ✓ | +| 用户管理 | ✓ | | | | | +| 权限管理 | ✓ | | | | | +| AI 模型管理 | ✓ | | ✓ | | | +| 服装 / 道具 / 家居 / 食物 | ✓ | ✓ | | ✓ | | +| 歌曲 / 舞蹈 / 成就 / 好感度 | ✓ | ✓ | | | | +| 系统设置 | ✓ | | | | | -1. 克隆仓库 -```bash -git clone -cd admin-dashboard -``` +辅助 API:`getUserRole()`、`getAllowedModules()`、`hasPermission(module)`、`hasPathPermission(pathname)`。 -2. 安装依赖 -```bash -npm install -# 或 -yarn -# 或 -pnpm install -``` +--- -3. 启动开发服务器 -```bash -npm run dev -# 或 -yarn dev -# 或 -pnpm dev -``` +## 快速开始 -4. 打开浏览器访问 http://localhost:3000 +### 环境要求 +- Node.js **22.x** 及以上(与 Dockerfile 中的 `node:22.10.0-alpine` 对齐) +- 包管理器:npm / yarn / pnpm 任意其一 +- 可访问的 Lila-Server API 服务 -## 构建与部署 +### 本地开发 ```bash -# 构建项目 -npm run build -# 或 -yarn build -# 或 -pnpm build +# 1. 安装依赖 +pnpm install # 或 yarn / npm install -# 启动生产环境服务器 -npm run start -# 或 -yarn start -# 或 -pnpm start +# 2. 配置环境变量 +cp .env.example .env.local +# 编辑 .env.local,填入 NEXT_PUBLIC_API_BASE_URL + +# 3. 启动开发服务器(默认 http://localhost:3000) +pnpm dev # 或 yarn dev / npm run dev ``` -## 项目结构 +### 可用脚本 + +| 命令 | 说明 | +|------|------| +| `next dev` | 启动开发服务器(HMR)| +| `next build` | 生产构建(启用 `webpackBuildWorker` 与并行编译)| +| `next start` | 启动生产服务器 | +| `next lint` | ESLint 检查(构建时已通过 `ignoreDuringBuilds` 忽略)| + +> 注意:[next.config.mjs](next.config.mjs) 中 `eslint.ignoreDuringBuilds` 与 `typescript.ignoreBuildErrors` 均为 `true`,构建不会因 lint / 类型错误而失败,务必在本地执行 `next lint` 与 `tsc --noEmit` 保证质量。 + +--- + +## 环境变量 + +采用 Next.js 标准的环境变量机制,加载优先级(从低到高): ``` -admin-dashboard/ -├── app/ # Next.js 应用程序目录 -│ ├── ai-model/ # AI模型管理相关页面 -│ ├── achievements/ # 成就系统页面 -│ ├── affinity/ # 好感度系统页面 -│ ├── dances/ # 舞蹈管理页面 -│ ├── food/ # 食物卡牌管理页面 -│ ├── home-decor/ # 家居装饰卡牌管理页面 -│ ├── login/ # 登录页面 -│ ├── outfits/ # 服装卡牌管理页面 -│ ├── permissions/ # 权限管理页面 -│ ├── props/ # 道具卡牌管理页面 -│ ├── register/ # 注册页面 -│ ├── settings/ # 系统设置页面 -│ ├── songs/ # 歌曲管理页面 -│ └── users/ # 用户管理页面 -├── components/ # React 组件 -├── hooks/ # 自定义 React hooks -├── lib/ # 工具函数和辅助库 -├── public/ # 静态资源 -└── styles/ # 全局样式 +.env → .env.development / .env.production → .env.local ``` -## 浏览器支持 +暴露给浏览器的变量必须以 `NEXT_PUBLIC_` 前缀声明。 -- Chrome (最新版本) -- Firefox (最新版本) -- Safari (最新版本) -- Edge (最新版本) +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `NEXT_PUBLIC_API_BASE_URL` | 后端 API 基础地址,被 [lib/api/client.ts](lib/api/client.ts) 的 axios 实例使用 | `http://localhost:8000/api` | -## 许可证 - -[MIT](LICENSE) - -## Environment Configuration - -This project uses environment variables for configuration across different environments. The configuration files are: - -- `.env` - Base environment variables (lowest priority) -- `.env.development` - Development environment variables (used with `next dev`) -- `.env.production` - Production environment variables (used with `next start`) -- `.env.local` - Local overrides for any environment (highest priority) - -### Setup - -1. Copy the example files to create your environment configuration: +初始化示例(存在 `.env.example` / `.env.local.example` 时): ```bash cp .env.example .env cp .env.local.example .env.local ``` -2. Update the variables in these files as needed for your environment. +--- -3. Environment variables that need to be exposed to the browser should be prefixed with `NEXT_PUBLIC_`. +## 项目结构 -### Environment Variables +``` +qy-lty-admin/ +├── app/ # Next.js App Router 页面 +│ ├── page.tsx # 仪表盘首页 +│ ├── layout.tsx # 根布局 +│ ├── globals.css # 全局样式 +│ ├── login/ register/ forgot-password/ +│ ├── ai-model/ # AI 模型管理 +│ ├── outfits/ props/ home-decor/ food/ # 卡牌管理 +│ ├── songs/ dances/ achievements/ affinity/ # 内容管理 +│ ├── users/ permissions/ settings/ # 系统管理 +├── components/ +│ ├── ui/ # shadcn 风格的 Radix UI 封装 +│ ├── sidebar.tsx # 带权限过滤的侧边栏 +│ ├── dashboard-shell.tsx # 后台外壳布局 +│ ├── dashboard-header.tsx # 页头 +│ ├── overview.tsx # 仪表盘图表 +│ ├── recent-activity.tsx # 最近活动列表 +│ ├── stat-card.tsx # 指标卡片 +│ ├── *-dialog.tsx # 新增 / 删除 / 发布等对话框 +│ └── / # 按业务模块拆分的业务组件 +├── hooks/ +│ ├── use-mobile.tsx # 响应式断点 hook +│ └── use-toast.ts # Toast hook +├── lib/ +│ ├── api/ # 业务 API 模块(见下节) +│ ├── permissions.ts # 角色 / 权限矩阵 +│ └── utils.ts # cn 等通用工具 +├── middleware.ts # 基于 cookie 的路由保护 +├── public/ # 静态资源 +├── styles/ # 附加样式 +├── Dockerfile # 多阶段构建(builder + runner) +├── docker-compose.yml # 容器编排(端口 3030→3000) +├── next.config.mjs # standalone 输出、实验项 +├── tailwind.config.ts +└── tsconfig.json +``` -- `NEXT_PUBLIC_API_BASE_URL`: The base URL for API requests \ No newline at end of file +--- + +## 鉴权与路由保护 + +项目采用"Cookie + localStorage + 前端权限矩阵"的三层方案: + +1. **Cookie (`auth_token`)** — 由 [middleware.ts](middleware.ts) 读取。受保护路径列表见 `protectedPaths`,未携带 token 时重定向至 `/login?callbackUrl=`。 +2. **localStorage (`auth_token`)** — 由 [lib/api/client.ts](lib/api/client.ts) 的 axios 请求拦截器在每个请求上自动附加 `Authorization: Bearer `;响应拦截器捕获 `401` 时清除 token 并跳转至 `/login`。 +3. **前端权限矩阵** — [lib/permissions.ts](lib/permissions.ts) + [components/sidebar.tsx](components/sidebar.tsx),根据 `localStorage.user_role` 过滤菜单与页面入口。 + +> 服务端(Next.js Middleware)仅校验 token 是否存在;真正的权限校验由后端 API 在业务接口中执行,前端权限矩阵用于提升 UX 而非安全边界。 + +--- + +## API 集成 + +所有业务 API 模块集中在 [lib/api/](lib/api/): + +| 文件 | 职责 | +|------|------| +| `client.ts` | axios 实例、拦截器、基础 URL、Mock 辅助工具 | +| `auth.ts` | 登录 / 注册 / 登出 / Token 管理 | +| `users.ts` / `roles.ts` | 用户与角色 | +| `ai-models.ts` | AI 模型相关接口 | +| `outfits.ts` / `props.ts` / `home-decor.ts` / `food.ts` / `card.ts` | 卡牌体系 | +| `songs.ts` / `dances.ts` / `achievements.ts` / `affinity.ts` | 内容与互动 | +| `upload.ts` | 文件上传 | +| `adapters.ts` | 后端 DTO → 前端 ViewModel 转换 | +| `error-handler.ts` | 统一错误处理 | +| `token-debug.ts` | Token 调试工具 | +| `types.ts` / `song.type.ts` | 共享类型定义 | +| `index.ts` | 聚合导出与 Mock 用户 / 角色 API | + +> `index.ts` 中包含 Mock 数据与 `simulateDelay`,供联调或离线开发使用;生产环境应确保实际请求打向 `NEXT_PUBLIC_API_BASE_URL`。 + +--- + +## 构建与部署 + +### 本地构建 + +```bash +pnpm build +pnpm start # 启动生产服务器 +``` + +`next.config.mjs` 配置了 `output: 'standalone'`,构建产物位于 `.next/standalone`,适合作为独立部署单元。 + +### Docker + +项目提供多阶段 [Dockerfile](Dockerfile)(builder + runner,基于 `node:22.10.0-alpine`,使用淘宝 npm 镜像)与 [docker-compose.yml](docker-compose.yml): + +```bash +# 构建镜像并启动 +docker-compose up -d --build + +# 访问 http://:3030 +``` + +Compose 关键配置: +- 容器名:`lty-admin` +- 端口映射:宿主机 `3030` → 容器 `3000` +- 环境文件:`.env.production` +- 挂载 `./public` 以便热更新静态资源 +- `restart: always` 故障自动恢复 + +部署前确认 `.env.production` 已正确配置 `NEXT_PUBLIC_API_BASE_URL`。 + +--- + +## 浏览器支持 + +最新稳定版本的 Chrome / Firefox / Safari / Edge。项目使用 React 19 与现代 CSS 特性,不再适配 IE。 + +--- + +## 许可证 + +[MIT](LICENSE) diff --git a/qy_lty/CLAUDE.md b/qy_lty/CLAUDE.md index 7c4ae34..4ba08af 100644 --- a/qy_lty/CLAUDE.md +++ b/qy_lty/CLAUDE.md @@ -1,210 +1,247 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -QY LTY Backend is a comprehensive Django-based backend service that provides: -- User management and authentication system -- AI conversation capabilities (single/multi-turn chat with voice support) -- Card management system with batch generation and QR code functionality -- Device interaction via WebSocket real-time communication -- Achievement system -- Subscription management -- Multiple third-party integrations (Aliyun, Volcengine, Tencent, etc.) - -## Development Commands - -### Environment Setup -```bash -# Install dependencies -pip install -r requirements.txt - -# Copy environment configuration -cp .env.bak .env - -# Database migrations -python manage.py migrate - -# Create superuser -python manage.py createsuperuser -``` - -### Running the Application -```bash -# Start development server (ASGI with WebSocket support) -./run.sh - -# Or directly with daphne -daphne -b 0.0.0.0 -p 8000 qy_lty.asgi:application - -# Traditional Django development server (HTTP only) -python manage.py runserver -``` - -### Database Management -```bash -# Create new migrations -python manage.py makemigrations -python manage.py makemigrations [app_name] - -# Apply migrations -python manage.py migrate - -# Database shell -python manage.py dbshell - -# Django shell -python manage.py shell -``` - -### Internationalization (i18n) -```bash -# Generate message files -django-admin makemessages -l en -django-admin makemessages -l zh_HAns - -# Compile translations -django-admin compilemessages -``` - -### Docker Deployment -```bash -# Build and start containers -docker-compose up -d --build - -# Access application at http://localhost:12012 -``` - -## Architecture Overview - -### Core Django Apps Structure -- **userapp/**: Custom user model (ParadiseUser) with authentication, phone verification, and user management -- **aiapp/**: AI conversation system with Kimi integration, voice synthesis/recognition via multiple providers -- **card/**: Card management system with categories, batches, QR codes, and usage tracking -- **device_interaction/**: WebSocket-based real-time device communication with authentication -- **achievement_app/**: User achievement and progress tracking system -- **subscription_app/**: User subscription and billing management -- **ali_vi_app/**: Alibaba Cloud vision intelligence integration -- **workflow_app/**: Multi-tenant workflow management (in development) - -### Key Technical Components - -#### ASGI/WebSocket Configuration -- Uses Django Channels for WebSocket support -- Custom token-based WebSocket authentication (`device_interaction.auth.TokenAuthMiddleware`) -- Redis-backed channel layers for real-time messaging -- WebSocket routing: `ws://domain/ws/device/` and `ws://domain/ws/device/token/{token}/` - -#### Authentication System -- Custom user model: `userapp.ParadiseUser` extending AbstractUser -- Token-based API authentication -- Phone verification with SMS (Aliyun SMS service) -- Social authentication (WeChat) via django-allauth - -#### Database Configuration -- Primary: PostgreSQL (configurable via environment) -- Redis for caching and WebSocket channel layers -- Environment-based configuration using python-decouple - -#### Audio/Voice Services -- Multi-provider audio support: Aliyun, Tencent, Volcengine (Huoshan) -- Provider configurable via `AUDIO_SERVICE_PROVIDER` setting -- Voice synthesis and recognition capabilities - -## API Documentation - -### Access Points -- Swagger UI: `http://localhost:8000/swagger/` -- ReDoc: `http://localhost:8000/redoc/` - -### Main API Modules -- `/api/user/` - User authentication and management -- `/api/ai/` - AI conversation endpoints -- `/api/device/` - Device interaction and WebSocket messaging -- `/api/card/` - Card system management -- `/api/achievement/` - Achievement system -- `/api/v1/admin/` - Administrative functions - -### WebSocket Message Types -```json -{ - "type": "message_type", - "message": "content", - "user_id": "user_id" -} -``` - -Supported types: `chat_message`, `weather`, `sing`, `dance` - -## Environment Configuration - -### Required Environment Variables (.env) -- `SECRET_KEY` - Django secret key -- `DEBUG` - Development mode flag -- Database: `POSTGRESQL_DATABASE_*` variables -- Redis: `REDIS_LOCATION`, `REDIS_PASSWORD` -- Kimi AI: `KIMI_API_KEY`, `KIMI_BASE_URL` -- Aliyun services: Various `ALIYUN_*` keys -- Audio services: Provider-specific configuration -- Volcengine: `VOLCENGINE_ACCESS_KEY`, `VOLCENGINE_SECRET_KEY` - -## Key Dependencies - -### Core Framework -- Django 4.2.13 with Django REST Framework -- Django Channels for WebSocket support -- Daphne ASGI server - -### Third-party Integrations -- Aliyun: SMS, OSS, NLS (speech), Vision Intelligence -- Volcengine (ByteDance): RTC services -- Tencent: Audio services -- OpenAI-compatible API (Kimi) - -### Development Tools -- django-debug-toolbar for development debugging -- django-simpleui for enhanced admin interface -- drf-yasg for API documentation generation - -## Code Patterns and Conventions - -### Model Structure -- Custom user model with extensive profile fields (gender, MBTI, interests, etc.) -- Card system uses batch-based generation with category classification -- Achievement system with rarity levels and progress tracking - -### API Response Format -- Standardized responses via `common.middleware.StandardResponseMiddleware` -- Custom pagination: `common.pagination.CustomPageNumberPagination` - -### Logging -- Aliyun Log Service integration for production logging -- Application-specific loggers for aiapp, userapp, common modules - -### File Storage -- OSS (Aliyun Object Storage) for audio and media files -- Local storage for development with configurable paths - -## Development Notes - -### WebSocket Development -- Device connections require token-based authentication -- Channel layer uses Redis for message passing -- Custom consumers in `device_interaction.consumers` - -### Audio Service Integration -- Provider-agnostic interface via `aiapp.audio.AudioService` -- Speech-to-text and text-to-speech capabilities -- File storage integration for audio assets - -### Card System Features -- Batch generation with configurable size and format -- QR code generation and scanning -- Usage tracking and analytics -- Category-based organization with attributes - -### Admin Interface -- Heavily customized SimpleUI theme -- Multi-language support (Chinese/English) -- Custom icons and organization for different modules \ No newline at end of file +# CLAUDE.md + +本文件为 Claude Code (claude.ai/code) 在本仓库中工作时提供指导。 + +## 项目概述 + +QY LTY Backend 是一个基于 Django 的综合性后端服务,提供以下功能: +- 用户管理与认证系统 +- AI 对话能力(支持语音的单轮/多轮聊天) +- 卡片管理系统,支持批量生成与二维码功能 +- 通过 WebSocket 实时通信进行设备交互 +- 成就系统 +- 订阅管理 +- 多种第三方集成(阿里云、火山引擎、腾讯等) + +### 对接的客户端项目 + +本服务器作为统一后端,与以下客户端/管理端项目进行通讯和数据交互: + +| 项目 | 路径 | 角色 | 通讯方式 | +|------|------|------|---------| +| **LTY_App_Project_URP** | `C:\Unity2022project\LTY_App_Project_URP` | 洛天依 APP(Unity URP 客户端) | HTTP REST API + WebSocket (`/ws/device/`) | +| **LTY_Project** | `C:\Unity2022project\LTY_Project` | 洛天依 Unity 项目(设备端/终端) | HTTP REST API + WebSocket + RTC(火山引擎) | +| **qy-lty-admin** | `../qy-lty-admin/`(`C:\Users\admin\Desktop\Lila-Server\qy-lty-admin`) | Web 管理后台(Next.js 15 + React 19) | HTTP REST API(`NEXT_PUBLIC_API_BASE_URL`) | + +- 两个 Unity 客户端通过 `device_interaction` 模块共享同一套 WebSocket 通道(分组模型 `device_{user_id}`),设备端与 APP 端解析到同一 user_id 才能互通消息 +- 管理后台通过 `/api/v1/admin/` 与各业务模块接口进行运营管理数据交互 + +## 开发命令 + +### 环境配置 +```bash +# 安装依赖 +pip install -r requirements.txt + +# 复制环境配置文件 +cp .env.bak .env + +# 数据库迁移 +python manage.py migrate + +# 创建超级用户 +python manage.py createsuperuser +``` + +### 运行应用 +```bash +# 启动开发服务器(支持 WebSocket 的 ASGI) +./run.sh + +# 或直接使用 daphne +daphne -b 0.0.0.0 -p 8000 qy_lty.asgi:application + +# 传统 Django 开发服务器(仅 HTTP) +python manage.py runserver +``` + +### 数据库管理 +```bash +# 创建新的迁移文件 +python manage.py makemigrations +python manage.py makemigrations [app_name] + +# 应用迁移 +python manage.py migrate + +# 数据库 shell +python manage.py dbshell + +# Django shell +python manage.py shell +``` + +### 国际化 (i18n) +```bash +# 生成消息文件 +django-admin makemessages -l en +django-admin makemessages -l zh_HAns + +# 编译翻译 +django-admin compilemessages +``` + +### Docker 部署 +```bash +# 构建并启动容器 +docker-compose up -d --build + +# 通过 http://localhost:12012 访问应用 +``` + +## 架构概览 + +### 核心 Django 应用结构 +- **userapp/**:自定义用户模型 (ParadiseUser),包含认证、手机验证和用户管理 +- **aiapp/**:AI 对话系统,集成 Kimi,通过多个服务商提供语音合成/识别 +- **card/**:卡片管理系统,包含分类、批次、二维码和使用追踪 +- **device_interaction/**:基于 WebSocket 的实时设备通信,带有认证机制 +- **achievement_app/**:用户成就与进度追踪系统 +- **subscription_app/**:用户订阅与计费管理 +- **ali_vi_app/**:阿里云视觉智能集成 +- **workflow_app/**:多租户工作流管理(开发中) + +### 关键技术组件 + +#### ASGI/WebSocket 配置 +- 使用 Django Channels 提供 WebSocket 支持 +- 自定义基于 token 的 WebSocket 认证(`device_interaction.auth.TokenAuthMiddleware`,复用 `userapp.authentication.RedisTokenAuthentication`) +- 基于 Redis 的通道层用于实时消息传递 +- WebSocket 路由:`ws://domain/ws/device/`(Header 鉴权)和 `ws://domain/ws/device/token/{token}/`(URL 鉴权) +- **分组模型**:`device_{user_id}` —— 设备端与手机端必须解析到 **同一个 user_id** 才能互通消息(详见下文"设备绑定与控制权") + +#### 认证系统 +- 自定义用户模型:`userapp.ParadiseUser`,继承自 AbstractUser +- 基于 token 的 API 认证:token 存储在 Redis 中,普通用户 key 为 `token:{token}`,管理员为 `admin_token:{token}`,TTL 30 天(见 `userapp/utils.py:generate_token`) +- 通过短信进行手机验证(阿里云 SMS 服务) +- 通过 django-allauth 实现社交认证(微信) +- 设备端通过 `POST /api/user/mac-login/` 用 MAC 地址换取绑定用户的 user-token + +#### 数据库配置 +- 主库:PostgreSQL(可通过环境变量配置) +- Redis 用于缓存和 WebSocket 通道层 +- 使用 python-decouple 进行基于环境的配置 + +#### 音频/语音服务 +- 多服务商音频支持:阿里云、腾讯、火山引擎 +- 通过 `AUDIO_SERVICE_PROVIDER` 配置切换服务商 +- 支持语音合成与识别能力 + +## API 文档 + +### 访问入口 +- Swagger UI:`http://localhost:8000/swagger/` +- ReDoc:`http://localhost:8000/redoc/` + +### 主要 API 模块 +- `/api/user/` - 用户认证与管理 +- `/api/ai/` - AI 对话接口 +- `/api/device/` - 设备交互与 WebSocket 消息 +- `/api/card/` - 卡片系统管理 +- `/api/achievement/` - 成就系统 +- `/api/v1/admin/` - 管理功能 + +### WebSocket 消息类型 +```json +{ + "type": "message_type", + "message": "content", + "user_id": "user_id" +} +``` + +支持的类型(见 `device_interaction/consumers.py:DeviceConsumer.receive`): +- `chat_message` — 默认文本消息 +- `weather` — 天气信息(message 为 JSON 字符串) +- `sing` / `dance` / `touch` — 动作指令 +- `flow_light` — 流水灯开关 +- `device_info` — 设备上报 MAC、电量、固件、WiFi 等,会写库并刷新心跳 +- `device_state` — 手机端设备状态 +- `conversation_status` / `conversation_subtitle` — 火山引擎对话回调转发 +- `factory_reset` — 恢复出厂设置 + +### RTC(火山引擎) +- 端点 `/api/device/rtc-token/get_by_mac/?mac_address=...` 不需鉴权,根据 MAC 返回该设备绑定用户对应的 RTC token +- `room_id = f"room_{user_id}"`、token 缓存 key 为 `rtc_room:{user_id}:{task_id}` +- `room_id` 与 WebSocket 分组绑定的是同一个 user_id,保持端到端一致 + +## 环境配置 + +### 必需的环境变量 (.env) +- `SECRET_KEY` - Django 密钥 +- `DEBUG` - 开发模式标志 +- 数据库:`POSTGRESQL_DATABASE_*` 系列变量 +- Redis:`REDIS_LOCATION`、`REDIS_PASSWORD` +- Kimi AI:`KIMI_API_KEY`、`KIMI_BASE_URL` +- 阿里云服务:各类 `ALIYUN_*` 密钥 +- 音频服务:服务商相关配置 +- 火山引擎:`VOLCENGINE_ACCESS_KEY`、`VOLCENGINE_SECRET_KEY` + +## 关键依赖 + +### 核心框架 +- Django 4.2.13 配合 Django REST Framework +- Django Channels 提供 WebSocket 支持 +- Daphne ASGI 服务器 + +### 第三方集成 +- 阿里云:SMS、OSS、NLS(语音)、视觉智能 +- 火山引擎(字节跳动):RTC 服务 +- 腾讯:音频服务 +- OpenAI 兼容 API(Kimi) + +### 开发工具 +- django-debug-toolbar 用于开发环境调试 +- django-simpleui 用于增强后台管理界面 +- drf-yasg 用于生成 API 文档 + +## 代码模式与约定 + +### 模型结构 +- 自定义用户模型,包含丰富的资料字段(性别、MBTI、兴趣等) +- 卡片系统采用基于批次的生成方式,配合分类管理 +- 成就系统包含稀有度等级与进度追踪 + +### API 响应格式 +- 通过 `common.middleware.StandardResponseMiddleware` 实现标准化响应 +- 自定义分页:`common.pagination.CustomPageNumberPagination` + +### 日志 +- 集成阿里云日志服务用于生产环境日志记录 +- 为 aiapp、userapp、common 模块配置专用日志记录器 + +### 文件存储 +- 使用 OSS(阿里云对象存储)存储音频和媒体文件 +- 开发环境使用本地存储,路径可配置 + +## 开发说明 + +### WebSocket 开发 +- 设备连接需要基于 token 的认证 +- 通道层使用 Redis 进行消息传递 +- 自定义消费者位于 `device_interaction.consumers` +- 设备心跳:每次收消息时刷新 `device:last_seen:{mac}`(TTL 5 分钟),断连时把 `Device.status` 标记为 `disconnected` + +### 设备绑定与控制权(重要) +- `UserDevice` 关联表的 `Meta.ordering = ['-bound_at']` +- **"后绑的挤掉先绑的"语义**:`userapp/views.py` 的 MAC 登录显式按 `order_by('-bound_at').first()` 取最新绑定者并签发 user-token;`device_interaction/views.py` 中的 `bind_status` / `rtc-token/get_by_mac` 等使用 `.first()` 隐式依赖该 ordering,结果一致 +- 由于 WebSocket 分组是 `device_{user_id}`,**同一台设备同一时刻只有一个用户能真正控制它**——即最近一次绑定的那个用户 +- 旧的 `UserDevice` 记录**不会**被自动删除,仅在控制权解析中被忽略;如需"换绑"语义请显式删除旧记录 +- `is_primary` 是"用户视角的主设备"(每个用户最多一个),**不是**"设备视角的主控用户"——同一台设备可能出现多条 `is_primary=True` 的记录 +- **测试 MAC `AA:BB:CC:DD:EE:FF`** 在 `device_interaction/serializers.py` 与 `views.py` 中被硬编码跳过"设备已被其他用户绑定"校验,仅供测试用 + +### 音频服务集成 +- 通过 `aiapp.audio.AudioService` 提供与服务商无关的接口 +- 支持语音转文本与文本转语音能力 +- 集成文件存储用于音频资源管理 + +### 卡片系统功能 +- 批量生成,支持配置数量与格式 +- 二维码生成与扫描 +- 使用追踪与数据分析 +- 基于分类的组织管理,支持属性配置 + +### 后台管理界面 +- 深度定制 SimpleUI 主题 +- 支持多语言(中文/英文) +- 为不同模块配置自定义图标与组织结构 diff --git a/qy_lty/device_interaction/views.py b/qy_lty/device_interaction/views.py index 5177db3..c7d09e5 100644 --- a/qy_lty/device_interaction/views.py +++ b/qy_lty/device_interaction/views.py @@ -21,6 +21,8 @@ import datetime from .volcengine_api import update_voice_chat from .amap_api import search_nearby from .models import DeviceType, DeviceBatch, Device, UserDevice +from aiapp.models import ChatMessage, Bot +from userapp.models import ParadiseUser from .serializers import ( DeviceTypeSerializer, DeviceBatchSerializer, DeviceSerializer, DeviceCreateSerializer, DeviceBatchCreateSerializer, @@ -1179,10 +1181,13 @@ class VolcEngineTokenViewSet(viewsets.ViewSet): 'device_code': device.device_code, 'device_type': device.device_type.name } - + # 存储到Redis,设置过期时间 cache.set(redis_key, token_data, expire_time) + # 反向索引:task_id -> user_id,用于字幕回调中 bot01 字幕的 user 归属 + cache.set(f"rtc_task_user:{task_id}", user_id, expire_time) + return success_response( data=token_data, message='Token生成成功' @@ -1215,6 +1220,12 @@ class VolcEngineTokenViewSet(viewsets.ViewSet): logger.info("-----------------------------------------") # 记录回调日志 logger.info('Conversation AI status callback: %s', request.method) + # 永久保留:记录 webhook 上下文(headers + query),用于字幕归属排查 + try: + logger.info('Webhook context: headers=%s, query=%s', + dict(request.headers), dict(request.query_params)) + except Exception: + pass # 处理消息 try: @@ -1291,14 +1302,90 @@ class VolcEngineTokenViewSet(viewsets.ViewSet): } subtitle_items.append(subtitle) logger.info('Subtitle: %s', subtitle) - + + # === 字幕落库(策略 A:仅在 definite && paragraph 时写入)=== + try: + # 主路径:尝试从 webhook 上下文提取 task_id(火山如带) + webhook_task_id = ( + request.query_params.get('task_id') + or request.headers.get('X-Volc-Task-Id') + or request.headers.get('X-Task-Id') + or json_data.get('task_id') + ) + + # 拿 RTC Bot id(带缓存,避免每条字幕查 DB) + rtc_bot_id = cache.get('rtc_voice_agent_bot_id') + if not rtc_bot_id: + rtc_bot_id = Bot.objects.filter(name='RTC_Voice_Agent').values_list('id', flat=True).first() + if rtc_bot_id: + cache.set('rtc_voice_agent_bot_id', rtc_bot_id, 3600) + + if not rtc_bot_id: + logger.error('RTC_Voice_Agent Bot 未配置,跳过字幕落库') + else: + for item in subtitle_items: + text = (item.get('text') or '').strip() + if not text: + continue + # 策略 A:只在一句话最终段落时落库 + if not (item.get('definite') and item.get('paragraph')): + continue + + user_id_in_subtitle = item.get('userId') or '' + sequence = item.get('sequence', 0) + + # 解析 ParadiseUser 归属 + paradise_user_id = None + if user_id_in_subtitle == 'bot01': + # AI 字幕:主路径 task_id 索引 -> 兜底 last_active_user + if webhook_task_id: + paradise_user_id = cache.get(f"rtc_task_user:{webhook_task_id}") + if not paradise_user_id: + paradise_user_id = cache.get('rtc_last_active_user') + sender = ChatMessage.SENDER_BOT + elif user_id_in_subtitle.isdigit(): + paradise_user_id = user_id_in_subtitle + sender = ChatMessage.SENDER_USER + # 用户字幕到达,刷新最近活跃用户(给后续 bot01 字幕兜底) + cache.set('rtc_last_active_user', user_id_in_subtitle, 60) + else: + logger.warning('字幕 userId %r 无法识别,跳过', user_id_in_subtitle) + continue + + if not paradise_user_id: + logger.warning('字幕无法归属用户: userId=%s task_id=%s', + user_id_in_subtitle, webhook_task_id) + continue + + # 防重:同一 (paradise_user_id, sequence) 只写一次 + dedup_key = f"rtc_subv_seen:{paradise_user_id}:{sequence}" + if cache.get(dedup_key): + continue + cache.set(dedup_key, 1, 300) + + # 写入 ChatMessage(截断超长,DB 字段上限 2048) + try: + ChatMessage.objects.create( + user_id=int(paradise_user_id), + bot_id=rtc_bot_id, + message=text[:2048], + sender=sender, + message_type=ChatMessage.MESSAGE_TYPE_TEXT, + ) + except Exception as e: + logger.error('字幕落库失败: %s, sender=%s, text=%r', + e, sender, text[:100]) + except Exception as e: + logger.error('字幕落库流程异常: %s', e) + # === 字幕落库结束 === + # 构建响应数据 response_data = { 'type': 'subtitle', 'subtitles': subtitle_items, 'signature': signature } - + return success_response(data=response_data, message='Subtitle received successfully') except UnicodeDecodeError as e: logger.error('Failed to decode subtitle JSON: %s', str(e)) @@ -1341,7 +1428,13 @@ class VolcEngineTokenViewSet(viewsets.ViewSet): # 记录状态信息 logger.info('Conversation status: TaskId=%s, UserID=%s, RoundID=%s, StageCode=%s, Description=%s', task_id, user_id, round_id, stage_code, stage_description) - + + # 维护 task->user 反向索引和最近活跃用户(用于 bot01 字幕归属) + if task_id and user_id: + cache.set(f"rtc_task_user:{task_id}", str(user_id), 3600) + if user_id: + cache.set('rtc_last_active_user', str(user_id), 60) + # 根据不同的状态码进行不同的处理 response_data = { 'task_id': task_id, diff --git a/qy_lty/docs/设备聊天记录_字幕落库方案.md b/qy_lty/docs/设备聊天记录_字幕落库方案.md new file mode 100644 index 0000000..b917638 --- /dev/null +++ b/qy_lty/docs/设备聊天记录_字幕落库方案.md @@ -0,0 +1,215 @@ +# 设备端聊天记录上报功能 — 修改计划(方案 B:服务器端字幕落库) + +## Context + +设备端 Unity 项目(LTY_Project,运行在 RK3588 上)需要把"用户在设备前与 Lila 的语音对话"以文字形式落到服务器,让手机端 App 能在原有 `/api/ai/rtc-chat-history/` 接口里看到。**不需要在设备屏幕显示文字,也不需要打字输入**。 + +调研后发现:火山引擎对话式 AI 的"服务端字幕回调"链路**已经全部搭好且在跑**: + +- 设备端 [getJson.cs:70-72](Assets/Scripts/getJson.cs#L70) 已经在 RTC AgentConfig 里配置了 `EnableConversationStateCallback=true` 和 `ServerMessageURLForRTS=httpBaseUrl+"api/device/rtc-token/conversation_status/"` —— 火山引擎会把房间内 ASR 字幕(用户和 AI 的)和会话状态推到这个 URL。 +- 服务器 [device_interaction/views.py:1199-1421](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1199) 的 `conversation_status` action 已经在接收并解析两种二进制格式: + - `subv`(字幕格式,[views.py:1258-1302](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1258)):包含 `text` / `userId` / `definite` / `paragraph` / `sequence` / `language` / `mode` / `timestamp` —— 其中 `text` 就是用户和 AI 的对话文本。**当前只 log 出来,没有写入数据库**。 + - `conv`(对话状态格式,[views.py:1309-1376](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1309)):包含 `TaskId` / `UserID` / `RoundID` / `Stage.Code/Description` —— 已经在通过 WebSocket 转推给设备/手机驱动状态机。 + +所以方案 B 的本质是:**只在服务器端 `conversation_status` 接口的 `subv` 分支里加几十行写库逻辑**,把字幕文本作为 `ChatMessage` 写到与手机端相同的表里。设备端、手机端、火山引擎一律不动。 + +## 关键设计决策(已与用户确认) + +- **设备端零改动**:不复制 ASR 客户端、不挂 ChatLogManager、不改 LoginRTC、不改 WebSocketConnection。 +- **手机端零改动**:手机端 App 现有的 `GET /api/ai/rtc-chat-history/?page_size=50` 在服务器开始落库后会自然返回设备端产生的消息(与手机端 App 自己跟 Lila 聊天的记录混在同一个 user 名下,与之前用户确认的"不加 device_mac 字段"一致)。 +- **bot 关联**:设备端字幕也归到 `RTC_Voice_Agent` 这个 Bot 名下(与手机端 App 用同一个 Bot),保证 [RTCChatHistoryAPIView](C:/Users/admin/Desktop/Lila-Server/qy_lty/aiapp/views.py#L440) 的 `filter(user=request.user, bot=bot)` 查询直接覆盖。 +- **落库时机**:每条字幕到达就处理,仅在判定为"一句话最终结果"时写入 `ChatMessage`。 + +## RTC userId ↔ ParadiseUser 映射(关键事实) + +从 [device_interaction/views.py:1142](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1142) 可知,`get_by_mac` 返回的 RTC `user_id` 就是 `str(user_device.user.id)`(ParadiseUser 主键的字符串形式)。这传给设备端 [getJson.cs:67](Assets/Scripts/getJson.cs#L67) 的 `targetUserIds.Add(UserId)`,最终火山字幕回调里 `subtitle.userId` 字段: + +- `userId == "bot01"` → AI 说的话(assistant),来自 [getJson.cs:69](Assets/Scripts/getJson.cs#L69) `agentConfig["UserId"] = "bot01"` 的硬编码 +- `userId == "<纯数字字符串>"` → 用户说的话,**该数字就是 ParadiseUser.id** + +这意味着用户消息可以直接从字幕字段反查 ParadiseUser,无需额外映射。 + +AI 消息(`bot01`)的 user 归属需要别的方法解决 —— 见下面"AI 字幕 user 归属"小节。 + +## 服务器端 — 核心修改 + +**唯一改动文件**:`C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py` + +### 1. 修改 `conversation_status` action 的 `subv` 分支([views.py:1258-1302](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1258)) + +当前代码:解析出 `subtitle_items` 列表后只构建 response_data 返回。 + +需要在 `subtitle_items` 构建后,紧接着遍历每条 subtitle 并按下面的规则写入 `ChatMessage`: + +```python +# 伪代码(详细见执行阶段) +for item in subtitle_items: + text = item['text'].strip() + user_id_in_subtitle = item['userId'] + is_definite = item['definite'] + is_paragraph_end = item['paragraph'] + sequence = item['sequence'] + + if not text: + continue + + # 只在"一句话结束"时写库(见下面"流式字幕去重"小节) + if not (is_definite and is_paragraph_end): + # 中间结果先在 Redis 里累积,不落库 + _accumulate_partial_subtitle(task_id_or_round_key, user_id_in_subtitle, text, sequence) + continue + + # 反查 ParadiseUser + if user_id_in_subtitle == 'bot01': + sender = ChatMessage.SENDER_BOT # 'assistant' + paradise_user = _resolve_user_for_bot_subtitle(...) # 见 AI 字幕 user 归属 + else: + sender = ChatMessage.SENDER_USER # 'user' + try: + paradise_user = ParadiseUser.objects.get(id=int(user_id_in_subtitle)) + except (ParadiseUser.DoesNotExist, ValueError): + logger.warning('subtitle userId %s 无法解析为 ParadiseUser,跳过', user_id_in_subtitle) + continue + + if paradise_user is None: + continue + + # 拿 RTC Bot + try: + bot = Bot.objects.get(name='RTC_Voice_Agent') + except Bot.DoesNotExist: + logger.error('RTC_Voice_Agent Bot 未配置,跳过字幕落库') + continue + + # 拼接累积的中间结果(如果之前累积过) + final_text = _flush_accumulated_subtitle(task_id_or_round_key, user_id_in_subtitle, text) + + ChatMessage.objects.create( + user=paradise_user, + bot=bot, + message=final_text, + sender=sender, + message_type=ChatMessage.MESSAGE_TYPE_TEXT, + ) +``` + +实际选择"直接落库 vs 累积后落库"的策略由执行阶段在第一次跑通后看真实字幕事件流来定 —— 见下面"流式字幕去重"小节的两种策略。 + +### 2. AI 字幕 user 归属(关键难点) + +字幕外层 JSON `{message, binary, signature}` 里**没有 task_id**([views.py:1232-1234](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1232))。`subv` 二进制内部解析出来也只有 `userId/text/sequence/...`,**也没有 task_id**([views.py:1281-1291](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1281))。所以"`userId='bot01'` 时归属哪个用户"需要外部上下文。 + +**首选方案:用最近一次 `conv` 状态回调的 user_id 作为上下文** + +`conv` 状态回调在 [views.py:1333-1334](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1333) 已经解析出 `TaskId` 和 `UserID`(这里的 UserID 来自火山,等于 ParadiseUser.id 的字符串)。在 `conv` 分支处理时把 `(TaskId, UserID, RoundID)` 写到 Redis short-TTL(如 30 秒),key 形如 `rtc_active_round:` value=`{user_id, round_id, last_seen_ts}`。 + +但字幕回调没有 task_id,没法按 task_id 反查 —— 退而求其次:**维护一个全局"最近活跃用户"的列表**(按时间倒序),AI 字幕到达时取距其最近的那个用户。这在并发设备数较少时有效;如果同时有多个设备 RTC 房间在跑,需要更精细的关联。 + +**更稳妥的备选方案:在 `_resolve_user_for_bot_subtitle` 里查 Redis** + +由于 `get_by_mac` 已经把 `rtc_room:{user_id}:{task_id}` 写到 Redis([views.py:1169](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1169)),可以在这里**额外加一份反向索引** `rtc_active_user:bot01_last:{round_or_task} → user_id`,由 `conv` 分支负责更新。 + +**最稳妥的兜底方案:等火山回调 URL 实测** + +火山引擎 RTS webhook **可能**在 URL query string 里带 `?task_id=xxx`(很多服务端 webhook 都这么干),或者在 HTTP header 里带(如 `X-Volc-Task-Id`)。设备端配置的 URL 是固定的 `api/device/rtc-token/conversation_status/`(无 query),但火山服务器实际调用时是否会附加,需要在生产日志(`logger.info('JSON data: %s', json_data)` [views.py:1229](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1229) 已经在打)里观察。 + +执行阶段第一步是**先打开生产日志看一次真实的 subv 字幕回调 raw payload**,确认能不能直接拿到 task_id 或类似关联字段。如果能,整个映射问题秒杀;如果不能,用 Redis 维护"最近活跃 task→user 映射"。 + +### 3. 流式字幕去重 / "一句话最终"判定 + +火山引擎对话式 AI 字幕是流式的,同一句话会推多条 `definite=false` 中间结果,最后推 `definite=true` 最终结果;多句话之间用 `paragraph=true` 标记段落结束。 + +参考火山官方文档([views.py:1206](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1206) 注释里给的 https://www.volcengine.com/docs/6348/1415216)的字段定义: + +- `definite=true`:当前 utterance 已确定 +- `paragraph=true`:当前段落(可能含多个 utterance)已结束 +- `sequence`:序号,单调递增 + +**两种落库策略,执行阶段二选一**: + +**策略 A(简单)**:只在 `definite=true && paragraph=true` 时把 `text` 直接落库。优点:实现简单、不需要状态。缺点:火山的 paragraph 颗粒度可能太大(一段话含好几个完整意群),落库的 message 可能很长;或者反过来颗粒度太小(一个 utterance 就是一段),导致同一轮对话被拆成多条。 + +**策略 B(拼接)**:用 Redis 的 List/String 累积同一对话回合(按 `task_id+userId+round_key`)的所有 `definite=true` text,遇到 `paragraph=true` 时把累积结果拼接成一条 ChatMessage 落库。需要超时清理(如累积 60 秒还没收到 paragraph_end 就强制落库)。 + +执行阶段第一步先用策略 A 跑通,观察 Unity Editor + 服务器日志里的真实字幕颗粒度,再决定要不要切策略 B。 + +### 4. (可选轻量优化)`get_by_mac` 写一份反向索引 + +修改 [views.py:1167-1184](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1167) 在已有 `cache.set(redis_key, token_data, expire_time)` 后追加: + +```python +# 反向索引:task_id → user_id,用于字幕回调中 bot01 字幕的 user 归属 +cache.set(f"rtc_task_user:{task_id}", user_id, expire_time) +``` + +这样 `conversation_status` 处理 `conv` 回调时能 `cache.get(f"rtc_task_user:{task_id}")` 直接拿到 user_id,不依赖时序假设。 + +只在执行阶段确认"火山字幕回调里有 task_id 关联"时这步才有意义;如果没有 task_id,这层映射救不了字幕分支(字幕分支根本不知道当前 task_id 是哪个)。 + +## 验证 + +按顺序: + +### 1. 摸火山真实字幕回调的 schema(最关键的第一步) + +在不改任何代码的情况下,先收集一次真实字幕事件: + +- 服务器开 DEBUG 日志,确认 [views.py:1229](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1229) `logger.info('JSON data: %s', json_data)` 和 [views.py:1275](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1275) `logger.info('Parsed subtitle data: %s', subtitle_data)` 都能输出 +- 设备端真机或 Editor Play → 进入 Game 场景 → RTC 加房 → 对着麦说一句话 +- 看服务器日志里: + - 外层 JSON 除了 `message/binary/signature` 还有没有别的字段(可能藏着 task_id) + - HTTP 请求 URL(可能带 `?task_id=`)和 headers(可能带 `X-Volc-*`)—— 需要在 view 函数最顶端临时加 `logger.info('headers=%s, query=%s', dict(request.headers), request.query_params.dict())` + - subtitle_data 内的所有字段,特别是 `userId='bot01'` 时有没有别的字段能关联到 user + +这一步直接决定第 2 节"AI 字幕 user 归属"用哪种方案。 + +### 2. 改代码 + 跑通用户消息 + +先实现"用户消息"分支(subtitle.userId 是数字 → ParadiseUser)—— 这部分不依赖 task_id 映射,最容易跑通。 + +- 改完后用 `python manage.py runserver` 起本地服务 +- 设备端切到本地([RootManager.cs:30](Assets/Scripts/RootManager.cs#L30) `LocalTest=true`) +- Editor Play 加 RTC 房 → 说一句话 → 在 Django shell 里 `ChatMessage.objects.filter(sender='user').order_by('-id')[:5]` 看是否有新记录 + +### 3. 跑通 AI 消息 + +按第 1 步实测结果选第 2 节的方案,实现 `bot01` → user 归属。 +然后让 Lila 回一句话,验证 `ChatMessage.objects.filter(sender='assistant').order_by('-id')[:5]`。 + +### 4. 三端联调 + +用手机端 App 登录绑定该设备的同一个用户账号,进入聊天记录界面,确认能看到刚才设备前对话的字幕(user 和 assistant 各一条)。 + +### 5. 真机验证 + 长稳 + +烧 APK 到 RK3588,对话 5-10 分钟,看: +- ChatMessage 表数量是否符合预期(无重复、无丢失) +- 服务器 error log 有没有写库失败(数据库唯一约束、字段长度等) +- ChatMessage.message 字段最大 2048([aiapp/models.py:39](C:/Users/admin/Desktop/Lila-Server/qy_lty/aiapp/models.py#L39)),超长字幕需要 truncate + +### 6. 回归 + +确认设备端 RTC 视频/音频/对话状态机(aiState)一切正常 —— 因为只改了服务器端,设备端逻辑应该零影响。 + +## 需修改文件清单 + +服务器端(**唯一改动**): +- `C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py` + - 在 `VolcEngineTokenViewSet.conversation_status` action 的 `subv` 分支([第 1278-1302 行](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1278))追加字幕落库逻辑(约 30-50 行) + - 顶部加 `from aiapp.models import ChatMessage, Bot` 和 `from userapp.models import ParadiseUser` + - 临时调试期间加 headers / query 的 logger.info([第 1217 行](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1217) 附近)—— 跑通后可保留或删除 + - 可选:在 `get_by_mac` 的 [第 1184 行](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1184) 后追加 `cache.set(f"rtc_task_user:{task_id}", user_id, expire_time)` + - 可选:在 `conv` 分支 [第 1356 行](C:/Users/admin/Desktop/Lila-Server/qy_lty/device_interaction/views.py#L1356) 附近追加"最近活跃 task→user"映射写入 + +服务器端**不需要 migration**(ChatMessage 表结构不变,复用现有字段)。 + +服务器端**不需要确认 RTC_Voice_Agent Bot 存在** —— 已经被手机端用着,必然存在;执行时跑一次 `Bot.objects.get(name='RTC_Voice_Agent')` 验证一次。 + +设备端:**无修改**。 +手机端:**无修改**。 +火山引擎:**无配置变化**(设备端 getJson.cs 已经配置好了)。 + +## 风险与回滚 + +- 主要风险:字幕落库逻辑写错 → 数据库写脏数据。缓解:先在本地服务器跑,写完后用 SQL 批量 DELETE 测试期产生的脏数据再上正式环境。 +- 回滚:只需 `git revert` views.py 的那个 commit,因为没有 schema 改动。