diff --git a/docs/superpowers/specs/2026-06-28-runtime-mode-design.md b/docs/superpowers/specs/2026-06-28-runtime-mode-design.md new file mode 100644 index 0000000..69c7b7a --- /dev/null +++ b/docs/superpowers/specs/2026-06-28-runtime-mode-design.md @@ -0,0 +1,161 @@ +# 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; + fallbackServers?: string[]; + 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; + debug?: boolean; // 新增:为真时 console 打印每帧 send/recv + logger?: (...args: unknown[]) => void; // 新增(可选):注入日志函数,默认 console.log,便于测试断言 +} +``` + +- 发送(`send`/`sendLogin`)与接收(`onMessage` 入口)时,若 `debug` 为真,调用 `logger`(默认 `console.log`)打印方向 + 帧内容(如 `[net] → ` / `[net] ← `)。 +- 接入:`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 收发包)。