---
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 `<action>` 段规定的探针 + 单例自检：

```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 `<verification>` 段完整脚本（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 生成*
