feat(framework): config resolveBootstrap 接入 mode + release 锁死 URL 覆盖
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,12 @@ import { resolveIdentity } from './identity.ts';
|
|||||||
import { BUILD_IDENTITY } from './sources/defaults.ts';
|
import { BUILD_IDENTITY } from './sources/defaults.ts';
|
||||||
import { queryStringSource } from './sources/query-string.ts';
|
import { queryStringSource } from './sources/query-string.ts';
|
||||||
import { nativeSettingsSource, type NativeHost } from './sources/native-settings.ts';
|
import { nativeSettingsSource, type NativeHost } from './sources/native-settings.ts';
|
||||||
import { resolveActiveProfile, DEFAULT_GAMESERVER, type DebugProfile } from './profiles.ts';
|
import { resolveActiveProfile, PROFILES, DEFAULT_GAMESERVER, type DebugProfile } from './profiles.ts';
|
||||||
import { resolveServers, type ConfigFetcher, type RemoteConfig, ConfigFetchError } from './remote-config.ts';
|
import { resolveServers, type ConfigFetcher, type RemoteConfig, ConfigFetchError } from './remote-config.ts';
|
||||||
|
import type { RuntimeMode } from './runtime-mode.ts';
|
||||||
|
|
||||||
export interface BootstrapOptions {
|
export interface BootstrapOptions {
|
||||||
|
mode: RuntimeMode; // 运行模式(由 resolveRuntimeMode 得到)
|
||||||
win: NativeHost; // 原生注入宿主(≈ window)
|
win: NativeHost; // 原生注入宿主(≈ window)
|
||||||
search: string; // location.search
|
search: string; // location.search
|
||||||
fetcher: ConfigFetcher; // 远程配置抓取
|
fetcher: ConfigFetcher; // 远程配置抓取
|
||||||
@@ -16,39 +18,50 @@ export interface BootstrapOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BootstrapResult {
|
export interface BootstrapResult {
|
||||||
|
mode: RuntimeMode;
|
||||||
|
isDebugger: boolean; // = (mode === 'debug')
|
||||||
identity: ChannelIdentity;
|
identity: ChannelIdentity;
|
||||||
servers: string[];
|
servers: string[];
|
||||||
rawConfig: RemoteConfig | null;
|
rawConfig: RemoteConfig | null;
|
||||||
profile: DebugProfile;
|
profile: DebugProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 启动编排:解析身份 + 决定服务器,产出可喂 NetClient 的结果。 */
|
/** 启动编排:按运行模式解析身份 + 决定服务器,产出可喂 NetClient 的结果。 */
|
||||||
export async function resolveBootstrap(opts: BootstrapOptions): Promise<BootstrapResult> {
|
export async function resolveBootstrap(opts: BootstrapOptions): Promise<BootstrapResult> {
|
||||||
const profile = resolveActiveProfile(opts.search);
|
const isDebug = opts.mode === 'debug';
|
||||||
const runtime = opts.isNative ? nativeSettingsSource(opts.win) : queryStringSource(opts.search);
|
|
||||||
const identity = resolveIdentity([
|
// 模式决定 profile 与身份来源:release 强制 prod,且身份只走原生(绝不读 URL)。
|
||||||
|
const profile = isDebug ? resolveActiveProfile(opts.search) : PROFILES.prod;
|
||||||
|
const identity = isDebug
|
||||||
|
? resolveIdentity([
|
||||||
() => BUILD_IDENTITY,
|
() => BUILD_IDENTITY,
|
||||||
runtime,
|
opts.isNative ? nativeSettingsSource(opts.win) : queryStringSource(opts.search),
|
||||||
() => profile.identity ?? {},
|
() => profile.identity ?? {},
|
||||||
queryStringSource(opts.search), // 显式 URL 覆盖(最高)
|
queryStringSource(opts.search), // 显式 URL 覆盖(最高)
|
||||||
|
])
|
||||||
|
: resolveIdentity([
|
||||||
|
() => BUILD_IDENTITY,
|
||||||
|
nativeSettingsSource(opts.win), // release:身份只取原生,URL 一律无效
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 服务器决策:profile 显式 server 直连捷径
|
const resultBase = { mode: opts.mode, isDebugger: isDebug, identity, profile };
|
||||||
|
|
||||||
|
// 服务器决策:profile 显式 server 直连捷径(仅 debug 的 local/lab 等档位会有)
|
||||||
if (profile.server) {
|
if (profile.server) {
|
||||||
return { identity, servers: [profile.server], rawConfig: null, profile };
|
return { ...resultBase, servers: [profile.server], rawConfig: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 否则远程配置
|
// 否则远程配置
|
||||||
const base = profile.gameserver ?? DEFAULT_GAMESERVER;
|
const gameserver = profile.gameserver ?? DEFAULT_GAMESERVER;
|
||||||
const bust = opts.cacheBust ? opts.cacheBust() : String(Date.now());
|
const bust = opts.cacheBust ? opts.cacheBust() : String(Date.now());
|
||||||
const url = base + '?' + bust; // 防缓存,等价原 min_timestamp/ifast_random
|
const url = gameserver + '?' + bust; // 防缓存,等价原 min_timestamp/ifast_random
|
||||||
|
|
||||||
let config: RemoteConfig;
|
let config: RemoteConfig;
|
||||||
try {
|
try {
|
||||||
config = await opts.fetcher.fetch(url);
|
config = await opts.fetcher.fetch(url);
|
||||||
} catch {
|
} catch {
|
||||||
if (opts.fallbackServers && opts.fallbackServers.length) {
|
if (opts.fallbackServers && opts.fallbackServers.length) {
|
||||||
return { identity, servers: opts.fallbackServers, rawConfig: null, profile };
|
return { ...resultBase, servers: opts.fallbackServers, rawConfig: null };
|
||||||
}
|
}
|
||||||
throw new ConfigFetchError(`远程配置抓取失败: ${url}`);
|
throw new ConfigFetchError(`远程配置抓取失败: ${url}`);
|
||||||
}
|
}
|
||||||
@@ -57,5 +70,5 @@ export async function resolveBootstrap(opts: BootstrapOptions): Promise<Bootstra
|
|||||||
if (!servers.length && opts.fallbackServers && opts.fallbackServers.length) {
|
if (!servers.length && opts.fallbackServers && opts.fallbackServers.length) {
|
||||||
servers = opts.fallbackServers;
|
servers = opts.fallbackServers;
|
||||||
}
|
}
|
||||||
return { identity, servers, rawConfig: config, profile };
|
return { ...resultBase, servers, rawConfig: config };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,47 +14,73 @@ const throwingFetcher: ConfigFetcher = {
|
|||||||
|
|
||||||
const REMOTE: RemoteConfig = { data: { player_server_tcp: '5.5.5.5:5000' } };
|
const REMOTE: RemoteConfig = { data: { player_server_tcp: '5.5.5.5:5000' } };
|
||||||
|
|
||||||
test('profile=local:用 profile.server 直连,不抓远程', async () => {
|
test('debug + profile=local:用 profile.server 直连,不抓远程', async () => {
|
||||||
const fetcher = fetcherReturning(REMOTE);
|
const fetcher = fetcherReturning(REMOTE);
|
||||||
const r = await resolveBootstrap({ win: {}, search: '?profile=local', fetcher, isNative: false });
|
const r = await resolveBootstrap({ mode: 'debug', win: {}, search: '?profile=local', fetcher, isNative: false });
|
||||||
assert.deepEqual(r.servers, ['ws://127.0.0.1:3088']);
|
assert.deepEqual(r.servers, ['ws://127.0.0.1:3088']);
|
||||||
assert.equal(r.rawConfig, null);
|
assert.equal(r.rawConfig, null);
|
||||||
assert.equal(fetcher.calls.length, 0);
|
assert.equal(fetcher.calls.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prod + H5:抓远程配置产出 servers,identity 来自 defaults', async () => {
|
test('debug + prod + H5:抓远程配置产出 servers,identity 来自 defaults', async () => {
|
||||||
const fetcher = fetcherReturning(REMOTE);
|
const fetcher = fetcherReturning(REMOTE);
|
||||||
const r = await resolveBootstrap({ win: {}, search: '', fetcher, isNative: false });
|
const r = await resolveBootstrap({ mode: 'debug', win: {}, search: '', fetcher, isNative: false });
|
||||||
assert.deepEqual(r.servers, ['ws://5.5.5.5:5000']);
|
assert.deepEqual(r.servers, ['ws://5.5.5.5:5000']);
|
||||||
assert.equal(r.identity.agentid, BUILD_IDENTITY.agentid);
|
assert.equal(r.identity.agentid, BUILD_IDENTITY.agentid);
|
||||||
assert.equal(r.identity.gameid, BUILD_IDENTITY.gameid);
|
assert.equal(r.identity.gameid, BUILD_IDENTITY.gameid);
|
||||||
assert.equal(fetcher.calls.length, 1);
|
assert.equal(fetcher.calls.length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('H5 显式 URL query 覆盖身份(最高优先级)', async () => {
|
test('debug + H5 显式 URL query 覆盖身份(最高优先级)', async () => {
|
||||||
const fetcher = fetcherReturning(REMOTE);
|
const fetcher = fetcherReturning(REMOTE);
|
||||||
const r = await resolveBootstrap({ win: {}, search: '?agentid=999&channelid=C9', fetcher, isNative: false });
|
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.agentid, '999');
|
||||||
assert.equal(r.identity.channelid, 'C9');
|
assert.equal(r.identity.channelid, 'C9');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('原生模式:identity 来自 window.settings', async () => {
|
test('debug + 原生模式:identity 来自 window.settings', async () => {
|
||||||
const fetcher = fetcherReturning(REMOTE);
|
const fetcher = fetcherReturning(REMOTE);
|
||||||
const win = { settings: { getothername: () => 'NA', getchannelName: () => 'NC', getmarketname: () => 3 } };
|
const win = { settings: { getothername: () => 'NA', getchannelName: () => 'NC', getmarketname: () => 3 } };
|
||||||
const r = await resolveBootstrap({ win, search: '', fetcher, isNative: true });
|
const r = await resolveBootstrap({ mode: 'debug', win, search: '', fetcher, isNative: true });
|
||||||
assert.equal(r.identity.agentid, 'NA');
|
assert.equal(r.identity.agentid, 'NA');
|
||||||
assert.equal(r.identity.channelid, 'NC');
|
assert.equal(r.identity.channelid, 'NC');
|
||||||
assert.equal(r.identity.marketid, 3);
|
assert.equal(r.identity.marketid, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('远程失败 + 有 fallbackServers → 降级使用', async () => {
|
test('debug + 远程失败 + 有 fallbackServers → 降级使用', async () => {
|
||||||
const r = await resolveBootstrap({ win: {}, search: '', fetcher: throwingFetcher, isNative: false, fallbackServers: ['ws://fb:1'] });
|
const r = await resolveBootstrap({ mode: 'debug', win: {}, search: '', fetcher: throwingFetcher, isNative: false, fallbackServers: ['ws://fb:1'] });
|
||||||
assert.deepEqual(r.servers, ['ws://fb:1']);
|
assert.deepEqual(r.servers, ['ws://fb:1']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('远程失败 + 无 fallback → 抛 ConfigFetchError', async () => {
|
test('debug + 远程失败 + 无 fallback → 抛 ConfigFetchError', async () => {
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
resolveBootstrap({ win: {}, search: '', fetcher: throwingFetcher, isNative: false }),
|
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');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user