2026-10-13 07:35:22 +08:00
# Phase 2:平台层(platform.db/认证/配置版本/试金石/统计)实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在 Phase 1 的多租户引擎之上建平台层:platform.db(租户/站点/域名/配置版本/账号)、超管+租户管理员认证、配置保存-校验-版本-回滚-试金石 API、数据视图与统计 API;Registry 数据源抽象(文件源保测试、DB 源上生产)。
**Architecture:** 平台层拥有独立的 platform.db(单库,WAL);`TenantSource` 协议隔开"租户数据从哪来"——`FileTenantSource` ( seed_profiles,测试与出厂档案)与 `DbTenantSource` ( platform.db,生产);admin API 挂 `/api/platform/*` (Host 中间件豁免,认证鉴权守护);配置保存 = 整份快照新版本(校验管线前置)→ registry 热加载。
**Tech Stack:** 同 Phase 1( FastAPI/SQLAlchemy async/aiosqlite/pytest);认证零新依赖(hashlib.scrypt + 内存 token)。
**参照 spec:** §5(配置模型/存储/校验试金石)、§7.2-7.3(认证/页面后端)、§7.6(统计)。Phase 1 遗产:CLAUDE.md 清理单 12 项(本计划顺势处理 #1/#2/#3 )、conftest 跨循环纪律、build_app/app_client/TenantTestClient 夹具。
**关键既有资产(不要重建):** `app/platform/registry.py` ( TenantRegistry/resolve_site/reload/dispose_all)、`wall_schema.compile_wall` (校验已含键 allowlist/占位符白名单/flags)、`PromptProvider` 、`config.py` 的 ProcessSettings( ADMIN_PASSWORD 保留即为本阶段超管引导)、`seed_profiles/` (出厂档案=创建租户的模板源)。
---
### Task 1: platform.db 模型与引导
**Files:**
- Create: `app/platform/db.py` 、`tests/test_platform_db.py`
- Modify: `app/main.py` ( lifespan 初始化 platform engine + bootstrap)
- [ ] **Step 1: 失败测试**
```python
# tests/test_platform_db.py 核心
async def test_bootstrap_creates_superuser_once ( tmp_path ):
engine = make_platform_engine ( tmp_path / "platform.db" )
await ensure_platform_schema ( engine )
await bootstrap_superuser ( engine , username = "admin" , password = "s3cret!" )
await bootstrap_superuser ( engine , username = "admin" , password = "other" ) # 幂等:不重复建/不改密
users = await list_admin_users ( engine )
assert len ( users ) == 1 and users [ 0 ] . username == "admin"
assert verify_password ( "s3cret!" , users [ 0 ]) and not verify_password ( "other" , users [ 0 ])
async def test_platform_db_wal ( tmp_path ):
engine = make_platform_engine ( tmp_path / "platform.db" )
await ensure_platform_schema ( engine )
# PRAGMA journal_mode == wal(照抄 test_registry 的查法)
```
- [ ] **Step 2: 实现 `app/platform/db.py`**
```python
# 模型(PlatformBase 独立 MetaData,与租户库 Base 无关)
class Tenant ( id , slug UNIQUE , name , status , created_at )
2026-10-13 07:41:53 +08:00
class Site ( id , tenant_id FK , slug , name , status , brand_overlay JSON NULL ) # UNIQUE(tenant_id, slug)
# brand_overlay = 站点门面覆盖层(spec §5.2; DbTenantSource 映射进站点 brand 合并链)
2026-10-13 07:35:22 +08:00
class SiteDomain ( id , domain UNIQUE , site_id FK )
class TenantConfigVersion ( id , tenant_id FK , version , config_json TEXT , updated_by , comment , created_at )
# 当前生效 = MAX(version); UNIQUE(tenant_id, version)
class AdminUser ( id , tenant_id FK NULL = 超管 , username UNIQUE , password_hash , salt ,
must_change_password BOOL , status , created_at )
class PlatformSetting ( key PK , value )
def make_platform_engine ( db_path ) -> AsyncEngine : # WAL + pool 2,照抄 make_tenant_engine 纪律
async def ensure_platform_schema ( engine ): ...
async def bootstrap_superuser ( engine , username , password ): ... # 空表才建;must_change_password=True
# hash: hashlib.scrypt(password, salt=salt, n=2**14, r=8, p=1); verify 常量时间比较(hmac.compare_digest)
```
- [ ] **Step 3: main.py 装配** : lifespan 建 platform engine( `Path(__file__).parents[1]/"platform.db"` , .gitignore 已覆盖)挂 `app.state.platform_engine` ; bootstrap 超管(env `ADMIN_USERNAME` 默认 admin / `ADMIN_PASSWORD` 默认 admin123, `must_change_password=True` );退出 dispose。测试缝:`app.state.platform_engine` 预置则沿用(照抄 registry 缝模式)。
- [ ] **Step 4: 全量回归 + 提交** `feat: platform.db 模型与超管引导(WAL/scrypt/幂等bootstrap) `
### Task 2: 认证(登录/令牌/限流/改密)
**Files:**
- Create: `app/platform/auth.py` 、`app/platform/routes/__init__.py` 、`app/platform/routes/auth_routes.py` 、`tests/test_auth.py`
2026-10-13 07:41:53 +08:00
- Modify: `app/main.py` (挂 auth_routes——**本任务就挂**, jiancai Host 下 /api/platform/auth/login 可经中间件直达路由,TDD 循环才能闭环;Task 3 只管豁免与守护)
2026-10-13 07:35:22 +08:00
- [ ] **Step 1: 失败测试** (核心 8 项)
```python
def test_login_ok_returns_token ( app_client ): # admin + 正确密码 → {"token", "role":"super", "must_change_password":true}
def test_login_bad_password_401 ( app_client )
def test_login_lockout_after_5_fails ( app_client ): # 第 6 次 429/423,15 分钟窗(测试里可注入窗长)
def test_token_expiry ( app_client , freezer_or_short_ttl ) # 过期 token → 401
def test_protected_requires_auth ( app_client ): # 无 Authorization → 401
def test_change_password_flow ( app_client ): # 旧密改新密成功;旧密失效;must_change 清零
2026-10-13 07:44:35 +08:00
def test_tenant_admin_role_scoped ( app_client ): # 依赖工厂直测(require_super 拿租户管理员 UserContext → HTTPException 403);端点级覆盖在 Task 5
2026-10-13 07:35:22 +08:00
def test_logout_invalidates_token ( app_client )
```
- [ ] **Step 2: 实现**
- `auth.py` : `hash_password/verify_password` ( Task 1 已有则上移共用);`TokenStore` (内存 dict: token(secrets.token_urlsafe 32B)→{user_id, role, tenant_slug, expires_at}; TTL 默认 8h; prune 过期;logout 删);`LoginLimiter` ( (username, ip) 键滑动窗 5 次/15 分钟,照抄 rate_limit 风格)
- `auth_routes.py` : `POST /api/platform/auth/login` (限流→验密→发 token);`POST /api/platform/auth/logout` ; `POST /api/platform/auth/change-password` (验旧密,must_change 清零)
- 依赖项:`get_current_user(request) -> UserContext` ( Authorization: Bearer 解析→TokenStore 查验→role/tenant);`require_super` /`require_tenant(slug)` 两个依赖工厂(403 语义)
2026-10-13 07:41:53 +08:00
- **must_change_password 强制语义**: UserContext 带该标志;除 `/auth/change-password` 外的所有 `/api/platform/*` 受保护端点在标志为真时返回 409(超管引导密码不得只当摆设)
2026-10-13 07:35:22 +08:00
- 密码永不明文进日志;token 走 Authorization 头(无 Cookie,免 CSRF——spec §7.2)
2026-10-13 07:41:53 +08:00
- 偏差注记:spec §7.2 写 bcrypt/argon2,本实现用 **hashlib.scrypt** ( stdlib 零新依赖,抗 GPU 性能同级)——记录为有意取舍
2026-10-13 07:35:22 +08:00
- [ ] **Step 3: 全量回归 + 提交** `feat: 平台认证(scrypt/内存token 8h/登录限流/改密/角色依赖)`
### Task 3: 中间件豁免与 admin 挂载骨架
**Files:**
2026-10-13 07:41:53 +08:00
- Modify: `app/platform/host_middleware.py` (豁免 `/api/platform/` )
2026-10-13 07:35:22 +08:00
- Test: `tests/test_admin_mounting.py`
- [ ] **Step 1: 失败测试**
```python
def test_platform_api_bypasses_tenant_resolution ( app_client ):
# 未知 Host 下 POST /api/platform/auth/login 可达(不被中间件 404)
def test_platform_prefix_not_open ( app_client ):
2026-10-13 07:44:35 +08:00
# 无 token 打已存在的受保护 auth 端点(POST /api/platform/auth/logout)→ 401(豁免≠放行,认证守护)
2026-10-13 07:35:22 +08:00
def test_prefix_match_precise ( app_client ):
# /api/platform-evil/ 仍走租户解析(未知 Host → 404)——豁免必须 startswith("/api/platform/") 精确
```
- [ ] **Step 2: 实现** :中间件豁免规则 `path == "/health" or path.startswith("/api/platform/")` ;顺序守护测试补一条(豁免后租户中间件仍最外层)。**全量回归确认既有 463 零破坏**。
- [ ] **Step 3: 提交** `feat: /api/platform 豁免租户解析(认证守护)+ admin 路由挂载`
### Task 4: TenantSource 抽象与 DB 源
**Files:**
- Create: `app/platform/tenant_source.py` 、`tests/test_db_tenant_source.py`
- Modify: `app/platform/registry.py` (构造收 source; `load_from_dir` 变 File 便捷构造【既有调用零破坏】)、`app/engine/services/ai_service.py` ( PromptProvider 加 `from_config(md: dict)` 类方法)
- [ ] **Step 1: 失败测试**
```python
async def test_db_source_loads_tenant ( platform_db_with_tenant ): # 夹具:建租户+站点+域名+版本1 config_json
src = DbTenantSource ( platform_engine )
loaded = await src . load_all ()
rt_cfg = loaded [ 0 ] # slug/status/sites(含域名)/snapshot(含 wall+prompts)
assert rt_cfg . snapshot [ "push" ][ "prefix" ] == "ai-kefu-jc" # 快照组直读
assert "system" in rt_cfg . snapshot [ "prompts" ]
async def test_db_source_no_version_raises ( ... ) # 有租户无版本 → 显性错误
async def test_file_source_unchanged ( ... ) # FileTenantSource 行为与旧 load_from_dir 一致(既有 test_registry 全绿即证)
async def test_registry_reload_via_db_source ( ... ) # DB 改版本 → registry_reload → 新配置生效
```
- [ ] **Step 2: 实现**
2026-10-13 07:41:53 +08:00
- `tenant_source.py` : `LoadedTenant` dataclass( slug/name/status/sites raw/snapshot dict);`TenantSource` Protocol( `load_all() / reload(slug)` );`FileTenantSource(seed_dir)` (把 registry 现有 `_load_tenant` 逻辑搬入,**行为逐字等价**);`DbTenantSource(engine)` ( JOIN tenants/sites/domains——**sites.brand_overlay 进站点覆盖层**——+ 当前版本 config_json; prompts 嵌在 config_json 的 `prompts` 键)
- **registry_reload 补 upsert 语义**:现行实现在 slug 未加载时抛 UnknownTenantError——改为"source 已知但未加载 → 载入并插入"(Task 5 创建租户后 reload 依赖此语义);补测试:新租户(未 resolve 过)创建后直接 reload 不炸且可 resolve
2026-10-13 07:35:22 +08:00
- registry: `__init__(source)` ; `load_from_dir(seed_dir, data_dir)` = `TenantRegistry(FileTenantSource(seed_dir), data_dir)` 便捷函数(**既有 conftest/test 调用零改动**);reload 走 source
- `PromptProvider.from_config(md: dict)` (四件套字符串构造;`from_dir` 改为读文件后委托它);DbTenantSource 路径的 TenantRuntime.prompts 用 from_config( seed_dir 字段对 DB 源为 None——avatar 解析链 Task 7 更新)
- [ ] **Step 3: 全量回归(463+新增)+ 提交** `feat: TenantSource 抽象——File源(测试/档案)与DB源(生产), registry 数据源可切换`
### Task 5: 租户管理 API(超管)
**Files:**
- Create: `app/platform/routes/tenant_routes.py` 、`tests/test_tenant_admin_api.py`
- [ ] **Step 1: 失败测试** (核心 6 项)
```python
def test_create_tenant_from_profile ( super_client , file_source_app ):
# POST /api/platform/tenants {name, slug:"demo", profile:"jiancai", admin_username, admin_password}
# → 201; platform.db 有 tenant+main 站点+版本1( config 内容=seed 档案);租户管理员可登录
def test_create_blank_tenant ( ... ) # profile=blank → 出厂默认配置
def test_slug_immutable ( ... ) # PATCH 改 slug → 400
def test_add_domain_and_resolve ( ... ) # 加域名 demo.example.com → registry reload → resolve_site 命中(File/DB 源均可,测试用注入)
def test_disable_tenant_releases ( ... ) # 停用 → resolve 抛 TenantDisabledError; engine 已 dispose
def test_tenant_admin_cannot_create_tenant ( ... ) # 403
```
- [ ] **Step 2: 实现** (全部 require_super,除标注)
2026-10-13 07:44:35 +08:00
- `POST /api/platform/tenants` : profile ∈ {auto, jiancai, blank}——auto/jiancai 读 seed_profiles/<slug>/ 合并出首版快照(tenant.json 的 config + wall.json + prompts/*.md 嵌入);**blank = 其余组取 DEFAULT_CONFIG + wall/prompts 取 jiancai 出厂档案**( compile_wall 要求 wall 全字段在位、PromptProvider 要求四件套——DEFAULT_CONFIG["wall"]={} 会加载即炸;对齐既有先例 DEFAULT_WALL/DEFAULT_PROVIDER 均锚 jiancai 档案);slug 校验(^[a-z0-9-]+$、不可变、与现有租户不撞);同时建租户管理员账号(**must_change_password 置 True**——Task 2 的 409 语义对新租户管理员同样生效)
2026-10-13 07:35:22 +08:00
- `POST /api/platform/tenants/{slug}/sites` (站点 CRUD:建站/停用);`POST .../sites/{site}/domains` 、`DELETE .../domains/{domain}`
- `PATCH /api/platform/tenants/{slug}` ( name/status; status≠active 触发 registry 释放)
- 写操作后 `registry_reload(slug)` ;域名表变更后重建域名索引(reload 内已做)
- GET 列表/详情(含版本号、域名、管理员账号概要)
- [ ] **Step 3: 提交** `feat: 租户管理API(创建向导消费出厂档案/域名/停用/slug不可变)`
### Task 6: 配置 API——校验管线/版本/回滚/试金石
**Files:**
- Create: `app/platform/config_validation.py` 、`app/platform/routes/config_routes.py` 、`tests/test_config_api.py` 、`tests/test_touchstone.py`
- Modify: `app/engine/services/ai_service.py` (无改则不动)
- [ ] **Step 1: 失败测试**
```python
def test_put_group_bumps_version_and_hot_reloads ( super_client , db_source_app ):
# PUT /api/platform/tenants/{slug}/config/brand {"color":"#ff0000"} → 200 {version:2}
# → registry resolve → brand.color 已变;GET config 返回新值+version
def test_put_wall_bad_regex_400_with_field ( super_client , ... ):
# price_patterns 含 "([unclosed" → 400,错误信息含字段路径与 regex 字样;版本不涨
def test_prompts_size_cap ( ... ) # company_facts > 50KB → 400
def test_prefix_uniqueness_across_tenants ( ... ) # 把 auto 的 push.prefix 改成 ai-kefu-jc → 400( spec E1)
def test_rollback_restores ( super_client , ... ):
# 版本列表 → rollback v1 → 生效值回到 v1;产生新版本(历史不可变)
def test_touchstone_verdicts ( super_client , ... ):
# POST /api/platform/tenants/{slug}/wall/test {"sentences":["The total is €500...","预算5000美元","DN800 specs?"]}
# → 逐句 {action: blocked|pass|replaced, replacement?}——不落库不 bump 版本
def test_tenant_admin_scoped_config ( ... ) # jiancai 管理员能改自己、403 改 auto(租户全开,spec §3)
```
- [ ] **Step 2: 实现校验管线 `config_validation.py`**
2026-10-13 07:41:53 +08:00
- `validate_group(group, payload, context) -> list[str]` (错误清单):类型检查(engine 组 int/bool; brand 字符串/字符串列表;contact 开关与 template_languages、misc 映射也纳入类型校验)、wall → `compile_wall` (复用全部既有校验)、prompts 大小上限(company_facts ≤50KB、其余 ≤100KB)、email notify_emails 格式、push/ai 数值范围、`context.other_prefixes` 查重
2026-10-13 07:44:35 +08:00
- `config_routes.py` : `GET /api/platform/tenants/{slug}/config` (当前 merged+version);`PUT .../config/{group}` (校验→整份快照写 MAX(version)+1【updated_by=当前用户, comment 可选】→ registry_reload);`POST .../config/{group}/reset` (**重置出厂**:拷出厂档案该组为新版本——auto/jiancai 档案取 seed_profiles; blank 租户的 wall/prompts 组取 jiancai 出厂档案,其余组取 DEFAULT_CONFIG; spec §5.4);`GET .../config/versions` (列表:版本/时间/作者/备注);`POST .../config/rollback` `{version}` (复制为新版本);`POST .../wall/test` (试金石:compile 请求体里的 wall【或当前+覆盖】→ 逐句调 boundary_filter 纯函数给判定)
2026-10-13 07:41:53 +08:00
- **平台设置 CRUD**(spec §7.3 平台配置页后端):`GET/PUT /api/platform/settings` (仅超管;PlatformSetting 表读写——集团默认 AI 凭据/默认 SMTP; **resolver 回退链接入 platform_settings 归 Phase 3**——本阶段先落存储与接口)
2026-10-13 07:35:22 +08:00
- 鉴权:超管任意租户;租户管理员限自己(require_tenant)
- [ ] **Step 3: 提交** `feat: 配置API(分组校验管线/整份版本/回滚/试金石端点/前缀唯一)`
### Task 7: 头像上传与解析链
**Files:**
- Create: `app/platform/routes/upload_routes.py` 、`tests/test_avatar_upload.py`
- Modify: `app/engine/routes/public.py` ( avatar 解析链:uploads → seed assets → static)
- [ ] **Step 1: 失败测试**
```python
def test_upload_and_serve ( super_client , app_client ):
# POST /api/platform/tenants/{slug}/brand/avatar (multipart png ≤2MB) → 200 {avatar:"<uuid>.png"}
# → GET /api/public/avatar(该租户 Host)返回新图字节;config 版本+1
def test_reject_bad_type_and_oversize ( ... ) # .txt → 400; 3MB → 413
def test_traversal_safe ( ... ) # 文件名服务端 uuid 重命名;../ 不生效(解析链已有防线,测试钉住 uploads 段)
```
- [ ] **Step 2: 实现** :存 `data/uploads/<slug>/<uuid>.<ext>` ( gitignore 已含 data/uploads/);类型白名单 png/jpg/jpeg/webp(魔数校验优先于扩展名);≤2MB;brand.avatar=文件名(经配置版本管线 Task 6——bump+reload);public.py avatar 解析链变:`data/uploads/<slug>/<name>` → `seed_profiles/<slug>/assets/<name>` → `static/kefu-small.png`
- [ ] **Step 3: 提交** `feat: 头像上传(uuid重命名/魔数校验/2MB)与三级解析链`
### Task 8: 数据视图 API(会话/消息/推送日志)
**Files:**
- Create: `app/platform/routes/data_routes.py` 、`tests/test_data_views.py`
- [ ] **Step 1: 失败测试**
```python
def test_conversations_list_with_filters ( super_client , seeded_app )
def test_conversation_detail_with_messages ( super_client , ... )
def test_push_logs_payload_lazy ( ... ) # 列表不含 request_payload 大字段
def test_tenant_scope_isolation ( ... ) # jiancai 管理员 GET auto 的会话 → 403;自己的 → 200
```
2026-10-13 07:41:53 +08:00
- [ ] **Step 2: 实现** (查询逻辑从 jc 原 admin.py 移植查询语义——源文件在 `D:\refactor\ai-kefu-jc\app\api\routes\admin.py` ,实施时可读;跑在 `runtime.session_factory()` 上;分页 page/page_size 上限;筛选:intent/site/日期区间;IP 归国沿用 ip_service)
2026-10-13 07:35:22 +08:00
- [ ] **Step 3: 提交** `feat: 数据视图API(会话/详情/推送日志/租户scope强制)`
### Task 9: 统计 API
**Files:**
- Create: `app/platform/routes/stats_routes.py` 、`app/platform/stats.py` 、`tests/test_stats.py`
- [ ] **Step 1: 失败测试** (小夹具造数→精确数断言)
```python
def test_cards_and_funnel ( ... ): # 会话5→留资3→评估完成2→推送成功1
def test_daily_trend ( ... ) # 两天的会话数按日分组
def test_distributions ( ... ) # intent 三级占比 / source / site / country top
def test_push_health ( ... ) # success/failed/skipped/duplicated 计数
def test_cross_tenant_super_view ( ... ) # 超管无 tenant 参数=全部租户合并;有=单租户
```
- [ ] **Step 2: 实现** : `stats.py` 纯查询函数(输入 runtime/session,输出指标 dict);路由 `GET /api/platform/stats` (超管,可 ?tenant=)与 `GET /api/platform/tenants/{slug}/stats` ( require_tenant);**逐库查询应用层合并,禁 ATTACH**(spec §7.6);日期边界用本地时区(容器 TZ=Asia/Shanghai 部署期生效,代码用 naive local 约定并注释)
- [ ] **Step 3: 提交** `feat: 统计API(卡片/漏斗/趋势/分布/推送健康,逐库聚合)`
### Task 10: 清理单顺势收拢(#1/#2/#3)
**Files:**
- Create: `app/platform/config_views.py` 、`tests/test_config_views.py`
- Modify: 各 services 的 resolver import 改从 config_views( **re-export 保旧 import 路径不断**——或全量替换+旧名删;选全量替换,测试锚定)、`app/engine/services/conn_limiter.py` 与 `rate_limit.py` 的 0 语义注释与行为、`tests/conftest.py` ( FakeAI 收敛)
- [ ] **Step 1: 失败测试** : config_views 单入口(一次 import 拿全 resolve_*);ws_conn_limit=0 与 rate_limit_per_hour=0 的语义统一测试(0=回退出厂默认,写进校验与文档)
- [ ] **Step 2: 实现** : `config_views.py` 聚合 resolve_email/push/ai/contact_flags/context_seed_map/name_to_region_extras(函数体从各服务搬入,服务层 import 之);0 语义统一(limiter 的 0 值回退默认);conftest 加 `FakeAI` 公共类(三份拷贝收敛,旧测试改 import)
- [ ] **Step 3: 全量回归 + 提交** `refactor: config_views 单入口 + 0语义统一 + FakeAI 收敛(清理单#1#2#3) `
### Task 11: 生产装配切换与 bootstrap 脚本
**Files:**
- Create: `scripts/bootstrap_platform.py` 、`tests/test_bootstrap_script.py`
- Modify: `app/main.py` ( TENANT_SOURCE 切换)
- [ ] **Step 1: 失败测试** :bootstrap 脚本幂等导入两租户(跑两遍第二遍零变化);db 源 boot 冒烟(bootstrap → DbTenantSource → registry resolve 两租户 → brand/墙/前缀就位)
- [ ] **Step 2: 实现** :脚本(init schema → bootstrap 超管 → 逐 seed 档案导入租户[已存在 slug 跳过]);main.py: env `TENANT_SOURCE` ( db|file, **默认 db**; conftest 预置 file registry 的测试缝已有,不受影响——conftest 显式构造 File 源 app,不依赖 env 默认)
- [ ] **Step 3: 提交** `feat: 生产装配切换DbTenantSource + bootstrap脚本(超管/两租户幂等导入)`
### Task 12: Phase 2 收尾
- [ ] **Step 1: 全量回归(根+tests/ 双跑)**
- [ ] **Step 2: 零同步 IO 审计复查** ( grep requests/urllib/time.sleep/open 于 app/——新增代码延续纪律;scrypt 在登录路径是 CPU 非阻塞 ~100ms 可接受,注释声明)
- [ ] **Step 3: 安全自查** :token 不进日志、密码不进日志、platform.db 进 gitignore 复核、登录限流在豁免路径上仍生效、上传目录不可执行
2026-10-13 07:41:53 +08:00
- [ ] **Step 4: CLAUDE.md/README 更新** ( Phase 2 完成状态、Phase 3 清理单刷新、bootstrap 用法、admin API 速览、基线数字刷新 463→当前)
2026-10-13 07:35:22 +08:00
- [ ] **Step 5: 提交** `chore: Phase 2 收尾——平台层完成`
---
## 显式不做(Phase 2 范围外)
2026-10-13 07:41:53 +08:00
art-design-pro SPA 全部页面(Phase 3);嵌入代码复制块的前端展示(Phase 3, API 已含 brand);**resolver 回退链接入 platform_settings**( Phase 3 平台配置页联动;本阶段 PlatformSetting 表与 CRUD 已落);Dockerfile/部署/存量数据导入(Phase 4);/health 分级健康(Phase 4 批次,清理单在案);推送失败告警(二期)。
2026-10-13 07:35:22 +08:00
## 执行注意(继承 Phase 1 血泪)
conftest 跨循环三坑(async_db NullPool / TestClient portal / 禁 asyncio.run 直开租户 session_factory)继续遵守;所有测试路径 `Path(__file__)` 锚定;`PYTHONUTF8=1` ;每任务提交带 Co-Authored-By。