From 825ddc25bb9d4a5f942fe94a4e2cc245e6e35f4a Mon Sep 17 00:00:00 2001 From: Elegy233 <82034747+wan0ge@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:37:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=A5=E6=A0=BC?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=E4=B8=8B=E8=AE=B0=E4=BD=8F=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E7=BB=93=E6=9E=9C=E5=8A=9F=E8=83=BD=E5=A4=B1?= =?UTF-8?q?=E6=95=88=E5=8F=8A=E5=8C=B9=E9=85=8D=E9=81=8D=E5=8E=86=E8=BF=87?= =?UTF-8?q?=E6=97=A9=E7=BB=88=E6=AD=A2=20(#401)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: matchAniAndEp 移除对非优先条目的独占过滤,保留评分优先 原代码通过 preferAnimeId 直接 continue 跳过所有不匹配的优先条目, 其意图是确保优先条目被命中。但该过滤存在两个问题: 1. 与 isPreferredAnime 的 +9999 评分加成功能重复——评分已提供 压倒性优先权,独占过滤属于冗余保护。 2. 独占过滤阻止了优先源缺失目标集时的自然回退:其他源被 跳过导致无匹配,需要额外走 fallbackMatchAniAndEp 兜底。 移除独占过滤后,isPreferredAnime 的 +9999 评分加成和匹配后的 break 仍然确保优先条目在正常匹配中绝对优先,同时允许优先源 无目标集时自然回退到其他可用源。 * fix: TITLE_NOISE_FILTER 默认规则增加方括号杂音词支持 默认 TITLE_NOISE_FILTER 仅匹配圆括号内的杂音词(如(日配)、(国配)), 遗漏方括号形式的变种(如[日语版]、[国语版]、[中字])。 tencent 源的标题格式"假面骑士ZZZ[日语版](2025)"中[日语版]未被清理, 导致 titleMatches 在严格匹配下的标题清理不完整。 修复:默认规则扩展为单字符类同时匹配圆括号(中英文)和方括号, 在原有词表基础上新增方括号特供词:日语版、国语版、英语版、中字。 圆括号兼容保持不变。 * fix: 偏好记录流程的titleMatches强制使用非严格匹配,不因STRICT_TITLE_MATCH受影响 titleMatches 在全局 STRICT_TITLE_MATCH 开启时统一走 strictTitleMatch, 但偏好记录(setPreferByAnimeId)场景应始终使用非严格匹配。原因: 1. 用户手动选择某个源的弹幕即表示"我认可这是正确匹配", 不应因标题含[日语版]等形式差异而被拒。 2. strictTitleMatch 限制 suffix 只能为 seasonPattern 中的模式, 而(2025)、[日语版]等源补充标识均不符合,导致有意义的用户选择被丢弃。 3. "记住手动选择结果"强调自由选择权,严格匹配的搜索过滤逻辑不适用于此。 修复:titleMatches 增加 forceNonStrict 参数(默认false,完全向后兼容), 偏好记录流程传入 true,跳过 strictTitleMatch 直接走包含匹配(includes)。 * fix: matchAniAndEp 无平台偏好时 break 应感知 preferAnimeId,防止跳过优先条目 第1435行 if(!platform || isPreferredAnime) break 在 platform 为空时 无条件跳出循环,导致即使 preferAnimeId 指向靠后的条目, 循环也会在第一个匹配条目后终止,优先条目从未被评估。 修复:break 仅在两种场景触发: 1. isPreferredAnime=true — 已命中优先条目,无需继续 2. !platform && !preferAnimeId — 无平台偏好且无优先条目,首个匹配即可 当 preferAnimeId 存在但尚未命中时,继续循环评估后续条目。 --- README.md | 2 +- config/.env.example | 2 +- danmu_api/apis/dandan-api.js | 21 ++++++++------------- danmu_api/configs/envs.js | 4 ++-- danmu_api/utils/common-util.js | 6 +++--- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 73586fe..0e8b1d8 100644 --- a/README.md +++ b/README.md @@ -492,7 +492,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 ```plain # TITLE_NOISE_FILTER 默认值 -[((](?:臻彩|真彩|高清|标清|超清|国配|中配|日配|粤语|原声|台配|无修|未删减|完整版)[))] +[((\\[](?:臻彩|真彩|高清|标清|超清|国配|中配|日配|粤语|原声|台配|无修|未删减|完整版|日语版|国语版|英语版|中字|字幕|助听|原版)[\\]))] ``` ```regex diff --git a/config/.env.example b/config/.env.example index 846c0c0..0f45fda 100644 --- a/config/.env.example +++ b/config/.env.example @@ -401,7 +401,7 @@ ANIME_TITLE_FILTER= # 示例:只清理括号臻彩和真彩 # TITLE_NOISE_FILTER=[((](?:臻彩|真彩)[))] # 设为空值可禁用 -TITLE_NOISE_FILTER=[((](?:臻彩|真彩|高清|标清|超清|国配|中配|日配|粤语|原声|台配|无修|未删减|完整版)[))] +TITLE_NOISE_FILTER=[((\\[](?:臻彩|真彩|高清|标清|超清|国配|中配|日配|粤语|原声|台配|无修|未删减|完整版|日语版|国语版|英语版|中字|字幕|助听|原版)[\\]))] # ==================== AI 配置 ==================== diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index 37d6c94..3a621b2 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -1291,13 +1291,6 @@ async function matchAniAndEp(season, episode, year, searchData, title, req, plat // 遍历所有搜索结果,寻找最佳匹配 for (const anime of searchData.animes) { - // 偏好过滤 - const animeIsNotPrefer = - globals.rememberLastSelect && - preferAnimeId && - String(anime.bangumiId) !== String(preferAnimeId) && - String(anime.animeId) !== String(preferAnimeId); - if (animeIsNotPrefer) continue; let isMatch = false; @@ -1330,7 +1323,8 @@ async function matchAniAndEp(season, episode, year, searchData, title, req, plat // 构造一个虚拟的 anime 对象传入 matchSeason,这样当命中别名时,matchSeason 才能正确判断后缀 const tempAnime = { ...anime, animeTitle: candTitle }; - if (matchSeason(tempAnime, title, season) || animeIsPrefer) { + const seasonOk = matchSeason(tempAnime, title, season); + if (seasonOk || animeIsPrefer) { isMatch = true; break; // 别名命中跳出 } @@ -1449,8 +1443,8 @@ async function matchAniAndEp(season, episode, year, searchData, title, req, plat }; } - // 如果没有指定平台偏好 (platform 为空) 或已命中最高优先级的手动优选,立刻跳出查找 - if (!platform || isPreferredAnime) { + // 已命中最高优先级的手动优选,或不存在平台偏好且无待匹配的优选条目时立刻跳出查找 + if (isPreferredAnime || (!platform && !preferAnimeId)) { break; } @@ -2400,11 +2394,12 @@ export async function getComment(path, queryFormat, segmentFlag, clientIp, inclu } } - log("info", `[system] [LogVar-API] animeTitle:${animeTitle}; lastTitle:${lastTitle}; titleMatches:${titleMatches(animeTitle, lastTitle)}`); + log("info", `[system] [LogVar-API] animeTitle:${animeTitle}; lastTitle:${lastTitle}; titleMatches:${titleMatches(animeTitle, lastTitle, null, true)}`); // 校验番剧标题或别名是否匹配最新搜索/匹配上下文,别名检查用于兼容不同源对同一番剧的标题命名差异 - const titleOrAliasMatches = titleMatches(animeTitle, lastTitle) || - (Array.isArray(animeAliases) && animeAliases.some(alias => titleMatches(alias, lastTitle))); + // 偏好记录使用非严格匹配(forceNonStrict=true),因为用户手动选择不应受严格标题匹配限制 + const titleOrAliasMatches = titleMatches(animeTitle, lastTitle, null, true) || + (Array.isArray(animeAliases) && animeAliases.some(alias => titleMatches(alias, lastTitle, null, true))); if (titleOrAliasMatches) { log("info", `[system] [LogVar-API] excute setPreferByAnimeId`); diff --git a/danmu_api/configs/envs.js b/danmu_api/configs/envs.js index cbaa505..c5e49a4 100644 --- a/danmu_api/configs/envs.js +++ b/danmu_api/configs/envs.js @@ -542,7 +542,7 @@ export class Envs { * @returns {RegExp|null} 全局正则,显式设为空时返回 null(禁用) */ static resolveTitleNoiseFilter() { - const defaultPattern = '[((](?:臻彩|真彩|高清|标清|超清|国配|中配|日配|粤语|原声|台配|无修|未删减|完整版)[))]'; + const defaultPattern = '[((\\[](?:臻彩|真彩|高清|标清|超清|国配|中配|日配|粤语|原声|台配|无修|未删减|完整版|日语版|国语版|英语版|中字|字幕|助听|原版)[\\]))]'; const raw = this.get('TITLE_NOISE_FILTER', '', 'string').trim(); const hasKey = (this.env && 'TITLE_NOISE_FILTER' in this.env) || (typeof process !== 'undefined' && 'TITLE_NOISE_FILTER' in process.env); @@ -638,7 +638,7 @@ export class Envs { 'TITLE_TO_CHINESE': { category: 'match', type: 'boolean', description: '外语标题转换中文开关' }, 'ANIME_TITLE_SIMPLIFIED': { category: 'match', type: 'boolean', description: '搜索的剧名标题自动繁转简' }, 'TITLE_MAPPING_TABLE': { category: 'match', type: 'map', description: '剧名映射表,用于自动匹配时替换标题进行搜索,格式:原始标题->映射标题;原始标题->映射标题;... ,例如:"唐朝诡事录->唐朝诡事录之西行;国色芳华->锦绣芳华"' }, - 'TITLE_NOISE_FILTER': { category: 'match', type: 'text', description: '剧名杂音清理规则,按正则表达式清理搜索与匹配阶段的剧名杂音词(如`百花杀(真彩)`→`百花杀`)。默认值:[((](?:臻彩|真彩|高清|标清|超清|国配|中配|日配|粤语|原声|台配|无修|未删减|完整版)[))],中英文括号均匹配。设为空值可禁用' }, + 'TITLE_NOISE_FILTER': { category: 'match', type: 'text', description: '剧名杂音清理规则,按正则表达式清理搜索与匹配阶段的剧名杂音词(如`百花杀(真彩)`→`百花杀`)。默认值:[((\\[](?:臻彩|真彩|高清|标清|超清|国配|中配|日配|粤语|原声|台配|无修|未删减|完整版|日语版|国语版|英语版|中字|字幕|助听|原版)[\\]))],中英文圆方括号均匹配。设为空值可禁用' }, 'AI_BASE_URL': { category: 'match', type: 'text', description: 'AI服务基础URL,不填默认为https://api.openai.com/v1' }, 'AI_MODEL': { category: 'match', type: 'text', description: 'AI模型名称,不填默认为gpt-4o' }, 'AI_API_KEY': { category: 'match', type: 'text', description: 'AI服务API密钥,默认为空,需手动填写' }, diff --git a/danmu_api/utils/common-util.js b/danmu_api/utils/common-util.js index e0b93ab..98b3166 100644 --- a/danmu_api/utils/common-util.js +++ b/danmu_api/utils/common-util.js @@ -269,15 +269,15 @@ export function getExplicitSeasonNumber(text) { * @param {number|null} parsedSeason - 解析出的目标季度 * @returns {boolean} 是否匹配 */ -export function titleMatches(title, query, parsedSeason = null) { +export function titleMatches(title, query, parsedSeason = null, forceNonStrict = false) { if (title == null || query == null) return false; const titleText = String(title); const queryText = String(query); if (!titleText || !queryText) return false; - // 策略1:严格模式仅允许头部或完全匹配 - if (globals.strictTitleMatch) return strictTitleMatch(titleText, queryText); + // 策略1:严格模式仅允许头部或完全匹配(forceNonStrict 为 true 时跳过,用于偏好记录等场景) + if (!forceNonStrict && globals.strictTitleMatch) return strictTitleMatch(titleText, queryText); // 剧名杂音清理:移除画质/配音/版本等杂音词,避免阻塞匹配 const tagFilter = globals.titleNoiseFilter || null;