- CLAUDE.md 新增「第二准则:数据源权威、唯一,下游不兜底」 - bootstrap 删除 fallbackServers 选项与 ?? DEFAULT_GAMESERVER 猜默认 - 无 server/gameserver、远程失败、解析不出地址 → 一律抛 ConfigFetchError 显式暴露 - 同步 guide + 两篇 spec(删 fallback/降级措辞) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
161 lines
8.4 KiB
Markdown
161 lines
8.4 KiB
Markdown
# debug/release 一键运行模式设计(framework/config runtime-mode)
|
||
|
||
> 状态:已与用户确认(brainstorming 通过),待转 writing-plans。
|
||
> 适用工程:`cocoscreator_projects/YouleNexus`。
|
||
> 关联:配置/渠道子系统 spec `2026-06-28-config-channel-design.md`(本设计在其 `resolveBootstrap` 上加运行模式)、`docs/guides/配置与调试指南.md`(需同步更新)。
|
||
|
||
## 0. 目标与背景
|
||
|
||
当前「调试 ↔ 正式」只能靠改 `ACTIVE_PROFILE` 常量或 URL `?profile=` 切换,且:
|
||
1. `DebugProfile.isDebugger` 字段**无人消费**(没有任何日志按它开关);
|
||
2. **没有 release 锁定**——即使正式包,URL `?profile=`/`?agentid=` 仍生效,可被篡改切到调试服或换渠道身份。
|
||
|
||
本设计引入一个**一键运行模式开关**,让整套行为随 debug/release 切换:
|
||
|
||
- **自动跟随 Cocos 构建类型**(debug/预览构建=debug,release 构建=release),并保留**可选手动覆盖常量**。
|
||
- **release 完全锁死 URL 覆盖**:强制走 `prod` profile,忽略所有 URL 参数。
|
||
- **release 安全前提**:正式包为 Cocos 原生形态(jsb),渠道身份走 `window.settings`/`app_*`,不依赖 URL,故锁 URL 不影响身份传递。
|
||
- 让 `isDebugger` 真正可观测:接到 `NetClient` 的收发包日志。
|
||
|
||
### 命名/红线
|
||
|
||
内部命名现代化(`RuntimeMode`/`resolveRuntimeMode`),不改任何外部契约字符串(协议字段、原生接口名、远程配置键)。详见 config-channel spec §5。
|
||
|
||
## 1. 开关机制(新增 `config/runtime-mode.ts`)
|
||
|
||
```ts
|
||
export type RuntimeMode = 'debug' | 'release';
|
||
|
||
/**
|
||
* 手动覆盖运行模式。
|
||
* null = 自动跟随构建类型(推荐,发布前保持 null)。
|
||
* 'debug' / 'release' = 强制该模式(用于在 release 包里临时调试,或在编辑器预览里验证正式行为)。
|
||
*/
|
||
export const MODE_OVERRIDE: RuntimeMode | null = null;
|
||
|
||
/**
|
||
* 纯函数解析运行模式:override 优先,否则由是否 debug 构建决定。
|
||
* isDebugBuild 由调用方读 Cocos 全局 DEBUG 传入(见 §4 接入)。
|
||
*/
|
||
export function resolveRuntimeMode(isDebugBuild: boolean, override: RuntimeMode | null = MODE_OVERRIDE): RuntimeMode {
|
||
if (override) return override;
|
||
return isDebugBuild ? 'debug' : 'release';
|
||
}
|
||
```
|
||
|
||
- `isDebugBuild` 来源:Cocos Creator 全局 `DEBUG`(构建期宏,release 构建为 false)。接入处用 `typeof DEBUG !== 'undefined' ? DEBUG : true`(编辑器/预览缺省视为 debug)。读全局这一薄层不写单测;纯函数 `resolveRuntimeMode` 全测。
|
||
- **一键切换三种姿势**:① 不动任何东西——发 release 包自动 release、预览自动 debug;② release 包里临时调试 → `MODE_OVERRIDE='debug'`;③ 预览里验证正式行为 → `MODE_OVERRIDE='release'`。
|
||
|
||
## 2. 模式如何重塑 resolveBootstrap
|
||
|
||
`BootstrapOptions` 新增必填 `mode: RuntimeMode`;`BootstrapResult` 新增 `mode: RuntimeMode` 与 `isDebugger: boolean`。
|
||
|
||
```ts
|
||
export interface BootstrapOptions {
|
||
mode: RuntimeMode; // 新增
|
||
win: NativeHost;
|
||
search: string;
|
||
fetcher: ConfigFetcher;
|
||
isNative: boolean;
|
||
cacheBust?: () => string;
|
||
}
|
||
|
||
export interface BootstrapResult {
|
||
mode: RuntimeMode; // 新增
|
||
isDebugger: boolean; // 新增 = (mode === 'debug')
|
||
identity: ChannelIdentity;
|
||
servers: string[];
|
||
rawConfig: RemoteConfig | null;
|
||
profile: DebugProfile;
|
||
}
|
||
```
|
||
|
||
行为分支:
|
||
|
||
- **release**:
|
||
- `profile = PROFILES.prod`(强制,**忽略 `opts.search` 的 `?profile=`**)。
|
||
- 身份来源仅 `[() => BUILD_IDENTITY, nativeSettingsSource(opts.win)]`——**不挂 `queryStringSource`,不应用 `profile.identity` 调试覆盖**。即任何 URL 参数无效。
|
||
- 服务器决策照旧(prod 无 `server` → 走远程配置)。
|
||
- **debug**:维持现状。
|
||
- `profile = resolveActiveProfile(opts.search)`。
|
||
- 身份来源 `[() => BUILD_IDENTITY, runtime(isNative?native:query), () => profile.identity ?? {}, queryStringSource(search)]`。
|
||
- `isDebugger = (mode === 'debug')`,并写入结果。(release 恒 false。`DebugProfile.isDebugger` 字段保留为档位元数据,但权威开关改由 mode 决定。)
|
||
|
||
> 设计要点:release 分支根本不构造 `queryStringSource`,从源头杜绝 URL 覆盖,而不是构造后再过滤——更不易出错。
|
||
|
||
## 3. 让 debug 模式可观测:NetClient 收发包日志
|
||
|
||
当前 `isDebugger` 无人消费。本期接上,给 `NetClient` 增可选日志开关:
|
||
|
||
```ts
|
||
export interface NetClientOptions {
|
||
servers: string | string[];
|
||
transportFactory: () => Transport;
|
||
clock?: Clock;
|
||
bus?: EventBus<NetClientEvents>;
|
||
debug?: boolean; // 新增:为真时 console 打印每帧 send/recv
|
||
logger?: (...args: unknown[]) => void; // 新增(可选):注入日志函数,默认 console.log,便于测试断言
|
||
}
|
||
```
|
||
|
||
- 发送(`send`/`sendLogin`)与接收(`onMessage` 入口)时,若 `debug` 为真,调用 `logger`(默认 `console.log`)打印方向 + 帧内容(如 `[net] → <frame>` / `[net] ← <frame>`)。
|
||
- 接入:`new NetClient({ servers, transportFactory, debug: result.isDebugger })`。release 静默,debug 输出。
|
||
- 日志只读不改流程(剥离 UI,纯 console),不影响协议行为。
|
||
|
||
## 4. 接入示例(更新到指南)
|
||
|
||
```ts
|
||
import { resolveRuntimeMode } from 'db://assets/framework/config/runtime-mode';
|
||
import { resolveBootstrap } from 'db://assets/framework/config/bootstrap';
|
||
import { HttpConfigFetcher } from 'db://assets/framework/config/remote-config-fetcher';
|
||
import { NetClient } from 'db://assets/framework/net/net-client';
|
||
import { CocosWebSocketTransport } from 'db://assets/framework/net/cocos-transport';
|
||
|
||
const isDebugBuild = typeof DEBUG !== 'undefined' ? DEBUG : true; // Cocos 全局
|
||
const mode = resolveRuntimeMode(isDebugBuild); // 'debug' | 'release'
|
||
|
||
const result = await resolveBootstrap({
|
||
mode,
|
||
win: globalThis as any,
|
||
search: location.search,
|
||
fetcher: new HttpConfigFetcher(),
|
||
isNative: location.href.indexOf('index.html') > -1,
|
||
});
|
||
|
||
const net = new NetClient({
|
||
servers: result.servers,
|
||
transportFactory: () => new CocosWebSocketTransport(),
|
||
debug: result.isDebugger,
|
||
});
|
||
net.setIdentity({ ...result.identity, openid, nickname, /* ...玩家档案 */ });
|
||
net.start();
|
||
```
|
||
|
||
## 5. 测试策略(tsx + node:test)
|
||
|
||
- `framework-tests/config/runtime-mode.test.ts`:
|
||
- override 为 'debug'/'release' 时优先返回 override(无视 isDebugBuild)。
|
||
- override 为 null 时:isDebugBuild=true → 'debug';false → 'release'。
|
||
- `framework-tests/config/bootstrap.test.ts`(扩充):
|
||
- **release**:`mode:'release'` + `search:'?profile=local&agentid=999'` → `profile.name==='prod'`、`servers` 走远程(非 local 直连)、`identity.agentid===BUILD_IDENTITY.agentid`(未被 999 覆盖)、`isDebugger===false`。
|
||
- **release 原生身份仍生效**:`mode:'release'`, `isNative:true`, fake `win.settings` → `identity.agentid` 来自原生。
|
||
- **debug**:`mode:'debug'` 维持原有覆盖行为(现有用例补传 `mode:'debug'`)。
|
||
- 结果含 `mode` 与 `isDebugger` 字段。
|
||
- `framework-tests/net/net-client.test.ts`(扩充):
|
||
- `debug:true` + 注入 fake `logger` → send 与 recv 各被记录一次(含方向标记)。
|
||
- `debug:false`(或不传)→ logger 不被调用。
|
||
|
||
验收:`npm run test:framework` 全绿、`npm run typecheck:framework` exit 0。
|
||
|
||
## 6. 影响面与迁移
|
||
|
||
- `resolveBootstrap` 新增**必填** `mode`——所有现有调用点(目前仅测试与未来启动场景)需补传。现有 bootstrap 测试用例补 `mode:'debug'` 保持原语义。
|
||
- `NetClient` 新增**可选** `debug`/`logger`——默认关闭,不影响现有调用与测试。
|
||
- `DebugProfile.isDebugger` 字段保留(档位元数据/可读性),但运行期权威开关改由 `mode` 决定;`prod.isDebugger:false` 与 release 自洽。
|
||
- 更新 `docs/guides/配置与调试指南.md`:新增「一键切换 debug/release」一节、`resolveRuntimeMode` 接入、发布前清单(确认 `MODE_OVERRIDE===null`)、API 速查表补项。
|
||
|
||
## 7. 范围边界(YAGNI)
|
||
|
||
- **做**:运行模式解析(自动+手动覆盖)、release URL 锁定、`isDebugger` 接到 NetClient 收发包日志、文档更新。
|
||
- **不做**:基于模式切换资源/分包;远程下发的 `isdebugger` 参数(原工程 `get_paravalue('isdebugger')`,属后续 platform 层);日志分级/上报(仅 console 收发包)。
|