Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions danmu_api/apis/env-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { jsonResponse } from '../utils/http-util.js';
import { log } from '../utils/log-util.js';
import { HandlerFactory } from '../configs/handlers/handler-factory.js';
import { globals } from '../configs/globals.js';
import { syncBangumiDataLifecycleOnConfigChange } from '../utils/bangumi-data-util.js';
import AIClient from '../utils/ai-util.js';

/**
Expand All @@ -24,6 +25,10 @@ export async function handleSetEnv(request) {
// 调用handler的setEnv方法
const result = await handler.setEnv(key, value);

if (result && key === 'USE_BANGUMI_DATA') {
syncBangumiDataLifecycleOnConfigChange(deployPlatform);
}

if (result) {
return jsonResponse({ success: true, message: `环境变量 ${key} 设置成功` });
} else {
Expand Down Expand Up @@ -55,6 +60,10 @@ export async function handleAddEnv(request) {
// 调用handler的addEnv方法
const result = await handler.addEnv(key, value);

if (result && key === 'USE_BANGUMI_DATA') {
syncBangumiDataLifecycleOnConfigChange(deployPlatform);
}

if (result) {
return jsonResponse({ success: true, message: `环境变量 ${key} 添加成功` });
} else {
Expand Down Expand Up @@ -86,6 +95,10 @@ export async function handleDelEnv(request) {
// 调用handler的delEnv方法
const result = await handler.delEnv(key);

if (result && key === 'USE_BANGUMI_DATA') {
syncBangumiDataLifecycleOnConfigChange(deployPlatform);
}

if (result) {
return jsonResponse({ success: true, message: `环境变量 ${key} 删除成功` });
} else {
Expand Down
13 changes: 7 additions & 6 deletions danmu_api/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
import dotenv from 'dotenv';
import { Request as NodeFetchRequest } from 'node-fetch';
import { handleRequest } from './worker.js';
import { Globals } from './configs/globals.js';
import { Globals, globals } from './configs/globals.js';
import { Envs } from './configs/envs.js';
import { clearBangumiDataCache, initBangumiData } from './utils/bangumi-data-util.js';
import { clearBangumiDataCache, initBangumiData, syncBangumiDataLifecycleOnConfigChange } from './utils/bangumi-data-util.js';
import { getLocalCaches, judgeLocalCacheValid } from './utils/cache-util.js';
import { getRedisCaches, judgeRedisValid } from './utils/redis-util.js';
import { persistFavorites, refreshFavoriteByKeyword } from './apis/favorite-api.js';
Expand Down Expand Up @@ -234,10 +234,11 @@ async function setupEnvWatcher() {
console.log('[server] Environment variables reloaded successfully');
console.log('[server] Updated keys:', Array.from(newEnvKeys).join(', '));

// 如果检测到关闭了 Bangumi Data 功能,主动释放内存与物理磁盘缓存
if (process.env.USE_BANGUMI_DATA === 'false' || process.env.USE_BANGUMI_DATA === false) {
clearBangumiDataCache(true);
}
// 配置变更后同步 Bangumi Data 生命周期:开启则立即确保缓存就绪(缺失则下载),
// 关闭则释放缓存;先按真实配置同步内存开关,避免重载未刷新 globals 时状态滞后
const bangumiEnabled = process.env.USE_BANGUMI_DATA === 'true' || process.env.USE_BANGUMI_DATA === true;
globals.useBangumiData = bangumiEnabled;
syncBangumiDataLifecycleOnConfigChange('node');

} catch (error) {
console.log('[server] Error reloading configuration files:', error.message);
Expand Down
67 changes: 48 additions & 19 deletions danmu_api/utils/bangumi-data-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ let downloadLockTime = 0;
let memoryFootprintMB = '0.00';
let hasLoggedCacheWarning = false;
let charInvertedIndex = new Map();
let currentBackgroundDownload = null; // 在途后台下载 Promise:由 initBangumiData 在发起非阻塞刷新时写入,供边缘层 ctx.waitUntil 延长 Serverless 生命周期

// 当前生效的数据源标识 ('custom' | 'official')
let activeDataSource = 'custom';
// 缓存已解析的版本号(进程内有效)
let cachedCustomVersion = null;
let activeDataSource = 'custom'; // 当前生效的数据源标识 ('custom' | 'official')
let cachedCustomVersion = null; // 缓存已解析的版本号(进程内有效)
let cachedOfficialVersion = null;

let versionQueryPromise = null; // 版本查询并发锁:serverless环境下冻结时异步信号可能不生效,共享同一Promise避免重复探测

// 定义缓存目录/文件名
Expand Down Expand Up @@ -193,6 +191,46 @@ async function selectBestDataSource() {
return versionQueryPromise;
}

// 数据下载触发时机:仅在 searchBangumiData 消费路径进入时触发下载;开关关闭或无所消费源时不下载
// 开启时复用 initBangumiData 加载/下载,并发调用等待同一就绪缓存而不重复请求
export async function ensureBangumiDataReady(deployPlatform = globals.deployPlatform) {
if (!globals.useBangumiData) return;
await initBangumiData(deployPlatform, true);
}

// 配置变更后同步 Bangumi Data 生命周期:开关开启立即下载(已缓存则幂等),关闭释放缓存
// 调用前须保证 globals.useBangumiData 已反映最新配置(watcher 场景须先同步内存开关)
export function syncBangumiDataLifecycleOnConfigChange(deployPlatform = globals.deployPlatform) {
if (globals.useBangumiData) {
initBangumiData(deployPlatform, true).catch(console.error);
} else {
clearBangumiDataCache(true);
}
}

// 读取在途后台下载 Promise,供边缘运行时在请求响应后延长生命周期(无在途时返回 null)
export function getBackgroundDownload() {
return currentBackgroundDownload;
}

// 边缘运行时在响应返回后延长生命周期:仅在存在在途后台下载且运行时提供 waitUntil 时注册
export function extendBangumiDownloadLifecycle(ctx) {
if (ctx && typeof ctx.waitUntil === 'function' && currentBackgroundDownload) {
ctx.waitUntil(currentBackgroundDownload);
}
}

// 发起后台静默下载并记录在途 Promise:下载完成时复位状态,供 getBackgroundDownload 暴露给边缘层
function startDownload(cachePath) {
isDownloading = true;
downloadLockTime = Date.now();
currentBackgroundDownload = downloadAndCache(cachePath).finally(() => {
isDownloading = false;
currentBackgroundDownload = null;
});
return currentBackgroundDownload;
}

/**
* 初始化 Bangumi Data 数据源
* 包含内存与磁盘双端缓存的生命周期校验,并在环境允许时持久化缓存数据
Expand All @@ -201,7 +239,7 @@ async function selectBestDataSource() {
* @param {boolean} isDataDependentRequest - 当前是否为强依赖数据的核心接口请求
* @returns {Promise<void>}
*/
export async function initBangumiData(deployPlatform, isDataDependentRequest = false, ctx = null) {
export async function initBangumiData(deployPlatform, isDataDependentRequest = false) {
if (!globals.useBangumiData) return;

let cachePath = null;
Expand Down Expand Up @@ -237,9 +275,7 @@ export async function initBangumiData(deployPlatform, isDataDependentRequest = f
// 当内存过期或配置强制更新时,仅在核心请求触发后台静默更新
if (isDataDependentRequest && !isDownloading) {
log("info", `[system] [Bangumi-Data] 内存数据${cacheDays === 0 ? '强制更新' : '已过期'},保留老数据服务本次请求,启动后台静默更新...`);
isDownloading = true;
downloadLockTime = Date.now();
downloadAndCache(cachePath).finally(() => { isDownloading = false; });
startDownload(cachePath);
}
return;
}
Expand Down Expand Up @@ -274,9 +310,7 @@ export async function initBangumiData(deployPlatform, isDataDependentRequest = f
if (cacheDays === 0 || (Date.now() - stats.mtimeMs >= expireMs)) {
if (isDataDependentRequest && !isDownloading) {
log("info", `[system] [Bangumi-Data] 磁盘数据${cacheDays === 0 ? '强制更新' : '已过期'},保留老数据服务本次请求,启动后台静默更新...`);
isDownloading = true;
downloadLockTime = Date.now();
downloadAndCache(cachePath).finally(() => { isDownloading = false; });
startDownload(cachePath);
}
}
return;
Expand All @@ -289,19 +323,13 @@ export async function initBangumiData(deployPlatform, isDataDependentRequest = f
// 内存与磁盘均无有效数据时的获取逻辑
if (!isDownloading) {
log("info", `[system] [Bangumi-Data] 未命中任何有效缓存,正在获取基础数据...`);
isDownloading = true;
downloadLockTime = Date.now();

const downloadPromise = downloadAndCache(cachePath).finally(() => { isDownloading = false; });
const downloadPromise = startDownload(cachePath);

if (isDataDependentRequest) {
await downloadPromise;
} else {
log("info", `[system] [Bangumi-Data] 当前非核心请求,数据获取转入后台异步执行`);
if (ctx && typeof ctx.waitUntil === 'function') {
log("info", `[system] [Bangumi-Data] 调用 ctx.waitUntil 延长 Serverless 生命周期`);
ctx.waitUntil(downloadPromise);
}
}
} else if (isDataDependentRequest) {
log("info", `[system] [Bangumi-Data] 正在等待基础数据下载完成...`);
Expand Down Expand Up @@ -503,6 +531,7 @@ async function downloadAndCache(cachePath) {
* @returns {Array<Object>} 匹配的动漫条目数组
*/
export async function searchBangumiData(keyword, siteKeys) {
await ensureBangumiDataReady();
if (!memoryCache || !memoryCache.items) return [];

let searchPromise = queryCache.get(keyword);
Expand Down
2 changes: 1 addition & 1 deletion danmu_api/utils/tmdb-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ export async function getTMDBChineseTitle(title, season = null, episode = null)
// 优先尝试本地 Bangumi Data 转换
if (globals.useBangumiData) {
const cleanTitle = cleanSearchQuery(title);
const localMatches = searchBangumiData(cleanTitle, ['tmdb', 'bangumi', 'anidb']);
const localMatches = await searchBangumiData(cleanTitle, ['tmdb', 'bangumi', 'anidb']);
if (localMatches && localMatches.length > 0) {
const m = localMatches[0];
// 找一个不全是外文的翻译作为中文名
Expand Down
16 changes: 6 additions & 10 deletions danmu_api/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { getFavoriteCachesFromRedis, getRedisCaches, judgeRedisValid } from "./u
import { cleanupExpiredIPs, findUrlById, getCommentCache, getLocalCaches, judgeLocalCacheValid } from "./utils/cache-util.js";
import { formatDanmuResponse } from "./utils/danmu-util.js";
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 { handleFavoriteAdd, handleFavoriteList, handleFavoriteRefresh, handleFavoriteRemove, handleFavoriteSchedule } from "./apis/favorite-api.js";
import { getFongmiDanmaku } from "./apis/clients/fongmi-api.js";
import { handleConfig, handleUI, handleLogs, handleClearLogs, handleDeploy, handleClearCache, handleReqRecords, handleCacheAnimes } from "./apis/system-api.js";
import { handleForwardTrace } from "./apis/forward-trace-api.js";
import { handleSetEnv, handleAddEnv, handleDelEnv, handleAiVerify } from "./apis/env-api.js";
import { extendBangumiDownloadLifecycle } from "./utils/bangumi-data-util.js";
import { Segment } from "./models/dandan-model.js"
import {
handleCookieStatus,
Expand All @@ -23,21 +23,14 @@ import {

let globals;

async function handleRequest(req, env, deployPlatform, clientIp, ctx) {
async function handleRequest(req, env, deployPlatform, clientIp) {
// 加载全局变量和环境变量配置
globals = Globals.init(env);

const url = new URL(req.url);
let path = url.pathname;
const method = req.method;

// Bangumi Data 辅助函数,用于判断数据更新
const isDataDependentRequest = path.includes('/search') || path.includes('/match') || path.includes('/danmaku');

if (globals.useBangumiData) {
await initBangumiData(deployPlatform, isDataDependentRequest, ctx);
}

globals.deployPlatform = deployPlatform;
if (deployPlatform === "node") {
await judgeLocalCacheValid(path, deployPlatform);
Expand Down Expand Up @@ -754,7 +747,10 @@ export default {
// 获取客户端的真实 IP
const clientIp = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || 'unknown';

return handleRequest(request, env, detectDeployPlatform(env), clientIp, ctx);
const response = await handleRequest(request, env, detectDeployPlatform(env), clientIp);
// 边缘运行时在响应返回后延长生命周期,容纳可能在途的 Bangumi Data 后台静默下载
extendBangumiDownloadLifecycle(ctx);
return response;
},
};

Expand Down
95 changes: 95 additions & 0 deletions danmu_api/worker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2850,6 +2850,101 @@ test('worker.js API endpoints', async (t) => {
// });
// });

// // 测试 Bangumi Data 数据下载时机(ensureBangumiDataReady)、配置变更触发下载(syncBangumiDataLifecycleOnConfigChange)
// // 以及 getTMDBChineseTitle 漏写 await 的修复;与 envs RAW_ENV_KEYS 测试同为按需启用的内部测试
// import { globals } from './configs/globals.js';
// import { ensureBangumiDataReady, syncBangumiDataLifecycleOnConfigChange, initBangumiData, clearBangumiDataCache } from './utils/bangumi-data-util.js';
// import fs from 'node:fs';
// import path from 'node:path';
//
// test('bangumi-data 数据下载时机与配置变更触发下载', async (t) => {
// const CACHE_DIR = path.join(process.cwd(), '.cache');
// const CACHE_FILE = path.join(CACHE_DIR, 'bangumi-data-cache.json');
// const FAKE_ITEM = {
// title: 'FrobeniusTestAnime',
// titleTranslate: { 'zh-Hans': ['弗罗贝尼乌斯测试动画', 'FrobeniusTestAnime'] },
// sites: [{ site: 'tmdb', id: '999999' }],
// _flatText: 'frobeniustestanime'
// };
// const reset = () => {
// globals.useBangumiData = false;
// clearBangumiDataCache(false);
// if (fs.existsSync(CACHE_FILE)) fs.writeFileSync(CACHE_FILE, '', 'utf-8');
// };
//
// await t.test('ensureBangumiDataReady 开关关闭时直接返回且不触发下载', async () => {
// reset();
// globals.useBangumiData = false;
// await ensureBangumiDataReady('node');
// assert.ok(true);
// });
//
// await t.test('syncBangumiDataLifecycleOnConfigChange 开关关闭释放缓存、开启安全触发', async () => {
// reset();
// globals.useBangumiData = false;
// assert.doesNotThrow(() => syncBangumiDataLifecycleOnConfigChange('node'));
// fs.mkdirSync(CACHE_DIR, { recursive: true });
// fs.writeFileSync(CACHE_FILE, JSON.stringify({ items: [FAKE_ITEM] }), 'utf-8');
// globals.useBangumiData = true;
// assert.doesNotThrow(() => syncBangumiDataLifecycleOnConfigChange('node'));
// });
//
// await t.test('getTMDBChineseTitle 经 await 命中本地中文名(修复漏写 await)', async () => {
// reset();
// globals.useBangumiData = true;
// const originalContent = fs.existsSync(CACHE_FILE) ? fs.readFileSync(CACHE_FILE, 'utf-8') : null;
// fs.mkdirSync(CACHE_DIR, { recursive: true });
// fs.writeFileSync(CACHE_FILE, JSON.stringify({ items: [FAKE_ITEM] }), 'utf-8');
// try {
// await initBangumiData('node', true);
// const result = await getTMDBChineseTitle('FrobeniusTestAnime');
// assert.equal(result, '弗罗贝尼乌斯测试动画');
// } finally {
// clearBangumiDataCache(false);
// if (originalContent !== null) fs.writeFileSync(CACHE_FILE, originalContent, 'utf-8');
// else fs.writeFileSync(CACHE_FILE, '', 'utf-8');
// }
// });
// });

// // 测试 Bangumi Data 在途下载暴露与边缘生命周期延长(getBackgroundDownload / extendBangumiDownloadLifecycle)
// // 与上方 bangumi 测试同为按需启用的内部测试;沙箱有网时真实下载以验证在途暴露、注册与清理
// import { globals } from './configs/globals.js';
// import { initBangumiData, getBackgroundDownload, extendBangumiDownloadLifecycle } from './utils/bangumi-data-util.js';
// import assert from 'node:assert';
// import fs from 'node:fs';
// import path from 'node:path';
//
// test('bangumi-data 在途下载暴露与边缘生命周期延长', async (t) => {
// const CACHE_DIR = path.join(process.cwd(), '.cache');
// const CACHE_FILE = path.join(CACHE_DIR, 'bangumi-data-cache.json');
// const hadCache = fs.existsSync(CACHE_DIR);
//
// // 空闲时无在途下载
// assert.strictEqual(getBackgroundDownload(), null);
//
// await t.test('extendBangumiDownloadLifecycle 在无在途或 ctx 缺失时不注册', async () => {
// const calls = [];
// extendBangumiDownloadLifecycle(null);
// extendBangumiDownloadLifecycle({ waitUntil: (p) => calls.push(p) });
// assert.strictEqual(calls.length, 0);
// });
//
// await t.test('在途下载被暴露、响应后由边缘 waitUntil 注册、完成后清理', async () => {
// globals.useBangumiData = true;
// // 启动真实下载(无 .cache 时走内存路径,不落地文件;有 .cache 则后台刷新),不在途时立即返回
// const initPromise = initBangumiData('node', true);
// const bg = getBackgroundDownload();
// assert.ok(bg && typeof bg.then === 'function', '下载在途时应暴露 Promise');
// const ctx = { waitUntil: (p) => { ctx.registered = p; } };
// extendBangumiDownloadLifecycle(ctx);
// assert.strictEqual(ctx.registered, bg, '边缘 waitUntil 应注册在途 Promise');
// await bg; // 等待下载完成(兼容阻塞与后台两种路径)
// assert.strictEqual(getBackgroundDownload(), null, '下载完成后应清理在途状态');
// globals.useBangumiData = false;
// if (!hadCache && fs.existsSync(CACHE_FILE)) fs.writeFileSync(CACHE_FILE, '', 'utf-8');
// await initPromise.catch(() => {});
// });
// // 测试自定义文本类变量绕过 dotenv 注释截断(保留 # 等字符),对应 envs.js RAW_ENV_KEYS 修复
// import { Envs } from './configs/envs.js';
//
Expand Down
Loading