- CLAUDE.md 新增「第二准则:数据源权威、唯一,下游不兜底」 - bootstrap 删除 fallbackServers 选项与 ?? DEFAULT_GAMESERVER 猜默认 - 无 server/gameserver、远程失败、解析不出地址 → 一律抛 ConfigFetchError 显式暴露 - 同步 guide + 两篇 spec(删 fallback/降级措辞) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
155 lines
11 KiB
Markdown
155 lines
11 KiB
Markdown
# 配置 / 渠道子系统设计(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` 事件)。
|