lty/qy_lty/docs/修改记录.md
pmc db4d5cf89d docs(03-02): docs/修改记录.md 顶部追加 Phase 3 条目 (CRED-05 + CRED-06)
- 5 处文件改动汇总: aiapp/views.py + qy_lty/urls.py + common/logging/__init__.py + common/logging/filters.py + qy_lty/settings.py
- 修改类型: 新增
- 修改内容: 客户端 GET 接口明文返回 + AccessTokenMaskFilter 4 正则脱敏 + LOGGING 注册
- 修改原因: Milestone v1.0 收尾 phase, 客户端读取 + 日志防御性兜底
- 跨项目联动: 无 — 客户端给 Unity (LTY_Project / LTY_App_Project_URP) 用, 那两个 repo 各自维护; qy-lty-admin 不消费此接口
- qy-lty-admin/docs/修改记录.md mtime 验证未变, 不写互引
2026-05-08 10:30:14 +08:00

33 KiB
Raw Blame History

服务器端代码修改记录

本文档记录每次对服务器端代码的修改,方便追踪变更历史。


修改格式说明

每次修改按以下格式记录:

### [日期] 修改简述

- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug
- **修改内容**: 具体修改了什么
- **修改原因**: 为什么要做这个修改

修改历史

[2026-05-08] Phase 3 — 客户端凭据槽位 GET 接口 + 阿里云日志 access_token 脱敏

