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

155 lines
11 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.
# 配置 / 渠道子系统设计(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<ChannelIdentity>(H5 模式)
└─ native-settings.ts # nativeSettingsSource(win) → Partial<ChannelIdentity>(同步 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<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` 语义)
```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<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)
```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` 事件)。