Files
youle_cocos/docs/superpowers/plans/2026-06-28-runtime-mode.md
T
2026-06-28 17:59:00 +08:00

562 lines
23 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.
# debug/release 一键运行模式 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 给 `framework/config` 增加一个一键运行模式开关:自动跟随 Cocos 构建类型(debug/release)、可选手动覆盖;release 强制 `prod` 并完全锁死 URL 覆盖;并把 `isDebugger` 接到 `NetClient` 的收发包日志,让 debug 模式真正可观测。
**Architecture:** 新增纯函数 `resolveRuntimeMode`(构建标志 + 可选覆盖 → 'debug'|'release')。`resolveBootstrap` 新增必填 `mode`:release 分支只用 `[BUILD_IDENTITY, nativeSettings]` 身份来源且强制 `PROFILES.prod`,从源头不构造 URL 来源;debug 分支维持现状。`NetClient` 增可选 `debug`/`logger`,在统一的发送/接收切面打印帧。
**Tech Stack:** TypeScript(Cocos 3.8.8);测试 Node 20 + `tsx` 跑 `node:test`,沿用 `npm run test:framework` / `npm run typecheck:framework`(cwd `cocoscreator_projects/`)。
> 依据 spec `docs/superpowers/specs/2026-06-28-runtime-mode-design.md`。当前分支 `feat/runtime-mode`。
> 命名红线:内部命名现代化,外部契约字符串(协议字段/原生接口名/远程配置键)逐字不变。
**所有 npm 命令工作目录 `cocoscreator_projects/`;git 命令工作目录 `G:/Works/YouleGamesCocosCreator`。导入必须带 `.ts` 后缀(tsconfig `allowImportingTsExtensions`)。`git commit` 的 CRLF 警告可忽略,用 `git log --oneline -1` 确认。**
---
## 文件结构(本计划涉及)
```
cocoscreator_projects/
├─ YouleNexus/assets/framework/
│ ├─ config/runtime-mode.ts # 新增:RuntimeMode + MODE_OVERRIDE + resolveRuntimeMode
│ ├─ config/bootstrap.ts # 修改:BootstrapOptions/Result 加 mode/isDebugger + release 分支
│ └─ net/net-client.ts # 修改:NetClientOptions 加 debug/logger + 收发日志切面
├─ framework-tests/config/
│ ├─ runtime-mode.test.ts # 新增
│ └─ bootstrap.test.ts # 修改:现有 6 用例补 mode + 新增 release 用例
└─ framework-tests/net/net-client.test.ts # 修改:追加 debug 日志 2 用例
docs/guides/配置与调试指南.md # 修改:新增模式切换章节 + 更新示例/清单/速查表
```
---
### Task 1: config/runtime-mode.ts — 运行模式解析
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts`
- Create: `cocoscreator_projects/framework-tests/config/runtime-mode.test.ts`
- [ ] **Step 1: 写失败测试**
创建 `framework-tests/config/runtime-mode.test.ts`:
```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { resolveRuntimeMode } from '../../YouleNexus/assets/framework/config/runtime-mode.ts';
test('override 优先于构建类型', () => {
assert.equal(resolveRuntimeMode(true, 'release'), 'release');
assert.equal(resolveRuntimeMode(false, 'debug'), 'debug');
});
test('无 override:debug 构建 → debug', () => {
assert.equal(resolveRuntimeMode(true, null), 'debug');
});
test('无 override:release 构建 → release', () => {
assert.equal(resolveRuntimeMode(false, null), 'release');
});
```
- [ ] **Step 2: 跑测试确认失败** — `npm run test:framework` → FAIL(找不到 runtime-mode.ts)
- [ ] **Step 3: 实现 runtime-mode.ts**
创建 `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 传入(typeof DEBUG !== 'undefined' ? DEBUG : true)。
*/
export function resolveRuntimeMode(
isDebugBuild: boolean,
override: RuntimeMode | null = MODE_OVERRIDE,
): RuntimeMode {
if (override) return override;
return isDebugBuild ? 'debug' : 'release';
}
```
- [ ] **Step 4: 跑测试 + 类型检查** — `npm run test:framework`(含 runtime-mode 3 用例 PASS)+ `npm run typecheck:framework`(exit 0)
- [ ] **Step 5: Commit**
```bash
cd G:/Works/YouleGamesCocosCreator
git add cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts cocoscreator_projects/framework-tests/config/runtime-mode.test.ts
git commit -m "feat(framework): config 运行模式解析 resolveRuntimeMode(自动+手动覆盖)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 2: bootstrap.ts — 接入 mode + release 锁定
**Files:**
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts`(全文替换为下方内容)
- Modify: `cocoscreator_projects/framework-tests/config/bootstrap.test.ts`(全文替换为下方内容)
> `mode` 设为**必填**。release 分支只用 `[BUILD_IDENTITY, nativeSettings]` 身份来源、强制 `PROFILES.prod`,从源头不构造 `queryStringSource`,杜绝 URL 覆盖。
- [ ] **Step 1: 改测试(先让其失败)—— 全文替换 `framework-tests/config/bootstrap.test.ts`**
```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { resolveBootstrap } from '../../YouleNexus/assets/framework/config/bootstrap.ts';
import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts';
import type { ConfigFetcher, RemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts';
function fetcherReturning(config: RemoteConfig): ConfigFetcher & { calls: string[] } {
const calls: string[] = [];
return { calls, fetch: async (url: string) => { calls.push(url); return config; } };
}
const throwingFetcher: ConfigFetcher = {
fetch: async () => { throw new Error('net down'); },
};
const REMOTE: RemoteConfig = { data: { player_server_tcp: '5.5.5.5:5000' } };
test('debug + profile=local:用 profile.server 直连,不抓远程', async () => {
const fetcher = fetcherReturning(REMOTE);
const r = await resolveBootstrap({ mode: 'debug', win: {}, search: '?profile=local', fetcher, isNative: false });
assert.deepEqual(r.servers, ['ws://127.0.0.1:3088']);
assert.equal(r.rawConfig, null);
assert.equal(fetcher.calls.length, 0);
});
test('debug + prod + H5:抓远程配置产出 servers,identity 来自 defaults', async () => {
const fetcher = fetcherReturning(REMOTE);
const r = await resolveBootstrap({ mode: 'debug', win: {}, search: '', fetcher, isNative: false });
assert.deepEqual(r.servers, ['ws://5.5.5.5:5000']);
assert.equal(r.identity.agentid, BUILD_IDENTITY.agentid);
assert.equal(r.identity.gameid, BUILD_IDENTITY.gameid);
assert.equal(fetcher.calls.length, 1);
});
test('debug + H5 显式 URL query 覆盖身份(最高优先级)', async () => {
const fetcher = fetcherReturning(REMOTE);
const r = await resolveBootstrap({ mode: 'debug', win: {}, search: '?agentid=999&channelid=C9', fetcher, isNative: false });
assert.equal(r.identity.agentid, '999');
assert.equal(r.identity.channelid, 'C9');
});
test('debug + 原生模式:identity 来自 window.settings', async () => {
const fetcher = fetcherReturning(REMOTE);
const win = { settings: { getothername: () => 'NA', getchannelName: () => 'NC', getmarketname: () => 3 } };
const r = await resolveBootstrap({ mode: 'debug', win, search: '', fetcher, isNative: true });
assert.equal(r.identity.agentid, 'NA');
assert.equal(r.identity.channelid, 'NC');
assert.equal(r.identity.marketid, 3);
});
test('debug + 远程失败 + 有 fallbackServers → 降级使用', async () => {
const r = await resolveBootstrap({ mode: 'debug', win: {}, search: '', fetcher: throwingFetcher, isNative: false, fallbackServers: ['ws://fb:1'] });
assert.deepEqual(r.servers, ['ws://fb:1']);
});
test('debug + 远程失败 + 无 fallback → 抛 ConfigFetchError', async () => {
await assert.rejects(
resolveBootstrap({ mode: 'debug', win: {}, search: '', fetcher: throwingFetcher, isNative: false }),
/远程配置抓取失败/,
);
});
test('debug 结果含 mode/isDebugger', async () => {
const fetcher = fetcherReturning(REMOTE);
const r = await resolveBootstrap({ mode: 'debug', win: {}, search: '', fetcher, isNative: false });
assert.equal(r.mode, 'debug');
assert.equal(r.isDebugger, true);
});
test('release:忽略所有 URL 覆盖,强制 prod,identity 不被 URL 改', async () => {
const fetcher = fetcherReturning(REMOTE);
const r = await resolveBootstrap({ mode: 'release', win: {}, search: '?profile=local&agentid=999', fetcher, isNative: false });
assert.equal(r.profile.name, 'prod');
assert.deepEqual(r.servers, ['ws://5.5.5.5:5000']); // 走远程,不是 local 直连
assert.equal(r.identity.agentid, BUILD_IDENTITY.agentid); // 999 无效
assert.equal(r.isDebugger, false);
assert.equal(r.mode, 'release');
assert.equal(fetcher.calls.length, 1);
});
test('release:原生身份仍生效(URL 的 999 无效)', async () => {
const fetcher = fetcherReturning(REMOTE);
const win = { settings: { getothername: () => 'NA', getchannelName: () => 'NC', getmarketname: () => 3 } };
const r = await resolveBootstrap({ mode: 'release', win, search: '?agentid=999', fetcher, isNative: true });
assert.equal(r.identity.agentid, 'NA');
assert.equal(r.identity.channelid, 'NC');
});
```
- [ ] **Step 2: 跑测试确认失败** — `npm run test:framework` → FAIL(`mode` 类型缺失 / release 行为未实现 / 结果缺 mode·isDebugger)
- [ ] **Step 3: 实现 —— 全文替换 `YouleNexus/assets/framework/config/bootstrap.ts`**
```ts
import type { ChannelIdentity } from './identity.ts';
import { resolveIdentity } from './identity.ts';
import { BUILD_IDENTITY } from './sources/defaults.ts';
import { queryStringSource } from './sources/query-string.ts';
import { nativeSettingsSource, type NativeHost } from './sources/native-settings.ts';
import { resolveActiveProfile, PROFILES, DEFAULT_GAMESERVER, type DebugProfile } from './profiles.ts';
import { resolveServers, type ConfigFetcher, type RemoteConfig, ConfigFetchError } from './remote-config.ts';
import type { RuntimeMode } from './runtime-mode.ts';
export interface BootstrapOptions {
mode: RuntimeMode; // 运行模式(由 resolveRuntimeMode 得到)
win: NativeHost; // 原生注入宿主(≈ window)
search: string; // location.search
fetcher: ConfigFetcher; // 远程配置抓取
isNative: boolean; // 环境判定(URL 含 index.html → true,由调用方算)
fallbackServers?: string[]; // 远程失败/无地址时降级
cacheBust?: () => string; // 防缓存串注入(默认 Date.now)
}
export interface BootstrapResult {
mode: RuntimeMode;
isDebugger: boolean; // = (mode === 'debug')
identity: ChannelIdentity;
servers: string[];
rawConfig: RemoteConfig | null;
profile: DebugProfile;
}
/** 启动编排:按运行模式解析身份 + 决定服务器,产出可喂 NetClient 的结果。 */
export async function resolveBootstrap(opts: BootstrapOptions): Promise<BootstrapResult> {
const isDebug = opts.mode === 'debug';
// 模式决定 profile 与身份来源:release 强制 prod,且身份只走原生(绝不读 URL)。
const profile = isDebug ? resolveActiveProfile(opts.search) : PROFILES.prod;
const identity = isDebug
? resolveIdentity([
() => BUILD_IDENTITY,
opts.isNative ? nativeSettingsSource(opts.win) : queryStringSource(opts.search),
() => profile.identity ?? {},
queryStringSource(opts.search), // 显式 URL 覆盖(最高)
])
: resolveIdentity([
() => BUILD_IDENTITY,
nativeSettingsSource(opts.win), // release:身份只取原生,URL 一律无效
]);
const resultBase = { mode: opts.mode, isDebugger: isDebug, identity, profile };
// 服务器决策:profile 显式 server 直连捷径(仅 debug 的 local/lab 等档位会有)
if (profile.server) {
return { ...resultBase, servers: [profile.server], rawConfig: null };
}
// 否则远程配置
const gameserver = profile.gameserver ?? DEFAULT_GAMESERVER;
const bust = opts.cacheBust ? opts.cacheBust() : String(Date.now());
const url = gameserver + '?' + bust; // 防缓存,等价原 min_timestamp/ifast_random
let config: RemoteConfig;
try {
config = await opts.fetcher.fetch(url);
} catch {
if (opts.fallbackServers && opts.fallbackServers.length) {
return { ...resultBase, servers: opts.fallbackServers, rawConfig: null };
}
throw new ConfigFetchError(`远程配置抓取失败: ${url}`);
}
let servers = resolveServers(config, identity);
if (!servers.length && opts.fallbackServers && opts.fallbackServers.length) {
servers = opts.fallbackServers;
}
return { ...resultBase, servers, rawConfig: config };
}
```
- [ ] **Step 4: 跑测试确认通过** — `npm run test:framework`(bootstrap 9 用例全 PASS)+ `npm run typecheck:framework`(exit 0)
- [ ] **Step 5: Commit**
```bash
cd G:/Works/YouleGamesCocosCreator
git add cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts cocoscreator_projects/framework-tests/config/bootstrap.test.ts
git commit -m "feat(framework): config resolveBootstrap 接入 mode + release 锁死 URL 覆盖
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 3: net-client.ts — debug 收发包日志
**Files:**
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/net/net-client.ts`
- Modify: `cocoscreator_projects/framework-tests/net/net-client.test.ts`(在文件末尾追加 2 个用例)
> 给 `NetClient` 加可选 `debug`/`logger`;统一经 `sendFrame()` 发送切面与 `onMessage` 入口打印帧。默认关闭,不影响现有行为/测试。
- [ ] **Step 1: 追加失败测试 —— 在 `framework-tests/net/net-client.test.ts` 末尾追加**
```ts
test('debug=true 记录 send 与 recv(注入 logger)', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const logs: unknown[][] = [];
const client = new NetClient({
servers: 'ws://a',
transportFactory: () => t,
clock,
debug: true,
logger: (...a: unknown[]) => logs.push(a),
});
client.setIdentity(IDENTITY);
client.start();
await t.flush();
assert.ok(logs.some((l) => l[0] === '[net] →')); // 发出 player_login 被记录
t.serverPush(JSON.stringify({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) }));
await t.flush();
assert.ok(logs.some((l) => l[0] === '[net] ←')); // 收包被记录
});
test('debug 默认关闭:logger 不被调用', async () => {
const t = new FakeTransport();
const { clock } = fakeClock();
const logs: unknown[][] = [];
const client = new NetClient({
servers: 'ws://a',
transportFactory: () => t,
clock,
logger: (...a: unknown[]) => logs.push(a),
});
client.setIdentity(IDENTITY);
client.start();
await t.flush();
t.serverPush(JSON.stringify({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) }));
await t.flush();
assert.equal(logs.length, 0);
});
```
- [ ] **Step 2: 跑测试确认失败** — `npm run test:framework` → FAIL(`debug`/`logger` 选项不存在,logger 未被调用)
- [ ] **Step 3: 改 `net-client.ts` —— 共 5 处编辑**
3a. `NetClientOptions` 接口加两个可选字段(替换整个接口):
```ts
export interface NetClientOptions {
servers: string | string[];
transportFactory: () => Transport;
clock?: Clock;
bus?: EventBus<NetClientEvents>;
debug?: boolean; // 为真时打印每帧 send/recv
logger?: (...args: unknown[]) => void; // 日志函数,默认 console.log(便于测试注入)
}
```
3b. 在字段声明区(`private stopped = false;` 这一行后面)新增两个字段:
```ts
private stopped = false;
private debug: boolean;
private logger: (...args: unknown[]) => void;
```
3c. 构造函数体末尾(`this.watchdog = ...` 那一行后)新增两行:
```ts
this.watchdog = new HeartbeatWatchdog(RECV_TIMEOUT_MS, () => this.onRecvTimeout(), this.clock);
this.debug = opts.debug ?? false;
this.logger = opts.logger ?? console.log;
```
3d. 新增私有发送切面,并把 `send()` 改为走它。把现有:
```ts
/** 发业务包(单层信封)。 */
send(route: string, rpc: string, data: unknown): void {
this.transport?.send(encodeOutbound(route, rpc, data));
}
```
替换为:
```ts
/** 发业务包(单层信封)。 */
send(route: string, rpc: string, data: unknown): void {
this.sendFrame(encodeOutbound(route, rpc, data));
}
/** 统一发送切面:debug 时打印出站帧。 */
private sendFrame(frame: string): void {
if (this.debug) this.logger('[net] →', frame);
this.transport?.send(frame);
}
```
3e. `sendLogin()` 内的直接发送改走切面。把:
```ts
this.isSendLoginState = true;
this.transport?.send(encodeOutbound(Route.agent, 'player_login', this.identity));
```
替换为:
```ts
this.isSendLoginState = true;
this.sendFrame(encodeOutbound(Route.agent, 'player_login', this.identity));
```
3f. `onMessage(frame)` 入口打印入站帧。把:
```ts
private onMessage(frame: string): void {
const r = decodeFrame(frame);
```
替换为:
```ts
private onMessage(frame: string): void {
if (this.debug) this.logger('[net] ←', frame);
const r = decodeFrame(frame);
```
- [ ] **Step 4: 跑测试确认通过** — `npm run test:framework`(net-client 含新 2 用例全 PASS,原有用例不受影响)+ `npm run typecheck:framework`(exit 0)
- [ ] **Step 5: Commit**
```bash
cd G:/Works/YouleGamesCocosCreator
git add cocoscreator_projects/YouleNexus/assets/framework/net/net-client.ts cocoscreator_projects/framework-tests/net/net-client.test.ts
git commit -m "feat(framework): net NetClient 可选 debug 收发包日志
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 4: 文档 — 更新配置与调试指南
**Files:**
- Modify: `docs/guides/配置与调试指南.md`
> 纯文档任务,无测试。改完跑一次全量回归确认未误改代码。所有改动在 `docs/` 下。
- [ ] **Step 1: 新增「一键切换 debug/release 模式」小节**
在 `## 4. 调试 Profiles` 这一节**之前**(即紧接 `## 3. 渠道身份 ChannelIdentity` 小节末尾之后)插入新的一节,并把后续小节序号顺延(4→5、5→6…;目录也同步顺延)。新节内容:
```markdown
## 4. 一键切换 debug / release 模式
运行模式是比 profile 更高一层的总开关,由 `config/runtime-mode.ts` 提供。
| 模式 | 怎么进入 | profile | URL 覆盖(?profile=/?agentid=…) | 身份来源 | 收发包日志 |
| --- | --- | --- | --- | --- | --- |
| **debug** | debug/预览构建自动进入;或 `MODE_OVERRIDE='debug'` | `?profile=` 可切(默认 prod) | **全部生效** | 原生或 URL(按 `isNative`) | 开(`isDebugger=true`) |
| **release** | release 构建自动进入;或 `MODE_OVERRIDE='release'` | **强制 prod** | **全部忽略** | **只走原生 `window.settings`** | 关 |
### 三种切换姿势
1. **什么都不动**:发 release 包自动 release,编辑器预览自动 debug。模式由 Cocos 全局 `DEBUG` 决定。
2. **release 包里临时调试**:把 `config/runtime-mode.ts` 的 `MODE_OVERRIDE` 设为 `'debug'`。
3. **预览里验证正式行为**:把 `MODE_OVERRIDE` 设为 `'release'`。
> release 之所以能放心锁死 URL:正式包是 Cocos 原生(jsb),渠道身份走 `window.settings`/`app_*`,本就不依赖 URL。
### 接入
```ts
import { resolveRuntimeMode } from 'db://assets/framework/config/runtime-mode';
const isDebugBuild = typeof DEBUG !== 'undefined' ? DEBUG : true; // Cocos 全局
const mode = resolveRuntimeMode(isDebugBuild); // 'debug' | 'release'
// 传给 resolveBootstrap({ mode, ... }),并把 result.isDebugger 传给 NetClient 的 debug 选项
```
```
- [ ] **Step 2: 更新 `resolveBootstrap` 接入示例**
找到原「启动编排 resolveBootstrap 与接入 NetClient」一节里的接入示例代码块,做两处改动:①`resolveBootstrap({...})` 的入参补上 `mode`;②`new NetClient({...})` 补上 `debug`。改后关键两处应为:
```ts
const isDebugBuild = typeof DEBUG !== 'undefined' ? DEBUG : true;
const mode = resolveRuntimeMode(isDebugBuild);
const result = await resolveBootstrap({
mode,
win: globalThis as any,
search: location.search,
fetcher: new HttpConfigFetcher(),
isNative: href.indexOf('index.html') > -1,
fallbackServers: [],
});
const net = new NetClient({
servers: result.servers,
transportFactory: () => new CocosWebSocketTransport(),
debug: result.isDebugger,
});
```
并在该示例的 import 区补一行 `import { resolveRuntimeMode } from 'db://assets/framework/config/runtime-mode';`。
- [ ] **Step 3: 更新发布前检查清单**
在「发布前检查清单」一节的清单**最前面**加一项:
```markdown
- [ ] `config/runtime-mode.ts` 的 `MODE_OVERRIDE` 为 `null`(发布走自动模式,由 release 构建决定)。
```
- [ ] **Step 4: 更新 API 速查表**
在 API 速查表里,于 `resolveBootstrap` 行**之上**新增一行:
```markdown
| `resolveRuntimeMode` | `config/runtime-mode.ts` | `(isDebugBuild, override?) => 'debug' | 'release'` 运行模式解析 |
```
并在 `resolveBootstrap` 行的说明里追加「(入参含 `mode`,结果含 `mode`/`isDebugger`)」。
- [ ] **Step 5: 全量回归 + Commit**
Run: `npm run test:framework`(应仍全绿,文档改动不影响)
Run: `npm run typecheck:framework`(exit 0)
```bash
cd G:/Works/YouleGamesCocosCreator
git add docs/guides/配置与调试指南.md
git commit -m "docs(guide): 配置与调试指南补充 debug/release 一键模式
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## 验收标准(完成定义)
1. `npm run test:framework` 全绿;`npm run typecheck:framework` exit 0。
2. `resolveRuntimeMode`:override 优先、否则跟随构建标志。
3. `resolveBootstrap` 接入必填 `mode`:release 强制 prod、忽略所有 URL 覆盖、身份只走原生、`isDebugger=false`;debug 维持原有全部覆盖行为;结果含 `mode`/`isDebugger`。
4. `NetClient` 可选 `debug`/`logger`:debug 时记录每帧 send/recv,默认关闭不影响现有行为。
5. `docs/guides/配置与调试指南.md` 含「一键切换 debug/release」章节、更新后的接入示例、发布前清单与 API 速查表。
## 后续衔接
- 启动场景(platform/Plan 3)接入:`resolveRuntimeMode` → `resolveBootstrap({mode})` → `NetClient({debug})`。
- 远程下发 `isdebugger`(原 `get_paravalue('isdebugger')`)若需要,归 platform 层,可与本地 mode 取或。