---
phase: 02-admin-rest
plan: 02
type: execute
wave: 2
depends_on:
  - "02-01"
files_modified:
  - qy_lty/docs/修改记录.md
  - qy-lty-admin/docs/修改记录.md
autonomous: true
requirements:
  - CRED-03
  - CRED-04
must_haves:
  truths:
    - "qy_lty/docs/修改记录.md 顶部新增一条 Phase 2 条目，跨项目联动字段引用 qy-lty-admin/docs/修改记录.md 的同期条目"
    - "qy-lty-admin/docs/修改记录.md 顶部新增一条 Phase 2 条目，服务端联动字段引用 qy_lty/docs/修改记录.md 的同期条目（互引闭环）"
    - "端到端 curl 验收：GET 携 admin token 返回 200 + 标准壳层 + access_token 脱敏（末 4 位明文 + 前缀 *）"
    - "端到端 curl 验收：PUT 携 admin token + body 后 DB 全字段覆写 + updated_at 刷新 + 响应 access_token 脱敏"
    - "端到端 curl 验收：无 token → 401 + 标准壳层；user token → 403 + message=需要管理员权限 + 标准壳层"
    - "Swagger schema 暴露：/swagger.json 含 /api/v1/admin/credential-slot/ 路径条目，含 GET/PUT 两 method，access_token 字段 description 标注脱敏掩码语义"
  artifacts:
    - path: "qy_lty/docs/修改记录.md"
      provides: "Phase 2 修改记录条目（含跨项目联动字段）"
      contains: "Phase 2"
    - path: "qy-lty-admin/docs/修改记录.md"
      provides: "Phase 2 互引条目（含服务端联动字段）"
      contains: "Phase 2"
  key_links:
    - from: "qy_lty/docs/修改记录.md (Phase 2 条目的跨项目联动字段)"
      to: "qy-lty-admin/docs/修改记录.md (Phase 2 同期条目)"
      via: "条目内文字引用 ../qy-lty-admin/docs/修改记录.md 同期条目"
      pattern: "qy-lty-admin/docs/修改记录"
    - from: "qy-lty-admin/docs/修改记录.md (Phase 2 条目的服务端联动字段)"
      to: "qy_lty/docs/修改记录.md (Phase 2 同期条目)"
      via: "条目内文字引用 ../qy_lty/docs/修改记录.md 同期条目"
      pattern: "qy_lty/docs/修改记录"
---

<objective>
本 plan 完成 Phase 2 收尾：
1. 端到端验收 02-01 落地的 GET/PUT 接口（curl + Django shell test client 双层验证 8 条 success criteria）
2. 在 qy_lty + qy-lty-admin 两端 `docs/修改记录.md` 顶部各写一条 Phase 2 条目，且**跨项目联动字段相互引用**（CLAUDE.md 强制规则；Phase 2 是首次跨项目接口契约落地）

Purpose：
- 端到端验收：把 02-01 写出来的 view + serializer + URL 真正跑起来，证明三处 success criteria（GET 脱敏 / PUT 覆写 + 脱敏响应 / 鉴权拒绝矩阵）在生产路径成立
- 跨项目互引：给后续在 qy-lty-admin 起 CRED-FE-01 phase 时，前端能从修改记录直接定位到本 phase 的接口契约文档；给本仓库未来回查"这个接口什么时候上的、谁在消费"留一个反向锚点

Output：
- `qy_lty/docs/修改记录.md` 顶部新增一条 Phase 2 条目（5 字段 + 跨项目联动字段）
- `qy-lty-admin/docs/修改记录.md` 顶部新增一条 Phase 2 条目（5 字段 + 服务端联动字段）
- VERIFICATION.md 补充端到端 curl + Django shell 验收记录（在 SUMMARY 中归档）
</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/phases/02-admin-rest/02-CONTEXT.md
@.planning/phases/02-admin-rest/02-RESEARCH.md
@.planning/phases/02-admin-rest/02-01-PLAN.md

# 02-01 落地后的 SUMMARY（含端到端 verify hook）
@.planning/phases/02-admin-rest/02-01-SUMMARY.md

# 修改记录格式 + 已落地条目模板
@qy_lty/docs/修改记录.md
@qy-lty-admin/docs/修改记录.md

