Files
youle_cocos/docs/superpowers/specs/2026-06-28-config-channel-design.md
T
joywayerandClaude Opus 4.8 51a4ee57ac refactor(framework): 移除 resolveBootstrap 下游兜底,立第二准则
- CLAUDE.md 新增「第二准则:数据源权威、唯一,下游不兜底」
- bootstrap 删除 fallbackServers 选项与 ?? DEFAULT_GAMESERVER 猜默认
- 无 server/gameserver、远程失败、解析不出地址 → 一律抛 ConfigFetchError 显式暴露
- 同步 guide + 两篇 spec(删 fallback/降级措辞)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:59:15 +08:00

11 KiB
Raw Blame History

配置 / 渠道子系统设计(framework/config)

状态:已与用户确认(brainstorming 通过),待转 writing-plans。 适用工程:cocoscreator_projects/YouleNexus(新 Cocos 前端)。 关联:Plan 2 框架内核 net(产出 NetClient,本子系统为其提供 identity 与 servers);spec 2026-06-28-cocos-framework-design.md §0.1(协议 SSOT)。

0. 目标与背景

新框架要在服务器零改动、原生接口逐字一致前提下,复刻原工程(Game_Surface_3)获取「渠道身份 + 远程配置 + 服务器地址」的整套机制,并让开发调试时方便切换调试服与渠道身份。

原工程机制(事实依据):

  1. 构建期渠道身份:version.js 写死 GameData.GameId(固定),GameData.AgentId/ChannelId 默认值,GameData.Version/versionCode。
  2. 运行期身份获取(12_Logic.js 初始化,isH5Version() 判定):
    • H5 模式(URL 不含 index.html):URL query fGetQuery("agentid"/"channelid");
    • 原生模式:Func.getothername("agent") / Func.getchannelName() / Func.getmarketname() → window.settings.* 或全局 app_*(05_Func.js:1961/2429/2467)。覆盖 version.js 默认值。
  3. 远程配置文件:Game_Config.Debugger.gameserver(.txt URL + 防缓存随机数 + 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)。
  4. 调试开关:Game_Config.Debugger(isDebugger/serverType/gameserver),发布前需手改。

命名原则(红线 vs 内部)

  • 红线(逐字不变):跨边界字符串契约——协议字段 agentid/channelid/gameid/marketid/openid/version;原生接口名 getothername/getchannelName/getmarketname 与全局 app_*;远程配置键 agentlist/gamelist/channellist/marketlist/player_server_tcp/...;gameserver URL 防缓存查询串。
  • 内部命名现代专业:不照抄 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. 服务器解析与数据流

服务器来源(按优先级)

  1. 调试 profile 显式 server:直接用,跳过远程抓取(开发期「一键调试服地址」)。
  2. 远程配置:ConfigFetcher GET gameserver txt(带防缓存时间戳)→ 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_* 参数键,逐字不变;
  • gameserver URL 防缓存:<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(注入 fake win.settings + 全局 app_*,含缺失降级);
  • profiles:active 选择 + ?profile= 覆盖 + prod 无调试覆盖;
  • bootstrap 集成:注入 fake ConfigFetcher + 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 事件)。