Skip to content

Commit be7e736

Browse files
committed
fix: align TouchGal API fallback behavior
1 parent 4f80c90 commit be7e736

16 files changed

Lines changed: 239 additions & 127 deletions

src/main/developerApi.ts

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ interface DeveloperSearchPagination {
3939
has_more?: boolean
4040
}
4141

42+
interface DeveloperApiStatusValue extends Record<string, unknown> {
43+
configured: boolean
44+
usable: boolean
45+
isDeveloperApiCredential: true
46+
applicationStatus: string
47+
dailyLimit: unknown
48+
minuteLimit: unknown
49+
}
50+
4251
interface DeveloperSearchOptions {
4352
hydrateDetails?: boolean
4453
}
@@ -581,11 +590,24 @@ const normalizeDeveloperPagination = (
581590
): DeveloperSearchResult['pagination'] => {
582591
if (!pagination) return null
583592

593+
const page = normalizeDeveloperNumber(pagination.page) ?? undefined
594+
const limit = normalizeDeveloperNumber(pagination.limit) ?? undefined
595+
const total = normalizeDeveloperNumber(pagination.total) ?? fallbackTotal
596+
const explicitHasMore = typeof pagination.hasMore === 'boolean'
597+
? pagination.hasMore
598+
: typeof pagination.has_more === 'boolean'
599+
? pagination.has_more
600+
: null
601+
584602
return {
585-
page: normalizeDeveloperNumber(pagination.page) ?? undefined,
586-
limit: normalizeDeveloperNumber(pagination.limit) ?? undefined,
587-
total: normalizeDeveloperNumber(pagination.total) ?? fallbackTotal,
588-
hasMore: Boolean(pagination.hasMore ?? pagination.has_more),
603+
page,
604+
limit,
605+
total,
606+
hasMore: explicitHasMore ?? (
607+
typeof page === 'number' && typeof limit === 'number'
608+
? page * limit < total
609+
: false
610+
),
589611
}
590612
}
591613

@@ -740,7 +762,14 @@ const detailCache = new Map<string, Promise<ReturnType<typeof normalizeDeveloper
740762
const searchCache = new Map<string, { expiresAt: number; value: DeveloperSearchResult }>()
741763
const searchInFlightCache = new Map<string, Promise<DeveloperSearchResult>>()
742764
const resourceCache = new Map<string, Promise<ReturnType<typeof normalizeDeveloperGameResources>>>()
743-
let statusCache: { expiresAt: number; value: Record<string, unknown> } | null = null
765+
let statusCache: { expiresAt: number; value: DeveloperApiStatusValue } | null = null
766+
let developerApiUsable = false
767+
768+
export const isTouchGalDeveloperApiUsable = () =>
769+
isTouchGalDeveloperApiConfigured() && developerApiUsable
770+
771+
export const shouldAttemptTouchGalDeveloperApi = () =>
772+
isTouchGalDeveloperApiConfigured() && statusCache?.value.usable !== false
744773

