# M2 CP2A 正式登录与原句柄交接完整方案(待独立审查) 日期:2026-09-29。依据修订2主会话裁决、CP1限定关闭及用户本轮两项明确选择:**首批仅账号 101、102 且已有 KV 档**;**本检查点只验正式登录与原 handle 交接,在进入首场景前停住**。本文是 CP2 的一个完整可独立验证的子边界,尚未获原 ZCode 审查/主会话批准,不能编码。CP2B 首场景、CP3 业务迁移、CP4 旧运行引用清零、CP5 无用代码清理均不因本方案自动通过。 ## 1. 可观察目标和停止点 默认/缺省 `SaveBackend=playfab` 的启动、登录、Mail/IAP 和进入 `FishingStage` 行为保持原状。仅 `SaveBackend=kv` 且非 AGG 目标、账号为 101/102、正式 API 恰为 `http://s101.local:18080/` 时,启动前不创建 `PlayFabMgr`,完成**一次** S101 登录、现存 KV 档读取与 31 模块 Apply,返回 CP1 同一原 `PlayerKVGameSessionHandle` 的内部会话交接检查点。此状态不进入 `FishingStage`、不初始化 Mail/IAP、不发布 `EvtAPISvrLoginSuccess`、不开放业务 API 或存档写入 UI。登录失败、无档、新建、Schema 不符、关闭失败均留在登录阶段且撤销身份/句柄,不回退 PF、不自动重试写入。`Logout`/应用退出同步撤销并登记异步排水;排水失败不准下一次 Context。 该“正式”只表示 Bootstrap/非 Editor 专用工厂/普通登录入口可到达,不表示已发布玩法。首场景的 Survey 构造与排行榜旧 PF 调用、Friend/AntiCheat/Club 路由另见[只读清单](M2-CP2首场景自动依赖核查-2026-09-29.md),本检查点以双重阶段阻断保护,不能靠运行时异常当边界。 ## 2. 精确设计合同 ### 2.1 启动与账号准入 - `Boostrap.AfterAssetUpdateV2` 将已加载 `config` 传入 `SubBoostrap.RegisterServices`。该方法在任何存档组件创建之前一次捕获精确 `playfab`/`kv`,注册不可变 `PlayerKVRuntimeSelection`。未配置为 PF;其他值在启动处停止,运行期间配置被改不切换当前后端。KV 若为 AGG 目标在启动处明确失败,绝不创建 PF 组件后继续。既有 Editor loop 在 `RegisterServices` 内稍后才把配置临时设为 `kv-loop`;正式捕获不得误拒该 Editor 许可流程,其登录分支仍先由原 `UNITY_EDITOR && !SERVICE_HOST` 入口处理。 - `kv` 分支核 API 的规范 URI 字符串恰为 `http://s101.local:18080/`,只创建必要的 KV 退出生命周期接收者,不创建/唤醒旧 `PlayFabMgr`。PF 分支组件顺序及旧 `Awake` 不变。正式入口不读取 Editor permit、claim、备份和 `PlayerKVGameplayLoop.Enabled`。 - 在 S101 请求前规范化 CustomID 为无符号十进制正整数的最短字符串,**仅 101/102**通过;名单为本批受审代码常量,后续扩员必须另审,不从运行时可改配置读取。响应的 `PlayerId` 必须仍等于请求 ID,`NewlyCreated` 必须为 false,`playerdata/Get` 的非空档必须经 31 模块完整 Schema 验证。`newCreate=false` 单独不算已有档。任一失败撤销关闭;不得创建空档或远端写入。 ### 2.2 三传输与原对象交接 - 给 `PlayerKVGameLoadCapability` 增**正式**工厂,保留 `CreateForValidation` 原签名和 Editor-only 限制。正式工厂从上述已捕获地址和 `Application.persistentDataPath` 形成根路径;`PlayerKVCacheIdentity` 已按 API 指纹和 PlayerId 隔离,不再自造第二层账号目录。`cfg.Tables` 必须在 `SubBoostrap.AfterAssetUpdate` 注册完成后才取快照;Bindings 对正式路径接受明确的本地 decoder 版本令牌,Editor 默认 `kv-loop` 不变。版本令牌仅校验本进程快照一致,不能冒充服务端 Schema 协议版本。 - 正式 auth、KV、account lease 各自有独立 `HttpClientHandler`,共享**同一个** `CookieContainer`,`AllowAutoRedirect=false`,所有权只转交一次;工厂先验证三者不共享,再允许认证发送。正式 handler 不能走 `CreateForValidation` 的“拒绝真实传输”构造;测试替身不能经 Editor permit 混入正式运行。任何部分构造失败逐个 Dispose,失败保留为关闭屏障。 - 正常 `PlayFabUserService` 在捕获的 KV 模式下获得一次性的正式加载准入;当前 `kvControlledInstance` 专属验证语义保持,不能简单改为 true。Coordinator 的 `unstarted` 前置必须在正式模式检查无旧 PF 实例、未发布 InternalId、尚无前一会话,并继续在 Apply 前复核。第 31 模块 `ResetForKVLoad`/`ReplaceFromKVDecoded` 仅在正式加载窗口内允许,已公开账号/其他后端/第二次 Apply 全拒。非 AGG 身份快照使用 `channel=none`、空 Linked、`NewlyCreated=false`,服务端 `NewlyCreated` 仍须独立拒绝。使用 `LoadForGameAsync` 共用 CP1 主体;`TryAcquireGameHandle` 仍只由 Coordinator 调用,UserService 按返回 epoch 调 `TryGetCurrentGameHandle` **一次**并保持同一对象。CP1 三接口均有真实消费者,不另造全局 handle、缓存、同步器或 PF 同名转发。 - `CustomServerMgr` 增正式租约绑定/清除,但 CP2A 的 API 业务路由集合为空;绑定时立即废弃旧空闲 transport,`Cookie` 不对外提供新 jar,任何 `CustomServerPost` 在 KV 模式仍显式拒绝。正式认证只有 Context 内那一次 `S101AccountSession.LoginAsync`;绝不调 `LoginApiServerCore` 建第二会话。租约和 handle 都由同一 Context/epoch 关闭。CP2B 才对 `mail/fetch` 等逐条路由补审;不能在 CP2A 偷开 `club/*` 或所有 POST。 ### 2.3 登录状态、阶段闸与关闭 - `PlayFabUserService.LoginWithBackend` 在原 Editor loop 优先分支之后,用启动捕获选择正式 KV 分支,不走 `LoginPlayFab`、`SetMMTestValue`(其十六进制 PF UserId 解析及 PF 写入均不适用)、`LoginApiServerCore`、TapJoy、YinDun PF 资料路径。构造函数已有的 `EvtAPISvrUnauthorized` 订阅在 KV 模式不得执行“PF 已登录则重登 S101”的旧回调。正式加载通知不得调用未审 `OnInitData` 订阅者;无订阅或唯一经受审登记的接收者才可通过。31 模块 Apply 后仅产生内部 `KVHandoffReady(epoch, originalHandle)`,不将 S101 十进制 ID 写成 PF `UserId`,不宣布 `IsLogin`/`IsAuthorized` 或发布 API 登录成功事件;这些公开状态留 CP2B 随首场景和业务调用一起评审。调用方可取得非权威历史结果,但旧 epoch 不能重新查询“当前账号”换 Owner。 - `PFLoginAct` 的 `rtService.WaitForAPISvrLogin()` 在 KV 首段也不注册(该订阅会一直等尚未发布的事件),收到 KV 内部成功状态后停止加载进度,在登录阶段保留可识别的“准备完成/暂不进入游戏”结果,不调用 `MailService.Init`、`iapService.Init`、`Unload`。`LoginStage.RequestNextAct` 再按捕获模式双重阻断转入 `FishingStage`,防止后续 UI 误调用 `Unload` 绕开。PF 分支完全不变;KV 加载失败不得进入原无限重试/改 ID 对话,应显示失败并要求显式重启或登出,保持关闭屏障。此检查点的用户界面文案不暴露 handle、Cookie 或内部存储细节。 - `Logout`、应用退出/销毁对 KV 分支先同步清内部 ready、账号、handle 和 epoch,再调用 Coordinator.Revoke/Registry 排水;不调用 PF 忘凭据/旧缓冲同步。Close 异常保持阻断状态,不能在故障后创建下一个 Context。后台回调按请求时捕获的 ID/epoch/原句柄判定,旧响应绝不重新发布;不可在锁内 await。 ## 3. CP2A 精确生产白名单候选 | 文件 | 只允许的变化 | |---|---| | `Client/Assets/Scripts/AOTScripts/Boostrap.cs`、`Client/Assets/Scripts/SubBoostrap.cs` | 传入/捕获配置,PF/KV 组件创建分流与 KV 退出钩子 | | 新 `Client/Assets/Scripts/Services/PlayerData/PlayerKVRuntimeSelection.cs`(含 `.meta`) | 不可变 backend/固定 API/101、102 准入;不持有 Cookie/handle | | `Client/Assets/Scripts/Services/PlayerData/PlayerKVControlledLoadCapability.cs`、`PlayerKVLoginContext.cs`、`PlayerKVGameLoadCoordinator.cs` | 正式三 handler 工厂与既有档门;Context/Coordinator 如确需身份/epoch 检查仅增加受限入口,保留 CP1 与验证旧合同 | | `Client/Assets/Scripts/Services/PlayerData/PlayerKVGameplayBindings.cs`、`Client/Assets/Scripts/Stages/LoginStage.cs` | 同一 31 模块绑定进入 Player 编译、补实际注册、双重阶段闸;Editor loop 路径不变 | | `Client/Assets/Scripts/Services/UserService.cs`、`UserService.KVLoad.cs`、`Client/Assets/Scripts/Stages/PFLoginAct.cs` | 正式非 AGG 登录窗口、内部交接与明确停留、Logout;PF 分支不变 | | `Client/Assets/Scripts/Core/CustomServerMgr.cs` | 正式租约绑定/清除与空业务路由拒绝;验证两路由原义不变 | | 新 `Client/Assets/Scripts/Services/PlayerData/PlayerKVRuntimeLifetime.cs`(含 `.meta`) | 若现有根对象生命周期没有合适位置,仅负责 KV 撤销/登记排水,不另造定时器 | 不在白名单:`PlayFabMgr.cs`、`KVMgr.cs` 四入口/Guard 方法体、AggUserService、FishingStage/Survey/Leaderboard、业务 API/活动/商店、Editor permit/受控循环、实际账号配置或数据文件。若实现证明某白名单项多余,删除它;若必须动白名单外文件,先补方案与独立审查。测试宿主可新增 CP2A 合成测试,不借真实 101/102 操作来填单测空白。 ## 4. 验收矩阵和停手条件 | 类别 | 必须验证 | |---|---| | PF 不回归 | 缺省及明确 PF 的 Bootstrap、旧组件、PF 登录、Mail/IAP、阶段跳转行为;Editor 旧 loop 与 CP1/B02 回归 | | KV 准入 | 未知 backend、AGG KV、地址不符、100/103、非规范数字、101/102 新建/空档/Schema 不符全部零业务写且不入场景 | | 成功交接 | 合成 101/102:仅一次 `account/Login`、一次 KV 开启、31 模块、原对象同一 `OwnerInstanceId`/epoch;Guard 释放后内部读取可用,公开玩法仍关闭;无 PF 组件/凭据/旧40秒循环 | | 竞态与关闭 | 加载中取消/Logout、Apply/notify 失败、同账号新 epoch、旧登录回包、GameLoaded 后 Close 故障、handler Dispose 故障、退出注册与观察;失败后无旧对象再发布 | | 网络 | 三 handler 同 jar 不同实例、重定向拒绝、业务路由全拒、无旧 Cookie 替换;没有正式地址请求或账号写入的合成测试 | | 目标编译 | Editor 与 Player 目标均 0 错误;历史 `_cheatingScore` 另列来源/最小修复审查,不把已有错误算通过;Unity 自动化仅 CLI Loop,不打包 | 只有原 ZCode 对**整个 CP2A** 及白名单作独立方案审查、主会话先复核受审 hash 再逐条裁决后,才能修改生产代码。CP2A 实施完还须批末独立实施复核;其关闭只表示登录与原 handle 交接,不启用首场景。CP2B 完成首场景/路由与身份发布后,CP3–CP5 继续逐域迁移并最终删除闲置实现。现有工作树全部保留。