Files
ft/CodexOutput/upserver/阶段4B3分期与4B3A固定存档入口方案-待评审-2026-09-24.md

179 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段4B3分期与4B3A固定存档入口方案(待独立评审)
日期:2026-09-24。作者:主会话。前置:[4B2关闭裁决](阶段4B2实施复核裁决与关闭-2026-09-24.md)。
**本包申请两项独立裁决:A=4B3内部分期是否合理;B=4B3A限定方案是否完整、可实施。不是整个4B3已经完成设计,更不是批准全量游戏接线。** 4B3B/C的业务调用起点、可变对象隔离及启用接线仍须分别形成完整方案;本包不以扫描表代替这些方案。当前没有4B3业务代码。
## 1. 目标、现状和最小改动原则
整个4B3最终交付:单次登录固定后端;普通KV读写使用创建操作时捕获的固定句柄;旧回调在改变共享内存前即失效;持久化失败可辨;定时/暂停/退出属于原会话。游戏正式启用仍受4C、tickets有界历史、身份专项、4D、P2-α真实档专项及用户产品确认约束。
当前4A固定句柄已有安全快照、Update/Delete/Flush/WaitForTicket,4B1/4B2已负责所有权和受控加载。**不再造Owner、缓存、传输、会话管理器或另一套验证专用普通存档框架。** 首单元只在既有PlayFabMgr增加显式固定句柄入口及一个键读取结果类型;不替换旧调用、不改模块字段、不放开旧guard、不公开Context的私有handle。
文本初扫234行/84文件包含注释和定义。Roslyn语法盘点及逐位置台账见[调用事实与分期附件](阶段4B3调用事实与分期附件-2026-09-24.md)和`B3DesignAudit/calls.json`。它记录实际语法调用及按名称匹配的上游候选;没有语义绑定,也未证明所有事件、反射、UnityEvent持久订阅和接口实现链完整。这个事实是分期理由,不能被隐藏为“全链隔离已经设计完成”。
## 2. 路线选择与分期裁决请求
| 路线 | 具体影响 | 建议 |
|---|---|---|
| 让旧PlayFabMgr.Instance自动取当前KV句柄 | 改动少,但A回调到B时会写B;保存前的共享内存污染也未解决 | 不采用 |
| 一次改全部普通调用、消费、支付和身份 | 跨越现有4C/身份边界,逐调用证据不足,白名单过大 | 不采用 |
| **显式固定入口→业务操作隔离→加载和生命周期接线** | 复用已验基础;每次只批准已完整定义的单元;直到全链通过仍禁用正式玩法 | 推荐,待独立评审 |
| 单元 | 交付与关闭条件 | 不包含/下一前置 |
|---|---|---|
| **4B3A 固定存档入口** | 本方案§3~8全部通过,旧PF路径字节保持、4B1/4B2回归及独立实施复核关闭 | 不发布游戏写权限、不更换任何业务调用、不接定时器;不能代替4B3B/C |
| 4B3B 业务操作及内存隔离 | 每个业务入口明确捕获点/对象归属/取消点;同步、await、广告/支付/事件/协程跨账号验证;接口拒读不再当缺档 | 完整逐调用合同及白名单另审;未完整审查的入口不得在KV模式运行;4C交易结果语义不在此伪造 |
| 4B3C 加载发布与生命周期 | 私有handle受限发布、后端冻结、初始化意图、40秒周期/暂停/恢复/退出,真实服务组合与31对象回归 | 必须先完成B;生产准入仍关;不能删除B2guard或把LoadedForValidation改为Done |
A→B→C全部独立关闭才可称4B3完成。每个单元完成后,主会话按用户最新授权自动做无需复审的后续准备,不在“是否继续”处停顿;遇到完整新方案/必要差异/独立实施复核门时,准备好可审材料后交接。**本次没有请求免除既定审查门。**
## 3. 4B3A开发范围与接口
新增方法放在`Core/PlayFabMgr.cs`一个有起止注释的`KV explicit fixed-handle access`块中,均为`internal static`;不访问`Instance`、Unity场景、PlayerPrefs、旧缓冲、SDK或任何全局“当前账号”。静态方法不触发组件Awake;不新增静态字段、timer、事件或内部重试。生产Player编译保留这些普通内部方法;不存在用户配置/公共登录路径调用它们。
调用者必须显式提供已有`PlayerKVGameSessionHandle`,方法只调用这个实例,绝不寻找替代句柄。该类型由4A Bridge产生,4B1 Context的private handle保持private,4B2 LoadedForValidation不会因此获得它。
| 计划接口 | 输出与用途 |
|---|---|
| `ReadKV(handle, string key)` | 新`PlayerKVKeyReadResult`;严格区分有效快照内缺键与拒读 |
| `WriteKV(handle, string key, string value, bool remoteStore = true)` | 既有`PlayerKVEditResult`,完整保留本地接受、原始错误、票据和不确定提交标志 |
| `FlushKVAsync(handle, CancellationToken waitToken = default)` | 既有`Task<PlayerKVFlushResult>`;透传水位和分类结果,不合并成bool |
| `WaitForKVTicketAsync(handle, PlayerKVSyncTicket ticket, CancellationToken waitToken = default)` | 既有`Task<PlayerKVTicketResult>`;确认的是原句柄的原票据 |
不新增Delete facade:当前业务没有显式Delete调用需求,空字符串写入仍是存在值,不能偷改为删除。底层已有Delete保持;未来确有调用需求再纳入对应单元合同。原`GetLocalData/UpdateUserDataValue/SyncDataImmidiate/CompareDatas`签名和实现一律不改。
### 3.1 新键读取结果
新增`Services/PlayerData/PlayerKVKeyReadResult.cs`,命名空间`game.PlayerDataStorage`。只含internal enum和internal sealed不可变结果;不增加业务DTO或可变字典。
`PlayerKVKeyReadError`:None、InvalidInput、LoadGuardActive、StaleOrClosing、NotReady。
结果属性:`Error`、`Success => Error == None`、可空bool `Found`、string `Value`、`OwnerInstanceId`、`PlayerId`、long? `Epoch`。创建器internal;没有set、隐式bool/string转换。失败时Found=null且Value=null,禁止构造伪缺键;null handle时身份全null;有效handle失败仍可带该原始身份用于诊断,但不带Cookie/存档正文。
读取顺序固定:
1. 先调用已有`PlayerKVGameLoadGuard.Reject(ReadLocal)`。命中则返回LoadGuardActive,记录已有违规计数,零快照读取。不能另开不计数的guard旁路。
2. null handle或`!KVSnapshot.ValidKey(key)`返回InvalidInput。使用已有规则:非null、非空、长度≤128;不Trim、不改大小写、不替换key、不额外拒绝当前规则允许的空格。
3. 仅调用指定handle的`ReadLocalSnapshot()`一次。其StaleOrClosing/NotReady分别映射;不再读取全局身份或重新Get远端。
4. 成功快照按Ordinal键查询:存在→Found=true/原字符串;缺键→Found=false/Value=null;空字符串是Found=true。
5. 读取成功是一次快照观察,不是未来业务操作许可。调用后账号失效时,B3B仍须在首次内存修改前重检其操作身份。A不宣称返回值能自动撤销已经拷出的字符串。
既有Snapshot实现负责操作租约及读后归属检查。4B3A不修改其并发线性化点,也不把普通结果对象当授权令牌。
### 3.2 写入与错误合同
先调用`guard.Reject(Update)`;命中返回`PlayerKVEditResult(StateResult=Blocked)`,无票据。null handle返回StateResult.Validation。随后**原样返回**指定handle.Update(key,value,remoteStore);key/value非法与Ready/关闭优先级沿用底层,不在facade复制第二套校验流程。
| 结果 | 可以作出的判断 | 不允许作出的判断 |
|---|---|---|
| Accepted=true、remoteStore=true | 该原Owner缓存已接受,Ticket保留 | 远端已确认、可发奖、可重放交易 |
| Accepted=true、remoteStore=false | localOnly接受,通常无Ticket | 该key所有历史远端pending被撤销,或永远不会发送历史值 |
| Validation | 本次输入未被接受,保留原字段 | 自动规范化输入后重试 |
| Blocked | 原Owner/状态不准入 | 改取当前B账号句柄再写 |
| StorageError | 保留AttemptSequence/Revision/LocalCommitMayHaveSucceeded | 将不确定提交改成false并回滚/重试业务动作 |
remoteStore=false沿用3A:写localOnly不清除同key已有remote pending;以后Flush可能发送此前remote值。先remote后localOnly、localOnly后remote必须分别测,不按旧PF名称猜语义。value=null不等于删除,沿用Validation/Blocked结果;value=""是正常存在值。
写操作与Close竞争时,已被底层准入的A操作可以完成于A;结果保留实际发生的接受事实。它永远不改发B,也不因返回后失效而把可能已持久化的Accepted伪装成未提交。新的调用在旧handle失效后由原Owner拒绝。B3B负责结果返回后的UI/内存归属,不能凭Accepted绕过。
底层预期错误结构化透传;未预期编程异常不catch成缺档、成功或可安全重试。允许向调用方传播,后续B3B入口按故障态中止,不能继续业务。A测试显式验证不吞异常。
### 3.3 Flush与票据
Flush先`guard.Reject(Sync)`;命中返回计数Confirmed=0/Superseded=0/Blocked=1/Unknown=0、TransportError=Cancelled、CacheStatus=handle?.Status或NotLoaded。null handle同样Blocked=1,但TransportError=Validation、CacheStatus=NotLoaded。通过后直接返回handle.FlushAsync(waitToken),不另开Task.Run/定时器/水位,不改Confirmed/Superseded/Unknown。
票据等待先`guard.Reject(Sync)`;命中返回Status=Blocked、TransportError=Cancelled及原handle.Status/NotLoaded。null handle返回Blocked/Validation/NotLoaded。通过后直接返回handle.WaitForTicketAsync(ticket,waitToken)。票据null/外来Owner/epoch/sequence由已有Coordinator检查,失效handle优先级沿用4A;不创建替代ticket。
取消token只结束调用方等待,不撤销已经发出的操作,不重试,不释放Owner资源。已有Owner跟踪的内部任务继续收尾,Close继续等待排水。预取消token也沿用现有4A调用语义,不在facade声称“保证未发送”。票据Confirmed仅表示该原票据确认;Superseded不等于当前值确认,Unknown不能当失败可重扣。等待不主动触发Flush。
## 4. 4B2兼容与账号归属
四个新入口全部服从现有B2加载guard。guard活跃期间任何新入口调用都拒绝,并增加现有计数;不允许为测试普通写临时释放B2guard。4B2仍是单认证、加载事务、零业务写、LoadedForValidation,旧受控工厂与私有handle不改。
这里的拒绝点是调用入口处的guard观察,不是跨整个异步操作持有的全局租约。已经在guard建立前准入的原Owner操作仍由原Owner排水,不能宣称新静态方法凭一次检查就解决“普通写→下一加载”的切换竞争。A没有生产调用者,也不开放加载后写权限;C必须在上一普通会话撤销/排水后才能进入新加载guard,不能与未排水普通访问并存。若外部认为此限制仍不足以允许单独实现A,应退回分期或要求最小准入合同补审,不能在实施中临时修改Guard。
普通A单元测试通过**现有4A合成session+Bridge**取得句柄,独立调用新facade;这不等于“4B2加载后写入已接通”,二者组合属于C。Editor验证沿用固定反射入口模式,但新验证host仅用既有internal合成工厂,没有运行时可用的网络工厂、反射通用执行器或配置开关。
验证host固定使用`http://kv-route.invalid/`,合成handler直接返回响应且检查固定host/path,禁止调用默认HttpClientHandler或任何转发代理;不更改项目正式地址。缓存仅在`CodexOutput/upserver/KVSaveRouteTests/unity-runs/<runId>/`。不创建PlayFabMgr组件/不触发Awake,不启动游戏容器。guard用TryEnter取得本次所有权,失败即停止该用例,不释放他人的guard;finally只释放本次取得的guard,先Close Bridge排水再Dispose认证session,异常和未完成Task保留为失败。不得覆盖既有静态单例来让测试通过。
固定身份为OwnerInstanceId+PlayerId+Epoch;同账号再次登录也必须是新操作归属。方法参数永不替换。A验证旧A句柄在B打开后不能读/写/Flush B,并核对B缓存和请求数,而不只看返回错误。
## 5. 后续业务合同(约束,不是本轮实施批准)
| 场景 | B/C必须满足的合同 |
|---|---|
| 同步按钮/业务入口 | 在首次可变状态写、事件或奖励前建立原会话操作归属。只在末端保存加校验不足。 |
| await/协程/SDK回调 | 发起时捕获操作;恢复后先重检再Resolve/修改业务对象、更新UI或发奖;同账号新epoch也拒绝旧结果。 |
| 常驻订阅/计时器 | 注册时捕获epoch与对象生命周期,关闭先撤销再退订;已经入队的回调仍需复查。 |
| 原地Reset/Replace的31对象 | 旧引用可能仍指向已装入B数据的同一实例,必须在变更入口校验原epoch;不能只比较ReferenceEquals或ID。 |
| 仓库读取异常 | EventDataRepository等不能把Stale/NotReady转换为new T并继续保存;只有有效快照的缺键可进入明确的新数据策略。 |
| 奖励/支付/消费 | B只处理归属与中止;持久化失败后的奖励一致性必须有单独业务裁决,不能凭本地Accepted发奖或重新扣减。两个立即保存消费点继续归4C。 |
| 普通写入失败 | 一次操作内停止后续依赖副作用;不以改写全局缓存/从新账号重试补救;是否可恢复以具体业务合同为准。 |
| 生命周期 | C才能建立原会话40秒周期、暂停Flush/恢复、同步撤销及有界退出观察;等待超时不释放占用资源、不宣称已确认。 |
上述合同尚需B/C逐入口落到源方法/字段/事件和文件白名单,未落完则B/C不得编码或关闭。本方案没有将全部业务方法的安全性归功于四个静态facade方法。
## 6. 4B3A精确白名单
| 文件 | 允许的唯一内容 |
|---|---|
| `Client/Assets/Scripts/Core/PlayFabMgr.cs` | 新增§3四个internal static方法及块内私有结果构造helper;旧代码块字节保持,不调整Awake/Compare/普通PF/立即PF/生命周期 |
| `Client/Assets/Scripts/Services/PlayerData/PlayerKVKeyReadResult.cs`及meta | §3.1不可变结果和枚举 |
| `Client/Assets/Scripts/Services/PlayerData/PlayerKVSaveRouteEditorValidationHost.cs`及meta | 整个文件UNITY_EDITOR包裹;固定合成传输、固定case,实际调用新增facade,无动态代码执行 |
| `Client/Assets/Editor/KVSaveRouteValidation.cs`及meta | Editor固定类型/方法/case白名单反射转接,无任意类型/路径/参数执行 |
| `CodexOutput/upserver/KVSaveRouteTests/` | 新显式链接源码测试项目/替身/证据;链接现有底层和PlayFabMgr,不复制实现 |
| 本阶段文档、入口、状态、TODO、日志 | 主会话检查点、送审/交付/hash材料;保留旧受审证据 |
禁止改:User/Agg/Context/Bridge/Owner/Guard/Registry/Schema/State/Cache/Coordinator、CustomServerMgr、QuitAct、任何业务DataCenter/UI、场景/字体/配置/包/SDK、旧测试源码与csproj。当前唯一显式链接PlayFabMgr的旧项目是`KVGameLoadTests/ServiceHost/ServiceHost.csproj`,PlayerKV源码为通配纳入;新增Editor host需使用UNITY_EDITOR且额外`!SERVICE_HOST`条件排除内容,避免旧宿主误编译验证实现。既有旧Editor host排除项不改。
普通新结果文件被该通配纳入,旧宿主不需要csproj补丁。其他旧显式Compile项目不链接PlayFabMgr,新增facade依赖不影响它们。如实施发现依赖与此不同,停止相关修改先提出精确差异,不默认扩大白名单。
新meta使用新GUID;不重新序列化场景或资产。新测试依赖用现有Unity Newtonsoft/PlayFab程序集与本地.NET SDK,不安装新包。测试stub仅替代SDK/Unity边界,必须说明它不能证明真实渠道全流程。
## 7. 4B3A验收矩阵
全部测试合成账号/传输,缓存位于独立运行目录;无正式服请求。按语义组计数,不预造断言总数。每条应有实际日志和对应源测试名。
| 编号 | 场景与必须观察到的结果 |
|---|---|
| B3A-01 | Ready读存在/缺键/空字符串,Found分别true/false/true;精确大小写、空格、Unicode/边界128字符按现有ValidKey规则;无新Get。 |
| B3A-02 | null handle、null/空/129字符key为InvalidInput;guard活跃优先LoadGuardActive;失败Found=null/Value=null。 |
| B3A-03 | 旧/关闭handle返回StaleOrClosing;先沿用4A Seed制造真实Conflict/Unknown,state.json.tmp目录冲突制造StorageError,合成401制造授权故障,验证失败Found=null。ReadFailed等底层状态覆盖沿用4A/3A回归;本层证明两个Snapshot错误的映射,不冒称所有状态均由新facade端到端重造。 |
| B3A-04 | remote=true写接受、返回真实票据;网络尚未发送,读取见本地合并值;Accepted不被测试命名为远端成功。 |
| B3A-05 | remote=false接受无ticket;remote→localOnly后Flush仍可发送旧pending;localOnly→remote后读/发送新remote值,分别验证。 |
| B3A-06 | value=null、非法key按底层Validation/Blocked,空串保留;无自动Delete、无规范化输入。 |
| B3A-07 | 沿用KVGameBridgeTests.NonReady:在本run唯一state.json旁创建state.json.tmp目录使实际写失败,facade返回StorageError及实际sequence/revision/uncertain=false,无重试。uncertain=true的AfterReplace分支沿用KVSyncTests现有五阶段fault回归+facade原样return源码审计,不宣称新facade已端到端注入AfterReplace;不得修改readonly字段或底层增加故障钩子。 |
| B3A-08 | 正常Flush真实计数/水位;调用后的新写不被旧水位虚报确认;同keySuperseded保留,后续ticket必须独立确认。 |
| B3A-09 | 合成Set已执行但响应丢失→Unknown;401及前置拒绝保留原结果;无再次Set、无改发PF、无自动Refresh。 |
| B3A-10 | null/外来Owner/同账号新epoch票据拒绝;旧A票据Confirmed也不代表B的新值确认;Wait不主动发送。 |
| B3A-11 | Flush等待取消/预取消仅取消观察;忽略取消传输仍排水,Close未完成前不能复用;保留原Task直至收尾。 |
| B3A-12 | A句柄捕获→关闭→B打开→A的四个facade调用拒绝,B缓存、请求、业务替身内存不变;同账号新epoch复测。内存断言仅证明测试回调自身遵循合同,不称所有业务已迁移。 |
| B3A-13 | Close与Write/Read/Flush竞争:允许底层已准入A操作完成在A;不改写接受事实,无跨Owner请求,随后调用拒绝。 |
| B3A-14 | B2guard活跃时四入口全部拒绝;ReadLocal/Update/Sync计数符合次数;SDK/Prefs/Set/Delete均0,guard不释放。 |
| B3A-15 | facade静态调用无需MonoBehaviour/Instance;未触发Awake/旧timer/缓冲/Prefs;四个方法及helper移除后PlayFabMgr与基线字节一致。 |
| B3A-16 | 新源码宿主两平台条件;无UNITY_EDITOR时验证host消失,四普通入口仍存在但无生产调用者;旧B2 ServiceHost双条件和PlayerProbe保持。 |
| B3A-17 | Unity CLI Loop实际HotUpdate程序集调用固定case,合成A/B固定句柄读写/guard测试,关闭所有资源并恢复静态guard;0编译错误/新文件0警告,无PlayMode。 |
| B3A-18 | 原4A18组、4B1全23组、4B2全75组/关闭失败三进程/服务双条件/Unity B2-20,42/86/29/Review/3C离线回归;KVLive及旧NetworkTransport仅build。间歇首败完整保留,不以重跑覆盖。 |
| B3A-19 | 源码范围/hash一致,新增生产依赖正确,限定自审、外部实施复核与主会话逐条裁决完成才关闭A;不将A通过等同B3或G4通过。 |
不在A测试真实广告、支付、PlayFab渠道、活动启动或全场景。这样的测试不属于A能力,不能声称通过。实际普通游戏启用证据属于后续B/C/4C/4D。
## 8. 实施检查点(仅在本方案独立通过并裁决后生效)
0. 核对受审hash、A/B两部分裁决及白名单;保存PlayFabMgr开工副本、现有源hash;记录实施批准。任一P1/P2未关闭不编码。
1. 新键结果与ReadKV;先测B3A-01~03、14/15相关项,旧PF区字节比对,更新日志。
2. WriteKV/FlushKVAsync/WaitForKVTicketAsync透传;B3A-04~14全部故障/竞争用例,记录最初失败与修复;不增加后台机制,更新日志。
3. 两平台真实源码宿主、非Editor表面、Unity CLI Loop及4B1/4B2全量回归;观察首败根因不明时保留原状态,必要问题单独裁决,更新日志。
4. 范围/hash/限定自审/交付矩阵及外部实施复核提示词封板;外部接受后主会话逐条裁决关闭A。接着自动开始B的完整方案准备,不询问是否继续;B方案未经审查不编码。
每检查点更新`开发日志.md`与新`阶段4B3A实施记录-2026-09-24.md`。只有文档准备时写“设计检查点”,不冒充实施检查点已完成。不Git提交、不打包、不运行正式转发代理。
## 9. 作者自审及外部重点
已核对A依赖复用、guard计数、无新会话拥有权、key规则、localOnly与已有pending语义、异步取消、旧PF不变、旧宿主通配依赖和Editor/Player隔离。作者自审不代替独立评审。
外部应分别回答:分期是否合理,A是否过薄/重复或存在必要接口缺口;四入口是否留下guard/所有权旁路;只改PlayFabMgr新增块是否可验证PF保持;A的测试是否可用现有故障能力实现;后续B/C不完整是否被正确限制为未批准,而非包装成全量完成。
审查若认为必须先完整证明全部业务调用再实施A,可明确退回分期A部分;主会话按报告裁决,不以“未来不启用”自动豁免任何本单元实际副作用问题。