@windrun-huaiin/third-ui 14.0.3 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,6 +21,7 @@ function useFingerprint(config) {
21
21
  isLoading: false,
22
22
  isInitialized: false,
23
23
  error: 'Server-side rendering is not supported',
24
+ clearError: () => { },
24
25
  initializeAnonymousUser: () => tslib_es6.__awaiter(this, void 0, void 0, function* () { }),
25
26
  refreshUserData: () => tslib_es6.__awaiter(this, void 0, void 0, function* () { }),
26
27
  };
@@ -32,6 +33,11 @@ function useFingerprint(config) {
32
33
  const [isLoading, setIsLoading] = React.useState(true);
33
34
  const [isInitialized, setIsInitialized] = React.useState(false);
34
35
  const [error, setError] = React.useState(null);
36
+ const isInitializingAnonymousUserRef = React.useRef(false);
37
+ const requestedAnonymousFingerprintRef = React.useRef(null);
38
+ const clearError = React.useCallback(() => {
39
+ setError(null);
40
+ }, []);
35
41
  /**
36
42
  * 第一阶段:初始化fingerprint ID
37
43
  */
@@ -60,7 +66,17 @@ function useFingerprint(config) {
60
66
  setError('Cannot initialize user: Missing fingerprint ID');
61
67
  return;
62
68
  }
69
+ if (isInitializingAnonymousUserRef.current) {
70
+ console.log('Skipping anonymous user initialization because a request is already in flight:', fingerprintId);
71
+ return;
72
+ }
73
+ if (requestedAnonymousFingerprintRef.current === fingerprintId && isInitialized) {
74
+ console.log('Skipping anonymous user initialization because fingerprint is already initialized:', fingerprintId);
75
+ return;
76
+ }
63
77
  try {
78
+ isInitializingAnonymousUserRef.current = true;
79
+ requestedAnonymousFingerprintRef.current = fingerprintId;
64
80
  setIsLoading(true);
65
81
  setError(null);
66
82
  console.log('Initializing anonymous user with fingerprintId:', fingerprintId);
@@ -92,13 +108,15 @@ function useFingerprint(config) {
92
108
  }
93
109
  }
94
110
  catch (err) {
111
+ requestedAnonymousFingerprintRef.current = null;
95
112
  console.error('Failed to initialize anonymous user:', err);
96
113
  setError(err instanceof Error ? err.message : 'Unknown error');
97
114
  }
98
115
  finally {
116
+ isInitializingAnonymousUserRef.current = false;
99
117
  setIsLoading(false);
100
118
  }
101
- }), [fingerprintId, config.apiEndpoint]);
119
+ }), [fingerprintId, config.apiEndpoint, isInitialized]);
102
120
  /**
103
121
  * 刷新用户数据 - 使用POST请求(后端支持upsert逻辑)
104
122
  */
@@ -157,6 +175,7 @@ function useFingerprint(config) {
157
175
  isLoading,
158
176
  isInitialized,
159
177
  error,
178
+ clearError,
160
179
  initializeAnonymousUser,
161
180
  refreshUserData,
162
181
  };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { __awaiter } from '../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.mjs';
3
- import { useState, useCallback, useEffect } from 'react';
3
+ import { useState, useRef, useCallback, useEffect } from 'react';
4
4
  import { getOrCreateFirstTouchData, getOrGenerateFingerprintId, createFingerprintHeaders } from './fingerprint-client.mjs';
5
5
  import { FINGERPRINT_SOURCE_REFER } from './fingerprint-shared.mjs';
6
6
 
@@ -19,6 +19,7 @@ function useFingerprint(config) {
19
19
  isLoading: false,
20
20
  isInitialized: false,
21
21
  error: 'Server-side rendering is not supported',
22
+ clearError: () => { },
22
23
  initializeAnonymousUser: () => __awaiter(this, void 0, void 0, function* () { }),
23
24
  refreshUserData: () => __awaiter(this, void 0, void 0, function* () { }),
24
25
  };