# 项目宪法（修改记录强制 + 跨项目互引规则）
@qy_lty/CLAUDE.md

# 鉴权 / 工具入口（用于端到端验收脚本）
@qy_lty/userapp/utils.py
@qy_lty/userapp/authentication.py
@qy_lty/aiapp/models.py
@qy_lty/common/utils.py
</context>

<tasks>

<task type="auto" tdd="false">
  <name>Task 1：端到端 curl + Django shell 验收（8 条 success criteria）</name>
  <files>qy_lty/.planning/phases/02-admin-rest/02-VERIFICATION.md</files>
  <read_first>
    - 必读 .planning/phases/02-admin-rest/02-01-SUMMARY.md（02-01 留给本 task 的 hook）
    - 必读 .planning/phases/02-admin-rest/02-CONTEXT.md `<specifics>` 段（8 条验收点表格）
    - 必读 .planning/phases/02-admin-rest/02-RESEARCH.md `Validation Architecture` 段（test client 程序化验收脚本片段 + 采样频率）
    - 必读 qy_lty/userapp/utils.py:33-37（generate_token 签发 admin / user token 的方式）
    - 必读 qy_lty/aiapp/models.py 的 CredentialSlot.get_solo（验证 PUT 在空记录场景的 get_or_create）
    - 必读 .planning/phases/01-credential-data-layer/01-VERIFICATION.md（Phase 1 程序化验收风格，本 task 1:1 沿用）
  </read_first>
  <action>
**目标**：在 Django shell + Daphne / runserver 两条路径都验完 8 条 success criteria，留一份证据归档进 02-VERIFICATION.md。

**Step 1：准备测试 token（一次性 Django shell 脚本）**

```bash
cd qy_lty && python manage.py shell <<'EOF'
from userapp.models import ParadiseUser
from userapp.utils import generate_token
import json

# 取一个已有 staff 用户（必须 is_staff=True；如全库无 staff，先 createsuperuser）
admin_user = ParadiseUser.objects.filter(is_staff=True).first()
assert admin_user is not None, "需要至少一个 is_staff=True 用户；运行 python manage.py createsuperuser"
admin_token = generate_token(admin_user.id, is_admin=True)

# 取一个普通用户（is_staff=False；如不存在可创建一个 phone-only 用户用于本验收）
user = ParadiseUser.objects.filter(is_staff=False).first()
if user is None:
    user = ParadiseUser.objects.create(username='phase2_verify_user_temp', is_staff=False)
user_token = generate_token(user.id, is_admin=False)

print(json.dumps({
    'admin_user_id': admin_user.id,
    'admin_token': admin_token,
    'user_id': user.id,
    'user_token': user_token,
}, ensure_ascii=False))
EOF
```

记录输出的 `admin_token` / `user_token` 备用。**注意**：token 写入 Redis 后 30 天内有效；本 task 验完无需清理（可让其自然过期）。

**Step 2：Django test client 程序化验收（无需启动服务进程，全部在 shell 内跑）**

