修复小程序内充值设备识别

为 API 请求和刷新请求补充宿主运行时请求头。

在小程序充值设备身份异常时清理旧 token 并重新拉起小程序登录。

补充小程序充值回归测试并隔离测试 spy。
This commit is contained in:
kdletters
2026-07-09 16:27:43 +08:00
parent 67fee3718d
commit c022bbb8a0
4 changed files with 221 additions and 9 deletions
+59 -3
View File
@@ -8,12 +8,17 @@ import {
parseApiErrorMessage,
unwrapApiResponse,
} from '../../packages/shared/src/http';
import { getHostRuntime } from './host-bridge/hostBridge';
const ACCESS_TOKEN_KEY = 'genarrative.auth.access-token.v1';
export const AUTH_STATE_EVENT = 'genarrative-auth-state-changed';
const REQUEST_ID_HEADER = 'x-request-id';
const API_VERSION_HEADER = 'x-api-version';
const ROUTE_VERSION_HEADER = 'x-route-version';
const CLIENT_TYPE_HEADER = 'x-client-type';
const CLIENT_RUNTIME_HEADER = 'x-client-runtime';
const CLIENT_PLATFORM_HEADER = 'x-client-platform';
const MINI_PROGRAM_ENV_HEADER = 'x-mini-program-env';
const DEFAULT_RETRYABLE_STATUS_CODES = [408, 425, 429, 502, 503, 504];
const DEFAULT_SAFE_RETRY_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
@@ -101,6 +106,57 @@ function normalizeHeaders(headers?: HeadersInit) {
return nextHeaders;
}
function hasHeader(headers: Record<string, string>, name: string) {
const normalizedName = name.toLowerCase();
return Object.keys(headers).some((key) => key.toLowerCase() === normalizedName);
}
function setHeaderIfMissing(
headers: Record<string, string>,
name: string,
value: string | null | undefined,
) {
const trimmedValue = value?.trim();
if (!trimmedValue || hasHeader(headers, name)) {
return;
}
headers[name] = trimmedValue;
}
function attachHostRuntimeHeaders(headers: Record<string, string>) {
let runtime: ReturnType<typeof getHostRuntime>;
try {
runtime = getHostRuntime();
} catch {
return headers;
}
if (runtime.kind === 'wechat_mini_program') {
setHeaderIfMissing(headers, CLIENT_TYPE_HEADER, 'mini_program');
setHeaderIfMissing(
headers,
CLIENT_RUNTIME_HEADER,
runtime.clientRuntime || 'wechat_mini_program',
);
setHeaderIfMissing(headers, CLIENT_PLATFORM_HEADER, runtime.hostPlatform);
setHeaderIfMissing(headers, MINI_PROGRAM_ENV_HEADER, runtime.miniProgramEnv);
return headers;
}
if (runtime.kind === 'native_app') {
setHeaderIfMissing(headers, CLIENT_TYPE_HEADER, 'native_app');
setHeaderIfMissing(
headers,
CLIENT_RUNTIME_HEADER,
runtime.clientRuntime || 'native_app',
);
setHeaderIfMissing(headers, CLIENT_PLATFORM_HEADER, runtime.hostPlatform);
}
return headers;
}
function buildClientRequestId() {
const randomId =
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
@@ -502,7 +558,7 @@ function withAuthorizationHeaders(
headers?: HeadersInit,
options: Pick<ApiRequestOptions, 'omitEnvelopeHeader' | 'skipAuth'> = {},
) {
const nextHeaders = normalizeHeaders(headers);
const nextHeaders = attachHostRuntimeHeaders(normalizeHeaders(headers));
const token = getStoredAccessToken();
if (token && !options.skipAuth) {
nextHeaders.Authorization = `Bearer ${token}`;
@@ -531,9 +587,9 @@ async function refreshAccessToken() {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'same-origin',
headers: {
headers: attachHostRuntimeHeaders({
[API_RESPONSE_ENVELOPE_HEADER]: API_RESPONSE_ENVELOPE_VERSION,
},
}),
});
if (!response.ok) {