# 配置 / 渠道子系统设计(framework/config) > 状态:已与用户确认(brainstorming 通过),待转 writing-plans。 > 适用工程:`cocoscreator_projects/YouleNexus`(新 Cocos 前端)。 > 关联:[Plan 2 框架内核 net](../plans/2026-06-28-framework-core-net-protocol.md)(产出 `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(H5 模式) └─ native-settings.ts # nativeSettingsSource(win) → Partial(同步 window.settings/app_*) ``` ## 2. 关键类型与身份合并 ```ts // 类型名现代化;属性名沿用协议字段(序列化进 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; /** 按数组顺序合并,后者覆盖前者(仅覆盖值非 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` 语义) ```ts /** 分层取参: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 编排 ```ts 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; ``` 流程:① `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) ```ts export interface DebugProfile { name: string; server?: string; // 显式 ws 调试服地址(给定则跳过远程抓取) gameserver?: string; // 覆盖远程配置 txt URL identity?: Partial;// 调试用渠道身份覆盖 isDebugger?: boolean; // 收发包日志开关(替代旧 Game_Config.Debugger.isDebugger) } export const PROFILES: Record; // 如 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=` 可临时切换。 ## 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 防缓存:`?`(等价 `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` 事件)。