配套 Phase.planning/phases/03-client-and-log-mask/ 覆盖需求CRED-05 + CRED-06 设计参考1:1 复刻 aiapp.views.CredentialSlotAdminView 的 GET 部分(删 _ensure_admin / _build_response_data / PUT 三处),实现明文返回客户端 view新建 common/logging/filters.py:AccessTokenMaskFilter 作为 LOGGING.handlers 层防御性兜底

  • 文件路径:
    • aiapp/views.py(修改 — 文件末尾追加 _credential_slot_client_data_schema 客户端响应 schema + CredentialSlotClientView APIView 类,仅 GET明文返回imports 段未动Phase 2 既有 CredentialSlotAdminView 未动)
    • qy_lty/urls.py(修改 — imports 段追加 from aiapp.views import CredentialSlotClientViewapi_urlpatterns 列表中追加 path('credential-slot/', CredentialSlotClientView.as_view(), name='client_credential_slot'),注册位置:common/upload/ 之后、v1/admin/ 之前)
    • common/logging/__init__.py新建 — 空文件,让 common.logging 成为可 import 的 Python 包)
    • common/logging/filters.py新建AccessTokenMaskFilter(logging.Filter) 类 + 4 个 regex 模式JSON / Python dict repr / URL query / 等号或冒号兜底)+ filter() 方法重写 record.msgrecord.args 中的 access_token 字段值为 mask_token(value) 输出)
    • qy_lty/settings.py(修改 — LOGGING 字典新增 'filters' 段(用 '()': 'common.logging.filters.AccessTokenMaskFilter' dictConfig 工厂语法);'handlers'.aliyun'handlers'.console 各追加 'filters': ['access_token_mask']loggers 段 5 条 logger 完全未动)
  • 修改类型: 新增
  • 修改内容:
    • 暴露 GET /api/credential-slot/(路径与管理端 /api/v1/admin/credential-slot/ 完全分开,客户端走 /api/ 一级命名空间不进 v1/admin/ 子路径):RedisTokenAuthentication + IsAuthenticated做 is_staff 二次校验admin / user token 都允许admin 用户是手机用户超集CONTEXT 锁定决策);返回 { success, code, message, data: { app_id, access_token: <**明文**>, updated_at } }Access Token 直接返回 serializer.data(不调 mask_token供手机端LTY_App_Project_URP/ 设备端LTY_Project实际调用阿里云 / 火山 / 腾讯第三方服务
    • 新建 AccessTokenMaskFilter4 个正则模式覆盖 JSON 字符串("access_token":"VALUE"、Python dict repr'access_token':'VALUE'、URL queryaccess_token=VALUE)、等号或冒号兜底(access_token: VALUE)共 4 种序列化形态filter 同时改 record.msgrecord.args(避免 Formatter 阶段再用 % 拼接出明文per RESEARCH Pitfall 2只匹配 access_token 字段名为前缀锚点,误伤 Authorization header: / Bearer / 裸 user tokenper RESEARCH Pitfall 3filter 永远 return True 不丢弃 recordper RESEARCH Pitfall 1
    • LOGGING dictConfig 注册filter 段用 '()': '...' 工厂语法(不是 'class'per RESEARCH Pitfall 5filter 挂在 handlers.aliyun / handlers.console 两个 handler 上(挂 loggers 段per RESEARCH Pitfall 1 — 挂 logger 仅过滤直接通过该 logger 的 record挂 handler 才统一覆盖所有 logger → handler 路径);既有 5 条 logger 配置完全未动
    • Swagger / ReDoc 自动暴露method-level @swagger_auto_schema 装饰器;响应 data schema 用独立 _credential_slot_client_data_schemaaccess_token 字段 description 显式标注「明文 Access Token供手机/设备端实际调用第三方服务(管理端同接口会脱敏返回末 4 位)」,避免前端误解明文 / 脱敏
    • 不引入新依赖(沿用 Django 4.2.13 + DRF + drf-yasg + Phase 1/2 落地的 CredentialSlot.get_solo / CredentialSlotSerializer / mask_token
  • 修改原因: Milestone v1.0「通用凭据槽位APP ID + Access Token」Phase 3 收尾 phase — 同时落地客户端读取CRED-05与日志脱敏CRED-06。客户端读取需要明文手机/设备端 Unity 调阿里云 / 火山 / 腾讯 SDK 时第三方 API 校验 token 字符级一致),所以 view 层不脱敏;但「明文走 view」会让任何后续开发者写 logger.info(f"PUT body: {request.data}") 类代码立即把 access_token 打到阿里云日志服务,所以新增 LOGGING.handlers 层 filter 作为防御性兜底。RESEARCH 已实证:当前仓库没有任何代码 logger 输出 CredentialSlot.access_token 明文(StandardResponseMiddleware 不打日志、view 不显式 logger 字段、Django 默认 access log 不含 body所以 CRED-06 的端到端验证靠单元测试伪造 LogRecord 验证 filter 行为4 种序列化形态 + 不误伤 Authorization 字段)+ 1 条端到端 logger.info 真实输出脱敏验证,不靠端到端找泄露路径。这是 CRED-06 的真实价值 — 防御性兜底,让未来代码改动天然安全
  • 跨项目联动: 无 — 客户端 GET /api/credential-slot/ 给 Unity 客户端(LTY_Project / LTY_App_Project_URP)使用,那两个 repo 各自维护修改记录,不在本仓库范畴;qy-lty-adminWeb 管理后台前端)不消费此接口(管理端走 Phase 2 落地的 /api/v1/admin/credential-slot/,由 admin token 鉴权 + 脱敏返回。CLAUDE.md 跨项目规则下:本 phase 既不影响 qy-lty-admin 也不与 Unity 客户端在同一仓库,故不在 qy-lty-admin/docs/修改记录.md 写互引条目Unity 客户端改动由 LTY_Project / LTY_App_Project_URP 在自身仓库各自记录
  • 后续动作: Milestone v1.0 至此完成;下一周期 milestone 候选见 .planning/REQUIREMENTS.md 「候选优先级」段HIGHACH-02 / SMS 频率限制 / DEBUG 收紧 / 测试基础设施 / 测试 MAC 硬编码MEDIUM好感度 P2-P4 / Python 版本升级 / device_interaction 拆分)

[2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口GET 脱敏 / PUT 覆写)

配套 Phase.planning/phases/02-admin-rest/ 覆盖需求CRED-03 + CRED-04 设计参考1:1 复刻 aiapp.views.RTCChatHistoryAPIViewaiapp/views.py:434-555)的单 URL 多方法 APIView 风格

  • 文件路径:
    • aiapp/serializers.py(修改 — 顶部 import 追加 CredentialSlot,文件末尾追加 CredentialSlotSerializer ModelSerializer 类)
    • aiapp/views.py(修改 — 顶部 import 追加 CredentialSlot / CredentialSlotSerializer / mask_token / get_standardized_response_schema;文件末尾追加 CredentialSlotPutRequestSchema swagger 请求体 + _credential_slot_data_schema 响应 data schema + CredentialSlotAdminView APIView 类)
    • userapp/admin_urls.py(修改 — 追加 from aiapp.views import CredentialSlotAdminViewpath('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot')
  • 修改类型: 新增
  • 修改内容:
    • 暴露 GET /api/v1/admin/credential-slot/admin token 鉴权(RedisTokenAuthentication + 视图内 is_staff 二次校验,不发明 admin-only permission 类);返回 { success, code, message, data: { app_id, access_token: <末 4 位脱敏掩码>, updated_at } },脱敏由 view 层调 common.utils.mask_token 完成serializer 不参与脱敏,避免双重责任)
    • 暴露 PUT /api/v1/admin/credential-slot/admin token 鉴权;接受 { app_id, access_token } 全字段覆写;空记录场景自动走 CredentialSlot.get_solo()get_or_create(pk=1);写入后 updated_atauto_now=True 自动刷新;响应同样脱敏 access_token避免运营在 admin UI 看到自己刚提交的明文回显)
    • 鉴权拒绝矩阵:无 token → 401DRF NotAuthenticated → middleware 兜底标准壳层);持普通 user token非 staff→ 403 + message="需要管理员权限"
    • Swagger / ReDoc 自动暴露method-level @swagger_auto_schema 装饰器;响应 schema 配 common.swagger_utils.get_standardized_response_schema()access_token 字段 description 显式标注「Access Token 末 4 位脱敏掩码(如 "*********1234")」,避免前端误解为明文
    • 不引入新依赖(沿用 Django 4.2.13 + DRF + drf-yasg + Phase 1 落地的 CredentialSlot.get_solo / mask_token
  • 修改原因: Milestone v1.0「通用凭据槽位APP ID + Access Token」Phase 2 — 给管理后台前端qy-lty-admin暴露受控的凭据读写入口让运营无需进 Django Admin 也能管理凭据GET 与 PUT 响应均脱敏,避免明文经管理端 UI / 浏览器 devtools / 阿里云日志GET 响应体路径)泄露;为 Phase 3 客户端明文 GET 接口 + 阿里云日志 formatter 提供"接口已上线、凭据可写入"的稳定起点
  • 跨项目联动: 前端联动条目 qy-lty-admin/docs/修改记录.md 同期 [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)。本 phase 是 Milestone v1.0 首次跨项目接口契约落地:本仓库(服务端)暴露 /api/v1/admin/credential-slot/ GET/PUT前端 qy-lty-admin 后续 phase 将基于该契约写 API client含 React Hooks 调用 + 表单录入 UI。前后端各自维护独立修改记录本条与对方条目互相引用便于未来回查接口的双向上下游

[2026-05-07] Phase 1 — Django Admin 注册凭据槽位(脱敏 + 单例约束 + 禁删)

配套 Phase.planning/phases/01-credential-data-layer/ 覆盖需求CRED-02

  • 文件路径: aiapp/admin.py(修改 — 顶部 import 追加 CredentialSlotmask_token,文件末尾追加 CredentialSlotAdmin 注册)
  • 修改类型: 新增
  • 修改内容:
    • 注册 CredentialSlotAdminlist_display = ('id', 'app_id', 'access_token_masked', 'updated_at'),其中 access_token_masked 是计算字段(调 common.utils.mask_token 仅显示末 4 位掩码)
    • fieldsets 分「凭据信息」(app_id / access_token 明文可写)+「元数据」(updated_at 只读、可折叠)
    • 重写 has_add_permission:已存在记录时返回 FalseAdmin 列表页隐藏「增加」按钮,强制单例语义)
    • 重写 has_delete_permission:永远返回 False(含批量动作;防运营误删丢失单例)
    • 不修改既有 BotAdmin / ChatMessageAdmin 注册块
  • 修改原因: CRED-02 — 在 SimpleUI 后台为运营提供受控的凭据录入入口;列表 / 查看态脱敏防截图 / 录屏泄露;编辑态保留明文供录入;新增 / 删除按钮隐藏强制单例语义不被运营误操作破坏
  • 跨项目联动: 无 — qy-lty-admin 同期 v1.0 前端集成 milestone 已规划但未启动;待前端启动 phase 后由对方仓库写一条互引条目。本改动仅触及服务端 Django Admin运营访问 /admin/aiapp/credentialslot/ 直接录入),与 qy-lty-admin/Web 管理后台前端)无 API 联动CLAUDE.md 跨项目规则下纯服务端改动不需要在 qy-lty-admin/docs/修改记录.md 写互引条目。Phase 2 暴露 /api/v1/admin/credential-slot/ 接口时再做前后端联动。

