完善联调标准化与诊断链路

This commit is contained in:
2026-04-03 17:01:04 +08:00
parent 114c524044
commit b09c21c814
35 changed files with 2677 additions and 175 deletions

View File

@@ -237,6 +237,20 @@ export interface BackendSessionResultView {
}
}
export interface BackendClientLogInput {
source: string
level: 'debug' | 'info' | 'warn' | 'error'
category: string
message: string
eventId?: string
releaseId?: string
sessionId?: string
manifestUrl?: string
route?: string
occurredAt?: string
details?: Record<string, unknown>
}
type BackendEnvelope<T> = {
data: T
}
@@ -428,3 +442,15 @@ export function getMyResults(input: {
authToken: input.accessToken,
})
}
export function postClientLog(input: {
baseUrl: string
payload: BackendClientLogInput
}): Promise<void> {
return requestBackend<void>({
method: 'POST',
baseUrl: input.baseUrl,
path: '/dev/client-logs',
body: input.payload as unknown as Record<string, unknown>,
})
}

View File

@@ -0,0 +1,90 @@
import { loadBackendBaseUrl } from './backendAuth'
import { postClientLog, type BackendClientLogInput } from './backendApi'
type ClientLogLevel = BackendClientLogInput['level']
type ClientLogEntry = {
level: ClientLogLevel
category: string
message: string
eventId?: string
releaseId?: string
sessionId?: string
manifestUrl?: string
route?: string
details?: Record<string, unknown>
}
const CLIENT_LOG_SOURCE = 'wechat-mini'
const MAX_PENDING_CLIENT_LOGS = 100
const pendingClientLogs: BackendClientLogInput[] = []
let clientLogFlushInProgress = false
let clientLogSequence = 0
function getCurrentRoute(): string {
const pages = getCurrentPages()
if (!pages.length) {
return ''
}
const current = pages[pages.length - 1]
return current && current.route ? current.route : ''
}
function enqueueClientLog(payload: BackendClientLogInput) {
pendingClientLogs.push(payload)
if (pendingClientLogs.length > MAX_PENDING_CLIENT_LOGS) {
pendingClientLogs.shift()
}
}
function flushNextClientLog() {
if (clientLogFlushInProgress || !pendingClientLogs.length) {
return
}
const baseUrl = loadBackendBaseUrl()
if (!baseUrl) {
pendingClientLogs.length = 0
return
}
const payload = pendingClientLogs.shift()
if (!payload) {
return
}
clientLogFlushInProgress = true
postClientLog({
baseUrl,
payload,
}).catch(() => {
// 联调日志不打断主流程,失败时静默丢弃。
}).finally(() => {
clientLogFlushInProgress = false
if (pendingClientLogs.length) {
flushNextClientLog()
}
})
}
export function reportBackendClientLog(entry: ClientLogEntry) {
clientLogSequence += 1
const details = entry.details ? { ...entry.details } : {}
details.seq = clientLogSequence
const payload: BackendClientLogInput = {
source: CLIENT_LOG_SOURCE,
level: entry.level,
category: entry.category,
message: entry.message,
eventId: entry.eventId || '',
releaseId: entry.releaseId || '',
sessionId: entry.sessionId || '',
manifestUrl: entry.manifestUrl || '',
route: entry.route || getCurrentRoute(),
occurredAt: new Date().toISOString(),
details,
}
enqueueClientLog(payload)
flushNextClientLog()
}

View File