745774
const runLimited = async <T, R>(
746775
items: T[],
@@ -894,21 +923,41 @@ export const fetchDeveloperGameResources = async (uniqueId: string) => {
894923
export const fetchDeveloperApiStatus = async () => {
895924
if (statusCache && statusCache.expiresAt > Date.now()) return statusCache.value
896925

897-
const client = createDeveloperApiClient()
898-
const response = await requestDeveloperApi(() =>
899-
client.get<DeveloperApiResponse<Record<string, unknown>>>('/me')
900-
)
901-
const data = unwrapDeveloperResponse(response.data)
902-
updateDeveloperRequestLimit(getDeveloperMinuteLimit(data))
903-
const safeData = { ...data }
904-
delete safeData.tokenPrefix
905-
delete safeData.token_prefix
906-
907-
const value = {
908-
...safeData,
909-
configured: true,
910-
isDeveloperApiCredential: true,
926+
try {
927+
const client = createDeveloperApiClient()
928+
const response = await requestDeveloperApi(() =>
929+
client.get<DeveloperApiResponse<Record<string, unknown>>>('/me')
930+
)
931+
const data = unwrapDeveloperResponse(response.data)
932+
updateDeveloperRequestLimit(getDeveloperMinuteLimit(data))
933+
const safeData = { ...data }
934+
delete safeData.tokenPrefix
935+
delete safeData.token_prefix
936+
937+
const value: DeveloperApiStatusValue = {
938+
...safeData,
939+
configured: true,
940+
usable: true,
941+
isDeveloperApiCredential: true,
942+
applicationStatus: String(safeData.applicationStatus ?? safeData.application_status ?? safeData.status ?? 'unknown'),
943+
dailyLimit: safeData.dailyLimit ?? safeData.daily_limit ?? asDeveloperRecord(safeData.quota).daily ?? null,
944+
minuteLimit: getDeveloperMinuteLimit(safeData) ?? null,
945+
}
946+
developerApiUsable = true
947+
statusCache = { expiresAt: Date.now() + DEVELOPER_STATUS_CACHE_TTL_MS, value }
948+
return value
949+
} catch (error) {
950+
developerApiUsable = false
951+
const value: DeveloperApiStatusValue = {
952+
configured: true,
953+
usable: false,
954+
isDeveloperApiCredential: true,
955+
applicationStatus: 'error',
956+
dailyLimit: null,
957+
minuteLimit: null,
958+
error: error instanceof Error ? error.message : String(error),
959+
}
960+
statusCache = { expiresAt: Date.now() + DEVELOPER_STATUS_CACHE_TTL_MS, value }
961+
return value
911962
}
912-
statusCache = { expiresAt: Date.now() + DEVELOPER_STATUS_CACHE_TTL_MS, value }
913-
return value
914963
}

src/main/index.ts

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ import {
4848
fetchDeveloperGameDetail,
4949
fetchDeveloperGameResources,
5050
fetchDeveloperGameSearch,
51-
isTouchGalDeveloperApiConfigured
51+
isTouchGalDeveloperApiConfigured,
52+
isTouchGalDeveloperApiUsable,
53+
shouldAttemptTouchGalDeveloperApi
5254
} from './developerApi'
5355

5456
// Configure logging
@@ -444,7 +446,7 @@ const isLegacySessionUnavailableError = (error: unknown) => {
444446
}
445447

446448
const shouldReturnDeveloperOnlyEmptyLegacyRead = (error: unknown) =>
447-
isTouchGalDeveloperApiConfigured() && isLegacySessionUnavailableError(error)
449+
isTouchGalDeveloperApiUsable() && isLegacySessionUnavailableError(error)
448450

449451
const buildDeveloperOnlyLegacyReadFallback = <T extends Record<string, unknown>>(fallback: T) => ({
450452
...fallback,
@@ -453,9 +455,8 @@ const buildDeveloperOnlyLegacyReadFallback = <T extends Record<string, unknown>>
453455
})
454456

455457
const getDeveloperModeLegacyRequestConfig = (): TouchGalAxiosRequestConfig =>
456-
isTouchGalDeveloperApiConfigured()
457-
? { __touchGalSkipChallengeVerification: true }
458-
: {}
458+
// Legacy fallbacks should trigger the normal browser verification flow on Cloudflare challenges.
459+
({})
459460

460461
const LEGACY_CLOUD_COLLECTION_LOGIN_REQUIRED_MESSAGE =
461462
'云端收藏需要旧站登录;TouchGal Developer API 暂不支持云端收藏写入。'
@@ -2034,12 +2035,7 @@ const fetchLegacyPatchDetail = async (
20342035
}
20352036

20362037
const fetchLegacyPatchDetailWhenAccessible = async (uniqueId: string) => {
2037-
if (!(await hasTouchGalClearanceCookie())) {
2038-
log.info(`[API] Skipping legacy detail hydration for ${uniqueId}; TouchGal clearance cookie is not available`)
2039-
return null
2040-
}
2041-
2042-
return fetchLegacyPatchDetail(uniqueId, { skipChallengeVerification: true })
2038+
return fetchLegacyPatchDetail(uniqueId)
20432039
}
20442040

20452041
const cacheFetchedGameDetail = (uniqueId: string, detail: any) => {
@@ -2253,16 +2249,45 @@ app.on('window-all-closed', () => {
22532249
}
22542250
})
22552251

2252+
const REDACTED_LOG_VALUE = '[REDACTED]'
2253+
const SENSITIVE_LOG_KEY_PATTERN = /password|token|captcha|cookie|authorization|api[-_]?key|secret/i
2254+
2255+
const redactForLog = (value: unknown, seen = new WeakSet<object>()): unknown => {
2256+
if (!value || typeof value !== 'object') return value
2257+
2258+
if (Array.isArray(value)) {
2259+
return value.map((item) => redactForLog(item, seen))
2260+
}
2261+
2262+
if (seen.has(value)) return '[Circular]'
2263+
seen.add(value)
2264+
2265+
return Object.fromEntries(
2266+
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
2267+
key,
2268+
SENSITIVE_LOG_KEY_PATTERN.test(key) ? REDACTED_LOG_VALUE : redactForLog(item, seen)
2269+
])
2270+
)
2271+
}
2272+
2273+
const redactIpcArgsForLog = (channel: string, args: unknown[]) => {
2274+
if (channel === 'tg-login') {
2275+
return args.map(() => REDACTED_LOG_VALUE)
2276+
}
2277+
2278+
return args.map((arg) => redactForLog(arg))
2279+
}
2280+
22562281
// IPC Handlers with Logging
22572282
const handleWithLog = (channel: string, listener: (...args: any[]) => any) => {
22582283
ipcMain.handle(channel, async (event, ...args) => {
2259-
log.debug(`[IPC Request] ${channel}`, args)
2284+
log.debug(`[IPC Request] ${channel}`, redactIpcArgsForLog(channel, args))
22602285
try {
22612286
const result = await listener(event, ...args)
22622287
log.debug(`[IPC Response] ${channel}`, { success: true })
22632288
return result
22642289
} catch (error) {
2265-
log.error(`[IPC Error] ${channel}`, error)
2290+
log.error(`[IPC Error] ${channel}`, getSafeErrorMessage(error))
22662291
throw error
22672292
}
22682293
})
@@ -2476,7 +2501,8 @@ handleWithLog('tg-fetch-resources', async (_event, page: number, limit: number,
24762501
log.warn('[API] selectedTags received by tg-fetch-resources; ignoring upstream tag filtering and relying on local advanced pipeline');
24772502
}
24782503

2479-
// Use the standard /galgame (GET) endpoint only.
2504+
// Home feed intentionally keeps the legacy /galgame endpoint first: the
2505+
// Developer API does not currently expose an equivalent general feed.
24802506
// Tag filtering is intentionally NOT delegated upstream because /galgame tagString is unreliable
24812507
// and /search has retrieval semantics that do not match strict local filtering.
24822508
// Advanced Year Logic Translation (Intersection of all constraints)
@@ -2534,7 +2560,7 @@ handleWithLog('tg-fetch-resources', async (_event, page: number, limit: number,
25342560
return normalized
25352561
} catch (err: any) {
25362562
log.error('[API] GET /galgame error:', err.response?.data || err.message);
2537-
if (isTouchGalDeveloperApiConfigured()) {
2563+
if (shouldAttemptTouchGalDeveloperApi()) {
25382564
try {
25392565
const fallback = await fetchDeveloperBrowseFallback(page, limit, query)
25402566
if (fallback.list.length > 0) {
@@ -2587,7 +2613,7 @@ handleWithLog('tg-search-resources', async (_event, keyword: string, page: numbe
25872613
}
25882614
}
25892615

2590-
if (isTouchGalDeveloperApiConfigured() && !preferLegacySearch) {
2616+
if (shouldAttemptTouchGalDeveloperApi() && !preferLegacySearch) {
25912617
try {
25922618
return await fetchFromDeveloperApi()
25932619
} catch (error) {
@@ -2611,16 +2637,14 @@ handleWithLog('tg-search-resources', async (_event, keyword: string, page: numbe
26112637

26122638
return normalized
26132639
} catch (error) {
2614-
if (!isTouchGalDeveloperApiConfigured()) {
2615-
throw error
2616-
}
2617-
26182640
if (preferLegacySearch) {
26192641
log.warn('[API] Legacy /search failed for optioned search; returning Developer API keyword results:', getSafeErrorMessage(error))
2620-
try {
2621-
return await fetchFromDeveloperApi(true)
2622-
} catch (fallbackError) {
2623-
log.warn('[Developer API] Search fallback failed after legacy /search error; trying local cache:', getSafeErrorMessage(fallbackError))
2642+
if (shouldAttemptTouchGalDeveloperApi()) {
2643+
try {
2644+
return await fetchFromDeveloperApi(true)
2645+
} catch (fallbackError) {
2646+
log.warn('[Developer API] Search fallback failed after legacy /search error; trying local cache:', getSafeErrorMessage(fallbackError))
2647+
}
26242648
}
26252649
} else {
26262650
log.warn('[API] Legacy /search failed after Developer API search error; trying local cache:', getSafeErrorMessage(error))
@@ -2642,7 +2666,7 @@ handleWithLog('tg-get-patch-detail', async (_event, uniqueId: string) => {
26422666
}
26432667

26442668
let developerDetail: any | null = null
2645-
if (isTouchGalDeveloperApiConfigured()) {
2669+
if (shouldAttemptTouchGalDeveloperApi()) {
26462670
try {
26472671
developerDetail = await fetchDeveloperGameDetail(uniqueId)
26482672
} catch (error) {
@@ -2790,7 +2814,7 @@ handleWithLog('tg-get-patch-ratings', async (_event, patchId: number, page: numb
27902814
})
27912815

27922816
handleWithLog('tg-get-patch-introduction', async (_event, uniqueId: string) => {
2793-
if (isTouchGalDeveloperApiConfigured()) {
2817+
if (shouldAttemptTouchGalDeveloperApi()) {
27942818
try {
27952819
const detail = await fetchDeveloperGameDetail(uniqueId)
27962820
return buildPatchIntroductionFromDetail(detail)
@@ -2866,7 +2890,7 @@ handleWithLog('tg-match-folder', async (_event, folderName: string) => {
28662890
}
28672891

28682892
const localMatches = mergeRows(aliasMatches, results as Array<any>)
2869-
if (localMatches.length > 0 || !isTouchGalDeveloperApiConfigured()) {
2893+
if (localMatches.length > 0 || !shouldAttemptTouchGalDeveloperApi()) {
28702894
return localMatches
28712895
}
28722896

@@ -3170,7 +3194,7 @@ handleWithLog('tg-search-tags', async (_event, keyword: string) => {
31703194
if (query.length === 0) return []
31713195

31723196
const cachedSuggestions = buildCachedTagSuggestions(keyword)
3173-
if (isTouchGalDeveloperApiConfigured()) {
3197+
if (shouldAttemptTouchGalDeveloperApi()) {
31743198
if (cachedSuggestions.length > 0) return cachedSuggestions
31753199

31763200
try {
@@ -3209,7 +3233,7 @@ handleWithLog('tg-get-user-status-self', async () => {
32093233
const response = await API_CLIENT.get('/user/status', getDeveloperModeLegacyRequestConfig())
32103234
return ensureValidResponse(response.data)
32113235
} catch (error) {
3212-
if (!isTouchGalDeveloperApiConfigured()) {
3236+
if (!isTouchGalDeveloperApiUsable()) {
32133237
throw error
32143238
}
32153239
log.warn('[API] Legacy /user/status failed; treating TouchGal user session as unavailable:', getSafeErrorMessage(error))
@@ -3221,6 +3245,7 @@ handleWithLog('tg-get-developer-api-status', async () => {
32213245
if (!isTouchGalDeveloperApiConfigured()) {
32223246
return {
32233247
configured: false,
3248+
usable: false,
32243249
isDeveloperApiCredential: true,
32253250
applicationStatus: 'missing',
32263251
dailyLimit: null,

src/renderer/src/components/CloudCollectionOverlay.tsx

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ export const CloudCollectionOverlay: React.FC<CloudCollectionOverlayProps> = ({
4242
const [bulkTargetId, setBulkTargetId] = React.useState<string>('');
4343
const [moveTargetById, setMoveTargetById] = React.useState<Record<string, string>>({});
4444
const scrollRef = React.useRef<HTMLDivElement>(null);
45+
const folderId = folder?.id;
4546

4647
const otherFolders = React.useMemo(
47-
() => allFolders.filter((candidate) => candidate?.id && candidate.id !== folder?.id),
48-
[allFolders, folder?.id]
48+
() => allFolders.filter((candidate) => candidate?.id && candidate.id !== folderId),
49+
[allFolders, folderId]
4950
);
5051

5152
const selectedSet = React.useMemo(() => new Set(selectedIds), [selectedIds]);
@@ -58,13 +59,13 @@ export const CloudCollectionOverlay: React.FC<CloudCollectionOverlayProps> = ({
5859

5960
const loadPage = React.useCallback(
6061
async (targetPage: number, { silent = false }: { silent?: boolean } = {}) => {
61-
if (!folder?.id) return;
62+
if (!folderId) return;
6263
if (!silent) {
6364
setIsLoading(true);
6465
}
6566
setError(null);
6667
try {
67-
const response = await TouchGalClient.getFavoriteFolderPatches(folder.id, targetPage, PAGE_SIZE);
68+
const response = await TouchGalClient.getFavoriteFolderPatches(folderId, targetPage, PAGE_SIZE);
6869
setItems(Array.isArray(response?.patches) ? response.patches : []);
6970
setTotal(typeof response?.total === 'number' ? response.total : 0);
7071
} catch (fetchError) {
@@ -77,7 +78,7 @@ export const CloudCollectionOverlay: React.FC<CloudCollectionOverlayProps> = ({
7778
}
7879
}
7980
},
80-
[folder?.id]
81+
[folderId]
8182
);
8283

8384
React.useEffect(() => {
@@ -96,18 +97,27 @@ export const CloudCollectionOverlay: React.FC<CloudCollectionOverlayProps> = ({
9697
}, [onClose]);
9798

9899
React.useEffect(() => {
99-
setPage(1);
100-
setSelectedIds([]);
101-
setBulkTargetId('');
102-
setMoveTargetById({});
103-
}, [folder?.id]);
100+
const timer = window.setTimeout(() => {
101+
setPage(1);
102+
setSelectedIds([]);
103+
setBulkTargetId('');
104+
setMoveTargetById({});
105+
}, 0);
106+
return () => window.clearTimeout(timer);
107+
}, [folderId]);
104108

105109
React.useEffect(() => {
106-
void loadPage(page);
110+
const timer = window.setTimeout(() => {
111+
void loadPage(page);
112+
}, 0);
113+
return () => window.clearTimeout(timer);
107114
}, [loadPage, page]);
108115

109116
React.useEffect(() => {
110-
setSelectedIds((current) => current.filter((id) => items.some((item) => item.uniqueId === id)));
117+
const timer = window.setTimeout(() => {
118+
setSelectedIds((current) => current.filter((id) => items.some((item) => item.uniqueId === id)));
119+
}, 0);
120+
return () => window.clearTimeout(timer);
111121
}, [items]);
112122

113123
const patchCount = typeof total === 'number' && total >= 0 ? total : folder?._count?.patch || 0;

src/renderer/src/components/FavoritesView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ export const FavoritesView: React.FC = () => {
780780
const { user, userCollections, isLoading: isAuthLoading, fetchUserActivity, setIsLoginOpen } = useAuthStore();
781781
const developerApiStatus = useDeveloperApiStore((state) => state.status);
782782
const refreshDeveloperApiStatus = useDeveloperApiStore((state) => state.refreshStatus);
783-
const isDeveloperApiMode = developerApiStatus?.configured === true;
783+
const isDeveloperApiMode = developerApiStatus?.configured === true && developerApiStatus.usable === true;
784784
const [newLocalCollectionName, setNewLocalCollectionName] = React.useState('');
785785
const [newCloudCollectionName, setNewCloudCollectionName] = React.useState('');
786786
const [isCloudCollectionPublic, setIsCloudCollectionPublic] = React.useState(false);

0 commit comments

Comments
 (0)