- CLAUDE.md 新增「第二准则:数据源权威、唯一,下游不兜底」 - bootstrap 删除 fallbackServers 选项与 ?? DEFAULT_GAMESERVER 猜默认 - 无 server/gameserver、远程失败、解析不出地址 → 一律抛 ConfigFetchError 显式暴露 - 同步 guide + 两篇 spec(删 fallback/降级措辞) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
配置 / 渠道子系统设计(framework/config)
状态:已与用户确认(brainstorming 通过),待转 writing-plans。 适用工程:
cocoscreator_projects/YouleNexus(新 Cocos 前端)。 关联:Plan 2 框架内核 net(产出NetClient,本子系统为其提供identity与servers);spec2026-06-28-cocos-framework-design.md§0.1(协议 SSOT)。
0. 目标与背景
新框架要在服务器零改动、原生接口逐字一致前提下,复刻原工程(Game_Surface_3)获取「渠道身份 + 远程配置 + 服务器地址」的整套机制,并让开发调试时方便切换调试服与渠道身份。
原工程机制(事实依据):
- 构建期渠道身份:
version.js写死GameData.GameId(固定),GameData.AgentId/ChannelId默认值,GameData.Version/versionCode。 - 运行期身份获取(
12_Logic.js初始化,isH5Version()判定):- H5 模式(URL 不含
index.html):URL queryfGetQuery("agentid"/"channelid"); - 原生模式:
Func.getothername("agent")/Func.getchannelName()/Func.getmarketname()→window.settings.*或全局app_*(05_Func.js:1961/2429/2467)。覆盖version.js默认值。
- H5 模式(URL 不含
- 远程配置文件:
Game_Config.Debugger.gameserver(.txtURL + 防缓存随机数 +serverType切正式/本地,00_SubGame_Config.js:11)→get_config(url)GET(12_Logic.js:1332)→getConfig_Succ存GameData.serverConfig.data(12_Logic.js:1392)→get_paravalue(config, paraname)按agentlist→gamelist→channellist→marketlist分层匹配取参(12_Logic.js:1345,越具体越优先)。连接相关参数:player_server_tcp/http、visitor_server_tcp/http、game_server_tcp/http(12_Logic.js:1404-1409)。 - 调试开关:
Game_Config.Debugger(isDebugger/serverType/gameserver),发布前需手改。
命名原则(红线 vs 内部)
- 红线(逐字不变):跨边界字符串契约——协议字段
agentid/channelid/gameid/marketid/openid/version;原生接口名getothername/getchannelName/getmarketname与全局app_*;远程配置键agentlist/gamelist/channellist/marketlist/player_server_tcp/...;gameserverURL 防缓存查询串。 - 内部命名现代专业:不照抄
GameData.AgentId/get_paravalue/Logic/Game_Config.Debugger,用ChannelIdentity/getParam/resolveBootstrap/DebugProfile等清晰命名。
1. 架构与分层
新增 framework/config/ 层,依赖方向 config → core(仅用 core 的常量/工具),向上被启动编排消费后把结果交给 net(NetClient.setIdentity + servers)。纯逻辑与异步 IO 严格分离,原生读取收敛为单一 source,全部可在 Node 下用 fake 测试。
framework/config/
├─ identity.ts # ChannelIdentity 类型 + IdentitySource 接口 + resolveIdentity() 纯合并
├─ profiles.ts # DebugProfile 类型 + PROFILES 表 + resolveActiveProfile()(含 ?profile= 覆盖)
├─ remote-config.ts # RemoteConfig 类型 + ConfigFetcher 接口 + getParam() 纯查找 + resolveServers()
├─ remote-config-fetcher.ts # 生产用 ConfigFetcher(XHR/fetch + 防缓存时间戳),仅 typecheck
├─ bootstrap.ts # resolveBootstrap():编排身份 + 服务器,产出 { identity, servers, rawConfig }
└─ sources/
├─ defaults.ts # BUILD_IDENTITY 构建期默认(version.js 等价物,每子游戏可改)
├─ query-string.ts # queryStringSource(search) → Partial<ChannelIdentity>(H5 模式)
└─ native-settings.ts # nativeSettingsSource(win) → Partial<ChannelIdentity>(同步 window.settings/app_*)
2. 关键类型与身份合并
// 类型名现代化;属性名沿用协议字段(序列化进 player_login,红线逐字不变)
export interface ChannelIdentity {
agentid: string | number;
channelid: string | number;
gameid: string;
marketid: string | number;
version: string; // 旧 GameData.Version
versionCode: number; // 旧 GameData.versionCode
}
export type IdentitySource = () => Partial<ChannelIdentity>;
/** 按数组顺序合并,后者覆盖前者(仅覆盖值非 undefined/非空字符串的字段)。 */
export function resolveIdentity(sources: IdentitySource[]): ChannelIdentity;
优先级(低 → 高,后者胜):
defaults < (nativeSettings 原生环境 | queryString H5 环境)< debugProfile(active≠prod 时贡献身份覆盖)< 显式 URL query 覆盖(最高,方便临时改单字段)。
说明:H5 模式下
queryString既是合法运行时来源、也承担「显式覆盖」职责,故排在最高位;原生模式下运行时来源是nativeSettings,URL query 仅在显式给出时覆盖。
BUILD_IDENTITY(sources/defaults.ts)即 version.js 等价物:gameid 固定写死(每子游戏不同),agentid/channelid/marketid/version/versionCode 给默认值,运行期被覆盖。
3. 服务器解析与数据流
服务器来源(按优先级)
- 调试 profile 显式
server:直接用,跳过远程抓取(开发期「一键调试服地址」)。 - 远程配置:
ConfigFetcherGETgameservertxt(带防缓存时间戳)→getParam()取连接参数 →resolveServers()组装 ws 候选列表。
getParam(纯函数,忠实 get_paravalue 语义)
/** 分层取参:data[key] 为顶层默认,逐层进入匹配的 agent→game→channel→market,越具体越优先。 */
export function getParam(config: RemoteConfig, key: string, id: ChannelIdentity): unknown;
匹配链:data.agentlist(按 agentid) → .gamelist(按 gameid) → .channellist(按 channelid) → .marketlist(按 marketid);每层若有该 key 则更新返回值,匹配不到则返回当前最具体值(最终可能为顶层值或 null)。
resolveServers(纯函数)
读连接参数键(逐字一致):player_server_tcp / player_server_http / visitor_server_tcp / visitor_server_http / game_server_tcp / game_server_http。规则:优先 player_server_tcp,回退 visitor_server_tcp;值形如 ip:port,前缀 ws:// 产出候选数组(多个 → 交 NetClient 轮询)。
resolveBootstrap 编排
export interface BootstrapOptions {
win: Window & { settings?: any }; // 原生注入对象宿主(测试可注入 fake)
search: string; // location.search(测试可注入)
fetcher: ConfigFetcher; // 远程配置抓取(测试注入 fake)
isNative: boolean; // 环境判定结果(URL 含 index.html → true)
}
export interface BootstrapResult {
identity: ChannelIdentity;
servers: string[];
rawConfig: RemoteConfig | null;
}
export function resolveBootstrap(opts: BootstrapOptions): Promise<BootstrapResult>;
流程:① resolveActiveProfile(search);② identity = resolveIdentity([defaults, 运行时 source(native|query), profile 身份, query 覆盖]);③ servers:profile 有 server 则用之,否则 fetcher.fetch(gameserverUrl) → getParam → resolveServers;④ 返回 { identity, servers, rawConfig }。
调用方(后续 platform/启动场景):net.setIdentity(result.identity) + new NetClient({ servers: result.servers, ... }) + net.start()。
4. 调试 profiles(profiles.ts)
export interface DebugProfile {
name: string;
server?: string; // 显式 ws 调试服地址(给定则跳过远程抓取)
gameserver?: string; // 覆盖远程配置 txt URL
identity?: Partial<ChannelIdentity>;// 调试用渠道身份覆盖
isDebugger?: boolean; // 收发包日志开关(替代旧 Game_Config.Debugger.isDebugger)
}
export const PROFILES: Record<string, DebugProfile>; // 如 prod / local / staging
export const ACTIVE_PROFILE = 'prod'; // 发布只需保持 prod
/** 解析当前 profile:ACTIVE_PROFILE 为基线,?profile=local 可临时切换。 */
export function resolveActiveProfile(search: string): DebugProfile;
prod profile 不含 server/identity 覆盖、isDebugger=false,等价正式发布;local/staging 提供调试服地址与测试身份。URL ?profile=<name> 可临时切换。
5. 外部契约保留(红线落点汇总)
- 序列化身份字段名
agentid/channelid/gameid/marketid/version逐字不变; - 原生读取:
win.settings.getothername('agent')、win.settings.getchannelName()、win.settings.getmarketname(),全局回退window['app_'+name](与05_Func.js:1961/2429/2467对齐); - 远程配置键
agentlist/gamelist/channellist/marketlist+ 匹配键agentid/gameid/channelid/marketid+*_server_*参数键,逐字不变; gameserverURL 防缓存:<url>?<timestamp>(等价min_timestamp/ifast_random)。
6. 错误处理
- 远程抓取失败 / 解析不出任何地址 / profile 既无 server 又无 gameserver:一律抛
ConfigFetchError显式暴露,不猜默认、不降级(CLAUDE.md 第二准则)。重试/重连归 net,不在本层造兜底; getParam:某层匹配不到 → 返回已找到的最具体值或顶层data[key],最终可能null(忠实原语义);- 原生 settings 缺失:
try/catch→ source 返回{}(不覆盖),等价原try/catch return ""; - 远程 txt JSON 解析失败:抛
ConfigParseError。
7. 测试策略(tsx + node:test,置于 framework-tests/config/)
- 纯函数:
resolveIdentity(优先级/部分覆盖/空值不覆盖)、getParam(分层匹配/逐层回退/最具体胜,用样例嵌套 config)、resolveServers(player 优先、visitor 回退、ws://前缀、多候选); - sources:
query-string(解析 agentid/channelid/marketid/profile)、native-settings(注入 fakewin.settings+ 全局app_*,含缺失降级); profiles:active 选择 +?profile=覆盖 + prod 无调试覆盖;bootstrap集成:注入 fakeConfigFetcher+ fake 环境,覆盖 (a) profile 直连捷径路径、(b) 远程配置产出 servers+identity 路径、(c) 远程失败降级/抛错;- 生产
remote-config-fetcher(XHR/fetch):仅typecheck:framework,不单测(同cocos-transport)。
验收:npm run test:framework 全绿、npm run typecheck:framework exit 0;resolveBootstrap 在 mock 下能产出可直接喂给 NetClient 的 { identity, servers }。
8. 范围边界(YAGNI)
- 本期做:渠道身份解析(含原生同步读取)、调试 profiles + URL 覆盖、远程配置抓取 + 通用
getParam+ 连接相关服务器地址解析、启动编排。 - 本期不做(后续 Plan):平台/UI 类远程参数(
menunotice/scrollmsg/service_*/logimage/rankList…,将复用同一getParam);完整异步 WVJB 桥(分享/视频/语音等,归 sdk Plan,本期只做身份相关的同步原生读取);服务器切换指令落地(已在 net 层connect_roomserver/agentserver事件)。