[2026-05-07] Phase 1 — 凭据槽位数据层CredentialSlot 单例模型 + 迁移 + mask_token 工具)

配套 Phase.planning/phases/01-credential-data-layer/ 覆盖需求CRED-01 设计参考1:1 复刻 userapp.models.AffinitySettinguserapp/models.py:247-314)的 pk=1 + save() 钩子 + get_solo() 单例三件套

  • 文件路径:
    • common/utils.py(新增 — mask_token(token, visible_tail=4) 工具函数,供本 Phase Admin 与 Phase 3 阿里云日志 formatter 共用)
    • aiapp/models.py(修改 — 文件末尾追加 CredentialSlot 模型3 字段 + save 钩子 + get_solo 类方法)
    • aiapp/migrations/0004_credentialslot.py(新增 — python manage.py makemigrations aiapp 自动生成)
  • 修改类型: 新增
  • 修改内容:
    • 新增 CredentialSlot 模型aiapp appapp_id CharField(128, blank=True, default='')、access_token CharField(512, blank=True, default='')、updated_at DateTimeField(auto_now=True)save() 钩子在已有记录时把新对象 pk 改为现有那条;get_solo() 类方法走 get_or_create(pk=1)
    • 新增 common.utils.mask_token(token, visible_tail=4, mask_char='*'):空输入返回 '';短于 visible_tail 时全脱敏不暴露长度;其余保留末 N 位明文
    • 自动生成迁移 aiapp/migrations/0004_credentialslot.pypython manage.py migrate 通过;首次访问 CredentialSlot.objects.get_or_create(pk=1) 拿到一条空记录
  • 修改原因: Milestone v1.0「通用凭据槽位APP ID + Access Token」Phase 1 — 在 DB 层落地全局单例的凭据存储槽位,为 Phase 2 管理端 REST、Phase 3 客户端 REST + 日志脱敏奠基mask_token 抽到 common/ 让 Phase 3 阿里云日志 formatter 直接复用,避免重复实现
  • 后续动作: Phase 2 暴露 /api/v1/admin/credential-slot/ GET脱敏 / PUT覆写Phase 3 暴露 /api/credential-slot/ GET 明文 + 阿里云日志 formatter 用 mask_token 过滤 access_token 字段
  • 跨项目联动: 无 — qy-lty-admin 同期 v1.0 前端集成 milestone 已规划但未启动;待前端启动 phase 后由对方仓库写一条互引条目。本改动是纯数据层 + 工具函数,无任何 HTTP / WebSocket 接口暴露,qy-lty-admin 与 Unity 客户端均无感知;不需要在前端写互引条目。

[2026-05-07] 引入 GSD 工作流并完成 brownfield 文档化初始化

  • 文件路径:
    • .planning/config.json(新增)
    • .planning/PROJECT.md(新增)
    • .planning/REQUIREMENTS.md(新增)
    • .planning/STATE.md(新增)
    • .planning/codebase/STACK.md / INTEGRATIONS.md / ARCHITECTURE.md / STRUCTURE.md / CONVENTIONS.md / TESTING.md / CONCERNS.md(前序 commit 64a8cb8 已建)
  • 修改类型: 新增
  • 修改内容: 在 qy_lty/ 下引入 GSDGet Shit Done 工作流目录 .planning/,包含:
    1. .planning/codebase/ — 7 份 codebase 反向工程文档(栈 / 集成 / 架构 / 目录 / 规约 / 测试 / 隐患)
    2. .planning/PROJECT.md — 项目愿景 + Core Value + 已交付能力Validated+ 关键决策记录
    3. .planning/REQUIREMENTS.md — 把已上线能力拆为带 REQ-ID 的清单AUTH/AI/DEV/CARD/ACH/SUB/AFF/VI/INF/ADM/DEPActive 段留空待 /gsd-new-milestone 启动
    4. .planning/STATE.md — 工作流状态机入口
    5. .planning/config.json — 工作流偏好YOLO / Coarse / Parallel / 三类辅助 agent 全开 / Balanced 模型档)
  • 修改原因:
    • 后续新功能 / 重构通过 GSD 走「discuss → plan → execute → verify」标准流程避免无规划的散弹式提交
    • 反向梳理一遍现状形成文档基线,方便新成员(含 AI agent秒级进入上下文
    • .planning/ 锚定在 qy_lty\ 而非父级 Lila-Server\,遵循 CLAUDE.md「qy_ltyqy-lty-admin 是独立项目」原则;通过预创建空 .planning/ 目录强制锚定生效
  • 后续动作: 新功能开发使用 /gsd-new-milestone 启动候选优先级HIGH 项含成就条件校验缺失、SMS 限流、DEBUG/CORS 收紧、测试 MAC 后门移除、测试基础设施搭建)见 .planning/REQUIREMENTS.md