```bash
cd qy_lty && python manage.py shell <<'EOF'
from django.test import Client
from aiapp.models import CredentialSlot
from common.utils import mask_token
import json

ADMIN_TOKEN = "<填入 Step 1 拿到的 admin_token>"
USER_TOKEN  = "<填入 Step 1 拿到的 user_token>"
URL = "/api/v1/admin/credential-slot/"

c = Client()

# === 验收点 1：GET 携 admin token 返回脱敏 ===
r = c.get(URL, HTTP_AUTHORIZATION=f"Bearer {ADMIN_TOKEN}")
body = r.json()
assert r.status_code == 200, f"#1 status={r.status_code} body={body}"
assert body['success'] is True
assert body['code'] == 200
assert 'data' in body
assert {'app_id', 'access_token', 'updated_at'} <= set(body['data'].keys())
# access_token 必须脱敏：要么是空（DB 为空时），要么以 * 开头且末 4 位为原 token 末 4 位
slot = CredentialSlot.get_solo()
expected_masked = mask_token(slot.access_token)
assert body['data']['access_token'] == expected_masked, f"#1 mask mismatch: got={body['data']['access_token']!r} expected={expected_masked!r}"
print("#1 PASS  GET admin -> 200 + masked access_token")

# === 验收点 2：PUT 携 admin token 全字段覆写 + 响应脱敏 ===
new_token = "sk-phase2_verify_secret_ABCD1234"
r = c.put(URL, data=json.dumps({"app_id": "phase2_app", "access_token": new_token}),
          content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {ADMIN_TOKEN}")
body = r.json()
assert r.status_code == 200, f"#2 status={r.status_code} body={body}"
assert body['success'] is True
slot = CredentialSlot.get_solo()
assert slot.app_id == "phase2_app"
assert slot.access_token == new_token  # DB 中明文存储
# 响应 access_token 必须脱敏：'sk-phase2_verify_secret_ABCD1234' -> '*****************************1234'
assert body['data']['access_token'] == mask_token(new_token), f"#2 PUT response not masked: {body['data']['access_token']!r}"
assert body['data']['access_token'].endswith('1234'), "末 4 位必须为 1234"
assert body['data']['access_token'].startswith('*'), "脱敏前缀必须以 * 开头"
print("#2 PASS  PUT admin -> 200 + DB updated + response masked")

# === 验收点 3：PUT 在空记录场景自动 get_or_create ===
CredentialSlot.objects.all().delete()
r = c.put(URL, data=json.dumps({"app_id": "after_delete", "access_token": "tok-XYZ9"}),
          content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {ADMIN_TOKEN}")
body = r.json()
assert r.status_code == 200, f"#3 status={r.status_code} body={body}"
slot = CredentialSlot.get_solo()
assert slot.app_id == "after_delete"
assert slot.access_token == "tok-XYZ9"
print("#3 PASS  PUT 空记录场景 -> 200 + get_or_create 创建并写入")

# === 验收点 4：无 Authorization 头 -> 401 + 标准壳层 ===
r = c.get(URL)
body = r.json()
assert r.status_code == 401, f"#4 status={r.status_code} body={body}"
assert body['success'] is False
assert body['code'] == 401
assert 'message' in body  # 中间件 / DRF 至少要给一个 message 字段
print("#4 PASS  no token -> 401 + 标准壳层")

# === 验收点 5：携普通 user token -> 403 + 标准壳层 + message 含管理员关键字 ===
r = c.get(URL, HTTP_AUTHORIZATION=f"Bearer {USER_TOKEN}")
body = r.json()
assert r.status_code == 403, f"#5 status={r.status_code} body={body}"
assert body['success'] is False
assert body['code'] == 403
assert "管理员" in body['message'], f"#5 message 不含'管理员'关键字: {body['message']!r}"
print("#5 PASS  user token -> 403 + 需要管理员权限")

# === 验收点 6：PUT 携 user token -> 403（同 #5 路径，验证 PUT 也走 _ensure_admin） ===
r = c.put(URL, data=json.dumps({"app_id": "should_fail"}),
          content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {USER_TOKEN}")
body = r.json()
assert r.status_code == 403, f"#6 PUT user token status={r.status_code}"
print("#6 PASS  PUT user token -> 403")

print("\n========== 全部 6 条 test client 验收通过 ==========")
EOF
```

**Step 3：Swagger schema 验收（启动 daphne 后做一次 curl）**

```bash
# 启动服务（如已运行可跳过）
# cd qy_lty && daphne -b 0.0.0.0 -p 8000 qy_lty.asgi:application &

curl -s http://localhost:8000/swagger.json | python -c "
import json, sys
schema = json.load(sys.stdin)
paths = schema.get('paths', {})
key = '/api/v1/admin/credential-slot/'
assert key in paths, f'#7 {key} 未出现在 swagger paths 中'
methods = paths[key]
assert 'get' in methods, '#7 GET 方法缺失'
assert 'put' in methods, '#7 PUT 方法缺失'
# 验证 access_token description 含脱敏关键字
get_resp = methods['get'].get('responses', {}).get('200', {})
schema_str = json.dumps(get_resp, ensure_ascii=False)
assert '脱敏' in schema_str or '末 4 位' in schema_str or 'mask' in schema_str.lower(), '#7 access_token description 不含脱敏掩码语义'
print('#7 PASS  swagger paths 含 GET/PUT 两 method + access_token 脱敏描述')
"
```

