启动与应用壳
Next / React;RootClientComponent提供Redux,Main执行初始化与生命周期管理。
SvelteKit入口按路由组合布局和页面节点。
启动负责接线,业务初始化按职责拆开;框架差异本身不决定优劣。
核对架构依据
gogogold源码
- src/app/RootClientComponent.tsx · 第18行
- src/app/Main.tsx · 第62行
- src/app/hooks/useIdleRoutePrefetch.ts · 第22行
先从架构层明确谁负责状态、请求和清理,再比较实现与风险。gogogold按Web应用评估;建议、事实与待验证项分别标注。
游戏容器与移动体验已经有基础。
优先补异步结果保护和交互一致性。
共享游戏壳、状态流程和事件账本值得借鉴。
加载、安全与交互的具体做法见独立分析。
gogogold是以供应商游戏容器为核心的Web应用;Stake包含自有游戏控制器。最值得对照的是会话、生命周期和基础设施,具体游戏业务须保留各自边界。
从启动到服务器边界,逐层比较职责与协作关系。
Next / React;RootClientComponent提供Redux,Main执行初始化与生命周期管理。
SvelteKit入口按路由组合布局和页面节点。
启动负责接线,业务初始化按职责拆开;框架差异本身不决定优劣。
首页、登录、游戏路由;RouterMgr;统一弹窗工厂和自动弹窗队列。
共享页面壳;网址驱动弹窗宿主;按定义检查限制和关闭条件。
可恢复入口与自动任务分别管理。登录/活动入口可进入URL,奖励继续使用队列。
供应商iframe与Unity适配器负责连接、超时、降级和退出。
共享游戏壳处理可用状态;Dice等内建游戏自己维护单局与自动模式。
gogogold保留适配器边界,统一加载阶段;不复制内建下注状态机。
Redux保存账号和余额;Zustand管理目录、分类与偏好;会话恢复及切换另有调用链。
组件上下文约束会话store;共享偏好存储;游戏账本按单局编号协调事件。
每类数据明确权威来源、写入入口、账号作用域和失效时机,避免重复所有者。
共享请求拦截器、超时/重试、短缓存、广播通道、LCPGate、图片与资源加载工具。
GraphQL/游戏HTTP/订阅通道;依赖预载、按需组件和图像工具。
协议服务于业务,不因参考站而迁移协议。先补取消、脱敏和观测边界。
登录、余额快照和供应商通信是可见客户端合同;未审计gogogold服务器实现。
只能看到客户端接口与响应处理,未取得真实服务器源码。
授权、资金与开奖的最终判断由服务器负责。客户端检查只改善早期反馈和状态协调。
要统一的是边界与写入规则,不能把所有状态简单塞进同一个store。
用户操作 → 页面 / 游戏适配器 → 共享请求 / 外部协议 → 服务端结果 → 账号或目录状态 → 组件显示
返回大厅触发余额刷新;跨标签广播负责前端协调。用户操作 → 共享壳 / 游戏控制器 → 查询 / 下注 / 订阅 → 本局事件与会话状态 → 展示与结束清理
账本描述本局事件协调,不是服务器资金账本。| 数据领域 | 当前归属 | 权威来源 | 建议约束 |
|---|---|---|---|
| 身份与会话 | gogogold:登录恢复、token刷新与账号状态;Stake:上下文中的session。 | 服务器身份为权威;前端会话仅为客户端视图。 | 切账号时标记身份版本;旧请求和账号相关缓存不得发布到新会话。 |
| GC / SC余额 | gogogold:userInfo快照、显示单位与回大厅刷新。Stake:本局事件账本与真实余额同步。 | 余额快照/事件应各自遵守服务器合同。 | 定义版本是否单调、币种单位及允许覆盖的字段;合同未确认前不随意丢弃旧版本响应。 |
| 目录与偏好 | gogogold:目录store与选择性持久化;Stake:偏好store与cookie。 | 目录来自服务;界面偏好由用户选择。 | 区分公共目录、账号收藏、最近记录与界面习惯;逐项规定持久化和切账号失效。 |
| 游戏实例 | gogogold:iframe/Unity实例及加载任务。Stake:游戏壳与具体游戏控制器。 | 当前实例拥有监听、计时器和资源句柄。 | 退出有统一dispose入口;迟到消息和异步结果须绑定当前实例。 |
| 弹窗 | gogogold:工厂管理资源,管理器管理显示,队列管理自动调度。Stake:URL与弹窗定义。 | 显示状态与加载资源分开。 | 避免队列、URL与组件同时争夺打开状态;入口恢复只覆盖可恢复的业务弹窗。 |
P1优先核验正确性与信任边界,P2治理生命周期与观测;均未据此宣称线上故障已复现。
依据首页effect依赖userId,但已见cleanup只清Live定时器,缺少等价的响应身份检查。
建议复用KYC当前身份校验方式,响应发布前检查身份/请求代次。
延迟账号A请求,切到B后再返回A:B的目录、收藏和定时任务不被覆盖。
依据共享请求存在内部timeout controller;传入外部signal时,fetch选外部signal。
建议将超时、外部取消和页面退出合成一个有效信号,并区分取消原因。
仅超时、仅外部取消、二者同时发生:请求均能终止,计时器被清理,取消不会误触重试。
依据富文本组件当前直接输出content;调试路径接收完整配置和响应。
建议先追踪内容源/服务端清洗与生产日志开关;按输出位置补清洗和字段脱敏。
不可信标签/属性被阻止;授权头、密码和验证码不会进入相关日志。
依据iframe与Unity已有不同的保护/销毁机制,尚不能由局部函数推出全链路清理完成。
建议让游戏会话适配器统一拥有消息订阅、加载任务和dispose职责。
进入后立即退出、连续切换游戏:旧实例不回写,监听和资源任务得到释放。
依据已有LCPGate、分区懒挂载、图片优先级和录屏按分支加载,尚无同条件性能对照。
建议记录门控完成原因与关键资源时序,再为首页和游戏入口设预算。
同设备/网络测量LCP、CLS、INP;能解释超时兜底、接口等待、图片和SDK成本。
以下是建议,尚未修改gogogold项目。先做局部治理,再用证据决定是否需要更大重构。
现有能力已覆盖容器、弹窗、缓存与加载。框架和协议迁移没有当前证据支持。
代价是继续治理已有接口和状态边界;不以重写替代问题定位。账号相关异步结果先证明属于当前会话,再写入共享状态;不要求每个公开请求都登录。
版本标记需要与登录、退出、切账号统一接线;取消请求本身仍不足以替代回写检查。供应商能力仍由iframe/Unity适配器实现,对外暴露加载、就绪、失败和退出阶段。
需要逐一适配供应商协议;共享壳不承担供应商内部规则。先建立状态清单与写入入口,再合并重复逻辑;保留Redux/Zustand现有明确职责。
短期仍是双状态库。是否收敛为一种,需由重复所有权和实际维护成本决定。保护覆盖具体输出/请求/会话,性能覆盖具体设备与场景;代码评分不替代验证。
需要针对性运行验证和观测数据,不能仅靠静态报告给出达标结论。6项具体工作,每项给出当前实现、改法、对应文件和验收条件。代码事实与尚未复现的影响分开说明。
| 建议 | 具体事项 | 类别 | 证据范围 |
|---|---|---|---|
| 先处理 | 首页旧请求失效:换账号或离开后不再回写 | 正确性保护 | 代码确认存在保护缺口;影响待运行验证 |
| 先处理 | 首页 Base Config 只选一条获取路径 | 请求与加载 | 重复获取路径已确认;性能收益需网络测量 |
| 先处理 | 游戏消息先检查类型,再调用业务回调 | 消息稳健性 | 类型保护缺口已确认;来源增强需兼容协议验证 |
| 小改即可 | 余额组件只订阅实际显示的字段 | 渲染订阅 | 订阅粒度优化明确;实际收益待测 |
| 顺手整理 | 同步表单校验直接返回结果 | 表单维护 | 同步Promise和重复校验已确认;属于维护优化 |
| 先核验再收口 | 请求调试日志默认遮蔽认证与个人字段 | 日志脱敏 | 全配置日志调用已确认;最终输出与生产状态待核验 |
开始任务:保存当前账号与任务代次 清理:标记失效,清计时器 每次异步返回:若任务失效或账号变了,退出 确认仍有效:再更新状态、存储或安排后续任务
配置请求 = 正式用户 ? 获取带身份保护的配置 : 获取访客配置 [推荐, 配置] = 并行等待两个请求 检查任务是否仍有效 使用本次配置结果,避免成功后重新请求
若 origin 不匹配:忽略 若 source 不是当前游戏窗口:忽略 若 data 不是对象或 type 不是字符串:忽略 若类型不在协议约定中:忽略 否则进入业务回调
币种 = 订阅 rewardType SC原值 = 订阅 convertTotalScAmount GC原值 = 订阅 gcAmount SC显示值 = 按现有单位规则换算SC原值
错误集合 = 校验当前登录方式的必要字段 一次发布错误集合 有错误:结束提交 无错误:按原业务顺序继续
调试日志 = {接口名, 方法, 状态, 耗时}
认证与个人字段 = 遮蔽或直接省略
输出允许字段,而非完整请求配置gogogold 的重点是“把供应商游戏接进来”:大厅找游戏、加载 iframe/Unity、处理连接与退出,再同步 GC/SC 余额。已捕获的 Stake 内建游戏则把单局交互也放在前端管理:输入、发请求、展示结果、自动继续和余额事件都有独立阶段。
因此,共享弹窗、表单、旧请求保护和加载安排最适合直接参考。Dice 自动策略和事件账本属于另一层业务,不能仅凭代码更复杂就判定更适合你的项目。
仅比较双方可对应的8类做法。点模块名看依据。
| 关键问题 | gogogold | Stake | 对项目有什么用 |
|---|---|---|---|
| 弹窗怎么打开 | — 重弹窗打开时才下载,下载过的内容会复用。 自动奖励弹窗按队列依次显示。 | — 网址决定当前打开哪个弹窗。 内容按需加载,关闭和权限检查集中处理。 | 两边都已具备统一弹窗机制。gogogold 可补“刷新或返回时恢复入口”,继续使用现有懒加载工厂。 |
| 旧请求会不会覆盖新页面 | — KYC 已检查响应是否属于当前账号。 首页切账号后的回写还需要补同类检查。 | — 检查游戏信息时,每次请求都会带一个编号。 只接收最新编号的结果,晚到的旧结果不更新页面。 | 优先核验 gogogold 首页。可把已有 KYC 的保护思路复用过来,防止旧账号数据晚到后覆盖新账号。 |
| 表单怎么显示错误 | — 已校验手机、邮箱、密码和验证码。 失焦与提交会分别维护错误信息。 | — 输入、失焦、错误和提交都有统一字段状态。 提交先检查所有字段,再决定是否执行。 | gogogold 的业务规则可以保留。把“何时校验、如何清错、如何定位首个错误”提为共同机制。 |
| 游戏列表怎么加载 | — 分区接近屏幕时才挂载。 游戏很多时只渲染可见行,已有图片占位。 | — 最近玩过、热门、推荐和原创游戏按组组织。 推荐保留版本与展示批次,便于跟踪。 | gogogold 的列表加载已有优势。可以补统一的“加载中/为空/失败”显示;是否加推荐批次取决于业务。 |
| 进入和离开游戏 | — 主要负责 iframe/Unity 的下载、连接和退出。 已有超时、迟到游戏编号检查和资源销毁。 | — 游戏壳负责游戏信息、布局和可用状态。 内建 Dice 自己控制请求、结果展示和自动模式。 | 比较加载与退出流程最有价值。gogogold 可统一阶段状态;Dice 的下注状态机不能直接套在供应商容器上。 |
| 余额怎么更新 | — 使用服务端返回的 GC/SC 余额快照。 从游戏回大厅时触发刷新。 | — 用本局编号记录响应、余额推送和游戏结束。 失败时回滚该局记录,再与真实余额对齐。 | 这是两种业务方案。先完善 gogogold 的刷新状态和账号保护;需要本地下注交互时,再考虑事件账本。 |
| 界面样式怎么统一 | — 有 GC/SC 主题、移动布局和比例缩放。 组件样式主要通过 CSS Modules 管理。 | — 颜色、间距和圆角使用统一的语义名称。 侧栏和移动底栏按屏幕条件切换。 | 保留 gogogold 的移动适配。新增组件可逐步使用“背景/文字/边框/警告”等统一变量。 |
| 刷新后记住界面习惯 | — 已保存搜索历史、最近游戏和收藏相关状态。 选择保存哪些字段,由各 store 决定。 | — 会记住侧栏开关和显示币种。 手机上打开一侧时,会收起另一侧,避免挤屏。 | 两边都已有保存偏好的能力。重点检查哪些数据能跨账号保留,哪些在刷新时应回到默认值。 |
已有能力继续复用,先核验具体边界。
这部分列做法和证据范围,不追加未经验证的安全或速度评分。
按项目适用性排序;是分析建议,不表示已经修改或运行验证项目。
| 建议顺序 | 事项 | 具体做法 | 完成后核验什么 |
|---|---|---|---|
| 优先核验 | 首页旧账号结果保护 | 复用已有 KYC 判断,在响应写入与建立后续定时器前再次检查当前账号。 | 切账号时旧数据不回写,旧请求不继续建立刷新任务。 查看对应源码与对照 → |
| 逐步统一 | 表单字段生命周期 | 共用输入、失焦、清错、提交规则;保留业务校验条件。 | 同类字段反馈一致,重复规则减少。 查看对应源码与对照 → |
| 按入口选择 | 网址可恢复弹窗 | 登录、活动等可分享入口使用网址状态;自动奖励继续使用队列。 | 刷新/后退能恢复指定入口,奖励显示顺序仍稳定。 查看对应源码与对照 → |
| 保留并完善 | 现有列表与游戏加载 | 沿用虚拟化、懒挂载和容器清理,统一加载/为空/失败提示。 | 错误有恢复入口,退出后监听与资源按现有边界释放。 查看对应源码与对照 → |
| 按业务决定 | 余额事件账本 | 先完善快照刷新与账号保护;只有本地控制单局交互时再评估账本。 | 先确认多来源余额协调问题,再选择相应机制。 查看对应源码与对照 → |
| 渐进整理 | 语义样式变量 | 新组件逐步使用背景、正文、边框、警告等变量,保留现有移动适配。 | 同类组件颜色与状态表达一致。 查看对应源码与对照 → |
紫色始终是gogogold;深色绿色始终是Stake。每项最后给一个“怎么选”。
两边都已具备统一弹窗机制。gogogold 可补“刷新或返回时恢复入口”,继续使用现有懒加载工厂。
优先核验 gogogold 首页。可把已有 KYC 的保护思路复用过来,防止旧账号数据晚到后覆盖新账号。
gogogold 的业务规则可以保留。把“何时校验、如何清错、如何定位首个错误”提为共同机制。
gogogold 的列表加载已有优势。可以补统一的“加载中/为空/失败”显示;是否加推荐批次取决于业务。
比较加载与退出流程最有价值。gogogold 可统一阶段状态;Dice 的下注状态机不能直接套在供应商容器上。
这是两种业务方案。先完善 gogogold 的刷新状态和账号保护;需要本地下注交互时,再考虑事件账本。
保留 gogogold 的移动适配。新增组件可逐步使用“背景/文字/边框/警告”等统一变量。
两边都已有保存偏好的能力。重点检查哪些数据能跨账号保留,哪些在刷新时应回到默认值。
10项优点与8项局限:先讲用户能感受到什么,再展开依据。
游戏信息与页面布局放在共享壳里,具体游戏只管自己的交互。
一局请求、结果展示、继续或停止被拆成不同状态。
同一局的响应、推送和结束事件用编号关联,失败可回滚。
语义样式变量统一,JS/CSS依赖按需加载并去重。
游戏信息检查用编号识别最新请求,旧结果不再发布。可借鉴这种具体保护,不应推断全站都有。
多个独立数据同时请求;其中一个失败时,其他结果仍可返回,页面有默认数据。
网址保存当前弹窗及参数,权限条件和关闭处理集中管理。适合登录、设置等可恢复入口。
字段共享输入、失焦与错误状态,提交前统一校验,减少页面各自维护。
先等初始消息就绪,再接增量推送并去重;发送失败可以回填输入。
弹窗、图标和可见区块分别延迟装载,资源预载会去重。减少不必要的首次执行是其目的。
具名Map缓存没有自动TTL,公共查询没有自动重试。
Dice结果为0的反馈存在真值判断边界,需运行核验。
独立请求失败会变成空结果。页面能继续,但用户可能难以区分没有内容与加载失败。
多个规则块能累积调整金额,但部分游戏状态以原输入重复计算;复杂组合应做定向运行检查。
进入游戏时可能收起左栏或打开推荐栏。适合给游戏腾空间,也增加了自动调整与用户意图的协调成本。
共享包包含不同产品开关与分支,复用更多代码,同时增加配置组合与回归检查。
第一次打开弹窗仍可能等待模块下载,失败也需要专门恢复;异步载入的过期保护未完全确认。
并行减少串行等待,但整批Promise仍等全部完成;不能据此认定首屏会逐块立即显示。
点节点看作用与代码依据;图中箭头是源码重建的前端关系,不表示已经跑完所有用户流程。
首页、找游戏、身份入口与游戏页怎么连接。灰色节点只恢复到入口。
这里只画已确认的前端控制流程。开奖结果由服务端返回,本轮没有执行下注。
能重建前端门控、弹窗、表单和OAuth分支;身份验证与登录后的完整返回仍有缺口。
历史消息、实时消息与发送恢复分开管理;本轮仅读代码,没有发送消息。
真实交易币、显示方式和余额来源分开;换币还会影响自动游戏状态。
选择图中节点,查看它在流程里负责什么。
9项直接对照:双方已经做了什么、差别在哪、项目该补什么。
同一场景左右对照,沿箭头看保护发生在哪一步,再看各自局限。
页面提交请求与配置
isToken启用时读取凭证并写入Authorization
这条拦截路径未见同位置的中止检查
实际权限校验属于服务器,本次未验证
已有保护凭证处理已集中,页面不必各自拼请求头。
局限与边界需登录请求的早期拦截可进一步核验;游客请求不能一律阻止。
选择普通查询或显式认证查询
加入访问凭证与语言信息
缺认证头:中止;存在:继续发送
实际权限校验属于服务器,本次未验证
已有保护显式认证入口能在缺凭证时提前中止。
局限与边界只证明这个入口;不能推断全站查询都会检查或服务器授权完整。
保留gogogold共享请求层,在需要登录的入口核验早期检查。双方都不能靠前端门控代替服务器授权。
生成随机state,保存在当前标签会话
将state交给Apple SDK
不匹配:抛错;匹配:删除临时state
提交登录处理;服务器兑换与校验未验证
已有保护前端能检查返回是否携带本次Apple授权的state。
局限与边界这里只确认Apple路径;其他提供商与服务器校验仍需分别核验。
由功能开关选择该分支
32字节随机值 → SHA-256 → URL安全编码
请求startOauthRedirect,取得授权网址并跳转
校验值的兑换、state绑定与过期规则尚未确认
已有保护前端已准备PKCE式随机挑战,临时校验值保存在标签会话。
局限与边界前端挑战不等于完整PKCE验证;回调与服务器验证链还未恢复。
state与PKCE式挑战作用不同,不能用其中一项替代另一项。Stake跳转失败还可走弹窗订阅分支;图中主线仅是Google跳转路径。
统一组件接收结构化数据
JSON.stringify → 小于号转为转义文本
这一输出位置已有边界保护
当前组件直接输出content,清洗调用被注释
已有保护JSON-LD已有明确转义,与Stake做法对应。
局限与边界公告和站内信等富文本应追踪信任来源与服务端清洗,不能只看组件函数名。
SEO适配器读取页面资料
JSON文本的小于号被转义
这一输出位置已有边界保护
未确认对应的完整清洗与输出路径
已有保护已确认JSON-LD输出转义。
局限与边界其他HTML输出证据不足,不能当作全站XSS防护优势。
上面前三步是JSON-LD主线,最后一项是独立富文本分支。gogogold优先核验富文本数据来源和实际清洗;双方JSON-LD已有的处理无需重做。
集中处理认证、协议与错误
加密前完整请求配置、响应结果交给日志工具
这条路径未见统一字段脱敏
实际启用状态、所有日志通道尚未核验
已有保护日志入口集中,便于统一补脱敏。
局限与边界开关仅控制是否记录,不会自动隐藏认证头、密码或验证码。
进入已确认的错误记录路径
保留操作与错误信息,隐藏变量值
变量值使用脱敏标记
错误文本、其他通道的脱敏范围未完整核验
已有保护这一类错误日志已隐藏查询变量值。
局限与边界不能从一个路径推出全站脱敏;错误消息本身也需要单独检查。
gogogold可优先建立共享字段脱敏;这属于明确代码核验点,尚未据此判定线上数据泄露。
下方9项对照用于定位具体实现。
已有共享拦截器,按配置添加Authorization。
统一添加登录凭证与语言信息。
两边都有共同请求入口。保留现有底座,核验公开请求与需登录请求的区分。
src/Fwk/Http/Interceptors/AuthInterceptor.ts · 第14行
export const AuthInterceptor: RequestInterceptor = async (config: any) => {
//参数默认会带上token
if (config.isToken) {
config.headers.Authorization = getToken();stake-us-source/chunks/Cu3B9wdT.mjs · 字符位置23291
格式化上下文 · 第1184行起;原始精确片段保留在证据JSON。打开带行号阅读版
function be({ request: e, session: t }) {
let n = {};
(t?.session && (n[`x-access-token`] = t.session), t && (n[`x-language`] = G(t.locale)));
let r = J(e, n);
return { ...e, headers: r };
}
var X = async ({ request: e, doc: t }) => {
let n = t?.definitions?.[0],
r = J(e, {
"x-operation-name": (`name` in n ? n.name?.value : void 0) || `unknown`,
"x-operation-type": `query`,
});
return { ...e, headers: r };
},
xe = class {
url;
websocketUrl;
socketEmitter;
logger;
defaultHeaders;
requestInterceptors;
graphQLErrorInterceptors;
networkErrorInterceptors;
cacheMap;
websocketClient = {};stake-us-source/chunks/DYosYWTC.mjs · 字符位置3365
格式化上下文 · 第23行起;原始精确片段保留在证据JSON。打开带行号阅读版
var O = () => ({ title: { id: `Ifq4hV` }, body: { id: `dDj4u0` }, icon: y, type: `warn` }),
k = (e, t) => {
let n = e - t;
return Math.abs((n / e) * 100);
},
A = ({ blockResults: e, balance: t, amount: n, gamesLeft: r, potentialStopLoss: i }) => {
if (e?.stop) return e.message ?? `conditionalBlockCalledStop`;
if (r === 1) return `limitReached`;
if (n > t) return `insufficientBalance`;
if (i) return `potentialStopLossReached`;
},
j = ({ blocks: e, id: t, value: n, blockDefault: r, target: i }) =>
e.some((e) => e.id === t)
? e.map((e) => {
let r = structuredClone(e);
return (e.id === t && (r[i].value = n), r);
})
: [...e, { ...r, [i]: { ...r[i], value: n } }],
M = (e) => {
if (!Array.isArray(e)) return [];
let t = (e) => typeof e == `object` && !!e,
n = (e) => typeof e.type == `string` && e.type !== ``,
r = new Set();
return e.reduce((e, i) => {
if (!t(i) || !t(i.on) || !t(i.do) || !n(i.on) || !n(i.do)) return e;这条拦截路径读取凭证并写入头部,未见同位置缺凭证就中止。
显式要求认证的查询,缺凭证会提前停止。
gogogold可在确实需要登录的请求入口补早期检查;不能把游客或公开请求一律拦掉。服务器仍须验证权限。
src/Fwk/Http/Interceptors/AuthInterceptor.ts · 第14行
export const AuthInterceptor: RequestInterceptor = async (config: any) => {
//参数默认会带上token
if (config.isToken) {
config.headers.Authorization = getToken();src/Fwk/Http/comRequest.ts · 第170行
// 主要流程:合并配置->请求拦截器->循环重试fetch->响应拦截器->错误处理
const {
url,
requestCount = defaultConfig.requestCount,
encryptionType = defaultConfig.encryptionType,
isToken = defaultConfig.isToken,
baseUrl = defaultConfig.baseUrl!,
method = defaultConfig.method!,
headers = {},
body = defaultConfig.body,
timeout = defaultConfig.timeout!,stake-us-source/chunks/Cu3B9wdT.mjs · 字符位置32639
语义命名上下文 · 非作者原名,原始片段保留在证据JSON。打开完整函数
function authenticatedQuery(queryOptions) {
let requireAuthenticationHeader = async ({ request: request, controller: controller }) => (
new Headers(request.headers).has(`x-access-token`) === !1 &&
controller.abort(`Request aborted: missing authentication token for authenticated query.`),
request
);
return getGraphQLClient().query({
...queryOptions,
requestInterceptors: [
...getGraphQLClient().getRequestInterceptors(),
requireAuthenticationHeader,
],
});
}
function je(e) {
return getGraphQLClient().fetchGraphQL(e);
}
function Me(e, t, n) {
return getGraphQLClient().mutation(e, t, n);
}
function Ne(e, t, n) {
return getGraphQLClient().mutate(e, t, n);
}
function Pe(e) {
getGraphQLClient().setUrl(e);stake-us-source/chunks/Cu3B9wdT.mjs · 字符位置25931
格式化上下文 · 第1190行起;原始精确片段保留在证据JSON。打开带行号阅读版
var X = async ({ request: e, doc: t }) => {
let n = t?.definitions?.[0],
r = J(e, {
"x-operation-name": (`name` in n ? n.name?.value : void 0) || `unknown`,
"x-operation-type": `query`,
});
return { ...e, headers: r };
},
xe = class {
url;
websocketUrl;
socketEmitter;
logger;
defaultHeaders;
requestInterceptors;
graphQLErrorInterceptors;
networkErrorInterceptors;
cacheMap;
websocketClient = {};
constructor(e) {
((this.url = e.url),
(this.socketEmitter = e.socketEmitter),
(this.logger = e.logger || console),
(this.defaultHeaders = { ...(e.defaultHeaders || W) }),
(this.cacheMap = new Map()),开启调试日志时,把加密前的完整请求配置交给日志工具;响应日志也接收完整结果。
已见的GraphQL错误日志会隐藏变量值。
优先统一脱敏认证头、密码和验证码。gogogold的日志开关不等于脱敏;目前只确认代码路径,未确认线上开关是否开启。
src/Fwk/Http/Interceptors/AuthInterceptor.ts · 第29行
if (Util.isLog) {
// 原:请求拦截器-加密前
LogUtil.info(comTag, '[Request] Before encryption', config.url, Util.DeepClone(config));
}src/Fwk/Http/Interceptors/LogResDataInterceptor.ts · 第26行
export const LogResDataInterceptor: ResDataInterceptor = async (
finalData: any,
config: HttpConfig
) => {
if (Util.isLog) {
// 原:响应拦截器-日志-finalData
LogUtil.info(comTag, '[Response] Received', config.url, finalData);
}
stake-us-source/chunks/Cu3B9wdT.mjs · 字符位置26479
格式化上下文 · 第1190行起;原始精确片段保留在证据JSON。打开带行号阅读版
var X = async ({ request: e, doc: t }) => {
let n = t?.definitions?.[0],
r = J(e, {
"x-operation-name": (`name` in n ? n.name?.value : void 0) || `unknown`,
"x-operation-type": `query`,
});
return { ...e, headers: r };
},
xe = class {
url;
websocketUrl;
socketEmitter;
logger;
defaultHeaders;
requestInterceptors;
graphQLErrorInterceptors;
networkErrorInterceptors;
cacheMap;
websocketClient = {};
constructor(e) {
((this.url = e.url),
(this.socketEmitter = e.socketEmitter),
(this.logger = e.logger || console),
(this.defaultHeaders = { ...(e.defaultHeaders || W) }),
(this.cacheMap = new Map()),在本次检查的src和Next配置中未找到nonce/CSP配置,部署层策略尚未知。
资源预载会继承页面已有nonce标记。
Stake多出可见的nonce兼容机制,但双方线上CSP头都未核验。先检查真实响应,再决定如何接入;未知不计为缺失。
next.config.ts · 第40行
reactStrictMode: true,
// ------------------- 新增关键配置 -------------------
transpilePackages: ['react-redux', '@reduxjs/toolkit'],
// ------------------- 兼容性优化 -------------------
compiler: {
styledComponents: true // 如果使用 styled-components 需开启
},
webpack: (config, { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack }) => {stake-us-source/chunks/uBIymjUX.mjs · 字符位置155
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
var e = function (e, t) {
return new URL(e, t).href;
},
t = {},
n = function (n, r, i) {
let a = Promise.resolve();
if (r && r.length > 0) {
let n = document.getElementsByTagName(`link`),
o = document.querySelector(`meta[property=csp-nonce]`),
s = o?.nonce || o?.getAttribute(`nonce`);
function c(e) {
return Promise.all(
e.map((e) =>
Promise.resolve(e).then(
(e) => ({ status: `fulfilled`, value: e }),
(e) => ({ status: `rejected`, reason: e }),
),
),
);
}
function l(e) {
return import.meta.resolve ? import.meta.resolve(e) : new URL(e, import.meta.url).href;
}
a = c(
rstake-us-source/chunks/uBIymjUX.mjs · 字符位置689
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
var e = function (e, t) {
return new URL(e, t).href;
},
t = {},
n = function (n, r, i) {
let a = Promise.resolve();
if (r && r.length > 0) {
let n = document.getElementsByTagName(`link`),
o = document.querySelector(`meta[property=csp-nonce]`),
s = o?.nonce || o?.getAttribute(`nonce`);
function c(e) {
return Promise.all(
e.map((e) =>
Promise.resolve(e).then(
(e) => ({ status: `fulfilled`, value: e }),
(e) => ({ status: `rejected`, reason: e }),
),
),
);
}
function l(e) {
return import.meta.resolve ? import.meta.resolve(e) : new URL(e, import.meta.url).href;
}
a = c(
rstake-us-source/chunks/Dh3-ySZv.mjs · 字符位置42094
格式化上下文 · 第1105行起;原始精确片段保留在证据JSON。打开带行号阅读版
import { t as M } from "./uBIymjUX.mjs";
import { I as N, u as ie, v as ae } from "./D4G09-Ja.mjs";
import { d as P } from "./B3xfQiSE.mjs";
import { t as F } from "./DiYNMAEe.mjs";
import "./Cu3B9wdT.mjs";
import "./U5cMxr-6.mjs";
import { t as oe } from "./BwOURxTH.mjs";
import { f as I } from "./yhZ8TxI1.mjs";
import { t as se } from "./C_ugM1im.mjs";
import { c as ce, l as L, n as R, r as le, s as ue, t as z, u as de } from "./CcUq4hRx.mjs";
import "./xihTtKlq.mjs";
import { t as B } from "./B3B76Gni.mjs";
import { n as V } from "./CHJosYmF.mjs";
import { a as fe } from "./CM5G2D07.mjs";
import { t as H } from "./WUNfd52e.mjs";
import { t as U } from "./9eOfpkDu.mjs";
import { t as pe } from "./CHcJ6lXX.mjs";
import { a as me } from "./DxpuMHIe.mjs";
import { t as he } from "./BFfxvZMa.mjs";
import "./rMb8lNRX.mjs";
import { t as W } from "./BnRO4wcp.mjs";
import { t as ge } from "./5thhBUIF.mjs";
import { t as _e } from "./C1RIH9pd.mjs";
import { p as ve } from "./DtvVV7G6.mjs";
import { t as G } from "./CGL0rmXg.mjs";Apple登录生成随机state,保存在当前标签会话;返回时比较并删除。
Google跳转分支生成随机校验值和SHA-256挑战,保存临时校验值。
两边各有可见保护,但state与PKCE式挑战解决的问题不同,不能互相替代。gogogold是否需要PKCE,应按具体提供商和后端兑换合同核验。
src/app/hooks/useSocialLogin.ts · 第204行
// 生成新的 state(防止 CSRF)
const state = crypto.randomUUID();
sessionStorage.setItem('apple_state', state);
window.AppleID.auth.init({
clientId: appleConfig.current?.clientId,
scope: appleConfig.current?.scope,
redirectURI: appleConfig.current?.redirectURI,
state: state,
usePopup: true
});src/app/hooks/useSocialLogin.ts · 第620行
// 校验 state
const storedState = sessionStorage.getItem('apple_state');
if (storedState !== result.authorization.state) {
throw new Error('Invalid Apple state');
}
sessionStorage.removeItem('apple_state');stake-us-source/chunks/CHukuwd3.mjs · 字符位置160
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
var e = `oauthRedirectVerifierV2`,
t = (e) => {
let t = ``;
for (let n of e) t += String.fromCharCode(n);
return btoa(t).replace(/\+/g, `-`).replace(/\//g, `_`).replace(/=+$/, ``);
},
n = () => {
let e = new Uint8Array(32);
return (crypto.getRandomValues(e), t(e));
},
r = async (e) => {
let n = await crypto.subtle.digest(`SHA-256`, new TextEncoder().encode(e));
return t(new Uint8Array(n));
},
i = (t) => {
window.sessionStorage.setItem(e, t);
},
a = () => window.sessionStorage.getItem(e),
o = () => window.sessionStorage.removeItem(e),
s = (e) => window.location.assign(e);
export { s as a, a as i, r as n, i as o, n as r, o as t };stake-us-source/chunks/DT98r2cN.mjs · 字符位置17128
格式化上下文 · 第666行起;原始精确片段保留在证据JSON。打开带行号阅读版
function L(v, w) {
let E = ne();
ae(w, !0);
let D = () => S(A, `$openOauthSubscriptions`, F),
P = () => S(de, `$i18n`, F),
[F, L] = l(),
R = _(w, `onlyShowDefaultOauth`, 7, !1),
Ne = _(w, `showProviderToggle`, 3, !1),
z = _(w, `primaryProvider`, 3, void 0),
Pe = _(w, `buttonText`, 3, `signInWith`),
B = _(w, `useLargeButtons`, 3, !1),
V = _(w, `useMediumButtons`, 3, !1),
H = _(w, `size`, 3, void 0),
Fe = _(w, `showProvider`, 3, !1),
Ie = _(w, `stretch`, 3, !1),
Le = _(w, `oauthProviders`, 3, void 0),
Re = _(w, `afterDefaultProvider`, 3, void 0),
ze = _(w, `disabled`, 3, !1),
Be = _(w, `id`, 3, E),
U = m(() => (Le()?.length ? Le() : ke)),
W,
G,
K,
q = t(!1),
J = !1,预加载定时器、页面恢复监听与一组返回检查定时器有卸载清理。
弹窗关闭或组件销毁时,清理窗口检查、订阅和加载状态。
两边都有清理路径。gogogold还应逐条核验延迟回调和晚到授权结果;不能据局部清理推断整个登录流程都已覆盖。
src/app/hooks/useSocialLogin.ts · 第220行
useEffect(() => {
if (!preloadApple) return;
const timer = setTimeout(() => {
initAppleSDK().catch((error) => logger.error('Apple SDK init failed', error));
}, 500);
return () => clearTimeout(timer);
}, [preloadApple]);src/app/hooks/useSocialLogin.ts · 第262行
window.addEventListener('pageshow', handlePageShow);
// 初始加载时如果是后退导航,也检查
if (navEntry?.type === 'back_forward') {
timers.push(checkBackNavigation());
}
return () => {
window.removeEventListener('pageshow', handlePageShow);
timers.forEach((timer) => clearTimeout(timer));
};
}, [pathname, isProcessingAuth]);stake-us-source/chunks/DT98r2cN.mjs · 字符位置15642
格式化上下文 · 第666行起;原始精确片段保留在证据JSON。打开带行号阅读版
function L(v, w) {
let E = ne();
ae(w, !0);
let D = () => S(A, `$openOauthSubscriptions`, F),
P = () => S(de, `$i18n`, F),
[F, L] = l(),
R = _(w, `onlyShowDefaultOauth`, 7, !1),
Ne = _(w, `showProviderToggle`, 3, !1),
z = _(w, `primaryProvider`, 3, void 0),
Pe = _(w, `buttonText`, 3, `signInWith`),
B = _(w, `useLargeButtons`, 3, !1),
V = _(w, `useMediumButtons`, 3, !1),
H = _(w, `size`, 3, void 0),
Fe = _(w, `showProvider`, 3, !1),
Ie = _(w, `stretch`, 3, !1),
Le = _(w, `oauthProviders`, 3, void 0),
Re = _(w, `afterDefaultProvider`, 3, void 0),
ze = _(w, `disabled`, 3, !1),
Be = _(w, `id`, 3, E),
U = m(() => (Le()?.length ? Le() : ke)),
W,
G,
K,
q = t(!1),
J = !1,stake-us-source/chunks/DT98r2cN.mjs · 字符位置16116
格式化上下文 · 第666行起;原始精确片段保留在证据JSON。打开带行号阅读版
function L(v, w) {
let E = ne();
ae(w, !0);
let D = () => S(A, `$openOauthSubscriptions`, F),
P = () => S(de, `$i18n`, F),
[F, L] = l(),
R = _(w, `onlyShowDefaultOauth`, 7, !1),
Ne = _(w, `showProviderToggle`, 3, !1),
z = _(w, `primaryProvider`, 3, void 0),
Pe = _(w, `buttonText`, 3, `signInWith`),
B = _(w, `useLargeButtons`, 3, !1),
V = _(w, `useMediumButtons`, 3, !1),
H = _(w, `size`, 3, void 0),
Fe = _(w, `showProvider`, 3, !1),
Ie = _(w, `stretch`, 3, !1),
Le = _(w, `oauthProviders`, 3, void 0),
Re = _(w, `afterDefaultProvider`, 3, void 0),
ze = _(w, `disabled`, 3, !1),
Be = _(w, `id`, 3, E),
U = m(() => (Le()?.length ? Le() : ke)),
W,
G,
K,
q = t(!1),
J = !1,stake-us-source/chunks/DT98r2cN.mjs · 字符位置16941
格式化上下文 · 第666行起;原始精确片段保留在证据JSON。打开带行号阅读版
function L(v, w) {
let E = ne();
ae(w, !0);
let D = () => S(A, `$openOauthSubscriptions`, F),
P = () => S(de, `$i18n`, F),
[F, L] = l(),
R = _(w, `onlyShowDefaultOauth`, 7, !1),
Ne = _(w, `showProviderToggle`, 3, !1),
z = _(w, `primaryProvider`, 3, void 0),
Pe = _(w, `buttonText`, 3, `signInWith`),
B = _(w, `useLargeButtons`, 3, !1),
V = _(w, `useMediumButtons`, 3, !1),
H = _(w, `size`, 3, void 0),
Fe = _(w, `showProvider`, 3, !1),
Ie = _(w, `stretch`, 3, !1),
Le = _(w, `oauthProviders`, 3, void 0),
Re = _(w, `afterDefaultProvider`, 3, void 0),
ze = _(w, `disabled`, 3, !1),
Be = _(w, `id`, 3, E),
U = m(() => (Le()?.length ? Le() : ke)),
W,
G,
K,
q = t(!1),
J = !1,stake-us-source/chunks/C07WLNJL.mjs · 字符位置0
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
var e = ({ windowName: e = `Connect With OAuth`, url: t }) => {
let n = screen.width / 2 - 300,
r = `location=0, status=0, width=600, height=600, top=${screen.height / 2 - 300}, left=${n}`;
return window.open(t, e, r);
};
export { e as t };JSON-LD共享组件将小于号转义后写入脚本。
JSON-LD输出同样转义小于号。
这一点双方已有对应处理,Pwa无需重做。保护范围限于JSON-LD;其他HTML输出应另查。
src/app/_components/JsonLd.tsx · 第11行
export default function JsonLd({ data }: JsonLdProps) {
if (!data) return null;
return (
<script
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(data).replace(/</g, '\\u003c'),
}}
/>
);stake-us-source/chunks/ysfTt0iE.mjs · 字符位置362
格式化上下文 · 第3行起;原始精确片段保留在证据JSON。打开带行号阅读版
function r(n) {
let r = {
...n,
robots: n?.robots ?? void 0,
canonical: n?.canonical ?? void 0,
sanitizedJsonLd: ``,
openGraph: {
title: n?.title ?? void 0,
description: n?.description ?? void 0,
images: n?.thumbnail?.asset?.url
? [{ url: `${n?.thumbnail?.asset?.url}`, alt: `${n?.thumbnailAlt || n?.title} thumbnail` }]
: [],
},
};
if (r?.jsonLd) {
let n;
try {
((n = t(r.jsonLd)), (r.sanitizedJsonLd = a(e(n))));
} catch (e) {
console.error(e);
}
}
return r;
}
var i = () => typeof window < `u` && !!n(`session`);stake-us-source/chunks/ysfTt0iE.mjs · 字符位置506
格式化上下文 · 第28行起;原始精确片段保留在证据JSON。打开带行号阅读版
function a(e) {
return `<script type="application/ld+json">${String(e).replace(/</g, `\\u003c`)}<\/script>`;
}
export { r as n, a as r, i as t };stake-us-source/chunks/1dM5wbsU.mjs · 字符位置2081
格式化上下文 · 第63行起;原始精确片段保留在证据JSON。打开带行号阅读版
function L(p, g) {
b(g, !0);
let x = () => y(T, `$page`, R),
L = () => y(E, `$i18n`, R),
[R, z] = a(),
B = f(g, `title`, 3, ``),
V = f(g, `description`, 3, ``),
De = f(g, `noindex`, 3, !1),
Oe = f(g, `nofollow`, 3, !1),
H = f(g, `openGraph`, 19, () => ({ title: B(), description: V() })),
U = f(g, `twitter`, 3, void 0),
ke = f(g, `hreflangList`, 19, () => []),
W = f(g, `sanitizedJsonLd`, 3, void 0),
G = f(g, `generatedJsonLd`, 3, void 0),
Ae = f(g, `robots`, 3, void 0),
K = f(g, `canonical`, 3, void 0),
je = u(() => j(x().params.lang)),
q = A() === `default-language-only`,
Me = M(),
J = u(() =>
k({ allowIndexing: !!C, lang: x().params.lang, robots: Ae(), noindex: De(), nofollow: Oe() }),
),
Y = u(() => K() ?? `${w.canonicalURL.slice(0, -1)}${x().url.pathname}`),
Ne = u(() => !v(J).includes(`noindex`)),
X = u(() => (v(Y) ? O(v(Y), x().params.lang) : void 0)),有清洗函数,但当前调用被注释,渲染直接使用content;公告、公共弹窗和站内信调用此组件。
已确认JSON-LD转义,未确认等价的富文本清洗路径。
gogogold优先追踪这些内容的信任来源和服务端清洗;按需要恢复受控清洗。Stake这一项留未知,不当作领先证据。
src/app/_components/RichText/RichText.tsx · 第199行
const RichTextRenderer: React.FC<RichTextRendererProps> = ({
content,
className = '',
sanitizeConfig = defaultConfig,
}) => {
// 清理HTML内容
// const cleanContent = sanitizeHtml(content, sanitizeConfig);
// 创建安全的HTML对象
const safeHtml = { __html: content };
return <div className={styles[className]} dangerouslySetInnerHTML={safeHtml} />;src/app/_dialogs/GameAnnouncementDialog/GameAnnouncementDialog.tsx · 第182行
<div className={styles.head}>
<div className={styles.title}>{title}</div>
<div className={styles.date}>{Util.timeStampToDateFormat(date)}</div>
</div>
<RichText content={content} className='wrap' />
</div>
{rewardItems && (getRewardSC(rewardItems) > 0 || getRewardGC(rewardItems) > 0) && (src/app/(pages)/components/InBoxPopup/Inbox/InfoDetailBox.tsx · 第148行
{!loading && (
<section
className={`${styles.gift_content} ${mailData.messageType === 1 ? styles.gift_msg_content : ''}`}
>
<RichText content={mailData.message} className='wrap' />
</section>
)}stake-us-source/chunks/ysfTt0iE.mjs · 字符位置362
格式化上下文 · 第3行起;原始精确片段保留在证据JSON。打开带行号阅读版
function r(n) {
let r = {
...n,
robots: n?.robots ?? void 0,
canonical: n?.canonical ?? void 0,
sanitizedJsonLd: ``,
openGraph: {
title: n?.title ?? void 0,
description: n?.description ?? void 0,
images: n?.thumbnail?.asset?.url
? [{ url: `${n?.thumbnail?.asset?.url}`, alt: `${n?.thumbnailAlt || n?.title} thumbnail` }]
: [],
},
};
if (r?.jsonLd) {
let n;
try {
((n = t(r.jsonLd)), (r.sanitizedJsonLd = a(e(n))));
} catch (e) {
console.error(e);
}
}
return r;
}
var i = () => typeof window < `u` && !!n(`session`);stake-us-source/chunks/ysfTt0iE.mjs · 字符位置506
格式化上下文 · 第28行起;原始精确片段保留在证据JSON。打开带行号阅读版
function a(e) {
return `<script type="application/ld+json">${String(e).replace(/</g, `\\u003c`)}<\/script>`;
}
export { r as n, a as r, i as t };stake-us-source/chunks/1dM5wbsU.mjs · 字符位置2081
格式化上下文 · 第63行起;原始精确片段保留在证据JSON。打开带行号阅读版
function L(p, g) {
b(g, !0);
let x = () => y(T, `$page`, R),
L = () => y(E, `$i18n`, R),
[R, z] = a(),
B = f(g, `title`, 3, ``),
V = f(g, `description`, 3, ``),
De = f(g, `noindex`, 3, !1),
Oe = f(g, `nofollow`, 3, !1),
H = f(g, `openGraph`, 19, () => ({ title: B(), description: V() })),
U = f(g, `twitter`, 3, void 0),
ke = f(g, `hreflangList`, 19, () => []),
W = f(g, `sanitizedJsonLd`, 3, void 0),
G = f(g, `generatedJsonLd`, 3, void 0),
Ae = f(g, `robots`, 3, void 0),
K = f(g, `canonical`, 3, void 0),
je = u(() => j(x().params.lang)),
q = A() === `default-language-only`,
Me = M(),
J = u(() =>
k({ allowIndexing: !!C, lang: x().params.lang, robots: Ae(), noindex: De(), nofollow: Oe() }),
),
Y = u(() => K() ?? `${w.canonicalURL.slice(0, -1)}${x().url.pathname}`),
Ne = u(() => !v(J).includes(`noindex`)),
X = u(() => (v(Y) ? O(v(Y), x().params.lang) : void 0)),游戏消息先检查来源origin,再按game_类型分发;退出时移除监听。
已见OAuth结果通过服务端订阅接收,不能拿它证明postMessage来源校验。
gogogold已有来源检查。可继续核验event.source是否绑定当前游戏窗口,以及每类消息的数据结构;两条通道不同,不评谁更安全。
src/app/hooks/useGameMessage.tsx · 第20行
const handleMessage = (event: MessageEvent) => {
if (Util.isLog) {
LogUtil.info(GameMsgUtil.tag1, 'useGameMessage event.origin', event.origin);
LogUtil.info(GameMsgUtil.tag1, 'useGameMessage allowedOrigin', allowedOrigin);
}
if (event.origin !== allowedOrigin) return;
const data: any = event.data;
if (Util.isLog) {
LogUtil.info(GameMsgUtil.tag1, 'useGameMessage', data);
}
if (data.type && data.type.startsWith('game_')) {
handlerRef.current(data);
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, [allowedOrigin]);stake-us-source/chunks/DT98r2cN.mjs · 字符位置15642
格式化上下文 · 第666行起;原始精确片段保留在证据JSON。打开带行号阅读版
function L(v, w) {
let E = ne();
ae(w, !0);
let D = () => S(A, `$openOauthSubscriptions`, F),
P = () => S(de, `$i18n`, F),
[F, L] = l(),
R = _(w, `onlyShowDefaultOauth`, 7, !1),
Ne = _(w, `showProviderToggle`, 3, !1),
z = _(w, `primaryProvider`, 3, void 0),
Pe = _(w, `buttonText`, 3, `signInWith`),
B = _(w, `useLargeButtons`, 3, !1),
V = _(w, `useMediumButtons`, 3, !1),
H = _(w, `size`, 3, void 0),
Fe = _(w, `showProvider`, 3, !1),
Ie = _(w, `stretch`, 3, !1),
Le = _(w, `oauthProviders`, 3, void 0),
Re = _(w, `afterDefaultProvider`, 3, void 0),
ze = _(w, `disabled`, 3, !1),
Be = _(w, `id`, 3, E),
U = m(() => (Le()?.length ? Le() : ke)),
W,
G,
K,
q = t(!1),
J = !1,stake-us-source/chunks/DT98r2cN.mjs · 字符位置16116
格式化上下文 · 第666行起;原始精确片段保留在证据JSON。打开带行号阅读版
function L(v, w) {
let E = ne();
ae(w, !0);
let D = () => S(A, `$openOauthSubscriptions`, F),
P = () => S(de, `$i18n`, F),
[F, L] = l(),
R = _(w, `onlyShowDefaultOauth`, 7, !1),
Ne = _(w, `showProviderToggle`, 3, !1),
z = _(w, `primaryProvider`, 3, void 0),
Pe = _(w, `buttonText`, 3, `signInWith`),
B = _(w, `useLargeButtons`, 3, !1),
V = _(w, `useMediumButtons`, 3, !1),
H = _(w, `size`, 3, void 0),
Fe = _(w, `showProvider`, 3, !1),
Ie = _(w, `stretch`, 3, !1),
Le = _(w, `oauthProviders`, 3, void 0),
Re = _(w, `afterDefaultProvider`, 3, void 0),
ze = _(w, `disabled`, 3, !1),
Be = _(w, `id`, 3, E),
U = m(() => (Le()?.length ? Le() : ke)),
W,
G,
K,
q = t(!1),
J = !1,stake-us-source/chunks/DT98r2cN.mjs · 字符位置16941
格式化上下文 · 第666行起;原始精确片段保留在证据JSON。打开带行号阅读版
function L(v, w) {
let E = ne();
ae(w, !0);
let D = () => S(A, `$openOauthSubscriptions`, F),
P = () => S(de, `$i18n`, F),
[F, L] = l(),
R = _(w, `onlyShowDefaultOauth`, 7, !1),
Ne = _(w, `showProviderToggle`, 3, !1),
z = _(w, `primaryProvider`, 3, void 0),
Pe = _(w, `buttonText`, 3, `signInWith`),
B = _(w, `useLargeButtons`, 3, !1),
V = _(w, `useMediumButtons`, 3, !1),
H = _(w, `size`, 3, void 0),
Fe = _(w, `showProvider`, 3, !1),
Ie = _(w, `stretch`, 3, !1),
Le = _(w, `oauthProviders`, 3, void 0),
Re = _(w, `afterDefaultProvider`, 3, void 0),
ze = _(w, `disabled`, 3, !1),
Be = _(w, `id`, 3, E),
U = m(() => (Le()?.length ? Le() : ke)),
W,
G,
K,
q = t(!1),
J = !1,stake-us-source/chunks/C07WLNJL.mjs · 字符位置0
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
var e = ({ windowName: e = `Connect With OAuth`, url: t }) => {
let n = screen.width / 2 - 300,
r = `location=0, status=0, width=600, height=600, top=${screen.height / 2 - 300}, left=${n}`;
return window.open(t, e, r);
};
export { e as t };10项左右对照:资源何时下载、内容何时挂载、哪些等待被延后。
首页活动条与Landing内容动态导入,构建保留Next默认拆包,再单独分组较大的SDK。
按路由动态导入布局和页面,预载其依赖,已经准备过的资源不重复添加。
双方都有拆包。gogogold继续沿用Next机制;要看实际请求瀑布才能决定是否增加预载,过多预载也会抢带宽。
src/app/(pages)/home/page.tsx · 第52行
import useAfterLCP from '@/app/hooks/useAfterLCP';
// 活动条拆成异步 chunk;仅在 LCP 后挂载,避免拖慢首屏 JS 解析(非点击加载)
const HomePopupsList = dynamic(
() => import('@/app/(pages)/components/HomePopupsList/HomePopupsList'),
{ ssr: false },
);
/** 落地页仅 isShowLadingGuide 时渲染;dynamic 避免登录用户首页背 Landing CSS */
const LadingPage = dynamic(() => import('@/app/(pages)/landingPage/landingPage'), {
ssr: false,
loading: () => null,
});
next.config.ts · 第178行
// ======== Next.js 代码分割优化 ========
// 保留 Next.js 默认拆包策略,只补充项目中体积较大的第三方库分组。
// 避免把业务 Core/Fwk/Components 强制抽成全局 chunk,导致首屏和普通路由加载不相关代码。
if (!dev && !isServer) {
const defaultSplitChunks = config.optimization.splitChunks || {};
const defaultCacheGroups = defaultSplitChunks.cacheGroups || {};
config.optimization.splitChunks = {
...defaultSplitChunks,
chunks: 'all',
minSize: 30000,
maxAsyncRequests: 30,
maxInitialRequests: 25,
automaticNameDelimiter: '.',
cacheGroups: {
...defaultCacheGroups,
sentry: {
name: 'sentry',
// 排除 @sentry/replay:与 instrumentation-client 动态 import 配合,避免 Replay 并入首屏 sentry chunk
test: /[\\/]node_modules[\\/]@sentry[\\/](?!replay(?:[\\/]|$))/,
priority: 60,
reuseExistingChunk: true,
enforce: true
},
// Replay 单独异步 chunk,仅采样命中 / 错误录屏路径加载
sentryReplay: {
name: 'sentry-replay',
test: /[\\/]node_modules[\\/]@sentry[\\/]replay(?:[\\/]|$)/,
chunks: 'async',
priority: 65,
reuseExistingChunk: true,
enforce: true
},
firebase: {
name: 'firebase',
test: /[\\/]node_modules[\\/](@firebase|firebase)[\\/]/,
priority: 55,PERF01 · stake-us-source/entry/app.Q5XUkswp.mjs · 字符位置92973
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
const __vite__mapDeps = (
i,
m = __vite__mapDeps,
d = m.f ||
(m.f = [
"/_app/immutable/chunks/CM5G2D07.mjs",
"/_app/immutable/chunks/C0FnF6B9.mjs",
"/_app/immutable/chunks/CYbi0xxB.mjs",
"/_app/immutable/chunks/BGNdxI84.mjs",
"/_app/immutable/chunks/Dt-HX3Vu.mjs",
"/_app/immutable/chunks/D5F2HdYq.mjs",
"/_app/immutable/chunks/D4G09-Ja.mjs",
"/_app/immutable/chunks/DiYNMAEe.mjs",
"/_app/immutable/chunks/B3xfQiSE.mjs",
"/_app/immutable/chunks/CPdaYJXM.mjs",
"/_app/immutable/chunks/DoD3rfum.mjs",
"/_app/immutable/nodes/0.CR0Lrtr3.mjs",
"/_app/immutable/chunks/uBIymjUX.mjs",
"/_app/immutable/chunks/Cu3B9wdT.mjs",
"/_app/immutable/chunks/U5cMxr-6.mjs",
"/_app/immutable/chunks/CEO7yYTd.mjs",
"/_app/immutable/chunks/BwOURxTH.mjs",
"/_app/immutable/chunks/CjxnTdcP.mjs",
"/_app/immutable/chunks/DdanaPe6.mjs",
"/_app/immutable/chunks/D35lYozi.mjs",PERF02 · stake-us-source/chunks/uBIymjUX.mjs · 字符位置511
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
var e = function (e, t) {
return new URL(e, t).href;
},
t = {},
n = function (n, r, i) {
let a = Promise.resolve();
if (r && r.length > 0) {
let n = document.getElementsByTagName(`link`),
o = document.querySelector(`meta[property=csp-nonce]`),
s = o?.nonce || o?.getAttribute(`nonce`);
function c(e) {
return Promise.all(
e.map((e) =>
Promise.resolve(e).then(
(e) => ({ status: `fulfilled`, value: e }),
(e) => ({ status: `rejected`, reason: e }),
),
),
);
}
function l(e) {
return import.meta.resolve ? import.meta.resolve(e) : new URL(e, import.meta.url).href;
}
a = c(
r推荐和基础首页接口并发;Live数据延后约1.5秒请求。
首页最多11类数据并行;关闭的产品功能跳过对应请求。
两边都已并行。接口数量不同不代表谁更快;双方关键批次都可能等最慢项,需核验哪些数据真正阻塞首屏。
src/app/(pages)/home/page.tsx · 第198行
// 拉取首页数据:recommend + base_config 并发;Live 短延后
useEffect(() => {
let liveTimer: ReturnType<typeof setTimeout> | undefined;
const getGameHomeData = async () => {
try {
// 关键接口并发,互不依赖
const [recommendResponse, oldResponse] = await Promise.all([
$http.Account.getUserMachineRecommend().catch((error) => {
console.error('getUserMachineRecommend error:', error);
return null;
}),
$http.Account.GameHomeData().catch((oldError) => {
console.error('getGameHomeData (old interface) error:', oldError);
return null;
}),
]);
src/app/(pages)/home/page.tsx · 第226行
// Live 延后,避免与首屏关键接口抢带宽
liveTimer = setTimeout(() => fetchLiveGameData(partitions), 1500);
}
// 设置收藏游戏列表(从接口返回,会自动生成GC和SC两份数据)
if (Array.isArray(favoriteMachineList)) {
useGameCategoryStore.getState().setFavoriteMachineList(favoriteMachineList);PERF03 · stake-us-source/nodes/55.BHHpgWjN.mjs · 字符位置16887
格式化上下文 · 第304行起;原始精确片段保留在证据JSON。打开带行号阅读版
var gt = {
kind: `Document`,
definitions: [
{
kind: `OperationDefinition`,
operation: `query`,
name: { kind: `Name`, value: `GetPlayerCountByScope` },
selectionSet: {
kind: `SelectionSet`,
selections: [
{
kind: `Field`,
name: { kind: `Name`, value: `playerCountByScope` },
selectionSet: {
kind: `SelectionSet`,
selections: [
{ kind: `Field`, name: { kind: `Name`, value: `sportsbookPlayerCount` } },
{ kind: `Field`, name: { kind: `Name`, value: `casinoPlayerCount` } },
],
},
},
],
},
},
],关键首页请求分别捕获失败并返回空值,其他请求仍能返回。
独立请求失败降级为空值,整页还有默认数据兜底。
机制类似。优先区分加载失败与没有内容;既要保留页面,又要让用户能理解并重试。这里只比较首页数据组织。
src/app/(pages)/home/page.tsx · 第205行
const [recommendResponse, oldResponse] = await Promise.all([
$http.Account.getUserMachineRecommend().catch((error) => {
console.error('getUserMachineRecommend error:', error);
return null;
}),
$http.Account.GameHomeData().catch((oldError) => {
console.error('getGameHomeData (old interface) error:', oldError);
return null;
}),
]);
PERF04 · stake-us-source/chunks/9eOfpkDu.mjs · 字符位置26914
格式化上下文 · 第1171行起;原始精确片段保留在证据JSON。打开带行号阅读版
async function Ae(e) {
return await Promise.all(e.map((e) => Promise.resolve(e).catch(() => void 0)));
}
function je() {
let e = new Date();
return (e.setSeconds(0, 0), e.toISOString());
}
export { Oe as i, je as n, Ae as r, ke as t };游戏分区接近视口时挂载,第一分区初始加载;活动条等待首屏门控后挂载。
下方投注榜可见后才装载组件,之前显示占位。
双方都会推迟下方内容。gogogold已有明确的分区策略,可继续核验占位高度与滚动时的加载连续性。
src/app/(pages)/components/GameCategoryContent/GameCategoryContent.tsx · 第62行
const ref = useRef<HTMLDivElement>(null);
const [inViewport] = useInViewport(ref, {
threshold: 0.1, // 当 10% 的元素进入视口时触发
rootMargin: '50px', // 提前 50px 开始加载
});
const [hasLoaded, setHasLoaded] = useState(sectionIndex === 0);
// 一旦进入视口,就标记为已加载(不再卸载)
useEffect(() => {
if (inViewport && !hasLoaded) {
setHasLoaded(true);
}
}, [inViewport, hasLoaded]);
return (src/app/(pages)/home/page.tsx · 第380行
{/* 活动列表:dynamic 拆包 + LCP 后挂载,不阻塞首屏;占位降低 CLS */}
{shouldShowHomePopupsList && isAfterLCP && <HomePopupsList />}PERF05 · stake-us-source/nodes/55.BHHpgWjN.mjs · 字符位置37027
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
const __vite__mapDeps = (
i,
m = __vite__mapDeps,
d = m.f ||
(m.f = [
"/_app/immutable/chunks/CUeUO4t2.mjs",
"/_app/immutable/chunks/BGNdxI84.mjs",
"/_app/immutable/chunks/C0FnF6B9.mjs",
"/_app/immutable/chunks/D4G09-Ja.mjs",
"/_app/immutable/chunks/B3xfQiSE.mjs",
"/_app/immutable/chunks/DiYNMAEe.mjs",
"/_app/immutable/chunks/Cu3B9wdT.mjs",
"/_app/immutable/chunks/D5F2HdYq.mjs",
"/_app/immutable/chunks/U5cMxr-6.mjs",
"/_app/immutable/chunks/CYbi0xxB.mjs",
"/_app/immutable/chunks/Dt-HX3Vu.mjs",
"/_app/immutable/chunks/CEO7yYTd.mjs",
"/_app/immutable/chunks/BwOURxTH.mjs",
"/_app/immutable/chunks/CjxnTdcP.mjs",
"/_app/immutable/chunks/C_ugM1im.mjs",
"/_app/immutable/chunks/BoKiwcaY.mjs",
"/_app/immutable/chunks/DA93Lu4n.mjs",
"/_app/immutable/chunks/DfQ0GKpz.mjs",
"/_app/immutable/chunks/BBRjFdJT.mjs",
"/_app/immutable/chunks/CcUq4hRx.mjs",超过45个游戏启用按行虚拟化,只渲染可见区域和缓冲行。
已恢复分组目录和卡片结构;当前证据未确认等价虚拟列表。
gogogold这项已有明确实现。保留并检查滚动体验;Stake未知不扣分,也不据此认定其列表更慢。
src/app/_components/GameGrid/GameGrid.tsx · 第19行
const COLUMN_COUNT = 3;
// 约三屏内容后启用虚拟化;手机实际只挂载可见区域及上下缓冲行。
const VIRTUALIZE_THRESHOLD = 45;
const OVERSCAN_ROWS = 4;
// 固定首屏参数保证 SSG 与客户端首次渲染一致,挂载后由 measureElement 校准。
const INITIAL_RECT = { width: 1080, height: 1200 };
const ESTIMATED_ROW_HEIGHT = 240;src/app/_components/GameGrid/GameGrid.tsx · 第95行
const virtualizer = useWindowVirtualizer({
count: rows.length,
estimateSize: () => ESTIMATED_ROW_HEIGHT,
initialOffset: 0,
initialRect: INITIAL_RECT,
overscan: OVERSCAN_ROWS,
scrollMargin,
});
return (
<div
ref={containerRef}
className={`${styles.virtualGrid} ${className ?? ''}`}
style={{ height: virtualizer.getTotalSize() }}
>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
ref={virtualizer.measureElement}
data-index={virtualRow.index}
className={styles.virtualRow}
style={{ transform: `translateY(${virtualRow.start - scrollMargin}px)` }}PERF05 · stake-us-source/nodes/55.BHHpgWjN.mjs · 字符位置37027
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
const __vite__mapDeps = (
i,
m = __vite__mapDeps,
d = m.f ||
(m.f = [
"/_app/immutable/chunks/CUeUO4t2.mjs",
"/_app/immutable/chunks/BGNdxI84.mjs",
"/_app/immutable/chunks/C0FnF6B9.mjs",
"/_app/immutable/chunks/D4G09-Ja.mjs",
"/_app/immutable/chunks/B3xfQiSE.mjs",
"/_app/immutable/chunks/DiYNMAEe.mjs",
"/_app/immutable/chunks/Cu3B9wdT.mjs",
"/_app/immutable/chunks/D5F2HdYq.mjs",
"/_app/immutable/chunks/U5cMxr-6.mjs",
"/_app/immutable/chunks/CYbi0xxB.mjs",
"/_app/immutable/chunks/Dt-HX3Vu.mjs",
"/_app/immutable/chunks/CEO7yYTd.mjs",
"/_app/immutable/chunks/BwOURxTH.mjs",
"/_app/immutable/chunks/CjxnTdcP.mjs",
"/_app/immutable/chunks/C_ugM1im.mjs",
"/_app/immutable/chunks/BoKiwcaY.mjs",
"/_app/immutable/chunks/DA93Lu4n.mjs",
"/_app/immutable/chunks/DfQ0GKpz.mjs",
"/_app/immutable/chunks/BBRjFdJT.mjs",
"/_app/immutable/chunks/CcUq4hRx.mjs",轻壳保持身份,重内容按需加载;后台预测预载等门控,用户主动打开立即加载;缓存合并并发加载。
按网址中的弹窗名加载模块,加载中与错误状态独立处理。
双方都具备按需弹窗。Pwa多出可见的首屏后预测预载调度,无需替换现有工厂;首次打开等待仍需实测。
src/Master/SDialogFC/createDeferredDialog.tsx · 第72行
// 模块级闭包缓存:contentRef 保存已解析组件,loadPromise 合并同时发生的 import()。
let contentRef: FC<P> | null = null;
let loadPromise: Promise<FC<P>> | null = null;
const loadContent = (requestedPhase: DeferredDialogLoadPhase): Promise<FC<P>> => {
if (contentRef) {
return Promise.resolve(contentRef);
}
if (!loadPromise) {
loadPromise = (async () => {
const mod = await loadModule();
const Comp = mod.default;
if (!Comp) {
throw new Error(`[${chunkName}] deferred content missing default export`);src/Master/SDialogFC/createDeferredDialog.tsx · 第123行
/** 对外的显式加载入口;用户操作触发时不等待 LCP。 */
const load = (): Promise<FC<P>> => loadContent('explicit');
if (typeof window !== 'undefined') {
// 只有预测性后台预加载等待 LCP;显式 load() 和已挂载薄壳的加载属于当前用户操作,
// 必须立即执行。后台失败已由 loadContent 上报 Sentry,这里只阻止 rejection 形成
// 未处理异常;真正打开时仍会按上面的重试规则再次加载。
LCPGate.run(() => loadContent('preload')).catch(() => {});
}PERF06 · stake-us-source/chunks/Dh3-ySZv.mjs · 字符位置157657
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
const __vite__mapDeps = (
i,
m = __vite__mapDeps,
d = m.f ||
(m.f = [
"/_app/immutable/chunks/B384JaQx.mjs",
"/_app/immutable/chunks/BGNdxI84.mjs",
"/_app/immutable/chunks/C0FnF6B9.mjs",
"/_app/immutable/chunks/CYbi0xxB.mjs",
"/_app/immutable/chunks/Dt-HX3Vu.mjs",
"/_app/immutable/chunks/D5F2HdYq.mjs",
"/_app/immutable/chunks/C_ugM1im.mjs",
"/_app/immutable/chunks/DiYNMAEe.mjs",
"/_app/immutable/chunks/B3xfQiSE.mjs",
"/_app/immutable/chunks/BoKiwcaY.mjs",
"/_app/immutable/chunks/DA93Lu4n.mjs",
"/_app/immutable/chunks/CjxnTdcP.mjs",
"/_app/immutable/chunks/DfQ0GKpz.mjs",
"/_app/immutable/chunks/BBRjFdJT.mjs",
"/_app/immutable/chunks/U5cMxr-6.mjs",
"/_app/immutable/chunks/CcUq4hRx.mjs",
"/_app/immutable/chunks/CRqwqRD6.mjs",
"/_app/immutable/chunks/D_XsA3GS.mjs",
"/_app/immutable/chunks/xihTtKlq.mjs",
"/_app/immutable/chunks/-T3WOthb.mjs",Banner实际设置高优先级时eager/high,其余lazy/low;图片组件支持ThumbHash占位和缓存图片就绪检测。
共享图像组件支持关键图eager/high、普通图lazy/async,以及响应式尺寸候选。
gogogold有Banner调用点的明确证据;Stake只确认共享组件能力,首页主图是否启用还未知。图片真实体积、解码与布局移动都未测。
src/app/(pages)/components/Banner/Banner.tsx · 第157行
<OssImage
src={image.imageUrl}
alt=''
fill
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
// 仅首张 eager+high,其余 lazy+low,避免非首帧抢占 LCP。
loading={highPriority ? 'eager' : 'lazy'}
fetchPriority={highPriority ? 'high' : 'low'}
style={{ objectFit: 'cover' }}
onLoad={() => notifyFirstBannerReady(true)}
onError={() => notifyFirstBannerReady(false)}
/>src/app/_components/OssImage/index.tsx · 第24行
const placeholder =
showPlaceholder && src && !src.startsWith('data:')
? (placeholderSrc ?? getImageThumbHash(src))
: undefined;
const showBackground = loadedSrc !== imageSrc && placeholder;
useLayoutEffect(() => {
// Tab 回切重新挂载时,缓存图片已就绪则在首帧绘制前跳过占位图。
if (imageRef.current?.complete && imageRef.current.naturalWidth > 0) {
setLoadedSrc(imageSrc);
}
}, [imageSrc]);
return (
<Image
ref={imageRef}PERF08 · stake-us-source/chunks/D0dggn6h.mjs · 字符位置19835
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
import {
Ct as e,
Lt as t,
Q as n,
Rt as r,
_t as i,
d as a,
et as o,
f as s,
ft as c,
hn as l,
lt as u,
on as d,
pt as f,
s as p,
sn as m,
st as h,
wt as ee,
x as te,
} from "./BGNdxI84.mjs";
import "./xihTtKlq.mjs";
import "./CX-yFtb_.mjs";
var g = {
"images.ctfassets.net": `contentful`,
"cdn.builder.io": `builder.io`,根布局的本地字体使用swap并启用预载。
JetBrains Mono技术字体设置swap;全站主字体不能据此推断。
双方都有先用替代字体显示的机制,但覆盖范围不同。gogogold需核验字体预载是否必要及切换后的布局变化。
src/app/layout.tsx · 第18行
// 目标五:仅预载 Roboto 400;700/900 由 DeferredRobotoWeights 空闲挂载(须同主 family 名)
const roboto = localFont({
src: [
{
path: '../fonts/Roboto-Regular.woff2',
weight: '400',
style: 'normal',
},
],
variable: '--font-roboto',
display: 'swap',
preload: true,
});PERF09 · stake-us-source/assets/0.CBZv068J.css · 字符位置61126
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
:root {
color: var(--color-grey-200);
--shadows-sm: 0 1px 3px 0 #0003, 0 1px 2px 0 #0000001f;
--shadows-md: 0 4px 6px -1px #1b171733, 0 2px 4px -1px #0000001f;
--shadows-lg: 0 10px 15px -3px #0003, 0 4px 6px -2px #0000001a;
--shadows-xl: 0 20px 25px -5px #0003, 0 10px 10px -5px #00000014;
--shadows-2xl: 0 25px 50px -12px #0006;
--shadows-inner: inset 0 2px 4px 0 #0003;
--shadow-inset: inset 0 1px #ffffff0a;
--btn-shadow: var(--shadows-sm), var(--shadow-inset);
--shadow-button-sm: 0rem 0.0625rem 0.1875rem #0003, 0rem 0.0625rem 0.125rem #0000001f;
--border-thin: 0.0625rem;
--border-light: 0.125rem;
--border-medium: 0.25rem;
--border-radius-none: 0;
--border-radius-sm: 0.125rem;
--border-radius-base: 0.25rem;
--border-radius-md: 0.5rem;
--border-radius-lg: 4rem;
--border-radius-0-15: 0.15rem;
--input-color: var(--color-white);
--input-background: var(--color-grey-700);
--input-border: var(--color-grey-400);
--input-placeholder: #566671;
--input-hover-color: var(--color-white);Replay录屏模块动态导入,复用加载Promise;采样命中时启动,未命中则错误路径按需启动;基础日志SDK仍静态引入。
日志和音频SDK独立成代码块,但未确认其初始化是否推迟。
gogogold已确认录屏模块分支加载,但采样命中会启动,不能称所有SDK都等首屏结束。Stake拆块也不等于延后执行。
src/instrumentation-client.ts · 第9行
const sessionSampleRate = isSentryProduction ? 0.2 : 1.0;
/** 客户端先掷骰:命中则常驻 Session Replay;未命中则仅错误路径按需加载 */
const shouldSampleSession = Math.random() < sessionSampleRate;
let replayLoading: Promise<void> | null = null;
let replayReady = false;
async function ensureReplayIntegration(): Promise<void> {
if (replayReady) return;
if (replayLoading) return replayLoading;
replayLoading = import('@sentry/replay')
.then(({ replayIntegration }) => {src/instrumentation-client.ts · 第168行
// Session 未命中时,真实异常上报前触发 Replay 动态加载(覆盖 onError 采样)
if (typeof window !== 'undefined' && !shouldSampleSession && !replayReady) {
void ensureReplayIntegration();
}
return event;
}
});
if (typeof window !== 'undefined' && shouldSampleSession) {
void ensureReplayIntegration();
}PERF10 · stake-us-source/chunks/CM5G2D07.mjs · 字符位置5123
格式化上下文 · 第317行起;原始精确片段保留在证据JSON。打开带行号阅读版
function ft(e, t) {
if (!e.forwardErrorsToLogs) return { stop: x };
let n = (C ? new Se() : Qe(e)).subscribe((e) => {
e.state === `complete` && a(E.XHR, e);
}),
r = j({ responseBodyAction: (e) => (i(e) ? 2 : 0) }).subscribe((e) => {
e.state === `resolve` && a(E.FETCH, e);
});
function i(e) {
return !l(e.url) && (pt(e) || ae(e.status));
}
function a(n, r) {
if (!i(r)) return;
let a = `error` in r && r.error ? Te(Ze(r.error)) : r.responseBody || `Failed to load`,
o = { isAborted: r.isAborted, handlingStack: r.handlingStack };
t.notify(0, {
rawLogsEvent: {
message: `${mt(n)} error ${r.method} ${r.url}`,
date: r.startClocks.timeStamp,
error: { stack: s(a, e.requestErrorResponseLengthLimit, `...`), handling: void 0 },
http: { method: r.method, status_code: r.status, url: r.url },
status: F.error,
origin: A.NETWORK,
},
domainContext: o,统一LCPGate观测最大内容绘制候选,包含稳定、交互、页面隐藏及超时兜底;活动条与弹窗后台预载采用它。
已确认路由预载和可见区块加载;未确认同类统一LCP门控。
gogogold已有调度机制,值得保留。门控完成可能来自超时,不等于浏览器LCP最终值;应记录触发原因,并检查后台预载是否过量。
src/Fwk/Performance/LCPGate.ts · 第29行
export type LCPGateResult = {
entry: PerformanceEntry | null;
/** true 表示走了 timeout 兜底,而非真实 LCP 观测 */
fallback: boolean;
/** 触发原因,便于排查 */
reason: 'paint' | 'stable' | 'final-input' | 'final-hidden' | 'timeout' | 'ssr';
/** navigationStart 起算的触发时刻(ms) */
readyAt: number;src/Fwk/Performance/LCPGate.ts · 第196行
observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
if (entries.length > 0) {
lastEntry = entries[entries.length - 1];
onLCPEntry();
}
});
// buffered 会补发 observer 注册前已经产生的候选,避免 hydration 较晚时漏报。
observer.observe({ type: 'largest-contentful-paint', buffered: true });
} catch (error) {
observer = undefined;
LogUtil.info(LOG_TAG, 'PerformanceObserver unavailable', error);
}
} else {
LogUtil.info(LOG_TAG, 'Browser does not support PerformanceObserver');
}
if (mode === 'final') {
bindFinalListeners();
}
// 兜底:6s 仍无 LCP 时强制执行
timeoutId = setTimeout(() => finish(!lastEntry, 'timeout'), FALLBACK_TIMEOUT_MS);src/app/(pages)/home/page.tsx · 第94行
const shouldRunEntryAnimation = useRef(!hasVisitedHomePage);
const shouldShowHomePopupsList = gameList && gameList.length > 0;
// 与 Geo / Sentry Replay 共用 LCPGate;LCP stable 后再挂载活动条
const isAfterLCP = useAfterLCP();
// 如果是fb落地页,则返回引导页PERF01 · stake-us-source/entry/app.Q5XUkswp.mjs · 字符位置92973
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
const __vite__mapDeps = (
i,
m = __vite__mapDeps,
d = m.f ||
(m.f = [
"/_app/immutable/chunks/CM5G2D07.mjs",
"/_app/immutable/chunks/C0FnF6B9.mjs",
"/_app/immutable/chunks/CYbi0xxB.mjs",
"/_app/immutable/chunks/BGNdxI84.mjs",
"/_app/immutable/chunks/Dt-HX3Vu.mjs",
"/_app/immutable/chunks/D5F2HdYq.mjs",
"/_app/immutable/chunks/D4G09-Ja.mjs",
"/_app/immutable/chunks/DiYNMAEe.mjs",
"/_app/immutable/chunks/B3xfQiSE.mjs",
"/_app/immutable/chunks/CPdaYJXM.mjs",
"/_app/immutable/chunks/DoD3rfum.mjs",
"/_app/immutable/nodes/0.CR0Lrtr3.mjs",
"/_app/immutable/chunks/uBIymjUX.mjs",
"/_app/immutable/chunks/Cu3B9wdT.mjs",
"/_app/immutable/chunks/U5cMxr-6.mjs",
"/_app/immutable/chunks/CEO7yYTd.mjs",
"/_app/immutable/chunks/BwOURxTH.mjs",
"/_app/immutable/chunks/CjxnTdcP.mjs",
"/_app/immutable/chunks/DdanaPe6.mjs",
"/_app/immutable/chunks/D35lYozi.mjs",PERF05 · stake-us-source/nodes/55.BHHpgWjN.mjs · 字符位置37027
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
const __vite__mapDeps = (
i,
m = __vite__mapDeps,
d = m.f ||
(m.f = [
"/_app/immutable/chunks/CUeUO4t2.mjs",
"/_app/immutable/chunks/BGNdxI84.mjs",
"/_app/immutable/chunks/C0FnF6B9.mjs",
"/_app/immutable/chunks/D4G09-Ja.mjs",
"/_app/immutable/chunks/B3xfQiSE.mjs",
"/_app/immutable/chunks/DiYNMAEe.mjs",
"/_app/immutable/chunks/Cu3B9wdT.mjs",
"/_app/immutable/chunks/D5F2HdYq.mjs",
"/_app/immutable/chunks/U5cMxr-6.mjs",
"/_app/immutable/chunks/CYbi0xxB.mjs",
"/_app/immutable/chunks/Dt-HX3Vu.mjs",
"/_app/immutable/chunks/CEO7yYTd.mjs",
"/_app/immutable/chunks/BwOURxTH.mjs",
"/_app/immutable/chunks/CjxnTdcP.mjs",
"/_app/immutable/chunks/C_ugM1im.mjs",
"/_app/immutable/chunks/BoKiwcaY.mjs",
"/_app/immutable/chunks/DA93Lu4n.mjs",
"/_app/immutable/chunks/DfQ0GKpz.mjs",
"/_app/immutable/chunks/BBRjFdJT.mjs",
"/_app/immutable/chunks/CcUq4hRx.mjs",PERF06 · stake-us-source/chunks/Dh3-ySZv.mjs · 字符位置157657
格式化上下文 · 第1行起;原始精确片段保留在证据JSON。打开带行号阅读版
const __vite__mapDeps = (
i,
m = __vite__mapDeps,
d = m.f ||
(m.f = [
"/_app/immutable/chunks/B384JaQx.mjs",
"/_app/immutable/chunks/BGNdxI84.mjs",
"/_app/immutable/chunks/C0FnF6B9.mjs",
"/_app/immutable/chunks/CYbi0xxB.mjs",
"/_app/immutable/chunks/Dt-HX3Vu.mjs",
"/_app/immutable/chunks/D5F2HdYq.mjs",
"/_app/immutable/chunks/C_ugM1im.mjs",
"/_app/immutable/chunks/DiYNMAEe.mjs",
"/_app/immutable/chunks/B3xfQiSE.mjs",
"/_app/immutable/chunks/BoKiwcaY.mjs",
"/_app/immutable/chunks/DA93Lu4n.mjs",
"/_app/immutable/chunks/CjxnTdcP.mjs",
"/_app/immutable/chunks/DfQ0GKpz.mjs",
"/_app/immutable/chunks/BBRjFdJT.mjs",
"/_app/immutable/chunks/U5cMxr-6.mjs",
"/_app/immutable/chunks/CcUq4hRx.mjs",
"/_app/immutable/chunks/CRqwqRD6.mjs",
"/_app/immutable/chunks/D_XsA3GS.mjs",
"/_app/immutable/chunks/xihTtKlq.mjs",
"/_app/immutable/chunks/-T3WOthb.mjs",先看这段代码为什么重要、对项目有什么价值,再展开实现步骤。辅助函数按职责抽象,不是可运行源码。
快速切换游戏时,为什么旧页面的结果不能覆盖新页面?
用户点击游戏卡片,等待页面加载,又切到另一款游戏。
游戏壳集中加载资料、判断能否展示,并用请求编号识别晚到的结果;具体游戏实例还有自己的生命周期清理。
let latestGameCheck = 0;
async function enterGame(gameId) {
const checkId = ++latestGameCheck;
const info = await loadGamePageData(gameId);
if (checkId !== latestGameCheck) return;
if (!info.game) return showNotFound();
if (!canRenderGame(info)) return showUnavailable();
applyGameLayout(info);
mountGameContent(info);
}
function disposeGameInstance(instance) {
instance.releaseRegisteredSubscriptions();
instance.releaseRegisteredResources();
}
// loadGamePageData抽象并行资料加载与后续状态检查。
// canRenderGame保留活动状态及未结束游戏等条件分支。
// 清理表示已见实例生命周期,不宣称所有供应商内部均已恢复。边界:这是前端游戏壳与实例边界;第三方供应商内部加载和清理未全部恢复。
请求结果、余额推送和动画先后到达,怎样协调同一局?
用户发起一局后,服务器响应、余额推送和结果动画可能在不同时间完成。
前端用本局编号关联响应、余额事件与展示结束;请求失败时回滚本局记录。响应中的结果和金额参与更新,不由客户端决定真实结算。
async function rollDice({ target, condition, amount, currency }) {
// 先创建零金额记录;响应到达后才用返回金额更新。
const identifier = ledger.create({ amount: 0, currency });
try {
const response = await gameClient.post("dice/roll", {
target, condition, identifier, amount, currency
});
const bet = response.diceRoll;
await waitForMinimumRevealTime();
ledger.applyResponse({
identifier,
amount: bet.amount,
payout: bet.payout,
currency: bet.currency
});
resultState.set(bet);
return { ...bet, identifier };
} catch (error) {
ledger.rollback(identifier);
throw error;
}
}
function finishPresentation(identifier) {
ledger.markGameFinished(identifier);
// 真实余额推送与本局状态满足同步条件后再清理记录。
ledger.clearIfSynchronized(identifier);
}边界:只恢复前端协调机制,不能据此证明服务器结算正确或所有事件顺序均已验证。
用户点停止、达到局数或余额不足,下一局还会不会发出?
连续游戏过程中,用户要求停止,或自动策略达到停止条件。
当前一局正常返回后更新净收益与统计,计算下一局金额,再检查停止意图、局数、余额与策略条件。
let stopRequested = false;
function requestStop() {
stopRequested = true;
}
async function runAutoBet(settings) {
stopRequested = false;
const stats = createStats();
let nextBet = settings.initialBet;
while (true) {
const bet = await rollDice({ ...settings.game, amount: nextBet });
stats.netProfit += bet.payout - bet.amount;
stats.updateStreaks(bet);
const decision = evaluateStrategyBlocks(settings, stats, nextBet);
nextBet = decision.nextBet;
// Dice自动展示:快速模式立即结束,否则等待400ms。
if (!settings.instant) await delay(400);
finishPresentation(bet.identifier);
if (stopRequested || decision.stop) break;
if (hasReachedGameLimit(settings, stats)) break;
if (nextBet > readAvailableBalance()) break;
if (wouldExceedLossLimit(settings, stats, nextBet)) break;
}
autoState.set("idle");
}
// 整理正常继续/停止路径;请求错误由游戏状态机处理。边界:伪代码展示正常继续与停止主线;异常路径及停止按钮真实延迟还需运行验证。
怎样关联本次授权,并在取消或失败时结束等待?
用户选择Google登录,浏览器跳转或打开授权窗口,随后成功、取消或失败。
跳转分支准备随机校验值与挑战,提交返回地址;失败可回退弹窗订阅。取消弹窗时清理定时检查、订阅和加载状态。
async function startGoogleLogin() {
if (redirectFeatureEnabled()) {
try {
const verifier = createRandomVerifier(32);
const challenge = await sha256Base64Url(verifier);
sessionStorage.setItem(verifierKey, verifier);
const response = await startOauthRedirect({
provider: "google",
codeChallenge: challenge,
returnTo: currentPathWithQueryAndHash()
});
if (!response.url) throw new Error("Authorization URL missing");
location.assign(response.url);
return;
} catch (error) {
clearTemporaryRedirectState();
}
}
startOauthPopupSubscription();
}
function cancelPopupLogin() {
clearPopupPollingTimer();
unsubscribeOauthResult();
closePopupIfNeeded();
resetLoginLoadingState();
}
// 跳转请求已确认;回调兑换、服务器验证与登录后恢复未完整确认。
// returnTo只证明传递返回地址,不证明后续流程一定成功恢复。边界:随机挑战的生成已确认,但服务器兑换、完整回调及登录后恢复仍有证据缺口。
首页这么多区块,哪些一起加载,哪些等用户看见才加载?
用户打开首页,浏览最近、热门与推荐游戏,然后点击进入游戏页。
首页最多11类独立数据并行组装,单项失败可降级;下方投注榜等复杂区块到可见时才装载。
async function loadHomepage(config) {
const jobs = [
loadRecentGames(),
loadTrendingGames(),
loadPersonalizedRecommendations(),
...otherConfiguredHomepageJobs(config)
];
// 独立失败降级为空值,但整批仍会等最慢任务。
const results = await Promise.all(
jobs.map(job => Promise.resolve(job).catch(() => undefined))
);
return buildHomepageGroups(results);
}
async function showVisibleBetsBoard(isVisible) {
if (!isVisible) return showBetsBoardPlaceholder();
const component = await importBetsBoard();
mountBetsBoard(component);
}
function openGameFromCard(card) {
navigateToGamePage(card.slug);
}
// 省略首页其余数据源与详情页逻辑,避免把不同阶段拼成一条假调用链。
// 仅说明并行组装、可见区加载和游戏入口,不承诺实际首屏耗时。边界:这是静态加载策略,没有同条件首屏测量。目录布局与下注逻辑属于不同模块。
按0–100显示:各维度1–5分,按权重平均后换算。未知项不计入分母。
这里只定位文件;简明结论在“逐项强对比”,完整原始证据可下载。
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
Stake依据:SUP16
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
Stake依据:SUP24
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码:
对应gogogold代码: