Files
community/Ink Canvas/Helpers/DlassNoteUploader.cs
T

494 lines
20 KiB
C#
Raw Normal View History

2025-11-02 10:11:15 +08:00
using Ink_Canvas.Helpers;
using Newtonsoft.Json;
using System;
2025-11-02 10:30:36 +08:00
using System.Collections.Concurrent;
2025-11-02 10:11:15 +08:00
using System.Collections.Generic;
using System.IO;
using System.Linq;
2025-11-02 10:30:36 +08:00
using System.Threading;
2025-11-02 10:11:15 +08:00
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass笔记自动上传辅助类
/// </summary>
public class DlassNoteUploader
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
2025-11-02 10:30:36 +08:00
private const int BATCH_SIZE = 10; // 批量上传大小
2025-11-02 10:46:16 +08:00
private const int MAX_RETRY_COUNT = 3; // 最大重试次数
2025-11-02 10:30:36 +08:00
/// <summary>
2025-11-02 10:46:16 +08:00
/// 上传队列项
2025-11-02 10:30:36 +08:00
/// </summary>
2025-11-02 10:46:16 +08:00
private class UploadQueueItem
{
public string FilePath { get; set; }
public int RetryCount { get; set; }
}
/// <summary>
/// 上传队列
/// </summary>
private static readonly ConcurrentQueue<UploadQueueItem> _uploadQueue = new ConcurrentQueue<UploadQueueItem>();
2025-11-02 10:30:36 +08:00
/// <summary>
/// 队列处理锁,防止并发处理
/// </summary>
private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
2025-11-02 10:11:15 +08:00
/// <summary>
/// 上传笔记响应模型
/// </summary>
public class UploadNoteResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("note_id")]
public int? NoteId { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("file_url")]
public string FileUrl { get; set; }
}
/// <summary>
/// 白板信息模型(用于查找白板)
/// </summary>
private class WhiteboardInfo
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("board_id")]
public string BoardId { get; set; }
[JsonProperty("secret_key")]
public string SecretKey { get; set; }
[JsonProperty("class_name")]
public string ClassName { get; set; }
[JsonProperty("class_id")]
public int ClassId { get; set; }
}
/// <summary>
/// 认证响应模型
/// </summary>
private class AuthWithTokenResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("whiteboards")]
public List<WhiteboardInfo> Whiteboards { get; set; }
}
2025-11-02 10:16:31 +08:00
/// <summary>
/// 异步上传笔记文件到Dlass(支持PNG和ICSTK格式)
/// </summary>
/// <param name="filePath">文件路径(支持PNG和ICSTK</param>
2025-11-02 10:30:36 +08:00
/// <returns>是否成功加入队列(不等待实际上传完成)</returns>
2025-11-02 10:16:31 +08:00
public static async Task<bool> UploadNoteFileAsync(string filePath)
2025-11-02 10:11:15 +08:00
{
try
{
// 检查是否启用自动上传
if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true)
{
return false;
}
2025-11-02 10:30:36 +08:00
// 基本验证
if (!File.Exists(filePath))
2025-11-02 10:11:15 +08:00
{
2025-11-02 10:30:36 +08:00
LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
2025-11-02 10:11:15 +08:00
return false;
}
2025-11-02 10:30:36 +08:00
var fileExtension = Path.GetExtension(filePath).ToLower();
2025-11-02 10:16:31 +08:00
if (fileExtension != ".png" && fileExtension != ".icstk")
{
return false;
}
2025-11-02 10:30:36 +08:00
var fileInfo = new FileInfo(filePath);
2025-11-02 10:11:15 +08:00
if (fileInfo.Length > 10 * 1024 * 1024)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过10MB限制", LogHelper.LogType.Error);
return false;
}
2025-11-02 10:30:36 +08:00
// 获取上传延迟时间(分钟)
var delayMinutes = MainWindow.Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
// 如果设置了延迟时间,在后台任务中等待后再加入队列
if (delayMinutes > 0)
2025-11-02 10:11:15 +08:00
{
2025-11-02 10:30:36 +08:00
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
EnqueueFile(filePath);
});
2025-11-02 10:11:15 +08:00
}
2025-11-02 10:30:36 +08:00
else
{
EnqueueFile(filePath);
}
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
2025-11-02 10:11:15 +08:00
2025-11-02 10:30:36 +08:00
/// <summary>
/// 将文件加入上传队列
/// </summary>
2025-11-02 10:46:16 +08:00
private static void EnqueueFile(string filePath, int retryCount = 0)
2025-11-02 10:30:36 +08:00
{
2025-11-02 10:46:16 +08:00
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = filePath,
RetryCount = retryCount
});
2025-11-02 10:30:36 +08:00
// 如果队列达到批量大小,触发批量上传
if (_uploadQueue.Count >= BATCH_SIZE)
{
_ = ProcessUploadQueueAsync();
}
}
/// <summary>
/// 处理上传队列,批量上传文件
/// </summary>
private static async Task ProcessUploadQueueAsync()
{
// 使用信号量防止并发处理
if (!await _queueProcessingLock.WaitAsync(0))
{
return; // 已有处理任务在运行
}
try
{
2025-11-02 10:46:16 +08:00
var filesToUpload = new List<UploadQueueItem>();
2025-11-02 10:30:36 +08:00
// 从队列中取出最多BATCH_SIZE个文件
2025-11-02 10:46:16 +08:00
while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
2025-11-02 10:11:15 +08:00
{
2025-11-02 10:46:16 +08:00
// 再次检查文件是否存在
if (File.Exists(item.FilePath))
2025-11-02 10:30:36 +08:00
{
2025-11-02 10:46:16 +08:00
filesToUpload.Add(item);
2025-11-02 10:30:36 +08:00
}
2025-11-02 10:11:15 +08:00
}
2025-11-02 10:30:36 +08:00
if (filesToUpload.Count == 0)
{
return;
}
2025-11-02 10:11:15 +08:00
2025-11-02 10:30:36 +08:00
// 获取共享的白板信息(同一批次的所有文件共享认证信息)
WhiteboardInfo sharedWhiteboard = null;
string apiBaseUrl = null;
string userToken = null;
2025-11-02 10:11:15 +08:00
try
{
2025-11-02 10:30:36 +08:00
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
return;
}
2025-11-02 10:11:15 +08:00
2025-11-02 10:30:36 +08:00
userToken = MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
2025-11-02 10:11:15 +08:00
{
2025-11-02 10:30:36 +08:00
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
return;
}
2025-11-02 10:11:15 +08:00
2025-11-02 10:30:36 +08:00
apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
2025-11-02 10:11:15 +08:00
2025-11-02 10:30:36 +08:00
// 获取白板信息(只获取一次,所有文件共享)
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
2025-11-02 10:11:15 +08:00
{
2025-11-02 10:30:36 +08:00
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
return;
}
sharedWhiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (sharedWhiteboard == null || string.IsNullOrEmpty(sharedWhiteboard.BoardId) || string.IsNullOrEmpty(sharedWhiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
return;
}
2025-11-02 10:11:15 +08:00
}
2025-11-02 10:30:36 +08:00
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error);
return;
}
2025-11-02 10:46:16 +08:00
// 并发上传所有文件(共享白板信息),并处理失败重试
var uploadTasks = filesToUpload.Select(async item =>
{
try
{
var success = await UploadFileInternalAsync(item.FilePath, sharedWhiteboard, apiBaseUrl, userToken);
if (!success)
{
// 检查是否是可重试的错误
if (IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1);
}
else
{
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
}
return success;
}
catch (Exception ex)
{
// 检查是否是可重试的错误(超时、网络错误等)
var errorMessage = ex.Message.ToLower();
bool isRetryable = errorMessage.Contains("超时") ||
errorMessage.Contains("timeout") ||
errorMessage.Contains("网络错误") ||
errorMessage.Contains("network");
if (isRetryable && IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1);
}
else
{
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
return false;
}
});
2025-11-02 10:30:36 +08:00
await Task.WhenAll(uploadTasks);
2025-11-02 10:46:16 +08:00
// 如果队列达到批量大小,继续处理
2025-11-02 10:30:36 +08:00
if (_uploadQueue.Count >= BATCH_SIZE)
{
_ = ProcessUploadQueueAsync();
}
}
finally
{
_queueProcessingLock.Release();
}
}
/// <summary>
/// 内部上传方法,执行实际上传操作
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="whiteboard">白板信息(如果为null则重新获取)</param>
/// <param name="apiBaseUrl">API基础URL(如果为null则从设置获取)</param>
/// <param name="userToken">用户Token(如果为null则从设置获取)</param>
private static async Task<bool> UploadFileInternalAsync(string filePath, WhiteboardInfo whiteboard = null, string apiBaseUrl = null, string userToken = null)
{
try
{
// 再次检查文件是否存在(可能在队列等待时被删除)
if (!File.Exists(filePath))
{
return false;
}
// 检查文件扩展名
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk")
{
return false;
}
// 检查文件大小(最大10MB
var fileInfo = new FileInfo(filePath);
if (fileInfo.Length > 10 * 1024 * 1024)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过10MB限制", LogHelper.LogType.Error);
return false;
}
2025-11-02 10:11:15 +08:00
2025-11-02 10:30:36 +08:00
// 如果白板信息未提供,则重新获取
if (whiteboard == null)
{
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
return false;
}
2025-11-02 10:11:15 +08:00
2025-11-02 10:30:36 +08:00
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
2025-11-02 10:11:15 +08:00
{
2025-11-02 10:30:36 +08:00
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
2025-11-02 10:11:15 +08:00
return false;
}
2025-11-02 10:30:36 +08:00
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
// 创建API客户端并获取白板信息
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
return false;
}
// 查找匹配班级的白板
whiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
return false;
}
}
}
// 获取API基础URL和用户Token(如果未提供)
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
// 准备上传参数
var fileName = Path.GetFileNameWithoutExtension(filePath);
var title = fileName;
var fileType = fileExtension == ".icstk" ? "墨迹文件" : "笔记";
var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
var tags = fileExtension == ".icstk" ? "自动上传,墨迹,icstk" : "自动上传,笔记,png";
2025-11-02 10:11:15 +08:00
2025-11-02 10:30:36 +08:00
// 创建API客户端并上传文件
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
2025-11-02 10:11:15 +08:00
var uploadResult = await apiClient.UploadNoteAsync<UploadNoteResponse>(
"/api/whiteboard/upload_note",
2025-11-02 10:30:36 +08:00
filePath,
2025-11-02 10:11:15 +08:00
whiteboard.BoardId,
whiteboard.SecretKey,
title,
description,
tags);
if (uploadResult != null && uploadResult.Success)
{
LogHelper.WriteLogToFile($"笔记上传成功:{fileName} -> {uploadResult.FileUrl}", LogHelper.LogType.Event);
return true;
}
else
{
LogHelper.WriteLogToFile($"上传失败:服务器响应失败 - {uploadResult?.Message ?? ""}", LogHelper.LogType.Error);
return false;
}
}
}
catch (Exception ex)
{
2025-11-02 10:46:16 +08:00
// 记录错误信息,抛出异常以便调用方判断是否可重试
2025-11-02 10:11:15 +08:00
LogHelper.WriteLogToFile($"上传笔记时出错: {ex.Message}", LogHelper.LogType.Error);
2025-11-02 10:46:16 +08:00
throw;
2025-11-02 10:11:15 +08:00
}
}
2025-11-02 10:46:16 +08:00
/// <summary>
/// 判断错误是否可重试(超时、网络错误等)
/// </summary>
private static bool IsRetryableError(string filePath)
{
// 检查文件是否存在
if (!File.Exists(filePath))
{
return false; // 文件不存在,不可重试
}
// 检查文件扩展名
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk")
{
return false; // 文件格式错误,不可重试
}
// 检查文件大小
try
{
var fileInfo = new FileInfo(filePath);
if (fileInfo.Length > 10 * 1024 * 1024)
{
return false; // 文件过大,不可重试
}
}
catch
{
return false; // 无法读取文件信息,不可重试
}
// 其他错误(超时、网络错误等)可以重试
return true;
}
2025-11-02 10:11:15 +08:00
}
}