@@ -21,6 +21,18 @@ export function adaptBackendLaunchResultToEnvelope(result: BackendLaunchResult):
sessionToken: result.launch.business.sessionToken,
sessionTokenExpiresAt: result.launch.business.sessionTokenExpiresAt,
},
resolvedRelease: result.launch.resolvedRelease
? {
launchMode: result.launch.resolvedRelease.launchMode || null,
source: result.launch.resolvedRelease.source || null,
eventId: result.launch.resolvedRelease.eventId || null,
releaseId: result.launch.resolvedRelease.releaseId || null,
configLabel: result.launch.resolvedRelease.configLabel || null,
manifestUrl: result.launch.resolvedRelease.manifestUrl || null,
manifestChecksumSha256: result.launch.resolvedRelease.manifestChecksumSha256 || null,
routeCode: result.launch.resolvedRelease.routeCode || null,
}
: null,
variant: result.launch.variant
? {
variantId: result.launch.variant.id,

View File

@@ -9,6 +9,17 @@ export interface GameConfigLaunchRequest {
routeCode?: string | null
}
export interface GameResolvedReleaseLaunchContext {
launchMode?: string | null
source?: string | null
eventId?: string | null
releaseId?: string | null
configLabel?: string | null
manifestUrl?: string | null
manifestChecksumSha256?: string | null
routeCode?: string | null
}
export interface BusinessLaunchContext {
source: BusinessLaunchSource
competitionId?: string | null
@@ -56,6 +67,7 @@ export interface GameContentBundleLaunchContext {
export interface GameLaunchEnvelope {
config: GameConfigLaunchRequest
business: BusinessLaunchContext | null
resolvedRelease?: GameResolvedReleaseLaunchContext | null
variant?: GameVariantLaunchContext | null
runtime?: GameRuntimeLaunchContext | null
presentation?: GamePresentationLaunchContext | null
@@ -65,6 +77,7 @@ export interface GameLaunchEnvelope {
export interface MapPageLaunchOptions {
launchId?: string
recoverSession?: string
autoStartOnEnter?: string
preset?: string
configUrl?: string
configLabel?: string
@@ -292,6 +305,7 @@ export function getDemoGameLaunchEnvelope(preset: DemoGamePreset = 'classic'): G
business: {
source: 'demo',
},
resolvedRelease: null,
variant: null,
runtime: null,
presentation: null,
@@ -324,12 +338,24 @@ export function consumePendingGameLaunchEnvelope(launchId: string): GameLaunchEn
return envelope
}
export function buildMapPageUrlWithLaunchId(launchId: string): string {
return `/pages/map/map?launchId=${encodeURIComponent(launchId)}`
export function buildMapPageUrlWithLaunchId(launchId: string, extraQuery?: Record<string, string>): string {
const queryParts = [`launchId=${encodeURIComponent(launchId)}`]
if (extraQuery) {
Object.keys(extraQuery).forEach((key) => {
const value = extraQuery[key]
if (typeof value === 'string' && value) {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
}
})
}
return `/pages/map/map?${queryParts.join('&')}`
}
export function prepareMapPageUrlForLaunch(envelope: GameLaunchEnvelope): string {
return buildMapPageUrlWithLaunchId(stashPendingGameLaunchEnvelope(envelope))
return buildMapPageUrlWithLaunchId(
stashPendingGameLaunchEnvelope(envelope),
{ autoStartOnEnter: '1' },
)
}
export function prepareMapPageUrlForRecovery(envelope: GameLaunchEnvelope): string {
@@ -367,6 +393,7 @@ export function resolveGameLaunchEnvelope(options?: MapPageLaunchOptions | null)
routeCode: normalizeOptionalString(options ? options.routeCode : undefined),
},
business: buildBusinessLaunchContext(options),
resolvedRelease: null,
variant: buildVariantLaunchContext(options),
runtime: buildRuntimeLaunchContext(options),
presentation: buildPresentationLaunchContext(options),

View File

@@ -0,0 +1,88 @@
import { MockSimulatorDebugLogger, type MockSimulatorDebugLogLevel } from '../engine/debug/mockSimulatorDebugLogger'
const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
const DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY = 'cmr.debug.autoConnectMockSources.v1'
const DEBUG_MOCK_LOG_URL_STORAGE_KEY = 'cmr.debug.logBridgeUrl.v1'
const DEFAULT_DEBUG_LOG_URL = 'wss://gs.gotomars.xyz/debug-log'
let globalMockDebugLogger: MockSimulatorDebugLogger | null = null
function ensureLogger(): MockSimulatorDebugLogger {
if (!globalMockDebugLogger) {
globalMockDebugLogger = new MockSimulatorDebugLogger()
}
return globalMockDebugLogger
}
export function loadStoredMockChannelIdForGlobalDebug(): string {
try {
const value = wx.getStorageSync(DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY)
if (typeof value === 'string' && value.trim().length > 0) {
return value.trim()
}
} catch (_error) {
// Ignore storage read failures and fall back to default.
}
return 'default'
}
export function loadMockAutoConnectEnabledForGlobalDebug(): boolean {
try {
return wx.getStorageSync(DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY) === true
} catch (_error) {
return false
}
}
export function loadStoredMockDebugLogBridgeUrl(): string {
try {
const value = wx.getStorageSync(DEBUG_MOCK_LOG_URL_STORAGE_KEY)
if (typeof value === 'string' && value.trim().length > 0) {
return value.trim()
}
} catch (_error) {
// Ignore storage read failures and fall back to default.
}
return DEFAULT_DEBUG_LOG_URL
}
export function persistStoredMockDebugLogBridgeUrl(url: string) {
try {
wx.setStorageSync(DEBUG_MOCK_LOG_URL_STORAGE_KEY, url)
} catch (_error) {
// Ignore storage write failures.
}
}
export function syncGlobalMockDebugBridgeFromStorage(): void {
const logger = ensureLogger()
logger.setChannelId(loadStoredMockChannelIdForGlobalDebug())
logger.setUrl(loadStoredMockDebugLogBridgeUrl())
logger.setEnabled(loadMockAutoConnectEnabledForGlobalDebug())
}
export function setGlobalMockDebugBridgeChannelId(channelId: string): void {
const logger = ensureLogger()
logger.setChannelId(channelId)
}
export function setGlobalMockDebugBridgeEnabled(enabled: boolean): void {
const logger = ensureLogger()
logger.setEnabled(enabled)
}
export function setGlobalMockDebugBridgeUrl(url: string): void {
const logger = ensureLogger()
logger.setUrl(url)
}
export function emitGlobalMockDebugLog(
scope: string,
level: MockSimulatorDebugLogLevel,
message: string,
payload?: Record<string, unknown>,
): void {
const logger = ensureLogger()
logger.log(scope, level, message, payload)
}

View File

@@ -68,6 +68,7 @@ export interface RemoteMapConfig {
configAppId: string
configSchemaVersion: string
configVersion: string
playfieldKind: string
tileSource: string
minZoom: number
maxZoom: number
@@ -122,6 +123,7 @@ interface ParsedGameConfig {
appId: string
schemaVersion: string
version: string
playfieldKind: string
mapRoot: string
mapMeta: string
course: string | null
@@ -1754,6 +1756,7 @@ function parseGameConfigFromJson(text: string, gameConfigUrl: string): ParsedGam
appId: rawApp && typeof rawApp.id === 'string' ? rawApp.id : '',
schemaVersion: typeof parsed.schemaVersion === 'string' ? parsed.schemaVersion : '1',
version: typeof parsed.version === 'string' ? parsed.version : '',
playfieldKind: rawPlayfield && typeof rawPlayfield.kind === 'string' ? rawPlayfield.kind : '',
mapRoot,
mapMeta,
course: rawPlayfieldSource && typeof rawPlayfieldSource.url === 'string'
@@ -1855,6 +1858,7 @@ function parseGameConfigFromYaml(text: string, gameConfigUrl: string): ParsedGam
appId: '',
schemaVersion: '1',
version: '',
playfieldKind: typeof config.playfieldkind === 'string' ? config.playfieldkind : '',
mapRoot,
mapMeta,
course: typeof config.course === 'string' ? config.course : null,
@@ -2157,6 +2161,7 @@ export async function loadRemoteMapConfig(gameConfigUrl: string): Promise<Remote
configAppId: gameConfig.appId || '',
configSchemaVersion: gameConfig.schemaVersion || '1',
configVersion: gameConfig.version || '',
playfieldKind: gameConfig.playfieldKind || '',
tileSource: resolveUrl(mapRootUrl, mapMeta.tilePathTemplate),
minZoom: mapMeta.minZoom,
maxZoom: mapMeta.maxZoom,