[2026-05-07] CLAUDE.md 新增「沟通语言」规则 — 强制中文回复

  • 文件路径: CLAUDE.md
  • 修改类型: 新增
  • 修改内容: 在文件顶部(项目概述之前)新增 ## 沟通语言(重要 — 始终生效) 章节明确所有面向用户的回复统一使用中文内部思考可用任意语言工具调用参数、commit message、代码注释保持项目原有约定此规则覆盖默认英文输出倾向仅在用户显式要求时切换。
  • 修改原因: 用户要求把"思考后的回答用中文显示"沉淀为本仓库长期生效的工作规则,避免每次会话重复声明,并让后续任何 Claude/Copilot 会话进入仓库即自动遵循。

[2026-04-24] 好感度系统 P1 阶段 — 数据模型扩展 + 迁移 + seed 命令

配套设计文档:docs/好感度系统功能与规则设计.md 配套任务清单:docs/好感度系统-开发任务清单.mdP1-01 ~ P1-10 全部完成)

本次改动把好感度系统的数据层从「用户级单值」(ParadiseUser.favorability)演进到「设备级独立计数」(UserDevice.favorability),并补齐规则、等级、配置、日志、计数器、奖励发放标记 6 类表,为后续 P2 service 层开发奠基。

P1-01 / P1-02 / P1-03 — AffinityRule、AffinityLevel 字段扩展

  • 文件路径: userapp/models.py
  • 修改类型: 重构
  • 修改内容:
    • AffinityRule 新增字段:rule_key(代码标识)、trigger_typeaction / companion_time / decaymin_change / max_change[min,max] 闭区间随机)、single_cap / daily_cap / cooldown_secondsis_negative / is_enabled / is_deletedmin_continuous_minutes / max_count_per_day(陪伴时长专用)
    • AffinityLevel 新增字段:min_affinity / max_affinity(区间)、unlock_contentreward_type / reward_currency / reward_itemsis_enabled / is_deleted
    • 旧字段 points / daily_limit / is_activeRulerequired_points / rewardsLevel保留作为兼容字段注释标记 "已弃用",下个版本删除
  • 修改原因:
    • 旧字段无法满足设计文档 §4 / §6 的规则与等级配置维度(缺范围、缺冷却、缺奖励细分)
    • 软删除字段 is_deleted 是 13.1-B1 默认方案的兜底,保留删除决策的可逆性

P1-04 — 新增 AffinitySetting单例表

  • 文件路径: userapp/models.py
  • 修改类型: 新增
  • 修改内容:
    • 新增 AffinitySetting 模型,存全局参数:initial_affinitymax_affinitydaily_cap(全局日上限)、衰减相关 6 字段、通知开关、timezone(默认 Asia/Shanghai
    • save() 强制单例:新增时若已有记录则覆盖到现有 pk
    • get_solo() 类方法:取唯一实例,不存在则创建默认
  • 修改原因: 设计文档 §3.2 全局参数 + §5.1 衰减字段需要持久化配置,单例表是简单可靠的存储模式

P1-05 — 新增 AffinityLog变化日志

  • 文件路径: userapp/models.py
  • 修改类型: 新增
  • 修改内容:
    • 新增 AffinityLog 模型:user / device(SET_NULL) / rule(SET_NULL) / rule_key(冗余文本)、change_value / before_value / after_valuesource5 种来源)、event_id(幂等去重)、operator_admin_id + reason(管理员调整审计)、metadata(JSON)
    • 索引:(device, -created_at) / (user, -created_at) / (rule_key, -created_at) / (source, -created_at)
    • 部分唯一约束 unique_affinity_event_id:仅当 event_id 非空时唯一
  • 修改原因:
    • 所有好感度变化必须可审计、可追溯(设计文档 §9.3 + §13 决策记录的 12 项)
    • event_id 唯一约束实现服务端去重(决策 C9

P1-06 — 新增 UserAffinityDailyCounter每日计数器

  • 文件路径: userapp/models.py
  • 修改类型: 新增
  • 修改内容: 新增 UserAffinityDailyCounter 模型:(device, rule, date) 唯一,accumulated_change + trigger_count
  • 修改原因: 热路径走 Redisdaily:{device}:{rule}:{YYYYMMDD}),数据库表作为审计兜底,每晚定时任务把 Redis 当日数据落库