@@ -30,6 +31,11 @@ function useFingerprint(config) {
30
31
  const [isLoading, setIsLoading] = useState(true);
31
32
  const [isInitialized, setIsInitialized] = useState(false);
32
33
  const [error, setError] = useState(null);
34
+ const isInitializingAnonymousUserRef = useRef(false);
35
+ const requestedAnonymousFingerprintRef = useRef(null);
36
+ const clearError = useCallback(() => {
37
+ setError(null);
38
+ }, []);
33
39
  /**
34
40
  * 第一阶段:初始化fingerprint ID
35
41
  */
@@ -58,7 +64,17 @@ function useFingerprint(config) {
58
64
  setError('Cannot initialize user: Missing fingerprint ID');
59
65
  return;
60
66
  }
67
+ if (isInitializingAnonymousUserRef.current) {
68
+ console.log('Skipping anonymous user initialization because a request is already in flight:', fingerprintId);
69
+ return;
70
+ }
71
+ if (requestedAnonymousFingerprintRef.current === fingerprintId && isInitialized) {
72
+ console.log('Skipping anonymous user initialization because fingerprint is already initialized:', fingerprintId);
73
+ return;
74
+ }
61
75
  try {
76
+ isInitializingAnonymousUserRef.current = true;
77
+ requestedAnonymousFingerprintRef.current = fingerprintId;
62
78
  setIsLoading(true);
63
79
  setError(null);
64
80
  console.log('Initializing anonymous user with fingerprintId:', fingerprintId);
@@ -90,13 +106,15 @@ function useFingerprint(config) {
90
106
  }
91
107
  }
92
108
  catch (err) {
109
+ requestedAnonymousFingerprintRef.current = null;
93
110
  console.error('Failed to initialize anonymous user:', err);
94
111
  setError(err instanceof Error ? err.message : 'Unknown error');
95
112
  }
96
113
  finally {
114
+ isInitializingAnonymousUserRef.current = false;
97
115
  setIsLoading(false);
98
116
  }
99
- }), [fingerprintId, config.apiEndpoint]);
117
+ }), [fingerprintId, config.apiEndpoint, isInitialized]);
100
118
  /**
101
119
  * 刷新用户数据 - 使用POST请求(后端支持upsert逻辑)
102
120
  */
@@ -155,6 +173,7 @@ function useFingerprint(config) {
155
173
  isLoading,
156
174
  isInitialized,
157
175
  error,
176
+ clearError,
158
177
  initializeAnonymousUser,
159
178
  refreshUserData,
160
179
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "14.0.3",
3
+ "version": "14.1.0",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -7,6 +7,8 @@ import React, { createContext, useContext, useEffect, useMemo, useRef, useState
7
7
  import type { FingerprintContextType, FingerprintProviderProps } from './types';
8
8
  import { useFingerprint } from './use-fingerprint';
9
9
  import { CopyableText } from '@windrun-huaiin/base-ui/ui';
10
+ import { createFingerprintHeaders, setFingerprintId } from './fingerprint-client';
11
+ import { FINGERPRINT_SOURCE_REFER } from './fingerprint-shared';
10
12
 
11
13
  const FingerprintContext = createContext<FingerprintContextType | undefined>(undefined);
12
14
 
@@ -73,10 +75,16 @@ export function FingerprintStatus() {
73
75
  xUser,
74
76
  xCredit,
75
77
  xSubscription,
76
- error
78
+ error,
79
+ clearError,
80
+ initializeAnonymousUser,
77
81
  } = useFingerprintContext();
78
82
 
79
83
  const [isOpen, setIsOpen] = useState(false);
84
+ const [panelMode, setPanelMode] = useState<'info' | 'test'>('info');
85
+ const [testFingerprintId, setTestFingerprintId] = useState('');
86
+ const [testResult, setTestResult] = useState<string>('');
87
+ const [isRunningTest, setIsRunningTest] = useState(false);
80
88
  const modalRef = useRef<HTMLDivElement>(null);
81
89
 
82
90
  const handleToggle = () => setIsOpen(prev => !prev);
@@ -99,12 +107,21 @@ export function FingerprintStatus() {
99
107
  }
100
108
  }, [xUser]);
