# 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 取或。