Skip to content

Commit ffe4c68

Browse files
authored
perf(search): 重构源多季场景匹配逻辑,大幅降低无效请求提升匹配速度;为人人源新增多端接口与智能路由,改善区域连通性问题 (huangxd-#303)
* perf(api): 增加季参数透传与缓存隔离机制 - 在 `matchAnime` 重定向时提取 season 参数并隐式传递给 `searchAnime`。 - 修改所有源站 `handleAnimes` 调用,追加 `querySeason` 参数下发。 - 重构搜索缓存的 Key 生成规则,引入 `_S{season}` 后缀,实现按季度的缓存空间隔离,解决换季搜索时的旧缓存穿透 Bug。 * feat(utils): 升级季度混合匹配算法 - 升级 `titleMatches` 函数签名,新增 `parsedSeason` 参数支持。 - 统一向外部源站暴露 `getExplicitSeasonNumber` 与 `extractSeasonNumberFromAnimeTitle` 解析函数。 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 透传querySeason * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(sources): 实现详情请求前置拦截与短路回退 * perf(api): 补全 `querySeason` 参数下发 * feat(test): 增加 Bangumi Data 本地检索与季度参数的拦截检索测试 * fix(sources): 修复韩剧TV定义了多余的参数导致缓存错误赋值无法参与match匹配 * perf(sources): 人人源新增 Win 端接口与智能路由,改善区域连通性问题 https://github.com/huangxd-/danmu_api/issues/295 wni 接口的详情接口疑似存在验证密钥,暂时没能力获取目前只添加了它的搜索和弹幕接口 * Fix TV_DANMU_HOST URL in renren.js * feat(utils): 新增 `validStatusCodes` 参数配置,允许外部主动接管特定的非 2xx 状态码 配合修复人人无弹幕状态码是404导致重试 * perf(sources): 修复人人源在“剧集无弹幕”时返回 HTTP 404导致无效重试的问题 * perf(sources): Animeko弹幕获取阶段引入人人一样的智能路由机制 * perf(sources): 使用曲线救国的方式补全人人 win 详情接口,同时增加 mac 接口,并调整了函数整体结构提高可读性 复杂化致歉()但是现在拥有了四个接口,完善的智能接口回退,希望这是最后一次更改这个源( * perf(sources): 挂载 WIN 接口的别名,并支持识别会员提示弹窗用于智能偏移时长 https://github.com/huangxd-/danmu_api/issues/305 * perf(sources): 调整日志输出 * feat(utils): 修复合并后源的别名列表丢失 * perf(sources): 完善接口降级逻辑,防止误触发 Bug修呀修 * perf(api): 清理合并导致的 search 接口多余 links * 1.19.5
1 parent 8e3d208 commit ffe4c68

26 files changed

Lines changed: 1601 additions & 485 deletions

danmu_api/apis/dandan-api.js

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ export function matchSeason(anime, queryTitle, season) {
219219
// Extracted function for GET /api/v2/search/anime
220220
export async function searchAnime(url, preferAnimeId = null, preferSource = null, detailStore = null) {
221221
let queryTitle = url.searchParams.get("keyword");
222-
log("info", `Search anime with keyword: ${queryTitle}`);
222+
let querySeason = url.searchParams.get("season");
223+
querySeason = querySeason ? parseInt(querySeason, 10) : null;
224+
log("info", `Search anime with keyword: ${queryTitle}, target season: ${querySeason}`);
223225

224226
// 关键字为空直接返回,不用多余查询
225227
if (queryTitle === "") {
@@ -239,9 +241,10 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
239241
}
240242

241243
const requestAnimeDetailsMap = detailStore instanceof Map ? detailStore : new Map();
244+
const cacheKey = querySeason !== null ? `${queryTitle}_S${querySeason}` : queryTitle;
242245

243246
// 检查搜索缓存
244-
const cachedResults = getSearchCache(queryTitle, requestAnimeDetailsMap);
247+
const cachedResults = getSearchCache(cacheKey, requestAnimeDetailsMap);
245248
if (cachedResults !== null) {
246249
return jsonResponse({
247250
errorCode: 0,
@@ -317,11 +320,14 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
317320
await updateLocalRedisCaches();
318321
}
319322

323+
// 构造响应 DTO:剥离合并产生的 links,确保接口纯净
324+
const responseAnimes = curAnimes.map(({ links, ...pureAnime }) => pureAnime);
325+
320326
return jsonResponse({
321327
errorCode: 0,
322328
success: true,
323329
errorMessage: "",
324-
animes: curAnimes,
330+
animes: responseAnimes,
325331
});
326332
}
327333

@@ -376,73 +382,73 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
376382
for (const key of globals.sourceOrderArr) {
377383
if (key === '360') {
378384
// 等待处理360来源
379-
await kan360Source.handleAnimes(animes360, queryTitle, curAnimes, requestAnimeDetailsMap);
385+
await kan360Source.handleAnimes(animes360, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
380386
} else if (key === 'vod') {
381387
// 等待处理Vod来源(遍历所有VOD服务器的结果)
382388
if (animesVodResults && Array.isArray(animesVodResults)) {
383389
for (const vodResult of animesVodResults) {
384390
if (vodResult && vodResult.list && vodResult.list.length > 0) {
385-
await vodSource.handleAnimes(vodResult.list, queryTitle, curAnimes, vodResult.serverName, requestAnimeDetailsMap);
391+
await vodSource.handleAnimes(vodResult.list, queryTitle, curAnimes, vodResult.serverName, requestAnimeDetailsMap, querySeason);
386392
}
387393
}
388394
}
389395
} else if (key === 'tmdb') {
390396
// 等待处理TMDB来源
391-
await tmdbSource.handleAnimes(animesTmdb, queryTitle, curAnimes, requestAnimeDetailsMap);
397+
await tmdbSource.handleAnimes(animesTmdb, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
392398
} else if (key === 'douban') {
393399
// 等待处理Douban来源
394-
await doubanSource.handleAnimes(animesDouban, queryTitle, curAnimes, requestAnimeDetailsMap);
400+
await doubanSource.handleAnimes(animesDouban, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
395401
} else if (key === 'renren') {
396402
// 等待处理Renren来源
397-
await renrenSource.handleAnimes(animesRenren, queryTitle, curAnimes, requestAnimeDetailsMap);
403+
await renrenSource.handleAnimes(animesRenren, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
398404
} else if (key === 'hanjutv') {
399405
// 等待处理Hanjutv来源
400-
await hanjutvSource.handleAnimes(animesHanjutv, queryTitle, curAnimes, requestAnimeDetailsMap);
406+
await hanjutvSource.handleAnimes(animesHanjutv, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
401407
} else if (key === 'bahamut') {
402408
// 等待处理Bahamut来源
403-
await bahamutSource.handleAnimes(animesBahamut, queryTitle, curAnimes, requestAnimeDetailsMap);
409+
await bahamutSource.handleAnimes(animesBahamut, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
404410
} else if (key === 'dandan') {
405411
// 等待处理弹弹play来源
406-
await dandanSource.handleAnimes(animesDandan, queryTitle, curAnimes, requestAnimeDetailsMap);
412+
await dandanSource.handleAnimes(animesDandan, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
407413
} else if (key === 'custom') {
408414
// 等待处理自定义弹幕源来源
409415
await customSource.handleAnimes(animesCustom, queryTitle, curAnimes, requestAnimeDetailsMap);
410416
} else if (key === 'tencent') {
411417
// 等待处理Tencent来源
412-
await tencentSource.handleAnimes(animesTencent, queryTitle, curAnimes, requestAnimeDetailsMap);
418+
await tencentSource.handleAnimes(animesTencent, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
413419
} else if (key === 'youku') {
414420
// 等待处理Youku来源
415-
await youkuSource.handleAnimes(animesYouku, queryTitle, curAnimes, requestAnimeDetailsMap);
421+
await youkuSource.handleAnimes(animesYouku, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
416422
} else if (key === 'iqiyi') {
417423
// 等待处理iQiyi来源
418-
await iqiyiSource.handleAnimes(animesIqiyi, queryTitle, curAnimes, requestAnimeDetailsMap);
424+
await iqiyiSource.handleAnimes(animesIqiyi, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
419425
} else if (key === 'imgo') {
420426
// 等待处理Mango来源
421-
await mangoSource.handleAnimes(animesImgo, queryTitle, curAnimes, requestAnimeDetailsMap);
427+
await mangoSource.handleAnimes(animesImgo, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
422428
} else if (key === 'bilibili') {
423429
// 等待处理Bilibili来源
424-
await bilibiliSource.handleAnimes(animesBilibili, queryTitle, curAnimes, requestAnimeDetailsMap);
430+
await bilibiliSource.handleAnimes(animesBilibili, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
425431
} else if (key === 'migu') {
426432
// 等待处理Migu来源
427-
await miguSource.handleAnimes(animesMigu, queryTitle, curAnimes, requestAnimeDetailsMap);
433+
await miguSource.handleAnimes(animesMigu, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
428434
} else if (key === 'sohu') {
429435
// 等待处理Sohu来源
430-
await sohuSource.handleAnimes(animesSohu, queryTitle, curAnimes, requestAnimeDetailsMap);
436+
await sohuSource.handleAnimes(animesSohu, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
431437
} else if (key === 'leshi') {
432438
// 等待处理Leshi来源
433-
await leshiSource.handleAnimes(animesLeshi, queryTitle, curAnimes, requestAnimeDetailsMap);
439+
await leshiSource.handleAnimes(animesLeshi, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
434440
} else if (key === 'xigua') {
435441
// 等待处理Xigua来源
436-
await xiguaSource.handleAnimes(animesXigua, queryTitle, curAnimes, requestAnimeDetailsMap);
442+
await xiguaSource.handleAnimes(animesXigua, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
437443
} else if (key === 'maiduidui') {
438444
// 等待处理Maiduidui来源
439-
await maiduiduiSource.handleAnimes(animesMaiduidui, queryTitle, curAnimes, requestAnimeDetailsMap);
445+
await maiduiduiSource.handleAnimes(animesMaiduidui, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
440446
} else if (key === 'aiyifan') {
441447
// 等待处理Aiyifan来源
442-
await aiyifanSource.handleAnimes(animesAiyifan, queryTitle, curAnimes, requestAnimeDetailsMap);
448+
await aiyifanSource.handleAnimes(animesAiyifan, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
443449
} else if (key === 'animeko') {
444450
// 等待处理Animeko来源
445-
await animekoSource.handleAnimes(animesAnimeko, queryTitle, curAnimes, requestAnimeDetailsMap);
451+
await animekoSource.handleAnimes(animesAnimeko, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
446452
}
447453
}
448454
} catch (error) {
@@ -509,16 +515,20 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
509515
await updateLocalRedisCaches();
510516
}
511517

518+
// 构造响应 DTO:剥离合并产生的 links,确保接口纯净
519+
const responseAnimes = curAnimes.map(({ links, ...pureAnime }) => pureAnime);
520+
512521
// 缓存搜索结果
513-
if (curAnimes.length > 0) {
514-
setSearchCache(queryTitle, curAnimes, requestAnimeDetailsMap);
522+
if (responseAnimes.length > 0) {
523+
const cacheKey = querySeason !== null ? `${queryTitle}_S${querySeason}` : queryTitle;
524+
setSearchCache(cacheKey, responseAnimes, requestAnimeDetailsMap);
515525
}
516526

517527
return jsonResponse({
518528
errorCode: 0,
519529
success: true,
520530
errorMessage: "",
521-
animes: curAnimes,
531+
animes: responseAnimes,
522532
});
523533

524534
}
@@ -1090,7 +1100,7 @@ export async function matchAnime(url, req, clientIp) {
10901100
log("info", `prefer animeId: ${preferAnimeId} from ${preferSource}`);
10911101

10921102
const requestAnimeDetailsMap = new Map();
1093-
let originSearchUrl = new URL(req.url.replace("/match", `/search/anime?keyword=${title}`));
1103+
let originSearchUrl = new URL(req.url.replace("/match", `/search/anime?keyword=${title}&season=${season || ''}`));
10941104
const searchRes = await searchAnime(originSearchUrl, preferAnimeId, preferSource, requestAnimeDetailsMap);
10951105
const searchData = await searchRes.json();
10961106
log("info", `searchData: ${searchData.animes}`);

danmu_api/configs/globals.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const Globals = {
1313
accessedEnvVars: {},
1414

1515
// 静态常量
16-
VERSION: '1.19.4',
16+
VERSION: '1.19.5',
1717
MAX_LOGS: 1000, // 日志存储,最多保存 1000 行
1818
MAX_RECORDS: 100, // 请求记录最大数量
1919

danmu_api/sources/aiyifan.js

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { convertToAsciiSum } from "../utils/codec-util.js";
55
import { hexToInt } from "../utils/danmu-util.js";
66
import { generateValidStartDate } from "../utils/time-util.js";
77
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
8-
import { titleMatches } from "../utils/common-util.js";
8+
import { titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
99
import { simplized } from "../utils/zh-util.js";
1010
import { globals } from '../configs/globals.js';
1111
import { AiyifanSigningProvider } from '../utils/aiyifan-util.js';
@@ -308,24 +308,42 @@ export default class AiyifanSource extends BaseSource {
308308
}
309309

310310
/**
311-
* 处理animes结果
312-
* @param {Array} sourceAnimes - 源动漫数据
313-
* @param {string} queryTitle - 查询标题
314-
* @param {Array} curAnimes - 当前动漫列表
315-
* @param {any} detailStore - 详情存储
316-
* @returns {Promise<Array>} 处理后的动漫列表
311+
* 处理搜索结果
312+
* @param {Array} sourceAnimes 原始数据
313+
* @param {string} queryTitle 关键词
314+
* @param {Array} curAnimes 结果池
315+
* @param {Map} detailStore 详情缓存
316+
* @param {number|null} querySeason 目标季度
317317
*/
318-
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
318+
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
319319
const tmpAnimes = [];
320320

321321
if (!sourceAnimes || !Array.isArray(sourceAnimes)) {
322322
log("error", "[Aiyifan] sourceAnimes is not a valid array");
323323
return [];
324324
}
325325

326-
const processPromises = sourceAnimes
327-
.filter(anime => titleMatches(anime.title, queryTitle))
328-
.map(async (anime) => {
326+
// 基础标题与季度匹配过滤
327+
let filteredAnimes = sourceAnimes.filter(anime => titleMatches(anime.title, queryTitle, querySeason));
328+
329+
// 提取搜索词中的明确季度信息或使用传入的季度参数
330+
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
331+
332+
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
333+
if (resolvedQuerySeason !== null) {
334+
const seasonFiltered = filteredAnimes.filter(anime => {
335+
const s = extractSeasonNumberFromAnimeTitle(anime.title).season;
336+
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
337+
});
338+
339+
// 如果已命中目标,减少详情请求量
340+
if (seasonFiltered.length > 0) {
341+
filteredAnimes = seasonFiltered;
342+
log("info", `[Aiyifan] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
343+
}
344+
}
345+
346+
const processPromises = filteredAnimes.map(async (anime) => {
329347
try {
330348
// 获取剧集列表
331349
const eps = await this.getEpisodes(anime.mediaId);

danmu_api/sources/animeko.js

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ import { httpGet, httpPost } from "../utils/http-util.js";
55
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
66
import { simplized } from "../utils/zh-util.js";
77
import { SegmentListResponse } from '../models/dandan-model.js';
8-
import { titleMatches } from "../utils/common-util.js";
8+
import { titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
99
import { searchBangumiData } from '../utils/bangumi-data-util.js';
1010

1111
// =====================
1212
// 获取Animeko弹幕(https://github.com/open-ani/animeko)
1313
// =====================
1414

15+
// 接口健康状态缓存 (全局共享,跨请求持久化,实现业务级智能路由)
16+
// 链路层级规范: Danmu (GLOBAL -> CN)
17+
const API_HEALTH = {
18+
danmu: 'GLOBAL'
19+
};
20+
1521
/**
1622
* Animeko 源适配器 (基于 Bangumi API V0)
1723
* 提供深度元数据搜索、结果过滤及条目关系检测功能
@@ -333,7 +339,6 @@ export default class AnimekoSource extends BaseSource {
333339
});
334340

335341
// 1. 结构校验:确保 resp.data.data 存在且为数组
336-
// 对应您的 JSON: resp.data 存在,resp.data.data 是 [] (数组),校验通过
337342
if (!resp || !resp.data || !Array.isArray(resp.data.data)) {
338343
if (offset === 0) {
339344
log("info", `[Animeko] Subject ${subjectId} 无剧集数据或响应异常`);
@@ -344,7 +349,6 @@ export default class AnimekoSource extends BaseSource {
344349
const currentBatch = resp.data.data;
345350

346351
// 2. 空数据校验:如果没有数据,停止
347-
// 对应您的 JSON: data 为 [],length 为 0,在此处 break 退出
348352
if (currentBatch.length === 0) {
349353
break;
350354
}
@@ -386,20 +390,42 @@ export default class AnimekoSource extends BaseSource {
386390
}
387391

388392
/**
389-
* 处理并存储番剧及剧集信息
390-
* @param {Array} sourceAnimes 搜索到的番剧列表
391-
* @param {string} queryTitle 原始查询标题
392-
* @param {Array} curAnimes 当前缓存的番剧列表
393+
* 处理搜索结果
394+
* @param {Array} sourceAnimes 原始数据
395+
* @param {string} queryTitle 关键词
396+
* @param {Array} curAnimes 结果池
397+
* @param {Map} detailStore 详情缓存
398+
* @param {number|null} querySeason 目标季度
393399
*/
394-
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
400+
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
395401
const tmpAnimes = [];
396402

397403
if (!sourceAnimes || !Array.isArray(sourceAnimes)) {
398404
if (sourceAnimes) log("error", "[Animeko] sourceAnimes is not a valid array");
399405
return [];
400406
}
401407

402-
const processAnimekoAnimes = await Promise.all(sourceAnimes.map(async (anime) => {
408+
let filteredAnimes = sourceAnimes;
409+
410+
// 提取搜索词中的明确季度信息或使用传入的季度参数
411+
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
412+
413+
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
414+
if (resolvedQuerySeason !== null) {
415+
const seasonFiltered = filteredAnimes.filter(anime => {
416+
const titleToCheck = anime.name_cn || anime.name;
417+
const s = extractSeasonNumberFromAnimeTitle(titleToCheck).season;
418+
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
419+
});
420+
421+
// 如果已命中目标,减少详情请求量
422+
if (seasonFiltered.length > 0) {
423+
filteredAnimes = seasonFiltered;
424+
log("info", `[Animeko] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
425+
}
426+
}
427+
428+
const processAnimekoAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
403429
try {
404430
const eps = await this.getEpisodes(anime.id);
405431
let links = [];
@@ -488,8 +514,11 @@ export default class AnimekoSource extends BaseSource {
488514
return [];
489515
}
490516

491-
const HOST_GLOBAL = "https://danmaku-global.myani.org";
492-
const HOST_CN = "https://danmaku-cn.myani.org";
517+
const endpoints = {
518+
'GLOBAL': "https://danmaku-global.myani.org",
519+
'CN': "https://danmaku-cn.myani.org"
520+
};
521+
const tiers = ['GLOBAL', 'CN'];
493522

494523
// 定义内部通用请求函数
495524
const fetchDanmu = async (hostUrl) => {
@@ -508,22 +537,35 @@ export default class AnimekoSource extends BaseSource {
508537
}
509538
};
510539

511-
// 2. 优先尝试 Global 节点
512-
let danmuList = await fetchDanmu(HOST_GLOBAL);
540+
let danmuList = null;
541+
let currentTierIndex = tiers.indexOf(API_HEALTH.danmu);
542+
if (currentTierIndex === -1) currentTierIndex = 0;
513543

514-
// 3. 如果失败,降级尝试 CN 节点
515-
if (!danmuList) {
516-
log("info", `[Animeko] Global 节点获取失败/无数据,降级尝试 CN 节点... ID:${realId}`);
517-
danmuList = await fetchDanmu(HOST_CN);
518-
}
544+
// 智能路由检测与降级回路
545+
for (let i = currentTierIndex; i < tiers.length; i++) {
546+
const tier = tiers[i];
547+
const hostUrl = endpoints[tier];
548+
log("info", `[Animeko] 尝试使用 ${tier} 节点获取弹幕`);
549+
550+
danmuList = await fetchDanmu(hostUrl);
519551

520-
// 4. 返回结果或空数组
521-
if (danmuList) {
522-
log("info", `[Animeko] 成功获取弹幕,共 ${danmuList.length} 条`);
523-
return danmuList;
552+
if (danmuList && Array.isArray(danmuList)) {
553+
// 记录当前健康的节点层级
554+
if (API_HEALTH.danmu !== tier) {
555+
log("info", `[Animeko] 弹幕域节点健康状态更新: ${API_HEALTH.danmu} -> ${tier}`);
556+
API_HEALTH.danmu = tier;
557+
}
558+
log("info", `[Animeko] 成功获取弹幕,共 ${danmuList.length} 条 (${tier}节点)`);
559+
return danmuList;
560+
} else {
561+
log("info", `[Animeko] ${tier} 节点获取失败/无数据,触发降级`);
562+
}
524563
}
525564

526-
log("error", "[Animeko] 所有节点尝试均失败,无法获取弹幕");
565+
// 所有节点轮换完毕仍未获取到数据,重置健康状态
566+
log("info", `[Animeko] 弹幕域所有降级节点均失败,重置健康状态至 GLOBAL 节点`);
567+
API_HEALTH.danmu = 'GLOBAL';
568+
527569
return [];
528570
}
529571

0 commit comments

Comments
 (0)