Files
community/Ink Canvas/Helpers/DlassApiClient.cs
T
doudou0720 0ad74d9f7f feat(Upload/WebDav):迁移Dlass并添加WebDav管理 (#381)
* 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>
2026-02-24 14:08:57 +08:00

485 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Newtonsoft.Json;
using System;
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
{
/// <summary>
/// Dlass API 客户端,用于与服务端通信
/// </summary>
public class DlassApiClient : IDisposable
{
private const string DEFAULT_BASE_URL = "https://dlass.tech";
private readonly string _appId;
private readonly string _appSecret;
private readonly string _baseUrl;
private HttpClient _httpClient;
private string _accessToken;
private DateTime _tokenExpiresAt;
private string _userToken;
/// <summary>
/// 初始化 Dlass API 客户端
/// </summary>
/// <param name="appId">应用ID</param>
/// <param name="appSecret">应用密钥</param>
/// <param name="baseUrl">API基础URL,如果为空则使用默认URL</param>
/// <param name="userToken">用户Token,如果提供则优先使用用户token而不是App Secret</param>
public DlassApiClient(string appId, string appSecret, string baseUrl = null, string userToken = null)
{
_appId = appId ?? throw new ArgumentNullException(nameof(appId));
_appSecret = appSecret ?? throw new ArgumentNullException(nameof(appSecret));
_userToken = userToken;
_baseUrl = baseUrl ?? DEFAULT_BASE_URL;
_baseUrl = _baseUrl.TrimEnd('/');
if (!_baseUrl.StartsWith("http://") && !_baseUrl.StartsWith("https://"))
{
_baseUrl = "https://" + _baseUrl;
}
_httpClient = new HttpClient
{
BaseAddress = new Uri(_baseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
_httpClient.DefaultRequestHeaders.Add("User-Agent", "InkCanvas/1.0");
}
/// <summary>
/// 获取访问令牌(Access Token
/// </summary>
/// <param name="cancellationToken">取消令牌</param>
public async Task<string> GetAccessTokenAsync(CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(_userToken))
{
return _userToken;
}
if (!string.IsNullOrEmpty(_accessToken) && DateTime.Now < _tokenExpiresAt.AddMinutes(-5))
{
return _accessToken;
}
try
{
cancellationToken.ThrowIfCancellationRequested();
var requestData = new
{
app_id = _appId,
app_secret = _appSecret,
grant_type = "client_credentials"
};
var json = JsonConvert.SerializeObject(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/oauth/token", content, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
_accessToken = tokenResponse.AccessToken;
_tokenExpiresAt = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn ?? 3600);
return _accessToken;
}
else
{
throw new Exception($"获取Access Token失败: {response.StatusCode}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"获取Access Token时网络错误: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"获取Access Token时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送GET请求
/// </summary>
/// <param name="endpoint">API端点</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> GetAsync<T>(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送POST请求
/// </summary>
/// <param name="endpoint">API端点</param>
/// <param name="data">请求数据</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> PostAsync<T>(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送PUT请求
/// </summary>
/// <param name="endpoint">API端点</param>
/// <param name="data">请求数据</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> PutAsync<T>(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送DELETE请求
/// </summary>
/// <param name="endpoint">API端点</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<bool> DeleteAsync(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
return true;
}
else
{
return false;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException)
{
return false;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 上传笔记文件
/// </summary>
/// <param name="endpoint">上传端点</param>
/// <param name="filePath">文件路径</param>
/// <param name="boardId">白板ID</param>
/// <param name="secretKey">白板密钥</param>
/// <param name="title">笔记标题(可选)</param>
/// <param name="description">笔记描述(可选)</param>
/// <param name="tags">笔记标签(可选)</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> UploadNoteAsync<T>(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}");
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
// 设置白板认证头
request.Headers.Add("X-Board-ID", boardId);
request.Headers.Add("X-Secret-Key", secretKey);
// 创建multipart/form-data内容
var content = new MultipartFormDataContent();
// 添加文件
var fileContent = new ByteArrayContent(File.ReadAllBytes(filePath));
var fileName = Path.GetFileName(filePath);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(fileContent, "file", fileName);
// 添加可选参数
if (!string.IsNullOrEmpty(title))
{
content.Add(new StringContent(title), "title");
}
if (!string.IsNullOrEmpty(description))
{
content.Add(new StringContent(description), "description");
}
if (!string.IsNullOrEmpty(tags))
{
content.Add(new StringContent(tags), "tags");
}
request.Content = content;
var response = await _httpClient.SendAsync(request, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(responseContent))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(responseContent);
}
else
{
throw new Exception($"上传文件失败: {response.StatusCode} - {responseContent}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"上传文件时网络错误: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"上传文件时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_httpClient?.Dispose();
}
#region
/// <summary>
/// Token响应模型
/// </summary>
private class TokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int? ExpiresIn { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
}
#endregion
}
}