From ca3babeeb2bf1e433726a385ceb6d3d6845d5028 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 28 Jun 2026 17:59:00 +0800 Subject: [PATCH] =?UTF-8?q?docs(plan):=20debug/release=20=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E8=BF=90=E8=A1=8C=E6=A8=A1=E5=BC=8F=E5=AE=9E=E6=96=BD?= =?UTF-8?q?=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-06-28-runtime-mode.md | 561 ++++++++++++++++++ 1 file changed, 561 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-28-runtime-mode.md diff --git a/docs/superpowers/plans/2026-06-28-runtime-mode.md b/docs/superpowers/plans/2026-06-28-runtime-mode.md new file mode 100644 index 0000000..be77b2a --- /dev/null +++ b/docs/superpowers/plans/2026-06-28-runtime-mode.md @@ -0,0 +1,561 @@ +# 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) " +``` + +--- + +### 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 { + 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) " +``` + +--- + +### 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; + 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) " +``` + +--- + +### 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) " +``` + +--- + +## 验收标准(完成定义) + +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 取或。