refactor(framework): 移除 resolveBootstrap 下游兜底,立第二准则

- CLAUDE.md 新增「第二准则:数据源权威、唯一,下游不兜底」
- bootstrap 删除 fallbackServers 选项与 ?? DEFAULT_GAMESERVER 猜默认
- 无 server/gameserver、远程失败、解析不出地址 → 一律抛 ConfigFetchError 显式暴露
- 同步 guide + 两篇 spec(删 fallback/降级措辞)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 18:59:15 +08:00
co-authored by Claude Opus 4.8
parent 39bb964a49
commit 51a4ee57ac
6 changed files with 42 additions and 29 deletions
@@ -3,7 +3,7 @@ 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 { resolveActiveProfile, PROFILES, type DebugProfile } from './profiles.ts';
import { resolveServers, type ConfigFetcher, type RemoteConfig, ConfigFetchError } from './remote-config.ts';
import type { RuntimeMode } from './runtime-mode.ts';
@@ -13,7 +13,6 @@ export interface BootstrapOptions {
search: string; // location.search
fetcher: ConfigFetcher; // 远程配置抓取
isNative: boolean; // 环境判定(URL 含 index.html → true,由调用方算)
fallbackServers?: string[]; // 远程失败/无地址时降级
cacheBust?: () => string; // 防缓存串注入(默认 Date.now)
}
@@ -26,7 +25,11 @@ export interface BootstrapResult {
profile: DebugProfile;
}
/** 启动编排:按运行模式解析身份 + 决定服务器,产出可喂 NetClient 的结果。 */
/**
* 启动编排:按运行模式解析身份 + 决定服务器,产出可喂 NetClient 的结果。
* 数据源权威唯一、下游不兜底(CLAUDE.md 第二准则):服务器地址只来自 profile.server(直连)
* 或远程配置解析;取不到一律显式抛错暴露,不猜默认、不降级。
*/
export async function resolveBootstrap(opts: BootstrapOptions): Promise<BootstrapResult> {
const isDebug = opts.mode === 'debug';
@@ -46,29 +49,29 @@ export async function resolveBootstrap(opts: BootstrapOptions): Promise<Bootstra
const resultBase = { mode: opts.mode, isDebugger: isDebug, identity, profile };
// 服务器决策:profile 显式 server 直连捷径(仅 debug 的 local/lab 等档位会有)
// 服务器决策:profile 显式 server 直连捷径
if (profile.server) {
return { ...resultBase, servers: [profile.server], rawConfig: null };
}
// 否则远程配置
const gameserver = profile.gameserver ?? DEFAULT_GAMESERVER;
// profile 是服务器地址的权威来源:无 server 则必须有 gameserver,缺则配置错误,显式暴露。
if (!profile.gameserver) {
throw new ConfigFetchError(`profile '${profile.name}' 未配置 server 或 gameserver`);
}
const bust = opts.cacheBust ? opts.cacheBust() : String(Date.now());
const url = gameserver + '?' + bust; // 防缓存,等价原 min_timestamp/ifast_random
const url = profile.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;
const servers = resolveServers(config, identity);
if (!servers.length) {
throw new ConfigFetchError(`远程配置未给出可用服务器地址: ${url}`);
}
return { ...resultBase, servers, rawConfig: config };
}
@@ -47,18 +47,21 @@ test('debug + 原生模式:identity 来自 window.settings', async () => {
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 () => {
test('远程抓取失败 → 抛 ConfigFetchError(不兜底)', async () => {
await assert.rejects(
resolveBootstrap({ mode: 'debug', win: {}, search: '', fetcher: throwingFetcher, isNative: false }),
/远程配置抓取失败/,
);
});
test('远程配置未给出可用服务器地址 → 抛错(不兜底)', async () => {
const emptyFetcher: ConfigFetcher = { fetch: async () => ({ data: {} }) };
await assert.rejects(
resolveBootstrap({ mode: 'debug', win: {}, search: '', fetcher: emptyFetcher, isNative: false }),
/未给出可用服务器/,
);
});
test('debug 结果含 mode/isDebugger', async () => {
const fetcher = fetcherReturning(REMOTE);
const r = await resolveBootstrap({ mode: 'debug', win: {}, search: '', fetcher, isNative: false });