P1-07 — 新增 UserLevelRewardGrant等级奖励发放标记

  • 文件路径: userapp/models.py
  • 修改类型: 新增
  • 修改内容: 新增 UserLevelRewardGrant 模型:(device, level) 唯一,reward_snapshot 保存发放时奖励快照
  • 修改原因:
    • 决策 3 + 决策 11升级逐级发奖励永久幂等衰减回升后不补发
    • reward_snapshot 防止 AffinityLevel 后续修改影响审计

P1-08 — UserDevice 加好感度字段(设备级模型核心)

  • 文件路径: device_interaction/models.py
  • 修改类型: 修改
  • 修改内容:
    • UserDevice 新增 4 字段:favorability(默认 10affinity_level(默认 1last_active_at(带 db_indexis_active(绑定有效软删除标记)
    • 在 docstring 中说明:UserDevice.is_active 是绑定软删除标记,与 Device.is_active(设备激活态)不是同一概念
  • 修改原因: 决策 8 — 好感度归属为「设备级」,每条用户-设备绑定独立维护值、等级、解锁内容

P1 自动迁移文件 — 由 makemigrations 生成

  • 文件路径:
    • device_interaction/migrations/0003_userdevice_affinity_level_userdevice_favorability_and_more.py
    • userapp/migrations/0005_affinitysetting_affinitylevel_is_deleted_and_more.py
  • 修改类型: 新增
  • 修改内容: Django 自动生成的 schema 迁移,按依赖顺序处理跨应用 FKAffinityLog → UserDevice
  • 修改原因: P1-01 ~ P1-08 模型变更需要落库

P1-09 — 数据迁移ParadiseUser.favorability → UserDevice.favorability

  • 文件路径: userapp/migrations/0006_migrate_favorability_to_userdevice.py
  • 修改类型: 新增
  • 修改内容:
    • 手写 RunPython 数据迁移:遍历所有 favorability > 0 的用户,写到主设备(无主设备则取最近绑定)
    • 仅当目标 UserDevice.favorability == 10(默认值)时写入,避免覆盖业务层后续修改
    • 提供 migrate_favorability_backward 回滚函数
    • ParadiseUser.favorability 字段保留不删,由后续版本统一清理
  • 修改原因: 设备级模型上线时,存量用户的好感度数据不能丢失,需平滑迁移到主设备

P1-10 — seed 默认数据 management command

  • 文件路径:
    • userapp/management/__init__.py(新建空文件)
    • userapp/management/commands/__init__.py(新建空文件)
    • userapp/management/commands/seed_affinity.py
  • 修改类型: 新增
  • 修改内容:
    • 新增 python manage.py seed_affinity 命令:写入 AffinitySetting 单例 + 8 条默认规则 + 5 个默认等级
    • 默认数据与设计文档 §4.2 / §6.2 一致;规则带 rule_keycooldown_secondschat=30stouch=10s其余 0等级带 min_affinity/max_affinity 闭区间
    • 幂等:默认按 rule_key / level 查询,已存在则跳过;--force 模式下覆盖已存在记录
  • 修改原因: 提供一键初始化能力,避免管理员手工逐条添加,且保证默认值与文档一致

后续步骤(不属于本次改动,留待用户确认后执行)

  1. 在合适时机执行 python manage.py migrate 应用 schema 变更和数据迁移
  2. 执行 python manage.py seed_affinity 写入默认规则/等级/配置
  3. 进入 P2 阶段service 层 + 管理端 API见任务清单

[2026-04-30] CLAUDE.md 新增"项目修改记录规则"段落

  • 文件路径: CLAUDE.md
  • 修改类型: 新增
  • 修改内容:
    • 在文末追加"项目修改记录规则(重要 — 自动执行)"段落,明确要求每次代码改动后必须在同一会话内追加到 docs/修改记录.md 顶部
    • 划清 qy_ltyqy-lty-admin 各自独立维护修改记录的边界,跨项目联动改动两端各写一条互相引用
    • 列出适用范围:业务/配置/迁移/CI/k8s/Dockerfile/文档结构性改动必须记录typo / 临时调试脚本可省
  • 修改原因:
    • 之前修改记录靠手工维护,部分改动遗漏未追加导致追踪历史中断
    • 规则写进 CLAUDE.md 后Claude Code 在每次会话中可自动遵守,减少漏记风险
    • 配套同步在 qy-lty-admin/CLAUDE.mdqy-lty-admin/docs/修改记录.md 建立独立修改记录骨架(详见 qy-lty-admin/docs/修改记录.md 同日条目)

[2026-04-29] strategy B group_send 推回消息体新增 timestamp_unix 字段

配套手机端记录:LTY_App_Project_URP/docs/修改记录.md 同日"修复 B' 双倒真正根因:时间戳时区解析"条目。

手机端实测 B' 方案出现 UI 双倒,根因定位为:服务端 chat_msg.timestamp.isoformat() 输出 UTC 带时区的 ISO8601+00:00),客户端 Unity Mono DateTime.TryParse 对此处理不稳定,可能丢失时区信息导致与本地时间戳比较时差 8 小时 → 替换匹配窗口15s永远不命中 → 走"作为新消息插入"兜底分支 → 双倒。

服务端最稳妥的修复方式:在 group_send payload 多附一个无时区歧义的 unix 秒级时间戳,让客户端优先使用。

修改strategy B 落库后 group_send payload 新增 timestamp_unix

  • 文件路径: device_interaction/views.py
  • 修改类型: 增强
  • 修改内容:
    • conversation_status action 内字幕落库分支(约 L1438 附近)的 channel_layer.group_send payload 新增字段:
      'timestamp_unix': int(chat_msg.timestamp.timestamp()),
      
    • 保留原 timestamp 字段ISO8601兼容老客户端不破坏现有约定
  • 修改原因:
    • Unix 秒级时间戳是绝对值,跨语言跨时区零歧义
    • 客户端 DateTimeOffset.FromUnixTimeSeconds(...).LocalDateTime 转换可靠
    • 服务端代价极小(一次 .timestamp() 调用),收益是消除一类隐性双倒 bug

客户端配套改动(仅记录依赖关系)

  • Assets/Scripts/AI/ChatLogManager.csServerPersistedData 结构体新增 long timestamp_unix 字段
  • OnServerChatPersisted 时间戳解析改为:优先 timestamp_unix > 0 → fallback DateTimeOffset.TryParse(timestamp) → fallback DateTime.Now
  • LoadChatHistoryFromServer 同步改用 DateTimeOffset.TryParseGET 接口暂未提供 unix 字段)

验证

服务端部署后,客户端 Console 应能在 [匹配诊断] 日志中看到 delta 缩小到秒级(之前是 ~28800s。修复确认后客户端会删除诊断日志。

待跟进

  • aiapp/views.pyRTCChatHistoryAPIView.get 也可在响应里加 timestamp_unix 字段进一步收紧(非必须,因为 GET 路径双倒不直接受影响 —— 走的是覆盖式拉取)

[2026-04-29] 手机端聊天记录切换服务端字幕落库B' 方案 服务端部分)

配套手机端方案文档:LTY_App_Project_URP/docs/手机端聊天记录_切换服务端字幕落库方案.md。手机端已实施 B'(本地 ASR 实时显示 + 服务端 webhook 静默替换服务端需要补三件事strategy B 落库后 group_send 推回客户端、DeviceConsumer 加 handler、RTCChatHistoryAPIView 灰度期去重 + since_id 增量拉取。

修改 1strategy B 落库成功后 group_send 转推

  • 文件路径: device_interaction/views.py
  • 修改类型: 新增功能
  • 修改内容:
    • conversation_status action 内字幕落库分支(约 L1414 ChatMessage.objects.create(...) 处):
      • create() 返回值赋给变量 chat_msg,落库成功 log 加上 id 字段
      • 落库成功后追加 channel_layer.group_send 调用,向 device_{paradise_user_id} 群组发送 type='chat_message_persisted' 消息payload 含 id / sender / message / timestamp / source_client
      • 用独立 try/except 包住,转推失败仅 warning 日志,不影响主落库流程
    • source_client 暂传 'unknown'(决策点 #3 落定后改为 'phone' / 'device'
  • 修改原因:
    • 手机端 B' 方案需要服务端在字幕入库后通过 WebSocket 把"权威 LLM 原始版本"推回客户端
    • 手机端按 chat_msg.id 去重 + 按 (sender, timestamp ±10s) 匹配本地待替换队列做静默替换,达到 UI 与 DB 字符级一致
    • 不影响设备端:设备端不订阅 chat_message_persisted 类型即可DeviceConsumer handler 仅向已实现处理的客户端透传)

修改 2DeviceConsumer 加 chat_message_persisted handler

  • 文件路径: device_interaction/consumers.py
  • 修改类型: 新增功能
  • 修改内容:
    • conversation_subtitle handler 之后新增 chat_message_persisted handler
    • 接收 group_send 事件后通过 self.send 把 JSON 推到 WebSocket 客户端
    • 日志记录 id / sender / source_client 用于后续排查
  • 修改原因:
    • Channels 协议要求 group_send 的 type 字段值在 Consumer 上有同名方法处理,否则消息被丢弃且报警
    • 必须与修改 1 同步部署,否则 strategy B 的 group_send 调用会失败

修改 3RTCChatHistoryAPIView 灰度期 POST 去重 + GET since_id 支持

  • 文件路径: aiapp/views.py
  • 修改类型: 新增功能 + 增强
  • 修改内容:
    • RTCChatHistoryAPIView.post() 入口加去重判定:同一 (user, bot, sender, message)±2s 时间窗内已存在则跳过 create,返回 deduplicated: true
    • RTCChatHistoryAPIView.get() 支持 since_id query 参数:传入则返回 id > since_id 的消息(升序,最多 page_size 条),未传则保持原最近 page_size 条逻辑
  • 修改原因:
    • 灰度期双倒保护:手机端 App 发版到用户手里需要时间,老版仍走 POST 落库strategy B webhook 此时也在落库 → 同一对话产生重复行。POST 去重让两条路径并存而不致脏库
    • 重放保护strategy B 自身被火山重试或客户端重连补提时,去重也能挡住
    • WebSocket 漏推兜底B' 方案手机端 5s 超时未收到 chat_message_persisted 时调 GET ?since_id=<last> 增量拉取替换队列里待修正的消息

关联代码(手机端,仅记录依赖关系)

  • 手机端 Assets/Scripts/Manager/WebSocketNetworking.cs 已新增 chat_message_persisted 类型分发分支
  • 手机端 Assets/Scripts/AI/ChatLogManager.cs 已新增 OnServerChatPersisted 方法、_pendingReplaceQueue 与 5s 超时兜底
  • 手机端 Assets/Scripts/AI/getJson.cs 已加 Config.SubtitleConfig.SubtitleMode=1

部署顺序与回滚

  • 部署顺序:服务端先部署(修改 1+2+3 三处一同上线)→ 验证 group_send 通道工作 → 手机端再发版
  • 回滚:三处改动都用独立 try/except 包住,可独立 git revert
    • 修改 1 revertstrategy B 主流程不受影响,只是不再 group_send手机端 UI 替换路径变为 5s 超时兜底
    • 修改 2 revert与修改 1 必须同时 revert否则 group_send 收方为空报警
    • 修改 3 revertPOST 不再去重(灰度期会出现双倒,需人工清理 DBGET 不再支持 since_id手机端兜底拉取无效

待跟进 TODO

  • 决策点 #3服务端区分手机端 / 设备端 RTC sessionmac 标记 / task_id 命名规则)→ source_client 字段填充真实值,让两端按需过滤
  • 决策点 #5服务端验证打断时是否仍 flush 部分内容;如不 flush手机端打断分支应跳过入待替换队列以避免 5s 超时空触发
  • Phase 0 步骤 1DB 双轨验证SQL 见 LTY_App_Project_URP/docs/手机端聊天记录_切换服务端字幕落库方案.md
  • Phase 0 步骤 4清理历史脏数据如发现

[2026-03-17] 修复手机号登录时 IntegrityError

  • 文件路径: userapp/views.py
  • 修改类型: 修复Bug
  • 修改内容: PhoneLoginView.post()get_or_create 新增 defaults={'username': phone_number}
  • 修改原因: 新用户首次通过手机号登录时,get_or_create 未设置 username 字段,导致 username="" 与数据库中已有空 username 记录冲突,触发 IntegrityError: duplicate key value violates unique constraint "userapp_paradiseuser_username_key"。改为用手机号作为默认 username保证唯一性。