From e6c292c463392b2ea3c567256f26665bb937aa32 Mon Sep 17 00:00:00 2001 From: Elegy233 <82034747+wan0ge@users.noreply.github.com> Date: Mon, 25 May 2026 10:05:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=20`/api/cache/an?= =?UTF-8?q?imes`=20API=20=E6=8E=A5=E5=8F=A3=E8=8E=B7=E5=8F=96=E6=9C=80?= =?UTF-8?q?=E8=BF=91=E7=9A=84=20animes=20=E7=BC=93=E5=AD=98=EF=BC=8C?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=88=B0CUSTOM=5FMERGE=5FRULES=E4=B8=8EDANMU?= =?UTF-8?q?=5FOFFSET=E9=85=8D=E7=BD=AE=E9=9D=A2=E6=9D=BF=E4=B8=AD=E8=BE=85?= =?UTF-8?q?=E5=8A=A9=E5=A1=AB=E5=86=99=20(#318)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(api): 新增处理获取最近animes缓存列表请求函数 * feat(api): 新增 `/api/cache/animes API` 接口 * feat(model): 新增 `mergedChildren` 与 `isHiddenChild` 参数 * feat(utils): 记录合并层级 * feat(ui): 新增最近数据与`animes`缓存面板样式 * feat(ui): 新增最近数据与`animes`缓存面板功能 * feat(md): 新增GET `/api/cache/animes`说明 * fix(ui): 修复合并结果由于长英文字符串导致前端网格布被撑破的 UI 错位问题 远古问题 * fix(ui): 调整换行逻辑提升可读性 * feat(ui): 给MERGE_SOURCE_PAIRS也添加最新数据面板,供合并参考 Add button to fetch and display recent data in the UI. --- README.md | 3 +- danmu_api/apis/system-api.js | 48 ++++ danmu_api/models/dandan-model.js | 7 +- danmu_api/ui/css/components.css.js | 204 ++++++++++++++++ danmu_api/ui/js/systemsettings.js | 376 ++++++++++++++++++++++++++++- danmu_api/utils/merge-util.js | 19 ++ danmu_api/worker.js | 7 +- 7 files changed, 658 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7f5111d..832c37c 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,10 @@ LogVar 弹幕 API 服务器 - `GET /api/v2/comment/:commentId?format=json&duration=true`:获取指定弹幕评论;当 `duration=true` 且返回 JSON 时,会额外附带 `videoDuration` 字段,优先返回源站时长,拿不到时返回 `0`。 - `GET /api/v2/comment?url=${videoUrl}&format=json`:通过视频URL直接获取弹幕(兼容第三方弹幕服务器格式)。 - `POST /api/v2/segmentcomment?format=json`:通过comment接口返回体中的Segment类JSON数据获取单独一个分片的弹幕数据。 - - `GET /api/logs`:获取最近的日志(最多 500 行,格式为 `[时间戳] 级别: 消息`)。 - `GET /api/v2/fongmi/danmaku?name={name}&episode={episode}`:FengMi影视api。 - `GET /danmaku/api/v2/fongmi/danmaku?name={name}&episode={episode}`:兼容FengMi影视api短路径。 + - `GET /api/logs`:获取最近的日志(最多 500 行,格式为 `[时间戳] 级别: 消息`)。 + - `GET /api/cache/animes`:获取最近的 animes 缓存。 - **弹幕格式输出**:支持 JSON 和 XML 两种格式输出,通过以下方式配置: - 环境变量:`DANMU_OUTPUT_FORMAT=json|xml`(默认:json) - 查询参数:`?format=xml` 或 `?format=json`(优先级最高) diff --git a/danmu_api/apis/system-api.js b/danmu_api/apis/system-api.js index b2de912..179e1ca 100644 --- a/danmu_api/apis/system-api.js +++ b/danmu_api/apis/system-api.js @@ -251,3 +251,51 @@ export function handleReqRecords() { return jsonResponse({ records, todayReqNum }, 200); } +/** + * 处理获取最近 animes 缓存列表的请求 + * @returns {Response} 包含格式化后番剧及子源集数的 JSON 响应 + */ +export function handleCacheAnimes() { + try { + const localAnimes = [...(globals.animes || [])].reverse(); + + // 1. 预构建一个全局映射字典,提升查找效率 + const fullAnimeMap = new Map(); + localAnimes.forEach(a => { + if (a?.source && a?.animeId) { + fullAnimeMap.set(`${a.source}_${a.animeId}`, a); + } + }); + + // 2. 视图层数据适配 + const formattedData = localAnimes + .filter(anime => !anime.isHiddenChild) + .map(anime => { + // 兼容实体类与普通对象 + const animeJson = typeof anime.toJson === 'function' ? anime.toJson() : { ...anime }; + const { links = [], mergedChildren = [], ...rest } = animeJson; + + return { + ...rest, + episodes: rest.episodeCount || rest.episodes || 1, + links, + mergedChildren: mergedChildren.map(child => { + const fullChild = fullAnimeMap.get(`${child.source}_${child.animeId}`); + const fullChildJson = typeof fullChild?.toJson === 'function' ? fullChild.toJson() : fullChild; + const fullLinks = (fullChildJson?.links?.length > 0) ? fullChildJson.links : (child.links || []); + + return { + ...child, + episodes: child.episodeCount || child.episodes || 1, + links: fullLinks + }; + }) + }; + }); + + return jsonResponse({ success: true, data: formattedData }, 200); + } catch (error) { + log("error", `[server] Fetch cache animes failed: ${error.message}`); + return jsonResponse({ success: false, message: `获取缓存失败: ${error.message}` }, 500); + } +} diff --git a/danmu_api/models/dandan-model.js b/danmu_api/models/dandan-model.js index 9727ed7..c81a43e 100644 --- a/danmu_api/models/dandan-model.js +++ b/danmu_api/models/dandan-model.js @@ -6,7 +6,8 @@ import { validateType } from "../utils/common-util.js"; export class Anime { constructor({ animeId = 111, bangumiId = "", animeTitle = "", type = "", typeDescription = "", imageUrl = "", startDate = "", episodeCount = 1, - rating = 0, isFavorited = true, source = "", links = [] } = {}) { + rating = 0, isFavorited = true, source = "", links = [], + mergedChildren = [], isHiddenChild = false } = {}) { // ---- 类型检查 ---- validateType(animeId, "number"); validateType(bangumiId, "string"); @@ -20,13 +21,15 @@ export class Anime { validateType(isFavorited, "boolean"); validateType(source, "string"); validateType(links, "array"); + validateType(mergedChildren, "array"); + validateType(isHiddenChild, "boolean"); // 将 links 转换为 Link 实例数组 this.links = links.map(linkData => Link.fromJson(linkData)); // 直接解构并赋值给 this Object.assign(this, { animeId, bangumiId, animeTitle, type, typeDescription, imageUrl, startDate, - episodeCount, rating, isFavorited, source }); + episodeCount, rating, isFavorited, source, mergedChildren, isHiddenChild }); } // ---- 静态方法:从 JSON 创建 Anime 对象 ---- diff --git a/danmu_api/ui/css/components.css.js b/danmu_api/ui/css/components.css.js index f978a4d..5662092 100644 --- a/danmu_api/ui/css/components.css.js +++ b/danmu_api/ui/css/components.css.js @@ -691,6 +691,13 @@ export const componentsCssContent = /* css */ ` .anime-title { margin: 8px 0 5px; font-size: 12px; + word-break: normal; /* 保持正常的单词换行规则,优先在空格处断句以保证可读性 */ + overflow-wrap: break-word; /* 当长字符串(如多源合并后缀)超出容器时,允许在其内部强制换行 */ + display: -webkit-box; /* 启用 WebKit 特有的弹性伸缩盒模型 */ + -webkit-line-clamp: 5; /* 限制文本块最多显示的行数为5行 */ + -webkit-box-orient: vertical; /* 设置弹性盒子的子元素垂直排列 */ + overflow: hidden; /* 隐藏超出容器设定高度范围的文本内容 */ + text-overflow: ellipsis; /* 文本溢出时使用省略号(...)进行截断展示 */ } .episode-list-container { @@ -1726,4 +1733,201 @@ export const componentsCssContent = /* css */ ` gap: 8px; justify-content: flex-end; } + +/* 最近数据与animes缓存面板样式 */ + +/* 面板容器 */ +.recent-data-panel { + display: none; + max-height: 350px; + overflow-y: auto; +} + +.anime-cache-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.anime-cache-card { + background: #fff; + border-radius: 6px; + border: 1px solid #eaeaea; + box-shadow: 0 1px 3px rgba(0,0,0,0.04); + overflow: hidden; +} + +.anime-cache-card-body { + display: flex; + gap: 12px; + align-items: center; + padding: 10px; +} + +.anime-cache-cover { + width: 44px; + height: 60px; + border-radius: 4px; + background-color: #e2e3e5; + background-size: cover; + background-position: center; + flex-shrink: 0; + box-shadow: 0 1px 4px rgba(0,0,0,0.15); +} + +.anime-cache-info, +.anime-cache-child-info { + flex: 1; + min-width: 0; +} + +.anime-cache-title { + font-weight: 600; + font-size: 13px; + line-height: 1.4; + margin-bottom: 4px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-all; +} + +.anime-cache-meta { + color: #888; + font-size: 11px; +} + +.anime-cache-actions, +.anime-cache-child-actions { + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.anime-cache-actions { gap: 6px; } + +.btn-xs { + padding: 2px 6px; + font-size: 11px; +} + +/* 卡片底部标签切换栏(Tab 风格) */ +.anime-cache-footer { + display: flex; + gap: 8px; + padding: 8px 10px; + background: #fdfdfd; + border-top: 1px solid #f0f0f0; +} + +.cache-badge { + font-size: 11px; + padding: 4px 10px; + border-radius: 12px; + cursor: pointer; + user-select: none; + display: inline-flex; + align-items: center; + font-weight: 500; + transition: all 0.2s; +} + +.cache-badge.badge-sources { background: #e3f2fd; color: #0d47a1; border: 1px solid #bbdefb; } +.cache-badge.badge-episodes { background: #e8f5e9; color: #1b5e20; border: 1px solid #c8e6c9; } +.cache-badge.active { filter: brightness(0.9); box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); } + +/* 折叠列表容器 */ +.merged-children-container, +.episodes-list-container { + display: none; + flex-direction: column; + gap: 6px; + padding: 8px 10px; + background: #fafafa; + border-top: 1px solid #f0f0f0; +} + +.episodes-list-container { + max-height: 200px; + overflow-y: auto; +} + +/* 子节点卡片 */ +.anime-cache-child-item { + display: flex; + flex-direction: column; + background: #fff; + padding: 8px; + border-radius: 4px; + border: 1px dashed #ccc; +} + +.anime-cache-child-main { + display: flex; + gap: 10px; + align-items: center; + width: 100%; +} + +.anime-cache-child-cover { + width: 33px; + height: 45px; + border-radius: 3px; + background-color: #e2e3e5; + background-size: cover; + background-position: center; + flex-shrink: 0; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.anime-cache-child-title { + font-weight: 500; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.anime-cache-child-actions { gap: 4px; } + +/* 合并映射详情 */ +.child-mapping-toggle { + font-size: 10px; + color: #666; + cursor: pointer; + margin-top: 8px; + padding-top: 6px; + border-top: 1px dashed #eee; + text-align: center; +} + +.child-mapping-toggle:hover { + color: #0066cc; +} + +.child-mapping-container { + display: none; + flex-direction: column; + gap: 4px; + margin-top: 6px; + background: #fdfdfd; + padding: 6px; + border-radius: 4px; + border: 1px solid #f0f0f0; + max-height: 150px; + overflow-y: auto; +} + +.mapping-row { + font-size: 10px; + display: flex; + gap: 6px; + align-items: flex-start; +} + +.mapping-status { flex-shrink: 0; font-weight: bold; } +.mapping-status.success { color: #28a745; } +.mapping-status.warning { color: #d39e00; } +.mapping-text { color: #555; word-break: break-all; } `; diff --git a/danmu_api/ui/js/systemsettings.js b/danmu_api/ui/js/systemsettings.js index 14a8f7c..184035e 100644 --- a/danmu_api/ui/js/systemsettings.js +++ b/danmu_api/ui/js/systemsettings.js @@ -522,6 +522,17 @@ function renderValueInput(item) { }).join('')} + + \${currentKey === 'MERGE_SOURCE_PAIRS' ? \` +
+ +
+
+
+
+ \` : ''} \`; // 设置拖动事件 @@ -614,10 +625,17 @@ function renderValueInput(item) { container.innerHTML = \` -
+
+ +
+
+
点击下方按钮可快捷填入规则表单:
+
季和集不填则对所有季/集生效
@@ -725,10 +743,17 @@ function renderValueInput(item) { container.innerHTML = \` -
+
+ +
+
+
点击下方按钮可快捷填入规则表单:
+
@@ -2450,4 +2475,351 @@ async function verifyAiConnection() { btn.disabled = false; } } + +/* ======================================== + 最近数据与animes缓存面板功能 + ======================================== */ + +// 统一切换卡片内的折叠区域 +function toggleCardSection(btnEl, targetSelector, openText, closeText) { + if (!btnEl) return; + const card = btnEl.closest('.anime-cache-card'); + if (!card) return; + const container = card.querySelector(targetSelector); + if (!container) return; + + const isHidden = window.getComputedStyle(container).display === 'none'; + + if (isHidden) { + container.style.display = 'flex'; + btnEl.innerHTML = closeText; + btnEl.classList.add('active'); + } else { + container.style.display = 'none'; + btnEl.innerHTML = openText; + btnEl.classList.remove('active'); + } +} + +// 切换子节点内的映射详情 +function toggleMapping(btnEl) { + if (!btnEl) return; + const parentItem = btnEl.closest('.anime-cache-child-item'); + if (!parentItem) return; + const container = parentItem.querySelector('.child-mapping-container'); + if (!container) return; + + const isHidden = window.getComputedStyle(container).display === 'none'; + if (isHidden) { + container.style.display = 'flex'; + btnEl.innerHTML = '📊 收起映射详情'; + btnEl.classList.add('active'); + } else { + container.style.display = 'none'; + btnEl.innerHTML = '📊 展开映射详情'; + btnEl.classList.remove('active'); + } +} + +// 快捷数据面板业务逻辑 +async function fetchAndShowRecentData() { + const panel = document.getElementById('recent-data-panel'); + const listContainer = document.getElementById('recent-data-list'); + + if (!panel || !listContainer) return; + + if (window.getComputedStyle(panel).display !== 'none') { + panel.style.display = 'none'; + return; + } + + panel.style.display = 'block'; + listContainer.innerHTML = ' 数据加载中...'; + + try { + const response = await fetch(buildApiUrl('/api/cache/animes', true)); + const result = await response.json(); + + if (result.success && result.data && result.data.length > 0) { + renderAnimeCachePanel(result.data, listContainer); + } else { + listContainer.innerHTML = '
缓存中暂无番剧数据,请先通过客户端请求弹幕接口以生成缓存。
'; + } + } catch (error) { + listContainer.innerHTML = \`
请求失败: \${error.message}
\`; + } +} + +// 渲染animes缓存面板 (含集数解析与映射详情) +function renderAnimeCachePanel(data, listContainer) { + const keyInput = document.getElementById('env-key'); + + if (!listContainer || !keyInput) return; + + const currentKey = keyInput.value; + + // 内部辅助函数:生成操作按钮 + const generateButtons = (title, source) => { + if (currentKey === 'CUSTOM_MERGE_RULES') { + return \` + + + \`; + } else if (currentKey === 'DANMU_OFFSET') { + return \` + + \`; + } + return ''; + }; + + // 内部辅助函数:清洗标题 + const cleanTitleStr = (rawTitle) => rawTitle.replace(/\\s*from\\s+.*$/i, '').trim().replace(/'/g, '''); + + let html = '
'; + + data.forEach(item => { + const cleanTitle = cleanTitleStr(item.animeTitle); + const coverStyle = item.imageUrl ? \`background-image: url('\${item.imageUrl}');\` : ''; + + // 1. 构建合并子源模块 + let childrenHtml = ''; + let childrenCount = 0; + if (item.mergedChildren && item.mergedChildren.length > 0) { + childrenCount = item.mergedChildren.length; + const childItems = item.mergedChildren.map(child => { + const childCleanTitle = cleanTitleStr(child.animeTitle); + + const childCoverStyle = child.imageUrl ? \`background-image: url('\${child.imageUrl}');\` : ''; + + // 解析映射数据并按匹配状态排序 + let mappingHtml = ''; + if (item.links && item.links.length > 0) { + const parsedRows = []; + + item.links.forEach((link, idx) => { + const urlStr = String(link.url || ''); + const hasMainMatch = urlStr.includes(item.source + ':') || !urlStr.includes(':'); + const hasChildMatch = urlStr.includes(child.source + ':'); + + if (!hasMainMatch && !hasChildMatch) return; + + let mainSide = '(主源越界)'; + if (hasMainMatch) { + const cleanMainEpTitle = (link.title || '未知剧集').replace(/^【.*?】\\s*/, '').replace(/'/g, ''').replace(/"/g, '"'); + mainSide = \`【\${item.source}】\${cleanMainEpTitle}\`; + } + + let childSide = '(副源缺失)'; + let childNum = NaN; + + if (hasChildMatch) { + const regex = new RegExp(child.source + ':([^$]+)'); + const match = urlStr.match(regex); + let childTitleStr = '(源ID未知)'; + + if (match) { + const childId = match[1]; + childTitleStr = \`(源ID: \${childId})\`; + + if (child.links && child.links.length > 0) { + const childLink = child.links.find(l => String(l.url) === String(childId)); + if (childLink && childLink.title) { + childTitleStr = childLink.title.replace(/^【.*?】\\s*/, '').replace(/'/g, ''').replace(/"/g, '"'); + } + } + } else { + childTitleStr = (link.title || '').replace(/^【.*?】\\s*/, '').replace(/'/g, ''').replace(/"/g, '"'); + } + childSide = \`【\${child.source}】\${childTitleStr}\`; + + const numMatch = childTitleStr.match(/\\d+/); + if (numMatch) { + childNum = parseInt(numMatch[0], 10); + } + } + + let rowHtml = ''; + if (hasMainMatch && hasChildMatch) { + rowHtml = \`
✓ 匹配 \${mainSide} ↔ \${childSide}
\`; + } else { + rowHtml = \`
✗ 落单 \${mainSide} ↔ \${childSide}
\`; + } + + parsedRows.push({ + originalIndex: idx, + hasChildMatch: hasChildMatch, + childNum: childNum, + html: rowHtml + }); + }); + + const matchedRows = parsedRows.filter(r => r.hasChildMatch).sort((a, b) => { + if (!isNaN(a.childNum) && !isNaN(b.childNum)) { + return a.childNum - b.childNum; + } + return a.originalIndex - b.originalIndex; + }); + + const lonelyRows = parsedRows.filter(r => !r.hasChildMatch).sort((a, b) => a.originalIndex - b.originalIndex); + + const finalRows = [...matchedRows, ...lonelyRows]; + const mappingRowsHtml = finalRows.map(r => r.html).join(''); + + if (mappingRowsHtml) { + mappingHtml = \` +
📊 展开映射详情
+
+ \${mappingRowsHtml} +
+ \`; + } + } + + return \` +
+
+
+
+
\${childCleanTitle}
+
[\${child.source}] (\${child.episodes}集)
+
+
+ \${generateButtons(childCleanTitle, child.source)} +
+
+ \${mappingHtml} +
+ \`; + }).join(''); + + childrenHtml = \` +
+ \${childItems} +
+ \`; + } + + // 2. 构建剧集参考列表模块 + let episodesHtml = ''; + let episodesCount = 0; + if (item.links && item.links.length > 0) { + episodesCount = item.links.length; + const epItems = item.links.map(link => { + const safeTitle = link.title ? link.title.replace(/'/g, ''').replace(/"/g, '"') : '未知剧集'; + return \` +
+
+
+
\${safeTitle}
+
+
+
+ \`; + }).join(''); + + episodesHtml = \` +
+ \${epItems} +
+ \`; + } + + // 3. 构建卡片底部专属切换栏 (Tab Bar) + let footerHtml = ''; + if (childrenCount > 0 || episodesCount > 0) { + let badges = ''; + if (episodesCount > 0) { + badges += \`
📺 \${episodesCount} 个剧集
\`; + } + if (childrenCount > 0) { + badges += \`
🔗 \${childrenCount} 个被合并源
\`; + } + footerHtml = \`\`; + } + + // 4. 组装完整卡片 + html += \` +
+
+
+
+
\${cleanTitle}
+
[\${item.source}] (\${item.episodes}集)
+
+
+ \${generateButtons(cleanTitle, item.source)} +
+
+ \${footerHtml} + \${childrenHtml} + \${episodesHtml} +
+ \`; + }); + + html += '
'; + listContainer.innerHTML = html; +} + +/* ======================================== + 表单填充快捷操作功能 + ======================================== */ + +// 内部辅助函数:输入框视觉反馈统一处理 +function applyInputFeedback(inputEl) { + if (!inputEl) return; + const oldBorder = inputEl.style.borderColor; + inputEl.style.borderColor = '#28a745'; + setTimeout(() => inputEl.style.borderColor = oldBorder, 800); + inputEl.focus(); +} + +// 表单填充逻辑:合并映射表 +function fillMergeEntity(type, title, source) { + const panel = document.getElementById('merge-rule-panel'); + if (panel && window.getComputedStyle(panel).display === 'none') { + toggleMergeRulePanel(); + } + + const inputId = type === 'sec' ? 'merge-sec-entity' : 'merge-prim-entity'; + const inputEl = document.getElementById(inputId); + + if (inputEl) { + inputEl.value = \`\${title}@\${source}\`; + applyInputFeedback(inputEl); + setMergeFocus(type); + } +} + +// 表单填充逻辑:弹幕偏移 +function fillOffsetEntity(title, source) { + const panel = document.getElementById('offset-rule-panel'); + if (panel && window.getComputedStyle(panel).display === 'none') { + toggleOffsetRulePanel(); + } + + // 视图层数据清洗:去除年份和类型后缀 + const cleanTitle = title + .replace(/[\\u200B-\\u200F\\uFEFF]/g, '') + .replace(/\\s*[((〔\\[]\\s*[0-90-9]{4}\\s*年?\\s*[))〕\\]]/g, '') + .replace(/(.+?)\\s*【[^】]+】$/, '$1') + .trim(); + + const inputEl = document.getElementById('offset-anime'); + + if (inputEl) { + inputEl.value = cleanTitle; + + // 基于 data-value 属性匹配并选中对应来源标签 + const sourceTags = document.querySelectorAll('#offset-sources .offset-source-tag'); + if (sourceTags.length > 0) { + sourceTags.forEach(el => el.classList.remove('selected')); + const targetTag = Array.from(sourceTags).find(el => el.dataset.value === source); + if (targetTag) targetTag.classList.add('selected'); + } + + applyInputFeedback(inputEl); + } +} `; diff --git a/danmu_api/utils/merge-util.js b/danmu_api/utils/merge-util.js index 72b0438..12afe0a 100644 --- a/danmu_api/utils/merge-util.js +++ b/danmu_api/utils/merge-util.js @@ -2827,6 +2827,22 @@ async function processMergeTask(params) { mappingEntries.sort((a, b) => a.idx - b.idx); log("info", `${logPrefix} [${secSource}] 映射详情:\n${mappingEntries.map(e => e.text).join('\n')}`); } + // ── 记录合并层级 ────────────────────────────────────── + derivedAnime.mergedChildren = derivedAnime.mergedChildren || []; + // 1. 将副源数据存入全新组合对象的 mergedChildren 集合中 + if (!derivedAnime.mergedChildren.some(c => String(c.animeId) === String(match.animeId) && c.source === match.source)) { + derivedAnime.mergedChildren.push({ + animeId: match.animeId, + animeTitle: match.animeTitle, + source: match.source, + episodes: derivedMatch.links ? derivedMatch.links.length : 0, + imageUrl: match.imageUrl || '' + }); + } + + // 2. 在全局缓存中将“被合并的原始副源”标记为隐藏 + const cachedSec = globals.animes.find(a => String(a.animeId) === String(match.animeId) && a.source === match.source); + if (cachedSec) cachedSec.isHiddenChild = true; // ── 合集进度双向写入 ────────────────────────────────────── // 支持主源为合集与副源为合集两种情况,为链式关联的下一次切片铺路 @@ -2913,6 +2929,9 @@ async function processMergeTask(params) { return null; } generatedSignatures.add(signature); + // 在全局缓存中将“被取代的原始主源”标记为隐藏 + const cachedPrim = globals.animes.find(a => String(a.animeId) === String(pAnime.animeId) && a.source === pAnime.source); + if (cachedPrim) cachedPrim.isHiddenChild = true; // 标记消费:合集 ID 仅标记全局消费(保留组内复用能力),普通 ID 标记两级消费 for (let i = 1; i < contentSignatureParts.length; i++) { diff --git a/danmu_api/worker.js b/danmu_api/worker.js index 4364cfa..d3befb1 100644 --- a/danmu_api/worker.js +++ b/danmu_api/worker.js @@ -8,7 +8,7 @@ import AIClient from './utils/ai-util.js'; import { initBangumiData } from "./utils/bangumi-data-util.js"; import { getBangumi, getComment, getCommentByUrl, getSegmentComment, matchAnime, searchAnime, searchEpisodes } from "./apis/dandan-api.js"; import { getFongmiDanmaku } from "./apis/clients/fongmi-api.js"; -import { handleConfig, handleUI, handleLogs, handleClearLogs, handleDeploy, handleClearCache, handleReqRecords } from "./apis/system-api.js"; +import { handleConfig, handleUI, handleLogs, handleClearLogs, handleDeploy, handleClearCache, handleReqRecords, handleCacheAnimes } from "./apis/system-api.js"; import { handleSetEnv, handleAddEnv, handleDelEnv, handleAiVerify } from "./apis/env-api.js"; import { Segment } from "./models/dandan-model.js" import { @@ -514,6 +514,11 @@ async function handleRequest(req, env, deployPlatform, clientIp, ctx) { return handleDeploy(); } + // GET /api/cache/animes - 获取最近 animes 缓存 + if (path === "/api/cache/animes" && method === "GET") { + return handleCacheAnimes(); + } + // POST /api/cache/clear - 清理缓存 if (path === "/api/cache/clear" && method === "POST") { return handleClearCache();