**Step 4：把 8 条验收结果写入 `.planning/phases/02-admin-rest/02-VERIFICATION.md`**

文件全文模板（直接 Write）：

```markdown
# Phase 2 Verification — 管理端读写接口端到端验收

**Verified**: 2026-05-07
**Phase**: 02-admin-rest
**Plan**: 02-02
**Coverage**: CRED-03 + CRED-04（ROADMAP Phase 2 全部 4 条 success criteria）

---

## 验收摘要

| # | 验收点 | 方法 | 结果 |
|---|--------|------|------|
| 1 | GET 携 admin token 返回脱敏壳层 | Django test client | ✓ PASS |
| 2 | PUT 携 admin token 全字段覆写 + 响应脱敏 | Django test client | ✓ PASS |
| 3 | PUT 在空记录场景自动 get_or_create | Django test client（手动 delete + PUT） | ✓ PASS |
| 4 | 无 Authorization 头 → 401 + 标准壳层 | Django test client | ✓ PASS |
| 5 | 携普通 user token → 403 + 需要管理员权限 | Django test client | ✓ PASS |
| 6 | PUT 携 user token → 403（验证 PUT 也走 _ensure_admin） | Django test client | ✓ PASS |
| 7 | /swagger.json 含路径条目 + GET/PUT 两 method + 脱敏 description | curl | ✓ PASS |
| 8 | 修改记录两端互引（02-02 Task 2 落地后追加） | 文件 grep | ⏳ 待 02-02 Task 2 |

---

## 证据片段

[黏贴 Step 1 / Step 2 / Step 3 输出的关键行]

---

*由 02-02-PLAN.md Task 1 生成*
```

**关键约束**：
- 6 条 test client 验收必须**全部 PASS** 才能进 02-02 Task 2；任何一条 FAIL 则回到 02-01 修 view / serializer / URL
- Swagger 验收（#7）若 daphne 未启动可暂时记 "deferred to integration"，但必须在 phase gate 前补做
- #8 在本 task 仅占位（标 ⏳），Task 2 落地修改记录后**回写**为 ✓ PASS（02-VERIFICATION.md 在 Task 2 末尾再 Edit 一次）
- 不要把 admin / user token 明文写进 02-VERIFICATION.md（Redis 30 天 TTL，落进 git 的 token 在 30 天内仍有效，是新的泄露面）
  </action>
  <verify>
    <automated>
      cd qy_lty && python manage.py shell -c "from aiapp.models import CredentialSlot; from common.utils import mask_token; slot = CredentialSlot.get_solo(); print('DB state ok:', slot.app_id, mask_token(slot.access_token))" && test -f .planning/phases/02-admin-rest/02-VERIFICATION.md && grep -q "PASS" .planning/phases/02-admin-rest/02-VERIFICATION.md && echo OK
    </automated>
  </verify>
  <acceptance_criteria>
    - Step 2 脚本输出"========== 全部 6 条 test client 验收通过 =========="
    - Step 3 输出 "#7 PASS"（如 daphne 未启动则记 deferred 但必须在 SUMMARY 中列出补做计划）
    - 文件 `.planning/phases/02-admin-rest/02-VERIFICATION.md` 存在
    - 文件含至少 6 处 "PASS"（验收点 1-6）
    - 文件不含 admin / user token 明文（grep `Bearer ` 应只出现在脚本模板段落、不含具体 UUID 字串）
    - DB 状态符合预期：`CredentialSlot.get_solo()` 返回的 app_id 与 access_token 是 Step 2 写入的最新值
  </acceptance_criteria>
  <done>
    8 条 success criteria 中 6 条 test client + 1 条 swagger 已验完并归档；DB 状态稳定；02-VERIFICATION.md 是 Phase 2 收尾时 ROADMAP 标记 ✓ Complete 的证据来源。
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 2：两端修改记录互引条目（qy_lty + qy-lty-admin）</name>
  <files>qy_lty/docs/修改记录.md, qy-lty-admin/docs/修改记录.md</files>
  <read_first>
    - 必读 qy_lty/docs/修改记录.md:1-90（确认头部「修改格式说明」+ 第 24 行 `<!-- 新的修改记录添加在此处下方，最新的在最前面 -->` 注释 + Phase 1 已有两条条目作模板）
    - 必读 qy-lty-admin/docs/修改记录.md:1-58（确认头部「修改格式说明」+ 第 26 行 `<!-- 新的修改记录添加在此处下方，最新的在最前面 -->` + 第 28-44 行 NEXT_PUBLIC_API_BASE_URL 修复条目作"含跨项目说明的"模板）
    - 必读 qy_lty/CLAUDE.md `## 项目修改记录规则（重要 — 自动执行）` 段（5 字段格式 + 跨项目联动两端各一条互引规则）
    - 必读 .planning/phases/02-admin-rest/02-01-SUMMARY.md（Phase 2 实际改动文件清单 + 接口契约）
    - 必读 02-CONTEXT.md `<decisions>` 跨项目联动段（互引文案要点）
  </read_first>
  <action>