101
109
 
110
+ useEffect(() => {
111
+ if (testFingerprintId) {
112
+ return;
113
+ }
114
+
115
+ const defaultFingerprintId = buildDebugFingerprintId();
116
+ setTestFingerprintId(defaultFingerprintId);
117
+ }, [testFingerprintId]);
118
+
102
119
  const creditBuckets = useMemo(() => {
103
120
  if (!xCredit) return [];
104
121
  return [
105
122
  {
106
123
  key: 'paid',
107
- label: '订阅积分',
124
+ label: 'Paid',
108
125
  icon: <icons.Settings2 className="size-4 text-green-500 dark:text-green-300" />,
109
126
  balance: xCredit.balancePaid,
110
127
  total: xCredit.totalPaidLimit,
@@ -113,7 +130,7 @@ export function FingerprintStatus() {
113
130
  },
114
131
  {
115
132
  key: 'oneTimePaid',
116
- label: '一次性积分',
133
+ label: 'OneTimePaid',
117
134
  icon: <icons.Coins className="size-4 text-amber-500 dark:text-amber-300" />,
118
135
  balance: xCredit.balanceOneTimePaid,
119
136
  total: xCredit.totalOneTimePaidLimit,
@@ -122,7 +139,7 @@ export function FingerprintStatus() {
122
139
  },
123
140
  {
124
141
  key: 'free',
125
- label: '免费积分',
142
+ label: 'Free',
126
143
  icon: <icons.Gift className="size-4 text-purple-500 dark:text-purple-300" />,
127
144
  balance: xCredit.balanceFree,
128
145
  total: xCredit.totalFreeLimit,
@@ -135,10 +152,10 @@ export function FingerprintStatus() {
135
152
  const subscriptionStatus = useMemo(() => {
136
153
  if (!xSubscription) {
137
154
  return {
138
- status: '未订阅',
155
+ status: 'Never',
139
156
  priceName: '--',
140
157
  creditsAllocated: '--',
141
- period: '无记录',
158
+ period: 'Unavailable',
142
159
  };
143
160
  }
144
161
  return {
@@ -154,6 +171,114 @@ export function FingerprintStatus() {
154
171
  const userStatus = xUser?.status || '--';
155
172
  const totalCredits = formatNumber(xCredit?.totalBalance);
156
173
  const subStatus = subscriptionStatus.status;
174
+ const themedGhostButtonClass = cn(
175
+ 'border-slate-200 bg-white/90 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950/80 dark:hover:bg-slate-900',
176
+ 'hover:border-current',
177
+ themeIconColor
178
+ );
179
+
180
+ const runContextParallelInitTest = async () => {
181
+ setIsRunningTest(true);
182
+ setTestResult('Running context parallel init x3...');
183
+
184
+ try {
185
+ await Promise.all([
186
+ initializeAnonymousUser(),
187
+ initializeAnonymousUser(),
188
+ initializeAnonymousUser(),
189
+ ]);
190
+ setTestResult(`Context parallel init finished. Active fingerprint: ${fingerprintId || '--'}`);
191
+ } catch (testError) {
192
+ setTestResult(`Context parallel init failed: ${formatErrorMessage(testError)}`);
193
+ } finally {
194
+ setIsRunningTest(false);
195
+ }
196
+ };
197
+
198
+ const runRawParallelPostTest = async () => {
199
+ const normalizedFingerprintId = testFingerprintId.trim();
200
+ if (!normalizedFingerprintId) {
201
+ setTestResult('Please input a valid test fingerprint id.');
202
+ return;
203
+ }
204
+
205
+ setIsRunningTest(true);
206
+ setTestResult(`Running raw POST x3 with fingerprint: ${normalizedFingerprintId}`);
207
+
208
+ try {
209
+ const fingerprintHeaders = await createFingerprintHeaders();
210
+ const requests = Array.from({ length: 3 }, () => fetch('/api/user/anonymous/init', {
211
+ method: 'POST',
212
+ headers: {
213
+ 'Content-Type': 'application/json',
214
+ [FINGERPRINT_SOURCE_REFER]: document.referrer || '',
215
+ ...fingerprintHeaders,
216
+ 'x-fingerprint-id-v8': normalizedFingerprintId,
217
+ },
218
+ body: JSON.stringify({ fingerprintId: normalizedFingerprintId }),
219
+ }));
220
+
221
+ const responses = await Promise.all(requests);
222
+ const payloads = await Promise.all(responses.map(async (response) => {
223
+ const data = await response.json().catch(() => ({}));
224
+ return {
225
+ ok: response.ok,
226
+ status: response.status,
227
+ isNewUser: typeof data?.isNewUser === 'boolean' ? data.isNewUser : null,
228
+ userId: typeof data?.xUser?.userId === 'string' ? data.xUser.userId : '--',
229
+ fingerprintId: typeof data?.xUser?.fingerprintId === 'string' ? data.xUser.fingerprintId : normalizedFingerprintId,
230
+ error: typeof data?.error === 'string' ? data.error : null,
231
+ };
232
+ }));
233
+
234
+ const createdUserIds = payloads
235
+ .filter((payload) => payload.ok && payload.isNewUser === true && payload.userId !== '--')
236
+ .map((payload) => payload.userId);
237
+ const reusedUserIds = payloads
238
+ .filter((payload) => payload.ok && payload.isNewUser === false && payload.userId !== '--')
239
+ .map((payload) => payload.userId);
240
+ const failedStatuses = payloads
241
+ .filter((payload) => !payload.ok)
242
+ .map((payload) => `${payload.status}${payload.error ? `:${payload.error}` : ''}`);
243
+
244
+ setTestResult(
245
+ [
246
+ `Raw POST x3 done.`,
247
+ `created=${createdUserIds.length}`,
248
+ `reused=${reusedUserIds.length}`,
249
+ `failed=${failedStatuses.length}`,
250
+ `createdUserIds=[${createdUserIds.join(', ')}]`,
251
+ failedStatuses.length > 0 ? `failedStatuses=[${failedStatuses.join(', ')}]` : null,
252
+ ].filter(Boolean).join('\n')
253
+ );
254
+ } catch (testError) {
255
+ setTestResult(`Raw POST test failed: ${formatErrorMessage(testError)}`);
256
+ } finally {
257
+ setIsRunningTest(false);
258
+ }
259
+ };
260
+
261
+ const applyTestFingerprintAndReload = () => {
262
+ const normalizedFingerprintId = testFingerprintId.trim();
263
+ if (!normalizedFingerprintId) {
264
+ setTestResult('Please input a valid test fingerprint id before applying.');
265
+ return;
266
+ }
267
+
268
+ try {
269
+ setFingerprintId(normalizedFingerprintId);
270
+ setTestResult(`Applied test fingerprint and reloading: ${normalizedFingerprintId}`);
271
+ window.location.reload();
272
+ } catch (testError) {
273
+ setTestResult(`Apply test fingerprint failed: ${formatErrorMessage(testError)}`);
274
+ }
275
+ };
276
+
277
+ const regenerateTestFingerprint = () => {
278
+ const nextFingerprintId = buildDebugFingerprintId();
279
+ setTestFingerprintId(nextFingerprintId);
280
+ setTestResult(`Generated test fingerprint: ${nextFingerprintId}`);
281
+ };
157
282
 
158
283
  return (
159
284
  <>
@@ -193,94 +318,213 @@ export function FingerprintStatus() {
193
318
  <icons.ShieldUser className="size-4" />
194
319
  Fingerprint Debug Panel
195
320
  </div>
196
- <button
197
- type="button"
198
- aria-label="Close fingerprint panel"
199
- className="rounded-full p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white"
200
- onClick={() => setIsOpen(false)}
201
- >
202
- <icons.X className="size-4" />
203
- </button>
321
+ <div className="flex items-center gap-2">
322
+ <button
323
+ type="button"
324
+ onClick={() => setPanelMode((prev) => prev === 'info' ? 'test' : 'info')}
325
+ className={cn(
326
+ 'inline-flex items-center gap-2 rounded-full border px-2 py-1 text-[11px] font-semibold shadow-sm transition-all duration-200',
327
+ panelMode === 'test'
328
+ ? cn('border-transparent text-white', themeButtonGradientClass, themeButtonGradientHoverClass)
329
+ : themedGhostButtonClass
330
+ )}
331
+ aria-pressed={panelMode === 'test'}
332
+ >
333
+ <span>Concurrent Test</span>
334
+ <span
335
+ className={cn(
336
+ 'relative inline-flex h-5 w-9 items-center rounded-full transition-colors',
337
+ panelMode === 'test'
338
+ ? 'bg-white/25'
339
+ : 'bg-slate-300 dark:bg-slate-700'
340
+ )}
341
+ >
342
+ <span
343
+ className={cn(
344
+ 'inline-block size-4 rounded-full shadow-sm transition-transform',
345
+ panelMode === 'test' ? 'bg-white' : 'bg-white dark:bg-slate-100',
346
+ panelMode === 'test' ? 'translate-x-4' : 'translate-x-0.5'
347
+ )}
348
+ />
349
+ </span>
350
+ </button>
351
+ <button
352
+ type="button"
353
+ aria-label="Close fingerprint panel"
354
+ className="rounded-full p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white"
355
+ onClick={() => setIsOpen(false)}
356
+ >
357
+ <icons.X className="size-4" />
358
+ </button>
359
+ </div>
204
360
  </div>
205
361
  </header>
206
362
 
207
363
  <section className="space-y-1">
208
- {/* 用户信息 */}
209
- <PanelSection
210
- icon={<icons.Fingerprint className="size-4" />}
211
- title="用户信息"
212
- rightInfo={<StatusTag value={userStatus} />}
213
- items={[
214
- { label: '用户ID', value: <CopyableText text={xUser?.userId || ''} /> },
215
- { label: '用户昵称', value: <CopyableText text={xUser?.userName || ''} /> },
216
- { label: 'FingerprintID', value: <CopyableText text={xUser?.fingerprintId || fingerprintId || ''} /> },
217
- { label: 'Clerk用户', value: <CopyableText text={xUser?.clerkUserId || ''} /> },
218
- { label: '邮箱', value: <CopyableText text={xUser?.email || ''} /> },
219
- { label: 'Stripe客户', value: <CopyableText text={xUser?.stripeCusId || ''} /> },
220
- { label: '创建时间', value: xUser?.createdAt || '--' },
221
- ]}
222
- />
223
-
224
- {/* 积分信息 */}
225
- <div className="space-y-2 rounded-xl border border-slate-200/70 bg-white/80 p-4 shadow-sm dark:border-white/12 dark:bg-slate-900/50">
226
- <PanelHeader
227
- icon={<icons.Gem className="size-4" />}
228
- title="积分信息"
229
- rightInfo={<span className={cn("font-semibold", themeIconColor)}>{totalCredits}</span>}
230
- />
231
- <div className="space-y-3">
232
- {creditBuckets.length > 0 ? (
233
- creditBuckets.map((bucket) => {
234
- const percent = Math.round(computeProgress(bucket.balance, bucket.total) * 100);
235
- return (
236
- <div key={bucket.key} className="rounded-lg border border-slate-200/70 bg-white/70 p-3 dark:border-white/10 dark:bg-slate-900/40">
237
- <div className="flex items-center justify-between text-xs font-medium text-slate-600 dark:text-slate-300">
238
- <div className="flex items-center gap-1.5">
239
- {bucket.icon}
240
- <span>{bucket.label}</span>
364
+ {panelMode === 'info' ? (
365
+ <>
366
+ <PanelSection
367
+ icon={<icons.Fingerprint className="size-4" />}
368
+ title="User"
369
+ rightInfo={<StatusTag value={userStatus} />}
370
+ items={[
371
+ { label: 'UserID', value: <CopyableText text={xUser?.userId || ''} /> },
372
+ { label: 'NickName', value: <CopyableText text={xUser?.userName || ''} /> },
373
+ { label: 'FingerprintID', value: <CopyableText text={xUser?.fingerprintId || fingerprintId || ''} /> },
374
+ { label: 'ClerkUserID', value: <CopyableText text={xUser?.clerkUserId || ''} /> },
375
+ { label: 'Email', value: <CopyableText text={xUser?.email || ''} /> },
376
+ { label: 'StripeCusID', value: <CopyableText text={xUser?.stripeCusId || ''} /> },
377
+ { label: 'CreatedAt', value: xUser?.createdAt || '--' },
378
+ ]}
379
+ />
380
+
381
+ <div className="space-y-2 rounded-xl border border-slate-200/70 bg-white/80 p-4 shadow-sm dark:border-white/12 dark:bg-slate-900/50">
382
+ <PanelHeader
383
+ icon={<icons.Gem className="size-4" />}
384
+ title="Credits Info"
385
+ rightInfo={<span className={cn("font-semibold", themeIconColor)}>{totalCredits}</span>}
386
+ />
387
+ <div className="space-y-3">
388
+ {creditBuckets.length > 0 ? (
389
+ creditBuckets.map((bucket) => {
390
+ const percent = Math.round(computeProgress(bucket.balance, bucket.total) * 100);
391
+ return (
392
+ <div key={bucket.key} className="rounded-lg border border-slate-200/70 bg-white/70 p-3 dark:border-white/10 dark:bg-slate-900/40">
393
+ <div className="flex items-center justify-between text-xs font-medium text-slate-600 dark:text-slate-300">
394
+ <div className="flex items-center gap-1.5">
395
+ {bucket.icon}
396
+ <span>{bucket.label}</span>
397
+ </div>
398
+ <span className="font-semibold text-slate-700 dark:text-slate-100">
399
+ {formatNumber(bucket.balance)} / {formatNumber(bucket.total)}
400
+ </span>
401
+ </div>
402
+ <div className="mt-2 h-1.5 w-full rounded-full bg-slate-200 dark:bg-slate-800">
403
+ <div
404
+ className="h-full rounded-full bg-linear-to-r from-purple-500 via-pink-500 to-rose-400 transition-[width]"
405
+ style={{ width: `${percent}%` }}
406
+ />
407
+ </div>
408
+ <div className="mt-2 flex items-center justify-between text-[11px] text-slate-500 dark:text-slate-400">
409
+ <span>{formatRangeText(bucket.start, bucket.end)}</span>
410
+ <span>{percent}%</span>
411
+ </div>
241
412
  </div>
242
- <span className="font-semibold text-slate-700 dark:text-slate-100">
243
- {formatNumber(bucket.balance)} / {formatNumber(bucket.total)}
244
- </span>
245
- </div>
246
- <div className="mt-2 h-1.5 w-full rounded-full bg-slate-200 dark:bg-slate-800">
247
- <div
248
- className="h-full rounded-full bg-linear-to-r from-purple-500 via-pink-500 to-rose-400 transition-[width]"
249
- style={{ width: `${percent}%` }}
250
- />
251
- </div>
252
- <div className="mt-2 flex items-center justify-between text-[11px] text-slate-500 dark:text-slate-400">
253
- <span>{formatRangeText(bucket.start, bucket.end)}</span>
254
- <span>{percent}%</span>
255
- </div>
256
- </div>
257
- );
258
- })
259
- ) : (
260
- <EmptyPlaceholder label="暂无积分数据" icon={<icons.DatabaseZap className="size-4" />} />
261
- )}
413
+ );
414
+ })
415
+ ) : (
416
+ <EmptyPlaceholder label="No Credits Yet" icon={<icons.DatabaseZap className="size-4" />} />
417
+ )}
418
+ </div>
419
+ </div>
420
+
421
+ <PanelSection
422
+ icon={<icons.Bell className="size-4" />}
423
+ title="Subscription"
424
+ rightInfo={<StatusTag value={subStatus} />}
425
+ items={[
426
+ { label: 'Plan', value: subscriptionStatus.priceName },
427
+ { label: 'Period', value: subscriptionStatus.period },
428
+ { label: 'Allocated', value: subscriptionStatus.creditsAllocated },
429
+ { label: 'SubID', value: <CopyableText text={xSubscription?.paySubscriptionId || ''} /> },
430
+ { label: 'OrderID', value: <CopyableText text={xSubscription?.orderId || ''} /> },
431
+ { label: 'PriceID', value: <CopyableText text={xSubscription?.priceId || ''} /> },
432
+ ]}
433
+ />
434
+ </>
435
+ ) : (
436
+ <div className="space-y-3 rounded-xl border border-slate-200/70 bg-white/85 p-4 shadow-sm dark:border-white/12 dark:bg-slate-900/45">
437
+ <PanelHeader
438
+ icon={<icons.DatabaseZap className="size-4" />}
439
+ title="Concurrent Base Info"
440
+ rightInfo={<StatusTag value={isRunningTest ? 'pending' : 'idle'} />}
441
+ />
442
+
443
+ <div className="space-y-2 text-xs text-slate-500 dark:text-slate-300">
444
+ <div className="flex items-center justify-between gap-3">
445
+ <span className="text-slate-400 dark:text-slate-500">Current Browser</span>
446
+ <CopyableText text={fingerprintId || ''} />
447
+ </div>
448
+ <div className="space-y-1">
449
+ <span className="text-slate-400 dark:text-slate-500">Generate New</span>
450
+ <div className="flex items-center gap-2 py-1">
451
+ <input
452
+ value={testFingerprintId}
453
+ onChange={(e) => setTestFingerprintId(e.target.value)}
454
+ className="min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-3 py-2 font-mono text-[0.5rem] sm:text-[0.625rem] md:text-xs leading-tight text-slate-700 outline-none transition focus:border-slate-400 dark:border-white/10 dark:bg-slate-950 dark:text-slate-100"
455
+ placeholder="fp_test_dbg_20260322_xxx"
456
+ />
457
+ <button
458
+ type="button"
459
+ disabled={isRunningTest}
460
+ onClick={regenerateTestFingerprint}
461
+ aria-label="Generate new test fingerprint"
462
+ className="inline-flex size-9 items-center justify-center rounded-lg border border-slate-200 bg-slate-50 text-slate-700 transition hover:border-slate-300 hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/10 dark:bg-slate-950 dark:text-slate-100 dark:hover:bg-slate-900"
463
+ >
464
+ <icons.RefreshCcw className="size-4" />
465
+ </button>
466
+ <button
467
+ type="button"
468
+ disabled={isRunningTest}
469
+ onClick={applyTestFingerprintAndReload}
470
+ aria-label="Apply test fingerprint"
471
+ className="inline-flex size-9 items-center justify-center rounded-lg border border-slate-200 bg-slate-50 text-slate-700 transition hover:border-slate-300 hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/10 dark:bg-slate-950 dark:text-slate-100 dark:hover:bg-slate-900"
472
+ >
473
+ <icons.CheckCheck className="size-4" />
474
+ </button>
475
+ </div>
476
+ </div>
477
+ </div>
478
+
479
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
480
+ <button
481
+ type="button"
482
+ disabled={isRunningTest}
483
+ onClick={runContextParallelInitTest}
484
+ className={cn(
485
+ 'shrink-0 rounded-full border px-3 py-2 text-xs font-semibold transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-50',
486
+ themedGhostButtonClass
487
+ )}
488
+ >
489
+ Frontend Prevention Test
490
+ </button>
491
+ <button
492
+ type="button"
493
+ disabled={isRunningTest}
494
+ onClick={runRawParallelPostTest}
495
+ className={cn(
496
+ 'shrink-0 rounded-full border px-3 py-2 text-xs font-semibold text-white shadow-sm transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-50',
497
+ 'border-transparent',
498
+ themeButtonGradientClass,
499
+ themeButtonGradientHoverClass
500
+ )}
501
+ >
502
+ Backend Idempotency Test
503
+ </button>
504
+ </div>
505
+
506
+ <div className="rounded-lg border border-dashed border-slate-200 bg-slate-50/80 p-3 dark:border-white/10 dark:bg-slate-950/50">
507
+ <pre className="overflow-x-auto whitespace-pre-wrap break-all font-mono text-[11px] leading-5 text-slate-600 dark:text-slate-300">
508
+ {testResult || 'No test executed yet.'}
509
+ </pre>
510
+ </div>
262
511
  </div>
263
- </div>
264
-
265
- {/* 订阅信息 */}
266
- <PanelSection
267
- icon={<icons.Bell className="size-4" />}
268
- title="订阅信息"
269
- rightInfo={<StatusTag value={subStatus} />}
270
- items={[
271
- { label: '订阅方案', value: subscriptionStatus.priceName },
272
- { label: '有效期', value: subscriptionStatus.period },
273
- { label: '分配额度', value: subscriptionStatus.creditsAllocated },
274
- { label: '订阅ID', value: <CopyableText text={xSubscription?.paySubscriptionId || ''} /> },
275
- { label: 'OrderID', value: <CopyableText text={xSubscription?.orderId || ''} /> },
276
- { label: 'Price ID', value: <CopyableText text={xSubscription?.priceId || ''} /> },
277
- ]}
278
- />
512
+ )}
279
513
 
280
514
  {error && (
281
- <div className="flex items-start gap-2 rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs text-amber-600 shadow-sm dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-200">
282
- <icons.X className="mt-0.5 size-4" />
283
- <span>{error}</span>
515
+ <div className="flex items-start justify-between gap-3 rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs text-amber-600 shadow-sm dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-200">
516
+ <div className="flex items-start gap-2">
517
+ <icons.X className="mt-0.5 size-4 shrink-0" />
518
+ <span>{error}</span>
519
+ </div>
520
+ <button
521
+ type="button"
522
+ aria-label="Dismiss error"
523
+ onClick={clearError}
524
+ className="shrink-0 rounded-full p-1 text-amber-500 transition hover:bg-amber-100 hover:text-amber-700 dark:text-amber-200 dark:hover:bg-amber-500/10 dark:hover:text-amber-100"
525
+ >
526
+ <icons.X className="size-4" />
527
+ </button>
284
528
  </div>
285
529
  )}
286
530
  </section>
@@ -367,7 +611,7 @@ function formatRangeText(start: string | null | undefined, end: string | null |
367
611
  const safeEnd = end && end.trim() ? end : '';
368
612
 
369
613
  if (!safeStart && !safeEnd) {
370
- return '无记录';
614
+ return 'No records';
371
615
  }
372
616
 
373
617
  if (!safeStart) {
@@ -418,3 +662,21 @@ function StatusTag({ value }: { value: string | undefined | null }) {
418
662
  </span>
419
663
  );
420
664
  }
665
+
666
+ function buildDebugFingerprintId() {
667
+ const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
668
+ const randomSuffix = Math.random().toString(36).slice(2, 8);
669
+ return `fp_test_dbg_${timestamp}_${randomSuffix}`;
670
+ }
671
+
672
+ function formatErrorMessage(error: unknown) {
673
+ if (error instanceof Error) {
674
+ return error.message;
675
+ }
676
+
677
+ if (typeof error === 'string') {
678
+ return error;
679
+ }
680
+
681
+ return 'Unknown error';
682
+ }