后端:adminpanel projects(全局项目流水线监控+状态筛)+ integrity(数据体检:无账户团队/无图商品/过期邀请码/ 无文件资产/失败项目/卡 reserved 计数 + totals);AdminProjectSerializer;IsPlatformAdmin。 前端:adminApi adminProjects/integrity;Admin 治理页(体检卡片超0高亮 + 项目监控表状态筛)。 测试:adminpanel 53 单测过(项目列/筛/体检报告);无头 e2e _admin-p9.mjs 4 断言过 + 0 console error(全只读);tsc+build 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
224 lines
19 KiB
Markdown
224 lines
19 KiB
Markdown
# 平台 Admin 后台 + 注册邀请码 · 落地进度
|
||
|
||
> 计划:`C:/Users/Air-work/.claude/plans/gleaming-sleeping-ullman.md`(Phase 0–9 全做)。
|
||
> 每个 Phase 走完「★测试底座」(后端单测 + 无头浏览器 e2e + tsc/build + 回归)才 commit。
|
||
> 全程本地、不 push;测试服 DB 可动。
|
||
|
||
---
|
||
|
||
## 测试基线(回归红线)
|
||
- `apps.accounts / apps.billing / apps.products`:全绿(Phase 0 后 15/15 OK)。
|
||
- `apps.projects`:**进 Phase 0 前已有 3 failures + 1 error**(均为 AI provider `image_edit` mock 相关,与本工程无关)——**非本次引入**,Phase 0 改动仅在 accounts + 前端,未触碰 projects/ai。后续 Phase 不得新增 fail。
|
||
|
||
---
|
||
|
||
## ✅ Phase 0 · 基础(角色 + 壳 + 审计)— 完成 2026-06-19
|
||
|
||
**后端**
|
||
- `User.is_platform_admin`(bool)+ migration `0004_user_is_platform_admin_adminauditlog`(已 apply 到测试 MySQL)。
|
||
- `apps/accounts/permissions.py` · `IsPlatformAdmin`(超管放行 / 普通拒 / 停用拒)。
|
||
- `apps/accounts/management/commands/create_platform_admin.py` · 建 **admin/admin123**(幂等;`--reset-password` 才重置;也置 `is_staff`)。已对测试 MySQL 执行,真账号可登。
|
||
- `AdminAuditLog` 模型(operator/operator_name/action/target_*/before/after/ip)+ `apps/accounts/audit.py` `log_admin_action()`(失败不阻断主流程)。
|
||
- `me` / `login` 对**无团队的平台超管**优雅返回 `team=null`(新增 `current_team_or_none`);`UserSerializer` 暴露 `is_platform_admin`。
|
||
|
||
**前端**
|
||
- 新 `routes/admin/admin-app.tsx`:超管后台外壳(折叠分组侧栏 6 组 11 项 + 顶栏 + 概览 + 占位页),全程复用 restraint 共享类。
|
||
- 路由:`route-config.ts` 解析 `/admin`、`/admin/<section>`;`App.tsx` 分流渲染 `AdminApp` + gating(超管无团队登录直落 /admin、非超管访问 /admin 纠回工作台)。
|
||
- `app-shell.tsx` 侧栏「平台」入口(仅超管可见)。
|
||
- `admin-page.css`(仅 token,无裸 hex)+ `IconKitSvg` 补 8 个 Lucide-line 图标。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.accounts` **11/11 OK**(新增 `PlatformAdminTests`:建超管命令幂等/权限类放行拒绝/登录+me 返回标志且 team 为空/普通注册非超管)。
|
||
- 无头 e2e:`core/qa/visual-parity/_admin-p0.mjs` —— **全部断言通过 + 0 console error**:
|
||
- admin/admin123 真登录 → 落 /admin;
|
||
- 超管壳:6 分组 / 11 导航 / `[ PLATFORM ADMIN ]` 徽标 / 概览标题 / 点「邀请码」→ /admin/invites 占位「模块即将上线」;
|
||
- 普通用户访问 /admin → 纠回 /dashboard、admin 壳不渲染。
|
||
- 截图:`output/admin/p0-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|
||
- 视觉:概览截图核对 —— 冷灰底 + 单橙 accent + 8px 圆角 + 准星,符合 design.md 克制调性。
|
||
|
||
**测试账号**:平台超管 `admin/admin123`(测试 MySQL 已建);普通 demo `airshelf/Restraint2026`。
|
||
|
||
> **e2e 端口提示**:5173 被同机另一项目(AirDrama)抢占且不稳;AirShelf e2e 改用**专用端口 5188**(`npx vite --port 5188 --strictPort`)。e2e 脚本默认 BASE 已指向 5188。
|
||
|
||
---
|
||
|
||
## ✅ Phase 1 · 注册邀请码逻辑 — 完成 2026-06-19
|
||
|
||
**后端**
|
||
- `Invitation.kind`(join_team / create_team)+ `team` 可空 + migration `0005_invitation_kind_alter_invitation_team`(已 apply 测试 MySQL)。
|
||
- `RegisterSerializer`:**invite_code 必填**;按 kind 分流 —— create_team 开新团队当 owner + 发试用额度(回填 invite.team),join_team 幂等加入该团队、不重复发额度;**删除「空码=开团队」**。
|
||
- `validate-invite` 端点(免登录,POST `/api/auth/validate-invite/`):验码 → `{valid, kind, role, team_name?}`,无效始终 200+`valid:false`。
|
||
- 团管发码恒为 `kind=join_team`;`InvitationSerializer` 增 `kind`/`team`/`team_name`。
|
||
|
||
**前端**
|
||
- 注册页重做(码优先):先填邀请码 → 点「验证」(或 Enter)→ 验过才展开;create_team 显示「团队名」输入,join_team 显示**只读团队名**(不暴露团队列表);CTA 文案随 kind 切换(创建团队 / 加入团队)。
|
||
- 邀请链接 `?invite=CODE` 进页自动回填 + 验码;改码即重置验证态。
|
||
- `api.validateInvite`;`field-note.ok/err` + `invite-verify-row` + 只读团队样式(仅 token)。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.accounts` **22/22 OK**(新增 `RegisterInviteCodeTests` 6 项 + `ValidateInviteTests` 5 项:无码拒/空码拒/create_team 开团队+试用额度/join_team 入伙不开新团队/已用·过期拒/验码返回正确 kind+team_name)。
|
||
- 无头 e2e:`_admin-p1.mjs`(端口 5188)—— **全断言过 + 0 console error**:create_team 验码→展开团队名→注册落工作台;join_team 验码→只读团队名「演示团队」→入伙;错码不展开+报错;URL 自动验码。截图 `output/admin/p1-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|
||
- 视觉:两态注册截图核对 —— 黑块等高、验证按钮对齐、绿色 ok 提示、只读团队名灰底,符合 restraint。
|
||
|
||
---
|
||
|
||
## ✅ Phase 2 · 邀请码发放管理(Admin)— 完成 2026-06-19
|
||
|
||
**后端(新建 `apps/adminpanel/`)**
|
||
- 新 Django app `apps.adminpanel`(跨团队后台端点容器),注册进 INSTALLED_APPS + 主 urls `path("api/admin/", ...)`。
|
||
- `GET/POST /api/admin/invitations/`:列所有邀请码(跨团队,按 kind/status/search 过滤 + 分页);POST 平台超管发 create_team「开团队码」(写审计 `invite.issue_create_team`)。
|
||
- `POST /api/admin/invitations/<id>/revoke/`:撤销待用码(写审计 `invite.revoke`)。
|
||
- 全程 `IsPlatformAdmin`,非超管 403。
|
||
|
||
**前端**
|
||
- `adminApi`(api.ts 分层):invitations / createInvite / revokeInvite。
|
||
- Admin「邀请码」页(`routes/admin/admin-invites.tsx`):类型筛选段控(全部/开团队码/加入码)+ 表格(码/类型/团队/状态徽章/创建时间/使用者)+「发开团队码」弹窗 + 复制邀请链接(剪贴板 + 反馈)+ 撤销;接入 AdminApp(本地 toast notify)。
|
||
- `Invitation` 类型补 kind/team/team_name;admin-page.css 补段控/表格/弹窗样式(仅 token)。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.adminpanel` **9 项 + apps.accounts 22 = 31 OK**(列权限 403/发码+审计/跨团队列表/kind 过滤/撤销+审计/端到端凭发出的码注册开团队/撤销后码失效)。
|
||
- 无头 e2e:`_admin-p2.mjs`(5188)—— **6 断言全过 + 0 console error**:页面加载、发码弹窗、新码入列、复制链接反馈「已复制」、**端到端用 UI 发的码 API 注册开团队 201**。截图 `output/admin/p2-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|
||
- 视觉:列表截图核对 —— 类型/状态橙/中性 pill、段控黑激活、成功 toast,符合 restraint。
|
||
- 回归:accounts 22 仍全绿;live 后端自动 reload 后 `/api/admin/invitations/` 200(admin)/401(匿名)。
|
||
|
||
---
|
||
|
||
## ✅ Phase 3 · 平台团队 + 用户管理(Admin)— 完成 2026-06-19
|
||
|
||
**后端(apps/adminpanel 扩展)**
|
||
- 团队:`GET teams/`(列跨团队 + status/search 过滤分页,带 owner/成员数/余额)、`GET teams/<id>/`(详情含成员)、`POST teams/<id>/toggle/`(启停 + 审计)。
|
||
- 用户:`GET users/`(列跨团队 + 过滤分页,带所属团队)、`POST users/<id>/toggle/`(启停,停用清 token;**不允许停用平台超管**)、`POST users/<id>/reset-password/`(强制改密,清 token)。
|
||
- **团队停用拦登录**:`login` 校验当前团队 `status`,停用团队的成员一律拒登(平台超管无团队不受影响)。
|
||
- 新 `apps/adminpanel/serializers.py`(AdminTeam/AdminTeamMember/AdminUser);全程 IsPlatformAdmin + 写审计。
|
||
|
||
**前端**
|
||
- `adminApi`:teams/teamDetail/toggleTeam/users/toggleUser/resetUserPassword;类型 AdminTeam/AdminTeamDetail/AdminUser。
|
||
- Admin「团队」页(状态筛选 + 搜索 + 表格 + 详情弹窗含成员 + 启停)、「用户」页(筛选 + 搜索 + 表格 + 启停 + 改密弹窗;超管行带「超管」标记且禁操作)。
|
||
- admin-page.css 补 search/宽弹窗/详情元信息样式(仅 token)。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.adminpanel` **19 项 + accounts 22 = 41 OK**(团队列字段/详情/启停拦登录再恢复/用户列含团队/启停拦登录/禁停超管/强制改密生效旧密失效/权限 403)。
|
||
- 无头 e2e:`_admin-p3.mjs`(5188,用一次性 P3 团队不动 demo)—— **7 断言全过 + 0 console error**:团队页+搜索+详情弹窗;**UI 停用团队→该成员 API 登录 400→启用→200**;用户页超管标记;**UI 改密→新密码登录 200**。截图 `output/admin/p3-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|
||
- 视觉:团队/用户表格、绿/红状态 pill、详情弹窗,符合 restraint。
|
||
|
||
---
|
||
|
||
## ✅ Phase 4 · 质量词 Admin(平台单层)— 完成 2026-06-19
|
||
|
||
**后端**
|
||
- 新模型 `apps/ai/models.QualityWord`(stage + slot + text + sort + enabled,平台单层无 team)+ migration `0009_qualityword`(已 apply 测试 MySQL)。
|
||
- 生成侧 `services.py` 新 `quality_words()/quality_suffix()`,4 个 builder 接入(`build_person_frontal_prompt` / `build_model_tryon_prompt_refs` / `build_storyboard_frame_prompt` / `build_video_segment_prompt`)——**无配置回落原写死值,零回归**。
|
||
- adminpanel CRUD:`GET/POST quality-words/` + `PATCH/DELETE quality-words/<id>/`,IsPlatformAdmin + 审计(create/update/delete);QualityWordSerializer。
|
||
|
||
**前端**
|
||
- `adminApi` qualityWords/create/update/delete;`AdminQualityWord` 类型。
|
||
- Admin「质量词」页(`admin-quality.tsx`):按 stage(人物立绘/模特上身图/故事板/视频)分组卡片 + **编辑模式开关防误删** + 行内增(回车)/改(失焦保存)/删(× 仅编辑态显示);橙色 chip + 虚线添加框(仅 token)。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.adminpanel` **23 项 + accounts 22 = 45 OK**(质量词 CRUD/权限 403/非法 stage 拒/**生成侧读配置 + 无配置回落 + 描述保留**/停用词不取用)。
|
||
- ⚠ `apps.ai` 有 3 个 `StandaloneImageReferenceTests`(image_edit 路由)失败 —— **已 git stash 比对确认:HEAD(未改 services.py)同样失败,为进场前既有,非本 Phase 引入**(本 Phase 只改提示词字符串内容,不动 provider 路由)。
|
||
- 无头 e2e:`_admin-p4.mjs`(5188,跑 live 库故用唯一标记词且**结束兜底清理**)—— **4 断言全过 + 0 console error**:页面 4 卡、编辑模式加词→chip 显示+API 持久化、删词→API 移除;live 库零残留。截图 `output/admin/p4-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|
||
- 视觉:分组卡 + 橙 chip + 编辑态,符合 restraint。
|
||
|
||
---
|
||
|
||
## ✅ Phase 5 · 火山人像审核队列(Admin)— 完成 2026-06-19
|
||
|
||
**后端(apps/adminpanel 扩展,复用 assets/review.py)**
|
||
- `GET asset-reviews/`:跨团队真人(person)资产队列(`?review_status=none|processing|active|failed` 筛 + 分页);`AdminReviewAssetSerializer`(team_name + 首图 preview_url)。
|
||
- `POST asset-reviews/submit/`(批量送审 / 失败重试,逐个 `submit_asset_for_review`)+ `POST asset-reviews/poll/`(轮询 processing,`poll_asset_review`);IsPlatformAdmin + 审计。
|
||
- **性能修复**:序列化器 `get_preview_url` 用 prefetch 缓存 `list(obj.files.all())[0]` 取代 `.first()`,消除列表页 N+1(100 条从 ~5s 降到 ~1s)。
|
||
|
||
**前端**
|
||
- `adminApi` assetReviews/submitReviews/pollReviews;`AdminReviewAsset` 类型。
|
||
- Admin「资产审核」页:状态筛选(全部/未送审/审核中/通过/失败)+ 表格(多选 + 缩略图 + 团队 + 状态盾绿/红/灰 + 失败/未送审行「重试/送审」)+ 吸底 bulk-bar「批量送审」+「刷新状态」(只读 poll);page_size 60。
|
||
- admin-page.css 补 checkbox 列/缩略图/红标记/bulk-bar(仅 token;`.bulk-bar` design 未实现,本次补齐)。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.adminpanel` **28 项 OK**(队列只列 person + 跨团队/状态筛 4 态/批量送审计数+审计(mock 火山)/送审权限 403/poll 只轮询 processing(mock))。
|
||
- 无头 e2e:`_admin-p5.mjs`(5188;wrapper 预建 1 条 person 资产保证有行,跑完删除)—— **5 断言全过 + 0 console error**:页面+5筛选 tab+刷新按钮、60 行、多选出 bulk-bar+批量送审可点(**不点真送审**,避免 Volcano 真消耗)、刷新状态(只读 poll)。截图 `output/admin/p5-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|
||
- 视觉:队列表 + 绿/红/灰状态盾,符合 restraint。
|
||
|
||
---
|
||
|
||
## ✅ Phase 6 · AI 任务监控 + 成本异常(Admin)— 完成 2026-06-19
|
||
|
||
**后端**
|
||
- `GET tasks/`(全局 AITask,?status/task_type/team/anomaly 筛 + 分页)+ `GET tasks/<id>/`(详情含 request/response payload + error)+ `POST tasks/<id>/retry/`(失败重投)。
|
||
- 成本异常:`is_cost_anomaly`(实际 > 预估 × 1.5 且预估>0)+ `?anomaly=1` F() 表达式筛;AdminTaskSerializer 暴露 `cost_anomaly`。
|
||
- 重投 best-effort:仅 FAILED + 图像类(基础资产→generate_base_asset_task / 独立生图→generate_standalone_image_task),其余类型 400(不冒险误投);IsPlatformAdmin + 审计。
|
||
|
||
**前端**
|
||
- `adminApi` tasks/taskDetail/retryTask;`AdminTask`/`AdminTaskDetail` 类型。
|
||
- Admin「任务监控」页:状态筛 tab(全部/失败/成功)+「仅成本异常」chip + 表格(类型/团队/模型/状态/成本 预估→实际+异常标/时间)+ **详情抽屉**(payload JSON)+ 失败行「重投」。
|
||
- admin-page.css 补 JSON 块 / 抽屉宽度 / 异常 chip(仅 token)。**修真 bug**:抽屉缺 `.show` 类导致面板停在屏外,补上后正常滑入。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.adminpanel` **36 项 OK**(列权限/状态·类型·异常筛/成本异常标志/详情 payload/重投 dispatch(mock celery)+ 非失败拒 + 不支持类型拒 + 权限 403)。
|
||
- 无头 e2e:`_admin-p6.mjs`(5188;wrapper 建 1 条 failed+异常任务,跑完删)—— **6 断言全过 + 0 console error**:页面+3tab+异常chip、60 行、详情抽屉含 JSON、失败 tab(43 条真实失败)首行有重投、异常筛精确 1 条;**不点真重投**。截图 `output/admin/p6-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|
||
- 视觉:任务表 + 详情抽屉(成本⚠ / 错误红框 / payload),符合 restraint。
|
||
|
||
---
|
||
|
||
## ✅ Phase 7 · 计费审计 + 4 层额度策略(Admin)— 完成 2026-06-19
|
||
|
||
**后端**
|
||
- 计费:`GET ledgers/`(全局信用流水 + type/team 筛分页)+ `POST ledgers/adjust/`(手动调额,落 ADJUSTMENT 流水 + 更新余额,调后不许为负);`adjust_credit` helper(billing/services/ledger.py)。
|
||
- 额度策略:`GET/POST quota-policies/` + `PATCH/DELETE quota-policies/<id>/`;AdminQuotaPolicySerializer。
|
||
- **额度拦截落地**:`_enforce_quota_policy`(单任务/团队月度/项目 三类限额)接入 `reserve_credit`,**仅当团队有 active 团队级策略才生效 → 无策略零回归**。
|
||
- **审计 helper 加固**:`log_admin_action` 用 savepoint 包裹 + `_json_safe`(UUID/Decimal/datetime 转换),修复「传 serializer.data(含 UUID)致 JSONField 报错 → 吞异常但污染外层事务 → 后续查询 TransactionManagementError」的真 bug。
|
||
|
||
**前端**
|
||
- `adminApi` ledgers/adjustCredit/quotaPolicies/create/update/delete;`AdminLedger`/`AdminQuotaPolicy` 类型。
|
||
- Admin「计费审计」页(类型筛 + 流水表 + 手动调额弹窗:团队下拉/金额±/备注)+「额度策略」页(策略表 + 新建/编辑/删除弹窗:团队下拉 + 月度/项目/单任务上限 + 启用开关)。
|
||
- admin-page.css 补开关行(仅 token)。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.adminpanel` **44 项 + billing + accounts = 70 OK**(流水筛/调额±+审计/超额拒/额度策略 CRUD/**额度拦截 per_task+monthly 生效 + 无策略·停用不拦(零回归)**/权限 403);billing 既有测试全绿(reserve_credit 改动零回归)。
|
||
- 无头 e2e:`_admin-p7.mjs`(5188;用一次性 P7 团队,不动 demo 余额;建的策略跑完删)—— **6 断言全过 + 0 console error**:计费页(80 流水)+ 手动调额→API 验持久化、额度策略页 + 新建→API 验 + UI 删→API 验空。截图 `output/admin/p7-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|
||
|
||
---
|
||
|
||
## ✅ Phase 8 · 模型供应商管理(Admin)— 完成 2026-06-19
|
||
|
||
**后端**
|
||
- `ModelConfig.is_default`(bool)+ migration `0010`;`get_default_model` 优先取 `is_default=True` 的 active 模型,**未钦定回落原「最早 active」(零回归)**。
|
||
- providers:`GET/POST providers/` + `PATCH/DELETE providers/<id>/`(启停 / 改 base_url / 改 key);**api_key write-only**(只写不回传,列表给 has_api_key 布尔)。
|
||
- models:`GET/POST models/`(?provider/capability 筛)+ `PATCH/DELETE models/<id>/`(改定价 / 启停)+ `POST models/<id>/set-default/`(同 capability 其余清默认)。IsPlatformAdmin + 审计。
|
||
|
||
**前端**
|
||
- `adminApi` providers/models/setDefault 全套;`AdminProvider`/`AdminModel` 类型。
|
||
- Admin「模型供应商」页:供应商表(名/Base URL/密钥已配/模型数/启停/编辑)+ 模型表(供应商/能力/定价/状态/默认标/设默认/编辑/启停/删除)+ 供应商弹窗 + 模型弹窗。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.adminpanel` **51 项 OK**(供应商列权限/api_key 隐藏+入库/启停/模型列筛/模型 CRUD/**set-default 改变 get_default_model + 清旧默认**/权限 403);`apps.ai` 仍只 3 个进场前既有失败(get_default_model 改动零回归)。
|
||
- 无头 e2e:`_admin-p8.mjs`(5188;⚠ 该页驱动真实路由 → 用一次性「禁用」供应商+模型 capability=export,set-default 作用在禁用模型无副作用,跑完 API 删)—— **5 断言全过 + 0 console error**:页面渲染、UI 设默认、UI 改定价→9、UI 启停。截图 `output/admin/p8-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|
||
|
||
---
|
||
|
||
## ✅ Phase 9 · 收尾治理(Admin)— 完成 2026-06-19
|
||
|
||
**后端**
|
||
- `GET projects/`:全局项目流水线监控(?status/search 筛 + 分页),`AdminProjectSerializer`(team_name + product_title + status + current_stage)。
|
||
- `GET integrity/`:数据完整性体检(只读)—— 无账户团队 / 无图商品 / 过期未标邀请码 / 无文件资产 / 失败项目 / 卡 reserved 任务 计数 + totals(teams/users/projects/products)。IsPlatformAdmin。
|
||
- 覆盖 plan 四项:项目监控(全)+ 数据完整性(全)+ 商品治理(体检含无图商品)+ 通知/孤儿(体检计数)。
|
||
|
||
**前端**
|
||
- `adminApi` adminProjects/integrity;`AdminProjectRow`/`AdminIntegrity` 类型。
|
||
- Admin「治理」页:数据体检卡片(超 0 高亮橙)+ 项目监控表(状态筛 tab + 团队/商品/状态/阶段/时间)。
|
||
- admin-page.css 补体检卡(仅 token)。
|
||
|
||
**测试底座(全过)**
|
||
- 后端单测:`apps.adminpanel` **53 项 OK**(项目列权限/状态筛/行带团队+商品名/体检报告含 products_without_image≥1 + failed_projects≥1 + totals)。
|
||
- 无头 e2e:`_admin-p9.mjs`(5188,全只读零副作用)—— **4 断言全过 + 0 console error**:页面 7 tab + 运行体检按钮、54 项目行、体检 6 卡、失败筛选。截图 `output/admin/p9-*.png`。
|
||
- 类型/构建:`tsc --noEmit` + `npm run build` 全绿。
|