385 lines
21 KiB
Markdown
385 lines
21 KiB
Markdown
---
|
||
phase: 01-credential-data-layer
|
||
plan: 01
|
||
type: execute
|
||
wave: 1
|
||
depends_on: []
|
||
files_modified:
|
||
- common/utils.py
|
||
- aiapp/models.py
|
||
- aiapp/migrations/0004_credentialslot.py
|
||
autonomous: true
|
||
requirements:
|
||
- CRED-01
|
||
must_haves:
|
||
truths:
|
||
- "common.utils.mask_token('sk-abcdef1234') 返回 '*********1234'(末 4 位明文)"
|
||
- "common.utils.mask_token('') 与 mask_token(None) 都返回 ''"
|
||
- "common.utils.mask_token('abc') 返回 '***'(短输入全脱敏)"
|
||
- "aiapp.models.CredentialSlot 类存在并含 app_id / access_token / updated_at 三个字段"
|
||
- "首次执行 CredentialSlot.objects.get_or_create(pk=1) 时 created=True,obj.app_id == '' 且 obj.access_token == ''"
|
||
- "在已有 1 条记录的情况下执行 CredentialSlot(app_id='x', access_token='y').save(),DB 仍只有 1 条记录,且新对象 pk 被重定向到现有那条"
|
||
- "python manage.py migrate 退出码为 0,PostgreSQL 出现 aiapp_credentialslot 表"
|
||
artifacts:
|
||
- path: common/utils.py
|
||
provides: "mask_token(token, visible_tail=4, mask_char='*') 工具函数"
|
||
contains: "def mask_token"
|
||
min_lines: 20
|
||
- path: aiapp/models.py
|
||
provides: "CredentialSlot 单例模型(含 save 钩子 + get_solo 类方法)"
|
||
contains: "class CredentialSlot(models.Model)"
|
||
- path: aiapp/migrations/0004_credentialslot.py
|
||
provides: "CreateModel(name='CredentialSlot') 迁移"
|
||
contains: "CreateModel"
|
||
key_links:
|
||
- from: aiapp/models.py
|
||
to: aiapp/migrations/0004_credentialslot.py
|
||
via: "makemigrations 自动生成;pattern 是正则;用 grep -E 匹配"
|
||
pattern: "name='CredentialSlot'"
|
||
- from: aiapp/models.py CredentialSlot.save
|
||
to: AffinitySetting.save 模式
|
||
via: "1:1 复刻 userapp/models.py:303-308;pattern 是正则;用 grep -E 匹配"
|
||
pattern: "if not self.pk and CredentialSlot.objects.exists"
|
||
---
|
||
|
||
<objective>
|
||
落地 Milestone v1.0「通用凭据槽位」Phase 1 的数据层 + 通用脱敏工具:
|
||
- 在 aiapp 新增 `CredentialSlot` 单例 Django 模型(pk=1 + save 钩子 + get_solo),覆盖 CRED-01
|
||
- 自动生成迁移文件,dev 环境 `python manage.py migrate` 通过
|
||
- 在 common/utils.py 新建 `mask_token(token, visible_tail=4)` 工具函数,本 phase Plan 02 的 Admin 脱敏会复用,Phase 3 的阿里云日志 formatter 也会复用
|
||
|
||
Purpose:为 Phase 2 管理端 REST、Phase 3 客户端 REST + 日志脱敏奠基。本 plan 不直接产生 REST 响应,也不动 admin。
|
||
|
||
Output:3 个文件(1 新建、1 追加、1 自动生成)。
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@.planning/PROJECT.md
|
||
@.planning/ROADMAP.md
|
||
@.planning/STATE.md
|
||
@.planning/REQUIREMENTS.md
|
||
@.planning/phases/01-credential-data-layer/01-CONTEXT.md
|
||
@.planning/phases/01-credential-data-layer/01-RESEARCH.md
|
||
@CLAUDE.md
|
||
@userapp/models.py
|
||
@aiapp/models.py
|
||
@aiapp/apps.py
|
||
|
||
<interfaces>
|
||
<!-- 关键已存在符号,executor 不需要再去 grep -->
|
||
|
||
来自 userapp/models.py(AffinitySetting 单例三件套,第 247-314 行 — 直接照抄结构):
|
||
```python
|
||
class AffinitySetting(models.Model):
|
||
# ...字段省略...
|
||
class Meta:
|
||
verbose_name = '好感度系统设置'
|
||
verbose_name_plural = '好感度系统设置'
|
||
|
||
def save(self, *args, **kwargs):
|
||
# 强制单例:新增时如果已有记录则覆盖到现有 pk
|
||
if not self.pk and AffinitySetting.objects.exists():
|
||
existing = AffinitySetting.objects.first()
|
||
self.pk = existing.pk
|
||
super().save(*args, **kwargs)
|
||
|
||
@classmethod
|
||
def get_solo(cls):
|
||
"""获取单例实例,不存在则用默认值创建"""
|
||
instance, _ = cls.objects.get_or_create(pk=1)
|
||
return instance
|
||
```
|
||
|
||
来自 aiapp/apps.py:
|
||
```python
|
||
class AiappConfig(AppConfig):
|
||
default_auto_field = 'django.db.models.BigAutoField'
|
||
name = 'aiapp'
|
||
verbose_name = 'AI'
|
||
```
|
||
(说明 aiapp 已正常注册;新增模型会用 BigAutoField 主键,pk=1 含义不变)
|
||
|
||
来自 aiapp/models.py(当前内容,executor 在文件末尾追加,不动 Bot / ChatMessage):
|
||
```python
|
||
# 文件顶部已有:
|
||
from django.db import models
|
||
from userapp.models import ParadiseUser
|
||
|
||
class Bot(models.Model): ... # 行 5-14
|
||
class ChatMessage(models.Model): ... # 行 16-52
|
||
# 文件末尾追加 CredentialSlot
|
||
```
|
||
|
||
aiapp 现有迁移序号(来自 ls aiapp/migrations/):
|
||
- 0001_initial.py
|
||
- 0002_initial.py
|
||
- 0003_create_rtc_bot.py
|
||
- → 新迁移会自动命名 `0004_credentialslot.py`(makemigrations 编号)
|
||
</interfaces>
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto">
|
||
<name>Task 1:新建 common/utils.py 落地 mask_token 工具函数</name>
|
||
<files>common/utils.py</files>
|
||
<read_first>
|
||
- common/__init__.py(看 common 是否真的不是 Django app — 应只有 1 行或为空,无 apps.py)
|
||
- common/responses.py(看 common 既有纯工具文件的 docstring / 注释风格)
|
||
- common/pagination.py(同上,确认命名 / 风格一致)
|
||
</read_first>
|
||
<action>
|
||
新建文件 `common/utils.py`(**必须是新建,禁止覆盖既有文件**;ls 显示当前 common/ 下没有 utils.py),完整内容如下:
|
||
|
||
```python
|
||
"""通用工具函数集合。
|
||
|
||
注:common/ 不是 Django app(无 apps.py、未注册到 INSTALLED_APPS),
|
||
仅作为跨 app 的纯函数 utility 命名空间使用。
|
||
|
||
不要在此放 Django Model / Manager / 任何依赖 app registry 的对象。
|
||
"""
|
||
|
||
|
||
def mask_token(token: str, visible_tail: int = 4, mask_char: str = '*') -> str:
|
||
"""脱敏长 token / secret,仅保留末 N 位明文。
|
||
|
||
设计动机:CredentialSlot.access_token 在 Admin 列表 / 查看态需仅显示末 4 位;
|
||
Phase 3 阿里云日志 formatter 也将复用本函数。
|
||
|
||
Args:
|
||
token: 待脱敏字符串;空字符串 / None 直接返回 ''
|
||
visible_tail: 末尾保留明文的字符数(默认 4)
|
||
mask_char: 掩码字符(默认 *)
|
||
|
||
Returns:
|
||
脱敏后的字符串。例:
|
||
'sk-abcdef1234' -> '*********1234'
|
||
'' -> ''
|
||
None -> ''
|
||
'abc' -> '***' # 短于 visible_tail 时全部脱敏,不暴露长度信号
|
||
"""
|
||
if not token:
|
||
return ''
|
||
if len(token) <= visible_tail:
|
||
return mask_char * len(token)
|
||
return mask_char * (len(token) - visible_tail) + token[-visible_tail:]
|
||
```
|
||
|
||
关键约束:
|
||
- 仅这一个函数,不要顺手加 mask_dict / mask_email / 其它脱敏变体(Phase 3 才需要 dict 递归版)
|
||
- 不要 `import` 任何 Django 模块(保持 common/utils.py 是纯 Python utility)
|
||
- 短输入兜底分支必须保留(见 docstring 示例 `'abc' -> '***'`)— 这是有意防长度信号泄露
|
||
</action>
|
||
<verify>
|
||
<automated>cd C:\Users\admin\Desktop\Lila-Server\qy_lty && python -c "from common.utils import mask_token; assert mask_token('sk-abcdef1234') == '*********1234'; assert mask_token('') == ''; assert mask_token(None) == ''; assert mask_token('abc') == '***'; assert mask_token('abcd') == '****'; assert mask_token('abcde') == '*bcde'; print('OK')"</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- 文件 `common/utils.py` 存在
|
||
- 文件首行后含 `def mask_token(token: str, visible_tail: int = 4, mask_char: str = '*') -> str:` 签名(grep 命中)
|
||
- 上面 verify 命令退出码 0 且输出 `OK`
|
||
- 文件不含 `import django` / `from django` 等 Django 依赖(grep 0 命中)
|
||
- 文件不含其它 mask_* 函数(grep `^def mask` 仅 1 命中)
|
||
</acceptance_criteria>
|
||
<done>verify 命令打印 OK;以上 5 条 acceptance criteria 全部满足。</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 2:在 aiapp/models.py 末尾追加 CredentialSlot 单例模型</name>
|
||
<files>aiapp/models.py</files>
|
||
<read_first>
|
||
- aiapp/models.py(看当前 51 行内容;新增内容必须追加到末尾,不修改 Bot / ChatMessage)
|
||
- userapp/models.py 第 247-314 行(AffinitySetting 单例三件套样板,要 1:1 复刻 save 钩子 + get_solo 写法)
|
||
- .planning/phases/01-credential-data-layer/01-RESEARCH.md「问题 2」「问题 5」段(字段命名 / verbose_name / __str__ 等约定)
|
||
</read_first>
|
||
<action>
|
||
在 `aiapp/models.py` 文件末尾(第 52 行 `ChatMessage.__str__` 之后)追加以下完整代码块(**不**修改文件已有 Bot / ChatMessage 任何一行,也**不**改动 `from django.db import models` / `from userapp.models import ParadiseUser` 这两个 import):
|
||
|
||
```python
|
||
|
||
|
||
class CredentialSlot(models.Model):
|
||
"""通用凭据槽位(单例)— Milestone v1.0 / Phase 1
|
||
|
||
全局唯一一条记录,存第三方服务(Kimi / 阿里云 / 火山等)的 APP ID + Access Token。
|
||
通过 pk=1 + get_or_create + save() 钩子三件套保证单例:
|
||
- 任何 .save() 在已有记录时把新对象 pk 改成现有那条
|
||
- get_solo() 是单一访问入口(Phase 2/3 视图统一调用)
|
||
- DB 层无额外约束,绕过 ORM 的 bulk_create / 原始 SQL 不在保护范围
|
||
"""
|
||
|
||
app_id = models.CharField(
|
||
'APP ID', max_length=128, blank=True, default='',
|
||
help_text='第三方服务商分配的 APP ID;运营在 Admin 录入'
|
||
)
|
||
access_token = models.CharField(
|
||
'Access Token', max_length=512, blank=True, default='',
|
||
help_text='第三方服务商访问令牌;DB 明文存储,Admin 列表/查看态末 4 位脱敏'
|
||
)
|
||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||
|
||
class Meta:
|
||
verbose_name = '凭据槽位'
|
||
verbose_name_plural = '凭据槽位'
|
||
|
||
def __str__(self):
|
||
return f"凭据槽位 (updated {self.updated_at:%Y-%m-%d %H:%M})"
|
||
|
||
def save(self, *args, **kwargs):
|
||
# 强制单例:新增时如果已有记录则覆盖到现有 pk
|
||
if not self.pk and CredentialSlot.objects.exists():
|
||
existing = CredentialSlot.objects.first()
|
||
self.pk = existing.pk
|
||
super().save(*args, **kwargs)
|
||
|
||
@classmethod
|
||
def get_solo(cls):
|
||
"""获取单例实例,不存在则用默认值创建(pk=1)"""
|
||
instance, _ = cls.objects.get_or_create(pk=1)
|
||
return instance
|
||
```
|
||
|
||
关键约束(违反任一即失败):
|
||
- 字段定义必须严格对齐:`app_id` 是 CharField(128, blank=True, default=''),`access_token` 是 CharField(512, blank=True, default=''),`updated_at` 是 DateTimeField(auto_now=True)
|
||
- **不**新增 `created_at`(CONTEXT.md 字段集合未列;单例无"创建"语义)
|
||
- **不**加 `null=True`(与 RESEARCH 问题 5 约定一致 — 用 `blank=True, default=''`)
|
||
- **不**用 `gettext_lazy as _`(与本仓库 14 个模型一致用中文字面量;详见 RESEARCH 问题 3)
|
||
- **不**加 `Meta.constraints` / `unique_together` / `UniqueConstraint`(单例靠 save 钩子,不靠 DB 约束 — RESEARCH 反模式段已说明)
|
||
- save 钩子必须用 `existing.pk` 动态写法,**不**得硬编码 `self.pk = 1`(RESEARCH 反模式段说明)
|
||
- **不**新建 `aiapp/models/credential_slot.py` 子包(CONTEXT 决策 D-01:追加到末尾,不拆子文件)
|
||
</action>
|
||
<verify>
|
||
<automated>cd C:\Users\admin\Desktop\Lila-Server\qy_lty && python -c "import django, os; os.environ.setdefault('DJANGO_SETTINGS_MODULE','qy_lty.settings'); django.setup(); from aiapp.models import CredentialSlot; assert CredentialSlot._meta.verbose_name == '凭据槽位'; fields = {f.name: f for f in CredentialSlot._meta.get_fields()}; assert 'app_id' in fields and 'access_token' in fields and 'updated_at' in fields; assert fields['app_id'].max_length == 128; assert fields['access_token'].max_length == 512; assert hasattr(CredentialSlot, 'get_solo'); print('OK')"</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `aiapp/models.py` 行数从 52 增至约 95(追加 ~43 行,含空行)
|
||
- grep `class CredentialSlot(models.Model):` 在 `aiapp/models.py` 命中 1 次
|
||
- grep `class Bot(models.Model):` 与 `class ChatMessage(models.Model):` 仍各命中 1 次(未被破坏)
|
||
- grep `def get_solo(cls):` 在 `aiapp/models.py` 命中 1 次
|
||
- grep `if not self.pk and CredentialSlot.objects.exists` 在 `aiapp/models.py` 命中 1 次
|
||
- grep `gettext_lazy` / `from django.utils.translation` 在 `aiapp/models.py` 0 命中
|
||
- grep `created_at` 在 `aiapp/models.py` 0 命中(CredentialSlot 不应有 created_at)
|
||
- 上面 verify 的 Django shell 一行命令退出码 0、输出 OK
|
||
- **count 守恒断言**(Plan-level success criterion #1 的"DB 最多 1 条"由 save() 静默重定向实现,非异常拒绝):在 Task 3 migrate 落地后跑一次 shell 自检 — `python -c "import django, os; os.environ.setdefault('DJANGO_SETTINGS_MODULE','qy_lty.settings'); django.setup(); from aiapp.models import CredentialSlot; CredentialSlot.objects.get_or_create(pk=1); CredentialSlot(app_id='probe1', access_token='probe_secret_xxxx').save(); CredentialSlot(app_id='probe2', access_token='another_value').save(); CredentialSlot(app_id='probe3', access_token='third_value').save(); count = CredentialSlot.objects.count(); assert count == 1, f'singleton violated: count={count}'; print('count_invariant_OK')"`,必须无异常抛出且打印 `count_invariant_OK`(验证 N 次 save 后 count 仍为 1)
|
||
</acceptance_criteria>
|
||
<done>verify 命令打印 OK;以上 9 条 acceptance criteria 全部满足;Bot / ChatMessage 未被改动;count 守恒断言通过(N 次 save 后 count 仍为 1)。</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 3:自动生成迁移文件并执行 migrate</name>
|
||
<files>aiapp/migrations/0004_credentialslot.py</files>
|
||
<read_first>
|
||
- aiapp/migrations/0003_create_rtc_bot.py(看本仓库迁移文件命名 / 头部注释风格)
|
||
- aiapp/migrations/0001_initial.py(看 dependencies 段格式)
|
||
</read_first>
|
||
<action>
|
||
在仓库根目录执行:
|
||
|
||
```bash
|
||
cd C:\Users\admin\Desktop\Lila-Server\qy_lty
|
||
python manage.py makemigrations aiapp
|
||
```
|
||
|
||
期望:Django 自动生成 `aiapp/migrations/0004_credentialslot.py`,其内容包含 `migrations.CreateModel(name='CredentialSlot', fields=[...])`。**不要手写迁移文件**;如果 makemigrations 没有生成新迁移(报 "No changes detected"),说明 Task 2 的模型未正确落地,回到 Task 2 排查,**不**得手动创建迁移文件。
|
||
|
||
生成成功后立即执行:
|
||
|
||
```bash
|
||
python manage.py migrate aiapp
|
||
```
|
||
|
||
期望:输出 `Applying aiapp.0004_credentialslot... OK`,退出码 0。
|
||
|
||
生成 + migrate 通过后,做一次 shell 自检(验证 success criterion #1 + #3):
|
||
|
||
```bash
|
||
python manage.py shell -c "from aiapp.models import CredentialSlot; obj, created = CredentialSlot.objects.get_or_create(pk=1); print('created=', created, 'app_id=', repr(obj.app_id), 'access_token=', repr(obj.access_token), 'pk=', obj.pk); obj2 = CredentialSlot(app_id='probe_app', access_token='probe_secret_xxxx'); obj2.save(); print('after second save count=', CredentialSlot.objects.count(), 'obj2.pk=', obj2.pk)"
|
||
```
|
||
|
||
期望输出(首次执行):
|
||
```
|
||
created= True app_id= '' access_token= '' pk= 1
|
||
after second save count= 1 obj2.pk= 1
|
||
```
|
||
(若是非首次,`created=False` 也可,关键是 `count=1` 与 `obj2.pk=1` 必须满足)
|
||
|
||
**重要 — 探针数据契约(与 Plan 02 联动)**:本任务故意往 DB 写入一条 `access_token='probe_secret_xxxx'` 的记录,且**不清理**。Plan 02 Task 2 浏览器 checkpoint 会读这条数据来验证脱敏显示(`probe_secret_xxxx` 长度 17,mask_token 期望返回 13 个 `*` + `xxxx` = `*************xxxx`)。Executor 必须确认 shell 自检里 `obj2.access_token == 'probe_secret_xxxx'` 写入成功,否则 Plan 02 的脱敏期望串会失配。
|
||
|
||
关键约束:
|
||
- **禁止手写**迁移;必须由 makemigrations 生成
|
||
- 迁移文件名必须是 `0004_credentialslot.py`(Django 默认);如出现冲突命名(如 `0004_xxx.py` 已被别的功能占用),停下并报告,**不**得改名硬塞
|
||
- 执行 migrate 失败时不要回滚,把完整错误贴回 — 多数原因是 settings 未指向正确数据库或 PostgreSQL 未启动,需用户介入
|
||
- 探针记录 `probe_secret_xxxx` 不要清理(Plan 02 依赖此值;Plan 02 checkpoint 完成后由运营覆写为真实值)
|
||
</action>
|
||
<verify>
|
||
<automated>cd C:\Users\admin\Desktop\Lila-Server\qy_lty && python manage.py makemigrations aiapp --check --dry-run && python manage.py migrate aiapp && python manage.py showmigrations aiapp | findstr /R "0004.*\[X\]"</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- 文件 `aiapp/migrations/0004_credentialslot.py` 存在
|
||
- grep `CreateModel` 在该迁移文件命中 1 次以上
|
||
- grep `name='CredentialSlot'` 在该迁移文件命中 1 次
|
||
- `python manage.py makemigrations aiapp --check --dry-run` 退出码 0(无未生成的模型变更)
|
||
- `python manage.py migrate aiapp` 退出码 0(迁移成功应用,无 'unrecognized arguments' 报错 — 注意 Django 4.2 的 migrate 子命令**没有** `--check` 选项,只有 makemigrations 才有)
|
||
- `python manage.py showmigrations aiapp` 输出中 `0004_credentialslot` 行带 `[X]` 标记(表示已应用)
|
||
- 上面的 shell 自检命令打印 `count= 1` 且 `obj2.pk= 1`
|
||
- 数据库中 `aiapp_credentialslot` 表至少有 1 条 pk=1 的记录(探针写入留下了 'probe_app' / 'probe_secret_xxxx',**不需要清理** — Plan 02 的 Admin checkpoint 会覆盖它,且整个 phase 的目标本就是让单条记录可被运营随时改写)
|
||
</acceptance_criteria>
|
||
<done>verify 命令三段(makemigrations --check --dry-run / migrate / showmigrations findstr)都退出码 0 且 findstr 命中 `0004.*[X]`;上述 8 条 acceptance criteria 全部满足。</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<verification>
|
||
本 plan 完成后必须能在 `python manage.py shell` 中跑通以下脚本(与 RESEARCH §代码示例「验证 success criteria 的 Django shell 脚本」一致)且全部 assert 通过:
|
||
|
||
```python
|
||
from aiapp.models import CredentialSlot
|
||
from common.utils import mask_token
|
||
|
||
obj, created = CredentialSlot.objects.get_or_create(pk=1)
|
||
# created 首次为 True,二次为 False,都接受;关键是 pk=1
|
||
assert obj.pk == 1
|
||
|
||
obj2 = CredentialSlot(app_id='test_app', access_token='secret123456')
|
||
obj2.save()
|
||
assert CredentialSlot.objects.count() == 1
|
||
assert obj2.pk == 1
|
||
|
||
assert mask_token('sk-abcdef1234') == '*********1234'
|
||
assert mask_token('') == ''
|
||
assert mask_token(None) == ''
|
||
assert mask_token('abc') == '***'
|
||
|
||
print("Plan 01 verification PASS")
|
||
```
|
||
|
||
**关于 ROADMAP success criterion #1("DB 层或模型层拒绝出现第二条记录")的实现说明**:
|
||
本 plan 用 **save() 钩子静默重定向 pk** 的方式(仓库既有 AffinitySetting 同款),而非抛 IntegrityError 拒绝。即 `CredentialSlot(app_id='x', access_token='y').save()` 在已存在记录时**不抛异常**,而是把新对象的 pk 改写为现有那条的 pk,于是 super().save() 走 UPDATE 路径,不创建第二行。最终效果是 **count 守恒为 1**,与 ROADMAP 目标"最多一条"语义等价。verify-work agent 在判定 success criterion #1 时应以"N 次 save 后 count == 1"为准,**不**应期望异常被抛出。Task 2 acceptance 已加 count 守恒 shell 断言显式验证此语义。
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- 三个文件按 acceptance criteria 落地:common/utils.py(新建)、aiapp/models.py(追加 CredentialSlot)、aiapp/migrations/0004_credentialslot.py(自动生成)
|
||
- 覆盖 ROADMAP Phase 1 success criteria 第 1、2、3 条(DB 单例约束、迁移落地 + 字段齐全、首访 get_or_create 拿空记录)
|
||
- 注:第 1 条的"DB / 模型层强制最多一条"由 save() 钩子静默重定向 pk 实现(count 守恒),非异常拒绝;详见 verification 段说明
|
||
- 覆盖 REQ CRED-01:单例 CredentialSlot 模型 + 迁移;DB 层最多 1 条记录(save 钩子保证);含 app_id / access_token / updated_at
|
||
- mask_token 已可被 Plan 02 与 Phase 3 直接 import 复用
|
||
- **本 plan 不涉及 Admin / REST / 修改记录**(修改记录由 Plan 02 一并写两条;Admin 由 Plan 02 落地)
|
||
- **探针数据契约**:Task 3 在 DB 留下 `access_token='probe_secret_xxxx'` 的记录,Plan 02 Task 2 浏览器 checkpoint 依赖此值验证脱敏显示
|
||
</success_criteria>
|
||
|
||
<output>
|
||
完成后由 /gsd-execute-phase 自动生成 `.planning/phases/01-credential-data-layer/01-01-SUMMARY.md`,须含:
|
||
- 实际新增 / 修改的代码行数
|
||
- 迁移文件实际名称(确认是 0004_credentialslot.py)
|
||
- 自检 shell 脚本的输出
|
||
- mask_token 验证结果
|
||
- 给 Plan 02 / Phase 2 / Phase 3 的 hand-off 说明(CredentialSlot.get_solo() / mask_token 是公开入口)
|
||
- **探针数据当前值确认**:`SELECT app_id, access_token FROM aiapp_credentialslot WHERE pk=1` 的实际内容(让 Plan 02 Task 2 checkpoint 知道 mask 期望串以何值算)
|
||
</output>
|
||
</content>
|
||
</invoke> |