**Step 1：在 `qy_lty/docs/修改记录.md` 顶部（紧跟第 24 行注释 `<!-- 新的修改记录添加在此处下方，最新的在最前面 -->` 之下、Phase 1 第一条 `### [2026-05-07] Phase 1 — Django Admin 注册凭据槽位...` 之上）插入条目**：

```markdown
### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口（GET 脱敏 / PUT 覆写）

配套 Phase：[.planning/phases/02-admin-rest/](.planning/phases/02-admin-rest/)
覆盖需求：CRED-03 + CRED-04
设计参考：1:1 复刻 `aiapp.views.RTCChatHistoryAPIView`（`aiapp/views.py:434-555`）的单 URL 多方法 APIView 风格

- **文件路径**:
  - `aiapp/serializers.py`（修改 — 顶部 import 追加 `CredentialSlot`，文件末尾追加 `CredentialSlotSerializer` ModelSerializer 类）
  - `aiapp/views.py`（修改 — 顶部 import 追加 `CredentialSlot` / `CredentialSlotSerializer` / `mask_token` / `get_standardized_response_schema`；文件末尾追加 `CredentialSlotPutRequestSchema` swagger 请求体 + `_credential_slot_data_schema` 响应 data schema + `CredentialSlotAdminView` APIView 类）
  - `userapp/admin_urls.py`（修改 — 追加 `from aiapp.views import CredentialSlotAdminView` 与 `path('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot')`）
- **修改类型**: 新增
- **修改内容**:
  - 暴露 `GET /api/v1/admin/credential-slot/`：admin token 鉴权（`RedisTokenAuthentication` + 视图内 `is_staff` 二次校验，不发明 admin-only permission 类）；返回 `{ success, code, message, data: { app_id, access_token: <末 4 位脱敏掩码>, updated_at } }`，脱敏由 view 层调 `common.utils.mask_token` 完成（serializer 不参与脱敏，避免双重责任）
  - 暴露 `PUT /api/v1/admin/credential-slot/`：admin token 鉴权；接受 `{ app_id, access_token }` 全字段覆写；空记录场景自动走 `CredentialSlot.get_solo()` 的 `get_or_create(pk=1)`；写入后 `updated_at` 由 `auto_now=True` 自动刷新；响应同样脱敏 access_token（避免运营在 admin UI 看到自己刚提交的明文回显）
  - 鉴权拒绝矩阵：无 token → 401（DRF NotAuthenticated → middleware 兜底标准壳层）；持普通 user token（非 staff）→ 403 + `message="需要管理员权限"`
  - Swagger / ReDoc 自动暴露：method-level `@swagger_auto_schema` 装饰器；响应 schema 配 `common.swagger_utils.get_standardized_response_schema()`；access_token 字段 description 显式标注「Access Token 末 4 位脱敏掩码（如 "*********1234"）」，避免前端误解为明文
  - 不引入新依赖（沿用 Django 4.2.13 + DRF + drf-yasg + Phase 1 落地的 `CredentialSlot.get_solo` / `mask_token`）
- **修改原因**: Milestone v1.0「通用凭据槽位（APP ID + Access Token）」Phase 2 — 给管理后台前端（qy-lty-admin）暴露受控的凭据读写入口，让运营无需进 Django Admin 也能管理凭据；GET 与 PUT 响应均脱敏，避免明文经管理端 UI / 浏览器 devtools / 阿里云日志（GET 响应体路径）泄露；为 Phase 3 客户端明文 GET 接口 + 阿里云日志 formatter 提供"接口已上线、凭据可写入"的稳定起点
- **跨项目联动**: 前端联动条目 [qy-lty-admin/docs/修改记录.md](../../qy-lty-admin/docs/修改记录.md) 同期 `[2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约（消费方文档化）`。本 phase 是 Milestone v1.0 首次跨项目接口契约落地：本仓库（服务端）暴露 `/api/v1/admin/credential-slot/` GET/PUT，前端 `qy-lty-admin` 后续 phase 将基于该契约写 API client（含 React Hooks 调用 + 表单录入 UI）。前后端各自维护独立修改记录，本条与对方条目互相引用，便于未来回查接口的双向上下游
```

**Step 2：在 `qy-lty-admin/docs/修改记录.md` 顶部（紧跟第 26 行注释 `<!-- 新的修改记录添加在此处下方，最新的在最前面 -->` 之下、第 28 行 `### [2026-05-07] 修复 NEXT_PUBLIC_API_BASE_URL...` 之上）插入条目**：

```markdown
### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约（消费方文档化）

配套服务端 Phase：[../qy_lty/.planning/phases/02-admin-rest/](../../qy_lty/.planning/phases/02-admin-rest/)
覆盖服务端需求：CRED-03 + CRED-04（本仓库消费方）

- **文件路径**: `docs/修改记录.md`（仅文档新增条目；本仓库代码未改）
- **修改类型**: 新增
- **修改内容**:
  - 文档化服务端在本日落地的 `/api/v1/admin/credential-slot/` REST 接口契约（GET 脱敏读取 + PUT 全字段覆写 + admin token 鉴权），为后续 Web 管理后台前端（本仓库）的 CRED-FE-* phase 写 API client 与表单 UI 留下契约锚点
  - 接口契约要点（消费方视角）：
    - URL：`{NEXT_PUBLIC_API_BASE_URL}/v1/admin/credential-slot/`
    - 鉴权：`Authorization: Bearer <admin_token>`（来源于 `/api/v1/admin/login/` 的现有 admin 登录返回值）
    - GET 响应：`{ success: boolean, code: number, message: string, data: { app_id: string, access_token: string /* 末 4 位脱敏掩码，前缀 * */, updated_at: string /* ISO 8601 */ } }`
    - PUT 请求体：`{ app_id?: string, access_token?: string }`（任一字段缺省时由后端兜底保留原值；写入是全字段覆写语义，建议前端 UI 始终提交两字段全集以避免歧义）
    - PUT 响应同 GET 形态（access_token 同样脱敏返回，前端**不应**用响应值回填明文输入框）
    - 错误矩阵：401（无 token / token 失效）、403（持非 admin token，message 含"需要管理员权限"）、400（参数无效）
- **修改原因**:
  - 服务端首次为本管理后台暴露受控的凭据读写接口；本仓库即将启动 CRED-FE-01（API client） + CRED-FE-02（表单录入页面）等 phase，先把后端契约固化进本仓库修改记录便于反查
  - 文档化"GET 与 PUT 响应均脱敏 access_token"避免前端工程师误以为可以从响应回填明文表单（实际明文仅存于 DB；任何回填只能保留掩码或要求运营重新输入）
- **服务端联动**: 后端联动条目 [../qy_lty/docs/修改记录.md](../../qy_lty/docs/修改记录.md) 同期 `[2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口（GET 脱敏 / PUT 覆写）`。本仓库代码未改，仅文档侧做契约固化；待本仓库 CRED-FE-01 phase 启动落地 API client + Hook 时再补一条独立条目并互引
```

**Step 3：互引校验（写完后立即 grep）**

```bash
# 验证两端互引字段都在
grep -n "qy-lty-admin/docs/修改记录" qy_lty/docs/修改记录.md | head -3
grep -n "qy_lty/docs/修改记录" qy-lty-admin/docs/修改记录.md | head -3
# 验证两边都标了 Phase 2 + 2026-05-07
grep -n "Phase 2.*2026-05-07\|2026-05-07.*Phase 2" qy_lty/docs/修改记录.md qy-lty-admin/docs/修改记录.md
```

**Step 4：回写 02-VERIFICATION.md 验收点 #8**

把 Task 1 写入的 02-VERIFICATION.md 中验收点 #8 的状态从 `⏳ 待 02-02 Task 2` 改为 `✓ PASS`，并追加证据：
```
| 8 | 修改记录两端互引 | grep `qy-lty-admin/docs/修改记录` qy_lty/docs/修改记录.md ✓；grep `qy_lty/docs/修改记录` qy-lty-admin/docs/修改记录.md ✓ | ✓ PASS |
```

**关键约束**：
- 两条条目必须用日期 `2026-05-07`（与 Phase 1 / 当前 currentDate 一致；从 system context 得知）
- 两条条目都必须含**跨项目联动 / 服务端联动**字段，且字段值必须**指向对方文件路径**（不能写"无"，Phase 1 那样的"暂无前端"逻辑在 Phase 2 不成立 —— 本 phase 就是首次跨项目接口契约落地）
- 服务端条目中的相对路径用 `../../qy-lty-admin/docs/修改记录.md`（位于 `qy_lty/docs/`，跳到 `qy_lty/` 上级 `Lila-Server/` 再进 `qy-lty-admin/docs/`）
- 前端条目中的相对路径用 `../../qy_lty/docs/修改记录.md`（同理对称）
- 两条条目都要插在各自文件的「修改历史」段顶部（最新在最前），不要追加到末尾
- **不要**修改 Phase 1 已有的两条条目（Phase 1 当时纯服务端、无前端互引是合理的，不要回头改成"互引"破坏历史归档）
- **不要**在 `qy-lty-admin` 仓库改任何代码（本 task 仅在 qy-lty-admin 仓库内动 docs 一个文件）
  </action>
  <verify>
    <automated>
      grep -c "Phase 2 — 管理端通用凭据槽位 REST 接口" "C:\Users\admin\Desktop\Lila-Server\qy_lty\docs\修改记录.md" && grep -c "Phase 2 — 锁定后端通用凭据槽位 REST 接口契约" "C:\Users\admin\Desktop\Lila-Server\qy-lty-admin\docs\修改记录.md" && grep -q "qy-lty-admin/docs/修改记录" "C:\Users\admin\Desktop\Lila-Server\qy_lty\docs\修改记录.md" && grep -q "qy_lty/docs/修改记录" "C:\Users\admin\Desktop\Lila-Server\qy-lty-admin\docs\修改记录.md" && echo OK
    </automated>
  </verify>
  <acceptance_criteria>
    - `qy_lty/docs/修改记录.md` 顶部「修改历史」段第一条标题为 `### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口（GET 脱敏 / PUT 覆写）`
    - 该条目含 5 个标准字段（文件路径 / 修改类型 / 修改内容 / 修改原因 / 跨项目联动）
    - 该条目"跨项目联动"字段含字串 `qy-lty-admin/docs/修改记录.md`（指向前端互引）
    - `qy-lty-admin/docs/修改记录.md` 顶部「修改历史」段第一条标题为 `### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约（消费方文档化）`
    - 该条目含 5 个标准字段（文件路径 / 修改类型 / 修改内容 / 修改原因 / 服务端联动）
    - 该条目"服务端联动"字段含字串 `qy_lty/docs/修改记录.md`（指向后端互引）
    - 两条条目互相引用对方（grep 双向均命中 → 闭环）
    - Phase 1 的两条条目位置不变（位于本次新增条目之下）
    - 02-VERIFICATION.md 中验收点 #8 已改为 ✓ PASS
  </acceptance_criteria>
  <done>
    `qy_lty` + `qy-lty-admin` 两端 `docs/修改记录.md` 顶部各有一条 Phase 2 条目，互相引用对方文件路径；CLAUDE.md "qy_lty 与 qy-lty-admin 是独立项目，跨项目联动两端各写一条互相引用对方"规则在本 phase 闭环；02-VERIFICATION.md 8 条全 PASS。
  </done>
</task>

</tasks>

<verification>
本 plan 完成后做一轮 phase-gate 自验：

1. **8 条 success criteria 全 PASS**：02-VERIFICATION.md 表格全部 ✓
2. **互引闭环**：
   - `grep "qy-lty-admin" qy_lty/docs/修改记录.md` ≥ 1 hit（在 Phase 2 条目内）
   - `grep "qy_lty" qy-lty-admin/docs/修改记录.md` ≥ 1 hit（在 Phase 2 条目内）
3. **ROADMAP Phase 2 4 条 success criteria 已覆盖**：
   - SC#1 GET 脱敏 ← 验收点 #1 ✓
   - SC#2 PUT 全字段覆写 + get_or_create ← 验收点 #2 + #3 ✓
   - SC#3 鉴权拒绝矩阵 ← 验收点 #4 + #5 + #6 ✓
   - SC#4 Swagger / ReDoc schema 一致 ← 验收点 #7 ✓
4. **Phase 1 条目未被改动**：`git diff qy_lty/docs/修改记录.md` 仅显示新增条目（在文件顶部追加），Phase 1 已有的两条 `[2026-05-07] Phase 1 — ...` 标题在 diff 内位置应该是"unchanged"
</verification>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| 端到端 verify 测试 token | 临时 admin / user token，30 天 TTL；不允许写入仓库 |
| 跨项目修改记录文件 | 文档边界；不修改对方仓库代码，仅在对方 docs/ 下追加一条文档 |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02P2-01 | Information Disclosure | 验收脚本中的 admin / user token 误入 git | mitigate | 02-VERIFICATION.md 仅记录脚本**模板**与"PASS"判定；不黏贴具体 token UUID；脚本输出在 SUMMARY 中也只摘要"6/6 PASS"不贴 token |
| T-02P2-02 | Information Disclosure | 验收用真实凭据写入 DB（覆盖 Phase 1 探针） | accept | DB 中 access_token 本来就是明文存储（Phase 1 落地决策）；PUT 写入的 `sk-phase2_verify_secret_ABCD1234` 不是真实第三方凭据，只是测试串；Task 1 Step 2 #3 还会再 delete + 重建覆盖一次 |
| T-02P2-03 | Tampering | 误修改 Phase 1 修改记录条目 | mitigate | Task 2 关键约束第 7 条明确"不要修改 Phase 1 已有的两条条目"；verify 步骤 #4 用 git diff 校验 Phase 1 条目位置不变 |
| T-02P2-04 | Information Disclosure | 互引文档泄露内部路径 | accept | `.planning/` 已是仓库内文档，路径暴露与本仓库 README 同等；不引入新泄露面 |
</threat_model>

<success_criteria>
本 plan 落地完成的标志（phase 收尾标志）：

- [ ] `.planning/phases/02-admin-rest/02-VERIFICATION.md` 存在，8 条验收点 6+1+1 = 8 全 ✓
- [ ] `qy_lty/docs/修改记录.md` 顶部含 `### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口（GET 脱敏 / PUT 覆写）`
- [ ] `qy-lty-admin/docs/修改记录.md` 顶部含 `### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约（消费方文档化）`
- [ ] 两条互引字段双向命中：`grep "qy-lty-admin/docs/修改记录" qy_lty/docs/修改记录.md` ≥ 1；`grep "qy_lty/docs/修改记录" qy-lty-admin/docs/修改记录.md` ≥ 1
- [ ] DB 中 CredentialSlot 单例存在且 access_token 是 Task 1 #3 写入的最终值（`tok-XYZ9` 或之后被 #3 覆盖的某个值）
- [ ] Phase 1 两条修改记录条目未被改动（git diff 确认）
- [ ] ROADMAP Phase 2 4 条 success criteria 在 02-VERIFICATION.md 中均找到对应的 ✓ 验收点映射
</success_criteria>

<output>
完成后创建 `.planning/phases/02-admin-rest/02-02-SUMMARY.md`，记录：
- 端到端验收 8 条结果（PASS / FAIL 计数）
- 两端修改记录条目的最终位置（行号 / 标题）
- 与 02-01-SUMMARY 合并形成 Phase 2 完整交付证据
- DB 状态最终值（app_id / access_token 末 4 位 / updated_at）
- 任何与本 PLAN 不一致的偏离（应为零）
- 标识 Phase 2 整体 Complete，下一步进入 Phase 3（CRED-05 + CRED-06）
</output>
