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

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

@@ -1108,6 +1108,7 @@ export class MapEngine {
configAppId: string
configSchemaVersion: string
configVersion: string
playfieldKind: string
controlScoreOverrides: Record<string, number>
controlContentOverrides: Record<string, GameControlDisplayContentOverride>
defaultControlContentOverride: GameControlDisplayContentOverride | null
@@ -1417,6 +1418,7 @@ export class MapEngine {
this.configAppId = ''
this.configSchemaVersion = '1'
this.configVersion = ''
this.playfieldKind = ''
this.controlScoreOverrides = {}
this.controlContentOverrides = {}
this.defaultControlContentOverride = null
@@ -1721,6 +1723,8 @@ export class MapEngine {
{ label: '比赛名称', value: title || '--' },
{ label: '配置版本', value: this.configVersion || '--' },
{ label: 'Schema版本', value: this.configSchemaVersion || '--' },
{ label: '场地类型', value: this.playfieldKind || '--' },
{ label: '模式编码', value: this.gameMode || '--' },
{ label: '活动ID', value: this.configAppId || '--' },
{ label: '动画等级', value: formatAnimationLevelText(this.state.animationLevel) },
{ label: '地图', value: this.state.mapName || '--' },
@@ -3423,8 +3427,8 @@ export class MapEngine {
this.courseOverlayVisible = true
const gameModeText = this.gameMode === 'score-o' ? '积分赛' : '顺序打点'
const defaultStatusText = this.currentGpsPoint
? `${gameModeText}开始 (${this.buildVersion})`
: `${gameModeText}已开始GPS定位启动中 (${this.buildVersion})`
? `已进入${gameModeText},请先打开始 (${this.buildVersion})`
: `已进入${gameModeText}GPS定位启动中,请先打开始点 (${this.buildVersion})`
this.commitGameResult(gameResult, defaultStatusText)
}
@@ -3683,6 +3687,15 @@ export class MapEngine {
this.mockSimulatorDebugLogger.disconnect()
}
handleEmitMockDebugLog(
scope: string,
level: 'info' | 'warn' | 'error',
message: string,
payload?: Record<string, unknown>,
): void {
this.mockSimulatorDebugLogger.log(scope, level, message, payload)
}
handleSetGameMode(nextMode: 'classic-sequential' | 'score-o'): void {
if (this.gameMode === nextMode) {
return
@@ -3882,6 +3895,7 @@ export class MapEngine {
this.configAppId = config.configAppId
this.configSchemaVersion = config.configSchemaVersion
this.configVersion = config.configVersion
this.playfieldKind = config.playfieldKind
this.controlScoreOverrides = config.controlScoreOverrides
this.controlContentOverrides = config.controlContentOverrides
this.defaultControlContentOverride = config.defaultControlContentOverride

View File

@@ -114,7 +114,7 @@ function getGuidanceEffects(
function buildPunchHintText(definition: GameDefinition, state: GameSessionState, currentTarget: GameControl | null): string {
if (state.status === 'idle') {
return '点击开始后先打开始点'
return '先打开始点即可正式开始比赛'
}
if (state.status === 'finished') {

View File

@@ -271,7 +271,7 @@ function buildPunchHintText(
focusedTarget: GameControl | null,
): string {
if (state.status === 'idle') {
return '点击开始后先打开始点'
return '先打开始点即可正式开始比赛'
}
if (state.status === 'finished') {

View File

@@ -3,6 +3,7 @@ import { getEventPlay, launchEvent, type BackendCourseVariantSummary, type Backe
import { adaptBackendLaunchResultToEnvelope } from '../../utils/backendLaunchAdapter'
import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
import { prepareMapPageUrlForLaunch } from '../../utils/gameLaunch'
import { reportBackendClientLog } from '../../utils/backendClientLogs'
import { HeartRateController } from '../../engine/sensor/heartRateController'
const PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY = 'cmr.preferredHeartRateDevice'
@@ -290,12 +291,32 @@ Page({
result.play.assignmentMode,
result.play.courseVariants,
)
const assignmentMode = result.play.assignmentMode ? result.play.assignmentMode : null
const logVariantId = assignmentMode === 'manual' && selectedVariantId ? selectedVariantId : null
const selectableVariants = buildSelectableVariants(
selectedVariantId,
result.play.assignmentMode,
result.play.courseVariants,
)
const selectedVariant = selectableVariants.find((item) => item.id === selectedVariantId) || null
reportBackendClientLog({
level: 'info',
category: 'event-prepare',
message: 'prepare play loaded',
eventId: result.event.id || this.data.eventId || '',
releaseId: result.resolvedRelease && result.resolvedRelease.releaseId
? result.resolvedRelease.releaseId
: '',
manifestUrl: result.resolvedRelease && result.resolvedRelease.manifestUrl
? result.resolvedRelease.manifestUrl
: '',
details: {
pageEventId: this.data.eventId || '',
resultEventId: result.event.id || '',
selectedVariantId: logVariantId,
assignmentMode,
},
})
this.setData({
loading: false,
titleText: `${result.event.displayName} / 开始前准备`,
@@ -586,6 +607,22 @@ Page({
})
try {
const assignmentMode = this.data.assignmentMode ? this.data.assignmentMode : null
const selectedVariantId = assignmentMode === 'manual' && this.data.selectedVariantId
? this.data.selectedVariantId
: null
reportBackendClientLog({
level: 'info',
category: 'event-prepare',
message: 'launch requested',
eventId: this.data.eventId || '',
details: {
pageEventId: this.data.eventId || '',
selectedVariantId,
assignmentMode,
phase: 'launch-requested',
},
})
const app = getApp<IAppOption>()
if (app.globalData) {
const pendingDeviceName = prepareHeartRateController && prepareHeartRateController.currentDeviceName
@@ -608,6 +645,32 @@ Page({
clientType: 'wechat',
deviceKey: 'mini-dev-device-001',
})
reportBackendClientLog({
level: 'info',
category: 'event-prepare',
message: 'launch response received',
eventId: result.launch.business && result.launch.business.eventId ? result.launch.business.eventId : this.data.eventId || '',
releaseId: result.launch.config && result.launch.config.releaseId ? result.launch.config.releaseId : '',
sessionId: result.launch.business && result.launch.business.sessionId ? result.launch.business.sessionId : '',
manifestUrl: result.launch.resolvedRelease && result.launch.resolvedRelease.manifestUrl
? result.launch.resolvedRelease.manifestUrl
: '',
details: {
pageEventId: this.data.eventId || '',
launchEventId: result.launch.business && result.launch.business.eventId ? result.launch.business.eventId : '',
launchSessionId: result.launch.business && result.launch.business.sessionId ? result.launch.business.sessionId : '',
configUrl: result.launch.config && result.launch.config.configUrl ? result.launch.config.configUrl : '',
releaseId: result.launch.config && result.launch.config.releaseId ? result.launch.config.releaseId : '',
resolvedReleaseId: result.launch.resolvedRelease && result.launch.resolvedRelease.releaseId
? result.launch.resolvedRelease.releaseId
: '',
resolvedManifestUrl: result.launch.resolvedRelease && result.launch.resolvedRelease.manifestUrl
? result.launch.resolvedRelease.manifestUrl
: '',
launchVariantId: result.launch.variant && result.launch.variant.id ? result.launch.variant.id : null,
phase: 'launch-response',
},
})
const envelope = adaptBackendLaunchResultToEnvelope(result)
wx.navigateTo({
url: prepareMapPageUrlForLaunch(envelope),

View File

@@ -1,6 +1,7 @@
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
import { getEventPlay, type BackendEventPlayResult } from '../../utils/backendApi'
import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
import { reportBackendClientLog } from '../../utils/backendClientLogs'
type EventPageData = {
eventId: string
@@ -130,6 +131,26 @@ Page({
},
applyEventPlay(result: BackendEventPlayResult) {
const assignmentMode = result.play.assignmentMode ? result.play.assignmentMode : null
reportBackendClientLog({
level: 'info',
category: 'event-play',
message: 'event play loaded',
eventId: result.event.id || this.data.eventId || '',
releaseId: result.resolvedRelease && result.resolvedRelease.releaseId
? result.resolvedRelease.releaseId
: '',
manifestUrl: result.resolvedRelease && result.resolvedRelease.manifestUrl
? result.resolvedRelease.manifestUrl
: '',
details: {
pageEventId: this.data.eventId || '',
resultEventId: result.event.id || '',
primaryAction: result.play.primaryAction || '',
assignmentMode,
variantCount: result.play.courseVariants ? result.play.courseVariants.length : 0,
},
})
this.setData({
loading: false,
titleText: result.event.displayName,

View File

@@ -1,5 +1,7 @@
import { clearBackendAuthTokens, loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
import { getEntryHome, type BackendCardResult, type BackendEntryHomeResult } from '../../utils/backendApi'
import { reportBackendClientLog } from '../../utils/backendClientLogs'
import { setGlobalMockDebugBridgeEnabled } from '../../utils/globalMockDebugBridge'
const DEFAULT_CHANNEL_CODE = 'mini-demo'
const DEFAULT_CHANNEL_TYPE = 'wechat_mini'
@@ -100,6 +102,18 @@ Page({
},
applyEntryHomeResult(result: BackendEntryHomeResult) {
reportBackendClientLog({
level: 'info',
category: 'entry-home',
message: 'entry home loaded',
details: {
ongoingSessionId: result.ongoingSession && result.ongoingSession.id ? result.ongoingSession.id : '',
ongoingEventId: result.ongoingSession && result.ongoingSession.eventId ? result.ongoingSession.eventId : '',
recentSessionId: result.recentSession && result.recentSession.id ? result.recentSession.id : '',
recentEventId: result.recentSession && result.recentSession.eventId ? result.recentSession.eventId : '',
cardEventIds: (result.cards || []).map((item) => (item.event && item.event.id ? item.event.id : '')),
},
})
this.setData({
loading: false,
statusText: '首页加载完成',
@@ -141,6 +155,7 @@ Page({
handleLogout() {
clearBackendAuthTokens()
setGlobalMockDebugBridgeEnabled(false)
const app = getApp<IAppOption>()
if (app.globalData) {
app.globalData.backendAuthTokens = null

View File

@@ -1,5 +1,6 @@
import { clearBackendAuthTokens, saveBackendAuthTokens, saveBackendBaseUrl } from '../../utils/backendAuth'
import { loginWechatMini } from '../../utils/backendApi'
import { setGlobalMockDebugBridgeEnabled } from '../../utils/globalMockDebugBridge'
const DEFAULT_BACKEND_BASE_URL = 'https://api.gotomars.xyz'
const DEFAULT_DEVICE_KEY = 'mini-dev-device-001'
@@ -116,6 +117,7 @@ Page({
handleClearLoginState() {
clearBackendAuthTokens()
setGlobalMockDebugBridgeEnabled(false)
const app = getApp<IAppOption>()
if (app.globalData) {
app.globalData.backendAuthTokens = null

View File

@@ -16,6 +16,13 @@ import {
import { finishSession, startSession, type BackendSessionFinishSummaryPayload } from '../../utils/backendApi'
import { loadBackendBaseUrl } from '../../utils/backendAuth'
import { loadRemoteMapConfig, type RemoteMapConfig } from '../../utils/remoteMapConfig'
import {
persistStoredMockDebugLogBridgeUrl,
setGlobalMockDebugBridgeChannelId,
setGlobalMockDebugBridgeEnabled,
setGlobalMockDebugBridgeUrl,
} from '../../utils/globalMockDebugBridge'
import { reportBackendClientLog } from '../../utils/backendClientLogs'
import { type H5ExperienceFallbackPayload, type H5ExperienceRequest } from '../../game/experience/h5Experience'
import { type TrackColorPreset } from '../../game/presentation/trackStyleConfig'
import { type GpsMarkerColorPreset } from '../../game/presentation/gpsMarkerStyleConfig'
@@ -146,6 +153,7 @@ type MapPageData = MapEngineViewState & {
showLeftButtonGroup: boolean
showRightButtonGroups: boolean
showBottomDebugButton: boolean
showStartEntryButton: boolean
}
function getGlobalTelemetryProfile(): PlayerTelemetryProfile | null {
@@ -184,6 +192,7 @@ let systemSettingsLockLifetimeActive = false
let syncedBackendSessionStartId = ''
let syncedBackendSessionFinishId = ''
let shouldAutoRestoreRecoverySnapshot = false
let shouldAutoStartSessionOnEnter = false
let redirectedToResultPage = false
let pendingHeartRateSwitchDeviceName: string | null = null
const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
@@ -828,6 +837,52 @@ function buildRuntimeSummaryRows(envelope: GameLaunchEnvelope): MapEngineGameInf
return rows
}
function buildLaunchConfigSummaryRows(envelope: GameLaunchEnvelope): MapEngineGameInfoRow[] {
const rows: MapEngineGameInfoRow[] = []
rows.push({ label: '配置标签', value: envelope.config.configLabel || '--' })
rows.push({ label: '配置URL', value: envelope.config.configUrl || '--' })
rows.push({ label: '配置Release', value: envelope.config.releaseId || '--' })
rows.push({
label: 'Launch Event',
value: envelope.business && envelope.business.eventId
? envelope.business.eventId
: '--',
})
rows.push({
label: 'Resolved Manifest',
value: envelope.resolvedRelease && envelope.resolvedRelease.manifestUrl
? envelope.resolvedRelease.manifestUrl
: '--',
})
rows.push({
label: 'Resolved Release',
value: envelope.resolvedRelease && envelope.resolvedRelease.releaseId
? envelope.resolvedRelease.releaseId
: '--',
})
return rows
}
function emitSimulatorLaunchDiagnostic(
stage: string,
payload: Record<string, unknown>,
) {
reportBackendClientLog({
level: 'info',
category: 'launch-diagnostic',
message: stage,
eventId: typeof payload.launchEventId === 'string' ? payload.launchEventId : '',
releaseId: typeof payload.configReleaseId === 'string'
? payload.configReleaseId
: (typeof payload.resolvedReleaseId === 'string' ? payload.resolvedReleaseId : ''),
sessionId: typeof payload.launchSessionId === 'string' ? payload.launchSessionId : '',
manifestUrl: typeof payload.resolvedManifestUrl === 'string'
? payload.resolvedManifestUrl
: (typeof payload.configUrl === 'string' ? payload.configUrl : ''),
details: payload,
})
}
Page({
data: {
showDebugPanel: false,
@@ -967,6 +1022,7 @@ Page({
centerScaleRulerMajorMarks: [],
compassTicks: buildCompassTicks(),
compassLabels: buildCompassLabels(),
showStartEntryButton: true,
...buildSideButtonVisibility('shown'),
...buildSideButtonState({
sideButtonMode: 'shown',
@@ -989,10 +1045,15 @@ Page({
syncedBackendSessionFinishId = ''
redirectedToResultPage = false
shouldAutoRestoreRecoverySnapshot = options && options.recoverSession === '1'
currentGameLaunchEnvelope = resolveGameLaunchEnvelope(options)
if (!hasExplicitLaunchOptions(options)) {
const recoverySnapshot = loadSessionRecoverySnapshot()
if (recoverySnapshot) {
shouldAutoStartSessionOnEnter = !!(options && options.autoStartOnEnter === '1')
const recoverySnapshot = loadSessionRecoverySnapshot()
if (shouldAutoRestoreRecoverySnapshot && recoverySnapshot) {
// Recovery should trust the persisted session envelope first so it can
// survive launchId stash misses and still reconstruct the original round.
currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
} else {
currentGameLaunchEnvelope = resolveGameLaunchEnvelope(options)
if (!hasExplicitLaunchOptions(options) && recoverySnapshot) {
currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
}
}
@@ -1005,6 +1066,9 @@ Page({
const statusBarHeight = systemInfo.statusBarHeight || 0
const menuButtonRect = wx.getMenuButtonBoundingClientRect()
const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
this.setData({
showStartEntryButton: !shouldAutoStartSessionOnEnter,
})
if (mapEngine) {
mapEngine.destroy()
@@ -1514,11 +1578,27 @@ Page({
systemSettingsLockLifetimeActive = false
currentGameLaunchEnvelope = getDemoGameLaunchEnvelope()
shouldAutoRestoreRecoverySnapshot = false
shouldAutoStartSessionOnEnter = false
redirectedToResultPage = false
stageCanvasAttached = false
},
loadGameLaunchEnvelope(envelope: GameLaunchEnvelope) {
emitSimulatorLaunchDiagnostic('loadGameLaunchEnvelope', {
launchEventId: envelope.business && envelope.business.eventId ? envelope.business.eventId : '',
launchSessionId: envelope.business && envelope.business.sessionId ? envelope.business.sessionId : '',
configUrl: envelope.config.configUrl || '',
configReleaseId: envelope.config.releaseId || '',
resolvedManifestUrl: envelope.resolvedRelease && envelope.resolvedRelease.manifestUrl
? envelope.resolvedRelease.manifestUrl
: '',
resolvedReleaseId: envelope.resolvedRelease && envelope.resolvedRelease.releaseId
? envelope.resolvedRelease.releaseId
: '',
launchVariantId: envelope.variant && envelope.variant.variantId ? envelope.variant.variantId : null,
launchVariantRouteCode: envelope.variant && envelope.variant.routeCode ? envelope.variant.routeCode : null,
runtimeCourseVariantId: envelope.runtime && envelope.runtime.courseVariantId ? envelope.runtime.courseVariantId : null,
})
this.loadMapConfigFromRemote(
envelope.config.configUrl,
envelope.config.configLabel,
@@ -1621,10 +1701,49 @@ Page({
reportAbandonedRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
const sessionContext = getBackendSessionContextFromLaunchEnvelope(snapshot.launchEnvelope)
if (!sessionContext) {
reportBackendClientLog({
level: 'warn',
category: 'session-recovery',
message: 'abandon recovery without valid session context',
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
? snapshot.launchEnvelope.business.eventId
: '',
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
? snapshot.launchEnvelope.config.releaseId
: '',
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
? snapshot.launchEnvelope.config.configUrl
: '',
details: {
phase: 'abandon-no-session',
},
})
clearSessionRecoverySnapshot()
return
}
reportBackendClientLog({
level: 'info',
category: 'session-recovery',
message: 'abandon recovery requested',
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
? snapshot.launchEnvelope.business.eventId
: '',
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
? snapshot.launchEnvelope.config.releaseId
: '',
sessionId: sessionContext.sessionId,
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
? snapshot.launchEnvelope.config.configUrl
: '',
details: {
phase: 'abandon-requested',
},
})
finishSession({
baseUrl: getCurrentBackendBaseUrl(),
sessionId: sessionContext.sessionId,
@@ -1634,6 +1753,26 @@ Page({
})
.then(() => {
syncedBackendSessionFinishId = sessionContext.sessionId
reportBackendClientLog({
level: 'info',
category: 'session-recovery',
message: 'abandon recovery synced as cancelled',
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
? snapshot.launchEnvelope.business.eventId
: '',
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
? snapshot.launchEnvelope.config.releaseId
: '',
sessionId: sessionContext.sessionId,
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
? snapshot.launchEnvelope.config.configUrl
: '',
details: {
phase: 'abandon-finished',
},
})
clearSessionRecoverySnapshot()
wx.showToast({
title: '已放弃上次对局',
@@ -1642,6 +1781,27 @@ Page({
})
})
.catch((error) => {
reportBackendClientLog({
level: 'warn',
category: 'session-recovery',
message: 'abandon recovery finish(cancelled) failed',
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
? snapshot.launchEnvelope.business.eventId
: '',
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
? snapshot.launchEnvelope.config.releaseId
: '',
sessionId: sessionContext.sessionId,
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
? snapshot.launchEnvelope.config.configUrl
: '',
details: {
phase: 'abandon-failed',
message: error && error.message ? error.message : '未知错误',
},
})
clearSessionRecoverySnapshot()
const message = error && error.message ? error.message : '未知错误'
this.setData({
@@ -1712,6 +1872,28 @@ Page({
this.applyRuntimeSystemSettings(true)
const restored = mapEngine ? mapEngine.restoreSessionRecoveryRuntimeSnapshot(snapshot.runtime) : false
if (!restored) {
reportBackendClientLog({
level: 'warn',
category: 'session-recovery',
message: 'recovery restore failed',
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
? snapshot.launchEnvelope.business.eventId
: '',
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
? snapshot.launchEnvelope.config.releaseId
: '',
sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
? snapshot.launchEnvelope.business.sessionId
: '',
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
? snapshot.launchEnvelope.config.configUrl
: '',
details: {
phase: 'restore-failed',
},
})
clearSessionRecoverySnapshot()
wx.showToast({
title: '恢复失败,已回到初始状态',
@@ -1726,11 +1908,34 @@ Page({
showDebugPanel: false,
showGameInfoPanel: false,
showSystemSettingsPanel: false,
showStartEntryButton: false,
})
const sessionContext = getCurrentBackendSessionContext()
if (sessionContext) {
syncedBackendSessionStartId = sessionContext.sessionId
}
reportBackendClientLog({
level: 'info',
category: 'session-recovery',
message: 'recovery restored',
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
? snapshot.launchEnvelope.business.eventId
: '',
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
? snapshot.launchEnvelope.config.releaseId
: '',
sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
? snapshot.launchEnvelope.business.sessionId
: '',
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
? snapshot.launchEnvelope.config.configUrl
: '',
details: {
phase: 'restored',
},
})
this.syncSessionRecoveryLifecycle('running')
return true
},
@@ -1752,24 +1957,77 @@ Page({
maybePromptSessionRecoveryRestore(config: RemoteMapConfig) {
const snapshot = loadSessionRecoverySnapshot()
if (!snapshot || !mapEngine) {
return
return false
}
if (
snapshot.launchEnvelope.config.configUrl !== currentGameLaunchEnvelope.config.configUrl
|| snapshot.configAppId !== config.configAppId
|| snapshot.configVersion !== config.configVersion
) {
reportBackendClientLog({
level: 'warn',
category: 'session-recovery',
message: 'recovery snapshot dropped due to config mismatch',
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
? snapshot.launchEnvelope.business.eventId
: '',
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
? snapshot.launchEnvelope.config.releaseId
: '',
sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
? snapshot.launchEnvelope.business.sessionId
: '',
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
? snapshot.launchEnvelope.config.configUrl
: '',
details: {
phase: 'config-mismatch',
currentConfigUrl: currentGameLaunchEnvelope.config.configUrl,
snapshotConfigUrl: snapshot.launchEnvelope.config.configUrl,
currentConfigAppId: config.configAppId,
snapshotConfigAppId: snapshot.configAppId,
},
})
clearSessionRecoverySnapshot()
return
this.setData({
statusText: '检测到旧局恢复记录,但当前配置源已变化,已回到初始状态',
})
return false
}
if (shouldAutoRestoreRecoverySnapshot) {
shouldAutoRestoreRecoverySnapshot = false
reportBackendClientLog({
level: 'info',
category: 'session-recovery',
message: 'auto recovery requested',
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
? snapshot.launchEnvelope.business.eventId
: '',
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
? snapshot.launchEnvelope.config.releaseId
: '',
sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
? snapshot.launchEnvelope.business.sessionId
: '',
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
? snapshot.launchEnvelope.config.configUrl
: '',
details: {
phase: 'auto-restore',
},
})
this.restoreRecoverySnapshot(snapshot)
return
return true
}
this.setData({
showStartEntryButton: true,
})
wx.showModal({
title: '恢复对局',
content: '检测到上次有未正常结束的对局,是否继续恢复?',
@@ -1784,6 +2042,21 @@ Page({
this.restoreRecoverySnapshot(snapshot)
},
})
return true
},
maybeAutoStartSessionOnEnter() {
if (!shouldAutoStartSessionOnEnter || !mapEngine) {
return
}
shouldAutoStartSessionOnEnter = false
systemSettingsLockLifetimeActive = true
this.applyRuntimeSystemSettings(true)
this.setData({
showStartEntryButton: false,
})
mapEngine.handleStartGame()
},
compileCurrentRuntimeProfile(lockLifetimeActive = isSystemSettingsLockLifetimeActive()) {
@@ -1913,20 +2186,76 @@ Page({
return
}
emitSimulatorLaunchDiagnostic('loadRemoteMapConfig:resolved', {
launchEventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
? currentGameLaunchEnvelope.business.eventId
: '',
configUrl,
configVersion: config.configVersion || '',
schemaVersion: config.configSchemaVersion || '',
playfieldKind: config.playfieldKind || '',
gameMode: config.gameMode || '',
configTitle: config.configTitle || '',
})
currentEngine.applyRemoteMapConfig(config)
this.applyConfiguredSystemSettings(config)
this.applyCompiledRuntimeProfiles(true, {
const compiledProfile = this.applyCompiledRuntimeProfiles(true, {
includeMap: true,
includeGame: true,
includePresentation: true,
})
this.maybePromptSessionRecoveryRestore(config)
if (compiledProfile) {
reportBackendClientLog({
level: 'info',
category: 'runtime-compiler',
message: 'compiled runtime profile applied',
eventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
? currentGameLaunchEnvelope.business.eventId
: '',
releaseId: currentGameLaunchEnvelope.config && currentGameLaunchEnvelope.config.releaseId
? currentGameLaunchEnvelope.config.releaseId
: '',
sessionId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.sessionId
? currentGameLaunchEnvelope.business.sessionId
: '',
manifestUrl: currentGameLaunchEnvelope.resolvedRelease && currentGameLaunchEnvelope.resolvedRelease.manifestUrl
? currentGameLaunchEnvelope.resolvedRelease.manifestUrl
: currentGameLaunchEnvelope.config && currentGameLaunchEnvelope.config.configUrl
? currentGameLaunchEnvelope.config.configUrl
: '',
details: {
phase: 'compiled-runtime-applied',
schemaVersion: config.configSchemaVersion || '',
playfield: {
kind: config.playfieldKind || '',
},
game: {
mode: config.gameMode || '',
},
},
})
}
const recoveryHandled = this.maybePromptSessionRecoveryRestore(config)
if (!recoveryHandled) {
this.maybeAutoStartSessionOnEnter()
} else {
shouldAutoStartSessionOnEnter = false
}
})
.catch((error) => {
if (mapEngine !== currentEngine) {
return
}
emitSimulatorLaunchDiagnostic('loadRemoteMapConfig:error', {
launchEventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
? currentGameLaunchEnvelope.business.eventId
: '',
configUrl,
message: error && error.message ? error.message : '未知错误',
})
const rawErrorMessage = error && error.message ? error.message : '未知错误'
const errorMessage = rawErrorMessage.indexOf('404') >= 0
? `release manifest 不存在或未发布 (${configLabel})`
@@ -2115,6 +2444,10 @@ Page({
})
persistMockChannelId(channelId)
persistMockAutoConnectEnabled(true)
setGlobalMockDebugBridgeChannelId(channelId)
setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
persistStoredMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
setGlobalMockDebugBridgeEnabled(true)
mapEngine.handleSetMockChannelId(channelId)
mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
@@ -2144,6 +2477,7 @@ Page({
mockChannelIdDraft: channelId,
})
persistMockChannelId(channelId)
setGlobalMockDebugBridgeChannelId(channelId)
if (mapEngine) {
mapEngine.handleSetMockChannelId(channelId)
}
@@ -2199,12 +2533,17 @@ Page({
},
handleSaveMockDebugLogBridgeUrl() {
persistStoredMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
if (mapEngine) {
mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
}
},
handleConnectMockDebugLogBridge() {
setGlobalMockDebugBridgeChannelId((this.data.mockChannelIdDraft || '').trim() || 'default')
setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
setGlobalMockDebugBridgeEnabled(true)
if (mapEngine) {
mapEngine.handleConnectMockDebugLogBridge()
}
@@ -2212,6 +2551,7 @@ Page({
handleDisconnectMockDebugLogBridge() {
persistMockAutoConnectEnabled(false)
setGlobalMockDebugBridgeEnabled(false)
if (mapEngine) {
mapEngine.handleDisconnectMockDebugLogBridge()
}
@@ -2358,8 +2698,12 @@ Page({
handleStartGame() {
if (mapEngine) {
shouldAutoStartSessionOnEnter = false
systemSettingsLockLifetimeActive = true
this.applyRuntimeSystemSettings(true)
this.setData({
showStartEntryButton: false,
})
mapEngine.handleStartGame()
}
},
@@ -2443,6 +2787,7 @@ Page({
const snapshot = mapEngine.getGameInfoSnapshot()
const localRows = snapshot.localRows.concat([
...buildRuntimeSummaryRows(currentGameLaunchEnvelope),
...buildLaunchConfigSummaryRows(currentGameLaunchEnvelope),
{ label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
{ label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
{ label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
@@ -2471,7 +2816,9 @@ Page({
resultSceneSubtitle: snapshot.subtitle,
resultSceneHeroLabel: snapshot.heroLabel,
resultSceneHeroValue: snapshot.heroValue,
resultSceneRows: snapshot.rows.concat(buildRuntimeSummaryRows(currentGameLaunchEnvelope)),
resultSceneRows: snapshot.rows
.concat(buildRuntimeSummaryRows(currentGameLaunchEnvelope))
.concat(buildLaunchConfigSummaryRows(currentGameLaunchEnvelope)),
})
},

View File

@@ -158,7 +158,7 @@
<cover-view class="map-content-entry__text">{{pendingContentEntryText}}</cover-view>
</cover-view>
<cover-view class="screen-button-layer screen-button-layer--start-left" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showResultScene && !showSystemSettingsPanel && showBottomDebugButton && gameSessionStatus !== 'running'}}" bindtap="handleStartGame">
<cover-view class="screen-button-layer screen-button-layer--start-left" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showResultScene && !showSystemSettingsPanel && showBottomDebugButton && showStartEntryButton && gameSessionStatus !== 'running'}}" bindtap="handleStartGame">
<cover-view class="screen-button-layer__text screen-button-layer__text--start">开始</cover-view>
</cover-view>

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,