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" /> - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextWrapping="Wrap" + AcceptsReturn="False" + MinHeight="36" + Tag="输入WebDav地址"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + - - - + + + 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 @@ - + - -