diff --git a/app/templates/index.html b/app/templates/index.html
index e4791be..cf0ab26 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -3237,6 +3237,15 @@
// 任务列表自动检测更新相关
tasklistAutoWatchTimer: null,
tasklistLatestFilesSignature: '',
+ // 全局 SSE 单例与监听标志
+ appSSE: null,
+ appSSEInitialized: false,
+ calendarSSEListenerAdded: false,
+ tasklistSSEListenerAdded: false,
+ // 存放已绑定的事件处理器,便于避免重复绑定
+ onCalendarChangedHandler: null,
+ onTasklistChangedHandler: null,
+ // 兼容旧字段(不再使用独立 SSE 实例)
tasklistSSE: null,
calendarSSE: null,
// 创建任务相关数据
@@ -3576,6 +3585,8 @@
activeTab(newValue, oldValue) {
// 如果切换到任务列表页面,则刷新任务最新信息和元数据
if (newValue === 'tasklist') {
+ // 确保全局 SSE 已建立
+ try { this.ensureGlobalSSE(); } catch (e) {}
this.loadTaskLatestInfo();
this.loadTasklistMetadata();
// 启动任务列表的后台监听
@@ -3586,6 +3597,8 @@
}
// 切换到追剧日历:立刻检查一次并启动后台监听;离开则停止监听
if (newValue === 'calendar') {
+ // 确保全局 SSE 已建立
+ try { this.ensureGlobalSSE(); } catch (e) {}
// 立即检查一次(若已初始化过监听,直接调用tick引用)
// 先本地读取一次,立刻应用“已转存”状态(不依赖轮询)
try { this.loadCalendarEpisodesLocal && this.loadCalendarEpisodesLocal(); } catch (e) {}
@@ -3633,6 +3646,9 @@
this.fetchUserInfo(); // 获取用户信息
this.fetchAccountsDetail(); // 获取账号详细信息
+ // 应用级别:在挂载时确保全局 SSE 建立一次
+ try { this.ensureGlobalSSE(); } catch (e) {}
+
// 迁移旧的localStorage数据到新格式(为每个账号单独存储目录)
this.migrateFileManagerFolderData();
@@ -3822,6 +3838,80 @@
this.stopCalendarAutoWatch();
},
methods: {
+ // 启动任务列表轮询兜底(仅在未运行时启动)
+ startTasklistPollingFallback() {
+ try {
+ if (!this.tasklistAutoWatchTimer) {
+ this.tasklistAutoWatchTimer = setInterval(async () => {
+ try {
+ const res = await axios.get('/task_latest_info');
+ if (res.data && res.data.success) {
+ const latestFiles = res.data.data.latest_files || {};
+ const sig = this.calcLatestFilesSignature(latestFiles);
+ if (sig && sig !== this.tasklistLatestFilesSignature) {
+ // 更新签名,触发热更新
+ this.tasklistLatestFilesSignature = sig;
+ this.taskLatestFiles = latestFiles;
+ // 重新加载任务元数据,确保海报和元数据能热更新
+ await this.loadTasklistMetadata();
+ }
+ }
+ } catch (e) {
+ console.warn('任务列表后台监听检查失败:', e);
+ }
+ }, 60000);
+ }
+ } catch (e) {}
+ },
+ // 确保全局 SSE 单例存在(仅建立一次)
+ ensureGlobalSSE() {
+ try {
+ if (this.appSSEInitialized && this.appSSE) return;
+ if (!this.appSSE) {
+ this.appSSE = new EventSource('/api/calendar/stream');
+ }
+ this.appSSEInitialized = true;
+ // 统一 onopen:SSE 成功后停止两侧轮询
+ this.appSSE.onopen = () => {
+ try {
+ if (this.calendarAutoWatchTimer) {
+ clearInterval(this.calendarAutoWatchTimer);
+ this.calendarAutoWatchTimer = null;
+ }
+ } catch (e) {}
+ try {
+ if (this.tasklistAutoWatchTimer) {
+ clearInterval(this.tasklistAutoWatchTimer);
+ this.tasklistAutoWatchTimer = null;
+ }
+ } catch (e) {}
+ };
+ // 统一 onerror:关闭SSE并回退双侧轮询
+ this.appSSE.onerror = () => {
+ try { this.appSSE.close(); } catch (e) {}
+ this.appSSE = null;
+ this.appSSEInitialized = false;
+ // 日历回退:若没有轮询定时器,则恢复轮询并立即执行一次
+ try {
+ if (!this.calendarAutoWatchTimer && this.calendarAutoWatchTickRef) {
+ const baseIntervalMs = this.calendar && this.calendar.manageMode ? 5 * 1000 : 60 * 1000;
+ this.calendarAutoWatchTimer = setInterval(this.calendarAutoWatchTickRef, baseIntervalMs);
+ this.calendarAutoWatchTickRef();
+ }
+ } catch (e) {}
+ // 任务列表回退:若没有轮询定时器,则恢复轮询
+ try {
+ if (!this.tasklistAutoWatchTimer) {
+ this.startTasklistPollingFallback();
+ }
+ } catch (e) {}
+ };
+ // ping 心跳占位
+ try { this.appSSE.addEventListener('ping', () => {}); } catch (e) {}
+ } catch (e) {
+ // 忽略失败,后续可回退轮询
+ }
+ },
// 任务列表海报标题(悬停:#编号 任务名称 · 状态)
getTasklistPosterTitle(task, index) {
try {
@@ -5612,7 +5702,7 @@
this.calendarAutoWatchTimer = null;
}
} else {
- if (!this.calendarSSE && !this.calendarAutoWatchTimer && this.calendarAutoWatchTickRef) {
+ if (!this.appSSE && !this.calendarAutoWatchTimer && this.calendarAutoWatchTickRef) {
const baseIntervalMs = this.calendar && this.calendar.manageMode ? 5 * 1000 : 60 * 1000;
this.calendarAutoWatchTimer = setInterval(this.calendarAutoWatchTickRef, baseIntervalMs);
this.calendarAutoWatchTickRef();
@@ -5624,19 +5714,10 @@
window.addEventListener('focus', this.calendarAutoWatchFocusHandler);
document.addEventListener('visibilitychange', this.calendarAutoWatchVisibilityHandler);
- // 建立 SSE 连接,实时感知日历数据库变化(成功建立后停用轮询,失败时回退轮询)
+ // 建立/复用全局 SSE 连接(成功建立后停用轮询,失败时回退轮询)
try {
- if (!this.calendarSSE) {
- this.calendarSSE = new EventSource('/api/calendar/stream');
- // SSE 打开后,停止轮询
- this.calendarSSE.onopen = () => {
- try {
- if (this.calendarAutoWatchTimer) {
- clearInterval(this.calendarAutoWatchTimer);
- this.calendarAutoWatchTimer = null;
- }
- } catch (e) {}
- };
+ this.ensureGlobalSSE();
+ if (this.appSSE && !this.calendarSSEListenerAdded) {
const onChanged = async (ev) => {
try {
// 解析变更原因(后端通过 SSE data 传递)
@@ -5717,21 +5798,10 @@
try { await this.loadTodayUpdatesLocal(); } catch (e) {}
} catch (e) {}
};
- this.calendarSSE.addEventListener('calendar_changed', onChanged);
- // 初次连接会收到一次 ping,不做处理即可
- this.calendarSSE.addEventListener('ping', () => {});
- this.calendarSSE.onerror = () => {
- try { this.calendarSSE.close(); } catch (e) {}
- this.calendarSSE = null;
- // 回退:若没有轮询定时器,则恢复轮询
- try {
- if (!this.calendarAutoWatchTimer && this.calendarAutoWatchTickRef) {
- const baseIntervalMs = this.calendar && this.calendar.manageMode ? 5 * 1000 : 60 * 1000;
- this.calendarAutoWatchTimer = setInterval(this.calendarAutoWatchTickRef, baseIntervalMs);
- this.calendarAutoWatchTickRef();
- }
- } catch (e) {}
- };
+ this.onCalendarChangedHandler = onChanged;
+ this.appSSE.addEventListener('calendar_changed', onChanged);
+ this.calendarSSEListenerAdded = true;
+ // onopen/onerror 已集中在 ensureGlobalSSE,无需重复设置
}
} catch (e) {
// 忽略 SSE 失败,继续使用轮询
@@ -5756,10 +5826,7 @@
document.removeEventListener('visibilitychange', this.calendarAutoWatchVisibilityHandler);
this.calendarAutoWatchVisibilityHandler = null;
}
- if (this.calendarSSE) {
- try { this.calendarSSE.close(); } catch (e) {}
- this.calendarSSE = null;
- }
+ // 不再关闭全局 SSE;仅移除本地监听(如有需要)
} catch (e) {
// ignore
}
@@ -9351,17 +9418,9 @@
// 建立 SSE 连接,实时感知任务列表变化(成功建立后停用轮询,失败时回退轮询)
try {
- if (!this.tasklistSSE) {
- this.tasklistSSE = new EventSource('/api/calendar/stream');
- // SSE 打开后,停止轮询
- this.tasklistSSE.onopen = () => {
- try {
- if (this.tasklistAutoWatchTimer) {
- clearInterval(this.tasklistAutoWatchTimer);
- this.tasklistAutoWatchTimer = null;
- }
- } catch (e) {}
- };
+ // 使用全局 SSE 单例
+ this.ensureGlobalSSE();
+ if (this.appSSE && !this.tasklistSSEListenerAdded) {
const onTasklistChanged = async (ev) => {
try {
// 解析变更原因(后端通过 SSE data 传递)
@@ -9406,37 +9465,10 @@
}
} catch (e) {}
};
- this.tasklistSSE.addEventListener('calendar_changed', onTasklistChanged);
- // 初次连接会收到一次 ping,不做处理即可
- this.tasklistSSE.addEventListener('ping', () => {});
- this.tasklistSSE.onerror = () => {
- try { this.tasklistSSE.close(); } catch (e) {}
- this.tasklistSSE = null;
- // 回退:若没有轮询定时器,则恢复轮询
- try {
- if (!this.tasklistAutoWatchTimer) {
- this.tasklistAutoWatchTimer = setInterval(async () => {
- try {
- const res = await axios.get('/task_latest_info');
- if (res.data && res.data.success) {
- const latestFiles = res.data.data.latest_files || {};
- const sig = this.calcLatestFilesSignature(latestFiles);
- if (sig && sig !== this.tasklistLatestFilesSignature) {
- // 更新签名,触发热更新
- this.tasklistLatestFilesSignature = sig;
- this.taskLatestFiles = latestFiles;
-
- // 重新加载任务元数据,确保海报和元数据能热更新
- await this.loadTasklistMetadata();
- }
- }
- } catch (e) {
- console.warn('任务列表后台监听检查失败:', e);
- }
- }, 60000);
- }
- } catch (e) {}
- };
+ this.onTasklistChangedHandler = onTasklistChanged;
+ this.appSSE.addEventListener('calendar_changed', onTasklistChanged);
+ this.tasklistSSEListenerAdded = true;
+ // onopen/onerror 已集中在 ensureGlobalSSE,无需重复设置
}
} catch (e) {
// 忽略 SSE 失败,继续使用轮询