diff --git a/docs/superpowers/specs/2026-06-28-config-channel-design.md b/docs/superpowers/specs/2026-06-28-config-channel-design.md new file mode 100644 index 0000000..3da3e1a --- /dev/null +++ b/docs/superpowers/specs/2026-06-28-config-channel-design.md @@ -0,0 +1,155 @@ +# 配置 / 渠道子系统设计(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) + fallbackServers?: string[]; // 远程失败时降级 +} +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. 错误处理 + +- 远程抓取失败:无 `fallbackServers` 时抛 `ConfigFetchError`,交调用方重试(重连归 net,不在本层重复造轮子);有 fallback 则降级使用; +- `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` 事件)。