漫画搜索增加流式输出功能

This commit is contained in:
mtvpls
2026-05-16 11:01:21 +08:00
parent 4f0a0db0fd
commit 0d3731f07c
3 changed files with 409 additions and 81 deletions
+89 -63
View File
@@ -338,26 +338,39 @@ export class SuwayomiClient {
}));
}
async searchManga(keyword: string, sourceId?: string, page = 1): Promise<MangaSearchResult> {
async getSearchSources(sourceId?: string): Promise<Array<{ id: string; displayName?: string; name?: string }>> {
const resolved = await resolveSuwayomiConfig(this.options);
let sources: Array<{ id: string; displayName?: string; name?: string }>;
if (sourceId) {
sources = [{ id: sourceId, displayName: sourceId, name: sourceId }];
} else {
try {
sources = (await this.getSources(resolved.defaultLang)).slice(0, resolved.maxSources);
} catch (error) {
if (resolved.sourceIds.length === 0) {
throw error;
}
sources = resolved.sourceIds.slice(0, resolved.maxSources).map((id) => ({
id,
displayName: id,
name: id,
}));
}
const matched = (await this.getSources()).find((item) => item.id === sourceId);
return [
{
id: sourceId,
displayName: matched?.displayName || matched?.name || sourceId,
name: matched?.name || matched?.displayName || sourceId,
},
];
}
try {
return (await this.getSources(resolved.defaultLang)).slice(0, resolved.maxSources);
} catch (error) {
if (resolved.sourceIds.length === 0) {
throw error;
}
return resolved.sourceIds.slice(0, resolved.maxSources).map((id) => ({
id,
displayName: id,
name: id,
}));
}
}
async searchMangaSource(
keyword: string,
source: { id: string; displayName?: string; name?: string },
page = 1
): Promise<{ source: { id: string; displayName?: string; name?: string }; results: MangaSearchItem[] }> {
const query = `
mutation GET_SOURCE_MANGAS_FETCH($input: FetchSourceMangaInput!) {
fetchSourceManga(input: $input) {
@@ -376,6 +389,60 @@ export class SuwayomiClient {
}
`;
const data = await this.graphqlRequest<{
fetchSourceManga?: {
mangas?: Array<{
id: string | number;
title?: string;
thumbnailUrl?: string;
sourceId?: string | number;
description?: string;
author?: string;
artist?: string;
genre?: string;
status?: string;
}>;
};
}>(
query,
{
input: {
type: 'SEARCH',
source: source.id,
query: keyword,
page,
},
},
'GET_SOURCE_MANGAS_FETCH'
);
const seen = new Set<string>();
const sourceName = source.displayName || source.name || String(source.id);
const results = (data.fetchSourceManga?.mangas || [])
.filter((manga) => {
const key = `${source.id}:${manga.id}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
})
.map((manga) => ({
id: String(manga.id),
sourceId: String(manga.sourceId || source.id),
sourceName,
title: manga.title || '未命名漫画',
cover: buildSuwayomiImageProxyUrl(manga.thumbnailUrl || ''),
description: manga.description,
author: manga.author,
artist: manga.artist,
genre: manga.genre,
status: normalizeMangaStatus(manga.status),
}));
return { source, results };
}
async searchManga(keyword: string, sourceId?: string, page = 1): Promise<MangaSearchResult> {
const sources = await this.getSearchSources(sourceId);
const results: MangaSearchItem[] = [];
const failedSources: MangaSearchFailure[] = [];
const seen = new Set<string>();
@@ -383,37 +450,7 @@ export class SuwayomiClient {
const perSourceResults = await Promise.all(
sources.map(async (source) => {
try {
const data = await this.graphqlRequest<{
fetchSourceManga?: {
mangas?: Array<{
id: string | number;
title?: string;
thumbnailUrl?: string;
sourceId?: string | number;
description?: string;
author?: string;
artist?: string;
genre?: string;
status?: string;
}>;
};
}>(
query,
{
input: {
type: 'SEARCH',
source: source.id,
query: keyword,
page,
},
},
'GET_SOURCE_MANGAS_FETCH'
);
return {
source,
mangas: data.fetchSourceManga?.mangas || [],
};
return await this.searchMangaSource(keyword, source, page);
} catch (error) {
const message = error instanceof Error ? error.message : '未知错误';
console.warn(`[Suwayomi] manga search source failed: ${source.id} - ${message}`);
@@ -424,29 +461,18 @@ export class SuwayomiClient {
});
return {
source,
mangas: [],
results: [],
};
}
})
);
for (const { source, mangas } of perSourceResults) {
for (const manga of mangas) {
const key = `${source.id}:${manga.id}`;
for (const { results: sourceResults } of perSourceResults) {
for (const manga of sourceResults) {
const key = `${manga.sourceId}:${manga.id}`;
if (seen.has(key)) continue;
seen.add(key);
results.push({
id: String(manga.id),
sourceId: String(manga.sourceId || source.id),
sourceName: source.displayName || source.name || String(source.id),
title: manga.title || '未命名漫画',
cover: buildSuwayomiImageProxyUrl(manga.thumbnailUrl || ''),
description: manga.description,
author: manga.author,
artist: manga.artist,
genre: manga.genre,
status: normalizeMangaStatus(manga.status),
});
results.push(manga);
}
}