--- phase: 01-credential-data-layer plan: 01 subsystem: aiapp / common tags: [credential, singleton, migration, mask, masking-util] requires: [] provides: - "aiapp.models.CredentialSlot(单例模型 + get_solo + save 钩子)" - "common.utils.mask_token(token, visible_tail=4, mask_char='*')" - "aiapp 迁移 0004_credentialslot.py(CreateModel)" - "DB 探针数据契约:pk=1 / app_id='probe_app' / access_token='probe_secret_xxxx'(供 Plan 02 浏览器 checkpoint 验证脱敏显示)" affects: - "Plan 01-02 Admin 注册 / 列表页脱敏将复用 mask_token 与 CredentialSlot.get_solo()" - "Phase 2 / Phase 3 REST 视图统一从 CredentialSlot.get_solo() 取数" - "Phase 3 阿里云日志 formatter 复用 common.utils.mask_token" tech_stack: added: [] patterns: - "Django 单例 = pk=1 + get_or_create + save() 钩子重定向(1:1 复刻 userapp.models.AffinitySetting 247-314 行)" - "中文字面量 verbose_name(与仓库 14 个模型实操一致;不引入 gettext_lazy)" key_files: created: - common/utils.py - aiapp/migrations/0004_credentialslot.py modified: - aiapp/models.py decisions: - "字段集合最小化:app_id(128) / access_token(512) / updated_at;不加 created_at(单例无创建语义)" - "单例靠 save() 钩子 + pk=1 静默重定向(不抛异常),与 AffinitySetting 一致;ROADMAP success criterion #1 解读为 count 守恒为 1,非异常拒绝" - "mask_token 短输入(len <= visible_tail)走全脱敏分支,防长度信号泄露" - "探针数据 probe_secret_xxxx 写入 DB 后保留不清理(Plan 02 浏览器 checkpoint 依赖)" metrics: duration_seconds: 184 tasks_completed: 3 tasks_total: 3 files_created: 2 files_modified: 1 commits: 3 completed_at: "2026-05-07T09:36Z" requirements: - CRED-01 --- # Phase 1 Plan 01-01:凭据槽位数据层 Summary **一句话**:落地 Milestone v1.0「通用凭据槽位」的数据基础——`CredentialSlot` 单例 Django 模型(pk=1 + save 钩子 + get_solo 三件套,1:1 复刻 AffinitySetting)+ 自动生成的 0004 迁移文件 + 通用 `mask_token` 工具函数(供 Phase 1 Admin / Phase 3 阿里云日志双方复用)。 ## 完成的 Tasks | Task | 名称 | Commit | 文件 | |------|------|--------|------| | 1 | 新建 common/utils.py 落地 mask_token 工具函数 | `a9c25eb` | common/utils.py(新建,32 行) | | 2 | 在 aiapp/models.py 末尾追加 CredentialSlot 单例模型 | `30c7caf` | aiapp/models.py(+42/-1) | | 3 | 自动生成迁移文件并执行 migrate | `a475fe4` | aiapp/migrations/0004_credentialslot.py(自动生成,26 行) | ## 实际新增 / 修改的代码行数 - `common/utils.py`:新建 32 行(含 docstring) - `aiapp/models.py`:从 52 行增至 93 行(+42 / -1,末尾追加 `CredentialSlot` 类含 4 字段 + Meta + `__str__` + `save` 钩子 + `get_solo` 类方法) - `aiapp/migrations/0004_credentialslot.py`:自动生成 26 行(依赖 `0003_create_rtc_bot`) ## 迁移文件实际名称 确认为 **`aiapp/migrations/0004_credentialslot.py`**,与 PLAN 期望一致。 依赖:`('aiapp', '0003_create_rtc_bot')`。 operations:`migrations.CreateModel(name='CredentialSlot', fields=[id BigAutoField, app_id CharField(128), access_token CharField(512), updated_at DateTimeField(auto_now=True)])`。 ## 自检 Shell 脚本输出 PLAN Task 3 `` 段规定的探针 + 单例自检: ```text created= True app_id= '' access_token= '' pk= 1 after second save count= 1 obj2.pk= 1 ``` PLAN Task 2 acceptance #9 规定的 N 次 save 守恒断言(4 次 save 验 count 恒为 1): ```text count_invariant_OK ``` PLAN `` 段完整脚本(assert 全部通过): ```text Plan 01 verification PASS ``` `showmigrations aiapp` 输出确认 `[X] 0004_credentialslot`: ```text aiapp [X] 0001_initial [X] 0002_initial [X] 0003_create_rtc_bot [X] 0004_credentialslot ``` `makemigrations aiapp --check --dry-run` 输出 `No changes detected in app 'aiapp'`,退出码 0,`CHECK_OK`。 ## mask_token 验证结果 ```text mask_token('sk-abcdef1234') == '*********1234' ✓ (末 4 位 '1234' 明文,前 9 字符脱敏) mask_token('') == '' ✓ (空串短路) mask_token(None) == '' ✓ (None 短路) mask_token('abc') == '***' ✓ (短输入全脱敏) mask_token('abcd') == '****' ✓ (恰等 visible_tail 全脱敏) mask_token('abcde') == '*bcde' ✓ (长 1 位露 4 位) mask_token('probe_secret_xxxx') == '*************xxxx' ✓ (与 Plan 02 浏览器 checkpoint 期望串一致) ``` ## 探针数据当前值确认 `SELECT app_id, access_token FROM aiapp_credentialslot WHERE pk=1` 通过 ORM 等价: ```text pk=1, app_id='probe_app', access_token='probe_secret_xxxx', count=1 ``` **Plan 02 Task 2 浏览器 checkpoint mask 期望值算法**: - 原 token:`probe_secret_xxxx`(长度 17) - `mask_token(...)` 返回:`*************xxxx`(13 个 `*` + 末 4 位 `xxxx`,总长 17) - 故 Admin 列表页 `access_token_masked` 列应渲染为 `*************xxxx` ## 给下游的 Hand-off | 下游 | 公开入口 / 契约 | |------|----------------| | Plan 01-02(Admin 注册) | `from aiapp.models import CredentialSlot` 取模型;`from common.utils import mask_token` 写 `access_token_masked(self, obj)`;用 `CredentialSlot.objects.exists()` 判断是否禁用「新增」按钮 | | Plan 01-02 浏览器 checkpoint | 依赖 DB 中 `pk=1, access_token='probe_secret_xxxx'` 探针;列表页脱敏期望串 `*************xxxx` | | Phase 2 管理端 REST | 单例统一访问入口:`CredentialSlot.get_solo()`(不要直接 `CredentialSlot.objects.first()` 防止空 DB 时拿 None);GET 响应序列化时调用 `mask_token(obj.access_token)` | | Phase 3 客户端 REST | 同样用 `CredentialSlot.get_solo()` 取数;客户端 GET 返回明文(不调 mask_token) | | Phase 3 日志脱敏 | 阿里云日志 formatter 用 `from common.utils import mask_token` 直接复用,签名兼容 | ## Deviations from Plan ### 自动调整(无需用户介入) **1. [Rule 3 - Blocking] verify 段 findstr /R 在 PowerShell + GBK 编码下不可靠** - **发现位置**:Task 3 verify 命令 `python manage.py showmigrations aiapp | findstr /R "0004.*\[X\]"` - **现象**:findstr 在 PowerShell 中报 `OSError: [Errno 22]` + 中文 `findstr: 无法` 乱码,pipe 因 stderr 警告污染失败 - **修复**:改用 `python manage.py showmigrations aiapp 2>nul` 直接看输出,逐行肉眼+grep 校验 `[X] 0004_credentialslot` 命中 - **影响**:仅影响 verify 显示方式,不影响功能 acceptance;showmigrations 输出已确认 `[X]` 标记到位 - **文件**:无代码改动,纯 verify 流程调整 ### 观察(不阻塞) **2. 迁移文件头部注释显示 `Generated by Django 5.2.12`** - **PLAN / PROJECT.md / CLAUDE.md 记录的版本是 Django 4.2.13** - **实际表现**:本机 Python 环境的 `django` 包是 5.2.12(pip 装的全局包),但项目代码是按 4.2 写的(迁移格式、字段属性、Meta 选项均跨 4.x/5.x 兼容,无破坏) - **未阻塞**:迁移成功生成 + 成功应用;模型行为完全符合 acceptance;`CredentialSlot` 类与 `AffinitySetting` 在 4.2 / 5.2 下行为等价 - **建议**:本仓库部署使用 Docker 镜像(CLAUDE.md 写明),Docker 内才是 4.2.13;本地开发版本漂移属于已知现象,不在本 plan 范围。可考虑在 Phase 3 收尾时由独立运维 plan 或 deferred-items 处理(建议加固 venv / requirements.txt 锁版本,但 PROJECT.md 已说"不锁版本,靠 Docker")。 ## 不在本 Plan 范围(按 PLAN 约束严格执行) - **未写 docs/修改记录.md 条目**(PLAN 与执行 prompt 显式说明:本 Plan 不写修改记录,由 Plan 01-02 Task 3 一并写两条 — 避免重复条目) - **未注册 Django Admin**(CRED-02,由 Plan 01-02 落地) - **未写 REST 接口**(Phase 2 / Phase 3) - **未引入新依赖**(沿用 Django 4.2 / Python 3.8 已有栈) ## 覆盖的需求与 ROADMAP Success Criteria - ✓ **CRED-01**:单例 `CredentialSlot` 模型 + 迁移落地;DB 层 count 守恒为 1(save 钩子保证);含 app_id / access_token / updated_at 三字段 - ✓ **ROADMAP Phase 1 Success Criterion #1**:DB / 模型层强制最多一条(save() 钩子静默重定向 pk,count 守恒,与 AffinitySetting 等价语义) - ✓ **ROADMAP Phase 1 Success Criterion #2**:迁移落地 + schema 字段齐全(migrate 退出码 0;showmigrations 显示 `[X]`;CreateModel 含 4 列) - ✓ **ROADMAP Phase 1 Success Criterion #3**:`get_or_create(pk=1)` 首访拿到 `created=True / app_id='' / access_token=''` 空记录 - — Phase 1 Success Criterion #4 / #5 / #6(Admin 列表 / 编辑 / 禁删)→ Plan 01-02 负责 ## Self-Check: PASSED 文件存在确认: ```text common/utils.py -> FOUND aiapp/models.py(含 class CredentialSlot) -> FOUND aiapp/migrations/0004_credentialslot.py -> FOUND .planning/phases/01-credential-data-layer/01-01-SUMMARY.md -> FOUND(本文件) ``` Commit 存在确认(`git log --oneline` 命中): ```text a9c25eb feat(01-01): 新增 common/utils.py 含 mask_token 工具函数 -> FOUND 30c7caf feat(01-01): aiapp 新增 CredentialSlot 单例模型 -> FOUND a475fe4 feat(01-01): 自动生成并应用 0004_credentialslot 迁移 -> FOUND ``` DB 状态确认:`aiapp_credentialslot` 表存在 pk=1 单条记录,`access_token='probe_secret_xxxx'` 探针就绪供 Plan 01-02 使用。 --- *由 /gsd-execute-phase 顺序执行器于 2026-05-07 生成*