下载模组更新,新增下载组件,下载监听改为全局,全量改为增量监听
This commit is contained in:
@@ -85,6 +85,16 @@ backend/
|
||||
- `GET /api/download/history` - 获取下载历史
|
||||
- 参数: `offset`, `limit`
|
||||
|
||||
#### 任务管理API(优化版本)
|
||||
|
||||
- `GET /api/download/tasks` - 获取所有任务(完整数据)
|
||||
- `GET /api/download/tasks/active` - 获取活跃任务(下载中或暂停)
|
||||
- `GET /api/download/tasks/summary` - 获取任务摘要(快速状态检查)
|
||||
- `GET /api/download/tasks/changes` - 获取任务变更(增量更新)
|
||||
- 参数: `since` (时间戳,获取指定时间后的变更)
|
||||
- `GET /api/download/tasks/completed` - 获取已完成任务(分页)
|
||||
- 参数: `offset`, `limit`
|
||||
|
||||
### 代理相关
|
||||
|
||||
- `GET /api/proxy/image` - 图片代理服务
|
||||
|
||||
@@ -18,9 +18,14 @@ class PixivAuth {
|
||||
this.refreshToken = null;
|
||||
this.user = null;
|
||||
this.proxy = proxy;
|
||||
this.isRefreshing = false;
|
||||
this.failedQueue = [];
|
||||
|
||||
// 创建 axios 实例,支持代理
|
||||
this.axiosInstance = this.createAxiosInstance();
|
||||
|
||||
// 设置响应拦截器,自动处理token刷新
|
||||
this.setupResponseInterceptor();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,12 +53,102 @@ class PixivAuth {
|
||||
return axios.create(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置响应拦截器,自动处理token刷新
|
||||
*/
|
||||
setupResponseInterceptor() {
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// 如果是401错误且不是刷新token的请求,尝试自动刷新
|
||||
if (error.response?.status === 401 &&
|
||||
!originalRequest._retry &&
|
||||
!originalRequest.url.includes('/auth/token') &&
|
||||
this.refreshToken) {
|
||||
|
||||
if (this.isRefreshing) {
|
||||
// 如果正在刷新,将请求加入队列
|
||||
return new Promise((resolve, reject) => {
|
||||
this.failedQueue.push({ resolve, reject });
|
||||
}).then(() => {
|
||||
return this.axiosInstance(originalRequest);
|
||||
}).catch((err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
console.log('检测到token过期,正在自动刷新...');
|
||||
const result = await this.refreshAccessToken(this.refreshToken);
|
||||
|
||||
if (result.success) {
|
||||
// 更新token
|
||||
this.accessToken = result.access_token;
|
||||
this.refreshToken = result.refresh_token;
|
||||
if (result.user) {
|
||||
this.user = result.user;
|
||||
}
|
||||
|
||||
// 处理队列中的请求
|
||||
this.processQueue(null, result.access_token);
|
||||
|
||||
// 重试原始请求
|
||||
originalRequest.headers['Authorization'] = `Bearer ${result.access_token}`;
|
||||
return this.axiosInstance(originalRequest);
|
||||
} else {
|
||||
throw new Error('Token刷新失败');
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error('自动刷新token失败:', refreshError.message);
|
||||
this.processQueue(refreshError, null);
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理失败的请求队列
|
||||
*/
|
||||
processQueue(error, token = null) {
|
||||
this.failedQueue.forEach(({ resolve, reject }) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(token);
|
||||
}
|
||||
});
|
||||
this.failedQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置代理
|
||||
*/
|
||||
setProxy(proxy) {
|
||||
this.proxy = proxy;
|
||||
this.axiosInstance = this.createAxiosInstance();
|
||||
this.setupResponseInterceptor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步token状态(从外部配置更新)
|
||||
*/
|
||||
syncTokens(accessToken, refreshToken, user = null) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
if (user) {
|
||||
this.user = user;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+46
-1
@@ -41,6 +41,15 @@ class PixivBackend {
|
||||
// 创建认证实例,传入代理配置
|
||||
this.auth = new PixivAuth(this.config.proxy);
|
||||
|
||||
// 同步已保存的token状态
|
||||
if (this.config.access_token && this.config.refresh_token) {
|
||||
this.auth.syncTokens(
|
||||
this.config.access_token,
|
||||
this.config.refresh_token,
|
||||
this.config.user
|
||||
);
|
||||
}
|
||||
|
||||
// 创建下载服务实例
|
||||
this.downloadService = new DownloadService(this.auth);
|
||||
await this.downloadService.init();
|
||||
@@ -53,9 +62,26 @@ class PixivBackend {
|
||||
console.log('未检测到登录信息,需要先登录');
|
||||
}
|
||||
|
||||
// 启动token同步定时任务
|
||||
this.startTokenSyncTask();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动token同步定时任务
|
||||
*/
|
||||
startTokenSyncTask() {
|
||||
// 每5分钟同步一次token状态到配置文件
|
||||
setInterval(() => {
|
||||
if (this.auth && this.isLoggedIn) {
|
||||
this.syncTokensToConfig();
|
||||
}
|
||||
}, 5 * 60 * 1000); // 5分钟
|
||||
|
||||
console.log('Token同步定时任务已启动');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化配置文件
|
||||
*/
|
||||
@@ -173,6 +199,13 @@ class PixivBackend {
|
||||
this.config.user = result.user;
|
||||
}
|
||||
|
||||
// 同步到auth实例
|
||||
this.auth.syncTokens(
|
||||
result.access_token,
|
||||
result.refresh_token,
|
||||
result.user
|
||||
);
|
||||
|
||||
this.saveConfig();
|
||||
|
||||
this.isLoggedIn = true;
|
||||
@@ -260,12 +293,24 @@ class PixivBackend {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认证实例(用于后续API调用)
|
||||
* 获取认证实例
|
||||
*/
|
||||
getAuth() {
|
||||
return this.auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步token状态到配置
|
||||
*/
|
||||
syncTokensToConfig() {
|
||||
if (this.auth) {
|
||||
this.config.access_token = this.auth.accessToken;
|
||||
this.config.refresh_token = this.auth.refreshToken;
|
||||
this.config.user = this.auth.user;
|
||||
this.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载服务实例
|
||||
*/
|
||||
|
||||
@@ -121,4 +121,43 @@ router.post('/logout', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 手动刷新token
|
||||
* POST /api/auth/refresh-token
|
||||
*/
|
||||
router.post('/refresh-token', async (req, res) => {
|
||||
try {
|
||||
if (!req.backend.config.refresh_token) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '没有可用的刷新令牌'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await req.backend.relogin();
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Token刷新成功',
|
||||
data: {
|
||||
isLoggedIn: req.backend.isLoggedIn,
|
||||
username: req.backend.config.user?.account
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: result.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('手动刷新token失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -273,6 +273,92 @@ router.get('/tasks', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取活跃任务(下载中或暂停)
|
||||
* GET /api/download/tasks/active
|
||||
*/
|
||||
router.get('/tasks/active', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.getActiveTasks();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取任务摘要(用于快速状态检查)
|
||||
* GET /api/download/tasks/summary
|
||||
*/
|
||||
router.get('/tasks/summary', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.getTasksSummary();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取任务变更(增量更新)
|
||||
* GET /api/download/tasks/changes?since=timestamp
|
||||
*/
|
||||
router.get('/tasks/changes', async (req, res) => {
|
||||
try {
|
||||
const { since } = req.query;
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.getTasksChanges(since ? parseInt(since) : null);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取已完成任务(分页)
|
||||
* GET /api/download/tasks/completed?offset=0&limit=50
|
||||
*/
|
||||
router.get('/tasks/completed', async (req, res) => {
|
||||
try {
|
||||
const { offset = 0, limit = 50 } = req.query;
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.getCompletedTasks(parseInt(offset), parseInt(limit));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 暂停任务
|
||||
* POST /api/download/pause/:taskId
|
||||
@@ -548,6 +634,11 @@ router.get('/stream/:taskId', async (req, res) => {
|
||||
// 创建进度监听器
|
||||
const progressListener = (task) => {
|
||||
if (task.id === taskId) {
|
||||
// 使用setImmediate避免阻塞事件循环
|
||||
setImmediate(() => {
|
||||
try {
|
||||
// 检查连接是否仍然有效
|
||||
if (!res.destroyed) {
|
||||
res.write(`data: ${JSON.stringify({
|
||||
type: 'progress',
|
||||
data: task
|
||||
@@ -562,6 +653,13 @@ router.get('/stream/:taskId', async (req, res) => {
|
||||
res.end();
|
||||
downloadService.removeProgressListener(taskId, progressListener);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SSE写入失败:', error);
|
||||
// 连接可能已断开,清理监听器
|
||||
downloadService.removeProgressListener(taskId, progressListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+5
-2
@@ -42,8 +42,11 @@ function customLogger(req, res, next) {
|
||||
// 过滤掉图片代理请求
|
||||
const isImageProxy = req.path === '/api/proxy/image';
|
||||
|
||||
// 只记录API请求和重要请求,排除静态资源和图片代理
|
||||
if (!isStaticResource && !isImageProxy) {
|
||||
// 过滤掉下载任务状态查询请求
|
||||
const isDownloadTasksQuery = req.path === '/api/download/tasks';
|
||||
|
||||
// 只记录API请求和重要请求,排除静态资源、图片代理和下载任务查询
|
||||
if (!isStaticResource && !isImageProxy && !isDownloadTasksQuery) {
|
||||
const start = Date.now();
|
||||
|
||||
// 原始响应结束方法
|
||||
|
||||
@@ -370,8 +370,8 @@ class ArtistService {
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送API请求
|
||||
const response = await axios(config);
|
||||
// 使用auth实例的axiosInstance发送请求,这样可以利用自动token刷新机制
|
||||
const response = await this.auth.axiosInstance(config);
|
||||
const responseData = response.data;
|
||||
|
||||
// 对于GET请求,将响应数据缓存
|
||||
|
||||
@@ -407,8 +407,8 @@ class ArtworkService {
|
||||
}
|
||||
}
|
||||
|
||||
// 发送API请求
|
||||
const response = await axios(config);
|
||||
// 使用auth实例的axiosInstance发送请求,这样可以利用自动token刷新机制
|
||||
const response = await this.auth.axiosInstance(config);
|
||||
const responseData = response.data;
|
||||
|
||||
// 对于GET请求,将响应数据缓存
|
||||
|
||||
@@ -98,6 +98,106 @@ class DownloadService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃任务(下载中或暂停)
|
||||
*/
|
||||
async getActiveTasks() {
|
||||
return {
|
||||
success: true,
|
||||
data: this.taskManager.getActiveTasks(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务摘要(用于快速状态检查)
|
||||
*/
|
||||
async getTasksSummary() {
|
||||
const allTasks = this.taskManager.getAllTasks();
|
||||
const activeTasks = this.taskManager.getActiveTasks();
|
||||
|
||||
const summary = {
|
||||
total: allTasks.length,
|
||||
active: activeTasks.length,
|
||||
downloading: activeTasks.filter(t => t.status === 'downloading').length,
|
||||
paused: activeTasks.filter(t => t.status === 'paused').length,
|
||||
completed: allTasks.filter(t => t.status === 'completed').length,
|
||||
failed: allTasks.filter(t => t.status === 'failed').length,
|
||||
cancelled: allTasks.filter(t => t.status === 'cancelled').length,
|
||||
partial: allTasks.filter(t => t.status === 'partial').length,
|
||||
lastUpdate: Date.now()
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: summary,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务变更(增量更新)
|
||||
*/
|
||||
async getTasksChanges(since = null) {
|
||||
const allTasks = this.taskManager.getAllTasks();
|
||||
|
||||
if (!since) {
|
||||
// 如果没有since参数,返回所有活跃任务
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: this.taskManager.getActiveTasks(),
|
||||
lastUpdate: Date.now()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 过滤出自指定时间后有变更的任务
|
||||
const changedTasks = allTasks.filter(task => {
|
||||
const lastModified = Math.max(
|
||||
new Date(task.created_at).getTime(),
|
||||
task.updated_at ? new Date(task.updated_at).getTime() : 0,
|
||||
task.end_time ? new Date(task.end_time).getTime() : 0
|
||||
);
|
||||
return lastModified > since;
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: changedTasks,
|
||||
lastUpdate: Date.now()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已完成任务(分页)
|
||||
*/
|
||||
async getCompletedTasks(offset = 0, limit = 50) {
|
||||
const allTasks = this.taskManager.getAllTasks();
|
||||
const completedTasks = allTasks.filter(task =>
|
||||
['completed', 'failed', 'cancelled', 'partial'].includes(task.status)
|
||||
);
|
||||
|
||||
// 按完成时间倒序排列
|
||||
completedTasks.sort((a, b) => {
|
||||
const timeA = a.end_time ? new Date(a.end_time).getTime() : 0;
|
||||
const timeB = b.end_time ? new Date(b.end_time).getTime() : 0;
|
||||
return timeB - timeA;
|
||||
});
|
||||
|
||||
const paginatedTasks = completedTasks.slice(offset, offset + limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: paginatedTasks,
|
||||
total: completedTasks.length,
|
||||
offset,
|
||||
limit
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async cancelTask(taskId) {
|
||||
const task = this.taskManager.getTask(taskId);
|
||||
if (!task) {
|
||||
|
||||
@@ -5,6 +5,10 @@ class ProgressManager {
|
||||
constructor() {
|
||||
// 进度监听器: taskId -> listeners[]
|
||||
this.progressListeners = new Map();
|
||||
// 节流控制: taskId -> { lastUpdate, pending }
|
||||
this.throttleControl = new Map();
|
||||
// 节流间隔(毫秒)
|
||||
this.throttleInterval = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,14 +33,56 @@ class ProgressManager {
|
||||
}
|
||||
if (listeners.length === 0) {
|
||||
this.progressListeners.delete(taskId);
|
||||
// 清理节流控制
|
||||
this.throttleControl.delete(taskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知进度更新
|
||||
* 通知进度更新(带节流)
|
||||
*/
|
||||
notifyProgressUpdate(taskId, task) {
|
||||
if (!this.progressListeners.has(taskId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const throttleInfo = this.throttleControl.get(taskId);
|
||||
|
||||
// 如果是重要状态变更(完成、失败、取消),立即通知
|
||||
const isImportantStatus = ['completed', 'failed', 'cancelled', 'partial', 'paused'].includes(task.status);
|
||||
|
||||
if (isImportantStatus) {
|
||||
// 立即通知重要状态变更
|
||||
this._executeListeners(taskId, task);
|
||||
// 清理节流控制
|
||||
this.throttleControl.delete(taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于普通进度更新,使用节流
|
||||
if (!throttleInfo || (now - throttleInfo.lastUpdate) >= this.throttleInterval) {
|
||||
// 立即通知
|
||||
this._executeListeners(taskId, task);
|
||||
this.throttleControl.set(taskId, { lastUpdate: now, pending: false });
|
||||
} else if (!throttleInfo.pending) {
|
||||
// 延迟通知
|
||||
throttleInfo.pending = true;
|
||||
setTimeout(() => {
|
||||
const currentThrottleInfo = this.throttleControl.get(taskId);
|
||||
if (currentThrottleInfo && currentThrottleInfo.pending) {
|
||||
this._executeListeners(taskId, task);
|
||||
this.throttleControl.delete(taskId);
|
||||
}
|
||||
}, this.throttleInterval - (now - throttleInfo.lastUpdate));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行监听器(内部方法)
|
||||
*/
|
||||
_executeListeners(taskId, task) {
|
||||
if (this.progressListeners.has(taskId)) {
|
||||
const listeners = this.progressListeners.get(taskId);
|
||||
listeners.forEach(listener => {
|
||||
|
||||
Reference in New Issue
Block a user