完善联调标准化与诊断链路
This commit is contained in:
@@ -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>,
|
||||
})
|
||||
}
|
||||
|
||||
90
miniprogram/utils/backendClientLogs.ts
Normal file
90
miniprogram/utils/backendClientLogs.ts
Normal 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()
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
88
miniprogram/utils/globalMockDebugBridge.ts
Normal file
88
miniprogram/utils/globalMockDebugBridge.ts
Normal 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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user