From 5a57f9132407d94ce4a0d88ffd61fe1503c5d225 Mon Sep 17 00:00:00 2001
From: pmc <740076875@qq.com>
Date: Fri, 8 May 2026 09:33:26 +0800
Subject: [PATCH] =?UTF-8?q?docs(03):=20=E4=BB=8E=E7=94=A8=E6=88=B7?=
=?UTF-8?q?=E5=86=85=E8=81=94=E7=BA=A6=E6=9D=9F=E7=94=9F=E6=88=90=20Phase?=
=?UTF-8?q?=203=20CONTEXT.md=EF=BC=88=E5=AE=A2=E6=88=B7=E7=AB=AF=20GET=20?=
=?UTF-8?q?=E6=98=8E=E6=96=87=20+=20=E9=98=BF=E9=87=8C=E4=BA=91=E6=97=A5?=
=?UTF-8?q?=E5=BF=97=E8=84=B1=E6=95=8F=20PRD=20=E5=BF=AB=E9=80=9F=E9=80=9A?=
=?UTF-8?q?=E9=81=93=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../03-client-and-log-mask/03-CONTEXT.md | 167 ++++++++++++++++++
1 file changed, 167 insertions(+)
create mode 100644 qy_lty/.planning/phases/03-client-and-log-mask/03-CONTEXT.md
diff --git a/qy_lty/.planning/phases/03-client-and-log-mask/03-CONTEXT.md b/qy_lty/.planning/phases/03-client-and-log-mask/03-CONTEXT.md
new file mode 100644
index 0000000..b3d316d
--- /dev/null
+++ b/qy_lty/.planning/phases/03-client-and-log-mask/03-CONTEXT.md
@@ -0,0 +1,167 @@
+# Phase 3:客户端读取与日志脱敏 - Context
+
+**Gathered**: 2026-05-08
+**Status**: Ready for planning
+**Source**: 用户在 `/gsd-plan-phase 3` 调用时提供的内联约束(PRD 快速通道,与 Phase 1/2 同模式)
+
+
+## Phase 边界
+
+本 phase 是 Milestone v1.0 的**收尾 phase**,负责:
+1. **客户端 GET 接口**(CRED-05):在 `/api/credential-slot/`(**非** admin 命名空间)暴露 GET,user token 鉴权(`token:{token}` Redis key),**明文**返回供手机/设备端 Unity 实际调用第三方服务
+2. **阿里云日志脱敏**(CRED-06):在 logging 链路(formatter / filter / handler)识别 `access_token` 字段并脱敏,覆盖管理端 PUT 请求体 / 管理端 GET 响应体 / 客户端 GET 响应体三条最易泄露路径
+
+**与 Phase 2 的关键差异**:
+- Phase 2 是管理端(admin token + view 层脱敏 + 双端互引修改记录)
+- Phase 3 是客户端(user token + view 层**明文** + 日志层脱敏)
+- 同一份 `CredentialSlot` 单例,**view 行为相反**(admin 脱敏 / client 明文)
+
+**不负责**(推迟至 v2.x 或独立 milestone):
+- DB at-rest 加密(运营仍可在 admin 看到明文录入态)
+- token 轮换 / 短长 access/refresh token 双轨
+- 客户端调用频率限流
+- 与 Unity 客户端的 SDK 联调(在 `LTY_Project` / `LTY_App_Project_URP` 独立仓库进行)
+- 跨项目联动修改记录(Unity 客户端在独立 repo,不涉及 qy-lty-admin 前端,**不**写互引)
+
+
+
+
+## 实现决策(锁定)
+
+### CRED-05:客户端 GET 接口
+
+- **路径**:`/api/credential-slot/`(与管理端 `/api/v1/admin/credential-slot/` **完全分开**;客户端走 `/api/` 一级命名空间,避免被 admin token 限制误拒)
+- **HTTP 方法**:仅 GET(客户端不能写)
+- **路由注册位置**:planner 必须 grep `/api/` 全仓找到现有客户端接口的路由汇总点。候选:
+ - 候选 A:`qy_lty/urls.py` 已有 `path('api/', include(...))` 总入口;本 phase 在该 include 链下找一个最合适的子 urls
+ - 候选 B:在 aiapp 自己 `aiapp/urls.py` 加 `path('credential-slot/', ...)`,再确认它是否已被 `qy_lty/urls.py` 的 `path('api/', include('aiapp.urls'))` 收容
+ - 候选 C:参考现有客户端接口(如 `/api/user/...`、`/api/device/...`、`/api/ai/...`)的注册位置照抄
+ - **planner 在 read_first 阶段必须把这 3 个候选挨个验证,给出明确选定的 urls.py 文件路径与一行注册代码**
+- **View 类**:`CredentialSlotClientView`(命名与 Phase 2 的 `CredentialSlotAdminView` 形成对称)
+- **View 实现**:自定义 `APIView` + 仅 `get` 方法(**不要** GET/PUT 双方法 —— 客户端只读)
+- **View 放置位置**:`aiapp/views.py` 末尾(与 `CredentialSlotAdminView` 紧邻),保持单 app 内同语义聚合
+- **鉴权**:
+ - `authentication_classes = [RedisTokenAuthentication]` —— 复用 Phase 2 已 import
+ - `permission_classes = [IsAuthenticated]`
+ - **不**做 `is_staff` 校验(手机/设备 user 没有 staff 权限)
+ - `RedisTokenAuthentication` 通过 `userapp/utils.py:get_user_id_from_token` 自动识别 admin/user token,但本 view **不区分**两种 token —— 都允许(admin 用户也是手机用户的超集,让 admin 用 user token 能访问也合理;不构成安全问题,因为响应数据相同)
+- **响应序列化器**:**直接复用** Phase 2 的 `CredentialSlotSerializer`(同 app 同字段);客户端 view 只是不调 `mask_token` 替换,所以是 `success_response(data=serializer.data)` 直返
+- **响应壳层**:用 `common.responses.success_response`(与 Phase 2 一致)
+- **Swagger**:method-level `@swagger_auto_schema` + `get_standardized_response_schema`,response description 标注「**明文** APP ID + Access Token,供手机/设备端实际调用第三方服务」
+
+### CRED-06:阿里云日志脱敏
+
+**实现策略选择**:
+
+仓库现有日志架构(researcher 必须 read_first 实证):
+- `qy_lty/settings.py` 中的 `LOGGING` 配置块
+- `common/logging/` 目录(如存在)或 `aliyun_log_python_sdk` 集成位置
+- 关键问题:日志写入是用 Python 标准 `logging.Logger` + 自定义 handler/formatter,还是直接 SDK 调用?
+
+**两种实现路径**(planner 选其一并落地):
+
+**路径 A — 自定义 logging.Filter**(推荐,最低侵入):
+1. 在 `common/logging/filters.py`(如不存在则新建)实现 `AccessTokenMaskFilter(logging.Filter)`
+2. `filter(record)` 方法:用正则扫描 `record.msg` + `record.args`(dict / tuple / str),替换 `access_token` 字段值为 `mask_token(value)` 输出
+3. **覆盖路径**:在 `LOGGING.filters` 注册该 filter,在 `LOGGING.handlers.aliyun` / `console` 等 handler 上配置 `filters: ['access_token_mask']`
+4. **优点**:所有日志路径(middleware request/response 日志、view 内 logger 日志、Django 默认 access log)统一覆盖
+5. **缺点**:正则匹配 `access_token` 可能误伤无关字段(如某用户名恰好叫 `access_token`),但实际场景概率极低
+
+**路径 B — 重写 StandardResponseMiddleware 的日志输出 + view 内 logger.info 显式脱敏调用**(侵入大,覆盖面窄,**不推荐**):
+- 仅推荐路径 A
+
+**所以本 phase 走路径 A**:实现 `common/logging/filters.py:AccessTokenMaskFilter`,挂到 `settings.LOGGING.handlers` 的所有 handler 上。
+
+**正则模式**:planner 决定具体 regex;建议覆盖以下 4 种序列化形态:
+- JSON 字符串:`"access_token":\s*"([^"]+)"` → 替换 group(1) 为脱敏
+- Python dict repr:`'access_token':\s*'([^']+)'` → 替换 group(1) 为脱敏
+- query string:`access_token=([^&\s]+)` → 替换 group(1) 为脱敏
+- 单独 token 值(极少见):暂不处理,靠上面三个序列化形态兜底
+
+**留给 Claude's Discretion**:
+- filter 是用 `logging.Filter` 子类还是 `logging.Formatter` 子类(前者改 record,后者改格式化输出;两者效果等价,前者更易测)
+- 是否同时在 `common/middleware.py:StandardResponseMiddleware` 加一道辅助保险(response.body 写日志前显式脱敏)—— 视 researcher 看到 middleware 实际是否输出日志而定
+
+### 跨 phase 契约
+
+- 客户端 view 复用 `CredentialSlotSerializer`(**不**新增 `CredentialSlotClientSerializer`)—— 字段集与管理端完全一致,避免分化
+- `mask_token` 工具继续复用(filter 内调用)
+- `get_solo()` 类方法继续复用
+
+### 兼容性 / 不引入新依赖
+
+- 沿用 Django 4.2.13、Python 3.8、DRF 3.x、`aliyun-log-python-sdk`(如已在 requirements)
+- 不引入新依赖
+
+
+
+
+## Canonical References
+
+**下游 agent 必读**:
+
+### 项目宪法
+- `qy_lty/CLAUDE.md` — 沟通语言(中文)+ 修改记录强制规则
+- `qy_lty/.planning/PROJECT.md` — Milestone v1.0「关键约束」段(明确客户端必须明文返回)
+- `qy_lty/.planning/REQUIREMENTS.md` — Active 段 CRED-05 + CRED-06 完整描述
+- `qy_lty/.planning/ROADMAP.md` — Phase 3 详情段(Goal、Success Criteria 4 条)
+
+### Phase 1 + Phase 2 已有产物(必读,对照 contract)
+- `qy_lty/aiapp/models.py` — `CredentialSlot` 单例模型 + `get_solo()`
+- `qy_lty/aiapp/serializers.py` — `CredentialSlotSerializer`
+- `qy_lty/aiapp/views.py` — `CredentialSlotAdminView`(Phase 2 模板,照搬结构但行为反向)
+- `qy_lty/userapp/admin_urls.py` — admin 命名空间的注册(**对照参考**,本 phase 走客户端命名空间)
+- `qy_lty/common/utils.py` — `mask_token`
+- `qy_lty/common/responses.py` — `success_response` / `error_response`
+- `qy_lty/common/swagger_utils.py` — `get_standardized_response_schema`
+- `qy_lty/userapp/authentication.py` — `RedisTokenAuthentication`
+- `qy_lty/userapp/utils.py` — `get_user_id_from_token`(admin/user token 区分逻辑,对照确认客户端走 user 路径)
+
+### 客户端路由汇总点(researcher 必须找出)
+- `qy_lty/qy_lty/urls.py` — 顶层 urls,`api/` namespace 入口
+- 候选:`aiapp/urls.py` / `userapp/urls.py` / `device_interaction/urls.py` 之一是客户端接口聚合点
+- 任一现有客户端接口(如 `/api/user/mac-login/` 或 `/api/ai/...`)作为 1:1 注册风格参考
+
+### 日志架构(researcher 必须找出)
+- `qy_lty/qy_lty/settings.py` — `LOGGING` 配置块
+- `qy_lty/common/logging/` 目录(如存在)
+- 阿里云 SDK handler 类(如 `aliyun.log.QueuedLogHandler` 或自定义封装)
+
+### 修改记录
+- `qy_lty/docs/修改记录.md` — 顶部 Phase 1 + Phase 2 已有 4 条条目;Phase 3 在最顶部追加新条目
+
+
+
+
+## 具体要点(Success Criteria 显式化)
+
+| # | 验证点 | 检查方式 |
+|---|--------|----------|
+| 1 | 客户端 GET 携 user token 返回明文壳层 | `curl -H "Authorization: Bearer " /api/credential-slot/` 返回 200 + JSON `data.access_token == 'probe_secret_xxxx'`(明文,**非脱敏**) |
+| 2 | 无 token 返回 401 + 标准壳层 | `curl /api/credential-slot/` 返回 401 + JSON 含 `success: false` |
+| 3 | 过期 / 无效 token 返回 401 | 用伪造 token 调用,`RedisTokenAuthentication` 拒绝 |
+| 4 | 管理端 PUT 请求体在日志中脱敏 | 触发一次 `PUT /api/v1/admin/credential-slot/` 提交 `{"access_token": "test_log_secret_zzzz"}`,检查 `tail -100 ` 不含 `test_log_secret_zzzz` 完整明文(应被替换为 `*****************zzzz` 或类似) |
+| 5 | 管理端 GET 响应体已脱敏(Phase 2 + 日志层双重保险) | 触发一次 admin GET,日志文件搜索响应段不含完整明文 |
+| 6 | 客户端 GET 响应明文不写入日志 | 触发一次 `GET /api/credential-slot/`(携 user token),日志文件搜索请求/响应段不含完整 `probe_secret_xxxx` 明文(应被替换或不打印响应体)|
+| 7 | 端到端往返一致 | admin PUT 写入 `{app_id: 'roundtrip_test', access_token: 'rt_secret_RT99'}` → client GET 返回 `{app_id: 'roundtrip_test', access_token: 'rt_secret_RT99'}`(明文一致)|
+| 8 | Swagger 暴露客户端接口 | `/swagger.json/` 含 `/api/credential-slot/` 路径条目 + GET 方法 + 响应 schema description 标注「明文返回」 |
+| 9 | 修改记录顶部追加 Phase 3 条目 | grep `qy_lty/docs/修改记录.md` 顶部出现 `[2026-05-08] Phase 3` 字样;不写 qy-lty-admin 互引 |
+
+
+
+
+## 推迟事项(明确不在 Phase 3 范围)
+
+- **DB at-rest 加密 access_token 字段** — 留 v2.x 评估
+- **客户端调用频率限流** — 现架构未上限流
+- **token 轮换 / refresh token 机制** — 留独立 milestone
+- **Unity 客户端 SDK 联调** — 在 `LTY_Project` / `LTY_App_Project_URP` 独立仓库
+- **生产日志脱敏自动化测试**(CI 跑实日志验证)— 当前用本地 dev 日志手动验证;CI 测试基础设施待 brownfield 候选优先级 #5(pytest 体系)落地后再做
+- **DEBUG / CORS_ALLOW_ALL_ORIGINS 收紧** — brownfield 候选优先级 #3,留独立 phase
+
+
+
+---
+
+*Phase: 03-client-and-log-mask*
+*Context gathered: 2026-05-08 via inline PRD(用户在 /gsd-plan-phase 3 调用时提供完整约束)*