fix(config): restore legacy gameserver and urlserver contract

This commit is contained in:
2026-09-05 01:25:50 +08:00
parent f25211c64e
commit d1ad63983b
16 changed files with 622 additions and 383 deletions
@@ -1,82 +1,52 @@
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, type DebugProfile, type DebugAccount } from './profiles.ts';
import { resolveServers, type ConfigFetcher, type RemoteConfig, ConfigFetchError } from './remote-config.ts';
import type { RuntimeMode } from './runtime-mode.ts';
import type { DebugAccount, DebugProfile } from './profiles.ts';
import { PROFILES, resolveActiveProfile } from './profiles.ts';
import {
resolveRuntimeConfig,
type HostKind,
type ResolveRuntimeConfigOptions,
type RuntimeConfig,
} from './runtime-config.ts';
export interface BootstrapOptions {
mode: RuntimeMode; // 运行模式(由 resolveRuntimeMode 得到)
win: NativeHost; // 原生注入宿主(≈ window)
search: string; // location.search
fetcher: ConfigFetcher; // 远程配置抓取
isNative: boolean; // 环境判定(URL 含 index.html → true,由调用方算)
cacheBust?: () => string; // 防缓存串注入(默认 Date.now)
}
type RuntimeOptionsWithoutHost = Omit<ResolveRuntimeConfigOptions, 'hostKind'>;
export interface BootstrapResult {
mode: RuntimeMode;
isDebugger: boolean; // = (mode === 'debug')
identity: ChannelIdentity;
account?: DebugAccount; // 调试登录账号(仅 debug 模式有值;release 恒 undefined)
servers: string[];
rawConfig: RemoteConfig | null;
profile: DebugProfile;
}
/** 新调用方显式给 hostKind;isNative 分支仅保留现有 LoginFlow 的兼容入口。 */
export type BootstrapOptions = RuntimeOptionsWithoutHost & (
| { readonly hostKind: HostKind; readonly isNative?: never }
| { readonly hostKind?: never; readonly isNative: boolean }
);
/**
* 启动编排:按运行模式解析身份 + 决定服务器,产出可喂 NetClient 的结果。
* 数据源权威唯一、下游不兜底(CLAUDE.md 第二准则):服务器地址只来自 profile.server(直连)
* 或远程配置解析;取不到一律显式抛错暴露,不猜默认、不降级。
* 旧 StartupOrchestrator 的过渡外观。严格的新代码使用 RuntimeConfig;这里保留
* 可写 servers 类型以及可选的新判别字段,直到旧启动编排迁移完成。
*/
export async function resolveBootstrap(opts: BootstrapOptions): Promise<BootstrapResult> {
const isDebug = opts.mode === 'debug';
export type BootstrapResult = Omit<RuntimeConfig, 'servers' | 'source' | 'gameserver'> & {
readonly servers: string[];
readonly source?: RuntimeConfig['source'];
readonly gameserver?: RuntimeConfig['gameserver'];
readonly account?: DebugAccount;
readonly profile: Pick<DebugProfile, 'name'>;
};
// 模式决定 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 一律无效
]);
function bootstrapHostKind(options: BootstrapOptions): HostKind {
if (options.hostKind) return options.hostKind;
return options.isNative ? 'native-settings' : 'h5';
}
// 调试账号只在 debug 模式从 profile 取;release 不读调试配置账号。
const account = isDebug ? profile.account : undefined;
const resultBase = { mode: opts.mode, isDebugger: isDebug, identity, account, profile };
// 服务器决策:profile 显式 server 直连捷径
if (profile.server) {
return { ...resultBase, servers: [profile.server], rawConfig: null };
}
// 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 = profile.gameserver + '?' + bust; // 防缓存,等价原 min_timestamp/ifast_random
let config: RemoteConfig;
try {
config = await opts.fetcher.fetch(url);
} catch {
throw new ConfigFetchError(`远程配置抓取失败: ${url}`);
}
const resolved = resolveServers(config, identity);
// debug 可把连接地址显式覆盖为本地测试服(仍走完整远程配置链路);release 一律用远程解析结果。
const servers = isDebug && profile.serverOverride ? [profile.serverOverride] : resolved;
if (!servers.length) {
throw new ConfigFetchError(`远程配置未给出可用服务器地址: ${url}`);
}
return { ...resultBase, servers, rawConfig: config };
/** 兼容启动编排外观;所有运行契约均由唯一的 RuntimeConfig 边界产出。 */
export async function resolveBootstrap(options: BootstrapOptions): Promise<BootstrapResult> {
const profile = options.mode === 'release' ? PROFILES.prod! : resolveActiveProfile(options.search);
const runtime = await resolveRuntimeConfig({
mode: options.mode,
hostKind: bootstrapHostKind(options),
win: options.win,
search: options.search,
fetcher: options.fetcher,
cacheBust: options.cacheBust,
});
return Object.freeze({
...runtime,
servers: [...runtime.servers],
account: profile.account,
profile,
});
}
@@ -14,9 +14,29 @@ function hasValue(v: unknown): boolean {
return v !== undefined && v !== null && v !== '';
}
function validateIdentity(identity: Partial<ChannelIdentity>): ChannelIdentity {
const missing = (['agentid', 'channelid', 'gameid', 'marketid', 'version'] as const)
.filter((key) => !hasValue(identity[key]));
if (missing.length > 0) {
throw new Error(`Invalid identity: missing ${missing.join(', ')}`);
}
for (const key of ['agentid', 'channelid', 'marketid'] as const) {
if (typeof identity[key] !== 'string' && typeof identity[key] !== 'number') {
throw new Error(`Invalid identity: ${key} type`);
}
}
if (typeof identity.gameid !== 'string') {
throw new Error('Invalid identity: gameid type');
}
if (typeof identity.version !== 'number' || !Number.isFinite(identity.version)) {
throw new Error('Invalid identity: version type');
}
return identity as ChannelIdentity;
}
/**
* 按数组顺序合并身份,后者覆盖前者(仅覆盖有效值字段)。
* 约定首个 source 为完整 defaults,故返回视为完整 ChannelIdentity。
* 合并后逐字段校验;来源缺失必须在边界显式暴露,禁止不完整类型断言。
*/
export function resolveIdentity(sources: IdentitySource[]): ChannelIdentity {
const out: Partial<ChannelIdentity> = {};
@@ -27,5 +47,5 @@ export function resolveIdentity(sources: IdentitySource[]): ChannelIdentity {
if (hasValue(v)) (out as Record<string, unknown>)[k] = v;
}
}
return out as ChannelIdentity;
return Object.freeze(validateIdentity(out));
}
@@ -1,8 +1,6 @@
import type { ChannelIdentity } from './identity.ts';
/** 调试用登录玩家档案(player_login 中除渠道身份外的字段)。仅 debug 模式生效。 */
/** 调试用登录玩家档案;值只能由测试或本地启动配置显式注入。 */
export interface DebugAccount {
openid: string; // 服务器据此标识玩家(必填)
openid: string;
nickname?: string;
avatar?: string;
sex?: number;
@@ -11,39 +9,58 @@ export interface DebugAccount {
unionid?: string | number;
}
export type ProfileSource = 'remote' | 'direct';
export interface DebugProfile {
name: string;
server?: string; // 显式 ws 调试服地址(给定则跳过远程抓取,纯直连)
gameserver?: string; // 远程配置 txt URL(不含防缓存串)
serverOverride?: string; // debug:抓远程配置走全链路后,把连接地址覆盖为此(联调本地游戏服用)
identity?: Partial<ChannelIdentity>; // 调试用渠道身份覆盖
account?: DebugAccount; // 调试用登录账号(仅 debug 模式生效)
isDebugger?: boolean; // 收发包日志开关(替代旧 Game_Config.Debugger.isDebugger)
readonly name: string;
readonly source: ProfileSource;
readonly gameserver?: string;
readonly servers?: readonly string[];
readonly account?: DebugAccount;
}
/** 正式 gameserver:原 00_SubGame_Config.js:11。 */
export const DEFAULT_GAMESERVER = 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt';
/** 测试 gameserver:原 00_SubGame_Config.js:9 注释。 */
const STAGING_GAMESERVER = 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt';
export const PROFILES: Record<string, DebugProfile> = {
prod: { name: 'prod', gameserver: DEFAULT_GAMESERVER, isDebugger: false },
staging: {
name: 'staging',
gameserver: STAGING_GAMESERVER,
serverOverride: 'ws://127.0.0.1:3088', // 走 staging 配置全链路,但游戏服连本地测试服
isDebugger: true,
// 调试登录账号:联调前替换为真实测试账号(openid 必填)。仅 debug 模式生效。
account: { openid: 'REPLACE_ME_test_openid', nickname: '测试号', avatar: 'https://gips3.baidu.com/it/u=4028841722,3177937756&fm=3074&app=3074&f=PNG?w=2048&h=2048', sex: 0, province: 'testprovince', city: 'testcity', unionid: 'test_unionid' },
},
local: { name: 'local', server: 'ws://127.0.0.1:3088', isDebugger: true },
};
export const PROFILES: Readonly<Record<string, DebugProfile>> = Object.freeze({
prod: Object.freeze({ name: 'prod', source: 'remote', gameserver: DEFAULT_GAMESERVER }),
staging: Object.freeze({ name: 'staging', source: 'remote', gameserver: DEFAULT_GAMESERVER }),
local: Object.freeze({
name: 'local',
source: 'direct',
servers: Object.freeze(['ws://127.0.0.1:3088']),
}),
});
/** 发布只需保持 'prod'。 */
export const ACTIVE_PROFILE = 'prod';
/** 解析当前 profile:ACTIVE_PROFILE 为基线,?profile=<name> 临时切换;未知名回退 ACTIVE_PROFILE。 */
export function resolveActiveProfile(search: string): DebugProfile {
const name = new URLSearchParams(search).get('profile') || ACTIVE_PROFILE;
return PROFILES[name] ?? PROFILES[ACTIVE_PROFILE];
const requested = new URLSearchParams(search).get('profile');
const name = requested || ACTIVE_PROFILE;
const profile = PROFILES[name];
if (!profile) throw new Error(`未知 profile: ${name}`);
return profile;
}
/** 忠实旧 min_replaceAll 链:- → /,# → :,再包成 http://...txt。 */
export function decodeGameConfig(value: string): string {
if (!value) throw new Error('gameconfig 必须是非空字符串');
const decoded = value.split('-').join('/').split('#').join(':');
return `http://${decoded}.txt`;
}
export interface ResolveGameServerOptions {
readonly configured: string;
readonly injectedGameConfig: unknown;
}
/** 注入的 gameconfig 非空时是唯一覆盖源;否则只使用 profile 配置源。 */
export function resolveGameServer(options: ResolveGameServerOptions): string {
const injected = options.injectedGameConfig;
if (injected === '' || injected === undefined || injected === null) {
if (!options.configured) throw new Error('profile 未配置 gameserver');
return options.configured;
}
if (typeof injected !== 'string') throw new Error('gameconfig 必须是字符串');
return decodeGameConfig(injected);
}
@@ -1,13 +1,18 @@
import type { ConfigFetcher, RemoteConfig } from './remote-config.ts';
import type { ConfigFetcher } from './remote-config.ts';
import { ConfigParseError } from './remote-config.ts';
/** 生产用 ConfigFetcher:全局 fetch 抓取远程配置 txt 并 JSON.parse。 */
type FetchImplementation = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
/** 生产 fetcher:逐字复刻 get_config 的 POST、空 body、无条件 ? cache bust。 */
export class HttpConfigFetcher implements ConfigFetcher {
async fetch(url: string): Promise<RemoteConfig> {
const resp = await fetch(url, { method: 'GET' });
const text = await resp.text();
constructor(private readonly fetchImpl: FetchImplementation = globalThis.fetch.bind(globalThis)) {}
async fetch(gameserver: string, cacheBust: () => string = () => String(Date.now())): Promise<unknown> {
const url = `${gameserver}?${cacheBust()}`;
const response = await this.fetchImpl(url, { method: 'POST', body: '' });
const text = await response.text();
try {
return JSON.parse(text) as RemoteConfig;
return JSON.parse(text) as unknown;
} catch {
throw new ConfigParseError(`远程配置 JSON 解析失败: ${url}`);
}
@@ -1,71 +1,41 @@
import type { ChannelIdentity } from './identity.ts';
/** 原配置服务的唯一服务器字段,逐字对齐 ServerUrl_Succ 的 data.urlserver。 */
export interface RemoteConfig {
readonly data: {
readonly urlserver: string | readonly string[];
readonly [key: string]: unknown;
};
}
/** 远程配置内层数据(键名逐字一致原 get_paravalue / getConfig_Succ)。 */
export interface MarketEntry { marketid: string | number; [k: string]: unknown; }
export interface ChannelEntry { channelid: string | number; marketlist?: MarketEntry[]; [k: string]: unknown; }
export interface GameEntry { gameid: string | number; channellist?: ChannelEntry[]; [k: string]: unknown; }
export interface AgentEntry { agentid: string | number; gamelist?: GameEntry[]; [k: string]: unknown; }
export interface RemoteConfigData { agentlist?: AgentEntry[]; [k: string]: unknown; }
export interface RemoteConfig { data?: RemoteConfigData; }
/** 远程配置抓取抽象(生产注入 HttpConfigFetcher,测试注入 fake)。 */
export interface ConfigFetcher { fetch(url: string): Promise<RemoteConfig>; }
/** 远程配置抓取抽象;cache bust 在 fetcher 内形成且只形成一次。 */
export interface ConfigFetcher {
fetch(gameserver: string, cacheBust?: () => string): Promise<unknown>;
}
export class ConfigFetchError extends Error {}
export class ConfigParseError extends Error {}
/**
* 分层取参,忠实原 get_paravalue:顶层 data[key] 为默认,逐层进入匹配项;
* 某层有该 key(truthy)则更新返回值;匹配链中断则返回当前最具体值。
*/
export function getParam(config: RemoteConfig, key: string, id: ChannelIdentity): unknown {
const data = config.data;
if (!data) return null;
let value: unknown = data[key] ? data[key] : null;
const descend = <T extends Record<string, unknown>>(
list: T[] | undefined, idKey: string, idVal: unknown,
): T | null => {
if (!list) return null;
for (const item of list) {
if (item[idKey] == idVal) { // 宽松比较,id 可能 string/number
if (item[key]) value = item[key];
return item;
}
}
return null;
};
const agent = descend(data.agentlist, 'agentid', id.agentid);
if (!agent) return value;
const game = descend(agent.gamelist, 'gameid', id.gameid);
if (!game) return value;
const channel = descend(game.channellist, 'channelid', id.channelid);
if (!channel) return value;
descend(channel.marketlist, 'marketid', id.marketid);
return value;
function normalizeWebSocketUrl(value: string): string {
return value.startsWith('ws://') || value.startsWith('wss://') ? value : `ws://${value}`;
}
/**
* 连接相关参数键(键名逐字一致原 getConfig_Succ)。优先级即数组顺序。
* 仅取 *_server_tcp:YouleNexus 为 WebSocket-only(原 netType==0 分支);
* 原工程的 *_server_http 属 HTTP 传输模式(netType==1),本期不在范围,故有意省略。
* 只接受 data.urlserver;不读取旧的 *_server_tcp,也不去重或改变配置顺序。
*/
const SERVER_KEYS = ['player_server_tcp', 'visitor_server_tcp', 'game_server_tcp'] as const;
function toWs(addr: string): string {
return (addr.startsWith('ws://') || addr.startsWith('wss://')) ? addr : 'ws://' + addr;
}
/** 解析 ws 候选服务器列表(按 SERVER_KEYS 优先级,去重)。 */
export function resolveServers(config: RemoteConfig, id: ChannelIdentity): string[] {
const out: string[] = [];
for (const key of SERVER_KEYS) {
const v = getParam(config, key, id);
if (typeof v === 'string' && v) {
const ws = toWs(v);
if (!out.includes(ws)) out.push(ws);
}
export function parseUrlServers(input: unknown): readonly string[] {
if (typeof input !== 'object' || input === null || !('data' in input)) {
throw new ConfigParseError('远程配置缺少 data.urlserver');
}
return out;
const data = (input as { data?: unknown }).data;
if (typeof data !== 'object' || data === null || !('urlserver' in data)) {
throw new ConfigParseError('远程配置缺少 data.urlserver');
}
const raw = (data as { urlserver?: unknown }).urlserver;
const values = typeof raw === 'string' ? [raw] : raw;
if (!Array.isArray(values) || values.length === 0
|| values.some((value) => typeof value !== 'string' || value.length === 0)) {
throw new ConfigParseError('远程配置 data.urlserver 必须是非空字符串或非空字符串数组');
}
return Object.freeze(values.map(normalizeWebSocketUrl));
}
@@ -0,0 +1,125 @@
import type { ChannelIdentity, IdentitySource } from './identity.ts';
import { resolveIdentity } from './identity.ts';
import {
PROFILES,
resolveActiveProfile,
resolveGameServer,
type DebugProfile,
} from './profiles.ts';
import {
ConfigFetchError,
parseUrlServers,
type ConfigFetcher,
type RemoteConfig,
} from './remote-config.ts';
import type { RuntimeMode } from './runtime-mode.ts';
import { BUILD_IDENTITY } from './sources/defaults.ts';
import {
getothername,
nativeIdentitySource,
type NativeHost,
type NativeHostKind,
} from './sources/native-settings.ts';
import { queryStringSource } from './sources/query-string.ts';
export type HostKind = 'h5' | NativeHostKind;
export interface RuntimeConfig {
readonly mode: RuntimeMode;
readonly isDebugger: boolean;
readonly identity: Readonly<ChannelIdentity>;
readonly servers: readonly string[];
readonly source: 'remote' | 'direct';
readonly gameserver: string | null;
readonly rawConfig: RemoteConfig | null;
}
export interface ResolveRuntimeConfigOptions {
readonly mode: RuntimeMode;
readonly hostKind: HostKind;
readonly win: NativeHost;
readonly search: string;
readonly fetcher: ConfigFetcher;
readonly cacheBust?: () => string;
}
function buildIdentitySource(): IdentitySource {
return () => ({ gameid: BUILD_IDENTITY.gameid, version: BUILD_IDENTITY.version });
}
function resolveHostIdentity(options: ResolveRuntimeConfigOptions): Readonly<ChannelIdentity> {
if (options.hostKind === 'h5') {
return resolveIdentity([() => BUILD_IDENTITY, queryStringSource(options.search)]);
}
return resolveIdentity([
buildIdentitySource(),
nativeIdentitySource(options.win, options.hostKind),
]);
}
function resolveInjectedGameConfig(options: ResolveRuntimeConfigOptions): unknown {
if (options.hostKind === 'h5') {
return new URLSearchParams(options.search).get('gameconfig') ?? '';
}
return getothername(options.win, options.hostKind, 'gameconfig');
}
function selectedProfile(mode: RuntimeMode, search: string): DebugProfile {
return mode === 'release' ? PROFILES.prod! : resolveActiveProfile(search);
}
function freezeResult(result: RuntimeConfig): RuntimeConfig {
return Object.freeze(result);
}
export async function resolveRuntimeConfig(
options: ResolveRuntimeConfigOptions,
): Promise<RuntimeConfig> {
const profile = selectedProfile(options.mode, options.search);
const identity = resolveHostIdentity(options);
if (profile.source === 'direct') {
if (options.mode !== 'debug' || profile.name !== 'local') {
throw new ConfigFetchError(`profile '${profile.name}' 不是允许的 local debug direct 模式`);
}
if (!profile.servers || profile.servers.length === 0) {
throw new ConfigFetchError(`profile '${profile.name}' 未配置完整 servers`);
}
return freezeResult({
mode: options.mode,
isDebugger: true,
identity,
servers: Object.freeze([...profile.servers]),
source: 'direct',
gameserver: null,
rawConfig: null,
});
}
if (!profile.gameserver) {
throw new ConfigFetchError(`profile '${profile.name}' 未配置 gameserver`);
}
const gameserver = resolveGameServer({
configured: profile.gameserver,
injectedGameConfig: resolveInjectedGameConfig(options),
});
let raw: unknown;
try {
raw = await options.fetcher.fetch(gameserver, options.cacheBust);
} catch (error) {
if (error instanceof ConfigFetchError) throw error;
throw new ConfigFetchError(`远程配置抓取失败: ${gameserver}`);
}
const servers = parseUrlServers(raw);
return freezeResult({
mode: options.mode,
isDebugger: options.mode === 'debug',
identity,
servers,
source: 'remote',
gameserver,
rawConfig: raw as RemoteConfig,
});
}
@@ -1,40 +1,66 @@
import type { ChannelIdentity, IdentitySource } from '../identity.ts';
import type { IdentitySource } from '../identity.ts';
/** 原生注入宿主(≈ window)。settings 由原生注入;全局 app_* 为 uAgent_3 路径回退。 */
export type NativeHostKind = 'native-settings' | 'uAgent_3';
/** window.settings 与 app_* 均由原生注入,名称不得改写。 */
export interface NativeHost {
settings?: {
getothername?(name: string): string | number;
getchannelName?(): string | number;
getmarketname?(): string | number;
};
[global: string]: unknown; // app_agent / app_market 等
[global: string]: unknown;
}
function tryRead(primary: () => unknown, fallback: () => unknown): unknown {
function hasValue(value: unknown): boolean {
return value !== undefined && value !== null && value !== '';
}
/** 忠实 Func.getothername:非 uAgent_3 的调用异常在源边界变成空串。 */
export function getothername(win: NativeHost, hostKind: NativeHostKind, name: string): unknown {
if (hostKind === 'uAgent_3') return win[`app_${name}`];
try {
const v = primary();
if (v !== undefined && v !== null && v !== '') return v;
} catch { /* 原生桥不存在,走回退 */ }
try { return fallback(); } catch { return undefined; }
return win.settings!.getothername!(name);
} catch {
return '';
}
}
function has(v: unknown): boolean { return v !== undefined && v !== null && v !== ''; }
/** 原生 getchannelName 的既有缺省语义只存在于此适配器。 */
export function getchannelName(win: NativeHost, hostKind: NativeHostKind): unknown {
try {
return win.settings!.getchannelName!() || '';
} catch {
return hostKind === 'uAgent_3' ? win.app_channel : '';
}
}
/** 原生 getmarketname 的既有缺省语义只存在于此适配器。 */
export function getmarketname(win: NativeHost, hostKind: NativeHostKind): unknown {
try {
return win.settings!.getmarketname!();
} catch {
return hostKind === 'uAgent_3' ? win.app_market : '4';
}
}
/**
* 原生模式:同步读取渠道身份。逐字对齐原 05_Func.js:
* agent → window.settings.getothername('agent') | window.app_agent
* channel → window.settings.getchannelName()
* market → window.settings.getmarketname() | window.app_market
* 只产出宿主负责的 agent/channel/market;game/version 由构建身份源负责。
* uAgent_3 全部走 app_<name>,不触碰 settings。
*/
export function nativeSettingsSource(win: NativeHost): IdentitySource {
export function nativeIdentitySource(win: NativeHost, hostKind: NativeHostKind): IdentitySource {
return () => {
const out: Partial<ChannelIdentity> = {};
const agentid = tryRead(() => win.settings?.getothername?.('agent'), () => win['app_agent']);
if (has(agentid)) out.agentid = agentid as string | number;
const channelid = tryRead(() => win.settings?.getchannelName?.(), () => undefined);
if (has(channelid)) out.channelid = channelid as string | number;
const marketid = tryRead(() => win.settings?.getmarketname?.(), () => win['app_market']);
if (has(marketid)) out.marketid = marketid as string | number;
const agentid = getothername(win, hostKind, 'agent');
const channelid = hostKind === 'uAgent_3'
? getothername(win, hostKind, 'channel')
: getchannelName(win, hostKind);
const marketid = hostKind === 'uAgent_3'
? getothername(win, hostKind, 'market')
: getmarketname(win, hostKind);
const out: Record<string, unknown> = {};
if (hasValue(agentid)) out.agentid = agentid;
if (hasValue(channelid)) out.channelid = channelid;
if (hasValue(marketid)) out.marketid = marketid;
return out;
};
}