From 68f4ceb0b9cd23621eb9e42a707afcabbe66e5bd Mon Sep 17 00:00:00 2001
From: pmc <740076875@qq.com>
Date: Thu, 7 May 2026 17:30:49 +0800
Subject: [PATCH] =?UTF-8?q?docs(01):=20Phase=201=20PLAN.md=20=C3=972?=
=?UTF-8?q?=EF=BC=8801-01=20=E6=A8=A1=E5=9E=8B=E8=BF=81=E7=A7=BB=E5=B7=A5?=
=?UTF-8?q?=E5=85=B7=20+=2001-02=20Admin=20=E4=BF=AE=E6=94=B9=E8=AE=B0?=
=?UTF-8?q?=E5=BD=95=EF=BC=89=EF=BC=8Cplan-checker=20=E9=80=9A=E8=BF=87=20?=
=?UTF-8?q?1=20=E8=BD=AE=E4=BF=AE=E8=AE=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../01-credential-data-layer/01-01-PLAN.md | 385 ++++++++++++++++
.../01-credential-data-layer/01-02-PLAN.md | 424 ++++++++++++++++++
2 files changed, 809 insertions(+)
create mode 100644 qy_lty/.planning/phases/01-credential-data-layer/01-01-PLAN.md
create mode 100644 qy_lty/.planning/phases/01-credential-data-layer/01-02-PLAN.md
diff --git a/qy_lty/.planning/phases/01-credential-data-layer/01-01-PLAN.md b/qy_lty/.planning/phases/01-credential-data-layer/01-01-PLAN.md
new file mode 100644
index 0000000..897edfc
--- /dev/null
+++ b/qy_lty/.planning/phases/01-credential-data-layer/01-01-PLAN.md
@@ -0,0 +1,385 @@
+---
+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"
+---
+
+
+落地 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 自动生成)。
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.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
+
+
+
+
+来自 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 编号)
+
+
+
+
+
+
+ Task 1:新建 common/utils.py 落地 mask_token 工具函数
+ common/utils.py
+
+ - common/__init__.py(看 common 是否真的不是 Django app — 应只有 1 行或为空,无 apps.py)
+ - common/responses.py(看 common 既有纯工具文件的 docstring / 注释风格)
+ - common/pagination.py(同上,确认命名 / 风格一致)
+
+
+ 新建文件 `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' -> '***'`)— 这是有意防长度信号泄露
+
+
+ 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')"
+
+
+ - 文件 `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 命中)
+
+ verify 命令打印 OK;以上 5 条 acceptance criteria 全部满足。
+
+
+
+ Task 2:在 aiapp/models.py 末尾追加 CredentialSlot 单例模型
+ aiapp/models.py
+
+ - 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__ 等约定)
+
+
+ 在 `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:追加到末尾,不拆子文件)
+
+
+ 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')"
+
+
+ - `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)
+
+ verify 命令打印 OK;以上 9 条 acceptance criteria 全部满足;Bot / ChatMessage 未被改动;count 守恒断言通过(N 次 save 后 count 仍为 1)。
+
+
+
+ Task 3:自动生成迁移文件并执行 migrate
+ aiapp/migrations/0004_credentialslot.py
+
+ - aiapp/migrations/0003_create_rtc_bot.py(看本仓库迁移文件命名 / 头部注释风格)
+ - aiapp/migrations/0001_initial.py(看 dependencies 段格式)
+
+
+ 在仓库根目录执行:
+
+ ```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 完成后由运营覆写为真实值)
+
+
+ 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\]"
+
+
+ - 文件 `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 的目标本就是让单条记录可被运营随时改写)
+
+ verify 命令三段(makemigrations --check --dry-run / migrate / showmigrations findstr)都退出码 0 且 findstr 命中 `0004.*[X]`;上述 8 条 acceptance criteria 全部满足。
+
+
+
+
+
+本 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 断言显式验证此语义。
+
+
+
+- 三个文件按 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 依赖此值验证脱敏显示
+
+
+
+
+
\ No newline at end of file
diff --git a/qy_lty/.planning/phases/01-credential-data-layer/01-02-PLAN.md b/qy_lty/.planning/phases/01-credential-data-layer/01-02-PLAN.md
new file mode 100644
index 0000000..91a7f8c
--- /dev/null
+++ b/qy_lty/.planning/phases/01-credential-data-layer/01-02-PLAN.md
@@ -0,0 +1,424 @@
+---
+phase: 01-credential-data-layer
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 01-01
+files_modified:
+ - aiapp/admin.py
+ - docs/修改记录.md
+autonomous: false
+requirements:
+ - CRED-02
+must_haves:
+ truths:
+ - "Django Admin 列表页 /admin/aiapp/credentialslot/ 中 access_token 列显示为末 4 位掩码(如 '*********1234')"
+ - "Admin 编辑页 /admin/aiapp/credentialslot/1/change/ 中 access_token 字段是 input 控件,预填明文(运营可改写)"
+ - "已存在 1 条记录时,Admin 列表页右上角无「增加 凭据槽位」按钮"
+ - "Admin 编辑页底部无「删除」按钮;批量动作下拉无「删除所选的 凭据槽位」选项"
+ - "qy_lty/docs/修改记录.md 顶部存在两条 [日期] Phase 1 — ... 条目(CRED-01 数据层 + CRED-02 Admin 各一条),位于第 23 行注释 `` 之下;两条都包含『跨项目联动: 无』字段"
+ - "qy-lty-admin/docs/修改记录.md 不被改动(本 phase 是纯服务端改动;CLAUDE.md 跨项目规则下,纯服务端不需要在前端写互引条目)"
+ artifacts:
+ - path: aiapp/admin.py
+ provides: "CredentialSlotAdmin(脱敏 + 单例新增约束 + 禁删)"
+ contains: "class CredentialSlotAdmin(admin.ModelAdmin)"
+ - path: docs/修改记录.md
+ provides: "Phase 1 两条修改记录条目(CRED-01 + CRED-02),均含『跨项目联动: 无』"
+ contains: "Phase 1 — 凭据槽位数据层"
+ key_links:
+ - from: aiapp/admin.py CredentialSlotAdmin.access_token_masked
+ to: common.utils.mask_token
+ via: "from common.utils import mask_token;pattern 是正则;用 grep -E 匹配(fixed string 也可命中)"
+ pattern: "from common.utils import mask_token"
+ - from: aiapp/admin.py CredentialSlotAdmin
+ to: aiapp.models.CredentialSlot
+ via: "@admin.register(CredentialSlot);pattern 是正则,括号已转义;用 grep -E 匹配,**不要**用 grep -F 或纯字面量匹配(否则反斜杠会被当字面量)"
+ pattern: "@admin.register\\(CredentialSlot\\)"
+ - from: aiapp/admin.py CredentialSlotAdmin.has_add_permission
+ to: 单例语义
+ via: "已存在记录时返回 False,按钮自动隐藏;pattern 是正则;用 grep -E 匹配"
+ pattern: "return not CredentialSlot.objects.exists"
+---
+
+
+完成 Milestone v1.0 / Phase 1 的 Admin 接入与文档归档:
+- 在 aiapp/admin.py 注册 `CredentialSlotAdmin`:列表 / 查看态脱敏(仅末 4 位);编辑态明文供运营录入;隐藏「增加」按钮;禁止删除(覆盖 CRED-02)
+- 浏览器端通过 SimpleUI 主题做一次人工 checkpoint,验收 ROADMAP Phase 1 success criteria 第 4、5、6 条
+- 在 qy_lty/docs/修改记录.md 顶部追加 2 条修改记录条目(CRED-01 数据层 + CRED-02 Admin),含『跨项目联动: 无』字段,满足 CLAUDE.md 强制规则
+
+Purpose:让运营能在 SimpleUI 后台安全地录入第三方服务凭据,且 DB 单例语义不会被运营误操作破坏;同时把 Phase 1 的 codebase 改动正式归档到修改记录。
+
+Output:1 个文件追加(aiapp/admin.py 末尾追加 CredentialSlotAdmin 与新 import)+ 1 个文件追加(docs/修改记录.md 顶部插入两条条目,每条均含『跨项目联动: 无』字段)。
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/phases/01-credential-data-layer/01-CONTEXT.md
+@.planning/phases/01-credential-data-layer/01-RESEARCH.md
+@CLAUDE.md
+@aiapp/admin.py
+@aiapp/models.py
+@common/utils.py
+@docs/修改记录.md
+
+
+
+
+来自 Plan 01 已交付(前置依赖,executor 应能直接 import):
+```python
+# aiapp/models.py(Plan 01 追加)
+class CredentialSlot(models.Model):
+ app_id = models.CharField('APP ID', max_length=128, blank=True, default='')
+ access_token = models.CharField('Access Token', max_length=512, blank=True, default='')
+ updated_at = models.DateTimeField('更新时间', auto_now=True)
+ @classmethod
+ def get_solo(cls): ...
+
+# common/utils.py(Plan 01 新建)
+def mask_token(token: str, visible_tail: int = 4, mask_char: str = '*') -> str: ...
+```
+
+**Plan 01 探针数据契约**(Plan 01 Task 3 在 DB 留下的探针记录):
+- pk=1,`app_id='probe_app'`,`access_token='probe_secret_xxxx'`(17 字符)
+- 经 mask_token(visible_tail=4) 处理后,列表页应显示 `*************xxxx`(13 个 `*` + `xxxx`)
+- 注:跨 session 执行可能此值已被改写;Task 2 浏览器 checkpoint 前置准备段会用 shell 探针读出实际 access_token 再按实际值算 mask 期望串
+
+来自 aiapp/admin.py 当前 15 行内容(executor 必须保留这两个既有 ModelAdmin、仅追加,不重写):
+```python
+from django.contrib import admin
+from .models import Bot, ChatMessage
+
+@admin.register(Bot)
+class BotAdmin(admin.ModelAdmin):
+ list_display = ('id', 'name', 'description')
+ search_fields = ('id', 'name', 'description')
+
+@admin.register(ChatMessage)
+class BotAdmin(admin.ModelAdmin): # 注:仓库现状的 class 名误用 BotAdmin,executor 不要顺手改 — 不在 phase scope
+ list_display = ('id', 'user', 'bot', 'message', 'timestamp', 'sender', 'message_type')
+ search_fields = ('id', 'user', 'bot', 'message', 'timestamp', 'sender', 'message_type')
+```
+
+来自 docs/修改记录.md 文件结构(追加位置定位):
+```
+第 1 行: # 服务器端代码修改记录
+第 7-19 行:## 修改格式说明(含模板代码块)
+第 22 行: ## 修改历史
+第 23 行:
+第 24 行: (空行)
+第 26 行: ### [2026-05-07] 引入 GSD 工作流并完成 brownfield 文档化初始化
+```
+新条目必须插在第 23 行注释下、第 26 行既有最新条目之上(即变成新的"最前")。
+
+来自 qy-lty-admin/docs/修改记录.md(路径 ../qy-lty-admin/docs/修改记录.md,**本 plan 不动**):
+- 已存在该文件,格式与本仓库一致
+- 本 phase 是纯服务端改动(无前端联动),按 CLAUDE.md 规则**不需要**在前端写互引条目
+- Task 3 在两条新条目中各加一行『跨项目联动: 无 — ...』字段,留下决策痕迹(INFO #2 调整:本字段从原 Task 4 合并到 Task 3 模板,避免对刚写入产物的二次修改)
+
+
+
+
+
+
+ Task 1:在 aiapp/admin.py 注册 CredentialSlotAdmin(脱敏 + 单例新增 + 禁删)
+ aiapp/admin.py
+
+ - aiapp/admin.py(当前 15 行;executor 须保留 BotAdmin 与 ChatMessage 注册不动,仅追加)
+ - aiapp/models.py(确认 CredentialSlot 已由 Plan 01 落地)
+ - common/utils.py(确认 mask_token 可 import)
+ - userapp/admin.py(看本仓库 ModelAdmin fieldsets / readonly_fields 写法风格)
+ - .planning/phases/01-credential-data-layer/01-RESEARCH.md「问题 3」「问题 4」「陷阱 2」「陷阱 3」段(脱敏字段必须在 list_display 用方法名而非真实字段名;access_token 不可放 readonly_fields)
+
+
+ 修改 `aiapp/admin.py` 两处:
+
+ **(1) 第 3 行原 import**:
+ ```python
+ from .models import Bot, ChatMessage
+ ```
+ 改写为:
+ ```python
+ from .models import Bot, ChatMessage, CredentialSlot
+ from common.utils import mask_token
+ ```
+
+ **(2) 在文件末尾(第 15 行 ChatMessage 注册块之后)追加完整新块**:
+
+ ```python
+
+
+ @admin.register(CredentialSlot)
+ class CredentialSlotAdmin(admin.ModelAdmin):
+ """通用凭据槽位 Admin(单例)— Milestone v1.0 / Phase 1
+
+ UX 行为:
+ - 列表 / 查看态 access_token 显示末 4 位掩码
+ - 编辑表单 access_token 明文(运营录入需要)
+ - 已存在记录时隐藏「增加」按钮
+ - 永远禁止删除(防运营误操作丢失单例)
+ """
+
+ list_display = ('id', 'app_id', 'access_token_masked', 'updated_at')
+ readonly_fields = ('updated_at',)
+
+ fieldsets = (
+ ('凭据信息', {
+ 'fields': ('app_id', 'access_token'),
+ 'description': '第三方服务商分配的 APP ID + Access Token;保存后立即对手机端 / 设备端生效',
+ }),
+ ('元数据', {
+ 'fields': ('updated_at',),
+ 'classes': ('collapse',),
+ }),
+ )
+
+ def access_token_masked(self, obj):
+ return mask_token(obj.access_token)
+ access_token_masked.short_description = 'Access Token (脱敏)'
+
+ def has_add_permission(self, request):
+ # 已存在记录时隐藏「增加」,配合 has_delete_permission 强制单例
+ return not CredentialSlot.objects.exists()
+
+ def has_delete_permission(self, request, obj=None):
+ # 永远禁止删除(含批量动作)
+ return False
+ ```
+
+ 关键约束(违反任一即失败):
+ - **不**把 `access_token` 放进 `readonly_fields`(否则编辑表单也会变只读,运营无法录入;见 RESEARCH 陷阱 2)
+ - **不**把 `access_token_masked` 放进 `fields` / `fieldsets`(计算字段只用于 list_display;见 RESEARCH 模式 2)
+ - `has_add_permission` 必须是"已存在则 False"的条件式写法,**不**得永远返回 False(否则首次部署运营无法录入第一条)
+ - `has_delete_permission` 必须永远返回 False(含 obj=None 的批量动作场景;见 RESEARCH 陷阱 3)
+ - **不**重写 BotAdmin / ChatMessage 注册块(仓库现状的 class 名 `class BotAdmin(admin.ModelAdmin):` 用于 ChatMessage 是历史遗留,**不在 phase 1 修复 scope**)
+ - **不**用 `gettext_lazy` / `_()`(与 RESEARCH 问题 3 决策一致 — 中文字面量与 14 个其它模型保持一致)
+ - **不**新增 search_fields / list_filter(单例只有 1 行,搜索 / 过滤无意义;UX discretion 决策)
+
+
+ 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 django.contrib import admin; from aiapp.models import CredentialSlot; ma = admin.site._registry[CredentialSlot]; assert 'access_token_masked' in ma.list_display; assert 'access_token' not in ma.readonly_fields; assert ma.has_delete_permission(None, None) is False; from aiapp.admin import CredentialSlotAdmin; print('OK')"
+
+
+ - grep `class CredentialSlotAdmin(admin.ModelAdmin):` 在 `aiapp/admin.py` 命中 1 次
+ - grep `from common.utils import mask_token` 在 `aiapp/admin.py` 命中 1 次
+ - grep `from .models import Bot, ChatMessage, CredentialSlot` 在 `aiapp/admin.py` 命中 1 次
+ - grep `return not CredentialSlot.objects.exists` 在 `aiapp/admin.py` 命中 1 次
+ - grep `def has_delete_permission` 在 `aiapp/admin.py` 命中 1 次,函数体含 `return False`
+ - grep `'access_token'` 在 `readonly_fields = ` 同一行 0 命中(access_token 不在 readonly)
+ - grep `access_token_masked.short_description` 在 `aiapp/admin.py` 命中 1 次
+ - 上面 verify 命令退出码 0、输出 OK(注:`has_delete_permission(None, None)` 显式传入 request=None 与 obj=None 两个位置参数,避免依赖默认值,更鲁棒)
+ - aiapp/admin.py 仍然包含 `@admin.register(Bot)` 与 `@admin.register(ChatMessage)`(grep 各 1 命中,旧注册未被破坏)
+
+ verify 命令打印 OK;以上 9 条 acceptance criteria 全部满足。
+
+
+
+ Task 2:浏览器端人工验收 Admin UX(success criteria #4 / #5 / #6)
+
+ Plan 01 已落地 CredentialSlot 单例模型 + 迁移 + mask_token;
+ 本 Plan 上一 task 已注册 CredentialSlotAdmin(脱敏 + 单例新增约束 + 禁删)。
+
+ 现在需要人工在浏览器中验收 ROADMAP Phase 1 的三个 UI 行为类 success criteria。
+
+
+ **前置准备**:
+ 1. 确保已有 superuser(如无,cd 到仓库根目录跑 `python manage.py createsuperuser` 临时建一个)
+ 2. 启动开发服务器:`cd C:\Users\admin\Desktop\Lila-Server\qy_lty && python manage.py runserver` 或 `./run.sh`(生产用 daphne)
+ 3. 浏览器访问 `http://localhost:8000/admin/`,用 superuser 登录
+ 4. **读取探针数据当前实际值并算出脱敏期望串**(避免硬编码字符串与 DB 实际状态不符):
+ ```bash
+ python manage.py shell -c "from aiapp.models import CredentialSlot; from common.utils import mask_token; obj = CredentialSlot.objects.filter(pk=1).first(); print('CURRENT app_id =', repr(obj.app_id) if obj else None); print('CURRENT access_token =', repr(obj.access_token) if obj else None); print('EXPECTED masked =', repr(mask_token(obj.access_token)) if obj else None)"
+ ```
+ 期望输出(Plan 01 Task 3 探针未被改写时):
+ ```
+ CURRENT app_id = 'probe_app'
+ CURRENT access_token = 'probe_secret_xxxx'
+ EXPECTED masked = '*************xxxx'
+ ```
+ 后续验收 1 「期望 B」「期望 C」「期望 E」用此处打印的 `EXPECTED masked` / `CURRENT access_token` 实际值替代下文的样板字符串。
+
+ **验收 1(success criterion #4 — 列表/编辑态脱敏行为)**:
+ - 访问 `http://localhost:8000/admin/aiapp/credentialslot/`
+ - **期望 A**:列表页表头含 `Id / APP ID / Access Token (脱敏) / 更新时间` 四列
+ - **期望 B**:列表页第 1 行的 `Access Token (脱敏)` 列显示**应等于**上面前置准备打印的 `EXPECTED masked` 值。
+ - 默认场景(探针未被改写):access_token 为 `probe_secret_xxxx`(17 字符),mask_token(visible_tail=4) 返回 `*************xxxx`(13 个 `*` + `xxxx`)。
+ - 校验方法:数显示串的字符总数应等于 `len(CURRENT access_token)`(即 17),其中末 4 位为明文 `xxxx`、前 13 位全为 `*`。**严禁**与"15 星 + xxxx"或"11 星 + xxxx"等任何与实际探针长度不符的字符串比对。
+ - 点击该行进入编辑页 `http://localhost:8000/admin/aiapp/credentialslot/1/change/`
+ - **期望 C**:编辑表单中 `Access Token` 字段是普通 input,**预填明文**(值等于前置准备打印的 `CURRENT access_token`,默认场景下为 `probe_secret_xxxx`)— 不是脱敏文本
+ - **期望 D**:编辑表单中 `更新时间` 字段为只读(不可编辑)
+ - 在编辑表单把 `APP ID` 改为 `kimi_test`、`Access Token` 改为 `sk-test-1234567890abcdef`,点保存
+ - **期望 E**:保存后回到列表页,`Access Token (脱敏)` 列显示 `********************cdef`(共 24 字符 = 20 个 `*` + 末 4 位 `cdef`;因为 `sk-test-1234567890abcdef` 长度为 24)
+
+ **验收 2(success criterion #5 — 列表页无「增加」按钮)**:
+ - 当前列表页右上角应**无**「增加 凭据槽位」按钮(DB 中已有 1 条记录,`has_add_permission` 返回 False)
+ - **期望**:手动访问 `http://localhost:8000/admin/aiapp/credentialslot/add/` 应返回 403 Forbidden 或被重定向到列表页(Django 默认行为)
+
+ **验收 3(success criterion #6 — 编辑页无「删除」按钮 + 批量动作无「删除所选」)**:
+ - 在编辑页 `http://localhost:8000/admin/aiapp/credentialslot/1/change/` 底部按钮区域应**无**「删除」按钮(仅有「保存」「保存并继续编辑」「保存并增加另一个」之类的保存按钮族)
+ - 回到列表页,顶部「动作」下拉框应**无**「删除所选的 凭据槽位」选项(应只有空选项或其它非删除动作)
+ - **期望**:手动访问 `http://localhost:8000/admin/aiapp/credentialslot/1/delete/` 应返回 403 Forbidden
+
+ **如发现任一期望不满足**,把现象 + 截图 / 错误信息描述清楚后回报;executor 会回到 Task 1 修复后再次进入本 checkpoint。
+
+ 输入 "approved"(5 条期望全部通过);或描述未通过的期望 + 现象 + 截图(驳回到 Task 1 修复)
+
+
+
+ Task 3:在 qy_lty/docs/修改记录.md 顶部追加 Phase 1 两条条目(CRED-01 + CRED-02),含『跨项目联动』字段
+ docs/修改记录.md
+
+ - docs/修改记录.md 第 1-50 行(确认追加位置 = 第 23 行注释 `` 之下;确认格式 = 第 7-19 行说明段;确认风格 = 第 26 行 / 第 47 行的既有条目示例)
+ - .planning/phases/01-credential-data-layer/01-RESEARCH.md「问题 6」段(修改记录格式 + Phase 1 推荐两条条目模板)
+ - CLAUDE.md 第 256-281 行(修改记录硬规则 + 跨项目联动规则)
+
+
+ **当前日期请用 `${TODAY}`**,executor 在落地时替换为实际日期 `YYYY-MM-DD`(系统时间今日为 `2026-05-07`,若 executor 执行日期不同请用执行当日)。
+
+ 在 `docs/修改记录.md` 第 23 行注释 `` 与第 26 行既有最新条目 `### [2026-05-07] 引入 GSD 工作流` 之间,插入以下两条新条目(**条目顺序:CRED-02 在最上方,CRED-01 紧随其后;这样从上往下读最新在最前**,与文件约定一致 — Plan 02 是更新的提交)。
+
+ **重要(INFO #2 调整)**:两条条目都必须直接包含 `- **跨项目联动**: 无 — ...` 字段(合在本 Task 一次写入;原计划由 Task 4 二次追加该字段,已废弃 — 那种二次写入会让 verify-work agent 误判 Task 3 失败)。
+
+ 模板:
+
+ ```markdown
+ ### [${TODAY}] Phase 1 — Django Admin 注册凭据槽位(脱敏 + 单例约束 + 禁删)
+
+ 配套 Phase:[.planning/phases/01-credential-data-layer/](.planning/phases/01-credential-data-layer/)
+ 覆盖需求:CRED-02
+
+ - **文件路径**: `aiapp/admin.py`(修改 — 顶部 import 追加 `CredentialSlot` 与 `mask_token`,文件末尾追加 `CredentialSlotAdmin` 注册)
+ - **修改类型**: 新增
+ - **修改内容**:
+ - 注册 `CredentialSlotAdmin`:`list_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`:已存在记录时返回 `False`(Admin 列表页隐藏「增加」按钮,强制单例语义)
+ - 重写 `has_delete_permission`:永远返回 `False`(含批量动作;防运营误删丢失单例)
+ - 不修改既有 `BotAdmin` / `ChatMessageAdmin` 注册块
+ - **修改原因**: CRED-02 — 在 SimpleUI 后台为运营提供受控的凭据录入入口;列表 / 查看态脱敏防截图 / 录屏泄露;编辑态保留明文供录入;新增 / 删除按钮隐藏强制单例语义不被运营误操作破坏
+ - **跨项目联动**: 无 — 本改动仅触及服务端 Django Admin(运营访问 `/admin/aiapp/credentialslot/` 直接录入),与 `qy-lty-admin/`(Web 管理后台前端)无 API 联动;CLAUDE.md 跨项目规则下纯服务端改动不需要在 `qy-lty-admin/docs/修改记录.md` 写互引条目。Phase 2 暴露 `/api/v1/admin/credential-slot/` 接口时再做前后端联动。
+
+ ### [${TODAY}] Phase 1 — 凭据槽位数据层(CredentialSlot 单例模型 + 迁移 + mask_token 工具)
+
+ 配套 Phase:[.planning/phases/01-credential-data-layer/](.planning/phases/01-credential-data-layer/)
+ 覆盖需求:CRED-01
+ 设计参考:1:1 复刻 `userapp.models.AffinitySetting`(`userapp/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 app):`app_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.py`,`python 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` 字段
+ - **跨项目联动**: 无 — 本改动是纯数据层 + 工具函数,无任何 HTTP / WebSocket 接口暴露,`qy-lty-admin` 与 Unity 客户端均无感知;不需要在前端写互引条目。
+
+ ```
+
+ (执行规则:上面整段连同两条条目一起插入;条目之间保留一个空行;最后一条条目与既有 `### [2026-05-07] 引入 GSD 工作流` 条目之间也保留一个空行)
+
+ 关键约束(违反任一即失败):
+ - **不**修改 / 删除 / 重排第 1-23 行的文件头说明与 `## 修改历史` 标题与定位注释
+ - **不**修改 / 删除 / 重排已有的 `### [2026-05-07] 引入 GSD 工作流...` 与 `### [2026-05-07] CLAUDE.md 新增...` 与 `### [2026-04-24] 好感度系统 P1...` 等任何既有条目
+ - 两条新条目顺序:**CRED-02 在上、CRED-01 在下**(最新在最前;本 Plan 的 admin 注册晚于 Plan 01 的模型)
+ - 日期格式严格 `[YYYY-MM-DD]` — 与既有条目一致(**不**用 `[2026-5-7]` 单数字月日)
+ - 「修改类型」必须是说明段第 13 行预设值之一(新增 / 修改 / 删除 / 重构 / 修复Bug),本 phase 用「新增」
+ - 不在条目里塞图片 / base64 / 大段代码块 — 这是变更日志,不是设计文档
+ - 两条条目都必须包含 `- **跨项目联动**: 无 — ...` 字段(**本 Task 一次写入完整模板**;不要预留为空让 Task 4 后补)
+ - 「跨项目联动」字段措辞与上面模板一致,不要意译 / 简化(这段措辞是为后续 verify-work agent 准备的、可被 grep 命中的"否定决策"标记)
+
+
+ cd C:\Users\admin\Desktop\Lila-Server\qy_lty && python -c "t = open('docs/修改记录.md', encoding='utf-8').read(); assert 'Phase 1 — Django Admin 注册凭据槽位' in t; assert 'Phase 1 — 凭据槽位数据层' in t; assert 'CRED-01' in t; assert 'CRED-02' in t; assert '引入 GSD 工作流' in t; assert t.index('Django Admin 注册凭据槽位') < t.index('凭据槽位数据层(CredentialSlot'); assert t.index('凭据槽位数据层(CredentialSlot') < t.index('引入 GSD 工作流'); admin_block = t[t.index('Phase 1 — Django Admin'):t.index('Phase 1 — 凭据槽位数据层')]; data_block = t[t.index('Phase 1 — 凭据槽位数据层'):t.index('引入 GSD 工作流')]; assert '**跨项目联动**: 无' in admin_block, 'Admin 条目缺少跨项目联动'; assert '**跨项目联动**: 无' in data_block, '数据层条目缺少跨项目联动'; assert 'qy-lty-admin' in admin_block; assert 'Phase 2 暴露' in admin_block; print('OK')"
+
+
+ - grep `Phase 1 — Django Admin 注册凭据槽位(脱敏 + 单例约束 + 禁删)` 在 `docs/修改记录.md` 命中 1 次
+ - grep `Phase 1 — 凭据槽位数据层(CredentialSlot 单例模型 + 迁移 + mask_token 工具)` 在 `docs/修改记录.md` 命中 1 次
+ - grep `引入 GSD 工作流并完成 brownfield 文档化初始化` 在 `docs/修改记录.md` 仍命中 1 次(旧条目未被破坏)
+ - grep `CLAUDE.md 新增「沟通语言」规则` 在 `docs/修改记录.md` 仍命中 1 次(旧条目未被破坏)
+ - grep `**跨项目联动**: 无` 在 `docs/修改记录.md` 命中 2 次(两条 Phase 1 条目各 1)
+ - grep `qy-lty-admin` 在 Admin 条目内命中至少 1 次(出现在跨项目联动字段措辞中)
+ - grep `Phase 2 暴露` 在 Admin 条目内命中 1 次
+ - 上面 verify 的 Python 一行命令退出码 0、输出 OK(含顺序断言:Admin 条目在 数据层 条目之上、数据层 条目在 GSD 条目之上;以及两条都含『跨项目联动: 无』)
+ - 文件首行仍为 `# 服务器端代码修改记录`
+ - 文件第 23 行附近仍含注释 ``
+ - 两条新条目都包含 `- **文件路径**:` `- **修改类型**:` `- **修改内容**:` `- **修改原因**:` `- **跨项目联动**:` 五个加粗字段
+
+ verify 命令打印 OK;以上 11 条 acceptance criteria 全部满足;既有条目与文件头说明区均未被破坏;两条新条目都内嵌『跨项目联动: 无』字段(无需 Task 4 二次追加)。
+
+
+
+ Task 4:纯断言型任务 — 确认前端项目修改记录未被改动 + 跨项目联动决策痕迹已落位
+
+
+ - CLAUDE.md 第 269-281 行(「跨项目联动」规则:服务端接口 + 管理后台调用 = 两端各写一条相互引用;纯单端改动 = 仅一端记)
+ - .planning/phases/01-credential-data-layer/01-CONTEXT.md `` 段(明确 Phase 1 仅数据层 + Admin,**无任何前端联动**)
+ - docs/修改记录.md(确认 Task 3 已落地的两条条目里**已经**含 `- **跨项目联动**: 无 — ...` 字段,无需补写)
+
+
+ **本任务为纯断言型 task — 不修改任何文件**(INFO #2 调整:跨项目联动字段已合并进 Task 3 模板一次写入;本 task 只负责断言落位 + 验证前端文件未动)。
+
+ 步骤:
+ 1. 用 Read 查看 `../qy-lty-admin/docs/修改记录.md` 顶部 30 行,**确认其内容未被本会话改动**(仅做读取,**不写**)
+ 2. 在仓库根目录跑 `git diff --quiet HEAD -- qy-lty-admin/docs/修改记录.md && echo CLEAN`(这是单条命令组合:`git diff --quiet` 在文件无 unstaged 改动时退出码 0,配合 `&& echo CLEAN` 仅在干净时打印 `CLEAN`;与之前 `cd ... && git status --short ... 输出为空` 的脆弱判式相比,本写法对 staged/unstaged 都鲁棒,且 PowerShell 5.x 与 bash 都可执行 — 因为 `&&` 是 git 自身命令链的语法,而非 shell pipeline)
+ 3. 用 grep 验证 `qy_lty/docs/修改记录.md` 中两条 Phase 1 条目都已含 `- **跨项目联动**: 无` 字段(应在 Task 3 一次性写入;本 task 不应触发任何写入)
+
+ 关键约束:
+ - **本 task 完全不修改任何文件**(含 `qy_lty/docs/修改记录.md` 与 `../qy-lty-admin/docs/修改记录.md`)
+ - 若发现 Task 3 写入的条目缺『跨项目联动』字段,**回到 Task 3** 修复,**不**在本 task 补写
+ - 若发现 `qy-lty-admin/docs/修改记录.md` 已被改动,立刻报错(CLAUDE.md 强制规则违反)
+
+
+ cd C:\Users\admin\Desktop\Lila-Server && git diff --quiet HEAD -- qy-lty-admin/docs/修改记录.md && echo CLEAN
+
+
+ - 上面 verify 命令退出码 0 且输出 `CLEAN`(说明 `qy-lty-admin/docs/修改记录.md` 相对 HEAD 无任何改动 — staged 与 unstaged 都干净)
+ - 在 `qy_lty/docs/修改记录.md` 中 grep `**跨项目联动**: 无` 命中 2 次(Task 3 已写入;本 task 不应改变此计数)
+ - 本 task 的 git status 应无新增 / 修改文件(即 Task 4 是纯只读断言)
+ - 跨项目联动决策痕迹已落位:可被后续 verify-work agent 通过 grep `**跨项目联动**: 无 —` 命中
+
+ verify 命令打印 CLEAN 且 git diff 退出码 0;以上 4 条 acceptance criteria 全部满足;前端修改记录文件未被本 plan 任意 task 改动。
+
+
+
+
+
+本 plan 完成 + Plan 01 完成后,整个 Phase 1 必须满足 ROADMAP「Phase 1: 凭据槽位数据层」的全部 4 条 success criteria:
+
+1. ✅ DB / 模型层最多 1 条记录 → 由 Plan 01 Task 2 acceptance 中 N 次 save 后 count == 1 的 shell 断言显式验证(注:实现方式是 save() 钩子静默重定向 pk,非异常拒绝;详见 Plan 01 verification 段说明)+ Plan 02 Task 2 浏览器尝试新增(应被拒)
+2. ✅ migrate 后 schema 含 app_id / access_token / updated_at + 首访 get_or_create 拿空记录 → Plan 01 Task 3 自检 + showmigrations 0004_credentialslot 行带 `[X]` 标记
+3. ✅ Admin 列表/查看态脱敏 + 编辑态明文 → 本 Plan Task 1 + Task 2 浏览器验收(验收 1 期望 B/C/E 用前置准备 shell 探针读出实际 access_token 算出的脱敏期望串作比对,避免硬编码字符串与 DB 状态不符)
+4. ✅ Admin 列表页无「增加」按钮 → 本 Plan Task 2 浏览器验收
+
+**额外满足**(Phase 工程硬要求):
+5. ✅ Admin 禁止删除(CONTEXT.md / Success criterion #6)→ 本 Plan Task 1 + Task 2 浏览器验收
+6. ✅ 修改记录两条已追加到 qy_lty/docs/修改记录.md 顶部、qy-lty-admin/docs/修改记录.md 未被改动(CLAUDE.md 强制规则)→ 本 Plan Task 3 一次性写入两条含『跨项目联动: 无』字段的条目;Task 4 纯断言确认前端文件未动
+
+
+
+- aiapp/admin.py 注册 CredentialSlotAdmin,覆盖 REQ CRED-02 完整语义(脱敏 + 单例新增 + 禁删)
+- ROADMAP Phase 1 success criteria #4 / #5 / #6 由人工 checkpoint 显式验收(验收 1 「期望 B」用前置准备 shell 探针读出的实际 access_token 算 mask 期望串,默认场景下为 `*************xxxx`,13 个 `*` + `xxxx`,对应 `probe_secret_xxxx` 17 字符)
+- qy_lty/docs/修改记录.md 顶部增加 2 条 Phase 1 条目(顺序:Admin 在上、数据层在下;最新在最前);两条都在 Task 3 一次性写入时即含『跨项目联动: 无』字段(INFO #2 调整:不再由 Task 4 二次追加)
+- qy-lty-admin/docs/修改记录.md **未被改动**(CLAUDE.md 跨项目规则 + 本 phase 是纯服务端;由 Task 4 `git diff --quiet HEAD -- qy-lty-admin/docs/修改记录.md && echo CLEAN` 鲁棒断言)
+- 与 Plan 01 联合交付,Phase 1 整体收尾,ROADMAP Phase 1 状态可推进至 Complete,可启动 `/gsd-plan-phase 2`(管理端 REST 接口)
+
+
+
+
+
\ No newline at end of file