From 0ad74d9f7fb268e1a106cc73e762ecfe42e00a65 Mon Sep 17 00:00:00 2001
From: doudou0720 <98651603+doudou0720@users.noreply.github.com>
Date: Tue, 24 Feb 2026 14:08:57 +0800
Subject: [PATCH] =?UTF-8?q?feat(Upload/WebDav):=E8=BF=81=E7=A7=BBDlass?=
=?UTF-8?q?=E5=B9=B6=E6=B7=BB=E5=8A=A0WebDav=E7=AE=A1=E7=90=86=20(#381)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(Upload/Common): 重构上传功能以添加通用设置管理
- 新增UploadSettings类用于管理上传通用设置
- 重构上传逻辑,将延迟上传功能移至UploadHelper
- 在Dlass设置窗口添加通用设置标签页
- 支持多上传提供者管理及取消操作
- 增强文件上传前的验证和错误处理
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* feat(upload): 添加WebDav文件上传支持
- 新增WebDavUploader工具类实现文件上传功能
- 添加WebDavUploadProvider作为上传提供者
- 在设置界面增加WebDav配置选项
- 添加WebDav.Client NuGet包依赖
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* feat(WebDAV): 实现WebDAV上传队列管理
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* feat(Upload): 重命名Dlass设置项为云存储以支持WebDav保存
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* feat(Dlass):迁移Dlass注册位置
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* refactor(Upload): 优化上传逻辑和界面交互
- 修改Dlass标签页检测逻辑,使用Tag属性替代Header
- 限制WebDav上传队列的批量处理大小
- 移除多处上传延迟逻辑,统一在通用设置中配置
- 更新Dlass设置界面提示文本
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* chore:修改窗口命名
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* refactor(upload): 重构上传队列为统一管理架构
重构上传队列系统,引入BaseUploadQueue基类实现通用队列管理逻辑,创建UploadQueueHelper统一管理所有上传队列。将DlassUploadQueue和WebDavUploadQueue重构为继承自BaseUploadQueue的具体实现,简化代码并提高可维护性。修改MainWindow初始化代码以使用新的统一初始化方法。
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* refactor(Upload): 重构上传队列系统,改进错误处理和资源管理
- 将上传队列改为可释放资源,实现IDisposable接口
- 移除硬编码的文件验证逻辑,改为可重写方法
- 改进API客户端,支持取消操作和更好的资源管理
- 优化队列初始化流程,增加错误处理
- 统一上传提供者的队列注册方式
- 改进日志记录和错误信息
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* Update Settings.cs
* refactor(UpLoad/Queue): 移除冗余的上传成功/失败日志记录
优化WebDav上传逻辑,增加目录创建重试机制
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
* refactor(MW_Settings): 重构全选复选框状态更新逻辑
将直接设置全选复选框状态的逻辑拆分为两步,先计算所有分类复选框状态,再更新全选复选框,提高代码可读性
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
---------
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
Co-authored-by: CJK_mkp <113243675+CJKmkp@users.noreply.github.com>
---
Ink Canvas/Helpers/BaseUploadQueue.cs | 604 +++++++++
Ink Canvas/Helpers/DlassApiClient.cs | 107 +-
Ink Canvas/Helpers/DlassNoteUploader.cs | 807 -----------
Ink Canvas/Helpers/DlassUploadQueue.cs | 257 ++++
Ink Canvas/Helpers/UploadHelper.cs | 93 +-
Ink Canvas/Helpers/UploadQueueHelper.cs | 94 ++
Ink Canvas/Helpers/WebDavUploadQueue.cs | 61 +
Ink Canvas/Helpers/WebDavUploader.cs | 171 +++
Ink Canvas/InkCanvasForClass.csproj | 1 +
Ink Canvas/MainWindow.xaml | 2 +-
Ink Canvas/MainWindow.xaml.cs | 23 +-
.../MainWindow_cs/MW_Save&OpenStrokes.cs | 377 ++++--
Ink Canvas/MainWindow_cs/MW_Settings.cs | 46 +-
Ink Canvas/Resources/Settings.cs | 40 +-
Ink Canvas/Windows/DlassSettingsWindow.xaml | 1200 +++++++++++------
.../Windows/DlassSettingsWindow.xaml.cs | 281 +++-
.../SettingsViews/AdvancedPanel.xaml | 6 +-
Ink Canvas/packages.lock.json | 6 +
18 files changed, 2729 insertions(+), 1447 deletions(-)
create mode 100644 Ink Canvas/Helpers/BaseUploadQueue.cs
delete mode 100644 Ink Canvas/Helpers/DlassNoteUploader.cs
create mode 100644 Ink Canvas/Helpers/DlassUploadQueue.cs
create mode 100644 Ink Canvas/Helpers/UploadQueueHelper.cs
create mode 100644 Ink Canvas/Helpers/WebDavUploadQueue.cs
create mode 100644 Ink Canvas/Helpers/WebDavUploader.cs
diff --git a/Ink Canvas/Helpers/BaseUploadQueue.cs b/Ink Canvas/Helpers/BaseUploadQueue.cs
new file mode 100644
index 00000000..27d306c9
--- /dev/null
+++ b/Ink Canvas/Helpers/BaseUploadQueue.cs
@@ -0,0 +1,604 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ink_Canvas.Helpers
+{
+ ///
+ /// 上传队列项数据(用于序列化)
+ ///
+ public class UploadQueueItemData
+ {
+ [JsonProperty("file_path")]
+ public string FilePath { get; set; }
+
+ [JsonProperty("retry_count")]
+ public int RetryCount { get; set; }
+
+ [JsonProperty("added_time")]
+ public DateTime AddedTime { get; set; }
+ }
+
+ ///
+ /// 上传队列项
+ ///
+ public class UploadQueueItem
+ {
+ public string FilePath { get; set; }
+ public int RetryCount { get; set; }
+ }
+
+ ///
+ /// 通用上传队列基类
+ ///
+ public abstract class BaseUploadQueue : IDisposable
+ {
+ protected const int BATCH_SIZE = 10; // 批量上传大小
+ protected const int MAX_RETRY_COUNT = 3; // 最大重试次数
+
+ ///
+ /// 上传队列
+ ///
+ protected readonly ConcurrentQueue _uploadQueue = new ConcurrentQueue();
+
+ ///
+ /// 队列处理锁,防止并发处理
+ ///
+ protected readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
+
+ ///
+ /// 队列保存锁,防止并发保存
+ ///
+ protected readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
+
+ ///
+ /// 是否已初始化队列
+ ///
+ protected bool _isQueueInitialized = false;
+
+ ///
+ /// 是否已释放资源
+ ///
+ private bool _disposed = false;
+
+ ///
+ /// 队列文件名
+ ///
+ protected abstract string QueueFileName { get; }
+
+ ///
+ /// 允许的文件扩展名
+ ///
+ protected virtual HashSet AllowedExtensions => new HashSet { ".png", ".icstk", ".xml", ".zip" };
+
+ ///
+ /// 获取队列文件路径
+ ///
+ protected string GetQueueFilePath()
+ {
+ var configsDir = Path.Combine(App.RootPath, "Configs");
+ if (!Directory.Exists(configsDir))
+ {
+ Directory.CreateDirectory(configsDir);
+ }
+ return Path.Combine(configsDir, QueueFileName);
+ }
+
+ ///
+ /// 获取最大文件大小
+ ///
+ /// 文件扩展名
+ /// 最大文件大小(字节)
+ protected virtual long GetMaxFileSize(string extension)
+ {
+ return extension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
+ }
+
+ ///
+ /// 初始化上传队列
+ ///
+ public void InitializeQueue()
+ {
+ if (_isQueueInitialized)
+ {
+ return;
+ }
+
+ try
+ {
+ var queueFilePath = GetQueueFilePath();
+ if (!File.Exists(queueFilePath))
+ {
+ _isQueueInitialized = true;
+ return;
+ }
+
+ var jsonContent = File.ReadAllText(queueFilePath);
+ if (string.IsNullOrWhiteSpace(jsonContent))
+ {
+ _isQueueInitialized = true;
+ return;
+ }
+
+ var queueData = JsonConvert.DeserializeObject>(jsonContent);
+ if (queueData == null || queueData.Count == 0)
+ {
+ _isQueueInitialized = true;
+ return;
+ }
+
+ int restoredCount = 0;
+ int skippedCount = 0;
+
+ foreach (var item in queueData)
+ {
+ // 验证文件是否存在
+ if (!File.Exists(item.FilePath))
+ {
+ skippedCount++;
+ continue;
+ }
+
+ // 验证文件格式和大小
+ if (!IsValidFile(item.FilePath))
+ {
+ skippedCount++;
+ continue;
+ }
+
+ // 恢复队列项
+ _uploadQueue.Enqueue(new UploadQueueItem
+ {
+ FilePath = item.FilePath,
+ RetryCount = item.RetryCount
+ });
+ restoredCount++;
+ }
+
+ _isQueueInitialized = true;
+
+ if (restoredCount > 0)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
+ // 如果恢复了队列,触发处理
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await ProcessUploadQueueAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 恢复上传队列后处理时出错: {ex}", LogHelper.LogType.Error);
+ }
+ });
+ }
+ else if (skippedCount > 0)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
+ _isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试
+ }
+ }
+
+ ///
+ /// 保存队列到文件
+ ///
+ protected async Task SaveQueueToFileAsync(CancellationToken cancellationToken = default)
+ {
+ if (!await _queueSaveLock.WaitAsync(1000, cancellationToken)) // 最多等待1秒
+ {
+ return; // 如果无法获取锁,跳过保存(避免阻塞)
+ }
+
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var queueData = new List();
+
+ // 将队列转换为可序列化的格式
+ foreach (var item in _uploadQueue)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ queueData.Add(new UploadQueueItemData
+ {
+ FilePath = item.FilePath,
+ RetryCount = item.RetryCount,
+ AddedTime = DateTime.Now
+ });
+ }
+
+ var queueFilePath = GetQueueFilePath();
+
+ // 如果队列为空,清空文件
+ if (queueData.Count == 0)
+ {
+ ClearQueueFile();
+ return;
+ }
+
+ var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented);
+
+ // 使用进程保护的写入门控,避免安全面板中"进程文件保护"占用导致无法写入
+ var tempFilePath = queueFilePath + ".tmp";
+ ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
+ {
+ File.WriteAllText(tempFilePath, jsonContent);
+ if (File.Exists(queueFilePath))
+ File.Delete(queueFilePath);
+ File.Move(tempFilePath, queueFilePath);
+ });
+ }
+ catch (OperationCanceledException)
+ {
+ // 取消操作,静默处理
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ finally
+ {
+ _queueSaveLock.Release();
+ }
+ }
+
+ ///
+ /// 清空队列文件
+ ///
+ protected void ClearQueueFile()
+ {
+ try
+ {
+ var queueFilePath = GetQueueFilePath();
+ ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
+ {
+ if (File.Exists(queueFilePath))
+ File.WriteAllText(queueFilePath, "[]");
+ });
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 将文件加入上传队列
+ ///
+ protected void EnqueueFile(string filePath, int retryCount = 0, CancellationToken cancellationToken = default)
+ {
+ _uploadQueue.Enqueue(new UploadQueueItem
+ {
+ FilePath = filePath,
+ RetryCount = retryCount
+ });
+
+ // 异步保存队列到文件
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await SaveQueueToFileAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // 取消操作,静默处理
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 保存上传队列时出错(后台任务): {ex}", LogHelper.LogType.Error);
+ }
+ }, cancellationToken);
+
+ // 触发队列处理
+ _ = ProcessUploadQueueAsync(cancellationToken);
+ }
+
+ ///
+ /// 处理上传队列,批量上传文件
+ ///
+ protected async Task ProcessUploadQueueAsync(CancellationToken cancellationToken = default)
+ {
+ // 使用信号量防止并发处理
+ if (!await _queueProcessingLock.WaitAsync(0, cancellationToken))
+ {
+ return; // 已有处理任务在运行
+ }
+
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var filesToUpload = new List();
+
+ // 从队列中取出最多BATCH_SIZE个文件
+ int count = 0;
+ while (count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // 再次检查文件是否存在
+ if (File.Exists(item.FilePath) && IsValidFile(item.FilePath))
+ {
+ filesToUpload.Add(item);
+ count++;
+ }
+ }
+
+ if (filesToUpload.Count == 0)
+ {
+ return;
+ }
+
+ // 检查是否启用
+ if (!IsUploadEnabled())
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:上传未启用", LogHelper.LogType.Error);
+ // 将文件重新加入队列
+ foreach (var item in filesToUpload)
+ {
+ EnqueueFile(item.FilePath, item.RetryCount, cancellationToken);
+ }
+ return;
+ }
+
+ // 并发上传所有文件,并处理失败重试
+ var uploadTasks = filesToUpload.Select(async item =>
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var success = await UploadFileInternalAsync(item.FilePath, cancellationToken);
+ if (!success)
+ {
+ // 检查是否是可重试的错误
+ if (IsRetryableError(item.FilePath))
+ {
+ // 检查重试次数
+ if (item.RetryCount < MAX_RETRY_COUNT)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
+ EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken);
+ }
+ else
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
+ }
+ }
+ }
+ return success;
+ }
+ catch (OperationCanceledException)
+ {
+ // 取消操作,将文件重新加入队列
+ EnqueueFile(item.FilePath, item.RetryCount, cancellationToken);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ // 检查是否是可重试的错误(超时、网络错误等)
+ var errorMessage = ex.Message.ToLower();
+ bool isRetryable = errorMessage.Contains("超时") ||
+ errorMessage.Contains("timeout") ||
+ errorMessage.Contains("网络错误") ||
+ errorMessage.Contains("network") ||
+ errorMessage.Contains("408") || // 请求超时
+ errorMessage.Contains("423") || // 资源锁定
+ errorMessage.Contains("429") || // 请求过多
+ errorMessage.Contains("500") || // 服务器错误
+ errorMessage.Contains("502") || // 网关错误
+ errorMessage.Contains("503") || // 服务不可用
+ errorMessage.Contains("504"); // 网关超时
+
+ if (isRetryable && IsRetryableError(item.FilePath))
+ {
+ // 检查重试次数
+ if (item.RetryCount < MAX_RETRY_COUNT)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
+ EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken);
+ }
+ else
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
+ }
+ }
+ else
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败(不可重试): {Path.GetFileName(item.FilePath)} - {ex.Message}", LogHelper.LogType.Error);
+ }
+ return false;
+ }
+ });
+ await Task.WhenAll(uploadTasks).ConfigureAwait(false);
+
+ // 上传完成后保存队列状态
+ await SaveQueueToFileAsync(cancellationToken).ConfigureAwait(false);
+
+ // 检查队列中是否还有文件,如果有就继续处理
+ if (_uploadQueue.Count > 0)
+ {
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await ProcessUploadQueueAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 继续处理上传队列时出错: {ex}", LogHelper.LogType.Error);
+ }
+ }, cancellationToken);
+ }
+ }
+ finally
+ {
+ _queueProcessingLock.Release();
+ }
+ }
+
+ ///
+ /// 验证文件是否有效
+ ///
+ protected virtual bool IsValidFile(string filePath)
+ {
+ try
+ {
+ var fileExtension = Path.GetExtension(filePath).ToLower();
+ if (!AllowedExtensions.Contains(fileExtension))
+ {
+ return false;
+ }
+
+ var fileInfo = new FileInfo(filePath);
+ long maxSize = GetMaxFileSize(fileExtension);
+ if (fileInfo.Length > maxSize)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件过大({fileInfo.Length / 1024 / 1024:F2}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
+ return false;
+ }
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 判断错误是否可重试
+ ///
+ protected bool IsRetryableError(string filePath)
+ {
+ // 检查文件是否存在
+ if (!File.Exists(filePath))
+ {
+ return false; // 文件不存在,不可重试
+ }
+
+ // 检查文件是否有效
+ if (!IsValidFile(filePath))
+ {
+ return false; // 文件无效,不可重试
+ }
+
+ // 检查是否启用
+ if (!IsUploadEnabled())
+ {
+ return false; // 上传未启用,不可重试
+ }
+
+ // 其他错误(超时、网络错误等)可以重试
+ return true;
+ }
+
+ ///
+ /// 检查上传是否启用
+ ///
+ protected abstract bool IsUploadEnabled();
+
+ ///
+ /// 内部上传方法,执行实际上传操作
+ ///
+ protected abstract Task UploadFileInternalAsync(string filePath, CancellationToken cancellationToken);
+
+ ///
+ /// 异步上传文件
+ ///
+ public async Task UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // 检查是否启用
+ if (!IsUploadEnabled())
+ {
+ return false;
+ }
+
+ // 基本验证
+ if (!File.Exists(filePath))
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
+ return false;
+ }
+
+ if (!IsValidFile(filePath))
+ {
+ return false;
+ }
+
+ // 确保队列已初始化
+ if (!_isQueueInitialized)
+ {
+ InitializeQueue();
+ }
+
+ // 加入队列
+ EnqueueFile(filePath, 0, cancellationToken);
+
+ return true;
+ }
+ catch (OperationCanceledException)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 上传被取消: {Path.GetFileName(filePath)}", LogHelper.LogType.Event);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"[{GetType().Name}] 加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
+ return false;
+ }
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// 释放资源
+ ///
+ /// 是否手动释放
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _queueProcessingLock?.Dispose();
+ _queueSaveLock?.Dispose();
+ }
+ _disposed = true;
+ }
+ }
+
+ ///
+ /// 析构函数
+ ///
+ ~BaseUploadQueue()
+ {
+ Dispose(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Helpers/DlassApiClient.cs b/Ink Canvas/Helpers/DlassApiClient.cs
index f34e81a7..c39597c2 100644
--- a/Ink Canvas/Helpers/DlassApiClient.cs
+++ b/Ink Canvas/Helpers/DlassApiClient.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
@@ -54,7 +55,8 @@ namespace Ink_Canvas.Helpers
///
/// 获取访问令牌(Access Token)
///
- public async Task GetAccessTokenAsync()
+ /// 取消令牌
+ public async Task GetAccessTokenAsync(CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(_userToken))
{
@@ -68,6 +70,8 @@ namespace Ink_Canvas.Helpers
try
{
+ cancellationToken.ThrowIfCancellationRequested();
+
var requestData = new
{
app_id = _appId,
@@ -78,7 +82,7 @@ namespace Ink_Canvas.Helpers
var json = JsonConvert.SerializeObject(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
- var response = await _httpClient.PostAsync("/oauth/token", content);
+ var response = await _httpClient.PostAsync("/oauth/token", content, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -93,14 +97,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"获取Access Token失败: {response.StatusCode}");
}
}
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
catch (HttpRequestException httpEx)
{
throw new Exception($"获取Access Token时网络错误: {httpEx.Message}", httpEx);
}
- catch (TaskCanceledException timeoutEx)
- {
- throw new Exception("获取Access Token时请求超时", timeoutEx);
- }
catch (Exception ex)
{
throw new Exception($"获取Access Token时出错: {ex.Message}", ex);
@@ -110,14 +114,19 @@ namespace Ink_Canvas.Helpers
///
/// 发送GET请求
///
- public async Task GetAsync(string endpoint, bool requireAuth = true)
+ /// API端点
+ /// 是否需要认证
+ /// 取消令牌
+ public async Task GetAsync(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
+ cancellationToken.ThrowIfCancellationRequested();
+
string token = null;
if (requireAuth)
{
- token = await GetAccessTokenAsync();
+ token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
@@ -134,7 +143,7 @@ namespace Ink_Canvas.Helpers
}
}
- var response = await _httpClient.SendAsync(request);
+ var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -150,14 +159,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
- catch (TaskCanceledException timeoutEx)
- {
- throw new Exception($"请求超时: {endpoint}", timeoutEx);
- }
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
@@ -167,14 +176,20 @@ namespace Ink_Canvas.Helpers
///
/// 发送POST请求
///
- public async Task PostAsync(string endpoint, object data = null, bool requireAuth = true)
+ /// API端点
+ /// 请求数据
+ /// 是否需要认证
+ /// 取消令牌
+ public async Task PostAsync(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
+ cancellationToken.ThrowIfCancellationRequested();
+
string token = null;
if (requireAuth)
{
- token = await GetAccessTokenAsync();
+ token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
@@ -197,7 +212,7 @@ namespace Ink_Canvas.Helpers
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
- var response = await _httpClient.SendAsync(request);
+ var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -213,14 +228,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
- catch (TaskCanceledException timeoutEx)
- {
- throw new Exception($"请求超时: {endpoint}", timeoutEx);
- }
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
@@ -230,14 +245,20 @@ namespace Ink_Canvas.Helpers
///
/// 发送PUT请求
///
- public async Task PutAsync(string endpoint, object data = null, bool requireAuth = true)
+ /// API端点
+ /// 请求数据
+ /// 是否需要认证
+ /// 取消令牌
+ public async Task PutAsync(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
+ cancellationToken.ThrowIfCancellationRequested();
+
string token = null;
if (requireAuth)
{
- token = await GetAccessTokenAsync();
+ token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
@@ -261,7 +282,7 @@ namespace Ink_Canvas.Helpers
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
- var response = await _httpClient.SendAsync(request);
+ var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -277,14 +298,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
- catch (TaskCanceledException timeoutEx)
- {
- throw new Exception($"请求超时: {endpoint}", timeoutEx);
- }
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
@@ -294,14 +315,19 @@ namespace Ink_Canvas.Helpers
///
/// 发送DELETE请求
///
- public async Task DeleteAsync(string endpoint, bool requireAuth = true)
+ /// API端点
+ /// 是否需要认证
+ /// 取消令牌
+ public async Task DeleteAsync(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
+ cancellationToken.ThrowIfCancellationRequested();
+
string token = null;
if (requireAuth)
{
- token = await GetAccessTokenAsync();
+ token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
@@ -319,7 +345,7 @@ namespace Ink_Canvas.Helpers
}
}
- var response = await _httpClient.SendAsync(request);
+ var response = await _httpClient.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
@@ -330,11 +356,11 @@ namespace Ink_Canvas.Helpers
return false;
}
}
- catch (HttpRequestException)
+ catch (OperationCanceledException)
{
- return false;
+ throw;
}
- catch (TaskCanceledException)
+ catch (HttpRequestException)
{
return false;
}
@@ -354,10 +380,13 @@ namespace Ink_Canvas.Helpers
/// 笔记标题(可选)
/// 笔记描述(可选)
/// 笔记标签(可选)
- public async Task UploadNoteAsync(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null)
+ /// 取消令牌
+ public async Task UploadNoteAsync(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null, CancellationToken cancellationToken = default)
{
try
{
+ cancellationToken.ThrowIfCancellationRequested();
+
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"文件不存在: {filePath}");
@@ -394,7 +423,7 @@ namespace Ink_Canvas.Helpers
request.Content = content;
- var response = await _httpClient.SendAsync(request);
+ var response = await _httpClient.SendAsync(request, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -410,14 +439,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"上传文件失败: {response.StatusCode} - {responseContent}");
}
}
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
catch (HttpRequestException httpEx)
{
throw new Exception($"上传文件时网络错误: {httpEx.Message}", httpEx);
}
- catch (TaskCanceledException timeoutEx)
- {
- throw new Exception($"上传文件超时: {endpoint}", timeoutEx);
- }
catch (Exception ex)
{
throw new Exception($"上传文件时出错: {ex.Message}", ex);
diff --git a/Ink Canvas/Helpers/DlassNoteUploader.cs b/Ink Canvas/Helpers/DlassNoteUploader.cs
deleted file mode 100644
index 295faf82..00000000
--- a/Ink Canvas/Helpers/DlassNoteUploader.cs
+++ /dev/null
@@ -1,807 +0,0 @@
-using Newtonsoft.Json;
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Ink_Canvas.Helpers
-{
- ///
- /// Dlass笔记自动上传辅助类
- ///
- public class DlassNoteUploader
- {
- private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
- private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
- private const int BATCH_SIZE = 10; // 批量上传大小
- private const int MAX_RETRY_COUNT = 3; // 最大重试次数
- private const string QUEUE_FILE_NAME = "DlassUploadQueue.json";
-
- ///
- /// 上传队列项
- ///
- private class UploadQueueItemData
- {
- [JsonProperty("file_path")]
- public string FilePath { get; set; }
-
- [JsonProperty("retry_count")]
- public int RetryCount { get; set; }
-
- [JsonProperty("added_time")]
- public DateTime AddedTime { get; set; }
- }
-
- ///
- /// 上传队列项
- ///
- private class UploadQueueItem
- {
- public string FilePath { get; set; }
- public int RetryCount { get; set; }
- }
-
- ///
- /// 上传队列
- ///
- private static readonly ConcurrentQueue _uploadQueue = new ConcurrentQueue();
-
- ///
- /// 队列处理锁,防止并发处理
- ///
- private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
-
- ///
- /// 队列保存锁,防止并发保存
- ///
- private static readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
-
- ///
- /// 是否已初始化队列
- ///
- private static bool _isQueueInitialized = false;
-
- ///
- /// 获取队列文件路径
- ///
- private static string GetQueueFilePath()
- {
- var configsDir = Path.Combine(App.RootPath, "Configs");
- if (!Directory.Exists(configsDir))
- {
- Directory.CreateDirectory(configsDir);
- }
- return Path.Combine(configsDir, QUEUE_FILE_NAME);
- }
-
- ///
- /// 初始化上传队列
- ///
- public static void InitializeQueue()
- {
- if (_isQueueInitialized)
- {
- return;
- }
-
- try
- {
- var queueFilePath = GetQueueFilePath();
- if (!File.Exists(queueFilePath))
- {
- _isQueueInitialized = true;
- return;
- }
-
- var jsonContent = File.ReadAllText(queueFilePath);
- if (string.IsNullOrWhiteSpace(jsonContent))
- {
- _isQueueInitialized = true;
- return;
- }
-
- var queueData = JsonConvert.DeserializeObject>(jsonContent);
- if (queueData == null || queueData.Count == 0)
- {
- _isQueueInitialized = true;
- return;
- }
-
- int restoredCount = 0;
- int skippedCount = 0;
-
- foreach (var item in queueData)
- {
- // 验证文件是否存在
- if (!File.Exists(item.FilePath))
- {
- skippedCount++;
- continue;
- }
-
- // 验证文件格式和大小
- var fileExtension = Path.GetExtension(item.FilePath).ToLower();
- if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
- {
- skippedCount++;
- continue;
- }
-
- try
- {
- var fileInfo = new FileInfo(item.FilePath);
- long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
- if (fileInfo.Length > maxSize)
- {
- skippedCount++;
- continue;
- }
- }
- catch
- {
- skippedCount++;
- continue;
- }
-
- // 恢复队列项
- _uploadQueue.Enqueue(new UploadQueueItem
- {
- FilePath = item.FilePath,
- RetryCount = item.RetryCount
- });
- restoredCount++;
- }
-
- _isQueueInitialized = true;
-
- if (restoredCount > 0)
- {
- LogHelper.WriteLogToFile($"已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
- // 如果恢复了队列,触发处理
- _ = Task.Run(async () =>
- {
- try
- {
- await ProcessUploadQueueAsync().ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"恢复上传队列后处理时出错: {ex}", LogHelper.LogType.Error);
- }
- });
- }
- else if (skippedCount > 0)
- {
- LogHelper.WriteLogToFile($"队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
- }
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
- _isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试
- }
- }
-
- ///
- /// 保存队列到文件
- ///
- private static async Task SaveQueueToFileAsync()
- {
- if (!await _queueSaveLock.WaitAsync(1000)) // 最多等待1秒
- {
- return; // 如果无法获取锁,跳过保存(避免阻塞)
- }
-
- try
- {
- var queueData = new List();
-
- // 将队列转换为可序列化的格式
- foreach (var item in _uploadQueue)
- {
- queueData.Add(new UploadQueueItemData
- {
- FilePath = item.FilePath,
- RetryCount = item.RetryCount,
- AddedTime = DateTime.Now
- });
- }
-
- var queueFilePath = GetQueueFilePath();
-
- // 如果队列为空,清空文件
- if (queueData.Count == 0)
- {
- ClearQueueFile();
- return;
- }
-
- var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented);
-
- // 使用进程保护的写入门控,避免安全面板中“进程文件保护”占用导致无法写入
- var tempFilePath = queueFilePath + ".tmp";
- ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
- {
- File.WriteAllText(tempFilePath, jsonContent);
- if (File.Exists(queueFilePath))
- File.Delete(queueFilePath);
- File.Move(tempFilePath, queueFilePath);
- });
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
- }
- finally
- {
- _queueSaveLock.Release();
- }
- }
-
- ///
- /// 清空队列文件
- ///
- private static void ClearQueueFile()
- {
- try
- {
- var queueFilePath = GetQueueFilePath();
- ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
- {
- if (File.Exists(queueFilePath))
- File.WriteAllText(queueFilePath, "[]");
- });
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error);
- }
- }
-
- ///
- /// 上传笔记响应模型
- ///
- 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; }
- }
-
- ///
- /// 白板信息模型(用于查找白板)
- ///
- 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; }
- }
-
- ///
- /// 认证响应模型
- ///
- private class AuthWithTokenResponse
- {
- [JsonProperty("success")]
- public bool Success { get; set; }
-
- [JsonProperty("whiteboards")]
- public List Whiteboards { get; set; }
- }
-
- ///
- /// 异步上传笔记文件到Dlass(支持PNG、ICSTK、XML和ZIP格式)
- ///
- /// 文件路径(支持PNG、ICSTK、XML和ZIP)
- /// 是否成功加入队列(不等待实际上传完成)
- public static async Task UploadNoteFileAsync(string filePath)
- {
- try
- {
- // 检查是否启用自动上传
- if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true)
- {
- return false;
- }
-
- // 基本验证
- if (!File.Exists(filePath))
- {
- LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
- return false;
- }
-
- var fileExtension = Path.GetExtension(filePath).ToLower();
- if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
- {
- return false;
- }
-
- var fileInfo = new FileInfo(filePath);
- long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
- if (fileInfo.Length > maxSize)
- {
- LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
- return false;
- }
-
- // 获取上传延迟时间(分钟)
- var delayMinutes = MainWindow.Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
-
- // 如果设置了延迟时间,在后台任务中等待后再加入队列
- if (delayMinutes > 0)
- {
- _ = Task.Run(async () =>
- {
- try
- {
- await Task.Delay(TimeSpan.FromMinutes(delayMinutes)).ConfigureAwait(false);
- if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true)
- {
- LogHelper.WriteLogToFile($"延迟结束后自动上传已关闭,跳过入队: {filePath}", LogHelper.LogType.Event);
- return;
- }
- if (!File.Exists(filePath))
- {
- LogHelper.WriteLogToFile($"延迟结束后文件已不存在,跳过入队: {filePath}", LogHelper.LogType.Event);
- return;
- }
- EnqueueFile(filePath);
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"延迟加入上传队列时出错: {ex}", LogHelper.LogType.Error);
- }
- });
- }
- else
- {
- EnqueueFile(filePath);
- }
-
- return true;
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
- return false;
- }
- }
-
- ///
- /// 将文件加入上传队列
- ///
- private static void EnqueueFile(string filePath, int retryCount = 0)
- {
- _uploadQueue.Enqueue(new UploadQueueItem
- {
- FilePath = filePath,
- RetryCount = retryCount
- });
-
- // 异步保存队列到文件
- _ = Task.Run(async () =>
- {
- try
- {
- await SaveQueueToFileAsync().ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"保存上传队列时出错(后台任务): {ex}", LogHelper.LogType.Error);
- }
- });
-
- // 如果队列达到批量大小,触发批量上传
- if (_uploadQueue.Count >= BATCH_SIZE)
- {
- _ = ProcessUploadQueueAsync();
- }
- }
-
- ///
- /// 处理上传队列,批量上传文件
- ///
- private static async Task ProcessUploadQueueAsync()
- {
- // 使用信号量防止并发处理
- if (!await _queueProcessingLock.WaitAsync(0))
- {
- return; // 已有处理任务在运行
- }
-
- try
- {
- var filesToUpload = new List();
-
- // 从队列中取出最多BATCH_SIZE个文件
- while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
- {
- // 再次检查文件是否存在
- if (File.Exists(item.FilePath))
- {
- filesToUpload.Add(item);
- }
- }
-
- if (filesToUpload.Count == 0)
- {
- return;
- }
-
- // 获取共享的白板信息(同一批次的所有文件共享认证信息)
- WhiteboardInfo sharedWhiteboard = null;
- string apiBaseUrl = null;
- string userToken = null;
-
- try
- {
- var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
- if (string.IsNullOrEmpty(selectedClassName))
- {
- LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
- // 将文件重新加入队列
- foreach (var item in filesToUpload)
- {
- EnqueueFile(item.FilePath, item.RetryCount);
- }
- return;
- }
-
- userToken = MainWindow.Settings?.Dlass?.UserToken;
- if (string.IsNullOrEmpty(userToken))
- {
- LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
- // 将文件重新加入队列
- foreach (var item in filesToUpload)
- {
- EnqueueFile(item.FilePath, item.RetryCount);
- }
- return;
- }
-
- apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
-
- // 获取白板信息(只获取一次,所有文件共享)
- 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("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
-
- if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
- {
- LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
- // 将文件重新加入队列
- foreach (var item in filesToUpload)
- {
- EnqueueFile(item.FilePath, item.RetryCount);
- }
- 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);
- // 将文件重新加入队列
- foreach (var item in filesToUpload)
- {
- EnqueueFile(item.FilePath, item.RetryCount);
- }
- return;
- }
- }
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error);
- // 将文件重新加入队列
- foreach (var item in filesToUpload)
- {
- EnqueueFile(item.FilePath, item.RetryCount);
- }
- return;
- }
-
- // 并发上传所有文件(共享白板信息),并处理失败重试
- 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;
- }
- });
- await Task.WhenAll(uploadTasks);
-
- // 上传完成后保存队列状态
- await SaveQueueToFileAsync();
-
- // 如果队列达到批量大小,继续处理
- if (_uploadQueue.Count >= BATCH_SIZE)
- {
- _ = Task.Run(async () =>
- {
- try
- {
- await ProcessUploadQueueAsync().ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"继续批量处理上传队列时出错: {ex}", LogHelper.LogType.Error);
- }
- });
- }
- }
- finally
- {
- _queueProcessingLock.Release();
- }
- }
-
- ///
- /// 内部上传方法,执行实际上传操作
- ///
- /// 文件路径
- /// 白板信息(如果为null则重新获取)
- /// API基础URL(如果为null则从设置获取)
- /// 用户Token(如果为null则从设置获取)
- private static async Task 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" && fileExtension != ".xml" && fileExtension != ".zip")
- {
- return false;
- }
-
- // 检查文件大小(最大10MB,ZIP文件可能更大,允许50MB)
- var fileInfo = new FileInfo(filePath);
- long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
- if (fileInfo.Length > maxSize)
- {
- LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
- return false;
- }
-
- // 如果白板信息未提供,则重新获取
- if (whiteboard == null)
- {
- var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
- if (string.IsNullOrEmpty(selectedClassName))
- {
- LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
- return false;
- }
-
- userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
- if (string.IsNullOrEmpty(userToken))
- {
- LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
- return false;
- }
-
- 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("/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;
- string fileType;
- string tags;
- if (fileExtension == ".zip")
- {
- fileType = "多页面墨迹压缩包";
- tags = "自动上传,多页面,zip,压缩包";
- }
- else if (fileExtension == ".icstk")
- {
- fileType = "墨迹文件";
- tags = "自动上传,墨迹,icstk";
- }
- else if (fileExtension == ".xml")
- {
- fileType = "XML文件";
- tags = "自动上传,xml";
- }
- else
- {
- fileType = "笔记";
- tags = "自动上传,笔记,png";
- }
- var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
-
- // 创建API客户端并上传文件
- using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
- {
- var uploadResult = await apiClient.UploadNoteAsync(
- "/api/whiteboard/upload_note",
- filePath,
- 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)
- {
- // 记录错误信息,抛出异常以便调用方判断是否可重试
- LogHelper.WriteLogToFile($"上传笔记时出错: {ex.Message}", LogHelper.LogType.Error);
- throw;
- }
- }
-
- ///
- /// 判断错误是否可重试(超时、网络错误等)
- ///
- private static bool IsRetryableError(string filePath)
- {
- // 检查文件是否存在
- if (!File.Exists(filePath))
- {
- return false; // 文件不存在,不可重试
- }
-
- // 检查文件扩展名
- var fileExtension = Path.GetExtension(filePath).ToLower();
- if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
- {
- return false; // 文件格式错误,不可重试
- }
-
- // 检查文件大小
- try
- {
- var fileInfo = new FileInfo(filePath);
- long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
- if (fileInfo.Length > maxSize)
- {
- return false; // 文件过大,不可重试
- }
- }
- catch
- {
- return false; // 无法读取文件信息,不可重试
- }
-
- // 其他错误(超时、网络错误等)可以重试
- return true;
- }
- }
-}
-
-
diff --git a/Ink Canvas/Helpers/DlassUploadQueue.cs b/Ink Canvas/Helpers/DlassUploadQueue.cs
new file mode 100644
index 00000000..6f679d84
--- /dev/null
+++ b/Ink Canvas/Helpers/DlassUploadQueue.cs
@@ -0,0 +1,257 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ink_Canvas.Helpers
+{
+ ///
+ /// Dlass上传队列
+ ///
+ public class DlassUploadQueue : BaseUploadQueue
+ {
+ private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
+ private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
+
+ ///
+ /// 队列文件名
+ ///
+ protected override string QueueFileName => "DlassUploadQueue.json";
+
+ ///
+ /// 上传笔记响应模型
+ ///
+ 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; }
+ }
+
+ ///
+ /// 白板信息模型(用于查找白板)
+ ///
+ 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; }
+ }
+
+ ///
+ /// 认证响应模型
+ ///
+ private class AuthWithTokenResponse
+ {
+ [JsonProperty("success")]
+ public bool Success { get; set; }
+
+ [JsonProperty("whiteboards")]
+ public List Whiteboards { get; set; }
+ }
+
+ ///
+ /// 检查上传是否启用
+ ///
+ protected override bool IsUploadEnabled()
+ {
+ return MainWindow.Settings?.Dlass?.IsAutoUploadNotes == true;
+ }
+
+ ///
+ /// 内部上传方法,执行实际上传操作
+ ///
+ protected override async Task UploadFileInternalAsync(string filePath, CancellationToken cancellationToken)
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // 再次检查文件是否存在(可能在队列等待时被删除)
+ if (!File.Exists(filePath))
+ {
+ return false;
+ }
+
+ // 获取白板信息
+ var whiteboard = await GetWhiteboardInfo(cancellationToken);
+ if (whiteboard == null)
+ {
+ return false;
+ }
+
+ // 获取API基础URL和用户Token
+ var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
+ var userToken = MainWindow.Settings?.Dlass?.UserToken;
+
+ // 准备上传参数
+ var fileName = Path.GetFileNameWithoutExtension(filePath);
+ var fileExtension = Path.GetExtension(filePath).ToLower();
+ var title = fileName;
+ string fileType;
+ string tags;
+ if (fileExtension == ".zip")
+ {
+ fileType = "多页面墨迹压缩包";
+ tags = "自动上传,多页面,zip,压缩包";
+ }
+ else if (fileExtension == ".icstk")
+ {
+ fileType = "墨迹文件";
+ tags = "自动上传,墨迹,icstk";
+ }
+ else if (fileExtension == ".xml")
+ {
+ fileType = "XML文件";
+ tags = "自动上传,xml";
+ }
+ else
+ {
+ fileType = "笔记";
+ tags = "自动上传,笔记,png";
+ }
+ var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
+
+ // 创建API客户端并上传文件
+ var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken);
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var uploadResult = await apiClient.UploadNoteAsync(
+ "/api/whiteboard/upload_note",
+ filePath,
+ whiteboard.BoardId,
+ whiteboard.SecretKey,
+ title,
+ description,
+ tags,
+ cancellationToken);
+
+ if (uploadResult != null && uploadResult.Success)
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ finally
+ {
+ apiClient.Dispose();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ }
+
+ ///
+ /// 获取白板信息
+ ///
+ private async Task GetWhiteboardInfo(CancellationToken cancellationToken)
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
+ if (string.IsNullOrEmpty(selectedClassName))
+ {
+ LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:未选择班级", LogHelper.LogType.Error);
+ return null;
+ }
+
+ var userToken = MainWindow.Settings?.Dlass?.UserToken;
+ if (string.IsNullOrEmpty(userToken))
+ {
+ LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:未设置用户Token", LogHelper.LogType.Error);
+ return null;
+ }
+
+ var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
+
+ // 创建API客户端并获取白板信息
+ var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken);
+ try
+ {
+ var authData = new
+ {
+ app_id = APP_ID,
+ app_secret = APP_SECRET,
+ user_token = userToken
+ };
+
+ var authResult = await apiClient.PostAsync("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false, cancellationToken: cancellationToken);
+
+ if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
+ {
+ LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:无法获取白板信息", LogHelper.LogType.Error);
+ return null;
+ }
+
+ // 查找匹配班级的白板
+ var 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($"[DlassUploadQueue] 上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
+ return null;
+ }
+
+ return whiteboard;
+ }
+ finally
+ {
+ apiClient.Dispose();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Helpers/UploadHelper.cs b/Ink Canvas/Helpers/UploadHelper.cs
index cef66c61..13a0e7bf 100644
--- a/Ink Canvas/Helpers/UploadHelper.cs
+++ b/Ink Canvas/Helpers/UploadHelper.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using System.Threading;
namespace Ink_Canvas.Helpers
{
@@ -24,8 +26,9 @@ namespace Ink_Canvas.Helpers
/// 上传文件
///
/// 文件路径
+ /// 取消令牌
/// 是否上传成功
- Task UploadAsync(string filePath);
+ Task UploadAsync(string filePath, CancellationToken cancellationToken = default);
}
///
@@ -33,6 +36,8 @@ namespace Ink_Canvas.Helpers
///
public class DlassUploadProvider : IUploadProvider
{
+ public static readonly DlassUploadQueue Queue = new DlassUploadQueue();
+
///
/// 提供者名称
///
@@ -41,16 +46,46 @@ namespace Ink_Canvas.Helpers
///
/// 是否启用
///
- public bool IsEnabled => MainWindow.Settings?.Dlass?.IsAutoUploadNotes ?? false;
+ public bool IsEnabled => MainWindow.Settings?.Upload?.EnabledProviders?.Contains(Name) ?? false;
///
/// 上传文件
///
/// 文件路径
+ /// 取消令牌
/// 是否上传成功
- public async Task UploadAsync(string filePath)
+ public async Task UploadAsync(string filePath, CancellationToken cancellationToken = default)
{
- return await DlassNoteUploader.UploadNoteFileAsync(filePath);
+ return await Queue.UploadFileAsync(filePath, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// WebDav上传提供者
+ ///
+ public class WebDavUploadProvider : IUploadProvider
+ {
+ public static readonly WebDavUploadQueue Queue = new WebDavUploadQueue();
+
+ ///
+ /// 提供者名称
+ ///
+ public string Name => "WebDav";
+
+ ///
+ /// 是否启用
+ ///
+ public bool IsEnabled => MainWindow.Settings?.Upload?.EnabledProviders?.Contains(Name) ?? false;
+
+ ///
+ /// 上传文件
+ ///
+ /// 文件路径
+ /// 取消令牌
+ /// 是否上传成功
+ public async Task UploadAsync(string filePath, CancellationToken cancellationToken = default)
+ {
+ return await Queue.UploadFileAsync(filePath, cancellationToken).ConfigureAwait(false);
}
}
@@ -77,6 +112,14 @@ namespace Ink_Canvas.Helpers
// 注册默认上传提供者
RegisterProviderInternal(new DlassUploadProvider());
+ RegisterProviderInternal(new WebDavUploadProvider());
+
+ // 注册上传队列
+ UploadQueueHelper.RegisterQueue(DlassUploadProvider.Queue);
+ UploadQueueHelper.RegisterQueue(WebDavUploadProvider.Queue);
+
+ // 初始化所有上传队列
+ UploadQueueHelper.InitializeAllQueues();
_initialized = true;
}
@@ -113,8 +156,9 @@ namespace Ink_Canvas.Helpers
/// 上传文件到所有启用的提供者
///
/// 文件路径
+ /// 取消令牌
/// 是否至少有一个提供者上传成功
- public static async Task UploadFileAsync(string filePath)
+ public static async Task UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
{
if (!_initialized)
{
@@ -129,19 +173,56 @@ namespace Ink_Canvas.Helpers
bool anySuccess = false;
+ // 获取上传延迟时间
+ int delayMinutes = MainWindow.Settings?.Upload?.UploadDelayMinutes ?? 0;
+
+ // 应用上传延迟
+ if (delayMinutes > 0)
+ {
+ LogHelper.WriteLogToFile($"上传延迟 {delayMinutes} 分钟", LogHelper.LogType.Event);
+ cancellationToken.ThrowIfCancellationRequested();
+ await Task.Delay(TimeSpan.FromMinutes(delayMinutes), cancellationToken).ConfigureAwait(false);
+ }
+
+ // 上传前验证文件是否存在且可访问
+ if (!File.Exists(filePath))
+ {
+ LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
+ return false;
+ }
+
+ try
+ {
+ // 检查文件是否可访问
+ using (var fileStream = File.OpenRead(filePath))
+ {
+ // 文件可访问
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"上传失败:文件不可访问 - {filePath}, 原因: {ex.Message}", LogHelper.LogType.Error);
+ return false;
+ }
+
foreach (var provider in providersSnapshot)
{
try
{
if (provider.IsEnabled)
{
- bool success = await provider.UploadAsync(filePath);
+ bool success = await provider.UploadAsync(filePath, cancellationToken).ConfigureAwait(false);
if (success)
{
anySuccess = true;
}
}
}
+ catch (OperationCanceledException)
+ {
+ LogHelper.WriteLogToFile($"上传被取消: {provider.Name}", LogHelper.LogType.Event);
+ throw;
+ }
catch (Exception ex)
{
LogHelper.WriteLogToFile($"使用 {provider.Name} 上传失败: {ex}", LogHelper.LogType.Error);
diff --git a/Ink Canvas/Helpers/UploadQueueHelper.cs b/Ink Canvas/Helpers/UploadQueueHelper.cs
new file mode 100644
index 00000000..d08efd46
--- /dev/null
+++ b/Ink Canvas/Helpers/UploadQueueHelper.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ink_Canvas.Helpers
+{
+ ///
+ /// 上传队列帮助类,提供统一的队列管理功能
+ ///
+ public static class UploadQueueHelper
+ {
+ private static readonly List _queues = new List();
+ private static readonly object _syncLock = new object();
+ private static volatile bool _initialized = false;
+
+ ///
+ /// 初始化所有上传队列
+ ///
+ public static void InitializeAllQueues()
+ {
+ lock (_syncLock)
+ {
+ if (_initialized)
+ return;
+
+ // 初始化所有注册的队列
+ foreach (var queue in _queues)
+ {
+ try
+ {
+ queue.InitializeQueue();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"[UploadQueueHelper] 初始化队列时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ _initialized = true;
+ }
+ }
+
+ ///
+ /// 注册上传队列
+ ///
+ /// 上传队列实例
+ public static void RegisterQueue(BaseUploadQueue queue)
+ {
+ if (queue == null)
+ return;
+
+ lock (_syncLock)
+ {
+ if (!_queues.Contains(queue))
+ {
+ try
+ {
+ // 先初始化队列,再添加到列表
+ queue.InitializeQueue();
+ _queues.Add(queue);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"[UploadQueueHelper] 注册队列时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 获取所有注册的上传队列
+ ///
+ /// 上传队列列表
+ public static IReadOnlyList GetAllQueues()
+ {
+ lock (_syncLock)
+ {
+ return new List(_queues).AsReadOnly();
+ }
+ }
+
+ ///
+ /// 确保所有队列都已初始化
+ ///
+ public static void EnsureQueuesInitialized()
+ {
+ if (!_initialized)
+ {
+ InitializeAllQueues();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Helpers/WebDavUploadQueue.cs b/Ink Canvas/Helpers/WebDavUploadQueue.cs
new file mode 100644
index 00000000..82bbf567
--- /dev/null
+++ b/Ink Canvas/Helpers/WebDavUploadQueue.cs
@@ -0,0 +1,61 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ink_Canvas.Helpers
+{
+ ///
+ /// WebDAV上传队列
+ ///
+ public class WebDavUploadQueue : BaseUploadQueue
+ {
+ ///
+ /// 队列文件名
+ ///
+ protected override string QueueFileName => "WebDavUploadQueue.json";
+
+ ///
+ /// 检查上传是否启用
+ ///
+ protected override bool IsUploadEnabled()
+ {
+ return WebDavUploader.IsWebDavEnabled();
+ }
+
+ ///
+ /// 内部上传方法,执行实际上传操作
+ ///
+ protected override async Task UploadFileInternalAsync(string filePath, CancellationToken cancellationToken)
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // 再次检查文件是否存在(可能在队列等待时被删除)
+ if (!File.Exists(filePath))
+ {
+ return false;
+ }
+
+ // 检查WebDAV是否仍然启用
+ if (!WebDavUploader.IsWebDavEnabled())
+ {
+ return false;
+ }
+
+ // 调用WebDavUploader进行实际上传
+ var success = await WebDavUploader.UploadFileAsync(filePath, cancellationToken);
+ return success;
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Helpers/WebDavUploader.cs b/Ink Canvas/Helpers/WebDavUploader.cs
new file mode 100644
index 00000000..7c229688
--- /dev/null
+++ b/Ink Canvas/Helpers/WebDavUploader.cs
@@ -0,0 +1,171 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using WebDav;
+
+namespace Ink_Canvas.Helpers
+{
+ ///
+ /// WebDav上传工具类
+ ///
+ public static class WebDavUploader
+ {
+ ///
+ /// 上传文件到WebDav服务器
+ ///
+ /// 文件路径
+ /// 取消令牌
+ /// 是否上传成功
+ public static async Task UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // 检查文件是否存在
+ if (!File.Exists(filePath))
+ {
+ return false;
+ }
+
+ // 获取WebDav设置
+ var webDavUrl = MainWindow.Settings?.Dlass?.WebDavUrl;
+ var username = MainWindow.Settings?.Dlass?.WebDavUsername;
+ var password = MainWindow.Settings?.Dlass?.WebDavPassword;
+ var rootDirectory = MainWindow.Settings?.Dlass?.WebDavRootDirectory;
+
+ // 验证设置
+ if (string.IsNullOrEmpty(webDavUrl))
+ {
+ return false;
+ }
+
+ // 构建完整的目标路径
+ var fileName = Path.GetFileName(filePath);
+ var targetPath = Path.Combine(rootDirectory ?? string.Empty, fileName).Replace("\\", "/");
+ if (targetPath.StartsWith("/"))
+ {
+ targetPath = targetPath.Substring(1);
+ }
+
+ // 创建WebDav客户端
+ var clientParams = new WebDavClientParams
+ {
+ BaseAddress = new Uri(webDavUrl),
+ Credentials = new NetworkCredential(username ?? string.Empty, password ?? string.Empty)
+ };
+
+ using (var client = new WebDavClient(clientParams))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // 先直接尝试上传文件
+ using (var fileStream = File.OpenRead(filePath))
+ {
+ // 检查取消令牌
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var result = await client.PutFile(targetPath, fileStream);
+ if (result.IsSuccessful)
+ {
+ return true;
+ }
+ else
+ {
+ // 上传失败,尝试创建目录
+ var directoryPath = Path.GetDirectoryName(targetPath);
+ if (!string.IsNullOrEmpty(directoryPath))
+ {
+ await EnsureDirectoryExistsAsync(client, directoryPath, cancellationToken);
+
+ // 再次尝试上传文件
+ cancellationToken.ThrowIfCancellationRequested();
+ using (var retryStream = File.OpenRead(filePath))
+ {
+ var retryResult = await client.PutFile(targetPath, retryStream);
+ return retryResult.IsSuccessful;
+ }
+ }
+ else
+ {
+ // 没有目录路径,直接返回失败
+ return false;
+ }
+ }
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 确保WebDav目录存在
+ ///
+ /// WebDav客户端
+ /// 目录路径
+ /// 取消令牌
+ private static async Task EnsureDirectoryExistsAsync(IWebDavClient client, string directoryPath, CancellationToken cancellationToken)
+ {
+ try
+ {
+ // 分割路径并逐级创建目录
+ var pathParts = directoryPath.Split('/');
+ var currentPath = string.Empty;
+
+ foreach (var part in pathParts)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (string.IsNullOrEmpty(part))
+ continue;
+
+ currentPath = Path.Combine(currentPath, part).Replace("\\", "/");
+
+ // 检查取消令牌
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // 尝试创建目录
+ await client.Mkcol(currentPath);
+ }
+ }
+ catch (Exception)
+ {
+ // 静默处理目录创建错误
+ }
+ }
+
+ ///
+ /// 检查WebDAV是否已启用
+ ///
+ /// 是否启用
+ public static bool IsWebDavEnabled()
+ {
+ // 检查WebDav设置是否有效
+ var webDavUrl = MainWindow.Settings?.Dlass?.WebDavUrl;
+ if (string.IsNullOrEmpty(webDavUrl))
+ {
+ return false;
+ }
+
+ // 尝试解析URL
+ try
+ {
+ new Uri(webDavUrl);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/Ink Canvas/InkCanvasForClass.csproj b/Ink Canvas/InkCanvasForClass.csproj
index a8bf0332..814181a8 100644
--- a/Ink Canvas/InkCanvasForClass.csproj
+++ b/Ink Canvas/InkCanvasForClass.csproj
@@ -166,6 +166,7 @@
+
diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml
index 8bfcafac..6ea32014 100644
--- a/Ink Canvas/MainWindow.xaml
+++ b/Ink Canvas/MainWindow.xaml
@@ -3655,7 +3655,7 @@
StrokeThickness="1" Margin="0,8,0,8" />
-
-
-
-
+
+
+
diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs
index 966d2b2b..6ccc9104 100644
--- a/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs
+++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs
@@ -7,20 +7,31 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
+using ui = iNKORE.UI.WPF.Modern.Controls;
namespace Ink_Canvas.Windows
{
///
- /// DlassSettingsWindow.xaml 的交互逻辑
+ /// 云储存管理窗口
///
+ ///
+ /// 该窗口包含三个标签页:
+ /// 1. 通用设置 - 管理所有上传提供者的通用设置,包括上传延迟时间和提供者启用/禁用
+ /// 2. Dlass - 管理Dlass服务端连接和设置,包括用户Token、班级选择和自动上传设置
+ /// 3. WebDav - 预留的WebDav连接设置页面
+ ///
public partial class DlassSettingsWindow : Window
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
+ // 静态 Regex 实例,用于验证数字输入
+ private static readonly Regex _nonDigitRegex = new Regex("[^0-9]+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
+
private DlassApiClient _apiClient;
private List _currentWhiteboards = new List();
private UserInfo _currentUser;
+ private bool _isFirstTimeDlassTab = true;
public DlassSettingsWindow(MainWindow mainWindow = null)
{
@@ -38,6 +49,12 @@ namespace Ink_Canvas.Windows
// 加载自动上传设置
LoadAutoUploadSettings();
+ // 加载通用设置
+ LoadUniversalUploadSettings();
+
+ // 加载WebDav设置
+ LoadWebDavSettings();
+
// 初始化API客户端(优先使用用户token)
InitializeApiClient();
@@ -291,7 +308,7 @@ namespace Ink_Canvas.Windows
{
delayMinutes = 0;
}
- TxtUploadDelayMinutes.Text = delayMinutes.ToString();
+
}
}
catch (Exception ex)
@@ -310,6 +327,31 @@ namespace Ink_Canvas.Windows
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.IsAutoUploadNotes = ToggleSwitchAutoUploadNotes.IsOn;
+
+ // 同步更新到EnabledProviders列表
+ if (MainWindow.Settings.Upload != null)
+ {
+ if (MainWindow.Settings.Upload.EnabledProviders == null)
+ {
+ MainWindow.Settings.Upload.EnabledProviders = new List();
+ }
+
+ if (ToggleSwitchAutoUploadNotes.IsOn)
+ {
+ if (!MainWindow.Settings.Upload.EnabledProviders.Contains("Dlass"))
+ {
+ MainWindow.Settings.Upload.EnabledProviders.Add("Dlass");
+ }
+ }
+ else
+ {
+ MainWindow.Settings.Upload.EnabledProviders.Remove("Dlass");
+ }
+
+ // 重新加载通用设置,更新UI
+ LoadUniversalUploadSettings();
+ }
+
MainWindow.SaveSettingsToFile();
}
}
@@ -319,53 +361,145 @@ namespace Ink_Canvas.Windows
}
}
+
+
+
+
///
- /// 上传延迟时间输入框文本改变事件
+ /// 加载通用设置
///
- private void TxtUploadDelayMinutes_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
+ private void LoadUniversalUploadSettings()
{
try
{
- if (MainWindow.Settings?.Dlass != null && int.TryParse(TxtUploadDelayMinutes.Text, out int delayMinutes))
+ // 加载上传延迟时间
+ if (MainWindow.Settings?.Upload != null)
+ {
+ var delayMinutes = MainWindow.Settings.Upload.UploadDelayMinutes;
+ if (delayMinutes < 0 || delayMinutes > 60)
+ {
+ delayMinutes = 0;
+ }
+ TxtUniversalUploadDelayMinutes.Text = delayMinutes.ToString();
+ }
+
+ // 加载上传提供者列表
+ LoadUploadProvidersList();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"加载通用设置时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 加载上传提供者列表
+ ///
+ private void LoadUploadProvidersList()
+ {
+ try
+ {
+ var providers = UploadHelper.GetProviders();
+ LstUploadProviders.ItemsSource = providers;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"加载上传提供者列表时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 通用设置延迟时间输入框文本改变事件
+ ///
+ private void TxtUniversalUploadDelayMinutes_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
+ {
+ try
+ {
+ if (MainWindow.Settings?.Upload != null && int.TryParse(TxtUniversalUploadDelayMinutes.Text, out int delayMinutes))
{
// 限制范围在0-60分钟
if (delayMinutes < 0)
{
delayMinutes = 0;
- TxtUploadDelayMinutes.Text = "0";
+ TxtUniversalUploadDelayMinutes.Text = "0";
}
else if (delayMinutes > 60)
{
delayMinutes = 60;
- TxtUploadDelayMinutes.Text = "60";
+ TxtUniversalUploadDelayMinutes.Text = "60";
}
- MainWindow.Settings.Dlass.AutoUploadDelayMinutes = delayMinutes;
+ MainWindow.Settings.Upload.UploadDelayMinutes = delayMinutes;
MainWindow.SaveSettingsToFile();
}
- else if (string.IsNullOrWhiteSpace(TxtUploadDelayMinutes.Text))
+ else if (string.IsNullOrWhiteSpace(TxtUniversalUploadDelayMinutes.Text))
{
// 空文本时设置为0
- if (MainWindow.Settings?.Dlass != null)
+ if (MainWindow.Settings?.Upload != null)
{
- MainWindow.Settings.Dlass.AutoUploadDelayMinutes = 0;
+ MainWindow.Settings.Upload.UploadDelayMinutes = 0;
MainWindow.SaveSettingsToFile();
}
}
}
catch (Exception ex)
{
- LogHelper.WriteLogToFile($"保存上传延迟时间时出错: {ex.Message}", LogHelper.LogType.Error);
+ LogHelper.WriteLogToFile($"保存通用设置延迟时间时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
///
- /// 上传延迟时间输入框预览文本输入事件(只允许数字)
+ /// 通用设置延迟时间输入框预览文本输入事件(只允许数字)
///
- private void TxtUploadDelayMinutes_PreviewTextInput(object sender, TextCompositionEventArgs e)
+ private void TxtUniversalUploadDelayMinutes_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
- Regex regex = new Regex("[^0-9]+");
- e.Handled = regex.IsMatch(e.Text);
+ e.Handled = _nonDigitRegex.IsMatch(e.Text);
+ }
+
+ ///
+ /// 上传提供者启用/禁用开关切换事件
+ ///
+ private void ToggleProviderEnabled_Toggled(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (sender is iNKORE.UI.WPF.Modern.Controls.ToggleSwitch toggleSwitch && toggleSwitch.DataContext is IUploadProvider provider)
+ {
+ if (MainWindow.Settings?.Upload != null)
+ {
+ if (MainWindow.Settings.Upload.EnabledProviders == null)
+ {
+ MainWindow.Settings.Upload.EnabledProviders = new List();
+ }
+
+ if (toggleSwitch.IsOn)
+ {
+ if (!MainWindow.Settings.Upload.EnabledProviders.Contains(provider.Name))
+ {
+ MainWindow.Settings.Upload.EnabledProviders.Add(provider.Name);
+ }
+ }
+ else
+ {
+ MainWindow.Settings.Upload.EnabledProviders.Remove(provider.Name);
+ }
+
+ // 同步更新Dlass的IsAutoUploadNotes设置(如果是Dlass提供者)
+ if (provider.Name == "Dlass" && MainWindow.Settings.Dlass != null)
+ {
+ MainWindow.Settings.Dlass.IsAutoUploadNotes = toggleSwitch.IsOn;
+ // 同步更新Dlass标签页中的开关状态
+ ToggleSwitchAutoUploadNotes.IsOn = toggleSwitch.IsOn;
+ }
+
+ MainWindow.SaveSettingsToFile();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"保存上传提供者启用状态时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
}
///
@@ -532,6 +666,121 @@ namespace Ink_Canvas.Windows
Close();
}
+ ///
+ /// TabControl选择改变事件
+ ///
+ private void TabControl_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
+ {
+ try
+ {
+ if (sender is System.Windows.Controls.TabControl tabControl && tabControl.SelectedItem is System.Windows.Controls.TabItem selectedTab)
+ {
+ // 检查是否切换到Dlass标签页
+ if (selectedTab.Tag?.ToString() == "DlassTab" && _isFirstTimeDlassTab)
+ {
+ // 检查是否是第一次打开(检查用户是否已设置Token)
+ bool hasToken = !string.IsNullOrEmpty(GetUserToken()?.Trim());
+ bool isFirstTime = !hasToken;
+
+ if (isFirstTime)
+ {
+ // 第一次打开,询问用户是否已注册
+ var result = MessageBox.Show(
+ "您是否已经注册了Dlass账号?\n\n" +
+ "• 如果已注册:将打开Dlass管理标签页\n" +
+ "• 如果未注册:将打开浏览器跳转到注册页面",
+ "Dlass账号注册",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+
+ if (result == MessageBoxResult.No)
+ {
+ // 用户未注册,打开浏览器
+ try
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "https://dlass.tech/dashboard",
+ UseShellExecute = true
+ });
+ LogHelper.WriteLogToFile("已打开浏览器跳转到Dlass注册页面", LogHelper.LogType.Event);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"打开浏览器时出错: {ex.Message}", LogHelper.LogType.Error);
+ MessageBox.Show($"无法打开浏览器。请手动访问: https://dlass.tech/dashboard",
+ "提示", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ }
+ // 如果用户选择"是",继续打开设置窗口
+ }
+
+ // 标记为已打开过Dlass标签页
+ _isFirstTimeDlassTab = false;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"TabControl选择改变事件处理出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 加载WebDav设置
+ ///
+ private void LoadWebDavSettings()
+ {
+ try
+ {
+ if (MainWindow.Settings?.Dlass != null)
+ {
+ TxtWebDavUrl.Text = MainWindow.Settings.Dlass.WebDavUrl;
+ TxtWebDavUsername.Text = MainWindow.Settings.Dlass.WebDavUsername;
+ TxtWebDavPassword.Password = MainWindow.Settings.Dlass.WebDavPassword;
+ TxtWebDavRootDirectory.Text = MainWindow.Settings.Dlass.WebDavRootDirectory;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"加载WebDav设置时出错: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ ///
+ /// 保存WebDav设置按钮点击事件
+ ///
+ private void BtnSaveWebDav_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (MainWindow.Settings?.Dlass != null)
+ {
+ MainWindow.Settings.Dlass.WebDavUrl = TxtWebDavUrl.Text;
+ MainWindow.Settings.Dlass.WebDavUsername = TxtWebDavUsername.Text;
+ MainWindow.Settings.Dlass.WebDavPassword = TxtWebDavPassword.Password;
+ MainWindow.Settings.Dlass.WebDavRootDirectory = TxtWebDavRootDirectory.Text;
+ MainWindow.SaveSettingsToFile();
+
+ MessageBox.Show("WebDav设置已保存", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"保存WebDav设置时出错: {ex.Message}", LogHelper.LogType.Error);
+ MessageBox.Show($"保存WebDav设置时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ ///
+ /// 取消WebDav设置按钮点击事件
+ ///
+ private void BtnCancelWebDav_Click(object sender, RoutedEventArgs e)
+ {
+ // 重新加载设置,恢复原值
+ LoadWebDavSettings();
+ }
+
///
/// 测试API连接
///
diff --git a/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml b/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml
index 21e162d0..fe5fec8c 100644
--- a/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml
+++ b/Ink Canvas/Windows/SettingsViews/SettingsViews/AdvancedPanel.xaml
@@ -657,11 +657,11 @@
-
+
-
-
+
+
diff --git a/Ink Canvas/packages.lock.json b/Ink Canvas/packages.lock.json
index 7d2e7578..a50648ea 100644
--- a/Ink Canvas/packages.lock.json
+++ b/Ink Canvas/packages.lock.json
@@ -137,6 +137,12 @@
"System.Text.Json": "8.0.5"
}
},
+ "WebDav.Client": {
+ "type": "Direct",
+ "requested": "[2.9.0, )",
+ "resolved": "2.9.0",
+ "contentHash": "GLhd1tQAJeuVO1sj3Wm/dkg0GEVWxk+XGl6rdegMSMHenZuOaWQw4PifWDsjNEC1dtV1/C8JJfK0qfdkM+VIgA=="
+ },
"AForge": {
"type": "Transitive",
"resolved": "2.2.5",